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

GEWIS / sudosos-backend / 18949526732

28 Oct 2025 08:59AM UTC coverage: 89.883% (-0.004%) from 89.887%
18949526732

push

github

web-flow
refactor: remove old deprecated member-authenticator.ts and move to organ membership (#618)

1373 of 1631 branches covered (84.18%)

Branch coverage included in aggregate %.

28 of 29 new or added lines in 6 files covered. (96.55%)

84 existing lines in 11 files now uncovered.

7218 of 7927 relevant lines covered (91.06%)

1112.1 hits per line

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

93.55
/src/controller/authentication-secure-controller.ts
1
/**
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2024  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 internal/controllers
23
 */
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
2✔
27
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
28
import Policy from './policy';
29
import { RequestWithToken } from '../middleware/token-middleware';
30
import AuthenticationService from '../service/authentication-service';
2✔
31
import TokenHandler from '../authentication/token-handler';
32
import User from '../entity/user/user';
2✔
33
import PointOfSaleController from './point-of-sale-controller';
2✔
34
import PointOfSale from '../entity/point-of-sale/point-of-sale';
2✔
35
import ServerSettingsStore from '../server-settings/server-settings-store';
2✔
36
import { ISettings } from '../entity/server-setting';
37
import { QRAuthenticatorStatus } from '../entity/authenticator/qr-authenticator';
2✔
38
import WebSocketService from '../service/websocket-service';
2✔
39
import QRService from '../service/qr-service';
2✔
40

41
/**
42
 * Handles authenticated-only authentication endpoints for token management and specialized flows.
43
 * All endpoints require valid JWT tokens and build upon existing authentication.
44
 *
45
 * ## Internal Implementation Notes
46
 * - Token refresh maintains the same access level (lesser/full) as the original token
47
 * - POS authentication uses custom expiry settings from server settings
48
 * - QR confirmation integrates with WebSocket service for real-time notifications
49
 * - All methods use the role manager for permission validation
50
 *
51
 * @promote
52
 */
53
export default class AuthenticationSecureController extends BaseController {
2✔
54
  private logger: Logger = log4js.getLogger('AuthenticationController');
2✔
55

56
  /**
57
   * Reference to the token handler of the application.
58
   */
59
  protected tokenHandler: TokenHandler;
60

61
  /**
62
   * Creates a new banner controller instance.
63
   * @param options - The options passed to the base controller.
64
   * @param tokenHandler - The token handler for creating signed tokens.
65
   */
66
  public constructor(options: BaseControllerOptions, tokenHandler: TokenHandler) {
67
    super(options);
2✔
68
    this.logger.level = process.env.LOG_LEVEL;
2✔
69
    this.tokenHandler = tokenHandler;
2✔
70
  }
71

72
  /**
73
   * @inheritdoc
74
   */
75
  public getPolicy(): Policy {
76
    return {
2✔
77
      '/refreshToken': {
78
        GET: {
79
          policy: async () => Promise.resolve(true),
1✔
80
          handler: this.refreshToken.bind(this),
81
          restrictions: { lesser: true, acceptedTOS: false },
82
        },
83
      },
84
      '/pointofsale/:id(\\d+)': {
85
        GET: {
86
          policy: async (req) => this.roleManager.can(req.token.roles, 'authenticate', await PointOfSaleController.getRelation(req), 'User', ['pointOfSale']),
6✔
87
          handler: this.authenticatePointOfSale.bind(this),
88
        },
89
      },
90
      '/qr/:sessionId/confirm': {
91
        POST: {
92
          policy: async () => Promise.resolve(true),
11✔
93
          handler: this.confirmQRCode.bind(this),
94
          restrictions: { lesser: false },
95
        },
96
      },
97
    };
98
  }
99

100
  /**
101
   * GET /authentication/refreshToken
102
   * @summary Get a new JWT token, lesser if the existing token is also lesser
103
   * @operationId refreshToken
104
   * @tags authenticate - Operations of the authentication controller
105
   * @security JWT
106
   * @return {AuthenticationResponse} 200 - The created json web token.
107
   */
108
  private async refreshToken(req: RequestWithToken, res: Response): Promise<void> {
109
    this.logger.trace('Refresh token for user', req.token.user.id);
1✔
110

111
    try {
1✔
112
      const user = await User.findOne({ where: { id: req.token.user.id } });
1✔
113
      const token = await new AuthenticationService().getSaltedToken(user, {
1✔
114
        roleManager: this.roleManager,
115
        tokenHandler: this.tokenHandler,
116
      }, req.token.lesser);
117
      res.json(token);
1✔
118
    } catch (error) {
UNCOV
119
      this.logger.error('Could not create token:', error);
×
UNCOV
120
      res.status(500).json('Internal server error.');
×
121
    }
122
  }
123

124
  /**
125
   * GET /authentication/pointofsale/{id}
126
   * @summary Get a JWT token for the given POS
127
   * @operationId authenticatePointOfSale
128
   * @tags authenticate - Operations of the authentication controller
129
   * @security JWT
130
   * @param {integer} id.path.required - The id of the user
131
   * @return {AuthenticationResponse} 200 - The created json web token.
132
   * @return {string} 404 - Point of sale not found
133
   * @return {string} 500 - Internal server error
134
   */
135
  private async authenticatePointOfSale(req: RequestWithToken, res: Response): Promise<void> {
136
    this.logger.trace('Authenticate point of sale', req.params.id, 'by user', req.token.user.id);
5✔
137

138
    try {
5✔
139
      const pointOfSaleId = Number(req.params.id);
5✔
140
      const pointOfSale = await PointOfSale.findOne({ where: { id: pointOfSaleId }, relations: { user: true } });
5✔
141
      if (!pointOfSale) {
5✔
142
        res.status(404).json('Point of sale not found.');
2✔
143
        return;
2✔
144
      }
145

146
      const expiry = ServerSettingsStore.getInstance().getSetting('jwtExpiryPointOfSale') as ISettings['jwtExpiryPointOfSale'];
3✔
147
      const token = await new AuthenticationService().getSaltedToken(pointOfSale.user, {
3✔
148
        roleManager: this.roleManager,
149
        tokenHandler: this.tokenHandler,
150
      }, false, undefined, expiry);
151
      res.json(token);
3✔
152
    } catch (error) {
UNCOV
153
      this.logger.error('Could not create token:', error);
×
UNCOV
154
      res.status(500).json('Internal server error.');
×
155
    }
156
  }
157

158
  /**
159
   * POST /authentication/qr/{sessionId}/confirm
160
   * @summary Confirm QR code authentication from mobile app
161
   * @operationId confirmQRCode
162
   * @tags authenticate - Operations of authentication controller
163
   * @param {string} sessionId.path.required - The session ID
164
   * @security JWT
165
   * @return 200 - Successfully confirmed
166
   * @return {string} 400 - Validation error
167
   * @return {string} 404 - Session not found
168
   * @return {string} 410 - Session expired
169
   * @return {string} 500 - Internal server error
170
   */
171
  private async confirmQRCode(req: RequestWithToken, res: Response): Promise<void> {
172
    const { sessionId } = req.params;
11✔
173
    this.logger.trace('Confirming QR code for session', sessionId, 'by user', req.token.user);
11✔
174

175
    try {
11✔
176
      const qrAuthenticator = await (new QRService()).get(sessionId);
11✔
177
      if (!qrAuthenticator) {
10✔
178
        res.status(404).json('Session not found.');
1✔
179
        return;
1✔
180
      }
181

182
      if (qrAuthenticator.status === QRAuthenticatorStatus.EXPIRED) {
9✔
183
        res.status(410).json('Session has expired.');
1✔
184
        return;
1✔
185
      }
186

187
      if (qrAuthenticator.status !== QRAuthenticatorStatus.PENDING) {
8✔
188
        res.status(400).json('Session is no longer pending.');
2✔
189
        return;
2✔
190
      }
191

192
      const user = await User.findOne({ where: { id: req.token.user.id } });
6✔
193
      const token = await new AuthenticationService().getSaltedToken(user, {
6✔
194
        roleManager: this.roleManager,
195
        tokenHandler: this.tokenHandler,
196
      }, req.token.lesser);
197

198
      // Let the service handle all business logic validation
199
      await (new QRService()).confirm(qrAuthenticator, user);
5✔
200

201
      // Notify WebSocket clients about the confirmation
202
      WebSocketService.emitQRConfirmed(qrAuthenticator, token);
4✔
203
      res.status(200).json({ message: 'QR code confirmed successfully.' });
3✔
204
    } catch (error) {
205
      this.logger.error('Could not confirm QR code:', error);
4✔
206
      res.status(500).json('Internal server error.');
4✔
207
    }
208
  }
209
}
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