13a0c07d1b
Jenkins Production Deployment
Reviewed-on: #14 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
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() === '';
|
|
}
|