Replaces Firebase Auth with the new JWT + httpOnly-cookie refresh flow. Smoke-tested end-to-end via curl (login → /me → refresh rotation → logout). - Remove firebase dep + firebase.js - New token-bridge decouples api-client from AuthContext and de-dupes concurrent 401 refreshes - AuthContext: in-memory access token (useRef), bootstrap via /internal/auth/refresh, login/logout/refresh methods - api-client: withCredentials, Bearer attach, auto-retry once on 401 - LoginPage: handle INVALID_CREDENTIALS / ACCOUNT_LOCKED / VALIDATION_ERROR - Layout: self-service "Ganti password" form - UsersPage: initial password field on create + per-row admin-forced reset - .env / .env.example: drop VITE_FIREBASE_* vars - backend/CLAUDE.md + control_center/CLAUDE.md: describe new auth (were stale on Firebase) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2.6 KiB
2.6 KiB
Halo Bestie — Backend
Fastify.js REST API serving both mobile apps and the internal control center.
See root
CLAUDE.mdfor full project context and architectural decisions.
Stack
- Runtime: Node.js + Fastify.js
- Database: PostgreSQL via GCP Cloud SQL
- Auth: Self-managed JWT (HS256 access, 1h) + opaque refresh token (30d, rotated, bcrypt-hashed in
auth_sessions). Firebase Auth removed in Phase 3.4 (commitf860ab6).firebase-adminis kept but only for FCM messaging. - Payment: Xendit
- Infra: GCP Cloud Run
Two Listeners
Public (0.0.0.0:3000) → client_app + mitra_app routes
Internal (private IP:3001) → control_center routes only
Internal listener must never be exposed to the public internet.
Route Namespacing
/api/client/... → client app routes
/api/mitra/... → mitra app routes
/api/shared/... → shared routes (e.g. auth, refresh, logout, anonymous)
/internal/... → control center routes (internal listener only)
Auth Flow
- Mobile (client/mitra):
Authorization: Bearer <access_token>header. Access token is our own JWT (HS256,AUTH_JWT_SECRET), with claims{ sub, user_type, session_id }. Refresh viaPOST /api/shared/auth/refreshwith the opaque refresh token in the body. - Control center: Access token in
Authorization: Bearer(kept in memory by the SPA). Refresh token lives in anhttpOnlySecure cookie; refresh callsPOST /internal/auth/refreshwithcredentials: 'include'. - Entry points:
- Anonymous customer:
POST /api/shared/auth/anonymous - Phone OTP (customer/mitra):
/api/{client,mitra}/auth/otp/{request,verify}— Fazpass is stubbed inotp.service.js; code is logged to the backend console ([OTP STUB] phone=… code=…) until real API docs arrive. - Google/Apple:
/api/client/auth/{google,apple}(client_app only — creds pending) - CC login:
POST /internal/auth/login(email + bcrypt password)
- Anonymous customer:
- Middleware:
authenticateplugin verifies the JWT and attachesrequest.auth = { userType, userId, sessionId }. WebSocket handshake uses the same verification. No DB lookup on every request — the user ID is encoded in the token.
Key Conventions
- All routes must be authenticated unless explicitly marked public (auth + anonymous routes are the exceptions)
- Internal routes additionally require
request.auth.userType === 'cc_user' - Use Fastify plugins for shared middleware (auth, error handling, logging)
- Business logic lives in
services/— never directly in route handlers - Never reintroduce Firebase Auth.
firebase-adminis FCM-only; do not import.auth()from it.