4.1 KiB
phase, status, depth, files_reviewed, findings, reviewed_at
| phase | status | depth | files_reviewed | findings | reviewed_at | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 37 | issues_found | standard | 9 |
|
2026-04-19 |
Code Review: Phase 37 — Admin Global Item Management
Summary
9 files reviewed at standard depth. No critical issues. 3 warnings (real bugs / UX gaps worth fixing), 2 info notes.
Findings
WR-01 — Tags not populated from loaded item on edit page
Severity: warning
File: src/client/routes/admin/items.$itemId.tsx (line 130)
The useEffect that populates form state from the loaded item hardcodes tags: [] instead of pulling tags from item. Since AdminGlobalItemDetail extends Omit<AdminGlobalItem, "tags">, tags are not present on the detail endpoint response. However, this means a user saving the form immediately will send tags: [] to the PUT endpoint, which calls syncGlobalItemTags with an empty array — silently clearing all existing tags.
Fix options:
- Include tags in the
GET /api/admin/items/:idresponse (extendgetGlobalItemWithOwnerCountto also fetch tags). - Populate tags on the list page item click by passing them through navigation state, or fetch them separately on the edit page.
The safest fix is option 1: add a tag fetch to getGlobalItemWithOwnerCount and include them in AdminGlobalItemDetail.
WR-02 — Save always sends tags: form.tags (empty array clears tags)
Severity: warning
File: src/client/routes/admin/items.$itemId.tsx (line 166)
Related to WR-01. The handleSave function always includes tags: form.tags in the PUT payload, even when the user hasn't interacted with the tag field. Since form.tags starts as [], saving without touching tags will call updateGlobalItemById with tags: [], which triggers syncGlobalItemTags(tx, id, []) — deleting all existing tags silently.
Fix: Only include tags in the payload when the user has explicitly modified the tag field. Track a tagsModified boolean flag, or use undefined as the default form tags value and only set it to [] when the user clears the last tag.
WR-03 — limit query param negative values not rejected
Severity: warning
File: src/server/routes/admin-items.ts (line 48)
The limit guard is:
limit: isNaN(limit) || limit > 100 ? 50 : limit,
This passes through negative limit values (e.g., ?limit=-1) to listGlobalItemsForAdmin, which passes them directly to Drizzle's .limit(). SQLite treats LIMIT -1 as no limit — returning all rows. This is an admin-only endpoint so exploitation risk is low, but it could cause unintended large payloads.
Fix: Add limit <= 0 to the fallback condition:
limit: isNaN(limit) || limit <= 0 || limit > 100 ? 50 : limit,
INFO-01 — type Env = { Variables: { db?: any; userId?: number } } duplicated
Severity: info
Files: src/server/routes/admin-items.ts (line 12), src/server/routes/admin.ts (line 4)
Both new files define Env locally using any for db. This is consistent with the existing pattern in other route files (e.g., global-items.ts), but it suppresses type safety on the db variable. Not a new issue — pre-existing pattern.
INFO-02 — AdminGlobalItemDetail redundantly declares ownerCount
Severity: info
File: src/client/hooks/useAdminGlobalItems.ts (line 40)
export interface AdminGlobalItemDetail extends Omit<AdminGlobalItem, "tags"> {
ownerCount: number;
}
AdminGlobalItem already includes ownerCount: number, so Omit<AdminGlobalItem, "tags"> already carries ownerCount. The explicit re-declaration is harmless but redundant. Minor cleanup opportunity.
Self-Check
- No SQL injection vectors (Drizzle parameterized queries throughout)
- Auth protection: all admin routes inherit
requireAuth + requireAdminfrom parent router - Transaction safety: delete and update operations use
db.transaction - FK integrity: delete nullifies
items.globalItemIdbefore deleting - Input validation: PUT body validated via
zValidatorwith Zod schema - No secrets or credentials in code
- Build passes, tests pass