user

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 56 Imported by: 0

Documentation

Index

Constants

View Source
const (
	SortOrderAsc  = pagination.SortOrderAsc
	SortOrderDesc = pagination.SortOrderDesc
)
View Source
const MaxProfilePictureBytes = 2 << 20

MaxProfilePictureBytes caps an uploaded avatar at 2 MiB. Mirrored by chk_profile_pictures_size, so the limit holds for writers that never touch this service.

Variables

This section is empty.

Functions

func AccountRoute

func AccountRoute(
	r chi.Router,
	accountHandler *AccountHandler,
	consentHandler *UserConsentHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	sensitiveActionStepUp func(http.Handler) http.Handler,
	rateLimitMiddleware ...middleware.Middleware,
)

sensitiveActionStepUp is a policy-aware step-up middleware (provided by the mfa package). It gates sensitive identity changes (email change) on a fresh step-up only when the tenant policy require_mfa_for_sensitive_actions is enabled AND the user has an enrolled MFA factor; otherwise it is a pass- through. When nil, the strict middleware.RequireStepUp is used as a safe fallback (preserving prior behavior).

func AccountSelfReadRoute

func AccountSelfReadRoute(
	r chi.Router,
	accountHandler *AccountHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

AccountSelfReadRoute mounts the read-only slice of AccountRoute for the internal console surface.

GET /account is what renders "signed in as", so it has to stay. The other thirteen endpoints on AccountRoute — email/phone/username/password changes, deletion, export, session management, consent — are account MANAGEMENT, and the identity app is the single place that does that now. Mounting them here as well would leave a second, unexercised copy of every one of those guards.

func BirthdateString

func BirthdateString(t *time.Time) *string

BirthdateString renders a stored birthdate in the "YYYY-MM-DD" shape the request DTO accepts, so a client can round-trip a GET straight into a PUT. Exported because internal/setup serialises the same shared DTO.

func DataErasureAdminRoute

func DataErasureAdminRoute(
	r chi.Router,
	handler *DataErasureHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

DataErasureAdminRoute mounts the admin GDPR erasure endpoint on the internal port (8080). It coexists with the /users subrouter registered by UserRoute.

func DataErasureSelfRoute

func DataErasureSelfRoute(
	r chi.Router,
	handler *DataErasureHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

DataErasureSelfRoute mounts the self-service GDPR erasure endpoint under /me. It coexists with the /me subrouter registered by UserTrustedDeviceRoute.

func ProfilePictureURL

func ProfilePictureURL(profileUUID uuid.UUID) string

ProfilePictureURL is the path an uploaded avatar is served from. Kept in one place so the stored profile_url, the router and the API response cannot disagree.

func ProfileRoute

func ProfileRoute(
	r chi.Router,
	profileHandler *ProfileHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

func ProfileSelfReadRoute

func ProfileSelfReadRoute(
	r chi.Router,
	profileHandler *ProfileHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

ProfileSelfReadRoute mounts the read-only slice of ProfileRoute for the internal console surface.

Two endpoints, both of which the console needs to DISPLAY people:

  • GET /profile — the signed-in admin's own profile (name and avatar in the top nav).
  • GET /profiles/{uuid}/picture — where an uploaded avatar is actually served from. Every profile_url the console renders points here, including on the admin user-management screens, so dropping it would break avatars for other users too. The handler's own owner-or-user:read check is what keeps that from becoming a way to enumerate profiles.

Profile EDITING is absent: self-editing belongs to the identity app, and an admin editing someone else goes through /users/{uuid}/profiles on UserRoute.

func RecoveryRoute

func RecoveryRoute(
	r chi.Router,
	accountHandler *AccountHandler,
)

RecoveryRoute mounts unauthenticated account recovery endpoints.

func StartDataErasureWorker

func StartDataErasureWorker(ctx context.Context, svc DataErasureService, interval time.Duration)

StartDataErasureWorker runs ProcessPendingErasureRequests on an interval until ctx is cancelled. It is a background worker distinct from the ephemeral-row cleanup runner.

func UserRoute

func UserRoute(
	r chi.Router,
	userHandler *UserHandler,
	profileHandler *ProfileHandler,
	deviceHandler *UserTrustedDeviceHandler,
	consentHandler *UserConsentHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

func UserSettingRoute

func UserSettingRoute(
	r chi.Router,
	userSettingHandler *UserSettingHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

func UserTrustedDeviceRoute

func UserTrustedDeviceRoute(
	r chi.Router,
	deviceHandler *UserTrustedDeviceHandler,
	userService middleware.UserContextProvider,
	appCache *cache.Cache,
	rateLimitMiddleware ...middleware.Middleware,
)

func ValidateProfileURL

func ValidateProfileURL(raw string) (string, error)

ValidateProfileURL bounds an externally hosted avatar URL.

https only and no embedded credentials: the value is rendered as an <img> src for anyone who views the profile, so http is mixed content and a user:pass@ link leaks a credential into markup and referrer headers. Rejecting non-http(s) schemes also keeps javascript: and data: out of that attribute.

func ValidateTenantAccess

func ValidateTenantAccess(actor *User, target *Tenant) error

Types

type AccountDeleteDTO

type AccountDeleteDTO struct {
	CurrentPassword string `json:"current_password"`
}

AccountDeleteDTO is the request to permanently delete an account.

func (*AccountDeleteDTO) Validate

func (r *AccountDeleteDTO) Validate() error

type AccountExportDTO

type AccountExportDTO struct {
	UserUUID  string      `json:"user_uuid"`
	Username  string      `json:"username"`
	Email     string      `json:"email"`
	Phone     string      `json:"phone"`
	CreatedAt time.Time   `json:"created_at"`
	Profile   interface{} `json:"profile,omitempty"`
	Roles     []string    `json:"roles"`
	Settings  interface{} `json:"settings,omitempty"`
}

AccountExportDTO is the response payload for account data export.

type AccountHandler

type AccountHandler struct {
	// contains filtered or unexported fields
}

AccountHandler handles self-service account management operations.

func NewAccountHandler

func NewAccountHandler(accountService AccountService, sessionService SessionService, profileRepo ProfileRepository) *AccountHandler

func (*AccountHandler) ChangePassword

func (h *AccountHandler) ChangePassword(w http.ResponseWriter, r *http.Request)

ChangePassword rotates the authenticated user's own password.

PUT /account/password

func (*AccountHandler) ChangeUsername

func (h *AccountHandler) ChangeUsername(w http.ResponseWriter, r *http.Request)

ChangeUsername updates the authenticated user's username.

PUT /account/username

func (*AccountHandler) DeleteAccount

func (h *AccountHandler) DeleteAccount(w http.ResponseWriter, r *http.Request)

DeleteAccount permanently deletes the authenticated user's account.

DELETE /account

func (*AccountHandler) ExportAccountData

func (h *AccountHandler) ExportAccountData(w http.ResponseWriter, r *http.Request)

ExportAccountData returns all personal data for the authenticated user.

GET /account/export

func (*AccountHandler) GetAccount

func (h *AccountHandler) GetAccount(w http.ResponseWriter, r *http.Request)

GetAccount returns consolidated user information: profile, roles, permissions, tenant.

GET /account

func (*AccountHandler) InitiateEmailChange

func (h *AccountHandler) InitiateEmailChange(w http.ResponseWriter, r *http.Request)

InitiateEmailChange starts the email change flow by sending an OTP to the new address.

POST /account/email/change

func (*AccountHandler) ListSessions

func (h *AccountHandler) ListSessions(w http.ResponseWriter, r *http.Request)

ListSessions returns all active sessions for the authenticated user.

GET /account/sessions

func (*AccountHandler) RevokeAllSessions

func (h *AccountHandler) RevokeAllSessions(w http.ResponseWriter, r *http.Request)

RevokeAllSessions revokes every active session for the authenticated user.

DELETE /account/sessions

func (*AccountHandler) RevokeOtherSessions

func (h *AccountHandler) RevokeOtherSessions(w http.ResponseWriter, r *http.Request)

RevokeOtherSessions revokes every session for the authenticated user except the one this request arrived on.

DELETE /account/sessions/others

A separate endpoint rather than a flag on DELETE /account/sessions: the two have genuinely different blast radii, and a flag makes the destructive variant the behaviour you get whenever the flag is dropped, mistyped, or stripped by a proxy. A distinct path cannot be reached by accident, and it leaves the existing sign-out-everywhere contract byte-for-byte unchanged for the credential-change flows that depend on it.

func (*AccountHandler) RevokeSession

func (h *AccountHandler) RevokeSession(w http.ResponseWriter, r *http.Request)

RevokeSession revokes a single session by UUID for the authenticated user.

DELETE /account/sessions/{session_uuid}

func (*AccountHandler) SendPhoneVerification

func (h *AccountHandler) SendPhoneVerification(w http.ResponseWriter, r *http.Request)

SendPhoneVerification sends an SMS OTP to the given phone so the authenticated user can verify ownership of the number.

POST /account/phone/send-verification

func (*AccountHandler) SetAuditLogger

func (h *AccountHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

func (*AccountHandler) VerifyBackupCode

func (h *AccountHandler) VerifyBackupCode(w http.ResponseWriter, r *http.Request)

VerifyBackupCode recovers account access using a backup code (unauthenticated).

POST /recovery/backup-code

func (*AccountHandler) VerifyEmailChange

func (h *AccountHandler) VerifyEmailChange(w http.ResponseWriter, r *http.Request)

VerifyEmailChange confirms the OTP and applies the new email address.

POST /account/email/verify

func (*AccountHandler) VerifyPhone

func (h *AccountHandler) VerifyPhone(w http.ResponseWriter, r *http.Request)

VerifyPhone confirms an SMS OTP and marks the authenticated user's phone verified.

POST /account/phone/verify

type AccountProfileDTO

type AccountProfileDTO struct {
	ProfileID   string  `json:"profile_id"`
	FirstName   string  `json:"first_name"`
	LastName    *string `json:"last_name,omitempty"`
	DisplayName *string `json:"display_name,omitempty"`
	Default     bool    `json:"default"`
}

type AccountResponseDTO

type AccountResponseDTO struct {
	UserID        string              `json:"user_id"`
	Email         string              `json:"email"`
	Phone         string              `json:"phone"`
	EmailVerified bool                `json:"email_verified"`
	PhoneVerified bool                `json:"phone_verified"`
	Profiles      []AccountProfileDTO `json:"profiles"`
	Roles         []string            `json:"roles"`
	Permissions   []string            `json:"permissions"`
	Tenant        AccountTenantDTO    `json:"tenant"`
}

type AccountService

type AccountService interface {
	// SetSessionCreator wires the store used to bind recovery logins to a real,
	// revocable session.
	SetSessionCreator(SessionCreator)
	InitiateEmailChange(ctx context.Context, userID int64, newEmail, currentPassword string) error
	VerifyEmailChange(ctx context.Context, userID int64, otp string) error
	ChangeUsername(ctx context.Context, userID int64, newUsername, currentPassword string) error
	ChangePassword(ctx context.Context, userID int64, currentPassword, newPassword string, callerSessionUUID *uuid.UUID) (*ChangePasswordResponseDTO, error)
	// RevokeOtherSessions is "sign out my other devices" — see the method.
	RevokeOtherSessions(ctx context.Context, userID int64, keepSessionUUID uuid.UUID) error
	DeleteAccount(ctx context.Context, userID int64, currentPassword string) error
	ExportAccountData(ctx context.Context, userID int64) (*AccountExportDTO, error)
	VerifyBackupCode(ctx context.Context, req VerifyBackupCodeDTO) (*LoginResponseDTO, error)
	SendPhoneVerification(ctx context.Context, userID int64, phone string) error
	VerifyPhone(ctx context.Context, userID int64, phone, code string) error
}

AccountService handles self-service account management operations.

func NewAccountService

func NewAccountService(
	db *gorm.DB,
	userRepo UserRepository,
	userTokenRepo UserTokenRepository,
	profileRepo ProfileRepository,
	userSettingRepo UserSettingRepository,
	roleRepo RoleRepository,
	clientRepo ClientRepository,
	mfaBackupCodeRepo UserMFABackupCodeRepository,
	userIdentityRepo UserIdentityRepository,
	identityProviderRepo IdentityProviderRepository,
	authEventService authevent.AuthEventService,
	securitySettingRepo secpolicy.SecuritySettingRepository,
	smsOtpRepo notifier.UserOTPRepository,
	passwordHistoryRepo UserPasswordHistoryRepository,
	anonymizer UserAnonymizer,
	sessionRepo SessionRevoker,
	refreshRevoker ...RefreshTokenRevoker,
) AccountService

type AccountTenantDTO

type AccountTenantDTO struct {
	UUID        string `json:"tenant_id"`
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	Identifier  string `json:"identifier"`
}

type BaseRepository

type BaseRepository[T any] = database.BaseRepository[T]

type BaseRepositoryMethods

type BaseRepositoryMethods[T any] = database.BaseRepositoryMethods[T]

type ChangeEmailRequestDTO

type ChangeEmailRequestDTO struct {
	NewEmail        string `json:"new_email"`
	CurrentPassword string `json:"current_password"`
}

ChangeEmailRequestDTO is the request to initiate an email address change.

func (*ChangeEmailRequestDTO) Validate

func (r *ChangeEmailRequestDTO) Validate() error

type ChangePasswordDTO

type ChangePasswordDTO struct {
	CurrentPassword string `json:"current_password"`
	NewPassword     string `json:"new_password"`
}

ChangePasswordDTO is the request to rotate the authenticated user's own password. Both fields are required; no length or composition rules are declared here on purpose — those belong to the tenant's password policy, and duplicating them at the DTO layer is how the two drift apart.

func (*ChangePasswordDTO) Validate

func (r *ChangePasswordDTO) Validate() error

Validate checks only presence. SanitizeInput is deliberately NOT applied to either password — it strips and rewrites characters, which would silently mutate the secret being set. Length and composition are the tenant policy's job (security.ValidatePasswordPolicyForUser); declaring them here too would give two sources of truth that drift.

type ChangePasswordResponseDTO

type ChangePasswordResponseDTO struct {
	OtherSessionsRevoked bool `json:"other_sessions_revoked"`
	// ReauthenticationRequired is true when the caller's own session could not
	// be identified and everything was revoked as the safe fallback.
	ReauthenticationRequired bool `json:"reauthentication_required"`
}

ChangePasswordResponseDTO reports what happened to the user's other sessions, so the client can tell them rather than leaving them to discover it.

type ChangeUsernameDTO

type ChangeUsernameDTO struct {
	NewUsername     string `json:"new_username"`
	CurrentPassword string `json:"current_password"`
}

ChangeUsernameDTO is the request to change a username.

func (*ChangeUsernameDTO) Validate

func (r *ChangeUsernameDTO) Validate() error

type Client

type Client struct {
	ClientID           int64
	ClientUUID         uuid.UUID
	TenantID           int64
	IdentityProviderID int64
	Name               string
	DisplayName        string
	ClientType         string
	Domain             *string
	Identifier         *string
	Status             string
	IsDefault          bool
	IsSystem           bool
	CreatedAt          time.Time
	UpdatedAt          time.Time
	IdentityProvider   *IdentityProvider `gorm:"foreignKey:IdentityProviderID;references:IdentityProviderID"`
}

func (Client) TableName

func (Client) TableName() string

type ClientRepository

type ClientRepository interface {
	BaseRepositoryMethods[Client]
	WithTx(tx *gorm.DB) ClientRepository
	FindByID(id any, preloads ...string) (*Client, error)
	FindByIDs(ids []int64) ([]Client, error)
	FindByUUIDAndTenantID(clientUUID uuid.UUID, tenantID int64) (*Client, error)
	FindDefaultByTenantID(tenantID int64) (*Client, error)
	FindByClientIDAndIdentityProvider(clientID, identityProviderIdentifier string) (*Client, error)
	// FindByIdentifier resolves a client by its OAuth client_id. The auth
	// middleware needs it because the client is now a property of the REQUEST,
	// not of the identity.
	FindByIdentifier(identifier string) (*Client, error)
}

type ClientResponseDTO

type ClientResponseDTO struct {
	ClientUUID  uuid.UUID `json:"client_id"`
	Name        string    `json:"name"`
	DisplayName string    `json:"display_name"`
	ClientType  string    `json:"client_type"`
	Domain      *string   `json:"domain,omitempty"`
	Status      string    `json:"status"`
	IsDefault   bool      `json:"is_default"`
	IsSystem    bool      `json:"is_system"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

type ClientServiceDataResult

type ClientServiceDataResult struct {
	ClientUUID  uuid.UUID
	Name        string
	DisplayName string
	ClientType  string
	Domain      *string
	Status      string
	IsDefault   bool
	IsSystem    bool
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

func ToClientServiceDataResult

func ToClientServiceDataResult(client *Client) *ClientServiceDataResult

type DataErasureHandler

type DataErasureHandler struct {
	// contains filtered or unexported fields
}

DataErasureHandler handles GDPR Article 17 erasure-request endpoints on the internal port (admin) and public port (self-service).

func NewDataErasureHandler

func NewDataErasureHandler(service DataErasureService, userRepo UserRepository) *DataErasureHandler

NewDataErasureHandler creates a new DataErasureHandler.

func (*DataErasureHandler) RequestAdmin

func (h *DataErasureHandler) RequestAdmin(w http.ResponseWriter, r *http.Request)

RequestAdmin creates an erasure request for a target user on behalf of an admin.

POST /users/{user_uuid}/erasure-requests

func (*DataErasureHandler) RequestSelf

func (h *DataErasureHandler) RequestSelf(w http.ResponseWriter, r *http.Request)

RequestSelf creates an erasure request for the calling user.

POST /me/erasure-request

func (*DataErasureHandler) SetAuditLogger

func (h *DataErasureHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

type DataErasureRequest

type DataErasureRequest struct {
	DataErasureRequestID   int64      `gorm:"column:data_erasure_request_id;primaryKey;autoIncrement"`
	DataErasureRequestUUID uuid.UUID  `gorm:"column:data_erasure_request_uuid;type:uuid;uniqueIndex;not null"`
	TenantID               int64      `gorm:"column:tenant_id;not null"`
	UserID                 int64      `gorm:"column:user_id;not null"`
	RequestedByUserID      *int64     `gorm:"column:requested_by_user_id"`
	RequestedByAdminID     *int64     `gorm:"column:requested_by_admin_id"`
	Status                 string     `gorm:"column:status;type:varchar(30);not null;default:'pending'"`
	Reason                 string     `gorm:"column:reason;type:text;not null;default:''"`
	RejectionReason        *string    `gorm:"column:rejection_reason;type:text"`
	LegalHold              bool       `gorm:"column:legal_hold;not null;default:false"`
	LegalHoldReason        *string    `gorm:"column:legal_hold_reason;type:text"`
	ScheduledAt            time.Time  `gorm:"column:scheduled_at;not null"`
	StartedAt              *time.Time `gorm:"column:started_at"`
	CompletedAt            *time.Time `gorm:"column:completed_at"`
	CreatedAt              time.Time  `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt              time.Time  `gorm:"column:updated_at;not null;autoUpdateTime"`
}

DataErasureRequest tracks the lifecycle of a GDPR Article 17 (right to erasure) request: received → in_progress → completed (or rejected / on_hold). It is the authoritative work list for the background erasure worker.

Backed by migration 080_create_data_erasure_requests_table.go.

func (*DataErasureRequest) BeforeCreate

func (d *DataErasureRequest) BeforeCreate(tx *gorm.DB) error

BeforeCreate assigns a UUID before insert when one has not been set.

func (DataErasureRequest) TableName

func (DataErasureRequest) TableName() string

TableName returns the database table name for DataErasureRequest.

type DataErasureRequestRepository

type DataErasureRequestRepository interface {
	BaseRepositoryMethods[DataErasureRequest]
	WithTx(tx *gorm.DB) DataErasureRequestRepository
	// FindActiveByUserID returns the most recent pending/in_progress request for
	// a user (used to make erasure requests idempotent). Returns nil, nil when
	// none exists.
	FindActiveByUserID(userID int64) (*DataErasureRequest, error)
	// FindDueForProcessing returns pending requests whose scheduled_at has passed
	// and which are not under legal hold, up to limit rows.
	FindDueForProcessing(now time.Time, limit int) ([]DataErasureRequest, error)
	// MarkInProgress flips a request to in_progress and records started_at.
	MarkInProgress(id int64, startedAt time.Time) error
	// MarkCompleted flips a request to completed and records completed_at.
	MarkCompleted(id int64, completedAt time.Time) error
	// MarkPending reverts a request to pending (used when processing fails so the
	// worker retries on the next tick). 'failed' is intentionally not used — it
	// is not an allowed status per chk_data_erasure_requests_status.
	MarkPending(id int64) error
}

DataErasureRequestRepository defines persistence operations for the data_erasure_requests entity.

func NewDataErasureRequestRepository

func NewDataErasureRequestRepository(db *gorm.DB) DataErasureRequestRepository

NewDataErasureRequestRepository creates a new repository backed by db.

type DataErasureRequestResult

type DataErasureRequestResult struct {
	UUID        uuid.UUID
	Status      string
	Reason      string
	ScheduledAt time.Time
	CreatedAt   time.Time
}

DataErasureRequestResult is the service-layer view of an erasure request.

type DataErasureService

type DataErasureService interface {
	// RequestErasure records an erasure request for a user, scheduled 30 days
	// out per GDPR Art.17(3). If a pending/in-progress request already exists for
	// the user it is returned unchanged (idempotent).
	RequestErasure(ctx context.Context, in RequestErasureInput) (*DataErasureRequestResult, error)
	// ProcessPendingErasureRequests anonymizes users whose scheduled_at has
	// passed and that are not under legal hold. Invoked by the background worker.
	ProcessPendingErasureRequests(ctx context.Context) error
}

DataErasureService manages GDPR Article 17 erasure requests: it records the request lifecycle and drives the background anonymization worker.

func NewDataErasureService

func NewDataErasureService(repo DataErasureRequestRepository, anonymizer UserAnonymizer) DataErasureService

NewDataErasureService creates a new DataErasureService.

type ErasureRequestDTO

type ErasureRequestDTO struct {
	Reason string `json:"reason"`
}

ErasureRequestDTO is the POST body for /users/{uuid}/erasure-requests and /me/erasure-request. Every field is optional — when the body is empty a default request is still created.

func (*ErasureRequestDTO) Validate

func (r *ErasureRequestDTO) Validate() error

type ErasureResponseDTO

type ErasureResponseDTO struct {
	UUID        string `json:"uuid"`
	Status      string `json:"status"`
	Reason      string `json:"reason,omitempty"`
	ScheduledAt string `json:"scheduled_at"`
	CreatedAt   string `json:"created_at"`
}

ErasureResponseDTO is the JSON representation of a data_erasure_requests row.

type GetUserIdentitiesFilter

type GetUserIdentitiesFilter struct {
	UserID    int64
	Provider  *string
	Page      int
	Limit     int
	SortBy    string
	SortOrder string
}

type GetUserRolesFilter

type GetUserRolesFilter struct {
	UserID      int64
	Name        *string
	Description *string
	Status      *string
	Page        int
	Limit       int
	SortBy      string
	SortOrder   string
}

type IdentityProvider

type IdentityProvider struct {
	IdentityProviderID   int64
	IdentityProviderUUID uuid.UUID
	TenantID             int64
	Name                 string
	DisplayName          string
	Provider             string
	ProviderType         string
	Identifier           string
	Config               datatypes.JSON
	Status               string
	IsDefault            bool
	IsSystem             bool
	CreatedAt            time.Time
	UpdatedAt            time.Time
	Tenant               *Tenant `gorm:"foreignKey:TenantID;references:TenantID"`
}

func (IdentityProvider) TableName

func (IdentityProvider) TableName() string

type IdentityProviderRepository

type IdentityProviderRepository interface {
	BaseRepositoryMethods[IdentityProvider]
	WithTx(tx *gorm.DB) IdentityProviderRepository
	FindByID(id any, preloads ...string) (*IdentityProvider, error)
	FindByIdentifier(identifier string) (*IdentityProvider, error)
	FindDefaultByTenantID(tenantID int64) (*IdentityProvider, error)
}

type IdentityUnlinker

type IdentityUnlinker interface {
	AdminUnlinkIdentity(ctx context.Context, tenantID int64, actorUserID int64, userUUID uuid.UUID, identityUUID string) error
}

IdentityUnlinker is the consumer-side view of the idp federation service that the admin user handler depends on to unlink a target user's external (federated) identity. It lives here so the user package does not import idp; the wiring layer injects the idp federation service, which satisfies it.

type LoginResponseDTO

type LoginResponseDTO struct {
	AccessToken           string  `json:"access_token"`
	IDToken               string  `json:"id_token"`
	RefreshToken          string  `json:"refresh_token,omitempty"`
	ExpiresIn             int64   `json:"expires_in"`
	TokenType             string  `json:"token_type"`
	IssuedAt              int64   `json:"issued_at"`
	RequirePasswordChange bool    `json:"require_password_change,omitempty"`
	SessionID             *string `json:"session_id,omitempty"`
}

type MembershipCandidateDTO

type MembershipCandidateDTO struct {
	UserUUID uuid.UUID `json:"user_id"`
	Username string    `json:"username"`
	Email    string    `json:"email"`
	Fullname string    `json:"fullname,omitempty"`
}

MembershipCandidateDTO is the minimum a member picker needs. It is deliberately not the full user projection: these are users from ANOTHER tenant (the system tenant) as far as most callers are concerned, so the response carries identity enough to choose a person and nothing more.

type PaginatedResponseDTO

type PaginatedResponseDTO[T any] = pagination.PaginatedResponseDTO[T]

type PaginationRequestDTO

type PaginationRequestDTO = pagination.PaginationRequestDTO

type PaginationResult

type PaginationResult[T any] = database.PaginationResult[T]

type Permission

type Permission struct {
	PermissionID   int64     `gorm:"column:permission_id;primaryKey"`
	PermissionUUID uuid.UUID `gorm:"column:permission_uuid"`
	Name           string    `gorm:"column:name"`
	Description    string    `gorm:"column:description"`
	Status         string    `gorm:"column:status"`
	IsDefault      bool      `gorm:"column:is_default"`
	IsSystem       bool      `gorm:"column:is_system"`
	CreatedAt      time.Time `gorm:"column:created_at"`
	UpdatedAt      time.Time `gorm:"column:updated_at"`
	// DeletedAt is REQUIRED on this projection, not decorative: GORM applies
	// the soft-delete scope only when the scanned struct declares it. Without
	// it, preloading this table returned rows that had been deleted — so
	// revoking a role or permission granted it forever.
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}

func (Permission) TableName

func (Permission) TableName() string

type Profile

type Profile struct {
	ProfileID   int64          `gorm:"column:profile_id;primaryKey"`
	ProfileUUID uuid.UUID      `gorm:"column:profile_uuid;unique;not null"`
	UserID      int64          `gorm:"column:user_id;not null"`
	FirstName   string         `gorm:"column:first_name;not null"`
	MiddleName  *string        `gorm:"column:middle_name"`
	LastName    *string        `gorm:"column:last_name"`
	DisplayName *string        `gorm:"column:display_name"`
	Birthdate   *time.Time     `gorm:"column:birthdate"`
	Gender      *string        `gorm:"column:gender"`
	Email       *string        `gorm:"-"`
	Timezone    *string        `gorm:"-"`
	Language    *string        `gorm:"-"`
	ProfileURL  *string        `gorm:"column:profile_url"`
	IsDefault   bool           `gorm:"column:is_default;not null;default:false"`
	Metadata    datatypes.JSON `gorm:"column:metadata;not null;type:jsonb;default:'{}'"`
	CreatedBy   *int64         `gorm:"column:created_by"`
	UpdatedBy   *int64         `gorm:"column:updated_by"`
	CreatedAt   time.Time      `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt   time.Time      `gorm:"column:updated_at;not null;autoUpdateTime"`
	DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at;index"`

	User *User `gorm:"foreignKey:UserID;references:UserID"`
}

Profile holds biographical/PII data for a user.

func (*Profile) BeforeCreate

func (p *Profile) BeforeCreate(tx *gorm.DB) (err error)

func (Profile) TableName

func (Profile) TableName() string

type ProfileFilterDTO

type ProfileFilterDTO struct {
	FirstName *string `json:"first_name,omitempty"`
	LastName  *string `json:"last_name,omitempty"`
	Email     *string `json:"email,omitempty"`
	PaginationRequestDTO
}

ProfileFilterDTO for filtering and paginating profiles

func (ProfileFilterDTO) Validate

func (f ProfileFilterDTO) Validate() error

type ProfileHandler

type ProfileHandler struct {
	// contains filtered or unexported fields
}

func NewProfileHandler

func NewProfileHandler(profileService ProfileService) *ProfileHandler

func (*ProfileHandler) AdminCreateProfile

func (h *ProfileHandler) AdminCreateProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) AdminDeleteProfile

func (h *ProfileHandler) AdminDeleteProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) AdminGetAllProfiles

func (h *ProfileHandler) AdminGetAllProfiles(w http.ResponseWriter, r *http.Request)

Admin handlers - for managing other users' profiles

func (*ProfileHandler) AdminGetProfile

func (h *ProfileHandler) AdminGetProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) AdminUpdateProfile

func (h *ProfileHandler) AdminUpdateProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) CreateOrUpdate

func (h *ProfileHandler) CreateOrUpdate(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) CreateProfile

func (h *ProfileHandler) CreateProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) Delete

func (h *ProfileHandler) Delete(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) DeleteByUUID

func (h *ProfileHandler) DeleteByUUID(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) DeletePicture

func (h *ProfileHandler) DeletePicture(w http.ResponseWriter, r *http.Request)

DeletePicture removes an uploaded avatar from the caller's own profile.

func (*ProfileHandler) Get

func (*ProfileHandler) GetAll

func (h *ProfileHandler) GetAll(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) GetByUUID

func (h *ProfileHandler) GetByUUID(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) GetPicture

func (h *ProfileHandler) GetPicture(w http.ResponseWriter, r *http.Request)

GetPicture serves a stored avatar.

A conditional request is answered from the ETag alone, without reading the image — an avatar is rendered on every page that shows the user, so the common case has to cost a small row read rather than up to 2 MiB.

The response is also deliberately hostile to being interpreted as anything but an image: nosniff stops a browser second-guessing the Content-Type, and a restrictive CSP neuters any active content that survived the decode check. So a crafted upload cannot become script on this origin even if the format checks are one day bypassed.

func (*ProfileHandler) SetAuditLogger

func (h *ProfileHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

func (*ProfileHandler) SetDefaultProfile

func (h *ProfileHandler) SetDefaultProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) SetUserRepo

func (h *ProfileHandler) SetUserRepo(r UserRepository)

SetUserRepo injects the user repository used to enforce tenant isolation on the admin profile endpoints (called by the wiring layer).

func (*ProfileHandler) UpdateProfile

func (h *ProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request)

func (*ProfileHandler) UploadPicture

func (h *ProfileHandler) UploadPicture(w http.ResponseWriter, r *http.Request)

UploadPicture stores an uploaded avatar on the caller's own profile.

The body is bounded by MaxBytesReader BEFORE anything reads it, so an oversized upload is refused as it arrives. Checking the size only after reading would still require buffering the whole thing first, which is the cheap way to exhaust memory on a service with a million users.

type ProfilePicture

type ProfilePicture struct {
	Data        []byte
	ContentType string
	ETag        string
}

ProfilePicture is a decoded, validated avatar ready to store or serve.

func DecodeProfilePicture

func DecodeProfilePicture(data []byte) (*ProfilePicture, error)

DecodeProfilePicture validates an uploaded avatar and reports its real type.

The type is established by DECODING the bytes, never from the multipart Content-Type or the filename: both are attacker-chosen, and the value is echoed back on the serve path, so trusting either would let an uploader pick how a browser interprets their bytes.

type ProfilePictureRecord

type ProfilePictureRecord struct {
	ProfilePictureID int64     `gorm:"column:profile_picture_id;primaryKey"`
	ProfileID        int64     `gorm:"column:profile_id;not null;uniqueIndex"`
	Data             []byte    `gorm:"column:data;not null"`
	ContentType      string    `gorm:"column:content_type;not null"`
	ETag             string    `gorm:"column:etag;not null"`
	CreatedAt        time.Time `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt        time.Time `gorm:"column:updated_at;not null;autoUpdateTime"`
}

ProfilePictureRecord is an uploaded avatar.

Deliberately its own table rather than columns on Profile: profiles is read on every profile fetch, list and Preload, and an ORM's default `SELECT *` would carry up to 2 MiB of image into all of them. Here the bytes are touched by exactly one endpoint.

func (ProfilePictureRecord) TableName

func (ProfilePictureRecord) TableName() string

type ProfilePictureRepository

type ProfilePictureRepository interface {
	// Upsert replaces the profile's avatar, or stores its first.
	Upsert(record *ProfilePictureRecord) error
	// FindByProfileID returns the full picture, bytes included. Only the serve
	// endpoint should call this.
	FindByProfileID(profileID int64) (*ProfilePictureRecord, error)
	// FindMetaByProfileID returns everything EXCEPT the bytes.
	//
	// This is what a conditional request needs: an ETag comparison that matched
	// would otherwise have read the whole image only to discard it and return
	// 304.
	FindMetaByProfileID(profileID int64) (*ProfilePictureRecord, error)
	// ExistsForProfileIDs reports which of the given profiles have an avatar.
	//
	// Batched because the alternative — asking per profile while rendering a
	// list — is a query per row. Returns a set rather than a slice so callers
	// can look up by id without scanning.
	ExistsForProfileIDs(profileIDs []int64) (map[int64]bool, error)
	Delete(profileID int64) error
	WithTx(tx *gorm.DB) ProfilePictureRepository
}

ProfilePictureRepository is the only place avatar bytes are read or written.

Narrow on purpose: every method here names the columns it needs, so no caller can accidentally pull a 2 MiB image into a query that only wanted to know whether one exists.

func NewProfilePictureRepository

func NewProfilePictureRepository(db *gorm.DB) ProfilePictureRepository

type ProfileRepository

type ProfileRepository interface {
	BaseRepositoryMethods[Profile]
	FindByUUID(uuid any, preloads ...string) (*Profile, error)
	DeleteByUUID(uuid any) error
	WithTx(tx *gorm.DB) ProfileRepository
	FindByUserID(userID int64) (*Profile, error)
	FindDefaultByUserID(userID int64) (*Profile, error)
	FindAllByUserID(filter ProfileRepositoryGetFilter) (*PaginationResult[Profile], error)
	UpdateByUserID(userID int64, updatedProfile *Profile) error
	DeleteByUserID(userID int64) error
	UnsetDefaultProfiles(userID int64) error
}

func NewProfileRepository

func NewProfileRepository(db *gorm.DB) ProfileRepository

type ProfileRepositoryGetFilter

type ProfileRepositoryGetFilter struct {
	UserID    int64
	FirstName *string
	LastName  *string
	Email     *string
	Phone     *string
	IsDefault *bool
	Page      int
	Limit     int
	SortBy    string
	SortOrder string
}

type ProfileRequestDTO

type ProfileRequestDTO struct {
	// Basic Identity Information
	FirstName   string  `json:"first_name"`
	MiddleName  *string `json:"middle_name,omitempty"`
	LastName    *string `json:"last_name,omitempty"`
	DisplayName *string `json:"display_name,omitempty"`

	// Personal Information
	Birthdate *string `json:"birthdate,omitempty"` // YYYY-MM-DD format
	Gender    *string `json:"gender,omitempty"`

	// Contact Information (transient — not stored on profile)
	Email *string `json:"email,omitempty"`

	// Preference
	Timezone *string `json:"timezone,omitempty"`
	Language *string `json:"language,omitempty"`

	// Media & Assets
	ProfileURL *string `json:"profile_url,omitempty"`

	// Extended data (custom fields — use metadata.address for OIDC address claim)
	Metadata map[string]any `json:"metadata,omitempty"`
}

func (ProfileRequestDTO) Validate

func (r ProfileRequestDTO) Validate() error

type ProfileResponseDTO

type ProfileResponseDTO struct {
	ProfileUUID string `json:"profile_id"`

	// Basic Identity Information
	FirstName   string  `json:"first_name"`
	MiddleName  *string `json:"middle_name,omitempty"`
	LastName    *string `json:"last_name,omitempty"`
	DisplayName *string `json:"display_name,omitempty"`

	// Personal Information
	//
	// Birthdate is a *string in the same "YYYY-MM-DD" shape the request DTO
	// declares. It used to be a *time.Time, which serialized as RFC3339
	// ("1990-01-25T00:00:00Z"), so echoing a GET response straight back on a PUT
	// failed validateDateFormat — the read and write halves of the same field
	// disagreed on its format. A birthdate is a calendar date, not an instant:
	// the time-of-day and UTC offset RFC3339 forces on it were never meaningful.
	Birthdate *string `json:"birthdate,omitempty"`
	Gender    *string `json:"gender,omitempty"`

	// Contact Information (transient)
	Email *string `json:"email,omitempty"`

	// Preference
	Timezone *string `json:"timezone,omitempty"`
	Language *string `json:"language,omitempty"`

	// Media & Assets (auth-centric)
	ProfileURL *string `json:"profile_url,omitempty"`

	// Extended data (includes OIDC address claim as metadata.address)
	Metadata map[string]any `json:"metadata"`

	// Profile state
	IsDefault bool `json:"is_default"`

	// System Fields
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

validateDateFormat ensures the date is in "YYYY-MM-DD" format.

func NewProfileResponseDTO

func NewProfileResponseDTO(p *Profile) *ProfileResponseDTO

type ProfileService

type ProfileService interface {
	// Avatar handling. Set/Clear take the caller's user id and refuse a profile
	// belonging to anyone else, so a UUID alone is not authority to change
	// someone's picture. SetProfilePicture returns the URL the profile now
	// points at.
	SetProfilePicture(ctx context.Context, profileUUID uuid.UUID, userID int64, picture *ProfilePicture) (string, error)
	ClearProfilePicture(ctx context.Context, profileUUID uuid.UUID, userID int64) error
	GetProfilePicture(ctx context.Context, profileUUID uuid.UUID) (*ProfilePicture, error)
	GetProfilePictureETag(ctx context.Context, profileUUID uuid.UUID) (string, error)
	// EnsureProfileOwnedBy gates the avatar READ for callers who do not
	// administer users.
	EnsureProfileOwnedBy(ctx context.Context, profileUUID uuid.UUID, userID int64) error
	CreateOrUpdateProfile(
		ctx context.Context,
		userUUID uuid.UUID,
		firstName string,
		middleName, lastName, displayName *string,
		birthdate *time.Time,
		gender *string,
		email *string,
		timezone, language *string,
		profileURL *string,
		metadata map[string]any,
	) (*ProfileServiceDataResult, error)
	CreateOrUpdateSpecificProfile(
		ctx context.Context,
		profileUUID uuid.UUID,
		userUUID uuid.UUID,
		firstName string,
		middleName, lastName, displayName *string,
		birthdate *time.Time,
		gender *string,
		email *string,
		timezone, language *string,
		profileURL *string,
		metadata map[string]any,
	) (*ProfileServiceDataResult, error)
	GetByUUID(ctx context.Context, profileUUID uuid.UUID, userUUID uuid.UUID) (*ProfileServiceDataResult, error)
	GetByUserUUID(ctx context.Context, userUUID uuid.UUID) (*ProfileServiceDataResult, error)
	GetAll(ctx context.Context, userUUID uuid.UUID, firstName, lastName, email *string, page, limit int, sortBy, sortOrder string) (*ProfileServiceListResult, error)
	SetDefault(ctx context.Context, profileUUID uuid.UUID, userUUID uuid.UUID) (*ProfileServiceDataResult, error)
	DeleteByUUID(ctx context.Context, profileUUID uuid.UUID, userUUID uuid.UUID) (*ProfileServiceDataResult, error)
}

func NewProfileService

func NewProfileService(
	db *gorm.DB,
	profileRepo ProfileRepository,
	userRepo UserRepository,
	opts ...ProfileServiceOption,
) ProfileService

type ProfileServiceDataResult

type ProfileServiceDataResult struct {
	ProfileUUID uuid.UUID
	// Basic Identity Information
	FirstName   string
	MiddleName  *string
	LastName    *string
	DisplayName *string
	// Personal Information
	Birthdate *time.Time
	Gender    *string
	// Contact Information (transient)
	Email *string
	// Preference
	Timezone *string
	Language *string
	// Media & Assets (auth-centric)
	ProfileURL *string
	// Extended data
	Metadata map[string]any
	// Profile state
	IsDefault bool
	// System Fields
	CreatedAt time.Time
	UpdatedAt time.Time
}

type ProfileServiceListResult

type ProfileServiceListResult struct {
	Data       []ProfileServiceDataResult
	Total      int64
	Page       int
	Limit      int
	TotalPages int
}

type ProfileServiceOption

type ProfileServiceOption func(*profileService)

ProfileServiceOption configures optional collaborators.

func WithProfilePictureRepository

func WithProfilePictureRepository(repo ProfilePictureRepository) ProfileServiceOption

WithProfilePictureRepository enables avatar upload and serving.

type RecordConsentRequestDTO

type RecordConsentRequestDTO struct {
	ConsentType   string `json:"consent_type"`
	PolicyVersion string `json:"policy_version"`
}

func (*RecordConsentRequestDTO) Validate

func (r *RecordConsentRequestDTO) Validate() error

type RefreshTokenRevoker

type RefreshTokenRevoker interface {
	// WithTx joins the caller's transaction so the password update and the
	// revocation commit or roll back together.
	WithTx(tx *gorm.DB) RefreshTokenRevoker
	RevokeByUserID(userID int64) (int64, error)
}

RefreshTokenRevoker is the slice of the OAuth refresh-token store this service needs. Declared here rather than importing internal/oauth, which already imports this package — internal/app supplies the adapter.

type RequestErasureInput

type RequestErasureInput struct {
	TenantID           int64
	UserID             int64
	RequestedByUserID  *int64
	RequestedByAdminID *int64
	Reason             string
}

RequestErasureInput carries the resolved parameters for creating a request.

type Role

type Role struct {
	RoleID          int64
	RoleUUID        uuid.UUID
	TenantID        int64
	Name            string
	Description     string
	IsDefault       bool
	IsSystem        bool
	Status          string
	CreatedAt       time.Time
	UpdatedAt       time.Time
	Tenant          *Tenant          `gorm:"foreignKey:TenantID;references:TenantID"`
	RolePermissions []RolePermission `gorm:"foreignKey:RoleID;references:RoleID"`
	Permissions     []Permission     `gorm:"many2many:role_permissions;joinForeignKey:RoleID;joinReferences:PermissionID"`
	// DeletedAt is REQUIRED on this projection, not decorative: GORM applies
	// the soft-delete scope only when the scanned struct declares it. Without
	// it, preloading this table returned rows that had been deleted — so
	// revoking a role or permission granted it forever.
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}

func (Role) TableName

func (Role) TableName() string

type RolePermission

type RolePermission struct {
	RolePermissionID int64
	RoleID           int64
	PermissionID     int64
	Permission       Permission `gorm:"foreignKey:PermissionID;references:PermissionID"`
}

func (RolePermission) TableName

func (RolePermission) TableName() string

type RoleRepository

type RoleRepository interface {
	BaseRepositoryMethods[Role]
	WithTx(tx *gorm.DB) RoleRepository
	FindByUUID(uuid any, preloads ...string) (*Role, error)
	FindByUUIDs(uuids []string, preloads ...string) ([]Role, error)
	FindByNameAndTenantID(name string, tenantID int64) (*Role, error)
	FindPaginated(filter RoleRepositoryGetFilter) (*PaginationResult[Role], error)
}

type RoleRepositoryGetFilter

type RoleRepositoryGetFilter struct {
	Name        *string
	Description *string
	IsDefault   *bool
	IsSystem    *bool
	Status      *string
	TenantID    int64
	Page        int
	Limit       int
	SortBy      string
	SortOrder   string
}

type RoleResponseDTO

type RoleResponseDTO struct {
	RoleUUID    uuid.UUID `json:"role_id"`
	Name        string    `json:"name"`
	Description string    `json:"description"`
	IsDefault   bool      `json:"is_default"`
	IsSystem    bool      `json:"is_system"`
	Status      string    `json:"status"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

type RoleServiceDataResult

type RoleServiceDataResult struct {
	RoleUUID    uuid.UUID
	Name        string
	Description string
	IsDefault   bool
	IsSystem    bool
	Status      string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type SendPhoneVerificationDTO

type SendPhoneVerificationDTO struct {
	Phone string `json:"phone"`
}

SendPhoneVerificationDTO is the request to send an SMS OTP to verify a phone number.

func (*SendPhoneVerificationDTO) Validate

func (r *SendPhoneVerificationDTO) Validate() error

type SessionCreator

type SessionCreator interface {
	CreateSession(ctx context.Context, userID, tenantID int64, ipAddress, userAgent string) (sessionUUID uuid.UUID, err error)
}

SessionCreator mints a canonical session row for a login this service completes. Backup-code recovery is an interactive login, so its token must be bound to a session like every other one: a token with no `sid` cannot be ended by logout, "sign out everywhere", or session revocation, and is rejected outright by the session middleware. internal/app supplies the adapter — importing authn here would invert the dependency.

type SessionDataResult

type SessionDataResult struct {
	SessionID         string     `json:"session_id"`
	IPAddress         *string    `json:"ip_address,omitempty"`
	UserAgent         *string    `json:"user_agent,omitempty"`
	LastUsedAt        *time.Time `json:"last_used_at,omitempty"`
	ExpiresAt         *time.Time `json:"expires_at,omitempty"`
	AbsoluteExpiresAt *time.Time `json:"absolute_expires_at,omitempty"`
	CreatedAt         time.Time  `json:"created_at"`
}

type SessionRevoker

type SessionRevoker interface {
	// WithTx lets a revoke join the caller's transaction, so a password change
	// and its session revocation commit or roll back together.
	WithTx(tx *gorm.DB) SessionRevoker
	RevokeAllByUserID(userID int64, reason string) error
	RevokeAllExceptUUID(userID int64, keepSessionUUID uuid.UUID, reason string) error
}

SessionRevoker is the slice of the canonical session store this service needs. Declared here rather than importing authn's repository interface so the user package keeps its existing dependency direction.

type SessionService

type SessionService interface {
	ListSessions(ctx context.Context, userID int64) ([]*SessionDataResult, error)
	RevokeSession(ctx context.Context, userID int64, sessionUUID uuid.UUID) error
	RevokeAllSessions(ctx context.Context, userID int64, reason string) error
	CreateSession(ctx context.Context, userID, tenantID int64, ipAddress, userAgent string) (*UserToken, error)
	EnforceConcurrentLimit(ctx context.Context, userUUID uuid.UUID, userID int64) error
	ValidateAndTouch(ctx context.Context, sessionUUID uuid.UUID, userID int64) error
}

type SuccessResponseDTO

type SuccessResponseDTO = pagination.SuccessResponseDTO

type Tenant

type Tenant struct {
	TenantID    int64
	TenantUUID  uuid.UUID
	Name        string
	DisplayName string
	Description string
	Status      string
	IsSystem    bool
	Metadata    datatypes.JSON
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

func (Tenant) TableName

func (Tenant) TableName() string

type TenantRepository

type TenantRepository interface {
	BaseRepositoryMethods[Tenant]
	WithTx(tx *gorm.DB) TenantRepository
	FindByUUID(uuid any, preloads ...string) (*Tenant, error)
	// FindSystem resolves the system tenant. Membership candidates are drawn
	// from it, and it is resolved server-side so a caller cannot aim the lookup
	// at an arbitrary tenant.
	FindSystem() (*Tenant, error)
}

type TenantResolver

type TenantResolver interface {
	GetByUUID(ctx context.Context, tenantUUID uuid.UUID) (*TenantServiceDataResult, error)
}

type TenantResponseDTO

type TenantResponseDTO struct {
	TenantUUID  uuid.UUID      `json:"tenant_id"`
	Name        string         `json:"name"`
	DisplayName string         `json:"display_name"`
	Description string         `json:"description"`
	Identifier  string         `json:"identifier"`
	Status      string         `json:"status"`
	IsSystem    bool           `json:"is_system"`
	Metadata    datatypes.JSON `json:"metadata"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
}

type TenantServiceDataResult

type TenantServiceDataResult struct {
	TenantID    int64
	TenantUUID  uuid.UUID
	Name        string
	DisplayName string
	Description string
	Identifier  string
	Status      string
	IsSystem    bool
	Metadata    datatypes.JSON
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type User

type User struct {
	UserID   int64     `gorm:"column:user_id;primaryKey"`
	UserUUID uuid.UUID `gorm:"column:user_uuid;unique"`
	// TenantID scopes the user to a tenant. Users are isolated per tenant:
	// email/username are unique per (tenant_id, ...), so the same address can
	// exist as a separate account in different tenants.
	TenantID int64  `gorm:"column:tenant_id"`
	Username string `gorm:"column:username"`
	// Fullname is not persisted on users; it lives in Profile.
	Fullname                   string         `gorm:"-"`
	Email                      string         `gorm:"column:email"`
	Phone                      string         `gorm:"column:phone"`
	Password                   *string        `gorm:"column:password" json:"-"`
	IsEmailVerified            bool           `gorm:"column:is_email_verified;not null;default:false"`
	IsPhoneVerified            bool           `gorm:"column:is_phone_verified;not null;default:false"`
	Status                     string         `gorm:"column:status;not null;default:'active'"`
	Metadata                   datatypes.JSON `gorm:"column:metadata;type:jsonb;not null;default:'{}'"`
	ForcePasswordChange        bool           `gorm:"column:force_password_change;default:false"`
	PasswordChangedAt          *time.Time     `gorm:"column:password_changed_at"`
	TemporaryPasswordExpiresAt *time.Time     `gorm:"column:temporary_password_expires_at"`
	IsTOTPEnabled              bool           `gorm:"column:is_totp_enabled;default:false"`
	IsWebAuthnEnabled          bool           `gorm:"column:is_webauthn_enabled;default:false"`
	FirstMFAEnrolledAt         *time.Time     `gorm:"column:first_mfa_enrolled_at"`
	LastLoginAt                *time.Time     `gorm:"column:last_login_at"`
	LoginCount                 int            `gorm:"column:login_count;not null;default:0"`
	EmailVerifiedAt            *time.Time     `gorm:"column:email_verified_at"`
	PhoneVerifiedAt            *time.Time     `gorm:"column:phone_verified_at"`
	ExternalID                 *string        `gorm:"column:external_id"`
	CreatedBy                  *int64         `gorm:"column:created_by"`
	UpdatedBy                  *int64         `gorm:"column:updated_by"`
	CreatedAt                  time.Time      `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt                  time.Time      `gorm:"column:updated_at;not null;autoUpdateTime"`
	DeletedAt                  gorm.DeletedAt `gorm:"column:deleted_at;index"`

	UserIdentities []UserIdentity `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
	UserRoles      []UserRole     `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
	Roles          []Role         `gorm:"many2many:user_roles;joinForeignKey:UserID;joinReferences:RoleID"`
	UserTokens     []UserToken    `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
	Profile        *Profile       `gorm:"foreignKey:UserID;references:UserID"`
	UserSetting    *UserSetting   `gorm:"foreignKey:UserID;references:UserID"`
}

func (*User) BeforeCreate

func (u *User) BeforeCreate(tx *gorm.DB) (err error)

func (User) TableName

func (User) TableName() string

type UserAnonymizer

type UserAnonymizer interface {
	AnonymizeUser(ctx context.Context, userID int64) error
}

UserAnonymizer is the narrow capability the erasure worker needs from the user service: the canonical multi-table anonymization cascade.

type UserAssignRolesRequestDTO

type UserAssignRolesRequestDTO struct {
	RoleUUIDs []uuid.UUID `json:"role_ids"`
}

func (UserAssignRolesRequestDTO) Validate

func (dto UserAssignRolesRequestDTO) Validate() error

type UserConsent

type UserConsent struct {
	UserConsentID   int64     `gorm:"column:user_consent_id;primaryKey"`
	UserConsentUUID uuid.UUID `gorm:"column:user_consent_uuid;unique;not null"`
	UserID          int64     `gorm:"column:user_id;not null"`
	TenantID        int64     `gorm:"column:tenant_id;not null"`
	ConsentType     string    `gorm:"column:consent_type;not null;size:50"`
	PolicyVersion   string    `gorm:"column:policy_version;not null;size:50"`
	Accepted        bool      `gorm:"column:accepted;not null"`
	IPAddress       *string   `gorm:"column:ip_address"`
	UserAgent       *string   `gorm:"column:user_agent"`
	CreatedAt       time.Time `gorm:"column:created_at;not null;autoCreateTime"`
}

func (*UserConsent) BeforeCreate

func (uc *UserConsent) BeforeCreate(tx *gorm.DB) (err error)

func (UserConsent) TableName

func (UserConsent) TableName() string

type UserConsentHandler

type UserConsentHandler struct {
	// contains filtered or unexported fields
}

func NewUserConsentHandler

func NewUserConsentHandler(consentService UserConsentService, userService UserService, userRepo UserRepository) *UserConsentHandler

func (*UserConsentHandler) GetUserConsents

func (h *UserConsentHandler) GetUserConsents(w http.ResponseWriter, r *http.Request)

GetUserConsents returns all consents for a user (admin).

GET /users/{user_uuid}/consents

func (*UserConsentHandler) RecordConsent

func (h *UserConsentHandler) RecordConsent(w http.ResponseWriter, r *http.Request)

RecordConsent records a user's consent to a policy (self-service).

POST /me/consent

func (*UserConsentHandler) SetAuditLogger

func (h *UserConsentHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

func (*UserConsentHandler) WithdrawUserConsent

func (h *UserConsentHandler) WithdrawUserConsent(w http.ResponseWriter, r *http.Request)

WithdrawUserConsent records a withdrawal of a user's consent (admin). The original grant is preserved; a withdrawal row is appended (GDPR Art. 7(3)).

POST /users/{user_uuid}/consents/withdraw

type UserConsentRepository

type UserConsentRepository interface {
	BaseRepositoryMethods[UserConsent]
	WithTx(tx *gorm.DB) UserConsentRepository
	FindByUserID(userID int64) ([]UserConsent, error)
	FindByUserAndType(userID int64, consentType string) ([]UserConsent, error)
	FindLatestByUserAndType(userID int64, consentType string) (*UserConsent, error)
	CreateConsent(consent *UserConsent) error
}

func NewUserConsentRepository

func NewUserConsentRepository(db *gorm.DB) UserConsentRepository

type UserConsentResponseDTO

type UserConsentResponseDTO struct {
	UUID          string `json:"uuid"`
	ConsentType   string `json:"consent_type"`
	PolicyVersion string `json:"policy_version"`
	Accepted      bool   `json:"accepted"`
	IPAddress     string `json:"ip_address,omitempty"`
	UserAgent     string `json:"user_agent,omitempty"`
	CreatedAt     string `json:"created_at"`
}

type UserConsentService

type UserConsentService interface {
	Record(ctx context.Context, tx *gorm.DB, userID, tenantID int64, consentType, policyVersion, ipAddress, userAgent string) error
	FindByUserID(ctx context.Context, userID int64) ([]UserConsent, error)
	Withdraw(ctx context.Context, userID, tenantID int64, consentType, ipAddress, userAgent string) error
}

func NewUserConsentService

func NewUserConsentService(repo UserConsentRepository) UserConsentService

type UserCreateRequestDTO

type UserCreateRequestDTO struct {
	Username string         `json:"username"`
	Email    *string        `json:"email,omitempty"`
	Phone    *string        `json:"phone,omitempty"`
	Password string         `json:"password"`
	Status   string         `json:"status"`
	Metadata datatypes.JSON `json:"metadata,omitempty"`
}

User input structures

func (UserCreateRequestDTO) Validate

func (dto UserCreateRequestDTO) Validate() error

type UserFilterDTO

type UserFilterDTO struct {
	Search     *string  `json:"search,omitempty"`
	Username   *string  `json:"username,omitempty"`
	Email      *string  `json:"email,omitempty"`
	Phone      *string  `json:"phone,omitempty"`
	Fullname   *string  `json:"fullname,omitempty"`
	Status     []string `json:"status,omitempty"`
	TenantUUID *string  `json:"tenant_id,omitempty"`
	RoleUUID   *string  `json:"role_id,omitempty"`
	ClientUUID *string  `json:"client_id,omitempty"`

	// Pagination and sorting
	PaginationRequestDTO
}

User filter structure

func (UserFilterDTO) Validate

func (f UserFilterDTO) Validate() error

type UserGRPCHandler

type UserGRPCHandler struct {
	authv1.UnimplementedUserServiceServer
	// contains filtered or unexported fields
}

func NewUserGRPCHandler

func NewUserGRPCHandler(tenantResolver TenantResolver, userService UserService) *UserGRPCHandler

func (*UserGRPCHandler) AssignUserRoles

func (*UserGRPCHandler) CompleteUserAccount

func (*UserGRPCHandler) CreateUser

CreateUser is replay-guarded. The unique index on (tenant_id, username) means a retry cannot mint a second user, but it answers that retry with a conflict — indistinguishable from "another caller took this name" — so core cannot tell its own duplicate from a race. The ledger returns the original response.

func (*UserGRPCHandler) DeleteUser

func (*UserGRPCHandler) GetUser

func (*UserGRPCHandler) ListUserIdentities

func (*UserGRPCHandler) ListUserRoles

func (*UserGRPCHandler) ListUsers

func (*UserGRPCHandler) RemoveUserRole

func (*UserGRPCHandler) SetUserStatus

func (*UserGRPCHandler) UpdateUser

func (*UserGRPCHandler) VerifyUserEmail

func (*UserGRPCHandler) VerifyUserPhone

type UserHandler

type UserHandler struct {
	// contains filtered or unexported fields
}

func NewUserHandler

func NewUserHandler(userService UserService, identityUnlinker ...IdentityUnlinker) *UserHandler

NewUserHandler creates a new user handler instance.

identityUnlinker is optional and injected by the wiring layer (the idp federation service). It is variadic so existing single-argument callers and tests that do not exercise identity unlinking keep compiling; the admin unlink endpoint guards against a nil unlinker.

func (*UserHandler) AssignRoles

func (h *UserHandler) AssignRoles(w http.ResponseWriter, r *http.Request)

AssignRoles assigns roles to a user.

POST /users/{user_uuid}/roles

Associates one or more roles with a user, granting them the permissions defined by those roles.

func (*UserHandler) CreateUser

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request)

CreateUser creates a new user for the tenant.

POST /users

Creates a new user account within the authenticated tenant. The creator's context is used for audit tracking.

func (*UserHandler) DeleteUser

func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request)

DeleteUser deletes a user.

DELETE /users/{user_uuid}

Permanently deletes a user account from the tenant. The service layer validates tenant ownership. The deleter's context is used for audit tracking.

func (*UserHandler) ForcePasswordChange

func (h *UserHandler) ForcePasswordChange(w http.ResponseWriter, r *http.Request)

ForcePasswordChange sets the force_password_change flag on a user.

PUT /users/{user_uuid}/force-password-change

Marks a user account so that they must change their password on next login.

func (*UserHandler) GetUser

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request)

GetUser retrieves a specific user by UUID.

GET /users/{user_uuid}

Returns detailed information about a single user. The service layer validates that the user belongs to the tenant.

func (*UserHandler) GetUserIdentities

func (h *UserHandler) GetUserIdentities(w http.ResponseWriter, r *http.Request)

GetUserIdentities retrieves all identities for a user with pagination and filters.

GET /users/{user_uuid}/identities

Returns a paginated list of identity providers linked to the user (e.g., Google, GitHub). Supports filtering by provider type.

func (*UserHandler) GetUserMFA

func (h *UserHandler) GetUserMFA(w http.ResponseWriter, r *http.Request)

GetUserMFA retrieves the MFA configuration for a specific user.

GET /users/{user_uuid}/mfa

func (*UserHandler) GetUserRoles

func (h *UserHandler) GetUserRoles(w http.ResponseWriter, r *http.Request)

GetUserRoles retrieves all roles assigned to a user with pagination and filters.

GET /users/{user_uuid}/roles

Returns a paginated list of roles assigned to the user. Supports filtering by role name, description, and status.

func (*UserHandler) GetUserSessions

func (h *UserHandler) GetUserSessions(w http.ResponseWriter, r *http.Request)

GetUserSessions returns the active sessions for a user.

GET /users/{user_uuid}/sessions

func (*UserHandler) GetUsers

func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request)

GetUsers retrieves all users for the tenant with pagination and filters.

GET /users

Returns a paginated list of users belonging to the authenticated tenant. Supports filtering by username, email, phone, status, and role UUID.

func (*UserHandler) LinkUserIdentity

func (h *UserHandler) LinkUserIdentity(w http.ResponseWriter, r *http.Request)

LinkUserIdentity links an existing external (federated) identity to a user.

POST /users/{user_uuid}/identities

The missing half of the admin identity surface, which could list and unlink but never link. Without it a user who signed up a second time through a new IdP was stuck with two accounts and no operator remedy, because the only linking path is self-service and requires access to the original account.

func (*UserHandler) ListMembershipCandidates

func (h *UserHandler) ListMembershipCandidates(w http.ResponseWriter, r *http.Request)

ListMembershipCandidates lists the SYSTEM-tenant users that may be added as members of a tenant.

GET /users/membership-candidates

tenant.CreateByUserUUID accepts only system-tenant users, and nothing exposed that set — so the console's Add Member picker listed the caller's own tenant users, every one of which was rejected with a 403. This is the missing half.

The system tenant is resolved server-side, never taken from the request, so this cannot be repurposed into a cross-tenant user enumeration endpoint.

func (*UserHandler) RemoveRole

func (h *UserHandler) RemoveRole(w http.ResponseWriter, r *http.Request)

RemoveRole removes a role from a user.

DELETE /users/{user_uuid}/roles/{role_uuid}

Removes the association between a role and a user, revoking the permissions granted by that role.

func (*UserHandler) RevokeAllUserSessions

func (h *UserHandler) RevokeAllUserSessions(w http.ResponseWriter, r *http.Request)

RevokeAllUserSessions revokes every active session for a user (force global sign-out).

DELETE /users/{user_uuid}/sessions

func (*UserHandler) RevokeUserSession

func (h *UserHandler) RevokeUserSession(w http.ResponseWriter, r *http.Request)

RevokeUserSession revokes a single active session for a user.

DELETE /users/{user_uuid}/sessions/{session_uuid}

func (*UserHandler) SetAuditLogger

func (h *UserHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

func (*UserHandler) SetLockoutClearer

func (h *UserHandler) SetLockoutClearer(c UserLockoutClearer)

SetLockoutClearer injects the failed-login lockout clearer (wiring layer).

func (*UserHandler) SetUserPassword

func (h *UserHandler) SetUserPassword(w http.ResponseWriter, r *http.Request)

SetUserPassword sets a user's password administratively.

PUT /users/{user_uuid}/password

The operator remedy for a user who can reach neither their password nor their inbox. force-password-change, the only lever that existed before, does nothing until the user manages to sign in on their own. The service holds this to the tenant's password policy and reuse history and evicts the target's live credentials, so it cannot be used to quietly take an account over and leave the previous holder's session working.

func (*UserHandler) SetUserStatus

func (h *UserHandler) SetUserStatus(w http.ResponseWriter, r *http.Request)

SetUserStatus updates the status of a user.

PATCH /users/{user_uuid}/status

Updates only the status field of a user (e.g., active, inactive, suspended). This is a convenience endpoint for status-only updates.

func (*UserHandler) UnlinkUserIdentity

func (h *UserHandler) UnlinkUserIdentity(w http.ResponseWriter, r *http.Request)

UnlinkUserIdentity unlinks an external (federated) identity from a user.

DELETE /users/{user_uuid}/identities/{identity_uuid}

Internal-surface admin operation: it requires a tenant context, records the authenticated admin as the actor, and delegates to the idp federation service which enforces tenant scoping and rejects unlinking the built-in identity.

func (*UserHandler) UnlockUser

func (h *UserHandler) UnlockUser(w http.ResponseWriter, r *http.Request)

UnlockUser clears a user's failed-login lockout (admin remediation).

POST /users/{user_uuid}/unlock

func (*UserHandler) UpdateUser

func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request)

UpdateUser updates an existing user.

PUT /users/{user_uuid}

Updates user account information. The service layer validates that the user belongs to the tenant. The updater's context is used for audit tracking.

func (*UserHandler) VerifyEmail

func (h *UserHandler) VerifyEmail(w http.ResponseWriter, r *http.Request)

VerifyEmail marks a user's email as verified.

POST /users/{user_uuid}/verify-email

Verifies the user's email address and may mark the account as completed if all required verification steps are done.

func (*UserHandler) VerifyPhone

func (h *UserHandler) VerifyPhone(w http.ResponseWriter, r *http.Request)

VerifyPhone marks a user's phone number as verified.

POST /users/{user_uuid}/verify-phone

Verifies the user's phone number for two-factor authentication or account recovery purposes.

type UserIdentity

type UserIdentity struct {
	UserIdentityID     int64          `gorm:"column:user_identity_id;primaryKey"`
	UserIdentityUUID   uuid.UUID      `gorm:"column:user_identity_uuid;unique"`
	TenantID           int64          `gorm:"column:tenant_id"`
	UserID             int64          `gorm:"column:user_id"`
	IdentityProviderID int64          `gorm:"column:identity_provider_id"`
	Provider           string         `gorm:"column:provider"`
	Sub                string         `gorm:"column:sub"`
	Metadata           datatypes.JSON `gorm:"column:metadata"`
	JITProvisionedAt   *time.Time     `gorm:"column:jit_provisioned_at"`
	ProvisioningSource *string        `gorm:"column:provisioning_source"`
	CreatedAt          time.Time      `gorm:"column:created_at;autoCreateTime"`
	UpdatedAt          time.Time      `gorm:"column:updated_at;autoUpdateTime"`

	// Relationships
	Tenant           *Tenant           `gorm:"foreignKey:TenantID;references:TenantID;constraint:OnDelete:CASCADE"`
	User             *User             `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
	IdentityProvider *IdentityProvider `gorm:"foreignKey:IdentityProviderID;references:IdentityProviderID"`
}

func (*UserIdentity) BeforeCreate

func (ui *UserIdentity) BeforeCreate(tx *gorm.DB) (err error)

func (UserIdentity) TableName

func (UserIdentity) TableName() string

type UserIdentityFilterDTO

type UserIdentityFilterDTO struct {
	Provider *string `json:"provider,omitempty"`

	// Pagination and sorting
	PaginationRequestDTO
}

User identity filter structure

func (UserIdentityFilterDTO) Validate

func (r UserIdentityFilterDTO) Validate() error

type UserIdentityRepository

type UserIdentityRepository interface {
	BaseRepositoryMethods[UserIdentity]
	FindAll(preloads ...string) ([]UserIdentity, error)
	FindByUUID(uuid any, preloads ...string) (*UserIdentity, error)
	FindByUUIDs(uuids []string, preloads ...string) ([]UserIdentity, error)
	FindByID(id any, preloads ...string) (*UserIdentity, error)
	UpdateByUUID(uuid any, updatedData any) (*UserIdentity, error)
	UpdateByID(id any, updatedData any) (*UserIdentity, error)
	DeleteByUUID(uuid any) error
	DeleteByID(id any) error
	Paginate(conditions map[string]any, page int, limit int, preloads ...string) (*PaginationResult[UserIdentity], error)
	WithTx(tx *gorm.DB) UserIdentityRepository
	FindByUserID(userID int64) ([]UserIdentity, error)
	FindUserIdentitiesPaginated(filter GetUserIdentitiesFilter) (*PaginationResult[UserIdentity], error)
	// FindByUserIDAndClientReachable returns the identity this user may present
	// to the given client. Identities belong to an identity provider, never to a
	// client (migration 030), so reachability is resolved through
	// client_identity_providers: the client must have an ENABLED connection to
	// the identity's provider. Disabling that connection immediately stops the
	// client authenticating the user, which a client_id column on the identity
	// could not express.
	FindByUserIDAndClientReachable(userID int64, clientID int64) (*UserIdentity, error)
	// FindByUserIDAndProvider returns the first identity for a user with the given provider slug.
	FindByUserIDAndProvider(userID int64, provider string) (*UserIdentity, error)
	// FindByUserIDAndIdentityProviderID returns the user's identity linked to a
	// specific configured IdP. This disambiguates the case where a user holds two
	// identities sharing the same provider slug (e.g. the built-in system
	// "maintainerd" and an external federated "maintainerd") — they differ only by
	// identity_provider_id / sub.
	FindByUserIDAndIdentityProviderID(userID int64, idpID int64) (*UserIdentity, error)
	// FindByIdentityProviderID lists all identities linked to a configured IDP.
	FindByIdentityProviderID(idpID int64) ([]UserIdentity, error)
	// FindByTenantProviderAndSub resolves an external identity by its
	// (tenant, provider, sub) triple. Returns nil when unlinked.
	FindByTenantProviderAndSub(tenantID int64, provider, sub string) (*UserIdentity, error)
	DeleteByUserID(userID int64) error
}

func NewUserIdentityRepository

func NewUserIdentityRepository(db *gorm.DB) UserIdentityRepository

type UserIdentityResponseDTO

type UserIdentityResponseDTO struct {
	UserIdentityUUID uuid.UUID      `json:"user_identity_id"`
	Provider         string         `json:"provider"`
	Sub              string         `json:"sub"`
	Metadata         datatypes.JSON `json:"metadata"`
	// The provider that issued this identity, replacing the former `client`
	// field. An identity is not scoped to one application — every client
	// connected to this provider can authenticate with it.
	IdentityProviderUUID *uuid.UUID `json:"identity_provider_id,omitempty"`
	IdentityProviderName string     `json:"identity_provider_name,omitempty"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

type UserIdentityServiceDataResult

type UserIdentityServiceDataResult struct {
	UserIdentityUUID uuid.UUID
	Provider         string
	Sub              string
	Metadata         datatypes.JSON
	// The identity provider that issued this identity. Replaces the former
	// Client field: identities belong to a provider, and which applications may
	// use one is a separate relationship (client_identity_providers).
	IdentityProviderUUID *uuid.UUID
	IdentityProviderName string
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

type UserLinkIdentityRequestDTO

type UserLinkIdentityRequestDTO struct {
	IdentityProviderUUID string `json:"identity_provider_id"`
	Sub                  string `json:"sub"`
}

UserLinkIdentityRequestDTO is the admin request to attach an existing external identity to a user. Sub is the subject identifier the upstream provider issues for that person.

func (UserLinkIdentityRequestDTO) Validate

func (dto UserLinkIdentityRequestDTO) Validate() error

type UserLockoutClearer

type UserLockoutClearer interface {
	ClearLockout(ctx context.Context, tenantID int64, identifier string) error
}

UserHandler handles user management operations.

This handler manages tenant-scoped user accounts. In the multi-tenant architecture, users are associated with tenants through the user_identities table. All operations are tenant-isolated - middleware validates tenant access and stores it in the request context. The handler supports CRUD operations, role management, identity management, and account verification workflows. UserLockoutClearer clears a user's failed-login lockout state. Implemented by the authn user-lockout repository (structurally satisfied) and injected by the wiring layer, so the user package doesn't depend on authn.

type UserMFABackupCode

type UserMFABackupCode struct {
	BackupCodeID   int64
	BackupCodeUUID uuid.UUID
	UserID         int64
	CodeHash       string
	Used           bool
	UsedAt         *time.Time
	CreatedAt      time.Time
}

func (UserMFABackupCode) TableName

func (UserMFABackupCode) TableName() string

type UserMFABackupCodeRepository

type UserMFABackupCodeRepository interface {
	BaseRepositoryMethods[UserMFABackupCode]
	WithTx(tx *gorm.DB) UserMFABackupCodeRepository
	CreateBulk(codes []*UserMFABackupCode) error
	// No FindByUserIDAndCodeHash here: backup codes are stored as bcrypt hashes,
	// which are salted, so a lookup keyed on a deterministic hash of the
	// submitted code can never match a row. Verification must load the unused
	// codes and bcrypt-compare each one (see accountService.VerifyBackupCode).
	FindUnusedByUserID(userID int64) ([]UserMFABackupCode, error)
	MarkUsed(id int64) error
	DeleteAllByUserID(userID int64) error
}

type UserMFAResponseDTO

type UserMFAResponseDTO struct {
	IsTOTPEnabled     bool `json:"is_totp_enabled"`
	IsWebAuthnEnabled bool `json:"is_webauthn_enabled"`
	IsSMSEnabled      bool `json:"is_sms_enabled"`
	// IsEmailOTPEnabled was read by the admin User → MFA tab but never sent, so
	// Email OTP always rendered as disabled regardless of the user's real state.
	IsEmailOTPEnabled  bool                    `json:"is_email_otp_enabled"`
	BackupCodesCount   int                     `json:"backup_codes_count"`
	WebAuthnKeys       []UserMFAWebAuthnKeyDTO `json:"webauthn_keys,omitempty"`
	FirstMFAEnrolledAt *string                 `json:"mfa_enabled_at,omitempty"`
}

type UserMFAWebAuthnKeyDTO

type UserMFAWebAuthnKeyDTO struct {
	CredentialUUID string  `json:"credential_uuid"`
	Name           string  `json:"name"`
	Transport      string  `json:"transport,omitempty"`
	LastUsedAt     *string `json:"last_used_at,omitempty"`
	CreatedAt      string  `json:"created_at"`
}

type UserPasswordHistory

type UserPasswordHistory struct {
	HistoryID    int64     `gorm:"column:history_id;primaryKey;autoIncrement"`
	HistoryUUID  uuid.UUID `gorm:"column:history_uuid;not null;unique"`
	UserID       int64     `gorm:"column:user_id;not null"`
	PasswordHash string    `gorm:"column:password_hash;not null"`
	CreatedAt    time.Time `gorm:"column:created_at;not null;autoCreateTime"`
}

UserPasswordHistory records previously used password hashes for a user so that re-use can be blocked according to the tenant's PasswordPolicy.HistoryCount. This table is append-only; the DB-level trigger trg_deny_uph_update enforces that no row can be updated after insertion.

func (*UserPasswordHistory) BeforeCreate

func (h *UserPasswordHistory) BeforeCreate(_ *gorm.DB) error

func (UserPasswordHistory) TableName

func (UserPasswordHistory) TableName() string

type UserPasswordHistoryRepository

type UserPasswordHistoryRepository interface {
	WithTx(tx *gorm.DB) UserPasswordHistoryRepository
	// AddEntry inserts a new hash record for the user.
	AddEntry(userID int64, hash string) error
	// FindRecentHashes returns the most recent `count` hashes for the user,
	// ordered newest first.
	FindRecentHashes(userID int64, count int) ([]string, error)
	// PruneExcess deletes all but the most recent `keepCount` records for the user.
	PruneExcess(userID int64, keepCount int) error
}

UserPasswordHistoryRepository manages previously used password hashes for a user so services can enforce PasswordPolicy.HistoryCount.

func NewUserPasswordHistoryRepository

func NewUserPasswordHistoryRepository(db *gorm.DB) UserPasswordHistoryRepository

type UserProfileGRPCHandler

type UserProfileGRPCHandler struct {
	authv1.UnimplementedUserProfileServiceServer
	// contains filtered or unexported fields
}

func NewUserProfileGRPCHandler

func NewUserProfileGRPCHandler(tenantResolver TenantResolver, profileService ProfileService) *UserProfileGRPCHandler

func (*UserProfileGRPCHandler) CreateUserProfile

CreateUserProfile is replay-guarded, and here the guard is load-bearing rather than cosmetic: the profile UUID is minted fresh per call (uuid.New below) and the only uniqueness on profiles is "one DEFAULT per user", so nothing at the database level stops a retry from creating a second, non-default profile for the same person. The ledger is what makes this RPC safe to retry at all.

func (*UserProfileGRPCHandler) DeleteUserProfile

func (*UserProfileGRPCHandler) GetUserProfile

func (*UserProfileGRPCHandler) ListUserProfiles

func (*UserProfileGRPCHandler) SetDefaultUserProfile

func (*UserProfileGRPCHandler) UpdateUserProfile

type UserRepository

type UserRepository interface {
	BaseRepositoryMethods[User]
	FindByID(id any, preloads ...string) (*User, error)
	FindByUUID(uuid any, preloads ...string) (*User, error)
	FindByUUIDs(uuids []string, preloads ...string) ([]User, error)
	FindAll(preloads ...string) ([]User, error)
	UpdateByID(id any, updatedData any) (*User, error)
	UpdateByUUID(uuid any, updatedData any) (*User, error)
	DeleteByUUID(uuid any) error
	DeleteByID(id any) error
	Paginate(conditions map[string]any, page int, limit int, preloads ...string) (*PaginationResult[User], error)
	WithTx(tx *gorm.DB) UserRepository
	// Tenant-scoped lookups. Users are isolated per tenant (email/username are
	// unique per tenant), so unscoped variants are deliberately not exposed.
	FindByEmailAndTenantID(email string, tenantID int64) (*User, error)
	FindByUsernameAndTenantID(username string, tenantID int64) (*User, error)
	FindByPhoneAndTenantID(phone string, tenantID int64) (*User, error)
	FindSuperAdmin() (*User, error)
	FindRoles(userID int64) ([]Role, error)
	// EffectivePermissionNames returns the permission names a user actually holds
	// in a tenant, through active, non-deleted roles and permissions. It is the
	// authority for "may this actor grant this?".
	EffectivePermissionNames(userID, tenantID int64) ([]string, error)
	FindRolesPaginated(filter GetUserRolesFilter) (*PaginationResult[Role], error)
	FindBySubAndClientID(sub string, clientID string) (*User, error)
	FindPaginated(filter UserRepositoryGetFilter) (*PaginationResult[User], error)
	SetEmailVerified(userUUID uuid.UUID, verified bool) error
	SetStatus(userUUID uuid.UUID, status string) error
	// Feature: Force password change
	SetForcePasswordChange(userUUID uuid.UUID, force bool) error
	// Feature: Email change with OTP re-verification. The OTP + pending address
	// live in user_otps (channel='email_change'); only the final apply remains here.
	UpdateEmail(userUUID uuid.UUID, email string) error
	UpdateUsername(userUUID uuid.UUID, username string) error
}

func NewUserRepository

func NewUserRepository(db *gorm.DB) UserRepository

type UserRepositoryGetFilter

type UserRepositoryGetFilter struct {
	Search    *string
	Username  *string
	Email     *string
	Phone     *string
	Fullname  *string
	Status    []string
	TenantID  *int64
	RoleID    *int64
	ClientID  *int64
	Page      int
	Limit     int
	SortBy    string
	SortOrder string
	Cursor    *int64
}

type UserResponseDTO

type UserResponseDTO struct {
	UserUUID        uuid.UUID          `json:"user_id"`
	Username        string             `json:"username"`
	Fullname        string             `json:"fullname"`
	Email           string             `json:"email"`
	Phone           string             `json:"phone"`
	IsEmailVerified bool               `json:"is_email_verified"`
	IsPhoneVerified bool               `json:"is_phone_verified"`
	PhoneVerifiedAt *time.Time         `json:"phone_verified_at,omitempty"`
	Status          string             `json:"status"`
	Metadata        datatypes.JSON     `json:"metadata"`
	LastLoginAt     *time.Time         `json:"last_login_at,omitempty"`
	LoginCount      int                `json:"login_count,omitempty"`
	EmailVerifiedAt *time.Time         `json:"email_verified_at,omitempty"`
	ExternalID      *string            `json:"external_id,omitempty"`
	Tenant          *TenantResponseDTO `json:"tenant,omitempty"`
	CreatedAt       time.Time          `json:"created_at"`
	UpdatedAt       time.Time          `json:"updated_at"`
}

User output structure

type UserRole

type UserRole struct {
	UserRoleID   int64     `gorm:"column:user_role_id;primaryKey"`
	UserRoleUUID uuid.UUID `gorm:"column:user_role_uuid;unique"`
	UserID       int64     `gorm:"column:user_id"`
	RoleID       int64     `gorm:"column:role_id"`
	CreatedAt    time.Time `gorm:"column:created_at;autoCreateTime"`

	// Relationships
	User *User `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
	Role *Role `gorm:"foreignKey:RoleID;references:RoleID;constraint:OnDelete:CASCADE"`
}

func (*UserRole) BeforeCreate

func (ur *UserRole) BeforeCreate(tx *gorm.DB) (err error)

func (UserRole) TableName

func (UserRole) TableName() string

type UserRoleFilterDTO

type UserRoleFilterDTO struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Status      *string `json:"status,omitempty"`

	// Pagination and sorting
	PaginationRequestDTO
}

Validate validates the user filter DTO. User role filter structure

func (UserRoleFilterDTO) Validate

func (r UserRoleFilterDTO) Validate() error

type UserRoleRepository

type UserRoleRepository interface {
	BaseRepositoryMethods[UserRole]
	FindAll(preloads ...string) ([]UserRole, error)
	FindByUUID(uuid any, preloads ...string) (*UserRole, error)
	FindByUUIDs(uuids []string, preloads ...string) ([]UserRole, error)
	FindByID(id any, preloads ...string) (*UserRole, error)
	UpdateByUUID(uuid any, updatedData any) (*UserRole, error)
	UpdateByID(id any, updatedData any) (*UserRole, error)
	DeleteByUUID(uuid any) error
	DeleteByID(id any) error
	Paginate(conditions map[string]any, page int, limit int, preloads ...string) (*PaginationResult[UserRole], error)
	WithTx(tx *gorm.DB) UserRoleRepository
	FindByUserID(userID int64) ([]UserRole, error)
	FindByUserIDAndRoleID(userID int64, roleID int64) (*UserRole, error)
	FindDefaultRolesByUserID(userID int64) ([]UserRole, error)
	DeleteByUserID(userID int64) error
	DeleteByUserIDAndRoleID(userID int64, roleID int64) error
}

func NewUserRoleRepository

func NewUserRoleRepository(db *gorm.DB) UserRoleRepository

type UserService

type UserService interface {
	Get(ctx context.Context, filter UserServiceGetFilter) (*UserServiceGetResult, error)
	GetByUUID(ctx context.Context, userUUID uuid.UUID, tenantID int64) (*UserServiceDataResult, error)
	Create(ctx context.Context, username string, email *string, phone *string, password string, status string, metadata datatypes.JSON, tenantUUID string, creatorUserUUID uuid.UUID) (*UserServiceDataResult, error)
	Update(ctx context.Context, userUUID uuid.UUID, tenantID int64, username string, email *string, phone *string, status string, metadata datatypes.JSON, updaterUserUUID uuid.UUID) (*UserServiceDataResult, error)
	SetStatus(ctx context.Context, userUUID uuid.UUID, tenantID int64, status string, updaterUserUUID uuid.UUID) (*UserServiceDataResult, error)
	VerifyEmail(ctx context.Context, userUUID uuid.UUID, tenantID int64) (*UserServiceDataResult, error)
	VerifyPhone(ctx context.Context, userUUID uuid.UUID, tenantID int64) (*UserServiceDataResult, error)
	CompleteAccount(ctx context.Context, userUUID uuid.UUID, tenantID int64) (*UserServiceDataResult, error)
	DeleteByUUID(ctx context.Context, userUUID uuid.UUID, tenantID int64, deleterUserUUID uuid.UUID) (*UserServiceDataResult, error)
	// AnonymizeUser is the canonical GDPR Article 17 erasure implementation. It
	// anonymizes the user's PII in place (rather than hard-deleting the row, so
	// audit/referential integrity is preserved) and cascades to the user's
	// profile, sessions, and consents. Immutable audit tables (auth_events,
	// management_audit_log) are intentionally left untouched: they store only
	// integer user-id references (no PII), and their BEFORE UPDATE triggers
	// forbid mutation.
	AnonymizeUser(ctx context.Context, userID int64) error
	AssignUserRoles(ctx context.Context, userUUID uuid.UUID, roleUUIDs []uuid.UUID, tenantID int64, actorUserUUID uuid.UUID) (*UserServiceDataResult, error)
	RemoveUserRole(ctx context.Context, userUUID uuid.UUID, roleUUID uuid.UUID, tenantID int64) (*UserServiceDataResult, error)
	GetUserRoles(ctx context.Context, userUUID uuid.UUID, tenantID int64, filter GetUserRolesFilter) ([]RoleServiceDataResult, int64, error)
	GetUserIdentities(ctx context.Context, userUUID uuid.UUID, tenantID int64, filter GetUserIdentitiesFilter) ([]UserIdentityServiceDataResult, int64, error)
	GetUserSessions(ctx context.Context, userUUID uuid.UUID, tenantID int64) ([]*SessionDataResult, error)
	RevokeUserSession(ctx context.Context, userUUID uuid.UUID, tenantID int64, sessionUUID uuid.UUID) error
	RevokeAllUserSessions(ctx context.Context, userUUID uuid.UUID, tenantID int64) error
	// FindBySubAndClientID resolves a user from a JWT sub claim and client ID.
	// Used by UserContextMiddleware to populate the request context.
	FindBySubAndClientID(ctx context.Context, sub string, clientID string) (*User, error)
	// FindClientByIdentifier resolves the request's OAuth client. The client is
	// a property of the REQUEST, not of the identity — identities belong to an
	// identity provider and are usable from every client connected to it.
	FindClientByIdentifier(ctx context.Context, identifier string) (*Client, error)
	// ListMembershipCandidates returns SYSTEM-tenant users, which are the only
	// users tenant.CreateByUserUUID will accept as members. It is deliberately
	// separate from the general user list: that one is pinned to the caller's own
	// tenant, and widening it with a tenant filter would turn it into a
	// cross-tenant enumeration surface.
	ListMembershipCandidates(ctx context.Context, search *string, page, limit int) ([]MembershipCandidateDTO, int64, error)
	// FindByUserID loads a user by primary key with roles, permissions,
	// identities, and identity tenants preloaded. Used by multi-issuer
	// middleware to build AuthContext for federated principals.
	FindByUserID(ctx context.Context, userID int64) (*User, error)
	// ForcePasswordChange sets or clears the force_password_change flag for a user.
	ForcePasswordChange(ctx context.Context, userUUID uuid.UUID, tenantID int64, force bool) error
	// SetPassword sets a user's password administratively, held to the same
	// tenant policy and reuse history as self-service rotation and always
	// evicting the target's live credentials. temporary=true forces the user to
	// choose their own on next login.
	SetPassword(ctx context.Context, userUUID uuid.UUID, tenantID int64, newPassword string, temporary bool, actorUserUUID uuid.UUID) error
	// AdminLinkIdentity attaches an existing external identity (provider + sub)
	// to a user on behalf of an administrator — the operator remedy for a
	// duplicate account created through a new IdP.
	AdminLinkIdentity(ctx context.Context, userUUID uuid.UUID, tenantID int64, providerUUID uuid.UUID, sub string, actorUserUUID uuid.UUID) (*UserIdentityServiceDataResult, error)
	GetUserMFA(ctx context.Context, userUUID uuid.UUID, tenantID int64) (*UserMFAResponseDTO, error)
	// EnsureUserInTenant copies the user identified by userUUID into the target
	// tenant if they do not already have a record there. Returns the userID in
	// the target tenant (existing or newly created).
	EnsureUserInTenant(ctx context.Context, userUUID uuid.UUID, targetTenantID int64) (int64, error)
	// GrantRoleByName looks up a role by name within the tenant and assigns it to
	// the user identified by userUUID. Used by tenant member provisioning to
	// grant the super-admin role when a user is added as an owner.
	GrantRoleByName(ctx context.Context, userUUID uuid.UUID, tenantID int64, roleName string) error
}

func NewUserService

func NewUserService(
	db *gorm.DB,
	userRepo UserRepository,
	userIdentityRepo UserIdentityRepository,
	userRoleRepo UserRoleRepository,
	roleRepo RoleRepository,
	tenantRepo TenantRepository,
	identityProviderRepo IdentityProviderRepository,
	clientRepo ClientRepository,
	cacheInvalidator cache.Invalidator,
	userTokenRepo UserTokenRepository,
	securitySettingRepo secpolicy.SecuritySettingRepository,
	passwordHistoryRepo UserPasswordHistoryRepository,
	authEventService authevent.AuthEventService,
	eventService event.EventService,
) UserService

type UserServiceDataResult

type UserServiceDataResult struct {
	UserUUID        uuid.UUID
	Username        string
	Fullname        string
	Email           string
	Phone           string
	IsEmailVerified bool
	IsPhoneVerified bool
	Status          string
	Metadata        datatypes.JSON
	LastLoginAt     *time.Time
	LoginCount      int
	EmailVerifiedAt *time.Time
	PhoneVerifiedAt *time.Time
	ExternalID      *string
	CreatedBy       *int64
	UpdatedBy       *int64
	Tenant          *TenantServiceDataResult
	UserIdentities  *[]UserIdentityServiceDataResult
	Roles           *[]RoleServiceDataResult
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

type UserServiceGetFilter

type UserServiceGetFilter struct {
	Search     *string
	Username   *string
	Email      *string
	Phone      *string
	Fullname   *string
	Status     []string
	TenantID   int64
	RoleUUID   *string
	ClientUUID *string
	Page       int
	Limit      int
	SortBy     string
	SortOrder  string
}

type UserServiceGetResult

type UserServiceGetResult struct {
	Data       []UserServiceDataResult
	Total      int64
	Page       int
	Limit      int
	TotalPages int
}

type UserSetPasswordRequestDTO

type UserSetPasswordRequestDTO struct {
	Password  string `json:"password"`
	Temporary bool   `json:"temporary"`
}

UserSetPasswordRequestDTO is the admin request to set a user's password.

Like ChangePasswordDTO it declares no length or composition rules: those belong to the tenant's password policy, and duplicating them here is how the two drift apart. Temporary marks the credential one-time — the user must choose their own on next login and the temp-password expiry clock starts.

func (UserSetPasswordRequestDTO) Validate

func (dto UserSetPasswordRequestDTO) Validate() error

type UserSetPasswordResponseDTO

type UserSetPasswordResponseDTO struct {
	Temporary           bool `json:"temporary"`
	ForcePasswordChange bool `json:"force_password_change"`
	// SessionsRevoked is always true: an administrative password set is the
	// remedy for a suspected compromise, so it never spares a live credential.
	SessionsRevoked bool `json:"sessions_revoked"`
}

UserSetPasswordResponseDTO reports what the set actually did, so the console can tell the operator rather than leaving them to assume.

type UserSetStatusRequestDTO

type UserSetStatusRequestDTO struct {
	Status string `json:"status"`
}

func (UserSetStatusRequestDTO) Validate

func (dto UserSetStatusRequestDTO) Validate() error

type UserSetting

type UserSetting struct {
	UserSettingID     int64     `gorm:"column:user_setting_id;primaryKey"`
	UserSettingUUID   uuid.UUID `gorm:"column:user_setting_uuid;unique;not null"`
	UserID            int64     `gorm:"column:user_id;not null;unique"`
	Timezone          *string   `gorm:"column:timezone"`
	Locale            *string   `gorm:"column:locale"`
	PreferredLanguage *string   `gorm:"-"`
	CreatedAt         time.Time `gorm:"column:created_at;autoCreateTime"`
	UpdatedAt         time.Time `gorm:"column:updated_at;autoUpdateTime"`

	User *User `gorm:"foreignKey:UserID;references:UserID"`
}

func (*UserSetting) BeforeCreate

func (us *UserSetting) BeforeCreate(tx *gorm.DB) (err error)

func (UserSetting) TableName

func (UserSetting) TableName() string

type UserSettingHandler

type UserSettingHandler struct {
	// contains filtered or unexported fields
}

func NewUserSettingHandler

func NewUserSettingHandler(userSettingService UserSettingService) *UserSettingHandler

func (*UserSettingHandler) CreateOrUpdate

func (h *UserSettingHandler) CreateOrUpdate(w http.ResponseWriter, r *http.Request)

func (*UserSettingHandler) Delete

func (*UserSettingHandler) Get

func (*UserSettingHandler) SetAuditLogger

func (h *UserSettingHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)

SetAuditLogger injects the audit logger (called by the wiring layer).

type UserSettingRepository

type UserSettingRepository interface {
	BaseRepositoryMethods[UserSetting]
	FindByUUID(uuid any, preloads ...string) (*UserSetting, error)
	DeleteByUUID(uuid any) error
	WithTx(tx *gorm.DB) UserSettingRepository
	FindByUserID(userID int64) (*UserSetting, error)
	UpdateByUserID(userID int64, updatedUserSetting *UserSetting) error
	DeleteByUserID(userID int64) error
}

func NewUserSettingRepository

func NewUserSettingRepository(db *gorm.DB) UserSettingRepository

type UserSettingRequestDTO

type UserSettingRequestDTO struct {
	Timezone          *string `json:"timezone,omitempty"`
	PreferredLanguage *string `json:"preferred_language,omitempty"`
	Locale            *string `json:"locale,omitempty"`
}

func (UserSettingRequestDTO) Validate

func (r UserSettingRequestDTO) Validate() error

type UserSettingResponseDTO

type UserSettingResponseDTO struct {
	UserSettingUUID   string    `json:"user_setting_id"`
	Timezone          *string   `json:"timezone,omitempty"`
	PreferredLanguage *string   `json:"preferred_language,omitempty"`
	Locale            *string   `json:"locale,omitempty"`
	CreatedAt         time.Time `json:"created_at"`
	UpdatedAt         time.Time `json:"updated_at"`
}

func NewUserSettingResponseDTO

func NewUserSettingResponseDTO(us *UserSetting) *UserSettingResponseDTO

type UserSettingService

type UserSettingService interface {
	CreateOrUpdateUserSetting(
		ctx context.Context,
		userUUID uuid.UUID,
		timezone, preferredLanguage, locale *string,
	) (*UserSettingServiceDataResult, error)
	GetByUUID(ctx context.Context, userSettingUUID uuid.UUID, userID int64) (*UserSettingServiceDataResult, error)
	GetByUserUUID(ctx context.Context, userUUID uuid.UUID) (*UserSettingServiceDataResult, error)
	DeleteByUUID(ctx context.Context, userSettingUUID uuid.UUID, userID int64) (*UserSettingServiceDataResult, error)
}

func NewUserSettingService

func NewUserSettingService(
	db *gorm.DB,
	userSettingRepo UserSettingRepository,
	userRepo UserRepository,
) UserSettingService

type UserSettingServiceDataResult

type UserSettingServiceDataResult struct {
	UserSettingUUID   uuid.UUID
	Timezone          *string
	PreferredLanguage *string
	Locale            *string
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

type UserToken

type UserToken struct {
	UserTokenID   int64      `gorm:"column:user_token_id;primaryKey"`
	UserTokenUUID uuid.UUID  `gorm:"column:user_token_uuid;not null;unique"`
	UserID        int64      `gorm:"column:user_id;not null"`
	TokenType     string     `gorm:"column:token_type;not null"`
	Token         string     `gorm:"column:token;uniqueIndex:idx_user_tokens_token_unique;not null"` // hashed
	UserAgent     *string    `gorm:"column:user_agent"`
	IPAddress     *string    `gorm:"column:ip_address"`
	IsRevoked     bool       `gorm:"column:is_revoked;not null;default:false"`
	ExpiresAt     *time.Time `gorm:"column:expires_at;index:idx_user_tokens_expires_at,where:expires_at IS NOT NULL"`

	// Session-specific fields — only populated for shared.TokenTypeSession records.
	LastUsedAt         *time.Time `gorm:"column:last_used_at"`
	IdleTimeoutSeconds *int       `gorm:"column:idle_timeout_seconds"`
	AbsoluteExpiresAt  *time.Time `gorm:"column:absolute_expires_at"`

	CreatedAt time.Time  `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"`

	// Relationships
	User *User `gorm:"foreignKey:UserID;references:UserID;constraint:OnDelete:CASCADE"`
}

UserToken stores short-lived tokens (email verification, password reset, magic links) and persistent session records.

Session-specific fields (LastUsedAt, IdleTimeoutSeconds, AbsoluteExpiresAt) are populated only when TokenType == shared.TokenTypeSession; they are NULL for all other token types.

func (*UserToken) BeforeCreate

func (ut *UserToken) BeforeCreate(tx *gorm.DB) (err error)

func (UserToken) TableName

func (UserToken) TableName() string

type UserTokenRepository

type UserTokenRepository interface {
	BaseRepositoryMethods[UserToken]
	FindAll(preloads ...string) ([]UserToken, error)
	FindByUUID(uuid any, preloads ...string) (*UserToken, error)
	FindByUUIDs(uuids []string, preloads ...string) ([]UserToken, error)
	FindByID(id any, preloads ...string) (*UserToken, error)
	UpdateByUUID(uuid any, updatedData any) (*UserToken, error)
	UpdateByID(id any, updatedData any) (*UserToken, error)
	DeleteByUUID(uuid any) error
	DeleteByID(id any) error
	Paginate(conditions map[string]any, page int, limit int, preloads ...string) (*PaginationResult[UserToken], error)
	WithTx(tx *gorm.DB) UserTokenRepository
	FindByUserID(userID int64) ([]UserToken, error)
	FindActiveTokensByUserID(userID int64) ([]UserToken, error)
	FindByUserIDAndTokenType(userID int64, tokenType string) ([]UserToken, error)
	RevokeByUUID(tokenUUID uuid.UUID) error
	RevokeAllByUserID(userID int64) error
	DeleteByUserID(userID int64) error
	DeleteExpiredTokens(before time.Time) error

	// Session-specific methods
	FindActiveSessions(userID int64) ([]UserToken, error)
	FindActiveSessionByUUID(userID int64, sessionUUID uuid.UUID) (*UserToken, error)
	CountActiveSessions(userID int64) (int64, error)
	TouchSession(userID int64, sessionUUID uuid.UUID, now time.Time) error
	RevokeSessionByUUID(userID int64, sessionUUID uuid.UUID) error
	RevokeAllSessionsByUserID(userID int64) error
}

func NewUserTokenRepository

func NewUserTokenRepository(db *gorm.DB) UserTokenRepository

type UserTrustedDevice

type UserTrustedDevice struct {
	UserTrustedDeviceID   int64          `gorm:"column:user_trusted_device_id;primaryKey"`
	UserTrustedDeviceUUID uuid.UUID      `gorm:"column:user_trusted_device_uuid;unique;not null"`
	UserID                int64          `gorm:"column:user_id;not null"`
	TenantID              int64          `gorm:"column:tenant_id;not null"`
	DeviceFingerprint     string         `gorm:"column:device_fingerprint;not null"`
	DeviceTokenHash       string         `gorm:"column:device_token_hash;not null"`
	DeviceName            *string        `gorm:"column:device_name"`
	Location              *string        `gorm:"column:location"`
	IPAddress             *string        `gorm:"column:ip_address"`
	UserAgent             *string        `gorm:"column:user_agent"`
	TrustedUntil          time.Time      `gorm:"column:trusted_until;not null"`
	LastSeenAt            *time.Time     `gorm:"column:last_seen_at"`
	CreatedAt             time.Time      `gorm:"column:created_at;not null;autoCreateTime"`
	UpdatedAt             time.Time      `gorm:"column:updated_at;not null;autoUpdateTime"`
	DeletedAt             gorm.DeletedAt `gorm:"column:deleted_at;index"`
}

func (*UserTrustedDevice) BeforeCreate

func (d *UserTrustedDevice) BeforeCreate(tx *gorm.DB) (err error)

func (UserTrustedDevice) TableName

func (UserTrustedDevice) TableName() string

type UserTrustedDeviceHandler

type UserTrustedDeviceHandler struct {
	// contains filtered or unexported fields
}

func NewUserTrustedDeviceHandler

func NewUserTrustedDeviceHandler(deviceService UserTrustedDeviceService, userService UserService, userRepo UserRepository) *UserTrustedDeviceHandler

func (*UserTrustedDeviceHandler) DeleteMyDevice

func (h *UserTrustedDeviceHandler) DeleteMyDevice(w http.ResponseWriter, r *http.Request)

DeleteMyDevice removes a trusted device for the authenticated user.

DELETE /me/devices/{device_uuid}

func (*UserTrustedDeviceHandler) DeleteUserDevice

func (h *UserTrustedDeviceHandler) DeleteUserDevice(w http.ResponseWriter, r *http.Request)

DeleteUserDevice revokes a specific trusted device for a user (admin).

DELETE /users/{user_uuid}/devices/{device_uuid}

func (*UserTrustedDeviceHandler) GetUserDevices

func (h *UserTrustedDeviceHandler) GetUserDevices(w http.ResponseWriter, r *http.Request)

GetUserDevices returns all trusted devices for a specific user (admin).

GET /users/{user_uuid}/devices

func (*UserTrustedDeviceHandler) ListMyDevices

func (h *UserTrustedDeviceHandler) ListMyDevices(w http.ResponseWriter, r *http.Request)

ListMyDevices returns all trusted devices for the authenticated user.

GET /me/devices

func (*UserTrustedDeviceHandler) SetAuditLogger

SetAuditLogger injects the audit logger (called by the wiring layer).

type UserTrustedDeviceRepository

type UserTrustedDeviceRepository interface {
	BaseRepositoryMethods[UserTrustedDevice]
	WithTx(tx *gorm.DB) UserTrustedDeviceRepository
	FindByUserID(userID int64) ([]UserTrustedDevice, error)
	FindActiveByUserID(userID int64) ([]UserTrustedDevice, error)
	FindByUUID(uuid any, preloads ...string) (*UserTrustedDevice, error)
	DeleteByUUID(uuid any) error
	CreateDevice(device *UserTrustedDevice) error
	DeleteExpired() (int64, error)
	UpdateLastSeen(deviceID int64, seenAt time.Time) error
}

func NewUserTrustedDeviceRepository

func NewUserTrustedDeviceRepository(db *gorm.DB) UserTrustedDeviceRepository

type UserTrustedDeviceResponseDTO

type UserTrustedDeviceResponseDTO struct {
	UUID              string `json:"uuid"`
	DeviceFingerprint string `json:"device_fingerprint"`
	DeviceName        string `json:"device_name,omitempty"`
	Location          string `json:"location,omitempty"`
	IPAddress         string `json:"ip_address,omitempty"`
	UserAgent         string `json:"user_agent,omitempty"`
	TrustedUntil      string `json:"trusted_until"`
	LastSeenAt        string `json:"last_seen_at,omitempty"`
	CreatedAt         string `json:"created_at"`
	// Current is true for the device making the request (self-service list only),
	// so the UI can badge "This device".
	Current bool `json:"current,omitempty"`
}

type UserTrustedDeviceService

type UserTrustedDeviceService interface {
	ListDevices(ctx context.Context, userID int64) ([]UserTrustedDevice, error)
	DeleteDevice(ctx context.Context, deviceUUID string) error
}

type UserUpdateRequestDTO

type UserUpdateRequestDTO struct {
	Username string         `json:"username"`
	Email    *string        `json:"email,omitempty"`
	Phone    *string        `json:"phone,omitempty"`
	Status   string         `json:"status"`
	Metadata datatypes.JSON `json:"metadata,omitempty"`
}

func (UserUpdateRequestDTO) Validate

func (dto UserUpdateRequestDTO) Validate() error

type VerifyBackupCodeDTO

type VerifyBackupCodeDTO struct {
	Email      string `json:"email"`
	Password   string `json:"password"`
	Code       string `json:"code"`
	ClientID   string `json:"client_id"`
	ProviderID string `json:"provider_id"`
}

VerifyBackupCodeDTO is the request to recover an account via a backup code. Password is the first factor; the backup code is the second. See Validate.

func (*VerifyBackupCodeDTO) Validate

func (r *VerifyBackupCodeDTO) Validate() error

Validate requires a password alongside the backup code. A backup code is a recovery SECOND factor, not a standalone primary credential: without the password this endpoint minted a full access + refresh token set from an email address and one 8–10 character code, which bypasses the tenant's enforced-MFA policy outright. SanitizeInput is deliberately NOT applied to the password — it rewrites characters, which would mutate the secret being compared.

type VerifyEmailChangeDTO

type VerifyEmailChangeDTO struct {
	OTP string `json:"otp"`
}

VerifyEmailChangeDTO is the request to confirm an email change via OTP.

func (*VerifyEmailChangeDTO) Validate

func (r *VerifyEmailChangeDTO) Validate() error

type VerifyPhoneDTO

type VerifyPhoneDTO struct {
	Phone string `json:"phone"`
	Code  string `json:"code"`
}

VerifyPhoneDTO is the request to verify a phone number with an SMS OTP.

func (*VerifyPhoneDTO) Validate

func (r *VerifyPhoneDTO) Validate() error

type WithdrawConsentRequestDTO

type WithdrawConsentRequestDTO struct {
	ConsentType string `json:"consent_type"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL