• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 25753937432

12 May 2026 09:17AM UTC coverage: 88.117% (-1.0%) from 89.089%
25753937432

push

github

web-flow
chore(deps): fix missing dependencies for running docs:dev (#911)

3925 of 4574 branches covered (85.81%)

Branch coverage included in aggregate %.

20093 of 22683 relevant lines covered (88.58%)

1125.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.71
/src/controller/event-controller.ts
1
/**
1✔
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
1✔
20

21
/**
1✔
22
 * This is the module page of event-controller.
23
 *
24
 * @module events
25
 * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
26
 */
1✔
27

28
import log4js, { Logger } from 'log4js';
29
import { Response } from 'express';
30
import BaseController, { BaseControllerOptions } from './base-controller';
31
import Policy from './policy';
32
import { RequestWithToken } from '../middleware/token-middleware';
33
import EventService, {
34
  CreateEventParams,
35
  EventFilterParameters,
36
  parseEventFilterParameters, parseUpdateEventRequestParameters, UpdateEventAnswerParams, UpdateEventParams,
37
} from '../service/event-service';
38
import { parseRequestPagination, toResponse } from '../helpers/pagination';
39
import { EventAnswerAssignmentRequest, EventAnswerAvailabilityRequest, EventRequest } from './request/event-request';
40
import Event from '../entity/event/event';
41
import EventShiftAnswer from '../entity/event/event-shift-answer';
42
import { asShiftAvailability } from '../helpers/validators';
43

44
/**
1✔
45
 * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
46
 */
1✔
47
export default class EventController extends BaseController {
1✔
48
  private logger: Logger = log4js.getLogger('EventLogger');
1✔
49

50
  /**
1✔
51
   * Create a new user controller instance.
52
   * @param options - The options passed to the base controller.
53
   */
1✔
54
  public constructor(
1✔
55
    options: BaseControllerOptions,
2✔
56
  ) {
2✔
57
    super(options);
2✔
58
    this.configureLogger(this.logger);
2✔
59
  }
2✔
60

61
  public getPolicy(): Policy {
1✔
62
    return {
2✔
63
      '/': {
2✔
64
        GET: {
2✔
65
          policy: async (req) => this.roleManager.can(
2✔
66
            req.token.roles, 'get', 'all', 'Event', ['*'],
9✔
67
          ),
68
          handler: this.getAllEvents.bind(this),
2✔
69
        },
2✔
70
        POST: {
2✔
71
          policy: async (req) => this.roleManager.can(
2✔
72
            req.token.roles, 'create', 'all', 'Event', ['*'],
12✔
73
          ),
74
          body: { modelName: 'CreateEventRequest' },
2✔
75
          handler: this.createEvent.bind(this),
2✔
76
        },
2✔
77
      },
2✔
78
      '/:id(\\d+)': {
2✔
79
        GET: {
2✔
80
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Event', ['*']),
2✔
81
          handler: this.getSingleEvent.bind(this),
2✔
82
        },
2✔
83
        PATCH: {
2✔
84
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']),
2✔
85
          handler: this.updateEvent.bind(this),
2✔
86
          body: { modelName: 'UpdateEventRequest' },
2✔
87
        },
2✔
88
        DELETE: {
2✔
89
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Event', ['*']),
2✔
90
          handler: this.deleteEvent.bind(this),
2✔
91
        },
2✔
92
      },
2✔
93
      '/:id(\\d+)/sync': {
2✔
94
        POST: {
2✔
95
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Event', ['*']),
2✔
96
          handler: this.syncEventShiftAnswers.bind(this),
2✔
97
        },
2✔
98
      },
2✔
99
      '/:eventId(\\d+)/shift/:shiftId(\\d+)/user/:userId(\\d+)/assign': {
2✔
100
        PUT: {
2✔
101
          policy: async (req) => this.roleManager.can(req.token.roles, 'assign', 'all', 'EventAnswer', ['*']),
2✔
102
          handler: this.assignEventShift.bind(this),
2✔
103
          body: { modelName: 'EventAnswerAssignmentRequest' },
2✔
104
        },
2✔
105
      },
2✔
106
      '/:eventId(\\d+)/shift/:shiftId(\\d+)/user/:userId(\\d+)/availability': {
2✔
107
        PUT: {
2✔
108
          policy: async (req) => this.roleManager.can(req.token.roles, 'assign', EventController.getRelation(req), 'EventAnswer', ['*']),
2✔
109
          handler: this.updateShiftAvailability.bind(this),
2✔
110
          body: { modelName: 'EventAnswerAvailabilityRequest' },
2✔
111
        },
2✔
112
      },
2✔
113
    };
2✔
114
  }
2✔
115

116
  private static getRelation(req: RequestWithToken): string {
1✔
117
    return req.params.userId === req.token.user.id.toString() ? 'own' : 'all';
7✔
118
  }
7✔
119

120
  /**
1✔
121
   * GET /events
122
   * @summary Get all events
123
   * @tags events - Operations of the event controller
124
   * @operationId getAllEvents
125
   * @security JWT
126
   * @param {string} name.query - Name of the event
127
   * @param {integer} createdById.query - ID of user that created the event
128
   * @param {string} beforeDate.query - Get only events that start after this date
129
   * @param {string} afterDate.query - Get only events that start before this date
130
   * @param {string} type.query - Get only events that are this type
131
   * @param {integer} take.query - How many entries the endpoint should return
132
   * @param {integer} skip.query - How many entries should be skipped (for pagination)
133
   * @return {PaginatedBaseEventResponse} 200 - All existing events
134
   * @return {string} 400 - Validation error
135
   * @return {string} 500 - Internal server error
136
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
137
   */
1✔
138
  public async getAllEvents(req: RequestWithToken, res: Response): Promise<void> {
1✔
139
    this.logger.trace('Get all events by user', req.token.user);
8✔
140

141
    let take;
8✔
142
    let skip;
8✔
143
    try {
8✔
144
      const pagination = parseRequestPagination(req);
8✔
145
      take = pagination.take;
8✔
146
      skip = pagination.skip;
8✔
147
    } catch (e) {
8!
148
      res.status(400).json(e.message);
×
149
      return;
×
150
    }
×
151

152
    let filters: EventFilterParameters;
8✔
153
    try {
8✔
154
      filters = parseEventFilterParameters(req);
8✔
155
    } catch (e) {
8✔
156
      res.status(400).json(e.message);
1✔
157
      return;
1✔
158
    }
1✔
159

160
    // Handle request
7✔
161
    try {
7✔
162
      const [events, count] = await EventService.getEvents(filters, { take, skip });
7✔
163
      res.json(toResponse(events.map((e) => EventService.asBaseEventResponse(e)), count, { take, skip }));
7✔
164
    } catch (e) {
8!
165
      this.logger.error('Could not return all events:', e);
×
166
      res.status(500).json('Internal server error.');
×
167
    }
×
168
  }
8✔
169

170
  /**
1✔
171
   * GET /events/{id}
172
   * @summary Get a single event with its answers and shifts
173
   * @tags events - Operations of the event controller
174
   * @operationId getSingleEvent
175
   * @security JWT
176
   * @param {integer} id.path.required - The id of the event which should be returned
177
   * @return {EventResponse} 200 - All existing events
178
   * @return {string} 400 - Validation error
179
   * @return {string} 500 - Internal server error
180
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
181
   */
1✔
182
  public async getSingleEvent(req: RequestWithToken, res: Response) {
1✔
183
    const { id } = req.params;
2✔
184
    this.logger.trace('Get single event with ID', id, 'by', req.token.user);
2✔
185

186
    try {
2✔
187
      const parsedId = Number.parseInt(id, 10);
2✔
188
      const event = await EventService.getSingleEvent(parsedId);
2✔
189
      if (event == null) {
2✔
190
        res.status(404).send();
1✔
191
        return;
1✔
192
      }
1✔
193
      res.json(EventService.asEventResponse(event));
1✔
194
    } catch (error) {
2!
195
      this.logger.error('Could not return single event:', error);
×
196
      res.status(500).json('Internal server error.');
×
197
    }
×
198
  }
2✔
199

200
  /**
1✔
201
   * POST /events
202
   * @summary Create an event with its corresponding answers objects
203
   * @tags events - Operations of the event controller
204
   * @operationId createEvent
205
   * @security JWT
206
   * @param {CreateEventRequest} request.body.required
207
   * @return {EventResponse} 200 - Created event
208
   * @return {string} 400 - Validation error
209
   * @return {string} 500 - Internal server error
210
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
211
   */
1✔
212
  public async createEvent(req: RequestWithToken, res: Response) {
1✔
213
    const body = req.body as EventRequest;
11✔
214
    this.logger.trace('Create event', body, 'by user', req.token.user);
11✔
215

216
    let params: CreateEventParams;
11✔
217
    try {
11✔
218
      params = {
11✔
219
        ...await parseUpdateEventRequestParameters(req),
11✔
220
        createdById: req.token.user.id,
1✔
221
      };
1✔
222
    } catch (e) {
11✔
223
      res.status(400).json(e.message);
10✔
224
      return;
10✔
225
    }
10✔
226

227
    // handle request
1✔
228
    try {
1✔
229
      const event = await EventService.createEvent(params);
1✔
230
      res.json(EventService.asEventResponse(event));
1✔
231
    } catch (error) {
11!
232
      this.logger.error('Could not create event:', error);
×
233
      res.status(500).json('Internal server error.');
×
234
    }
×
235
  }
11✔
236

237
  /**
1✔
238
   * PATCH /events/{id}
239
   * @summary Update an event with its corresponding answers objects
240
   * @tags events - Operations of the event controller
241
   * @operationId updateEvent
242
   * @security JWT
243
   * @param {integer} id.path.required - The id of the event which should be returned
244
   * @param {UpdateEventRequest} request.body.required
245
   * @return {EventResponse} 200 - Created event
246
   * @return {string} 400 - Validation error
247
   * @return {string} 500 - Internal server error
248
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
249
   */
1✔
250
  public async updateEvent(req: RequestWithToken, res: Response) {
1✔
251
    const { id } = req.params;
18✔
252
    const body = req.body as EventRequest;
18✔
253
    this.logger.trace('Update event', id, 'with body', body, 'by user', req.token.user);
18✔
254

255
    let parsedId = Number.parseInt(id, 10);
18✔
256
    try {
18✔
257
      const event = await EventService.getSingleEvent(parsedId);
18✔
258
      if (event == null) {
18✔
259
        res.status(404).send();
1✔
260
        return;
1✔
261
      }
1✔
262
    } catch (error) {
18!
263
      this.logger.error('Could not update event:', error);
×
264
      res.status(500).json('Internal server error.');
×
265
    }
✔
266

267
    let params: Partial<UpdateEventParams>;
17✔
268
    try {
17✔
269
      params = {
17✔
270
        ...await parseUpdateEventRequestParameters(req, true, parsedId),
17✔
271
      };
6✔
272
    } catch (e) {
18✔
273
      res.status(400).json(e.message);
11✔
274
      return;
11✔
275
    }
11✔
276

277
    // handle request
6✔
278
    try {
6✔
279
      const event = await EventService.updateEvent(parsedId, params);
6✔
280
      res.json(EventService.asEventResponse(event));
6✔
281
    } catch (error) {
18!
282
      this.logger.error('Could not update event:', error);
×
283
      res.status(500).json('Internal server error.');
×
284
    }
×
285
  }
18✔
286

287
  /**
1✔
288
   * DELETE /events/{id}
289
   * @summary Delete an event with its answers
290
   * @tags events - Operations of the event controller
291
   * @operationId deleteEvent
292
   * @security JWT
293
   * @param {integer} id.path.required - The id of the event which should be deleted
294
   * @return 204 - Success
295
   * @return {string} 400 - Validation error
296
   * @return {string} 500 - Internal server error
297
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
298
   */
1✔
299
  public async deleteEvent(req: RequestWithToken, res: Response) {
1✔
300
    const { id } = req.params;
2✔
301
    this.logger.trace('Get single event with ID', id, 'by', req.token.user);
2✔
302

303
    try {
2✔
304
      const parsedId = Number.parseInt(id, 10);
2✔
305
      const event = await EventService.getSingleEvent(parsedId);
2✔
306
      if (event == null) {
2✔
307
        res.status(404).send();
1✔
308
        return;
1✔
309
      }
1✔
310

311
      await EventService.deleteEvent(parsedId);
1✔
312
      res.status(204).send();
1✔
313
    } catch (error) {
2!
314
      this.logger.error('Could not delete event:', error);
×
315
      res.status(500).json('Internal server error.');
×
316
    }
×
317
  }
2✔
318

319
  /**
1✔
320
   * Synchronize an event, so that EventShiftAnswers are created/deleted
321
   * for users that are (no longer) part of a shift
322
   * @route GET /events/{id}/sync
323
   * @tags events - Operations of the event controller
324
   * @operationId syncEventShiftAnswers
325
   * @security JWT
326
   * @param {integer} id.path.required - The id of the event which should be returned
327
   * @return {EventResponse} 200 - All existing events
328
   * @return {string} 400 - Validation error
329
   * @return {string} 500 - Internal server error
330
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
331
   */
1✔
332
  public async syncEventShiftAnswers(req: RequestWithToken, res: Response) {
1✔
333
    const { id } = req.params;
2✔
334
    this.logger.trace('Synchronise single event with ID', id, 'by', req.token.user);
2✔
335

336
    try {
2✔
337
      const parsedId = Number.parseInt(id, 10);
2✔
338
      const event = await Event.findOne({ where: { id: parsedId }, relations: ['answers', 'shifts'] });
2✔
339
      if (event == null) {
2✔
340
        res.status(404).send();
1✔
341
        return;
1✔
342
      }
1✔
343

344
      await EventService.syncEventShiftAnswers(event);
1✔
345
      const updatedEvent = await EventService.getSingleEvent(parsedId);
1✔
346
      res.status(200).json(EventService.asEventResponse(updatedEvent));
1✔
347
    } catch (error) {
2!
348
      this.logger.error('Could not synchronize event answers:', error);
×
349
      res.status(500).json('Internal server error.');
×
350
    }
×
351
  }
2✔
352

353
  /**
1✔
354
   * PUT /events/{eventId}/shift/{shiftId}/user/{userId}/assign
355
   * @summary Change the assignment of users to shifts on an event
356
   * @tags events - Operations of the event controller
357
   * @operationId assignEventShift
358
   * @security JWT
359
   * @param {integer} eventId.path.required - The id of the event
360
   * @param {integer} shiftId.path.required - The id of the shift
361
   * @param {integer} userId.path.required - The id of the user
362
   * @param {EventAnswerAssignmentRequest} request.body.required
363
   * @return {BaseEventAnswerResponse} 200 - Created event
364
   * @return {string} 400 - Validation error
365
   * @return {string} 500 - Internal server error
366
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
367
   */
1✔
368
  public async assignEventShift(req: RequestWithToken, res: Response) {
1✔
369
    const { eventId: rawEventId, shiftId: rawShiftId, userId: rawUserId } = req.params;
6✔
370
    const body = req.body as EventAnswerAssignmentRequest;
6✔
371
    this.logger.trace('Update event shift selection for event', rawEventId, 'for shift', rawShiftId, 'for user', rawUserId, 'by', req.token.user);
6✔
372

373
    let eventId = Number.parseInt(rawEventId, 10);
6✔
374
    let shiftId = Number.parseInt(rawShiftId, 10);
6✔
375
    let userId = Number.parseInt(rawUserId, 10);
6✔
376
    try {
6✔
377
      const answer = await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId }, relations: ['event'] });
6✔
378
      if (answer == null) {
6✔
379
        res.status(404).send();
3✔
380
        return;
3✔
381
      }
3✔
382
      if (answer.event.startDate.getTime() < new Date().getTime()) {
6✔
383
        res.status(400).json('Event has already started or is already over.');
1✔
384
        return;
1✔
385
      }
1✔
386
    } catch (error) {
6!
387
      this.logger.error('Could not update event:', error);
×
388
      res.status(500).json('Internal server error.');
×
389
      return;
×
390
    }
✔
391

392
    let params: Partial<UpdateEventAnswerParams> = {
2✔
393
      selected: body.selected,
2✔
394
    };
2✔
395

396
    // handle request
2✔
397
    try {
2✔
398
      const answer = await EventService.updateEventShiftAnswer(eventId, shiftId, userId, params);
2✔
399
      res.json(answer);
2✔
400
    } catch (error) {
6!
401
      this.logger.error('Could not update event:', error);
×
402
      res.status(500).json('Internal server error.');
×
403
    }
×
404
  }
6✔
405

406
  /**
1✔
407
   * POST /events/{eventId}/shift/{shiftId}/user/{userId}/availability
408
   * @summary Update the availability of a user for a shift in an event
409
   * @tags events - Operations of the event controller
410
   * @operationId updateEventShiftAvailability
411
   * @security JWT
412
   * @param {integer} eventId.path.required - The id of the event
413
   * @param {integer} shiftId.path.required - The id of the shift
414
   * @param {integer} userId.path.required - The id of the user
415
   * @param {EventAnswerAvailabilityRequest} request.body.required
416
   * @return {BaseEventAnswerResponse} 200 - Created event
417
   * @return {string} 400 - Validation error
418
   * @return {string} 500 - Internal server error
419
   * @deprecated Events are out of scope for SudoSOS. Delete from 01/11/2026.
420
   */
1✔
421
  public async updateShiftAvailability(req: RequestWithToken, res: Response) {
1✔
422
    const { userId: rawUserId, shiftId: rawShiftId, eventId: rawEventId } = req.params;
6✔
423
    const body = req.body as EventAnswerAvailabilityRequest;
6✔
424
    this.logger.trace('Update event shift availability for user', rawUserId, 'for shift', rawShiftId, 'for event', rawEventId, 'by', req.token.user);
6✔
425

426
    let userId = Number.parseInt(rawUserId, 10);
6✔
427
    let shiftId = Number.parseInt(rawShiftId, 10);
6✔
428
    let eventId = Number.parseInt(rawEventId, 10);
6✔
429
    try {
6✔
430
      const answer = await EventShiftAnswer.findOne({ where: { eventId, shiftId, userId }, relations: ['event'] });
6✔
431
      if (answer == null) {
6✔
432
        res.status(404).send();
3✔
433
        return;
3✔
434
      }
3✔
435
      if (answer.event.startDate.getTime() < new Date().getTime()) {
6✔
436
        res.status(400).json('Event has already started or is already over.');
1✔
437
        return;
1✔
438
      }
1✔
439
    } catch (error) {
6!
440
      this.logger.error('Could not update event:', error);
×
441
      res.status(500).json('Internal server error.');
×
442
      return;
×
443
    }
✔
444

445
    let params: Partial<UpdateEventAnswerParams>;
2✔
446
    try {
2✔
447
      params = {
2✔
448
        availability: asShiftAvailability(body.availability),
2✔
449
      };
2✔
450
    } catch (e) {
6✔
451
      res.status(400).json('Invalid event availability.');
1✔
452
      return;
1✔
453
    }
1✔
454

455
    // handle request
1✔
456
    try {
1✔
457
      const answer = await EventService.updateEventShiftAnswer(eventId, shiftId, userId, params);
1✔
458
      res.json(answer);
1✔
459
    } catch (error) {
6!
460
      this.logger.error('Could not update event:', error);
×
461
      res.status(500).json('Internal server error.');
×
462
    }
×
463
  }
6✔
464
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc