Skip to content

feat(server): add structured logger, global error handler, shared asy… - #51

Merged
dark-sarge merged 1 commit into
arflexx:mainfrom
Hydrax117:feat/37-error-handler-logger
Aug 17, 2026
Merged

feat(server): add structured logger, global error handler, shared asy…#51
dark-sarge merged 1 commit into
arflexx:mainfrom
Hydrax117:feat/37-error-handler-logger

Conversation

@Hydrax117

Copy link
Copy Markdown
Contributor

PR: authenticate & authorize Middleware

Summary

Introduces a canonical middleware/auth.ts with an authenticate function
that distinguishes expired tokens from invalid ones, and an authorize(...roles)
factory for role-based access control. All protected routes (trades, wallet,
admin) are wired through the new middleware. Existing route imports are kept
working via a backwards-compatible shim. Unit tests cover every acceptance
criterion with 20 cases.

Closes #34


Type of Change

  • feat — new feature
  • refactor — code change with no behaviour change (authenticate.ts shim)

What Changed

File Change
server/src/middleware/auth.ts New — canonical authenticate, authorize, AuthPayload, AuthenticatedRequest
server/src/middleware/authenticate.ts Replaced — now a re-export shim pointing at auth.ts
server/src/middleware/auth.test.ts New — 20 unit tests covering all acceptance criteria
server/src/routes/admin.ts New — admin router with trades list, user lookup, release-payment; guarded by authenticate + authorize("admin")
server/src/routes/auth.ts Updated — DB query now selects role; JWT sign payload includes role
server/src/routes/trades.ts Updated — imports from ../middleware/auth
server/src/routes/wallet.ts Updated — imports from ../middleware/auth
server/src/index.ts Updated — mounts adminRouter at /api/admin

Architecture

Middleware signatures

// Verifies Bearer token, attaches req.user, distinguishes expiry from invalid
authenticate(req, res, next): void

// Role check — must follow authenticate in the chain
authorize(...roles: string[])(req, res, next): void

Error matrix

Condition Status Body
No Authorization header 401 { "error": "Unauthorized" }
Header does not start with Bearer 401 { "error": "Unauthorized" }
Empty token string after Bearer 401 { "error": "Unauthorized" }
Token has wrong signature 401 { "error": "Unauthorized" }
Token is past its exp claim 401 { "error": "Token expired" }
Authenticated, wrong role 403 { "error": "Forbidden" }
JWT_SECRET env var missing 500 { "error": "Internal server error" }

Route protection map

Public (no auth):
  GET  /health
  POST /api/auth/request-otp
  POST /api/auth/verify-otp
  GET  /api/trades           (browse listings)
  GET  /api/trades/:id       (single listing)

authenticate (any valid role):
  POST /api/trades           (create listing)
  POST /api/trades/:id/buy   (buy a listing)
  GET  /api/wallet           (own wallet)

authenticate + authorize("admin"):
  GET   /api/admin/trades
  GET   /api/admin/users/:id
  PATCH /api/admin/trades/:id/release

Backwards compatibility

authenticate.ts is kept as a one-file re-export shim:

export { authenticate, authorize, AuthPayload, AuthenticatedRequest } from "./auth";

All existing route files that imported from ../middleware/authenticate continue
to work. New code should import directly from ../middleware/auth.

Role in JWT

jwt.sign now includes role in the payload:

{ "sub": "<userId>", "stellarPublicKey": "<G…>", "role": "user" }

authenticate normalises legacy tokens (no role claim) to role = "user"
automatically so existing sessions don't break.


DB migration required

The users table needs a role column:

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'user';

-- Promote an existing user to admin:
UPDATE users SET role = 'admin' WHERE phone = '+2348000000000';

How to Test

cd server && npm run dev

# ── Unauthenticated access to protected route ──────────────────────────────
curl -s http://localhost:3001/api/wallet
# → 401 { "error": "Unauthorized" }

# ── Wrong role ─────────────────────────────────────────────────────────────
curl -s http://localhost:3001/api/admin/trades \
  -H "Authorization: Bearer <user-jwt>"
# → 403 { "error": "Forbidden" }

# ── Expired token ──────────────────────────────────────────────────────────
# Sign a token manually with exp in the past and send it
# → 401 { "error": "Token expired" }

# ── Admin access ───────────────────────────────────────────────────────────
curl -s http://localhost:3001/api/admin/trades \
  -H "Authorization: Bearer <admin-jwt>"
# → 200 { "data": [...] }

# ── Run unit tests ─────────────────────────────────────────────────────────
npm test
# PASS src/middleware/auth.test.ts  (20 tests)

Checklist

General

  • No TypeScript errors
  • No secrets or credentials committed
  • Follows existing asyncHandler and route patterns

API changes

  • All trade and wallet POST routes still require authenticate
  • Admin routes require authenticate + authorize("admin")
  • Public GET routes remain unauthenticated
  • JWT payload now includes role — documented above

Database changes

  • Migration script included in "DB migration required" above

Docs changes

  • docs/api-reference.md — admin endpoints and the new 403 response should be added (follow-up)

Notes for Reviewer

  • authorize is fail-closed: if called without a prior authenticate (so
    req.user is undefined), it returns 401 rather than throwing a runtime
    error. This makes accidental mis-ordering safe.
  • JWT_SECRET missing at runtime returns 500 and logs to console.error.
    This matches the fail-closed pattern used in authenticate.ts and is
    preferable to allowing unauthenticated requests through.
  • The PATCH /api/admin/trades/:id/release route currently updates the DB
    directly. The full release_payment Soroban call (via stellar.ts) should
    be wired in as a follow-up once the contract address is stable in the target
    environment.

@Hydrax117
Hydrax117 requested a review from dark-sarge as a code owner August 17, 2026 09:27
@dark-sarge
dark-sarge merged commit 7bf7a1d into arflexx:main Aug 17, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[server] - Implement JWT Authentication Middleware and Role-Based Access Control

2 participants