feat: add recovery codes to user repository (#11702)

# Which Problems Are Solved

Adds a repository implementation to add/remove recovery codes to the
users relational table.

# How the Problems Are Solved

This is achieved by:
* adding the following columns to the users table: `recovery_codes`,
`recovery_code_last_successful_check`, `recovery_code_failed_attempts`
* adding the `RecoveryCodes` field to the `HumanUser` domain with fields
to set recovery `codes`, `lastSuccessfullyCheckedAt` timestamp, and
`failedAttempts`
* setting `recoveryCodes` in the `Get` user query statement to return a
json object with details related to the recovery codes
* adding the repository-layer implementation to add/remove recovery
codes and set fields related to recovery code checks.
* adding projection reducers to handle the following events:
`HumanRecoveryCodesAddedEvent`, `HumanRecoveryCodesRemovedEvent`,
`HumanRecoveryCodeCheckSucceededEvent`, and
`HumanRecoveryCodeCheckFailedEvent`
* adding unit tests

# Additional Changes
* fix the error message when the recovery is empty in
`internal/command/user_human_recovery_codes.go`
* add a new error message for empty recovery code during checks in
`en.yaml`

# Additional Context
- Closes https://github.com/zitadel/zitadel/issues/11212
- Follow-up: integration tests for the reducers will be added in a
different PR after this
[PR](https://github.com/zitadel/zitadel/pull/11478) is merged
This commit is contained in:
Gayathri Vijayan
2026-02-26 19:22:37 +01:00
committed by GitHub
parent 850adc0da4
commit fb35fdd3c7
12 changed files with 502 additions and 16 deletions
@@ -148,7 +148,7 @@ func checkRecoveryCode(
secretHasher *crypto.Hasher,
) ([]eventstore.Command, error) {
if code == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.UserIDMissing")
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.MFA.RecoveryCodes.Empty")
}
if userID == "" {
@@ -660,7 +660,7 @@ func TestCommands_HumanCheckRecoveryCode(t *testing.T) {
code: "",
resourceOwner: "org1",
},
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.UserIDMissing"),
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.MFA.RecoveryCodes.Empty"),
},
{
name: "missing userID, error",
@@ -933,7 +933,7 @@ func TestCommands_checkRecoveryCode(t *testing.T) {
secretHasher: hasher,
},
wantCommands: 0,
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.UserIDMissing"),
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-u0b6c", "Errors.User.MFA.RecoveryCodes.Empty"),
},
{
name: "missing userID, error",