Skip to content

feat: add API-key authenticated Admin API for users and time logs#63

Merged
kennhung merged 2 commits into
mainfrom
feat/admin-api
Jul 4, 2026
Merged

feat: add API-key authenticated Admin API for users and time logs#63
kennhung merged 2 commits into
mainfrom
feat/admin-api

Conversation

@kennhung

@kennhung kennhung commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

  • New REST endpoints under /api/admin/users and /api/admin/timelogs (GET/POST/PUT/DELETE) for external systems to manage users and time logs
  • Authenticated via a static Bearer API key (ADMIN_API_KEY env var), checked with a constant-time comparison; returns 503 if unconfigured, 401 if invalid
  • New admin-scoped data functions (adminGet/Create/Update/DeleteUser, adminGet/Create/Update/DeleteTimeLog) alongside existing session-based DTO functions, since the API key auth path has no NextAuth session
  • OpenAPI 3.0 schema at public/openapi/admin-api.json documenting all endpoints, schemas, and the bearer auth scheme
  • README section documenting usage

Test plan

  • npx tsc --noEmit and npm run build pass
  • Manually verified against dev DB via curl:
    • No/invalid token → 401; ADMIN_API_KEY unset → 503
    • POST/GET/PUT/DELETE /api/admin/users and /api/admin/users/{id} — create, fetch, update, delete, 404 on missing id
    • POST/GET/PUT/DELETE /api/admin/timelogs and /api/admin/timelogs/{id} — including validation error (DONE without outTime → 400) and query filters
  • Validated public/openapi/admin-api.json is well-formed JSON

Note: found a pre-existing gap where the dev MongoDB doesn't enforce the email @unique index (duplicate emails succeed) — predates this change and also affects the existing UI create flow; out of scope here.

🤖 Generated with Claude Code

Exposes REST endpoints under /api/admin/{users,timelogs} for external
systems to manage users and time logs, secured via a static Bearer
API key (ADMIN_API_KEY), plus an OpenAPI 3.0 schema at
/openapi/admin-api.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kennhung

kennhung commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Review

The PR adds a new API-key-authenticated REST API (/api/admin/users, /api/admin/timelogs) with full CRUD, a matching OpenAPI schema, and admin-scoped Prisma DTO functions that bypass the existing NextAuth session checks (gated instead by a constant-time Bearer key comparison). Auth handling itself (verifyAdminApiKey) looks solid. Two issues surfaced during review, both around unhandled edge cases now reachable by untrusted external callers:

1. Malformed id/userId crashes with an unhandled 500 instead of 400/404

src/app/api/admin/users/[id]/route.ts and src/app/api/admin/timelogs/[id]/route.ts (all GET/PUT/DELETE), plus the userId body field in the timelog POST/PUT.

  • User.id, TimeLog.id, and TimeLog.userId are @db.ObjectId (prisma/schema.prisma:12,33,38). None of the new routes or admin DTO functions (adminGetUser/adminUpdateUser/adminDeleteUser in user-dto.ts, adminGetTimeLog/adminUpdateTimeLog/adminDeleteTimeLog/adminCreateTimeLog in timelog-dto.ts) validate the id's shape before handing it to Prisma. The zod schemas only check nonempty() for userId, not ObjectId format.
  • The catch blocks only special-case Prisma error codes P2025 (not found) and P2002 (unique conflict); every other error — including the P2023 "Malformed ObjectID" error Prisma's Mongo connector throws for a non-24-hex-char id — falls through to throw e and becomes an uncaught 500.
  • Failure scenario: GET /api/admin/users/abc123 (a typo'd or wrong-format id) returns an unhandled 500 instead of 400/404. This mirrors a pre-existing gap in the session-based routes, but the PR newly exposes it to arbitrary external API input rather than internally-generated session ids, making it much easier to trigger in practice.

2. Stale outTime/notes survive a status transition away from DONE/LOCKED

src/lib/data/timelog-dto.ts:995-1024 (adminUpdateTimeLog, used by PUT /api/admin/timelogs/{id}).

  • The update always does outTime: data.outTime, where data.outTime is undefined if the caller omits it. Prisma treats an undefined field value as "leave unchanged," not "clear it" — only an explicit null unsets a field.
  • Failure scenario: a time log is DONE with outTime set. An admin PUTs { userId, status: "CURRENTLY_IN", inTime } (no outTime, since the schema only requires it for DONE/LOCKED) intending to reopen the log. The record ends up with status: CURRENTLY_IN but a stale, non-null outTime from the prior state — an inconsistent combination nothing else in the codebase produces.

No cross-file breakage, dead code, or CLAUDE.md violations found. Minor sub-threshold note: toPrismaTimeLogStatus duplicates status-mapping logic already inlined in getAllTimelogDTOs (lines 108-116) — pre-existing style, not introduced as shared, low severity.

Malformed ids/userIds previously reached Prisma directly and crashed
with an unhandled 500 (Mongo P2023); now validated with a shared
ObjectId zod schema and mapped to a clean 400. Also fix
adminUpdateTimeLog to null out omitted outTime/notes instead of
leaving Prisma's stale values in place when a PUT transitions a log
away from DONE/LOCKED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kennhung

kennhung commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Addressed both review findings in b73dadb:

  1. Added a shared objectIdSchema (src/lib/objectid.ts) and validated it on all [id] path params (users, timelogs) and on the userId field in timelog create/update/list — malformed ids now return a clean 400 instead of an unhandled 500. Also added P2023 handling in the route catch blocks as defense-in-depth.
  2. Fixed adminUpdateTimeLog to explicitly null out omitted outTime/notes instead of passing undefined through to Prisma, so PUT correctly clears them on a full-resource replacement (e.g. reopening a DONE log back to CURRENTLY_IN no longer leaves a stale outTime).

Verified manually against the dev DB: malformed ids on all affected routes now 400, and the reopen-to-CURRENTLY_IN scenario now clears outTime/notes as expected. No regressions in the original happy-path CRUD checks.

@kennhung kennhung merged commit d19d2cf into main Jul 4, 2026
6 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.

1 participant