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

GEWIS / sudosos-backend / 27283082186

10 Jun 2026 02:13PM UTC coverage: 91.996% (+0.04%) from 91.956%
27283082186

push

github

web-flow
chore(deps): bump bullmq from 5.77.6 to 5.78.0 (#949)

Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 5.77.6 to 5.78.0.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.77.6...v5.78.0)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-version: 5.78.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

4199 of 4800 branches covered (87.48%)

Branch coverage included in aggregate %.

21593 of 23236 relevant lines covered (92.93%)

847.01 hits per line

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

86.86
/src/controller/banner-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
 * @module banners
23
 */
1✔
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
27
import { UploadedFile } from 'express-fileupload';
28
import BaseController, { BaseControllerOptions } from './base-controller';
29
import Policy from './policy';
30
import BannerRequest from './request/banner-request';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import BannerService, { BannerFilterParameters } from '../service/banner-service';
33
import { globalAsyncValidatorRegistry } from '../middleware/async-validator-registry';
34
import bannerRequestSpec from './request/validators/banner-request-spec';
35
import Banner from '../entity/banner';
36
import FileService from '../service/file-service';
37
import { BANNER_IMAGE_LOCATION } from '../files/storage';
38
import { parseRequestPagination, toResponse } from '../helpers/pagination';
39
import { asBoolean } from '../helpers/validators';
40

41
/**
1✔
42
 * Controller for managing all routes related to the `banner` entity.
43
 */
1✔
44
export default class BannerController extends BaseController {
1✔
45
  private logger: Logger = log4js.getLogger('BannerController');
1✔
46

47
  private fileService: FileService;
48

49
  /**
1✔
50
   * Creates a new banner controller instance.
51
   * @param options - The options passed to the base controller.
52
   */
1✔
53
  public constructor(options: BaseControllerOptions) {
1✔
54
    super(options);
2✔
55
    this.configureLogger(this.logger);
2✔
56
    this.fileService = new FileService(BANNER_IMAGE_LOCATION);
2✔
57
    globalAsyncValidatorRegistry.register('BannerRequest', bannerRequestSpec);
2✔
58
  }
2✔
59

60
  /**
1✔
61
   * @inheritdoc
62
   */
1✔
63
  public getPolicy(): Policy {
1✔
64
    return {
2✔
65
      '/': {
2✔
66
        GET: {
2✔
67
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Banner', ['*']),
2✔
68
          handler: this.returnAllBanners.bind(this),
2✔
69
        },
2✔
70
        POST: {
2✔
71
          body: { modelName: 'BannerRequest' },
2✔
72
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Banner', ['*']),
2✔
73
          handler: this.createBanner.bind(this),
2✔
74
        },
2✔
75
      },
2✔
76
      '/:id(\\d+)': {
2✔
77
        GET: {
2✔
78
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Banner', ['*']),
2✔
79
          handler: this.returnSingleBanner.bind(this),
2✔
80
        },
2✔
81
        PATCH: {
2✔
82
          body: { modelName: 'BannerRequest' },
2✔
83
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Banner', ['*']),
2✔
84
          handler: this.updateBanner.bind(this),
2✔
85
        },
2✔
86
        DELETE: {
2✔
87
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', 'all', 'Banner', ['*']),
2✔
88
          handler: this.removeBanner.bind(this),
2✔
89
        },
2✔
90
      },
2✔
91
      '/:id(\\d+)/image': {
2✔
92
        POST: {
2✔
93
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', 'all', 'Banner', ['*']),
2✔
94
          handler: this.uploadBannerImage.bind(this),
2✔
95
        },
2✔
96
      },
2✔
97
      '/active': {
2✔
98
        GET: {
2✔
99
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Banner', ['*']),
2✔
100
          handler: this.returnActiveBanners.bind(this),
2✔
101
        },
2✔
102
      },
2✔
103
    };
2✔
104
  }
2✔
105

106
  /**
1✔
107
   * GET /banners
108
   * @summary Returns all existing banners
109
   * @operationId getAllBanners
110
   * @tags banners - Operations of banner controller
111
   * @security JWT
112
   * @param {integer} take.query - How many banners the endpoint should return
113
   * @param {integer} skip.query - How many banners should be skipped (for pagination)
114
   * @param {boolean} active.query - Filter by active status
115
   * @param {boolean} expired.query - Filter by expired status (endDate <= now)
116
   * @param {string} order.query - Sort order by startDate (ASC or DESC, default: DESC)
117
   * @return {PaginatedBannerResponse} 200 - All existing banners
118
   * @return {string} 400 - Validation error
119
   * @return {string} 500 - Internal server error
120
   */
1✔
121
  public async returnAllBanners(req: RequestWithToken, res: Response): Promise<void> {
1✔
122
    this.logger.trace('Get all banners by', req.token.user);
13✔
123

124
    let take;
13✔
125
    let skip;
13✔
126
    try {
13✔
127
      const pagination = parseRequestPagination(req);
13✔
128
      take = pagination.take;
13✔
129
      skip = pagination.skip;
13✔
130
    } catch (e) {
13!
131
      res.status(400).send(e.message);
×
132
      return;
×
133
    }
×
134

135
    // Parse filter parameters
13✔
136
    const filters: BannerFilterParameters = {};
13✔
137

138
    if (req.query.active !== undefined) {
13✔
139
      filters.active = asBoolean(req.query.active);
4✔
140
    }
4✔
141

142
    if (req.query.expired !== undefined) {
13✔
143
      filters.expired = asBoolean(req.query.expired);
3✔
144
    }
3✔
145

146
    if (req.query.order !== undefined) {
13✔
147
      const order = String(req.query.order).toUpperCase();
4✔
148
      if (order === 'ASC' || order === 'DESC') {
4✔
149
        filters.order = order;
3✔
150
      } else {
4✔
151
        res.status(400).send('Invalid order parameter. Must be ASC or DESC.');
1✔
152
        return;
1✔
153
      }
1✔
154
    }
4✔
155

156
    // handle request
12✔
157
    try {
12✔
158
      const [banners, count] = await BannerService.getBanners(filters, { take, skip });
12✔
159
      res.json(toResponse(banners.map(BannerService.asBannerResponse), count, { take, skip }));
12✔
160
    } catch (error) {
13!
161
      this.logger.error('Could not return all banners:', error);
×
162
      res.status(500).json('Internal server error.');
×
163
    }
×
164
  }
13✔
165

166
  /**
1✔
167
   * POST /banners
168
   * @summary Saves a banner to the database
169
   * @operationId create
170
   * @tags banners - Operations of banner controller
171
   * @param {BannerRequest} request.body.required - The banner which should be created
172
   * @security JWT
173
   * @return {BannerResponse} 200 - The created banner entity
174
   * @return {ValidationResponse} 400 - Validation error
175
   * @return {string} 500 - Internal server error
176
   */
1✔
177
  public async createBanner(req: RequestWithToken, res: Response): Promise<void> {
1✔
178
    const body = req.body as BannerRequest;
1✔
179
    this.logger.trace('Create banner', body, 'by user', req.token.user);
1✔
180

181
    // handle request
1✔
182
    try {
1✔
183
      const banner = await BannerService.createBanner(body);
1✔
184
      res.json(BannerService.asBannerResponse(banner));
1✔
185
    } catch (error) {
1!
186
      this.logger.error('Could not create banner:', error);
×
187
      res.status(500).json('Internal server error.');
×
188
    }
×
189
  }
1✔
190

191
  /**
1✔
192
   * POST /banners/{id}/image
193
   * @summary Uploads a banner image to the given banner
194
   * @operationId updateImage
195
   * @tags banners - Operations of banner controller
196
   * @param {integer} id.path.required - The id of the banner
197
   * @param {FileRequest} request.body.required - banner image - multipart/form-data
198
   * @security JWT
199
   * @return 204 - Success
200
   * @return {string} 400 - Validation error
201
   * @return {string} 500 - Internal server error
202
   */
1✔
203
  public async uploadBannerImage(req: RequestWithToken, res: Response): Promise<void> {
1✔
204
    const { id } = req.params;
7✔
205
    const { files } = req;
7✔
206
    this.logger.trace('Upload banner image for banner', id, 'by user', req.token.user);
7✔
207

208
    if (!req.files || Object.keys(files).length !== 1) {
7✔
209
      res.status(400).send('No file or too many files were uploaded');
2✔
210
      return;
2✔
211
    }
2✔
212
    if (files.file === undefined) {
7✔
213
      res.status(400).send("No file is uploaded in the 'file' field");
1✔
214
      return;
1✔
215
    }
1✔
216
    const file = files.file as UploadedFile;
4✔
217
    if (file.data === undefined) {
7✔
218
      res.status(400).send('File body data is missing from request');
1✔
219
      return;
1✔
220
    }
1✔
221
    if (file.name === undefined) {
7!
222
      res.status(400).send('File name is missing from request');
×
223
      return;
×
224
    }
✔
225

226
    const bannerId = parseInt(id, 10);
3✔
227

228
    try {
3✔
229
      const banner = await Banner.findOne({ where: { id: bannerId }, relations: {
3✔
230
        image: true,
3✔
231
      } });
3✔
232
      if (banner) {
7✔
233
        await this.fileService.uploadEntityImage(
2✔
234
          banner, file, req.token.user,
2✔
235
        );
236
        res.status(204).send();
2✔
237
        return;
2✔
238
      }
2✔
239
      res.status(404).json('Banner not found');
1✔
240
      return;
1✔
241
    } catch (error) {
7!
242
      this.logger.error('Could not upload image:', error);
×
243
      res.status(500).json('Internal server error');
×
244
    }
×
245
  }
7✔
246

247
  /**
1✔
248
   * GET /banners/{id}
249
   * @summary Returns the requested banner
250
   * @operationId getBanner
251
   * @tags banners - Operations of banner controller
252
   * @param {integer} id.path.required - The id of the banner which should be returned
253
   * @security JWT
254
   * @return {BannerResponse} 200 - The requested banner entity
255
   * @return {string} 404 - Not found error
256
   * @return {string} 500 - Internal server error
257
   */
1✔
258
  public async returnSingleBanner(req: RequestWithToken, res: Response): Promise<void> {
1✔
259
    const { id } = req.params;
4✔
260
    this.logger.trace('Get single banner', id, 'by user', req.token.user);
4✔
261

262
    // handle request
4✔
263
    try {
4✔
264
      // check if banner in database
4✔
265
      const [banners] = await BannerService.getBanners({ bannerId: Number.parseInt(id, 10) });
4✔
266
      if (banners.length > 0) {
4✔
267
        res.json(BannerService.asBannerResponse(banners[0]));
3✔
268
      } else {
4✔
269
        res.status(404).json('Banner not found.');
1✔
270
      }
1✔
271
    } catch (error) {
4!
272
      this.logger.error('Could not return banner:', error);
×
273
      res.status(500).json('Internal server error.');
×
274
    }
×
275
  }
4✔
276

277
  /**
1✔
278
   * PATCH /banners/{id}
279
   * @summary Updates the requested banner
280
   * @operationId update
281
   * @tags banners - Operations of banner controller
282
   * @param {integer} id.path.required - The id of the banner which should be updated
283
   * @param {BannerRequest} request.body.required - The updated banner
284
   * @security JWT
285
   * @return {BannerResponse} 200 - The requested banner entity
286
   * @return {ValidationResponse} 400 - Validation error
287
   * @return {string} 404 - Not found error
288
   * @return {string} 500 - Internal server error
289
   */
1✔
290
  public async updateBanner(req: RequestWithToken, res: Response): Promise<void> {
1✔
291
    const body = req.body as BannerRequest;
2✔
292
    const { id } = req.params;
2✔
293
    this.logger.trace('Update banner', id, 'by user', req.token.user);
2✔
294

295
    // handle request
2✔
296
    try {
2✔
297
      const banner = await BannerService.updateBanner(Number.parseInt(id, 10), body);
2✔
298
      if (banner) {
2✔
299
        res.json(BannerService.asBannerResponse(banner));
1✔
300
      } else {
1✔
301
        res.status(404).json('Banner not found.');
1✔
302
      }
1✔
303
    } catch (error) {
2!
304
      this.logger.error('Could not update banner:', error);
×
305
      res.status(500).json('Internal server error.');
×
306
    }
×
307
  }
2✔
308

309
  /**
1✔
310
   * DELETE /banners/{id}
311
   * @summary Deletes the requested banner
312
   * @operationId delete
313
   * @tags banners - Operations of banner controller
314
   * @param {integer} id.path.required - The id of the banner which should be deleted
315
   * @security JWT
316
   * @return 204 - Update success
317
   * @return {string} 404 - Not found error
318
   */
1✔
319
  public async removeBanner(req: RequestWithToken, res: Response): Promise<void> {
1✔
320
    const { id } = req.params;
2✔
321
    this.logger.trace('Remove ban ner', id, 'by user', req.token.user);
2✔
322

323
    // handle request
2✔
324
    try {
2✔
325
      // check if banner in database
2✔
326
      const banner = await BannerService.deleteBanner(Number.parseInt(id, 10), this.fileService);
2✔
327
      if (banner) {
2✔
328
        res.status(204).json();
1✔
329
      } else {
1✔
330
        res.status(404).json('Banner not found.');
1✔
331
      }
1✔
332
    } catch (error) {
2!
333
      this.logger.error('Could not remove banner:', error);
×
334
      res.status(500).json('Internal server error.');
×
335
    }
×
336
  }
2✔
337

338
  /**
1✔
339
   * GET /banners/active
340
   * @summary Returns all active banners
341
   * @operationId getActive
342
   * @tags banners - Operations of banner controller
343
   * @security JWT
344
   * @param {integer} take.query - How many banners the endpoint should return
345
   * @param {integer} skip.query - How many banners should be skipped (for pagination)
346
   * @return {PaginatedBannerResponse} 200 - All active banners
347
   * @return {string} 400 - Validation error
348
   */
1✔
349
  public async returnActiveBanners(req: RequestWithToken, res: Response): Promise<void> {
1✔
350
    const { body } = req;
3✔
351
    this.logger.trace('Get active banners', body, 'by user', req.token.user);
3✔
352

353
    const { take, skip } = parseRequestPagination(req);
3✔
354

355
    // handle request
3✔
356
    try {
3✔
357
      const [banners, count] = await BannerService.getBanners({ active: true }, { take, skip });
3✔
358
      res.json(toResponse(banners.map(BannerService.asBannerResponse), count, { take, skip }));
3✔
359
    } catch (error) {
3!
360
      this.logger.error('Could not return active banners:', error);
×
361
      res.status(500).json('Internal server error.');
×
362
    }
×
363
  }
3✔
364
}
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