VOD Docs

Authentication

Sign-up, email verification, JWT access and refresh tokens, roles and password reset.

VOD uses stateless JWT access tokens for requests and database-tracked refresh tokens for staying signed in. Passwords are hashed with bcrypt. Roles (USER, ADMIN) are read from the database on every request, never trusted from the token.

Sequence diagram · placeholder
Auth lifecycle: sign-up → verify email → sign in → authenticated requests → refresh → logout

Show which steps touch the database (users, email_verification_tokens, refresh_tokens) and which are pure JWT checks.

The two tokens

Access tokenRefresh token
PurposeSent with every API requestExchanged for a new access token
Claimsuser_id, email, role, type: "access", expuser_id, type: "refresh", exp
LifetimeACCESS_TOKEN_EXPIRE_MINUTES (code default 30)REFRESH_TOKEN_EXPIRE_DAYS (default 7)
Stored server-sideNo: verified by signature and expiry onlyYes: a SHA-256 hash in the refresh_tokens table
RevocableNo, it expires on its ownYes: is_revoked flag
Sent asAuthorization: Bearer <token>JSON body to /auth/refresh and /auth/logout

Why two tokens: the access token is short-lived and needs no database lookup to validate its signature, while the refresh token gives a revocable, longer session. Storing only a hash of the refresh token means a database leak does not expose usable tokens.

Signing uses JWT_SECRET_KEY with JWT_ALGORITHM (core/jwt.py, python-jose). Every token carries a type claim, and verification checks it, so a refresh token cannot be used as an access token.

Flows

POST /auth/signup (services/user_service.py)

  1. Validates input: username 3–50 characters, password 8–100 characters, email format.
  2. Rejects a duplicate email or username with 400.
  3. Hashes the password with bcrypt and inserts the user with role USER, is_verified = false.
  4. Creates an email-verification token (UUID, valid 24 hours) and emails a link to {FRONTEND_URL}/verify-email?token=… through Resend.

Email sending failures are logged and do not fail the sign-up; the user can request a new link.

Protecting routes

Auth is applied per route with FastAPI dependencies (core/dependencies.py):

DependencyAllowsUsed by
get_current_userAny valid access tokenGET /videos/user/me, GET /videos/{id}/status
get_current_verified_userValid token and verified emailGET /user/profile
get_current_admin_userValid token and role = ADMIN (403 otherwise)All admin video routes, uploads, the tus status bridge
get_current_user_optionalAnyone; returns the user if a valid token is present, None otherwisePublic video reads, view counting

Each dependency decodes the token, then loads the user from the database. The role check uses that database row (User.is_admin()), not the role claim inside the token, so demoting an admin takes effect on their next request instead of when their token expires.

There is no /admin path prefix; admin protection is explicit on each route.

Private videos

GET /videos/by-id/{id} returns a private video only to its owner or an admin. For anyone else it returns 404, not 403, so the response does not reveal that the video exists.

Roles

UserRole has two values, USER and ADMIN (models/enums.py); the database stores the enum names. New accounts are always USER. There is no promotion endpoint: an admin is created by updating the row directly in the database.

On the frontend

The frontend stores both tokens in localStorage, attaches the access token in an Axios interceptor, and refreshes automatically on a 401. See Data and auth.

On this page