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

GEWIS / sudosos-backend / 23234935187

18 Mar 2026 08:04AM UTC coverage: 89.109% (-0.2%) from 89.315%
23234935187

push

github

web-flow
refactor: migrate test suite to use production roles instead. (#782)

1781 of 2184 branches covered (81.55%)

Branch coverage included in aggregate %.

9207 of 10147 relevant lines covered (90.74%)

996.99 hits per line

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

86.13
/src/controller/banner-controller.ts
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
 */
20

21
/**
22
 * @module banners
23
 */
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
2✔
27
import { UploadedFile } from 'express-fileupload';
28
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
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';
2✔
33
import { globalAsyncValidatorRegistry } from '../middleware/async-validator-registry';
2✔
34
import bannerRequestSpec from './request/validators/banner-request-spec';
2✔
35
import Banner from '../entity/banner';
2✔
36
import FileService from '../service/file-service';
2✔
37
import { BANNER_IMAGE_LOCATION } from '../files/storage';
2✔
38
import { parseRequestPagination, toResponse } from '../helpers/pagination';
2✔
39
import { asBoolean } from '../helpers/validators';
2✔
40

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

47
  private fileService: FileService;
48

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

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

106
  /**
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
   */
121
  public async returnAllBanners(req: RequestWithToken, res: Response): Promise<void> {
122
    this.logger.trace('Get all banners by', req.token.user);
13✔
123

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

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

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

142
    if (req.query.expired !== undefined) {
13✔
143
      filters.expired = asBoolean(req.query.expired);
3✔
144
    }
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 {
151
        res.status(400).send('Invalid order parameter. Must be ASC or DESC.');
1✔
152
        return;
1✔
153
      }
154
    }
155

156
    // handle request
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) {
161
      this.logger.error('Could not return all banners:', error);
×
162
      res.status(500).json('Internal server error.');
×
163
    }
164
  }
165

166
  /**
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 {object} 400 - Validation error
175
   * @return {string} 500 - Internal server error
176
   */
177
  public async createBanner(req: RequestWithToken, res: Response): Promise<void> {
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
182
    try {
1✔
183
      const banner = await BannerService.createBanner(body);
1✔
184
      res.json(BannerService.asBannerResponse(banner));
1✔
185
    } catch (error) {
186
      this.logger.error('Could not create banner:', error);
×
187
      res.status(500).json('Internal server error.');
×
188
    }
189
  }
190

191
  /**
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
   */
203
  public async uploadBannerImage(req: RequestWithToken, res: Response): Promise<void> {
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
    }
212
    if (files.file === undefined) {
5✔
213
      res.status(400).send("No file is uploaded in the 'file' field");
1✔
214
      return;
1✔
215
    }
216
    const file = files.file as UploadedFile;
4✔
217
    if (file.data === undefined) {
4✔
218
      res.status(400).send('File body data is missing from request');
1✔
219
      return;
1✔
220
    }
221
    if (file.name === undefined) {
3!
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: ['image'] });
3✔
230
      if (banner) {
3✔
231
        await this.fileService.uploadEntityImage(
2✔
232
          banner, file, req.token.user,
233
        );
234
        res.status(204).send();
2✔
235
        return;
2✔
236
      }
237
      res.status(404).json('Banner not found');
1✔
238
      return;
1✔
239
    } catch (error) {
240
      this.logger.error('Could not upload image:', error);
×
241
      res.status(500).json('Internal server error');
×
242
    }
243
  }
244

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

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

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

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

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

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

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

351
    const { take, skip } = parseRequestPagination(req);
3✔
352

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