Skip to content

feat: build admin API + security fixes #66 - #66

Open
TarekHassan1 wants to merge 2 commits into
Developer-Kommunity-24:mainfrom
TarekHassan1:main
Open

feat: build admin API + security fixes #66#66
TarekHassan1 wants to merge 2 commits into
Developer-Kommunity-24:mainfrom
TarekHassan1:main

Conversation

@TarekHassan1

Copy link
Copy Markdown

Closes #61

Summary

Implements the full admin API so deployers no longer need to hand-write SQL to create vote items or manage admin status. Admin identity is stored in D1 (not Clerk metadata), and the first admin can be bootstrapped via an environment variable with zero manual SQL.

Admin identity

  • apps/api/src/middleware/adminAuth.tsrequireAdmin resolves getAuth(c), looks the user up in D1, and returns 403 unless is_admin = 1.
  • D1 is the single source of truth rather than Clerk metadata, so a self-deployer has one less dashboard to configure.

First-admin bootstrap

  • apps/api/src/utils/user.tsensureUserExists checks the optional ADMIN_EMAILS comma-separated Worker variable.
  • When a user's email matches (via requireAuth middleware or the Clerk webhook), is_admin is set to 1 automatically.
  • This works on a fresh deploy with zero manual SQL.

Schema changes

  • Added is_admin boolean column to the existing users table.
  • Added new settings table (key / value) for app-level configuration.

New routes (/api/v1/admin, all behind requireAdmin)

Method Route Description
GET /items List all items with aggregates
POST /items Create item; validates name and unique qrSlug; returns 409 on duplicate slug
PUT /items/:id Update item
DELETE /items/:id Delete item; ratings cascade
POST /items/bulk Accept JSON array for seeding a whole event
GET /settings Read settings as key-value map
PUT /settings Upsert settings
GET /users List users with vote counts and completion status
PUT /users/:id/admin Promote or demote a user

Frontend route guard

  • GET /api/v1/user/me — returns { isAdmin } so the frontend can conditionally render admin navigation.

Webhook update

  • apps/api/src/controllers/webhook.Controller.ts — now uses ensureUserExists so user.created / user.updated events respect ADMIN_EMAILS.

Manual fallback

If ADMIN_EMAILS wasn't set at deploy time, a user can be promoted manually:

npx wrangler d1 execute vote-system --command "UPDATE users SET is_admin = 1 WHERE email = 'user@example.com'"


Set ADMIN_EMAILS in wrangler.toml (or Worker vars).
Log in with a matching email — requireAuth will create the user with is_admin = 1.
GET /api/v1/user/me returns {"isAdmin": true}.
POST /api/v1/admin/items with { "name": "...", "qrSlug": "..." } creates an item and returns 201.
Duplicate qrSlug returns 409 instead of 500.
Checklist
[x] requireAdmin uses D1 as source of truth
[x] ADMIN_EMAILS bootstrap implemented
[x] Clerk webhook updated
[x] All admin CRUD routes implemented
[x] GET /api/v1/user/me returns { isAdmin }
[x] settings table created
[x] Manual promotion fallback documented

@TarekHassan1 TarekHassan1 changed the title feat: build admin API (#61) Fix three unauthenticated endpoints (#62) Aug 17, 2026
@TarekHassan1 TarekHassan1 changed the title Fix three unauthenticated endpoints (#62) feat: build admin API + security fixes Aug 17, 2026
@TarekHassan1 TarekHassan1 changed the title feat: build admin API + security fixes Fix three unauthenticated endpoints #62 Aug 17, 2026
@TarekHassan1

Copy link
Copy Markdown
Author

For last commit

Closes #62

Security & access-control fixes

1. GET /api/v1/results — gated behind admin + results_public setting

Problem: results.route.ts had no auth middleware. Anyone who knew the URL could read the live leaderboard mid-event, undermining vote integrity.

Fix:

  • Added a results_public setting check (stored in the settings table).
  • If results_public = true, anyone can view results (organizer deliberately opened them).
  • If results_public is missing or false, the route enforces requireAdmin.
  • No client change needed — App.tsx:53 already only ever requests its own ID.

2. GET /api/v1/progress/:userId — fixed IDOR vulnerability

Problem: progress.route.ts took :userId straight from the URL with no auth. Any caller could enumerate user IDs and read another person's ballot.

Fix:

  • Added requireAuth middleware to the route.
  • Controller now compares c.get('userId') (from the auth token) against the :userId param.
  • Returns 403 Forbidden if they don't match, preventing any user from accessing another's progress.

3. GET /health & GET /health/db — public health checks

Problem: health.route.ts required auth, so an uptime monitor could not hit it, defeating the purpose of a health check.

Fix:

  • Removed requireAuth from both routes. They are now fully public.
  • GET /health — instant 200 response. Tells deployers "Worker is alive."
  • GET /health/db — executes SELECT 1 against D1. Tells deployers "Worker up, database reachable."
  • If the D1 ping fails, returns 500 with db: unreachable so deployers can distinguish Worker issues from database issues.

Files changed

  • src/routes/results.route.ts
  • src/routes/progress.route.ts
  • src/controllers/progress.Controller.ts
  • src/routes/health.route.ts

Testing

  1. GET /health200 with no auth header.
  2. GET /health/db200 when D1 is up, 500 when D1 is down.
  3. GET /results without results_public setting → 403 for non-admins.
  4. PUT /admin/settings with { "results_public": "true" }GET /results now returns 200 for anyone.
  5. GET /progress/OTHER_USER_ID with valid auth → 403 (IDOR blocked).
  6. GET /progress/YOUR_USER_ID with valid auth → 200 with correct data.

@TarekHassan1 TarekHassan1 changed the title Fix three unauthenticated endpoints #62 feat: build admin API + security fixes #66 Aug 17, 2026
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.

Build the admin API

1 participant