Skip to content

Auth and roles

How users sign in, how the JWT is issued and checked, and which roles can do what.

Every BabyMart client signs in the same way. The user proves who they are, either with an email and password or through Google or GitHub. The API then returns a signed JWT. The client stores the token and sends it as Authorization: Bearer <token> with every protected request. The API reads the user’s role from the database and decides what they may do.

sequenceDiagram
  autonumber
  participant C as Client
  participant F as Firebase Auth
  participant A as API
  participant DB as MongoDB
  alt Email and password
    C->>A: POST /api/auth/login
    A->>DB: find user, compare bcrypt hash
  else Google or GitHub
    C->>F: signInWithPopup
    F-->>C: Firebase ID token
    C->>A: POST /api/auth/oauth with idToken
    A->>A: verify with Google JWKS (jose)
    A->>DB: find or create user
  end
  A-->>C: user + JWT (30 days)
  C->>A: GET /api/auth/profile with Bearer token
POST/api/auth/login POST/api/auth/register POST/api/auth/oauth GET/api/auth/profile
  • Email. Passwords are stored as bcrypt hashes. Registering does not sign the user in on the storefront. They sign in afterwards.
  • OAuth. The storefront and admin use Firebase only to open the Google or GitHub popup. The API does not use firebase-admin. It checks the Firebase ID token itself with jose, against Google’s public keys, with issuer securetoken.google.com/<FIREBASE_PROJECT_ID>. Plain Google ID tokens are accepted too, when their audience is in GOOGLE_CLIENT_IDS. The mobile app sends its Google ID token this way.
  • Conflicts. If an email already belongs to an account with another provider, the API answers 409 and names the provider to use.
apps/api/src/modules/auth/oauthVerifier.ts› verifyOAuthIdToken
Algorithm HS256, signed with JWT_SECRET (at least 32 characters in production)
Lifetime 30 days (TOKEN_TTL)
Payload { id, tv }, where tv is the user’s tokenVersion
Transport Authorization: Bearer header only. The API never reads cookies
Revoking Changing or resetting a password bumps tokenVersion, so older tokens stop working

POST /api/auth/logout does nothing on the server, because there are no sessions. Each client signs out by deleting its own copy of the token.

apps/api/src/modules/auth/token.ts› signAccessToken apps/api/src/modules/auth/protect.middleware.ts› protect
Client Stored in
Storefront auth_token cookie (7 days, sameSite: lax). proxy.ts checks it on /user/*
Admin dashboard localStorage auth-storage (zustand persist). A 401 clears it and sends the user to /login
Mobile app AsyncStorage, on the persisted user-storage user object

Every user has one role. Employees also have an employee_role.

Role Who What they can do
user Customer Shop, check out, manage their own profile, addresses, cart, wishlist and orders
vendor Seller Manage their own store and products, once a admin approves them
employee Staff Work the order queues for their employee_role (see below)
admin Owner Everything, including users, catalog, content, salaries and purchases
preview Demo visitor Browse the admin dashboard read-only

The API’s guards live in one file. An admin passes every guard, and an incharge passes every employee guard.

apps/api/src/modules/users/users.guards.ts

Fulfilment is split between staff. Each guard opens one part of /api/orders/workflow or /api/cash-collections:

employee_role Can call
call_center GET /call-center/queue, PUT /:id/confirm-address, PUT /:id/confirm-order
packer GET /packer/queue, PUT /:id/pack
incharge GET /delivery/queue, PUT /:id/assign-packer, PUT /:id/assign-deliveryman, plus everything below
deliveryman GET /my-deliveries, PUT /:id/start-delivery, PUT /:id/collect-cod, PUT /:id/deliver, PUT /:id/return-cod, and submitting cash to accounts
accounts GET /accounts/cod-pending, GET /accounts/completion-queue, PUT /:id/complete, and confirming cash receipts

Paths in the table are relative to /api/orders/workflow. Analytics (/api/analytics) is open to admin, accounts and incharge. The order statuses these actions move through are shown in How it works.

Demo accounts can look around without changing anything. Two separate layers enforce this:

  • API. preventReadOnlyActions blocks POST, PUT, PATCH and DELETE for the emails demo@gmail.com and test@gmail.com. It responds with 403 and code READ_ONLY_USER.
  • Admin dashboard. An axios request interceptor cancels every mutating request from a user with role preview and shows a toast. The admin’s demo buttons sign in as that preview account.
apps/api/middleware/readOnlyMiddleware.ts apps/admin/src/lib/config.ts
  1. POST /api/auth/forgot-password creates a random token, stores its SHA-256 hash with a one-hour expiry, and emails ${CLIENT_URL}/auth/reset-password?token=….
  2. The storefront page posts the token and the new password to POST /api/auth/reset-password.
  3. The password is replaced and tokenVersion is bumped, which signs the user out on every device.

OAuth users can add a password later with POST /api/auth/set-password.