Documentation
¶
Index ¶
- Constants
- func AccountRoute(r chi.Router, accountHandler *AccountHandler, ...)
- func AccountSelfReadRoute(r chi.Router, accountHandler *AccountHandler, ...)
- func BirthdateString(t *time.Time) *string
- func DataErasureAdminRoute(r chi.Router, handler *DataErasureHandler, ...)
- func DataErasureSelfRoute(r chi.Router, handler *DataErasureHandler, ...)
- func ProfilePictureURL(profileUUID uuid.UUID) string
- func ProfileRoute(r chi.Router, profileHandler *ProfileHandler, ...)
- func ProfileSelfReadRoute(r chi.Router, profileHandler *ProfileHandler, ...)
- func RecoveryRoute(r chi.Router, accountHandler *AccountHandler)
- func StartDataErasureWorker(ctx context.Context, svc DataErasureService, interval time.Duration)
- func UserRoute(r chi.Router, userHandler *UserHandler, profileHandler *ProfileHandler, ...)
- func UserSettingRoute(r chi.Router, userSettingHandler *UserSettingHandler, ...)
- func UserTrustedDeviceRoute(r chi.Router, deviceHandler *UserTrustedDeviceHandler, ...)
- func ValidateProfileURL(raw string) (string, error)
- func ValidateTenantAccess(actor *User, target *Tenant) error
- type AccountDeleteDTO
- type AccountExportDTO
- type AccountHandler
- func (h *AccountHandler) ChangePassword(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) ChangeUsername(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) DeleteAccount(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) ExportAccountData(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) GetAccount(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) InitiateEmailChange(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) ListSessions(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) RevokeAllSessions(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) RevokeOtherSessions(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) RevokeSession(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) SendPhoneVerification(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- func (h *AccountHandler) VerifyBackupCode(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) VerifyEmailChange(w http.ResponseWriter, r *http.Request)
- func (h *AccountHandler) VerifyPhone(w http.ResponseWriter, r *http.Request)
- type AccountProfileDTO
- type AccountResponseDTO
- type AccountService
- type AccountTenantDTO
- type BaseRepository
- type BaseRepositoryMethods
- type ChangeEmailRequestDTO
- type ChangePasswordDTO
- type ChangePasswordResponseDTO
- type ChangeUsernameDTO
- type Client
- type ClientRepository
- type ClientResponseDTO
- type ClientServiceDataResult
- type DataErasureHandler
- type DataErasureRequest
- type DataErasureRequestRepository
- type DataErasureRequestResult
- type DataErasureService
- type ErasureRequestDTO
- type ErasureResponseDTO
- type GetUserIdentitiesFilter
- type GetUserRolesFilter
- type IdentityProvider
- type IdentityProviderRepository
- type IdentityUnlinker
- type LoginResponseDTO
- type MembershipCandidateDTO
- type PaginatedResponseDTO
- type PaginationRequestDTO
- type PaginationResult
- type Permission
- type Profile
- type ProfileFilterDTO
- type ProfileHandler
- func (h *ProfileHandler) AdminCreateProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) AdminDeleteProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) AdminGetAllProfiles(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) AdminGetProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) AdminUpdateProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) CreateOrUpdate(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) CreateProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) DeleteByUUID(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) DeletePicture(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) GetAll(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) GetByUUID(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) GetPicture(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- func (h *ProfileHandler) SetDefaultProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) SetUserRepo(r UserRepository)
- func (h *ProfileHandler) UpdateProfile(w http.ResponseWriter, r *http.Request)
- func (h *ProfileHandler) UploadPicture(w http.ResponseWriter, r *http.Request)
- type ProfilePicture
- type ProfilePictureRecord
- type ProfilePictureRepository
- type ProfileRepository
- type ProfileRepositoryGetFilter
- type ProfileRequestDTO
- type ProfileResponseDTO
- type ProfileService
- type ProfileServiceDataResult
- type ProfileServiceListResult
- type ProfileServiceOption
- type RecordConsentRequestDTO
- type RefreshTokenRevoker
- type RequestErasureInput
- type Role
- type RolePermission
- type RoleRepository
- type RoleRepositoryGetFilter
- type RoleResponseDTO
- type RoleServiceDataResult
- type SendPhoneVerificationDTO
- type SessionCreator
- type SessionDataResult
- type SessionRevoker
- type SessionService
- type SuccessResponseDTO
- type Tenant
- type TenantRepository
- type TenantResolver
- type TenantResponseDTO
- type TenantServiceDataResult
- type User
- type UserAnonymizer
- type UserAssignRolesRequestDTO
- type UserConsent
- type UserConsentHandler
- func (h *UserConsentHandler) GetUserConsents(w http.ResponseWriter, r *http.Request)
- func (h *UserConsentHandler) RecordConsent(w http.ResponseWriter, r *http.Request)
- func (h *UserConsentHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- func (h *UserConsentHandler) WithdrawUserConsent(w http.ResponseWriter, r *http.Request)
- type UserConsentRepository
- type UserConsentResponseDTO
- type UserConsentService
- type UserCreateRequestDTO
- type UserFilterDTO
- type UserGRPCHandler
- func (h *UserGRPCHandler) AssignUserRoles(ctx context.Context, req *authv1.AssignUserRolesRequest) (*authv1.AssignUserRolesResponse, error)
- func (h *UserGRPCHandler) CompleteUserAccount(ctx context.Context, req *authv1.CompleteUserAccountRequest) (*authv1.CompleteUserAccountResponse, error)
- func (h *UserGRPCHandler) CreateUser(ctx context.Context, req *authv1.CreateUserRequest) (*authv1.CreateUserResponse, error)
- func (h *UserGRPCHandler) DeleteUser(ctx context.Context, req *authv1.DeleteUserRequest) (*authv1.DeleteUserResponse, error)
- func (h *UserGRPCHandler) ForceUserPasswordChange(ctx context.Context, req *authv1.ForceUserPasswordChangeRequest) (*authv1.ForceUserPasswordChangeResponse, error)
- func (h *UserGRPCHandler) GetUser(ctx context.Context, req *authv1.GetUserRequest) (*authv1.GetUserResponse, error)
- func (h *UserGRPCHandler) ListUserIdentities(ctx context.Context, req *authv1.ListUserIdentitiesRequest) (*authv1.ListUserIdentitiesResponse, error)
- func (h *UserGRPCHandler) ListUserRoles(ctx context.Context, req *authv1.ListUserRolesRequest) (*authv1.ListUserRolesResponse, error)
- func (h *UserGRPCHandler) ListUsers(ctx context.Context, req *authv1.ListUsersRequest) (*authv1.ListUsersResponse, error)
- func (h *UserGRPCHandler) RemoveUserRole(ctx context.Context, req *authv1.RemoveUserRoleRequest) (*authv1.RemoveUserRoleResponse, error)
- func (h *UserGRPCHandler) SetUserStatus(ctx context.Context, req *authv1.SetUserStatusRequest) (*authv1.SetUserStatusResponse, error)
- func (h *UserGRPCHandler) UpdateUser(ctx context.Context, req *authv1.UpdateUserRequest) (*authv1.UpdateUserResponse, error)
- func (h *UserGRPCHandler) VerifyUserEmail(ctx context.Context, req *authv1.VerifyUserEmailRequest) (*authv1.VerifyUserEmailResponse, error)
- func (h *UserGRPCHandler) VerifyUserPhone(ctx context.Context, req *authv1.VerifyUserPhoneRequest) (*authv1.VerifyUserPhoneResponse, error)
- type UserHandler
- func (h *UserHandler) AssignRoles(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) ForcePasswordChange(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUserIdentities(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUserMFA(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUserRoles(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUserSessions(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) LinkUserIdentity(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) ListMembershipCandidates(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) RemoveRole(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) RevokeAllUserSessions(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) RevokeUserSession(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- func (h *UserHandler) SetLockoutClearer(c UserLockoutClearer)
- func (h *UserHandler) SetUserPassword(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) SetUserStatus(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) UnlinkUserIdentity(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) UnlockUser(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) VerifyEmail(w http.ResponseWriter, r *http.Request)
- func (h *UserHandler) VerifyPhone(w http.ResponseWriter, r *http.Request)
- type UserIdentity
- type UserIdentityFilterDTO
- type UserIdentityRepository
- type UserIdentityResponseDTO
- type UserIdentityServiceDataResult
- type UserLinkIdentityRequestDTO
- type UserLockoutClearer
- type UserMFABackupCode
- type UserMFABackupCodeRepository
- type UserMFAResponseDTO
- type UserMFAWebAuthnKeyDTO
- type UserPasswordHistory
- type UserPasswordHistoryRepository
- type UserProfileGRPCHandler
- func (h *UserProfileGRPCHandler) CreateUserProfile(ctx context.Context, req *authv1.CreateUserProfileRequest) (*authv1.CreateUserProfileResponse, error)
- func (h *UserProfileGRPCHandler) DeleteUserProfile(ctx context.Context, req *authv1.DeleteUserProfileRequest) (*authv1.DeleteUserProfileResponse, error)
- func (h *UserProfileGRPCHandler) GetUserProfile(ctx context.Context, req *authv1.GetUserProfileRequest) (*authv1.GetUserProfileResponse, error)
- func (h *UserProfileGRPCHandler) ListUserProfiles(ctx context.Context, req *authv1.ListUserProfilesRequest) (*authv1.ListUserProfilesResponse, error)
- func (h *UserProfileGRPCHandler) SetDefaultUserProfile(ctx context.Context, req *authv1.SetDefaultUserProfileRequest) (*authv1.SetDefaultUserProfileResponse, error)
- func (h *UserProfileGRPCHandler) UpdateUserProfile(ctx context.Context, req *authv1.UpdateUserProfileRequest) (*authv1.UpdateUserProfileResponse, error)
- type UserRepository
- type UserRepositoryGetFilter
- type UserResponseDTO
- type UserRole
- type UserRoleFilterDTO
- type UserRoleRepository
- type UserService
- type UserServiceDataResult
- type UserServiceGetFilter
- type UserServiceGetResult
- type UserSetPasswordRequestDTO
- type UserSetPasswordResponseDTO
- type UserSetStatusRequestDTO
- type UserSetting
- type UserSettingHandler
- func (h *UserSettingHandler) CreateOrUpdate(w http.ResponseWriter, r *http.Request)
- func (h *UserSettingHandler) Delete(w http.ResponseWriter, r *http.Request)
- func (h *UserSettingHandler) Get(w http.ResponseWriter, r *http.Request)
- func (h *UserSettingHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- type UserSettingRepository
- type UserSettingRequestDTO
- type UserSettingResponseDTO
- type UserSettingService
- type UserSettingServiceDataResult
- type UserToken
- type UserTokenRepository
- type UserTrustedDevice
- type UserTrustedDeviceHandler
- func (h *UserTrustedDeviceHandler) DeleteMyDevice(w http.ResponseWriter, r *http.Request)
- func (h *UserTrustedDeviceHandler) DeleteUserDevice(w http.ResponseWriter, r *http.Request)
- func (h *UserTrustedDeviceHandler) GetUserDevices(w http.ResponseWriter, r *http.Request)
- func (h *UserTrustedDeviceHandler) ListMyDevices(w http.ResponseWriter, r *http.Request)
- func (h *UserTrustedDeviceHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
- type UserTrustedDeviceRepository
- type UserTrustedDeviceResponseDTO
- type UserTrustedDeviceService
- type UserUpdateRequestDTO
- type VerifyBackupCodeDTO
- type VerifyEmailChangeDTO
- type VerifyPhoneDTO
- type WithdrawConsentRequestDTO
Constants ¶
const ( SortOrderAsc = pagination.SortOrderAsc SortOrderDesc = pagination.SortOrderDesc )
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 ¶
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 ¶
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 ¶
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 ¶
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 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 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"`
}
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 GetUserRolesFilter ¶
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.
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 (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request)
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 ¶
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 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"`
}
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 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 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 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 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 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"`
}
type UserAnonymizer ¶
UserAnonymizer is the narrow capability the erasure worker needs from the user service: the canonical multi-table anonymization cascade.
type UserAssignRolesRequestDTO ¶
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 (h *UserGRPCHandler) AssignUserRoles(ctx context.Context, req *authv1.AssignUserRolesRequest) (*authv1.AssignUserRolesResponse, error)
func (*UserGRPCHandler) CompleteUserAccount ¶
func (h *UserGRPCHandler) CompleteUserAccount(ctx context.Context, req *authv1.CompleteUserAccountRequest) (*authv1.CompleteUserAccountResponse, error)
func (*UserGRPCHandler) CreateUser ¶
func (h *UserGRPCHandler) CreateUser(ctx context.Context, req *authv1.CreateUserRequest) (*authv1.CreateUserResponse, error)
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 (h *UserGRPCHandler) DeleteUser(ctx context.Context, req *authv1.DeleteUserRequest) (*authv1.DeleteUserResponse, error)
func (*UserGRPCHandler) ForceUserPasswordChange ¶
func (h *UserGRPCHandler) ForceUserPasswordChange(ctx context.Context, req *authv1.ForceUserPasswordChangeRequest) (*authv1.ForceUserPasswordChangeResponse, error)
func (*UserGRPCHandler) GetUser ¶
func (h *UserGRPCHandler) GetUser(ctx context.Context, req *authv1.GetUserRequest) (*authv1.GetUserResponse, error)
func (*UserGRPCHandler) ListUserIdentities ¶
func (h *UserGRPCHandler) ListUserIdentities(ctx context.Context, req *authv1.ListUserIdentitiesRequest) (*authv1.ListUserIdentitiesResponse, error)
func (*UserGRPCHandler) ListUserRoles ¶
func (h *UserGRPCHandler) ListUserRoles(ctx context.Context, req *authv1.ListUserRolesRequest) (*authv1.ListUserRolesResponse, error)
func (*UserGRPCHandler) ListUsers ¶
func (h *UserGRPCHandler) ListUsers(ctx context.Context, req *authv1.ListUsersRequest) (*authv1.ListUsersResponse, error)
func (*UserGRPCHandler) RemoveUserRole ¶
func (h *UserGRPCHandler) RemoveUserRole(ctx context.Context, req *authv1.RemoveUserRoleRequest) (*authv1.RemoveUserRoleResponse, error)
func (*UserGRPCHandler) SetUserStatus ¶
func (h *UserGRPCHandler) SetUserStatus(ctx context.Context, req *authv1.SetUserStatusRequest) (*authv1.SetUserStatusResponse, error)
func (*UserGRPCHandler) UpdateUser ¶
func (h *UserGRPCHandler) UpdateUser(ctx context.Context, req *authv1.UpdateUserRequest) (*authv1.UpdateUserResponse, error)
func (*UserGRPCHandler) VerifyUserEmail ¶
func (h *UserGRPCHandler) VerifyUserEmail(ctx context.Context, req *authv1.VerifyUserEmailRequest) (*authv1.VerifyUserEmailResponse, error)
func (*UserGRPCHandler) VerifyUserPhone ¶
func (h *UserGRPCHandler) VerifyUserPhone(ctx context.Context, req *authv1.VerifyUserPhoneRequest) (*authv1.VerifyUserPhoneResponse, error)
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 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 ¶
func (h *UserProfileGRPCHandler) CreateUserProfile(ctx context.Context, req *authv1.CreateUserProfileRequest) (*authv1.CreateUserProfileResponse, error)
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 (h *UserProfileGRPCHandler) DeleteUserProfile(ctx context.Context, req *authv1.DeleteUserProfileRequest) (*authv1.DeleteUserProfileResponse, error)
func (*UserProfileGRPCHandler) GetUserProfile ¶
func (h *UserProfileGRPCHandler) GetUserProfile(ctx context.Context, req *authv1.GetUserProfileRequest) (*authv1.GetUserProfileResponse, error)
func (*UserProfileGRPCHandler) ListUserProfiles ¶
func (h *UserProfileGRPCHandler) ListUserProfiles(ctx context.Context, req *authv1.ListUserProfilesRequest) (*authv1.ListUserProfilesResponse, error)
func (*UserProfileGRPCHandler) SetDefaultUserProfile ¶
func (h *UserProfileGRPCHandler) SetDefaultUserProfile(ctx context.Context, req *authv1.SetDefaultUserProfileRequest) (*authv1.SetDefaultUserProfileResponse, error)
func (*UserProfileGRPCHandler) UpdateUserProfile ¶
func (h *UserProfileGRPCHandler) UpdateUserProfile(ctx context.Context, req *authv1.UpdateUserProfileRequest) (*authv1.UpdateUserProfileResponse, error)
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 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"`
}
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 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 (h *UserSettingHandler) Delete(w http.ResponseWriter, r *http.Request)
func (*UserSettingHandler) Get ¶
func (h *UserSettingHandler) Get(w http.ResponseWriter, r *http.Request)
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 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.
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 ¶
func (h *UserTrustedDeviceHandler) SetAuditLogger(l auditlog.ManagementAuditLogger)
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
}
func NewUserTrustedDeviceService ¶
func NewUserTrustedDeviceService(repo UserTrustedDeviceRepository) UserTrustedDeviceService
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 ¶
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"`
}
Source Files
¶
- deps.go
- foundation.go
- handler_account.go
- handler_data_erasure.go
- handler_profile.go
- handler_profile_grpc.go
- handler_profile_picture.go
- handler_setting.go
- handler_user.go
- handler_user_consent.go
- handler_user_grpc.go
- handler_user_trusted_device.go
- model_data_erasure_request.go
- model_profile.go
- model_profile_picture.go
- model_setting.go
- model_user.go
- model_user_consent.go
- model_user_identity.go
- model_user_password_history.go
- model_user_role.go
- model_user_token.go
- model_user_trusted_device.go
- repository_data_erasure_request.go
- repository_profile.go
- repository_profile_picture.go
- repository_setting.go
- repository_user.go
- repository_user_consent.go
- repository_user_identity.go
- repository_user_password_history.go
- repository_user_role.go
- repository_user_token.go
- repository_user_trusted_device.go
- routes.go
- service_account.go
- service_data_erasure.go
- service_profile.go
- service_profile_compat.go
- service_profile_picture.go
- service_setting.go
- service_user.go
- service_user_consent.go
- service_user_trusted_device.go
- types.go
- validation_account.go
- validation_profile.go
- validation_setting.go
- validation_user.go
- validation_user_consent.go