From afeeee87c364c029b320395b3a7bf76f24547843 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 20 Feb 2026 13:34:07 +0100 Subject: [PATCH] fix: allow creating new invite code before previous is invalid (#11649) # Which Problems Are Solved In the refactoring of the logic for the creating and resending of invite codes (#9962), a bug was introduced where the creating of a new code was not possible if the request was to return it. It worked when being sent via mail. # How the Problems Are Solved Fixed the check for an existing code. # Additional Changes none # Additional Context - closes https://github.com/zitadel/zitadel/issues/10718 - requires backport to v4.x - relates to #9962 Co-authored-by: Marco A. --- internal/command/user_v2_invite.go | 2 +- internal/command/user_v2_invite_test.go | 80 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 10948164e9..c1fff07de5 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -62,7 +62,7 @@ func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, if !wm.CreationAllowed() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised") } - if requireExisting && wm.InviteCode == nil || wm.CodeReturned { + if requireExisting && (wm.InviteCode == nil || wm.CodeReturned) { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound") } code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 49a2e78249..8e58515701 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -662,6 +662,86 @@ func TestCommands_CreateInviteCode(t *testing.T) { returnCode: gu.Ptr("code2"), }, }, + { + "return ok after previous code created", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + // first invite code generated and returned + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code1"), + }, + time.Minute, // expired code + "", + true, + "", + "", + ), + ), + // simulate a failed verification attempt due to expiry + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code2"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code2"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {