fix(feedback): stop exposing done_by user id in API responses

Problem: GET /api/feedback (moderator-readable) returned the user id of
the admin who marked an entry done. Moderators don't need to triangulate
"which admin closed which ticket" — done_at alone is sufficient signal
that the entry has been triaged. Keeping the user id in the response
made it possible to cross-reference admins with the user list via a
second authenticated call.

Fix: the `feedback.done_by` column stays in the schema (server-side
audit trail is preserved) but the column is no longer SELECTed by the
list or update endpoints, and is no longer in the FeedbackEntry wire
type. Moderators see only the `done_at` timestamp.

Surfaced by /audit security (data exposure lens).
This commit is contained in:
Ole-Morten Duesund 2026-05-25 13:54:07 +02:00
commit fbe37109a4
2 changed files with 9 additions and 5 deletions

View file

@ -40,7 +40,7 @@ feedbackRoutes.post('/', requireAuth, async (c) => {
const entry: FeedbackEntry = {
id, kind: body.kind, body: trimmed, created_at: Date.now(),
done_at: null, done_by: null,
done_at: null,
};
return c.json(entry, 201);
});
@ -49,9 +49,11 @@ feedbackRoutes.get('/', requireAuth, (c) => {
const userId = c.get('userId');
if (!isModerator(userId)) return c.json({ error: 'forbidden' }, 403);
// done_by is intentionally NOT selected — it's stored for server-side audit
// but never returned via the API. See FeedbackEntry doc in shared/types.ts.
const rows = getDb()
.prepare(`
SELECT f.id, f.kind, f.body, f.created_at, f.done_at, f.done_by,
SELECT f.id, f.kind, f.body, f.created_at, f.done_at,
f.user_id, u.email AS user_email, u.display_name AS user_display
FROM feedback f
JOIN users u ON u.id = f.user_id
@ -85,7 +87,7 @@ feedbackRoutes.patch('/:id', requireAuth, async (c) => {
}
const row = db.prepare(`
SELECT f.id, f.kind, f.body, f.created_at, f.done_at, f.done_by,
SELECT f.id, f.kind, f.body, f.created_at, f.done_at,
f.user_id, u.email AS user_email, u.display_name AS user_display
FROM feedback f
JOIN users u ON u.id = f.user_id

View file

@ -152,8 +152,10 @@ export interface FeedbackEntry {
created_at: number;
/** Set when an admin has marked the entry as done. */
done_at: number | null;
/** User id of the admin who marked it done; null when done_at is null. */
done_by: string | null;
// The admin who marked the entry done is recorded in the `feedback.done_by`
// column for audit purposes, but deliberately NOT exposed via the API —
// moderators don't need to triangulate which admin closed which ticket
// against the user list. The `done_at` timestamp is sufficient signal.
// Moderator-only fields; included when the caller is a moderator viewing
// the list. (The submit endpoint doesn't return these — a submitter doesn't
// need to see them.)