Authentication
Tokens
- Login and registration return a JWT signed with
JWT_SECRET(HS256). - Payload:
{ sub: userId, role } - Lifetime:
JWT_EXPIRES_IN(default7d) - Clients send
Authorization: Bearer <token>
requireAuth
src/middleware/auth.js:
- Extracts the bearer token (→
401 UNAUTHORIZEDif missing). - Verifies it (→
401 SESSION_EXPIREDif invalid or expired). - Loads the user with
sessionUserSelect(→401if deleted). - Rejects non-
ACTIVEusers with403 ACCOUNT_RESTRICTED. - Sets
req.user.
Because the user is reloaded on every request, suspensions take effect immediately even for valid tokens.
requireAdmin
Requires req.user.role === 'ADMIN', otherwise 403.
Trading eligibility
requireTradingEligibility (posting orders, opening trades):
| Check | Error |
|---|---|
emailVerified | 403 EMAIL_NOT_VERIFIED |
kycStatus === 'VERIFIED' (when REQUIRE_KYC=true) | 403 KYC_REQUIRED |
requireVerifiedEmail guards withdrawals.
Passwords
- bcrypt with 12 rounds
- Login compares against a dummy hash when the email doesn't exist, keeping response timing similar
- Failed logins always return
401 INVALID_CREDENTIALSwithout revealing which field was wrong
Email and reset tokens
- 32 random bytes, hex-encoded (64 characters)
- Only the SHA-256 hash is stored
- Verification tokens are single-use; reset tokens expire after 1 hour
forgot-passwordresponds identically for unknown emails
Client
The Zustand authStore persists { token, user } to localStorage under nexlm-auth. Any 401 response while a token is present logs the user out.