Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ff5164c
Apply security hardening across authentication, access control, and o…
claude Jun 27, 2026
3266767
Address PR review comments: shared password policy and admin role bac…
claude Jun 27, 2026
8349cad
Fix ESLint errors: import Buffer from node:buffer, fix import order i…
claude Jun 27, 2026
b48157e
Fix import order in users.test.ts: type-internal before value-external
claude Jun 27, 2026
574adbf
Fix test passwords to satisfy new complexity requirement
claude Jun 27, 2026
0a0d1fb
Fix test failures from security changes
claude Jun 27, 2026
f64a882
Fix 4 remaining security issues from PR review
github-actions[bot] Jun 27, 2026
b00f4ab
Fix three remaining timing and permissions issues
claude Jun 27, 2026
5704ba8
Merge pull request #29 from rujealfon/main
rujealfon Jun 27, 2026
2e83eb5
Remove redundant bcrypt.compare in new-user registration path
claude Jun 27, 2026
e83dbdc
Rename migration 0009 to reflect its no-op intent
claude Jun 27, 2026
ce98bfd
Merge pull request #30 from rujealfon/claude/auth-timing-oracle-fix-t…
rujealfon Jun 27, 2026
fc05f20
Fix comma expression in loginUser and sync RouteSchema with RouteMap
claude Jun 27, 2026
eb4aeca
Merge pull request #31 from rujealfon/claude/auth-timing-oracle-fix-t…
rujealfon Jun 27, 2026
617fffb
Fix MOBILE_API_KEY empty-string bypass and add security test coverage
claude Jun 27, 2026
87e988c
Fix formatting of RBAC description in products test
rujealfon Jun 27, 2026
53971ab
Add MOBILE_API_KEY production fast-fail and gate /health/details by p…
claude Jun 27, 2026
8987226
Fix casing in HTTP method descriptions for health API tests
rujealfon Jun 27, 2026
48ccd0d
Refactor health API to include memory usage metrics and update tests …
rujealfon Jun 28, 2026
252c6ef
Update environment configuration and documentation for rate limiting …
rujealfon Jun 28, 2026
346c2c8
Refactor user role assignment in registerUser function to use transac…
rujealfon Jun 28, 2026
a2e0e0a
Merge branch 'main' into claude/security-review-codebase-lm1m7b
rujealfon Jun 28, 2026
dd9b33b
fix: remove duplicate registration of rateLimitPlugin in app setup
rujealfon Jun 29, 2026
d71c79b
feat: add TRUST_PROXY configuration and update related documentation
rujealfon Jun 29, 2026
7a9a29c
fix: improve .env value parsing and update proxy test setup
rujealfon Jun 29, 2026
e08d535
feat: add CORS_ORIGIN and TRUST_PROXY environment variables to docker…
rujealfon Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
PORT=3000
HOST=0.0.0.0
# Rate limiting is enabled only when NODE_ENV=production.
NODE_ENV=development
DATABASE_URL=postgres://postgres:password@localhost:5432/fastify_dev
JWT_SECRET=change_this_to_a_random_string_at_least_32_characters
Expand All @@ -11,3 +12,9 @@ OTEL_ENDPOINT=
LOG_LEVEL=info
# Required — shared secret for mobile clients; generate with: openssl rand -hex 32
MOBILE_API_KEY=change_this_to_a_random_string_at_least_32_characters
# Required in production — comma-separated list of allowed CORS origins
# Example: CORS_ORIGIN=https://app.example.com,https://admin.example.com
CORS_ORIGIN=
# Set in production behind a reverse proxy so request.ip uses the real client IP.
# Examples: TRUST_PROXY=127.0.0.1, TRUST_PROXY=10.0.0.0/8, TRUST_PROXY=1
TRUST_PROXY=
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,11 @@ cp .env.example .env
| `TEST_DATABASE_URL` | | *(empty)* | PostgreSQL connection string used by test runs |
| `PORT` | | `3000` | Server port |
| `HOST` | | `0.0.0.0` | Server host |
| `NODE_ENV` | | `development` | `development` \| `production` \| `test` |
| `NODE_ENV` | | `development` | `development` \| `production` \| `test`. Rate limiting is enabled only in production. |
| `LOG_LEVEL` | | `info` | Pino log level |
| `COOKIE_SECRET` | | *(JWT_SECRET)* | Secret for signed cookies — falls back to `JWT_SECRET` if empty |
| `OTEL_ENDPOINT` | | *(disabled)* | OTLP HTTP endpoint (e.g. `http://localhost:4318/v1/traces`). Leave empty to disable tracing. |
| `TRUST_PROXY` | | *(disabled)* | Fastify trusted proxy setting for production client IPs, e.g. `127.0.0.1`, `10.0.0.0/8`, or `1` trusted hop |

## API Documentation

Expand Down Expand Up @@ -128,6 +129,8 @@ All scripts run via `nub` (or `nubx` inside containers). See [package.json](pack
| POST | `/api/v1/auth/mobile/login` | `x-mobile-api-key` | Login for mobile — returns `token` in body, no cookie |
| POST | `/api/v1/auth/logout` | — | Logout — clears the `token` cookie |

Auth entry points (`register`, `login`, and `mobile/login`) are rate-limited to 5 requests per 15 minutes per client key in production. Development and test runs skip rate limiting so local API clients and integration tests are not throttled.

### Users

| Method | Path | Permission | Description |
Expand Down Expand Up @@ -193,6 +196,10 @@ Each log entry records `action`, `resource_type`, `resource_id`, `metadata`, and

> **Production note:** `/metrics` should be restricted at the network/gateway level and not exposed publicly.

### Deployment Notes

In production, rate-limit keys prefer the leftmost `X-Forwarded-For` entry so clients behind a reverse proxy are keyed by their originating IP. Only allow trusted proxies or gateways to set or forward `X-Forwarded-For`; if clients can send that header directly to the app, they can spoof different IPs and evade per-IP limits. Deployments should strip inbound forwarding headers at the edge and re-add them from the trusted proxy layer.

### Pagination

All list endpoints accept `?page=1&limit=10` query parameters.
Expand Down Expand Up @@ -361,7 +368,7 @@ Errors from the server surface as `RpcError` (with `.status` and `.data`) on the
- **Zod is the single source of truth** for types — no manual interfaces. All types are derived via `z.infer<>` from schemas in each module's `schemas/index.ts`.
- **Services have no Fastify imports** — they receive `db` as a parameter, making them independently testable.
- **Error handling** is centralized in `app.ts` via `setErrorHandler`. All domain errors extend `AppError`.
- **Rate limiting** uses Valkey as the store — safe for multi-instance / horizontally scaled deployments.
- **Rate limiting** uses Valkey as the store — safe for multi-instance / horizontally scaled deployments. It is active only when `NODE_ENV=production`; development and test runs skip it.
- **Request context** (`@fastify/request-context`) stores `requestId`, `userId`, `permissions`, and `isSuperAdmin` via AsyncLocalStorage, accessible anywhere in the call stack without passing them explicitly.
- **Audit logging** is fire-and-forget (`logAudit` in `src/modules/audit-logs/helpers/`) — inserts never block the request path. Failures are silently swallowed so a logging error never surfaces to the caller.
- **Graceful shutdown** is handled in `server.ts` — `SIGINT`/`SIGTERM` closes Fastify (draining connections) and flushes OpenTelemetry spans before exiting.
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ services:
OTEL_ENDPOINT: ''
LOG_LEVEL: info
MOBILE_API_KEY: change_this_to_a_random_string_at_least_32_characters
CORS_ORIGIN: ''
TRUST_PROXY: ''
ports:
- '3000:3000'
volumes:
Expand Down
1 change: 1 addition & 0 deletions migrations/0008_audit_logs_user_id_idx.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs" ("user_id");
35 changes: 35 additions & 0 deletions migrations/0009_noop_admin_role_not_system.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
INSERT INTO "permissions" ("id", "resource", "action", "scope")
VALUES
(gen_random_uuid(), 'product', 'read', 'any'),
(gen_random_uuid(), 'product', 'create', 'any'),
(gen_random_uuid(), 'product', 'update', 'any'),
(gen_random_uuid(), 'product', 'delete', 'any'),
(gen_random_uuid(), 'metrics', 'read', 'any'),
(gen_random_uuid(), 'health', 'read', 'details')
ON CONFLICT ("resource", "action", "scope") DO NOTHING;
--> statement-breakpoint
INSERT INTO "role_permissions" ("role_id", "permission_id")
SELECT r."id", p."id"
FROM "roles" r
JOIN (
VALUES
('product', 'read', 'any'),
('product', 'create', 'any'),
('product', 'update', 'any'),
('product', 'delete', 'any'),
('metrics', 'read', 'any'),
('health', 'read', 'details')
) AS added_permissions("resource", "action", "scope")
ON true
JOIN "permissions" p ON (
p."resource" = added_permissions."resource"
AND p."action" = added_permissions."action"
AND p."scope" = added_permissions."scope"
)
WHERE r."name" = 'super-admin'
OR (r."name" = 'admin' AND (
added_permissions."resource" = 'product'
OR added_permissions."resource" = 'health'
))
OR (r."name" = 'user' AND added_permissions."resource" = 'product' AND added_permissions."action" = 'read')
ON CONFLICT DO NOTHING;
Loading
Loading