Files
zitadel/backend/v3/domain/user.go
T
a2bf528ca0 feat(domain): introduce error slugs (#12030)
# Which Problems Are Solved

As part of https://github.com/zitadel/zitadel/issues/11917 we want to
introduce error slugs so (API) clients can rely on stable,
machine-readable errors and act accordingly.

# How the Problems Are Solved

- Added a `NewSlug` helper function in the domain package.
- Added `ErrorDetails` to the `ZitadelError`
- Added an `zitadel.error.v2.ErrorDetail` proto message
- Updated the connectRPC error interceptor to map new slug based errors
to the new `ErrorDetail`
- Defined some common slugs and error functions like internal errors
- Defined (session) specific slugs used in the `DeleteSession` and
`CheckUser` functions and replaced old implementations
- Updated integration tests to check specific errors if the relation
database feature is enabled
- Updated doc and guideline to reflect the latest changes and decisions
- Updated DeleteSession endpoint API to list possible slugs

# Additional Changes

None

# Additional Context

- closes #11957

---------

Co-authored-by: Wim Van Laer <[email protected]>
2026-04-13 09:47:04 +02:00

279 lines
11 KiB
Go

package domain
import (
"time"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/zerrors"
)
type User struct {
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
OrganizationID string `json:"organizationId,omitempty" db:"organization_id"`
ID string `json:"id,omitempty" db:"id"`
Username string `json:"username,omitempty" db:"username"`
LoginNames []LoginName `json:"loginNames,omitempty" db:"login_names"`
State UserState `json:"state,omitempty" db:"state"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
Machine *MachineUser `json:"machine,omitempty" db:"-"`
Human *HumanUser `json:"human,omitempty" db:"-"`
Metadata []*Metadata `json:"metadata,omitempty" db:"metadata"`
}
type LoginName struct {
LoginName string `json:"loginName,omitempty" db:"-"`
IsPreferred bool `json:"isPreferred,omitempty" db:"-"`
}
type MachineUser struct {
Name string `json:"name,omitempty" db:"-"`
Description string `json:"description,omitempty" db:"-"`
Secret string `json:"secret,omitempty" db:"-"`
AccessTokenType AccessTokenType `json:"accessTokenType,omitempty" db:"-"`
PATs []*PersonalAccessToken `json:"pats,omitempty" db:"-"`
Keys []*MachineKey `json:"keys,omitempty" db:"-"`
}
type HumanUser struct {
FirstName string `json:"firstName,omitempty" db:"-"`
LastName string `json:"lastName,omitempty" db:"-"`
Nickname string `json:"nickname,omitempty" db:"-"`
DisplayName string `json:"displayName,omitempty" db:"-"`
PreferredLanguage language.Tag `json:"preferredLanguage,omitzero" db:"-"`
Gender HumanGender `json:"gender,omitempty" db:"-"`
AvatarKey string `json:"avatarKey,omitempty" db:"-"`
MultifactorInitializationSkippedAt time.Time `json:"multifactorInitializationSkippedAt,omitzero" db:"-"`
Email HumanEmail `json:"email,omitzero" db:"-"`
Phone *HumanPhone `json:"phone,omitempty" db:"-"`
Passkeys []*Passkey `json:"passkeys,omitempty" db:"-"`
Password HumanPassword `json:"password,omitzero" db:"-"`
TOTP *HumanTOTP `json:"totp,omitempty" db:"-"`
RecoveryCodes *HumanRecoveryCodes `json:"recoveryCodes,omitempty" db:"-"`
IdentityProviderLinks []*IdentityProviderLink `json:"identityProviderLinks,omitempty" db:"-"`
Invite *HumanInvite `json:"invite,omitempty" db:"-"`
// Verifications are used if the Login does something on behalf of the user that requires verification
// e.g. starting passkey registration
Verifications []*Verification `json:"verifications,omitempty" db:"-"`
}
type UserMetadata struct {
Metadata
UserID string `json:"userId,omitempty" db:"-"`
}
//go:generate enumer -type UserState -transform lower -trimprefix UserState -sql
type UserState uint8
const (
UserStateUnspecified UserState = iota
UserStateActive
UserStateInactive
UserStateLocked
UserStateInitial
)
//go:generate enumer -type HumanGender -transform lower -trimprefix HumanGender
type HumanGender uint8
const (
HumanGenderUnspecified HumanGender = iota
HumanGenderFemale
HumanGenderMale
HumanGenderDiverse
)
type HumanPassword struct {
// Hash is the hashed password
Hash string `json:"hash" db:"-"`
// IsChangeRequired indicates if the user must change their password
IsChangeRequired bool `json:"isChangeRequired,omitempty" db:"-"`
// ChangedAt is the time when the current password was last updated
ChangedAt time.Time `json:"changedAt,omitzero" db:"-"`
// PendingVerification is the verification data for setting a new password
// If nil, no password change is in progress
PendingVerification *Verification `json:"pendingVerification,omitempty" db:"-"`
// LastSuccessfullyCheckedAt is the time when the password was last successfully checked
LastSuccessfullyCheckedAt *time.Time `json:"lastSuccessfullyCheckedAt,omitzero" db:"-"`
// FailedAttempts is the number of consecutive failed password attempts
// It is reset to 0 on successful verification
FailedAttempts uint8 `json:"failedAttempts,omitempty" db:"-"`
}
type HumanEmail struct {
Address string `json:"address" db:"-"`
UnverifiedAddress string `json:"unverifiedAddress,omitempty" db:"-"`
VerifiedAt time.Time `json:"verifiedAt,omitzero" db:"-"`
OTP OTP `json:"otp" db:"-"`
// PendingVerification is the verification data for UnverifiedAddress
// If nil, no email change is in progress
PendingVerification *Verification `json:"pendingVerification,omitempty" db:"-"`
}
type HumanPhone struct {
Number string `json:"number" db:"-"`
UnverifiedNumber string `json:"unverifiedNumber,omitempty" db:"-"`
VerifiedAt time.Time `json:"verifiedAt,omitzero" db:"-"`
OTP OTP `json:"otp,omitzero" db:"-"`
// PendingVerification is the verification data for setting a new phone number
// If nil, no phone change is in progress
PendingVerification *Verification `json:"pendingVerification,omitempty" db:"-"`
}
type HumanTOTP struct {
VerifiedAt time.Time `json:"verifiedAt,omitzero" db:"-"`
Secret *crypto.CryptoValue `json:"secret,omitempty" db:"-"`
// LastSuccessfullyCheckedAt is the time when the TOTP was last successfully checked
LastSuccessfullyCheckedAt *time.Time `json:"lastSuccessfullyCheckedAt,omitzero" db:"-"`
// FailedAttempts is the number of consecutive failed password attempts
// It is reset to 0 on successful verification
FailedAttempts uint8 `json:"failedAttempts,omitempty" db:"-"`
}
type OTP struct {
EnabledAt time.Time `json:"enabledAt,omitzero" db:"-"`
// LastSuccessfullyCheckedAt is the time when the OTP was last successfully checked
LastSuccessfullyCheckedAt *time.Time `json:"lastSuccessfullyCheckedAt,omitzero" db:"-"`
// FailedAttempts is the number of consecutive failed password attempts
// It is reset to 0 on successful verification
FailedAttempts uint8 `json:"failedAttempts,omitempty" db:"-"`
}
type PersonalAccessToken struct {
ID string `json:"id" db:"-"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"-"`
ExpiresAt time.Time `json:"expiresAt,omitzero" db:"-"`
Scopes []string `json:"scopes" db:"-"`
}
//go:generate enumer -type AccessTokenType -transform lower -trimprefix AccessTokenType
type AccessTokenType uint8
const (
AccessTokenTypeUnspecified AccessTokenType = iota
AccessTokenTypeBearer
AccessTokenTypeJWT
)
type MachineKey struct {
ID string `json:"id" db:"-"`
PublicKey []byte `json:"publicKey" db:"-"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"-"`
ExpiresAt time.Time `json:"expiresAt,omitzero" db:"-"`
Type MachineKeyType `json:"type" db:"-"`
}
//go:generate enumer -type MachineKeyType -transform lower -trimprefix MachineKeyType
type MachineKeyType uint8
const (
MachineKeyTypeNone MachineKeyType = iota
MachineKeyTypeJSON
)
type Passkey struct {
ID string `json:"id" db:"-"`
KeyID []byte `json:"keyId" db:"-"`
Name string `json:"name" db:"-"`
SignCount uint32 `json:"signCount" db:"-"`
PublicKey []byte `json:"publicKey" db:"-"`
AttestationType string `json:"attestationType" db:"-"`
AuthenticatorAttestationGUID []byte `json:"aaGuid" db:"-"`
Type PasskeyType `json:"type" db:"-"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"-"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"-"`
VerifiedAt time.Time `json:"verifiedAt,omitzero" db:"-"`
// Challenge is used during device registration
Challenge []byte `json:"challenge" db:"-"`
// RelyingPartyID is used during device registration
RelyingPartyID string `json:"rpId" db:"-"`
// Initialization is used during user registration
Initialization *Verification `json:"-" db:"-"`
}
//go:generate enumer -type PasskeyType -transform lower -trimprefix PasskeyType -json -sql
type PasskeyType uint8
const (
PasskeyTypeUnspecified PasskeyType = iota
PasskeyTypePasswordless
PasskeyTypeU2F
)
type IdentityProviderLink struct {
ProviderID string `json:"providerId" db:"-"`
ProvidedUserID string `json:"providedUserId" db:"-"`
ProvidedUsername string `json:"providedUsername" db:"-"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"-"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"-"`
}
//go:generate enumer -type UserType -transform lower -trimprefix UserType
type UserType uint8
const (
UserTypeUnspecified UserType = iota
UserTypeHuman
UserTypeMachine
)
type HumanInvite struct {
AcceptedAt time.Time `json:"acceptedAt,omitzero" db:"-"`
// PendingVerification is the verification data for UnverifiedAddress
// If nil, no email change is in progress
PendingVerification *Verification `json:"pendingVerification,omitempty" db:"-"`
// FailedAttempts is the number of consecutive failed invite attempts
FailedAttempts uint8 `json:"failedAttempts,omitempty" db:"-"`
}
type HumanRecoveryCodes struct {
// Codes are the hashed recovery codes for the user
Codes []string `json:"codes" db:"-"`
// LastSuccessfullyCheckedAt is the time when a recovery code was last successfully checked
LastSuccessfullyCheckedAt *time.Time `json:"lastSuccessfullyCheckedAt,omitzero" db:"-"`
// FailedAttempts is the number of consecutive failed recovery code attempts
// It is reset to 0 on successful verification
FailedAttempts uint8 `json:"failedAttempts,omitempty" db:"-"`
}
var (
// SlugUserNotFound is the slug whenever a user is not found by the provided identifier.
SlugUserNotFound = NewSlug("user", "not_found")
// SlugUserNotActive is the slug whenever the user requires to be in state "active", but isn't.
SlugUserNotActive = NewSlug("user", "not_active")
)
var (
// ErrUserNotFound is an error that can be used when a user is not found by the provided identifier.
ErrUserNotFound = func(err error, identifier string) error {
return zerrors.CreateZitadelError(zerrors.KindNotFound, err, string(SlugUserNotFound), "user was not found", 1).
WithDetails(zerrors.ErrorDetailsMap{"identifier": identifier})
}
// ErrUserNotActive is an error that can be returned when a user needs to be active for the operation,
// e.g. sign in, reactivation, etc. but has a different state.
ErrUserNotActive = func(id, message string) error {
return zerrors.CreateZitadelError(zerrors.KindPreconditionFailed, nil, string(SlugUserNotActive), message, 1).
WithDetails(zerrors.ErrorDetailsMap{"id": id})
}
)