Skip to content

feat: superadmin powers, ownership, runtime settings, API keys, rate-limiting (auth PR B) - #19

Merged
deckyfx merged 2 commits into
mainfrom
feat/auth-superadmin
Jul 1, 2026
Merged

feat: superadmin powers, ownership, runtime settings, API keys, rate-limiting (auth PR B)#19
deckyfx merged 2 commits into
mainfrom
feat/auth-superadmin

Conversation

@deckyfx

@deckyfx deckyfx commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

PR B of the auth milestone — the superadmin half. PR A (#18) gave accounts + sessions + gated writes; this makes role powers actually do something, makes config admin-editable at runtime, gives non-browser clients a durable credential, and throttles the credential endpoints. It also folds in the small home-page account-links gap from PR A. (Delivered as one PR by request.)

What's in it

Ownership (owner-or-admin)

  • PATCH /assets/:id and PATCH /assets/:id/tags now require the uploader or an admin (was any authenticated user). Resumable upload-session ops (HEAD/PATCH/DELETE /uploads/:token) enforce ownership inside the service using the already-loaded session (zero extra queries), and begin now derives the owner from the authenticated user (the service is the source of truth, not a caller-supplied field).
  • PATCH /tags/:name (set a tag's category) is admin-only (canModerate).

Runtime settings (upload caps)

  • New settings KV table + SettingsService: env values seed the defaults, DB rows override them, cached in-process (single-instance). GET/PATCH /settings (admin) edit the one-shot + resumable caps; the upload routes read caps from settings (and the resumable route now correctly guards on the resumable cap). A one-shot cap above the request-body ceiling → 400 (ValidationError).

API keys

  • New api_keys table, folded into authService (Core owns Auth). bnb_<hex> tokens, sha256-hashed like sessions, full account powers, no expiry. currentUser dispatches by prefix (bnb_ → key, else session). CRUD under /account/api-keys (raw key shown once); revoke is owner-scoped. The hash never leaves the repository (ApiKeySummary).

Rate-limiting

  • In-memory per-IP fixed-window limiter on POST /auth/login + /register429. Single-instance deployment, so no Redis. IP keying uses the socket address by default; X-Forwarded-For is trusted only when TRUST_PROXY=true.

Web

  • Extracted AccountLinks into a shared component (fixes the home-page dead Login/Sign-up links), role-gated Admin nav, /admin (edit caps + set tag category) and /account (manage API keys) pages.

Schema (migration 0009)

settings (key PK, value, updated_by FK→users set null, updated_at) and api_keys (token_hash unique, user_id FK→users cascade, name, last_used_at, created_at, index on user_id).

Testing

  • typecheck ✅, lint:boundaries ✅ (no new edges), 257 tests pass, migration 0009 applies.
  • DB-integration (settings + api-key repos, FK behaviours), core-unit (settings-service cache/validation, api-key create/resolve/revoke, upload ownership), API-route (403/401/404 gating, settings, api-keys incl. Bearer-via-key, 429), plus an in-process real-Postgres e2e (admin bootstrap → API-key auth → gating → settings update → 429).

Review

A pre-PR CodeRabbit CLI pass surfaced 10 findings; 9 are folded into this commit — cache-invalidation on partial settings write, ApiKeySummary (keep tokenHash repo-internal), owner-derived-in-begin, runtime-cap validation, TRUST_PROXY-gated XFF, hard rate-limit key cap, deterministic key ordering, whitespace-name rejection, and seed-once admin form. The 10th (settings updated_at "insert-only") is a non-issue — the repository sets updated_at on every upsert.

Deferred (future)

Per-key scopes/expiry; cookie-only web login now that API keys exist; GC-interval / session-expiry as runtime settings; Redis-backed rate-limiting if the app ever scales past one instance.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added account and admin pages for managing API keys, upload limits, and tag categories.
    • Signed-in users can create, list, and revoke API keys from the account page.
    • Admins can update upload caps and tag categories from the admin interface.
    • Upload limits are now editable at runtime and applied to upload/session constraints.
  • Bug Fixes
    • Improved upload ownership enforcement so only the owner (or an admin) can modify/cancel uploads and related operations.
    • Added client-IP rate limiting for login and registration.
    • Validation errors now consistently return HTTP 400.
  • Documentation
    • Updated the environment example with guidance for TRUST_PROXY.

…limiting

PR B of the auth milestone — the superadmin half. Role powers now do something,
config is admin-editable at runtime, non-browser clients get durable credentials,
and the credential endpoints are throttled. Also folds in the home-page
account-links gap from PR A.

Ownership (owner-or-admin):
- PATCH /assets/:id and PATCH /assets/:id/tags now require the uploader or an
  admin (was any authenticated user). Resumable upload-session ops
  (HEAD/PATCH/DELETE /uploads/:token) enforce ownership inside the service using
  the already-loaded session (zero extra queries). PATCH /tags/:name (set a tag's
  category) is admin-only.

Runtime settings (upload caps):
- New `settings` KV table + SettingsService: env values seed the defaults, DB
  rows override them, cached in-process (single-instance). GET/PATCH /settings
  (admin) edit the one-shot + resumable caps; the upload routes read the caps
  from settings, and the resumable route now correctly guards on the resumable
  cap. `ValidationError` (→ 400) rejects a one-shot cap above the request-body
  ceiling.

API keys:
- New `api_keys` table, folded into authService (Core owns Auth). `bnb_<hex>`
  tokens, sha256-hashed like sessions, full account powers, no expiry.
  `currentUser` dispatches by prefix (`bnb_` → key, else session). CRUD under
  /account/api-keys (key shown once); revoke is owner-scoped.

Rate-limiting:
- In-memory per-IP fixed-window limiter on POST /auth/login + /register (429).
  Single-instance deployment, so no Redis needed.

Web:
- Extracted AccountLinks into a shared component (fixes the home-page dead
  Login/Sign-up links), role-gated Admin nav, /admin (edit caps + set tag
  category) and /account (manage API keys) pages.

Covered by db-integration (settings + api-key repos, FK behaviours), core-unit
(settings-service, api-key create/resolve/revoke, upload ownership), and
API-route tests (403/401/404 gating, settings, api-keys incl. Bearer-via-key,
429), plus an in-process Postgres e2e. Migration 0009 adds both tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9736ac7-d939-49a1-9452-529d3037f282

📥 Commits

Reviewing files that changed from the base of the PR and between 1536833 and 8682214.

📒 Files selected for processing (8)
  • apps/api/src/lib/rate-limit.ts
  • apps/web/src/lib/api-keys.ts
  • apps/web/src/routes/admin.tsx
  • apps/web/src/routes/home.tsx
  • packages/core/src/services/settings-service.ts
  • packages/core/test/settings-service.test.ts
  • packages/db/src/repositories/settings-repository.ts
  • packages/db/test/settings-api-key-repositories.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/core/test/settings-service.test.ts
  • apps/web/src/lib/api-keys.ts
  • apps/api/src/lib/rate-limit.ts
  • packages/core/src/services/settings-service.ts
  • apps/web/src/routes/admin.tsx
  • packages/db/test/settings-api-key-repositories.test.ts

📝 Walkthrough

Walkthrough

This PR adds runtime-editable upload limits, API-key authentication, ownership checks for uploads/assets/tags, IP-based auth rate limiting, new settings/API-key persistence, and matching web admin/account pages.

Changes

Settings, API keys, upload authorization feature

Layer / File(s) Summary
TRUST_PROXY and rate limiting
.env.example, apps/api/src/env-config.ts, apps/api/src/lib/rate-limit.ts, apps/api/src/lib/http.ts
Adds TRUST_PROXY, client IP resolution, an in-memory fixed-window rate limiter, and ValidationError → 400 mapping.
Database schema and repositories
packages/db/src/schema.ts, packages/db/src/repositories/settings-repository.ts, packages/db/src/repositories/api-key-repository.ts, packages/db/src/index.ts, packages/db/drizzle/*, packages/db/test/settings-api-key-repositories.test.ts
Adds settings and api_keys tables, repository accessors, migration metadata, and integration tests.
Core settings, API keys, and upload services
packages/core/src/services/settings-service.ts, packages/core/src/services/auth-service.ts, packages/core/src/services/upload-service.ts, packages/core/src/core.ts, packages/core/src/errors.ts, packages/core/src/index.ts, packages/core/test/*.test.ts, scripts/seed.ts
Adds cached settings reads/updates, API-key mint/list/revoke/auth support, user-aware upload ownership checks, Core limit wiring, and related tests/exports.
API routes and request handling
apps/api/src/index.ts, apps/api/src/server.ts, apps/api/test/server.test.ts
Wires runtime upload limits, ownership-gated asset/tag routes, settings and API-key endpoints, and rate-limited auth endpoints.
Web admin and account UI
apps/web/src/components/account-links.tsx, apps/web/src/lib/api-keys.ts, apps/web/src/lib/settings.ts, apps/web/src/lib/tags.ts, apps/web/src/routes/account.tsx, apps/web/src/routes/admin.tsx, apps/web/src/router.tsx, apps/web/src/routes/__root.tsx, apps/web/src/routes/home.tsx
Adds account/admin hooks and pages, header links, and route registration for account and admin screens.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • deckyfx/bunbooru#2: Extends the same apps/api/src/lib/http.ts status-mapping logic with ValidationError handling.
  • deckyfx/bunbooru#13: Modifies the resumable upload flow that this PR further updates with user-aware ownership checks and runtime limits.
  • deckyfx/bunbooru#15: Touches the same Core upload-size limit wiring that this PR refactors into CoreLimits.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: superadmin, ownership, runtime settings, API keys, and auth rate limiting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth-superadmin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (1)
packages/core/test/settings-service.test.ts (1)

50-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add one test for the partial-write cache reset branch.

createSettingsService.updateUploadLimits() explicitly drops its cache when one repo.set() lands and a later write throws. This suite covers the happy path, but not that recovery branch, so a regression there could leave stale upload limits in memory after a failed admin update.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/settings-service.test.ts` around lines 50 - 58, The
updateUploadLimits flow in createSettingsService needs coverage for the
partial-write failure path where one repo.set succeeds and a later write throws,
causing the cache to be cleared. Add a test alongside the existing
updateUploadLimits cases that simulates the second write failing after the first
succeeds, then verifies the service resets its cached upload limits by
re-reading from the repo on the next getUploadLimits call and does not retain
stale values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/lib/rate-limit.ts`:
- Around line 67-72: The clientIp logic in rate-limit.ts should not trust the
first X-Forwarded-For token when TRUST_PROXY is enabled, since that allows
spoofed multi-hop chains. Update clientIp to only accept a sanitized single-hop
XFF value or to derive the client IP from a trusted proxy hop, and explicitly
reject any multi-hop header before falling back to the server/IP resolver path.

In `@apps/web/src/lib/api-keys.ts`:
- Around line 16-23: The `useApiKeys()` cache is currently shared across users
because `API_KEYS_KEY` is only `["api-keys"]`, so stale key names can show after
logout/login. Update the query key in `useApiKeys` to include the authenticated
user identity (for example the current user id) or clear this cache on auth
changes. Keep existing prefix invalidations like `invalidateQueries({ queryKey:
["api-keys"] })` working by preserving `["api-keys", ...]` as the base key
shape.

In `@apps/web/src/routes/admin.tsx`:
- Around line 104-106: The admin upload hint in the JSX span renders stray
literal backticks because `POST /assets` is written as plain text in
`admin.tsx`. Update that inline text in the relevant `<span>` so the route label
is rendered without backticks, matching the neighboring resumable-cap hint
formatting and keeping the UI text consistent.
- Around line 73-82: The onSubmit handler in admin.tsx currently calls
update.mutate(patch) even when both maxUpload and maxResumable are invalid,
leaving patch empty and causing a false “Saved.” state. Update onSubmit to
validate the assembled patch before mutating, and return early (or otherwise
prevent submission) when neither maxUploadBytes nor maxResumableUploadBytes was
added. Use the existing onSubmit, update.isPending, and update.mutate flow to
keep the fix localized.

In `@apps/web/src/routes/home.tsx`:
- Around line 76-79: Make the divider conditional with AccountLinks in the home
page header so the stray separator does not render while /auth/me is still
loading. Update the home route JSX around AccountLinks and ThemeSwitcher to
either render the divider only when AccountLinks has content, or move the
separator into AccountLinks so both stay in the same auth-gated branch.

In `@packages/core/src/services/settings-service.ts`:
- Around line 74-110: The updateUploadLimits flow in SettingsService is
publishing an optimistic in-memory cache after writing to repo, which can leave
stale field values when concurrent PATCHes touch different upload limit keys.
After the awaited repo.set calls succeed, reload the authoritative UploadLimits
from storage (or otherwise refresh via currentLimits/repo) and assign that
result to cache before returning. Keep the existing validation and partial-write
cache invalidation behavior intact, and update the updateUploadLimits method so
cache always reflects the persisted state.

In `@packages/db/src/repositories/settings-repository.ts`:
- Around line 9-13: The settings update flow is doing two separate writes
through SettingsRepository, so a failure in the second write can leave only part
of the /settings PATCH persisted. Extend SettingsRepository with a transactional
batch-update method that can apply multiple key/value changes atomically, then
update SettingsService to use that new path instead of calling set for
maxUploadBytes and maxResumableUploadBytes separately. Keep the existing
single-key API if needed, but route the multi-setting update logic through the
new batch method so both values are committed or rolled back together.

In `@packages/db/test/settings-api-key-repositories.test.ts`:
- Around line 77-78: The test in apiKeys.listByUser is masking the repository’s
newest-first ordering contract by sorting the results before asserting. Update
the assertion to check the returned sequence directly on aliceKeys, using the
existing aliceKeys.map((k) => k.name) result, so it verifies the documented
order for these inserts is ["two", "one"].

In `@scripts/seed.ts`:
- Around line 123-126: The seed storage fallback in createCore(...) is still
pointing at a different default directory than the runtime config, so seeded
files end up outside what the API reads. Update the storage-root fallback used
in scripts/seed.ts to match the runtime default of <cwd>/storage, and keep the
change localized around the createCore configuration so the seed path resolution
stays consistent with the app’s env config.

---

Nitpick comments:
In `@packages/core/test/settings-service.test.ts`:
- Around line 50-58: The updateUploadLimits flow in createSettingsService needs
coverage for the partial-write failure path where one repo.set succeeds and a
later write throws, causing the cache to be cleared. Add a test alongside the
existing updateUploadLimits cases that simulates the second write failing after
the first succeeds, then verifies the service resets its cached upload limits by
re-reading from the repo on the next getUploadLimits call and does not retain
stale values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9601cae-983a-4a28-8413-eff7fc32f055

📥 Commits

Reviewing files that changed from the base of the PR and between 38b4bd3 and 1536833.

📒 Files selected for processing (34)
  • .env.example
  • apps/api/src/env-config.ts
  • apps/api/src/index.ts
  • apps/api/src/lib/http.ts
  • apps/api/src/lib/rate-limit.ts
  • apps/api/src/server.ts
  • apps/api/test/server.test.ts
  • apps/web/src/components/account-links.tsx
  • apps/web/src/lib/api-keys.ts
  • apps/web/src/lib/settings.ts
  • apps/web/src/lib/tags.ts
  • apps/web/src/router.tsx
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/account.tsx
  • apps/web/src/routes/admin.tsx
  • apps/web/src/routes/home.tsx
  • packages/core/src/core.ts
  • packages/core/src/errors.ts
  • packages/core/src/index.ts
  • packages/core/src/services/auth-service.ts
  • packages/core/src/services/settings-service.ts
  • packages/core/src/services/upload-service.ts
  • packages/core/test/auth-service.test.ts
  • packages/core/test/settings-service.test.ts
  • packages/core/test/upload-service.test.ts
  • packages/db/drizzle/0009_settings_and_api_keys.sql
  • packages/db/drizzle/meta/0009_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/index.ts
  • packages/db/src/repositories/api-key-repository.ts
  • packages/db/src/repositories/settings-repository.ts
  • packages/db/src/schema.ts
  • packages/db/test/settings-api-key-repositories.test.ts
  • scripts/seed.ts

Comment thread apps/api/src/lib/rate-limit.ts
Comment thread apps/web/src/lib/api-keys.ts
Comment thread apps/web/src/routes/admin.tsx
Comment thread apps/web/src/routes/admin.tsx
Comment thread apps/web/src/routes/home.tsx
Comment thread packages/core/src/services/settings-service.ts Outdated
Comment thread packages/db/src/repositories/settings-repository.ts Outdated
Comment thread packages/db/test/settings-api-key-repositories.test.ts Outdated
Comment thread scripts/seed.ts
Comment on lines +123 to +126
// so these caps are effectively unused here; keep them generous.
maxUploadBytes: 100 * 1024 * 1024,
maxResumableUploadBytes: 100 * 1024 * 1024,
requestBodyCeilingBytes: 2 * 1024 * 1024 * 1024,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the seed storage fallback with the runtime default.

While touching this createCore(...) config, Line 121 still falls back to <cwd>/data/storage. The app runtime uses <cwd>/storage, so seeded files land in a different directory than the API reads from.

🔧 Suggested fix
-  storageRoot: Bun.env.STORAGE_ROOT?.trim() || resolve(process.cwd(), "data/storage"),
+  storageRoot: Bun.env.STORAGE_ROOT?.trim() || resolve(process.cwd(), "storage"),

Based on learnings, scripts/seed.ts should use the same storage-root fallback as the runtime env config (<cwd>/storage), not data/storage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/seed.ts` around lines 123 - 126, The seed storage fallback in
createCore(...) is still pointing at a different default directory than the
runtime config, so seeded files end up outside what the API reads. Update the
storage-root fallback used in scripts/seed.ts to match the runtime default of
<cwd>/storage, and keep the change localized around the createCore configuration
so the seed path resolution stays consistent with the app’s env config.

Source: Learnings

- settings: make multi-key updates atomic — SettingsRepository.set → setMany
  (one transaction), and the service reloads the authoritative DB state after a
  write instead of publishing its optimistic snapshot, so concurrent admin PATCHes
  to different keys can't leave the cache half-stale.
- rate-limit: only trust a SINGLE-hop X-Forwarded-For under TRUST_PROXY; a
  multi-hop chain (possible spoof / extra proxy) falls back to the socket IP.
- web: scope the API-key query cache by user id (no cross-account leak across a
  logout/login); gate the home-page divider on auth resolution (no lone "·");
  guard the admin form against an empty patch; drop stray literal backticks.
- tests: assert the api-key newest-first order directly (not sorted); update the
  settings repo/service tests for setMany.

Not changed: the seed storage fallback already matches the runtime default
(both `resolve(cwd, "data/storage")`), so that finding was a false positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@deckyfx

deckyfx commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in 8682214. 8 of 9 findings fixed:

  • Atomic settings updatesSettingsRepository.setsetMany (one transaction), and the service now reloads the authoritative DB state after a write instead of publishing an optimistic snapshot. This fixes both the partial-write and concurrent-admin-PATCH cases (the two settings findings + the earlier partial-write concern).
  • Rate limiter — under TRUST_PROXY, only a single-hop X-Forwarded-For is trusted; a multi-hop chain (extra proxy or a prepended spoof) falls back to the socket IP.
  • Web — API-key query cache scoped by user id (no cross-account leak across logout/login); home-page divider gated on auth resolution (no lone ·); admin form guards an empty patch; stray literal backticks removed.
  • Tests — assert the api-key newest-first order directly.

Not changed (false positive): the seed storage fallback already matches the runtime default — both are resolve(cwd, "data/storage") (env-config.ts:78 and seed.ts:121), so seeded files land where the API reads them. The review's "runtime uses <cwd>/storage" was inaccurate.

typecheck + lint:boundaries clean, 257 tests pass.

@deckyfx

deckyfx commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@deckyfx
deckyfx merged commit 7925d3d into main Jul 1, 2026
2 checks passed
@deckyfx
deckyfx deleted the feat/auth-superadmin branch July 1, 2026 14:55
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