User Account System
Core concepts
- User authentication record –
UserAuthAccountis the canonical row inauth.user. It tracks the account UUID, ban flag, registration timestamp, and whether two-factor is enabled while letting the same identity be accessed through multiple login surfaces:
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::FromRow)]
/// The core entity of user authentication.
///
/// This is the top-level entity that represents a user's authentication account.
/// It contains the user's ID, whether they are banned, and the date they registered.
///
/// The user can have multiple way to login, such as email, OAuth.
pub struct UserAuthAccount {
pub id: Uuid,
pub is_banned: bool,
pub registered_at: time::PrimitiveDateTime,
pub two_factor_enabled: bool,
}
- Profile vs. login surface –
UserProfilekeeps mutable presentation data (name, picture, marketing email, group membership, MFA flag) separate from credentials; the email stored here is not automatically a login method:
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
/// The profile of a user.
pub struct UserProfile {
pub id: Uuid,
pub name: Option<String>,
pub picture: Option<String>,
/// The email address will be used for notification and marketing.
///
/// For the address used for authentication or security, refer to the EmailAccount entity.
pub email: Option<String>,
pub created_at: time::PrimitiveDateTime,
pub updated_at: time::PrimitiveDateTime,
/// User's group determines what production can be shown to the user.
pub user_group: i32,
/// User's extra groups are used to determine what production can be shown to the user.
/// Extra group is for private production.
pub user_extra_groups: Vec<i32>,
/// Whether MFA is enabled for the user
pub mfa_enabled: bool,
}
Dedicated tables hold actual login credentials: password-backed EmailAccount rows link an address and password hash to the user:
#[derive(Clone, PartialEq, Eq, sqlx::FromRow, Zeroize, ZeroizeOnDrop)]
pub struct EmailAccount {
pub id: i64,
pub email: String,
pub password_hash: CompactString,
pub user_id: Uuid,
}
Each OAuth connection lives in OAuthAccount with provider metadata and timestamps:
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
/// The OAuth account of a user.
///
/// One user can have multiple OAuth accounts.
pub struct OAuthAccount {
pub id: i64,
pub user_id: Uuid,
pub provider_name: OAuthProviderName,
pub provider_user_id: String,
pub registered_at: PrimitiveDateTime,
pub token_updated_at: PrimitiveDateTime,
}
- Events and audits – account binding/unbinding and security changes emit AMQP events (see
AccountBindEvent,AccountUnbindEvent,PasswordResetEvent, etc.) so downstream systems can react or log activity. When you add new surfaces remember to publish the appropriate events.
Account data layout
When a user registers through email, RegisterEmailAccount creates the auth.user row, seeds a profile, and stores the hashed password in auth.email_account:
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct RegisterEmailAccount {
pub email: String,
pub password_hash: CompactString,
pub user_group: i32,
}
pub enum RegisterEmailAccountResult {
Success {
user_id: Uuid,
email_account_id: i64,
},
EmailAlreadyExists,
}
OAuth registrations follow the same pattern via RegisterOAuthAccount, inserting the profile with provider metadata before creating the first OAuth credential row:
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisterOAuthAccount {
pub provider_name: OAuthProviderName,
pub provider_user_id: String,
pub email: Option<String>,
pub name: Option<String>,
pub picture: Option<String>,
pub user_group: i32,
}
The account service layers aggregate these shards on demand. UserManageService::process(ShowUserDetail) joins profile, auth flags, email login (if any), OAuth logins, and whether TOTP exists so dashboards can render a full state snapshot.
Whenever you extend the schema, double-check that:
CountUserLoginMethodscontinues to report the real number of usable login paths (it currently sums email + OAuth rows)- Removal flows (user-facing and admin) still guard against deleting the last login method.
- Admin-facing DTOs (
UserDetailResponse,UserSummary) expose whatever additional surface you add for operations tooling.
Login flows and user self-service
Email/password login
EmailProviderService::process(EmailLogin) performs credential lookup, constant-time password verification (dummy hash fallback), and MFA evaluation before minting access/refresh JWTs through SessionService::CreateSession and emitting a UserLoginEvent for analytics:
#[derive(Clone, PartialEq, Eq)]
pub struct EmailLogin {
pub email: String,
pub password: String,
pub mfa: Option<MfaMethod>,
pub ip: Option<IpAddr>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmailLoginResult {
Success(AccessToken, RefreshToken),
WrongCredential,
RequireMfa,
MfaFailed,
NotFound,
}
The session creation process stores refresh tokens in Redis and generates JWT access tokens:
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSession {
pub user_id: Uuid,
pub login_method: LoginMethod,
pub ip: Option<std::net::IpAddr>,
pub user_agent: Option<String>,
}
Expect a RequireMfa or MfaFailed result when MFA is toggled on and the client omits or fails verification.
OAuth login
OAuthProviderService::process(OAuthLogin) validates the state challenge stored in Redis, exchanges the provider code for tokens, fetches user info, and either looks up or registers an OAuthAccount. New registrations populate profile defaults and raise UserRegisterEvent. Successful logins create a session tagged with the provider for downstream attribution:
#[derive(Debug, Clone)]
pub struct OAuthLogin {
pub provider_name: OAuthProviderName,
pub code: String,
pub state: Uuid,
pub ip: Option<std::net::IpAddr>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OAuthLoginResult {
LoggedIn(AccessToken, RefreshToken),
InvalidState,
ProviderMismatch,
ProviderError(String),
UserRegistered(AccessToken, RefreshToken),
}
Managing login methods
Users can bind extra surfaces only after entering sudo mode. Email-based sudo tokens are issued via MfaService and verified by both EmailProviderService and OAuthProviderService before binding, changing passwords, or unlinking methods.
For removal, EmailProviderService::RemoveEmailAccount and OAuthProviderService::RemoveOAuthAccount ensure at least one login method remains, delete the credential row, and fire an AccountUnbindEvent so audit logs stay complete.
Those flows back the gRPC UserAccountService endpoints that power user settings; when adding a new method wire it through the same guardrails.
Security hardening
- MFA & sudo mode –
MfaServicesupports TOTP and email OTP verification, toggles MFA on the profile, and issues short-lived sudo tokens cached in Redis. Any destructive credential change validates a sudo token first:
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MfaMethod {
Totp { code: u32 },
Email { email: String, code: String },
}
-
Password lifecycle – Password resets validate email links, hash the new password with the configured algorithm, terminate every active session, and broadcast a
PasswordResetEvent. See the Password Reset Flow guide for detailed information about the reset process and APIs. Explicit password changes follow the same hashing path and sudo check. -
Session management – The session service stores refresh tokens in Redis keyed by
SessionId, issues JWTs from config, writes login events, and can terminate an entire user’s session set on demand (used after password reset or by security tooling). -
Constant-time credential checks – Email login performs dummy verifications when the address is missing to minimize timing leaks, and password hashes are never logged thanks to explicit redaction in debug output (as shown in the
EmailAccountdebug implementation above).
Administrative operations
UserManageService exposes RBAC-protected processors for customer support and moderation:
- Counting users in configurable time windows, optionally excluding banned accounts
- Listing users with filters on email, group, ban status, and registration time, plus aggregate OAuth provider names for quick scanning
- Showing detailed account state (profile, auth flags, login methods, MFA) for a specific user (as shown in the
ShowUserDetailimplementation above) - Removing login methods while keeping at least one usable path, editing profile basics, banning/unbanning, and forcibly removing TOTP when users are locked out
All administrative operations follow the RBAC pattern established in the Manage module, where each operation implements AdminOperation with role restrictions and audit logging:
impl AdminOperation for RemoveLoginMethodRequest {
const ALLOWED_ROLES: &'static [AdminRole] = &[AdminRole::SuperAdmin, AdminRole::Moderator];
const OPERATION_NAME: &'static str = "remove_login_method";
const OPERATION_TARGET: &'static str = "user_account";
fn to_audit_log(&self) -> Result<String, serde_json::Error> {
// ...
}
}
Whenever you add a new credential type or security lever, update these processors plus the protobufs exposed by UserAccountService so both admins and end users can inspect and manage the new surface: