Files
halobestie-clone/backend/CLAUDE.md
ramadhan sjamsani 4a796277b8 Phase 3.4: control_center self-managed auth cutover
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>
2026-04-24 15:32:32 +08:00

2.6 KiB

Halo Bestie — Backend

Fastify.js REST API serving both mobile apps and the internal control center.

See root CLAUDE.md for 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 (commit f860ab6). firebase-admin is 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 via POST /api/shared/auth/refresh with the opaque refresh token in the body.
  • Control center: Access token in Authorization: Bearer (kept in memory by the SPA). Refresh token lives in an httpOnly Secure cookie; refresh calls POST /internal/auth/refresh with credentials: 'include'.
  • Entry points:
    • Anonymous customer: POST /api/shared/auth/anonymous
    • Phone OTP (customer/mitra): /api/{client,mitra}/auth/otp/{request,verify}Fazpass is stubbed in otp.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)
  • Middleware: authenticate plugin verifies the JWT and attaches request.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-admin is FCM-only; do not import .auth() from it.