Add Feedback domain module: public submission flow, admin CRUD, reporting (#7)
Jenkins Production Deployment
Jenkins Production Deployment
Co-authored-by: Patrick Müller <mail@pmueller.me> Reviewed-on: #7 Co-authored-by: Patrick Mueller <patrick@mueller-patrick.tech> Co-committed-by: Patrick Mueller <patrick@mueller-patrick.tech>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* Required External Modules and Interfaces
|
||||
*/
|
||||
import express, {Request, Response} from 'express';
|
||||
import * as EventsAdminService from './events.admin.service';
|
||||
import * as SongsAdminService from './songs.admin.service';
|
||||
import {sendServerError} from '../feedback.errors';
|
||||
|
||||
/**
|
||||
* Router Definition
|
||||
*/
|
||||
export const eventsAdminRouter = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/events:
|
||||
* get:
|
||||
* summary: List all events (admin)
|
||||
* description: All events, published or not, past or future, with submission counts.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/EventAdminSummary'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* post:
|
||||
* summary: Create an event
|
||||
* description: Auto-generates the slug from the name and event year; defaults feedback_deadline to event_date + 14 days 23:59:59 unless supplied.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [name, eventDate]
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* subtitle:
|
||||
* type: string
|
||||
* eventDate:
|
||||
* type: string
|
||||
* format: date
|
||||
* feedbackDeadline:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* introText:
|
||||
* type: string
|
||||
* posterImageUrl:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created
|
||||
* 400:
|
||||
* description: Missing required fields
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
eventsAdminRouter.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
res.status(200).send(await EventsAdminService.listEventsAdmin());
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
eventsAdminRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl} = req.body || {};
|
||||
if (!name || !eventDate) {
|
||||
res.status(400).send({status: 'BAD_REQUEST', message: 'name and eventDate are required'});
|
||||
return;
|
||||
}
|
||||
const eventId = await EventsAdminService.createEvent(
|
||||
{name, subtitle, eventDate, feedbackDeadline, introText, posterImageUrl},
|
||||
res.locals.admin.email
|
||||
);
|
||||
res.status(201).send({eventId});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/events/{eventId}:
|
||||
* get:
|
||||
* summary: Get one event (admin)
|
||||
* description: Full event detail including setlist and assigned questions.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/EventAdminDetail'
|
||||
* 404:
|
||||
* description: Unknown event
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* put:
|
||||
* summary: Update an event
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Updated
|
||||
* 404:
|
||||
* description: Unknown event
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* delete:
|
||||
* summary: Delete an event
|
||||
* description: Refuses with 409 if submissions exist unless ?force=true is passed.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: query
|
||||
* name: force
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Deleted
|
||||
* 404:
|
||||
* description: Unknown event
|
||||
* 409:
|
||||
* description: Submissions exist and force was not set
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
eventsAdminRouter.get('/:eventId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const event = await EventsAdminService.getEventAdmin(Number(req.params.eventId));
|
||||
if (!event) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send(event);
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
eventsAdminRouter.put('/:eventId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const updated = await EventsAdminService.updateEvent(Number(req.params.eventId), req.body || {});
|
||||
if (!updated) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send({status: 'OK'});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
eventsAdminRouter.delete('/:eventId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const force = req.query.force === 'true';
|
||||
const result = await EventsAdminService.deleteEvent(Number(req.params.eventId), force);
|
||||
if (result === 'NOT_FOUND') {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
if (result === 'HAS_SUBMISSIONS') {
|
||||
res.status(409).send({status: 'HAS_SUBMISSIONS', message: 'This event has submissions. Pass ?force=true to delete anyway.'});
|
||||
return;
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/events/{eventId}/songs:
|
||||
* get:
|
||||
* summary: Get an event's setlist
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* post:
|
||||
* summary: Add a song to an event's setlist
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [title]
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* composer:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created
|
||||
* 400:
|
||||
* description: Missing title
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
eventsAdminRouter.get('/:eventId/songs', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const event = await EventsAdminService.getEventAdmin(Number(req.params.eventId));
|
||||
if (!event) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send(event.songs);
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
eventsAdminRouter.post('/:eventId/songs', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {title, composer} = req.body || {};
|
||||
if (!title) {
|
||||
res.status(400).send({status: 'BAD_REQUEST', message: 'title is required'});
|
||||
return;
|
||||
}
|
||||
const songId = await SongsAdminService.addSong(Number(req.params.eventId), title, composer || null);
|
||||
res.status(201).send({songId});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/events/{eventId}/songs/order:
|
||||
* put:
|
||||
* summary: Bulk reorder an event's setlist
|
||||
* description: Rewrites song positions as a dense 0..n-1 sequence in one transaction.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [songIds]
|
||||
* properties:
|
||||
* songIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Reordered
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
eventsAdminRouter.put('/:eventId/songs/order', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const songIds: number[] = req.body?.songIds || [];
|
||||
await EventsAdminService.reorderSongs(Number(req.params.eventId), songIds);
|
||||
res.status(200).send({status: 'OK'});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/events/{eventId}/questions:
|
||||
* get:
|
||||
* summary: Get an event's assigned questions
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* put:
|
||||
* summary: Bulk-set an event's assigned questions
|
||||
* description: One transaction - inserts new, updates existing, deletes removed. Keeps the admin UI a simple save-the-whole-list form.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: eventId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [items]
|
||||
* properties:
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* questionId:
|
||||
* type: integer
|
||||
* position:
|
||||
* type: integer
|
||||
* isActive:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Saved
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
eventsAdminRouter.get('/:eventId/questions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const event = await EventsAdminService.getEventAdmin(Number(req.params.eventId));
|
||||
if (!event) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send(event.questions);
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
eventsAdminRouter.put('/:eventId/questions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const items = req.body?.items || [];
|
||||
await EventsAdminService.setEventQuestions(Number(req.params.eventId), items);
|
||||
res.status(200).send({status: 'OK'});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user