Skip to content

users

The auth + user-management module: email/password login, sessions, registration, invites, password reset, email verification, role assignment, admin UI, mailer backends, and the AuthMiddleware that other modules depend on for request.state.user.

ModuleMeta

FieldValue
nameUsers
route_prefix/api/users
view_prefix/users
depends_on["Auth"]

Auth flow

The module is built on fastapi-users for password hashing, registration, password reset, and verification. On top of that it layers:

  • A signed session cookie (sm_auth by default) — AuthMiddleware reads it on every request and populates request.state.user.
  • An invite flow — admins generate an invite link; the recipient sets their password via POST /api/users/auth/accept-invite.
  • LoginRateLimiter — N failures within a window triggers a cooldown per email.
  • AuthRateLimiter — global rate limit across signup / forgot-password / verify endpoints.

Auth endpoints

Method + pathBodyNotes
POST /api/users/auth/loginOAuth2PasswordRequestFormsets sm_auth cookie + session["user_id"]; rate-limited per email
POST /api/users/auth/registerUserCreategated by users.allow_signup; rate-limited
POST /api/users/auth/forgot-passwordPasswordResetrate-limited
POST /api/users/auth/reset-password/{token}ResetPassword
POST /api/users/auth/request-verify-tokenRequestVerifyTokenrate-limited
POST /api/users/auth/verifyVerifyRequest
POST /api/users/auth/accept-inviteAcceptInviteRequestsets password + signs the user in

Self-service endpoints

Method + pathBody / responsePermission
GET /api/users/meUserReadactive user
PATCH /api/users/meSelfProfileUpdateUserReadactive user

Admin endpoints (users.manage)

Method + pathBody / response
GET /api/users/admin?q=&status=&role=&verified=&sort=&order=&page=&per_page=list[UserListItem]
POST /api/users/admin/inviteUserInviteUserListItem
POST /api/users/admin/{user_id}/disableUserListItem
POST /api/users/admin/{user_id}/enableUserListItem
POST /api/users/admin/{user_id}/rolesRoleAssignment
POST /api/users/admin/{user_id}/mark-verifiedUserListItem
POST /api/users/admin/{user_id}/reset-password-linkPasswordResetLink

View routes

Public:

  • GET /users/loginUsers/Login (shows dev-account buttons in dev)
  • POST /users/logout → 303 redirect, clears cookie
  • GET /users/registerUsers/Register (404 if signup disabled)
  • GET /users/forgot-passwordUsers/ForgotPassword
  • GET /users/reset-passwordUsers/ResetPassword
  • GET /users/verifyUsers/VerifyEmail
  • GET /users/invite/acceptUsers/AcceptInvite

Authenticated:

  • GET /users/meUsers/Profile
  • PATCH /users/me → form action (redirects)

Admin (users.manage):

  • GET /users/adminUsers/Users/Index
  • GET /users/admin/inviteUsers/Users/Invite
  • GET /users/admin/{user_id}/editUsers/Users/Edit

Public contracts

python
from users.contracts import (
    UserRead, UserCreate, UserUpdate, UserInvite,
    UserListItem, RoleListItem, RoleAssignment,
    AcceptInviteRequest, PasswordResetLink, SelfProfileUpdate,
)
from users.contracts.events import (
    UserRegistered, UserInvited, UserDisabled, RoleAssigned,
)
ClassPurpose
UserReadid, email, is_active, is_superuser, is_verified, full_name, tenant_id, disabled_at, last_login_at.
UserListItemAdmin list row with roles.
RoleListItemid, name, description, user_count.
UserRegistered, UserInvited, UserDisabled, RoleAssignedEvents — see Events.

Models

User (table users_user)

ColumnTypeNotes
idUUIDPK
emailstrunique, indexed; functional index on lower(email)
hashed_passwordstr
is_active / is_superuser / is_verifiedbool
full_namestr | None
tenant_idstr | Noneindexed; only set when multi-tenant
disabled_atdatetime | None
last_login_atdatetime | Noneindexed
rolesrelationship → Role via UserRoleeagerly loaded by AuthMiddleware

Role (table users_role) — id, name (unique, indexed), description.

UserRole (table users_user_role) — composite-PK join table with assigned_at, assigned_by. The (user_id, role_id) PK is user-id-first; a separate index covers reverse lookups by role_id.

UserAccessToken — fastapi-users API tokens (rare path; sessions are the primary auth).

Two pre-seeded roles get fixed UUIDs so other modules can reference them safely:

RoleUUIDDefault permissions
admin00000000-0000-0000-0000-000000000001implicitly all (admin bypass in RequiresPermission)
user00000000-0000-0000-0000-000000000002users.self.profile, file_storage.{upload,download,delete}

Settings

DB-backed via register_module_settings. Two values are read only from the env at module import time because they bootstrap token signing before the DB is reachable:

Env varDefaultPurpose
SM_USERS_RESET_PASSWORD_TOKEN_SECRETdev-reset-token-secret-change-mepassword-reset token signing
SM_USERS_VERIFICATION_TOKEN_SECRETdev-verify-token-secret-change-meemail-verify token signing

Both must be replaced with non-placeholder values in production — the boot-time check refuses to start otherwise.

Everything else is DB-backed (initial values are pydantic defaults; edit at /settings/modules/users):

FieldDefault
allow_signupFalse
require_verificationTrue
login_redirect_url"/dashboard/" (auto-falls back to / if dashboard module isn't installed)
reset_password_token_lifetime_seconds3600
verification_token_lifetime_seconds604_800 (7 days)
cookie_name"sm_auth"
cookie_max_age_seconds1_209_600 (14 days)
cookie_secureTrue (flipped to False in dev at startup)
cookie_samesite"lax"
mailer"console" (or "smtp")
base_url"http://localhost:8000"
smtp_host / smtp_port / smtp_username / smtp_password / smtp_from / smtp_tlsSMTP config when mailer="smtp"
login_rate_limit_failures5
login_rate_limit_window_seconds300
login_rate_limit_cooldown_seconds900
auth_rate_limit_attempts10
auth_rate_limit_window_seconds300
bootstrap_email, bootstrap_password, bootstrap_user_email, bootstrap_user_password"" — see Bootstrap

Permissions

CodePurpose
users.manageadmin: list / invite / disable / role-assign
users.self.profileedit own profile (granted to user role)
LabelURLIconSectionGroupOrderRoles
Users/users/adminusersSIDEBARAdministration100["admin"]
Profile/users/meuserUSER_DROPDOWN990logged-in
Logout/users/logout (POST)log-outUSER_DROPDOWN999logged-in

Events

EventFieldsFired
UserRegistereduser_id, emailon signup
UserInviteduser_id, email, invited_byon admin invite
UserDisableduser_idon admin disable
RoleAssigneduser_id, role_nameonce per role on POST /admin/{user_id}/roles

CLI

  • sm-users create-admin --email <e> --password <p> [--full-name <name>] [--force] — creates (or, with --force, updates) an admin user. Idempotent: re-running with the same email is a no-op without --force.
bash
uv run sm-users create-admin --email admin@example.com --password changeme

Programmatically:

python
from users.bootstrap import create_admin

result = await create_admin(db, email="admin@example.com", password="...")
# result.user, result.created -> bool

Bootstrap (the first admin)

Two paths to seed the first admin:

  1. CLIsm-users create-admin ....
  2. Env vars — set SM_USERS_BOOTSTRAP_EMAIL + SM_USERS_BOOTSTRAP_PASSWORD before first make dev. bootstrap_admin_from_env(app) runs at startup and creates the admin if the users_user table is empty. Optionally seed a non-admin too via SM_USERS_BOOTSTRAP_USER_EMAIL + SM_USERS_BOOTSTRAP_USER_PASSWORD.

Mailer backends

mailer/ ships two implementations of the Mailer protocol:

BackendWhen to useConfigured via
ConsoleMailerdev — prints invite / verify / reset links to stdoutmailer="console"
SmtpMailerprod — talks SMTPmailer="smtp" + smtp_* settings

build_mailer(settings) returns the right instance based on users.mailer.

AuthMiddleware

Reads session["user_id"], loads the User row with eagerly-loaded roles, builds a UserContext, and writes it to request.state.user. Caches the context in session["user_ctx"] so subsequent requests don't re-query.

Public paths (no redirect to login):

/users/login, /users/register, /users/forgot-password, /users/reset-password, /users/verify, /users/invite/accept, /api/users/auth/, /api/users/register, /health, /static/, /api/docs, /api/redoc, /openapi.json, /i18n/, and the exact path /.

Anything else without a session redirects to /users/login.

Roles cache

roles_cache.py keeps an in-memory list of RoleSummary(id, name) on app.state.users.roles_cache. Refreshed at startup and on demand via refresh_roles_cache(app). Used by the admin UI to render role pickers without hitting the DB on every request.

Inertia pages

Auth flow:

  • Users/Login.tsx, Users/Register.tsx, Users/ForgotPassword.tsx, Users/ResetPassword.tsx, Users/VerifyEmail.tsx, Users/AcceptInvite.tsx, Users/Profile.tsx.

Admin:

  • Users/Users/Index.tsx, Users/Users/Invite.tsx, Users/Users/Edit.tsx.

Components:

  • Users/components/IndexFilters.tsx, Users/components/RolesTab.tsx.

Notes

  • cookie_secure is automatically flipped to False in dev (SM_ENVIRONMENT=development) so login works over plain HTTP. Don't override it in non-dev environments.
  • LoginRateLimiter keys by lowercased email, so an attacker spreading attempts across emails won't be slowed down — pair with WAF / IP-based rate limiting in front for that.
  • The functional lower(email) index makes case-insensitive lookups fast on Postgres; on SQLite the lookup falls back to LOWER(email) = ? which still uses the regular index.

Released under the MIT License.