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

GEWIS / sudosos-backend / 18700071187

21 Oct 2025 11:03PM UTC coverage: 89.878% (+0.002%) from 89.876%
18700071187

Pull #471

github

web-flow
Merge accc7f7b5 into d3814f109
Pull Request #471: Add `authentication` docs

1370 of 1628 branches covered (84.15%)

Branch coverage included in aggregate %.

1 of 2 new or added lines in 1 file covered. (50.0%)

10 existing lines in 2 files now uncovered.

7208 of 7916 relevant lines covered (91.06%)

1111.83 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 Authentication
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
 * @module Internal/Controllers
52
 * @promote
53
 */
54
export default class AuthenticationSecureController extends BaseController {
2✔
55
  private logger: Logger = log4js.getLogger('AuthenticationController');
2✔
56

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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