d960ac8e24
A fresh-context review before deploying found two things that would have broken production, both in the runbook rather than the code. The deploy order named only migration 003. Production has none of the three - 001 and 002 were only ever applied to the dev database - and the new API reads the columns they add on every request, so following it literally would have 500'd every calendar call including the anonymous feed the public website uses. Step 4 now carries a numbered checklist with a verification query. APP_ORIGINS replaces the code's default list rather than adding to it, so naming calendar.nachklang.art in DEFAULT_APP_ORIGINS is not enough if that variable is set on the vhost - and its failure mode is the quiet one the config already warns about, where everything works except sign-out. Added to the same checklist. Also from the review: The two operands of the read guard on /json/next and /ical were swapped so the password check short-circuits first. They are side-effect free, so the order was free - but the old one put an admin-database query in front of the public feed for any caller holding a .nachklang.art cookie, which is a dependency that feed has never had. Two tests now assert the admin database is not consulted at all. /:calendar/json/next had no route-level test, despite being the endpoint the public website actually calls and the property named as load-bearing. Covered now, along with the rest of its credential matrix. Migrations 001 and 002 gained IF NOT EXISTS. They are applied by hand with no tracking table, so a partial re-run should be a no-op rather than an error that aborts the rest of the paste. Verified by applying all three twice to a throwaway container and diffing against the dev schema. Swagger: two descriptions still claimed authentication was required where the public calendar needs none, the calendar enum omitted `birthdays`, and a `createdBy` request-body field was documented and read but never persisted - misleading in a way that suggests a client can set authorship. Removed. The CORS comment describing the calendar's query-parameter sessions is no longer true and was rewritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1092 lines
44 KiB
TypeScript
1092 lines
44 KiB
TypeScript
/**
|
|
* Required External Modules and Interfaces
|
|
*/
|
|
|
|
import express, {Request, Response} from 'express';
|
|
import {Event} from './event.interface.js';
|
|
import * as EventService from './events.service.js';
|
|
import * as iCalService from './icalgenerator.service.js';
|
|
import * as CredentialService from './credentials.service.js';
|
|
import {requireAppAccess, resolveAccess, AdminAccess} from '../../admin/admin.middleware.js';
|
|
import {Guid} from 'guid-typescript';
|
|
import logger from '../../../middleware/logger.js';
|
|
|
|
|
|
/**
|
|
* Router Definition
|
|
*/
|
|
|
|
export const eventsRouter = express.Router();
|
|
|
|
/**
|
|
* Constants
|
|
*/
|
|
export const calendarNames = new Map<string, any>([
|
|
['public', {id: 1, name: 'Nachklang_calendar'}],
|
|
['members', {id: 2, name: 'Nachklang_internal_calendar'}],
|
|
['choir', {id: 4, name: 'Nachklang_choir_calendar'}],
|
|
['management', {id: 3, name: 'Nachklang_management_calendar'}],
|
|
['birthdays', {id: 5, name: 'Nachklang_birthday_calendar'}]
|
|
]);
|
|
|
|
/**
|
|
* The gate on everything that writes. Step 4 of
|
|
* docs/calendar-auth-migration.md replaced a sessionId/sessionKey pair in the
|
|
* query string (DEFERRED_SECURITY.md item 1) with the same session cookie the
|
|
* other three apps use, and "any activated @nachklang.art account" with an
|
|
* explicit per-user calendar permission.
|
|
*/
|
|
const requireCalendarAccess = requireAppAccess('calendar');
|
|
|
|
/** Set by requireCalendarAccess; the writer's admin identity. */
|
|
const adminOf = (res: Response): AdminAccess => res.locals.admin as AdminAccess;
|
|
|
|
/**
|
|
* Resolves a signed-in calendar user for the *read* routes, or null.
|
|
*
|
|
* Reads cannot use the middleware: the same URL serves an anonymous visitor
|
|
* (the public calendar the website polls), someone holding a shared password
|
|
* (an iCal subscription), and a signed-in editor who should see drafts. So it
|
|
* answers "who is this, if anyone?" instead of refusing the request, and each
|
|
* handler decides what that means.
|
|
*
|
|
* A failure to reach the admin database is swallowed for the same reason the
|
|
* name lookup in events.service.ts swallows one: it must not be able to take
|
|
* the anonymous public calendar down.
|
|
*/
|
|
const signedInEditor = async (req: Request): Promise<AdminAccess | null> => {
|
|
try {
|
|
const access = await resolveAccess(req);
|
|
if (!access || access.disabled || !access.apps.includes('calendar')) {
|
|
return null;
|
|
}
|
|
return access;
|
|
} catch (e: any) {
|
|
logger.warn('Calendar: could not resolve the session, continuing as anonymous: ' + e.message);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Controller Definitions
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/{calendar}/json:
|
|
* get:
|
|
* summary: Get all events from a specific calendar in JSON format
|
|
* description: >
|
|
* Returns the calendar's events. The public calendar is open to everyone; the
|
|
* others need either a signed-in account with the calendar permission - which
|
|
* also unlocks drafts - or the calendar's shared password.
|
|
* tags:
|
|
* - calendar
|
|
* parameters:
|
|
* - in: path
|
|
* name: calendar
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* enum: [public, members, choir, management, birthdays]
|
|
* description: The name of the calendar to get events from
|
|
* - in: query
|
|
* name: password
|
|
* schema:
|
|
* type: string
|
|
* description: The calendar's shared password, for callers with no account
|
|
* responses:
|
|
* 200:
|
|
* description: Success
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/Event'
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Please state the name of the calendar you want events from.
|
|
* 403:
|
|
* description: Forbidden - no access to the calendar
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: You do not have access to the specified calendar.
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
*/
|
|
eventsRouter.get('/:calendar/json', async (req: Request, res: Response) => {
|
|
try {
|
|
// Get request params
|
|
let calendarName: string = req.params.calendar as string ?? '';
|
|
let password: string = req.query.password as string ?? '';
|
|
|
|
if (calendarName.length < 1) {
|
|
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'});
|
|
return;
|
|
}
|
|
|
|
if (!Array.from(calendarNames.keys()).includes(calendarName)) {
|
|
res.status(400).send({'message': 'Unknown calendar.'});
|
|
return;
|
|
}
|
|
|
|
let calendarId: number = calendarNames.get(calendarName)!.id;
|
|
|
|
const editor = await signedInEditor(req);
|
|
|
|
// Not signed in: fall back to the shared password for this calendar.
|
|
if (!editor && ! await CredentialService.hasAccess(calendarName, password)) {
|
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
|
return;
|
|
}
|
|
|
|
// Editors get the admin view (drafts included, calendar includes ignored);
|
|
// everyone else gets published events only.
|
|
let events: Event[] = editor
|
|
? await EventService.getAllEventsAdmin(calendarId)
|
|
: await EventService.getAllEvents(calendarId);
|
|
|
|
// Send the events back
|
|
res.status(200).send(events);
|
|
} catch (e: any) {
|
|
let errorGuid = Guid.create().toString();
|
|
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
|
|
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/{calendar}/json/next:
|
|
* get:
|
|
* summary: Get the next upcoming event from a calendar
|
|
* description: >
|
|
* The next upcoming event. The public calendar is open to everyone; the
|
|
* others need either a signed-in account with the calendar permission or the
|
|
* calendar's shared password.
|
|
* tags:
|
|
* - calendar
|
|
* parameters:
|
|
* - in: path
|
|
* name: calendar
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* enum: [public, members, choir, management, birthdays]
|
|
* description: The name of the calendar to get the next event from
|
|
* - in: query
|
|
* name: password
|
|
* schema:
|
|
* type: string
|
|
* description: The calendar's shared password, for callers with no account
|
|
* responses:
|
|
* 200:
|
|
* description: Success
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/Event'
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Please state the name of the calendar you want events from.
|
|
* 403:
|
|
* description: Forbidden - no access to the calendar
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: You do not have access to the specified calendar.
|
|
* 404:
|
|
* description: Not found - no upcoming events
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: No upcoming events found.
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.get('/:calendar/json/next', async (req: Request, res: Response) => {
|
|
try {
|
|
// Get request params
|
|
let calendarName: string = req.params.calendar as string ?? '';
|
|
let password: string = req.query.password as string ?? '';
|
|
|
|
if (calendarName.length < 1) {
|
|
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'});
|
|
return;
|
|
}
|
|
|
|
if (!Array.from(calendarNames.keys()).includes(calendarName)) {
|
|
res.status(400).send({'message': 'Unknown calendar.'});
|
|
return;
|
|
}
|
|
|
|
let calendarId: number = calendarNames.get(calendarName)!.id;
|
|
|
|
// Holding the calendar's shared password, or signed in. The password path
|
|
// is what keeps iCal subscriptions working - a calendar client cannot
|
|
// send a cookie.
|
|
//
|
|
// The password is checked FIRST so that `public`, which needs no
|
|
// credential at all, short-circuits before signedInEditor runs. Otherwise
|
|
// every request from a browser that happens to hold a .nachklang.art
|
|
// cookie - which is any signed-in user on any of the four apps - would put
|
|
// an admin-database query in front of the anonymous public feed, with no
|
|
// timeout. Both operands are side-effect free, so the order is free to
|
|
// choose; this order is the one that keeps the public calendar
|
|
// independent of the admin database.
|
|
if (! await CredentialService.hasAccess(calendarName, password) && !await signedInEditor(req)) {
|
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
|
return;
|
|
}
|
|
|
|
// Get next upcoming event
|
|
let event = await EventService.getNextUpcomingEvent(calendarId);
|
|
|
|
if (event === null) {
|
|
res.status(404).send({'message': 'No upcoming events found.'});
|
|
return;
|
|
}
|
|
|
|
// Send the event back
|
|
res.status(200).send(event);
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/{calendar}/ical:
|
|
* get:
|
|
* summary: Get all events from a specific calendar in iCal format
|
|
* description: >
|
|
* The calendar in iCal format. The public calendar is open to everyone; the
|
|
* others take the calendar's shared password in the query string, which is
|
|
* why that mechanism survives - an iCal client cannot send a cookie.
|
|
* tags:
|
|
* - calendar
|
|
* parameters:
|
|
* - in: path
|
|
* name: calendar
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* enum: [public, members, choir, management, birthdays]
|
|
* description: The name of the calendar to get events from
|
|
* - in: query
|
|
* name: password
|
|
* schema:
|
|
* type: string
|
|
* description: The calendar's shared password, for callers with no account
|
|
* responses:
|
|
* 200:
|
|
* description: Success - returns iCal file
|
|
* content:
|
|
* text/calendar:
|
|
* schema:
|
|
* type: string
|
|
* format: binary
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Please state the name of the calendar you want events from.
|
|
* 403:
|
|
* description: Forbidden - no access to the calendar
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: You do not have access to the specified calendar.
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.get('/:calendar/ical', async (req: Request, res: Response) => {
|
|
try {
|
|
// Get request params
|
|
let calendarName: string = req.params.calendar as string ?? '';
|
|
let password: string = req.query.password as string ?? '';
|
|
|
|
if (calendarName.length < 1) {
|
|
res.status(400).send({'message': 'Please state the name of the calendar you want events from.'});
|
|
return;
|
|
}
|
|
|
|
if (!Array.from(calendarNames.keys()).includes(calendarName)) {
|
|
res.status(400).send({'message': 'Unknown calendar.'});
|
|
return;
|
|
}
|
|
|
|
let calendarId: number = calendarNames.get(calendarName)!.id;
|
|
|
|
// Holding the calendar's shared password, or signed in. The password path
|
|
// is what keeps iCal subscriptions working - a calendar client cannot
|
|
// send a cookie.
|
|
//
|
|
// The password is checked FIRST so that `public`, which needs no
|
|
// credential at all, short-circuits before signedInEditor runs. Otherwise
|
|
// every request from a browser that happens to hold a .nachklang.art
|
|
// cookie - which is any signed-in user on any of the four apps - would put
|
|
// an admin-database query in front of the anonymous public feed, with no
|
|
// timeout. Both operands are side-effect free, so the order is free to
|
|
// choose; this order is the one that keeps the public calendar
|
|
// independent of the admin database.
|
|
if (! await CredentialService.hasAccess(calendarName, password) && !await signedInEditor(req)) {
|
|
res.status(403).send({'message': 'You do not have access to the specified calendar.'});
|
|
return;
|
|
}
|
|
|
|
// Get events
|
|
let events = await EventService.getAllEvents(calendarId);
|
|
|
|
// generate file name
|
|
let fileName = calendarNames.get(calendarName)!.name;
|
|
let file = await iCalService.convertToIcal(events);
|
|
|
|
// Send the ical file back
|
|
res.set({'Content-Disposition': 'attachment; filename=' + fileName + '.ics'});
|
|
res.status(200).send(file);
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events:
|
|
* post:
|
|
* summary: Create a new event
|
|
* description: Creates a new event. Requires a signed-in account with the calendar permission.
|
|
* tags:
|
|
* - calendar
|
|
* security:
|
|
* - AdminSessionCookie: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - calendarId
|
|
* - name
|
|
* - startDateTime
|
|
* - endDateTime
|
|
* properties:
|
|
* calendarId:
|
|
* type: integer
|
|
* example: 1
|
|
* name:
|
|
* type: string
|
|
* example: "Concert at Musikhochschule"
|
|
* description:
|
|
* type: string
|
|
* example: "Annual concert at the Musikhochschule"
|
|
* startDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T19:00:00.000Z"
|
|
* endDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T21:00:00.000Z"
|
|
* location:
|
|
* type: string
|
|
* example: "Musikhochschule, Karlsruhe"
|
|
* url:
|
|
* type: string
|
|
* example: "https://www.nachklang.art/events/concert"
|
|
* wholeDay:
|
|
* type: boolean
|
|
* example: false
|
|
* status:
|
|
* type: string
|
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
|
* example: "PUBLIC"
|
|
* responses:
|
|
* 201:
|
|
* description: Event created successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Event with id 123 was created successfully.
|
|
* eventId:
|
|
* type: integer
|
|
* example: 123
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Required parameters missing
|
|
* 401:
|
|
* description: Unauthorized - not signed in
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: UNAUTHORIZED
|
|
* message:
|
|
* type: string
|
|
* example: Anmeldung erforderlich.
|
|
* 403:
|
|
* description: Forbidden - the account lacks the calendar permission
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: FORBIDDEN
|
|
* message:
|
|
* type: string
|
|
* example: "Für diesen Bereich fehlt dir die Berechtigung."
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.post('/', requireCalendarAccess, async (req: Request, res: Response) => {
|
|
try {
|
|
const admin = adminOf(res);
|
|
|
|
if (
|
|
req.body.calendarId === undefined ||
|
|
isNullOrBlank(req.body.name) ||
|
|
req.body.startDateTime === undefined ||
|
|
req.body.endDateTime === undefined
|
|
) {
|
|
res.status(400).send({'message': 'Required parameters missing'});
|
|
return;
|
|
}
|
|
|
|
let event: Event = {
|
|
eventId: -1,
|
|
calendarId: req.body.calendarId,
|
|
uuid: '',
|
|
name: req.body.name,
|
|
description: req.body.description ?? '',
|
|
startDateTime: new Date(req.body.startDateTime),
|
|
endDateTime: new Date(req.body.endDateTime),
|
|
createdDate: new Date(),
|
|
location: req.body.location ?? '',
|
|
// LEGACY createdById is deliberately not set: there is no calendar
|
|
// user id any more, and migration 003 made the column nullable.
|
|
createdByUserId: admin.id,
|
|
url: req.body.url ?? '',
|
|
wholeDay: req.body.wholeDay ?? false,
|
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
|
status: req.body.status ?? 'PUBLIC'
|
|
};
|
|
|
|
let eventId = await EventService.createEvent(event);
|
|
|
|
res.status(201).send({
|
|
'message': 'Event with id ' + eventId + ' was created successfully.',
|
|
'eventId': eventId
|
|
});
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/{eventId}:
|
|
* put:
|
|
* summary: Update an existing event
|
|
* description: Updates an existing event. Requires a signed-in account with the calendar permission.
|
|
* tags:
|
|
* - calendar
|
|
* security:
|
|
* - AdminSessionCookie: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: eventId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The ID of the event to update
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - calendarId
|
|
* - name
|
|
* - startDateTime
|
|
* - endDateTime
|
|
* properties:
|
|
* calendarId:
|
|
* type: integer
|
|
* example: 1
|
|
* name:
|
|
* type: string
|
|
* example: "Updated Concert at Musikhochschule"
|
|
* description:
|
|
* type: string
|
|
* example: "Updated annual concert at the Musikhochschule"
|
|
* startDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T19:00:00.000Z"
|
|
* endDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T21:00:00.000Z"
|
|
* location:
|
|
* type: string
|
|
* example: "Musikhochschule, Karlsruhe"
|
|
* url:
|
|
* type: string
|
|
* example: "https://www.nachklang.art/events/concert"
|
|
* wholeDay:
|
|
* type: boolean
|
|
* example: false
|
|
* status:
|
|
* type: string
|
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
|
* example: "PUBLIC"
|
|
* responses:
|
|
* 200:
|
|
* description: Event updated successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Event was successfully updated
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Required parameters missing
|
|
* 401:
|
|
* description: Unauthorized - not signed in
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: UNAUTHORIZED
|
|
* message:
|
|
* type: string
|
|
* example: Anmeldung erforderlich.
|
|
* 403:
|
|
* description: Forbidden - the account lacks the calendar permission
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: FORBIDDEN
|
|
* message:
|
|
* type: string
|
|
* example: "Für diesen Bereich fehlt dir die Berechtigung."
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.put('/:eventId', requireCalendarAccess, async (req: Request, res: Response) => {
|
|
try {
|
|
const admin = adminOf(res);
|
|
|
|
if (
|
|
req.params.eventId === undefined ||
|
|
req.body.calendarId === undefined ||
|
|
isNullOrBlank(req.body.name) ||
|
|
req.body.startDateTime === undefined ||
|
|
req.body.endDateTime === undefined
|
|
) {
|
|
res.status(400).send({'message': 'Required parameters missing'});
|
|
return;
|
|
}
|
|
|
|
let event: Event = {
|
|
eventId: parseInt(req.params.eventId, 10),
|
|
calendarId: req.body.calendarId,
|
|
uuid: '',
|
|
name: req.body.name,
|
|
description: req.body.description ?? '',
|
|
startDateTime: new Date(req.body.startDateTime),
|
|
endDateTime: new Date(req.body.endDateTime),
|
|
createdDate: new Date(),
|
|
location: req.body.location ?? '',
|
|
// LEGACY createdById is deliberately not set: there is no calendar
|
|
// user id any more, and migration 003 made the column nullable.
|
|
createdByUserId: admin.id,
|
|
url: req.body.url ?? '',
|
|
wholeDay: req.body.wholeDay ?? false,
|
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
|
status: req.body.status ?? 'PUBLIC'
|
|
};
|
|
|
|
let successRows = await EventService.updateEvent(event);
|
|
|
|
if (successRows === 1) {
|
|
res.status(200).send({'message': 'Event was successfully updated'});
|
|
} else if (successRows === 0) {
|
|
res.status(200).send({'message': '0 rows were updated'});
|
|
} else {
|
|
res.status(500).send({'message': 'An error occurred during the update process. Please try again.'});
|
|
}
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/move/{eventId}:
|
|
* put:
|
|
* summary: Move an event to a different calendar
|
|
* description: Moves an event to a different calendar. Requires a signed-in account with the calendar permission.
|
|
* tags:
|
|
* - calendar
|
|
* security:
|
|
* - AdminSessionCookie: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: eventId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The ID of the event to move
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - calendarId
|
|
* properties:
|
|
* calendarId:
|
|
* type: integer
|
|
* example: 2
|
|
* description: The ID of the calendar to move the event to
|
|
* name:
|
|
* type: string
|
|
* example: "Concert at Musikhochschule"
|
|
* description:
|
|
* type: string
|
|
* example: "Annual concert at the Musikhochschule"
|
|
* startDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T19:00:00.000Z"
|
|
* endDateTime:
|
|
* type: string
|
|
* format: date-time
|
|
* example: "2023-06-15T21:00:00.000Z"
|
|
* location:
|
|
* type: string
|
|
* example: "Musikhochschule, Karlsruhe"
|
|
* url:
|
|
* type: string
|
|
* example: "https://www.nachklang.art/events/concert"
|
|
* wholeDay:
|
|
* type: boolean
|
|
* example: false
|
|
* status:
|
|
* type: string
|
|
* enum: [PUBLIC, PRIVATE, DRAFT, DELETED]
|
|
* example: "PUBLIC"
|
|
* responses:
|
|
* 200:
|
|
* description: Event moved successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Event was successfully moved
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Required parameters missing
|
|
* 401:
|
|
* description: Unauthorized - not signed in
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: UNAUTHORIZED
|
|
* message:
|
|
* type: string
|
|
* example: Anmeldung erforderlich.
|
|
* 403:
|
|
* description: Forbidden - the account lacks the calendar permission
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: FORBIDDEN
|
|
* message:
|
|
* type: string
|
|
* example: "Für diesen Bereich fehlt dir die Berechtigung."
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.put('/move/:eventId', requireCalendarAccess, async (req: Request, res: Response) => {
|
|
try {
|
|
const admin = adminOf(res);
|
|
|
|
if (
|
|
req.params.eventId === undefined ||
|
|
req.body.calendarId === undefined
|
|
) {
|
|
res.status(400).send({'message': 'Required parameters missing'});
|
|
return;
|
|
}
|
|
|
|
let event: Event = {
|
|
eventId: parseInt(req.params.eventId, 10),
|
|
calendarId: req.body.calendarId,
|
|
uuid: '',
|
|
name: req.body.name,
|
|
description: req.body.description ?? '',
|
|
startDateTime: new Date(req.body.startDateTime),
|
|
endDateTime: new Date(req.body.endDateTime),
|
|
createdDate: new Date(),
|
|
location: req.body.location ?? '',
|
|
// LEGACY createdById is deliberately not set: there is no calendar
|
|
// user id any more, and migration 003 made the column nullable.
|
|
createdByUserId: admin.id,
|
|
url: req.body.url ?? '',
|
|
wholeDay: req.body.wholeDay ?? false,
|
|
repeatFrequency: req.body.repeatFrequency ?? '',
|
|
status: req.body.status ?? 'PUBLIC'
|
|
};
|
|
|
|
let success = await EventService.moveEvent(event);
|
|
|
|
if (success) {
|
|
res.status(200).send({'message': 'Event was successfully moved'});
|
|
} else {
|
|
res.status(500).send({'message': 'An error occurred during the moving process. Please try again.'});
|
|
}
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /calendar/events/{eventId}:
|
|
* delete:
|
|
* summary: Delete an event
|
|
* description: Deletes an event. Requires a signed-in account with the calendar permission.
|
|
* tags:
|
|
* - calendar
|
|
* security:
|
|
* - AdminSessionCookie: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: eventId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The ID of the event to delete
|
|
* responses:
|
|
* 200:
|
|
* description: Event deleted successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Event was successfully deleted
|
|
* 400:
|
|
* description: Bad request - missing or invalid parameters
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Required parameters missing
|
|
* 401:
|
|
* description: Unauthorized - not signed in
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: UNAUTHORIZED
|
|
* message:
|
|
* type: string
|
|
* example: Anmeldung erforderlich.
|
|
* 403:
|
|
* description: Forbidden - the account lacks the calendar permission
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: FORBIDDEN
|
|
* message:
|
|
* type: string
|
|
* example: "Für diesen Bereich fehlt dir die Berechtigung."
|
|
* 500:
|
|
* description: Server error
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* status:
|
|
* type: string
|
|
* example: PROCESSING_ERROR
|
|
* message:
|
|
* type: string
|
|
* example: Internal Server Error. Try again later.
|
|
* reference:
|
|
* type: string
|
|
* example: 6ec1361c-4175-4e81-b2ef-a0792a9a1dc3
|
|
*/
|
|
eventsRouter.delete('/:eventId', requireCalendarAccess, async (req: Request, res: Response) => {
|
|
try {
|
|
const admin = adminOf(res);
|
|
|
|
if (
|
|
req.params.eventId === undefined
|
|
) {
|
|
res.status(400).send({'message': 'Required parameters missing'});
|
|
return;
|
|
}
|
|
|
|
let event: Event = {
|
|
eventId: parseInt(req.params.eventId, 10),
|
|
calendarId: -1,
|
|
uuid: '',
|
|
name: '',
|
|
description: '',
|
|
startDateTime: new Date(),
|
|
endDateTime: new Date(),
|
|
createdDate: new Date(),
|
|
location: '',
|
|
createdBy: '',
|
|
// LEGACY createdById is deliberately not set: there is no calendar
|
|
// user id any more, and migration 003 made the column nullable.
|
|
createdByUserId: admin.id,
|
|
url: '',
|
|
wholeDay: false,
|
|
repeatFrequency: '',
|
|
status: 'DELETED'
|
|
};
|
|
|
|
let success = await EventService.deleteEvent(event);
|
|
|
|
if (success) {
|
|
res.status(200).send({'message': 'Event was successfully deleted'});
|
|
} else {
|
|
res.status(500).send({'message': 'An error occurred during deletion. Please try again.'});
|
|
}
|
|
} 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
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Checks if a given string is null, undefined or blank
|
|
* @param str The string to check
|
|
*/
|
|
function isNullOrBlank(str: string | null): boolean {
|
|
return str === null || str === undefined || str.trim() === '';
|
|
}
|