Files
zitadel/backend/v3/storage/database/repository/verification.go
T
5df8a97f9d feat(repository): implement user repository (#10967)
## Summary

This PR introduces the relational user repository for backend v3,
including schema, repository APIs, reducers/projections, and tests for
both human and machine users.

## What changed

* Added user-focused relational migrations (users, verifications,
metadata, PATs, machine keys, passkeys, identity provider links) and
related FK wiring.
* Implemented/expanded repository domain contracts and storage
implementations for human and machine user flows.
* Added support for machine secret handling and aligned access token
type modeling.
* Extended user domain models (verifications, password/email/phone/TOTP
state, passkeys, metadata, invite handling, identity provider links).
* Updated projection/reduction coverage and integration tests for key
user lifecycle events.
* Regenerated and updated mocks/enums plus supporting database
change/column utilities.

## Notes for reviewers

* This PR is large because it establishes the foundational relational
user storage layer and related test scaffolding. Sorry for that.

## Closes

Closes #10218
Closes #10224
Closes #10225
Closes #10209
Closes #10802

---------

Co-authored-by: Iraq Jaber <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
2026-02-23 16:03:47 +01:00

172 lines
6.4 KiB
Go

package repository
import (
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type verification struct{}
func (v verification) unqualifiedTableName() string {
return "verifications"
}
func (v verification) qualifiedTableName() string {
return "zitadel." + v.unqualifiedTableName()
}
// init creates a new verification
func (v verification) init(init *domain.VerificationTypeInit, existingTableName string, verificationID database.Column) database.CTEChange {
return database.NewCTEChange(func(builder *database.StatementBuilder) {
var (
createdAt any = database.NowInstruction
expiry any = database.NullInstruction
id any = database.GenRandomUUIDInstruction
)
if !init.CreatedAt.IsZero() {
createdAt = init.CreatedAt
}
if init.Expiry != nil {
expiry = *init.Expiry
}
if init.ID != nil {
id = *init.ID
}
builder.WriteString("INSERT INTO zitadel.verifications(instance_id, user_id, id, code, created_at, updated_at, expiry) SELECT instance_id, id, ")
builder.WriteArgs(id, init.Code, createdAt, createdAt, expiry)
builder.WriteString(" FROM ")
builder.WriteString(existingTableName)
builder.WriteString(" RETURNING id")
}, func(name string) database.Change {
return database.NewChangeToStatement(verificationID, func(builder *database.StatementBuilder) {
builder.WriteString("SELECT id FROM ")
builder.WriteString(name)
})
})
}
// updates the given fields of the verification with the given ID. Only non-nil fields get updated.
func (v verification) update(update *domain.VerificationTypeUpdate, existingTableName string, instanceID, verificationID database.Column) database.CTEChange {
changes := make(database.Changes, 0, 3)
if update.Code != nil {
changes = append(changes, database.NewChange(v.codeColumn(), update.Code))
}
if update.Expiry != nil {
changes = append(changes, database.NewChange(v.expiryColumn(), *update.Expiry))
}
if update.UpdatedAt.IsZero() {
changes = append(changes, database.NewChange(v.updatedAtColumn(), database.NowInstruction))
} else {
changes = append(changes, database.NewChange(v.updatedAtColumn(), update.UpdatedAt))
}
return database.NewCTEChange(func(builder *database.StatementBuilder) {
builder.WriteString("UPDATE ")
builder.WriteString(v.qualifiedTableName())
builder.WriteString(" SET ")
err := changes.Write(builder)
logging.New(logging.StreamRuntime).Debug("write changes in cte failed", "error", err)
builder.WriteString(" FROM ")
builder.WriteString(existingTableName)
writeCondition(builder, database.And(
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
database.NewColumnCondition(v.idColumn(), verificationID),
))
}, nil)
}
// verified constructs the following changes:
// - sets the verifiedAt column to the current time or the provided time
// - clears the pending verification ID
// - deletes the verification
// - resets the failed attempts counter
func (v verification) verified(verified *domain.VerificationTypeSucceeded, existingTableName string, instanceID, pendingVerificationID, verifiedAt, failedAttempts database.Column) database.Changes {
verifiedAtChange := database.NewChange(verifiedAt, database.NowInstruction)
if !verified.VerifiedAt.IsZero() {
verifiedAtChange = database.NewChange(verifiedAt, verified.VerifiedAt)
}
return database.NewChanges(
verifiedAtChange,
database.NewChangeToNull(pendingVerificationID),
database.NewChange(failedAttempts, 0),
database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("DELETE FROM ")
builder.WriteString(v.qualifiedTableName())
builder.WriteString(" USING ")
builder.WriteString(existingTableName)
writeCondition(builder, database.And(
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
database.NewColumnCondition(v.idColumn(), pendingVerificationID),
))
}, nil,
),
)
}
// skipped constructs the following changes:
// - sets the verifiedAt column to the current time or the provided time
// - clears the pending verification ID
// - resets the failed attempts counter
func (v verification) skipped(skipped *domain.VerificationTypeSkipped, verifiedAt, pendingVerificationID, failedAttempts database.Column) database.Changes {
verifiedAtChange := database.NewChange(verifiedAt, database.NowInstruction)
if !skipped.SkippedAt.IsZero() {
verifiedAtChange = database.NewChange(verifiedAt, skipped.SkippedAt)
}
return database.NewChanges(
verifiedAtChange,
database.NewChange(failedAttempts, 0),
database.NewChangeToNull(pendingVerificationID),
)
}
// failed increments the failed attempts counter.
func (v verification) failed(existingTableName string, instanceID, verificationID database.Column) database.CTEChange {
return database.NewCTEChange(func(builder *database.StatementBuilder) {
builder.WriteString("UPDATE ")
builder.WriteString(v.qualifiedTableName())
builder.WriteString(" SET ")
err := database.NewIncrementColumnChange(v.failedAttemptsColumn(), database.Coalesce(v.failedAttemptsColumn(), 0)).Write(builder)
logging.New(logging.StreamRuntime).Debug("write cte failed", "error", err)
builder.WriteString(" FROM ")
builder.WriteString(existingTableName)
writeCondition(builder, database.And(
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
database.NewColumnCondition(v.idColumn(), verificationID),
))
}, nil)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
func (v verification) instanceIDColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "instance_id")
}
func (v verification) idColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "id")
}
func (v verification) codeColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "code")
}
func (v verification) expiryColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "expiry")
}
func (v verification) failedAttemptsColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "failed_attempts")
}
func (v verification) userIDColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "user_id")
}
func (v verification) updatedAtColumn() database.Column {
return database.NewColumn(v.unqualifiedTableName(), "updated_at")
}