Allow dev CORS from LAN IPs; add submission deletion
The dev-only CORS bypass in app.ts only ever matched http://localhost:<port>, never the LAN IP a phone actually connects through over WiFi - so testing the feedback form from a real device against a local dev API had its submissions silently rejected by CORS. Extended the bypass to also allow private LAN ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x), dev-only as before. Also adds DELETE /feedback/admin/submissions/:submissionId (cascades to the submission's answers, guest book entry, and newsletter signup in explicit dependency order, single-path by submission_id) so an admin can remove an individual abusive/inappropriate entry - decided in IMPLEMENTATION_PLAN.md §7 item 8. getGuestBookEntries now also returns submissionId so the admin UI can target the delete call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
* Required External Modules and Interfaces
|
||||
*/
|
||||
import express, {Request, Response} from 'express';
|
||||
import {Guid} from 'guid-typescript';
|
||||
import logger from '../../../middleware/logger';
|
||||
import {requireAdminAuth} from '../feedback.auth';
|
||||
import {eventsAdminRouter} from './events.admin.router';
|
||||
import {songsAdminRouter} from './songs.admin.router';
|
||||
import {questionsAdminRouter} from './questions.admin.router';
|
||||
import {reportsAdminRouter} from './reports.admin.router';
|
||||
import * as ReportsAdminService from './reports.admin.service';
|
||||
|
||||
/**
|
||||
* Router Definition
|
||||
@@ -46,6 +49,48 @@ adminRouter.get('/me', (req: Request, res: Response) => {
|
||||
res.status(200).send({email: res.locals.admin.email, fullName: res.locals.admin.displayName});
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/submissions/{submissionId}:
|
||||
* delete:
|
||||
* summary: Delete a single submission
|
||||
* description: Removes the submission and everything under it (its answers, guest book entry, newsletter signup) - for removing an individual abusive or inappropriate entry. Not a bulk moderation tool.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: submissionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Deleted
|
||||
* 404:
|
||||
* description: Unknown submission
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
adminRouter.delete('/submissions/:submissionId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const deleted = await ReportsAdminService.deleteSubmission(Number(req.params.submissionId));
|
||||
if (!deleted) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (e: any) {
|
||||
let errorGuid = Guid.create().toString();
|
||||
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
||||
res.status(500).send({
|
||||
status: 'PROCESSING_ERROR',
|
||||
message: 'Internal Server Error. Try again later.',
|
||||
reference: errorGuid
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.use('/events', eventsAdminRouter);
|
||||
adminRouter.use('/events', reportsAdminRouter);
|
||||
adminRouter.use('/songs', songsAdminRouter);
|
||||
|
||||
@@ -160,6 +160,7 @@ export const getReport = async (eventId: number): Promise<EventReport | null> =>
|
||||
|
||||
export interface GuestBookEntry {
|
||||
entryId: number;
|
||||
submissionId: number;
|
||||
submittedAt: string;
|
||||
displayName: string | null;
|
||||
message: string | null;
|
||||
@@ -170,12 +171,18 @@ export const getGuestBookEntries = async (eventId: number, page: number, pageSiz
|
||||
try {
|
||||
const totalRows = await conn.query('SELECT COUNT(*) as cnt FROM guest_book_entries WHERE event_id = ?', [eventId]);
|
||||
const rows = await conn.query(
|
||||
'SELECT entry_id, created_at, display_name, message FROM guest_book_entries WHERE event_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?',
|
||||
'SELECT entry_id, submission_id, created_at, display_name, message FROM guest_book_entries WHERE event_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?',
|
||||
[eventId, pageSize, (page - 1) * pageSize]
|
||||
);
|
||||
return {
|
||||
total: Number(totalRows[0].cnt),
|
||||
entries: rows.map((r: any) => ({entryId: r.entry_id, submittedAt: r.created_at, displayName: r.display_name, message: r.message}))
|
||||
entries: rows.map((r: any) => ({
|
||||
entryId: r.entry_id,
|
||||
submissionId: r.submission_id,
|
||||
submittedAt: r.created_at,
|
||||
displayName: r.display_name,
|
||||
message: r.message
|
||||
}))
|
||||
};
|
||||
} finally {
|
||||
await conn.end();
|
||||
@@ -192,6 +199,39 @@ export interface NewsletterSignupRow {
|
||||
lastError: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes one submission and everything under it (its answers, guest book
|
||||
* entry, newsletter signup). Single-path deletes by submission_id - unlike
|
||||
* deleteEvent's multi-path cascade issue, there's only one way to reach each
|
||||
* child table here, so explicit ordering is for consistency with that
|
||||
* function's style, not to work around an ambiguous-cascade error.
|
||||
*/
|
||||
export const deleteSubmission = async (submissionId: number): Promise<boolean> => {
|
||||
let conn = await NachklangFeedbackDB.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const rows = await conn.query('SELECT submission_id FROM submissions WHERE submission_id = ?', [submissionId]);
|
||||
if (rows.length === 0) {
|
||||
await conn.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
await conn.query('DELETE FROM guest_book_entries WHERE submission_id = ?', [submissionId]);
|
||||
await conn.query('DELETE FROM newsletter_signups WHERE submission_id = ?', [submissionId]);
|
||||
await conn.query('DELETE FROM submission_answers WHERE submission_id = ?', [submissionId]);
|
||||
await conn.query('DELETE FROM submissions WHERE submission_id = ?', [submissionId]);
|
||||
|
||||
await conn.commit();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getNewsletterSignups = async (eventId: number): Promise<NewsletterSignupRow[]> => {
|
||||
let conn = await NachklangFeedbackDB.getConnection();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user