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

GEWIS / sudosos-backend / 15953859818

29 Jun 2025 09:21AM UTC coverage: 85.104% (+0.01%) from 85.094%
15953859818

push

github

web-flow
feat: check filter params for RBAC relation in get all transaction requests (#550)

1291 of 1576 branches covered (81.92%)

Branch coverage included in aggregate %.

9 of 10 new or added lines in 1 file covered. (90.0%)

6936 of 8091 relevant lines covered (85.72%)

1080.9 hits per line

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

87.41
/src/controller/transaction-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
 * This is the module page of the transaction-controller.
23
 *
24
 * @module transactions
25
 */
26

27
import { Response } from 'express';
28
import log4js, { Logger } from 'log4js';
2✔
29
import BaseController, { BaseControllerOptions } from './base-controller';
2✔
30
import Policy from './policy';
31
import { RequestWithToken } from '../middleware/token-middleware';
32
import TransactionService, {
2✔
33
  parseGetTransactionsFilters,
34
} from '../service/transaction-service';
35
import { TransactionResponse } from './response/transaction-response';
36
import { parseRequestPagination } from '../helpers/pagination';
2✔
37
import { TransactionRequest } from './request/transaction-request';
38
import Transaction from '../entity/transactions/transaction';
2✔
39
import User from '../entity/user/user';
2✔
40
import { asNumber } from '../helpers/validators';
2✔
41
import userTokenInOrgan from '../helpers/token-helper';
2✔
42
import UserService from '../service/user-service';
2✔
43

44
export default class TransactionController extends BaseController {
2✔
45
  private logger: Logger = log4js.getLogger('TransactionController');
3✔
46

47
  /**
48
   * Creates a new transaction controller instance.
49
   * @param options - The options passed to the base controller.
50
   */
51
  public constructor(options: BaseControllerOptions) {
52
    super(options);
3✔
53
    this.logger.level = process.env.LOG_LEVEL;
3✔
54
  }
55

56
  /**
57
   * @inheritDoc
58
   */
59
  public getPolicy(): Policy {
60
    return {
3✔
61
      '/': {
62
        GET: {
63
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransactionController.filterRelation(req), 'Transaction', ['*']),
32✔
64
          handler: this.getAllTransactions.bind(this),
65
        },
66
        POST: {
67
          body: { modelName: 'TransactionRequest' },
68
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await TransactionController.postRelation(req), 'Transaction', ['*']),
6✔
69
          handler: this.createTransaction.bind(this),
70
        },
71
      },
72
      '/:id(\\d+)': {
73
        GET: {
74
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', await TransactionController.getRelation(req), 'Transaction', ['*']),
7✔
75
          handler: this.getTransaction.bind(this),
76
        },
77
        PATCH: {
78
          body: { modelName: 'TransactionRequest' },
79
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', await TransactionController.postRelation(req), 'Transaction', ['*']),
4✔
80
          handler: this.updateTransaction.bind(this),
81
        },
82
        DELETE: {
83
          policy: async (req) => this.roleManager.can(req.token.roles, 'delete', await TransactionController.getRelation(req), 'Transaction', ['*']),
3✔
84
          handler: this.deleteTransaction.bind(this),
85
        },
86
      },
87
      '/:validate': {
88
        POST: {
89
          policy: async (req) => this.roleManager.can(req.token.roles, 'create', await TransactionController.getRelation(req), 'Transaction', ['*']),
2✔
90
          handler: this.validateTransaction.bind(this),
91
        },
92
      },
93
    };
94
  }
95

96
  /**
97
   * GET /transactions
98
   * @summary Get a list of all transactions
99
   * @operationId getAllTransactions
100
   * @tags transactions - Operations of the transaction controller
101
   * @security JWT
102
   * @param {integer} fromId.query - From-user for selected transactions
103
   * @param {integer} createdById.query - User that created selected transaction
104
   * @param {integer} toId.query - To-user for selected transactions
105
   * transactions. Requires ContainerId
106
   * @param {integer} excludeById.query - Your own ID to not include in transactions
107
   * @param {integer} pointOfSaleId.query - Point of sale ID for selected transactions
108
   * @param {integer} productId.query - Product ID for selected transactions
109
   * @param {integer} productRevision.query - Product Revision for selected
110
   * transactions. Requires ProductID
111
   * @param {string} fromDate.query - Start date for selected transactions (inclusive)
112
   * @param {string} tillDate.query - End date for selected transactions (exclusive)
113
   * @param {integer} take.query - How many transactions the endpoint should return
114
   * @param {integer} skip.query - How many transactions should be skipped (for pagination)
115
   * @return {PaginatedBaseTransactionResponse} 200 - A list of all transactions
116
   */
117
  public async getAllTransactions(req: RequestWithToken, res: Response): Promise<void> {
118
    this.logger.trace('Get all transactions by user', req.token.user);
28✔
119

120
    // Parse the filters given in the query parameters. If there are any issues,
121
    // the parse method will throw an exception. We will then return a 400 error.
122
    let filters;
123
    let take;
124
    let skip;
125
    try {
28✔
126
      filters = parseGetTransactionsFilters(req);
28✔
127
      const pagination = parseRequestPagination(req);
20✔
128
      take = pagination.take;
17✔
129
      skip = pagination.skip;
17✔
130
    } catch (e) {
131
      res.status(400).json(e.message);
11✔
132
      return;
11✔
133
    }
134

135
    try {
17✔
136
      const transactions = await new TransactionService().getTransactions(filters, { take, skip });
17✔
137
      res.status(200).json(transactions);
17✔
138
    } catch (e) {
139
      res.status(500).send();
×
140
      this.logger.error(e);
×
141
    }
142
  }
143

144
  /**
145
   * POST /transactions
146
   * @summary Creates a new transaction
147
   * @operationId createTransaction
148
   * @tags transactions - Operations of the transaction controller
149
   * @param {TransactionRequest} request.body.required -
150
   * The transaction which should be created
151
   * @security JWT
152
   * @return {TransactionResponse} 200 - The created transaction entity
153
   * @return {string} 400 - Validation error
154
   * @return {string} 403 - Insufficient balance error
155
   * @return {string} 500 - Internal server error
156
   */
157
  public async createTransaction(req: RequestWithToken, res: Response): Promise<void> {
158
    const body = req.body as TransactionRequest;
4✔
159
    this.logger.trace('Create transaction', body, 'by user', req.token.user);
4✔
160

161
    // handle request
162
    try {
4✔
163
      if (!await new TransactionService().verifyTransaction(body)) {
4✔
164
        res.status(400).json('Invalid transaction.');
1✔
165
        return;
1✔
166
      }
167

168
      // verify balance if user cannot have negative balance.
169
      const user = await User.findOne({ where: { id: body.from } });
3✔
170
      if (!user.canGoIntoDebt && !await new TransactionService().verifyBalance(body)) {
3✔
171
        res.status(403).json('Insufficient balance.');
1✔
172
      } else {
173
        // create the transaction
174
        res.json(await new TransactionService().createTransaction(body));
2✔
175
      }
176
    } catch (error) {
177
      this.logger.error('Could not create transaction:', error);
×
178
      res.status(500).json('Internal server error.');
×
179
    }
180
  }
181

182
  /**
183
   * GET /transactions/{id}
184
   * @summary Get a single transaction
185
   * @operationId getSingleTransaction
186
   * @tags transactions - Operations of the transaction controller
187
   * @param {integer} id.path.required - The id of the transaction which should be returned
188
   * @security JWT
189
   * @return {TransactionResponse} 200 - Single transaction with given id
190
   * @return {string} 404 - Nonexistent transaction id
191
   */
192
  public async getTransaction(req: RequestWithToken, res: Response): Promise<TransactionResponse> {
193
    const parameters = req.params;
6✔
194
    this.logger.trace('Get single transaction', parameters, 'by user', req.token.user);
6✔
195

196
    let transaction;
197
    try {
6✔
198
      transaction = await new TransactionService().getSingleTransaction(parseInt(parameters.id, 10));
6✔
199
    } catch (e) {
200
      res.status(500).send();
×
201
      this.logger.error(e);
×
202
      return;
×
203
    }
204

205
    // If the transaction is undefined, there does not exist a transaction with the given ID
206
    if (transaction === undefined) {
6!
207
      res.status(404).json('Unknown transaction ID.');
×
208
      return;
×
209
    }
210

211
    res.status(200).json(transaction);
6✔
212
  }
213

214
  /**
215
   * PATCH /transactions/{id}
216
   * @summary Updates the requested transaction
217
   * @operationId updateTransaction
218
   * @tags transactions - Operations of transaction controller
219
   * @param {integer} id.path.required - The id of the transaction which should be updated
220
   * @param {TransactionRequest} request.body.required -
221
   * The updated transaction
222
   * @security JWT
223
   * @return {TransactionResponse} 200 - The requested transaction entity
224
   * @return {string} 400 - Validation error
225
   * @return {string} 404 - Not found error
226
   * @return {string} 500 - Internal server error
227
   */
228
  public async updateTransaction(req: RequestWithToken, res: Response): Promise<void> {
229
    const body = req.body as TransactionRequest;
3✔
230
    const { id } = req.params;
3✔
231
    this.logger.trace('Update Transaction', id, 'by user', req.token.user);
3✔
232

233
    // handle request
234
    try {
3✔
235
      if (await Transaction.findOne({ where: { id: parseInt(id, 10) } })) {
3✔
236
        if (await new TransactionService().verifyTransaction(body, true)) {
2✔
237
          res.status(200).json(await new TransactionService().updateTransaction(
1✔
238
            parseInt(id, 10), body,
239
          ));
240
        } else {
241
          res.status(400).json('Invalid transaction.');
1✔
242
        }
243
      } else {
244
        res.status(404).json('Transaction not found.');
1✔
245
      }
246
    } catch (error) {
247
      this.logger.error('Could not update transaction:', error);
×
248
      res.status(500).json('Internal server error.');
×
249
    }
250
  }
251

252
  /**
253
   * DELETE /transactions/{id}
254
   * @summary Deletes a transaction
255
   * @operationId deleteTransaction
256
   * @tags transactions - Operations of the transaction controller
257
   * @param {integer} id.path.required - The id of the transaction which should be deleted
258
   * @security JWT
259
   * @return 204 - No content
260
   * @return {string} 404 - Nonexistent transaction id
261
   */
262
  // eslint-disable-next-line class-methods-use-this
263
  public async deleteTransaction(req: RequestWithToken, res: Response): Promise<void> {
264
    const { id } = req.params;
2✔
265
    this.logger.trace('Delete transaction', id, 'by user', req.token.user);
2✔
266

267
    // handle request
268
    try {
2✔
269
      if (await Transaction.findOne({ where: { id: parseInt(id, 10) } })) {
2✔
270
        await new TransactionService().deleteTransaction(parseInt(id, 10));
1✔
271
        res.status(204).json();
1✔
272
      } else {
273
        res.status(404).json('Transaction not found.');
1✔
274
      }
275
    } catch (error) {
276
      this.logger.error('Could not delete transaction:', error);
×
277
      res.status(500).json('Internal server error.');
×
278
    }
279
  }
280

281
  /**
282
   * POST /transactions/validate
283
   * @summary Function to validate the transaction immediatly after it is created
284
   * @operationId validateTransaction
285
   * @tags transactions - Operations of the transaction controller
286
   * @param {TransactionRequest} request.body.required -
287
   * The transaction which should be validated
288
   * @return {boolean} 200 - Transaction validated
289
   * @security JWT
290
   * @return {string} 400 - Validation error
291
   * @return {string} 500 - Internal server error
292
   */
293
  public async validateTransaction(req: RequestWithToken, res: Response): Promise<void> {
294
    const body = req.body as TransactionRequest;
2✔
295
    this.logger.trace('Validate transaction', body, 'by user', req.token.user);
2✔
296

297
    try {
2✔
298
      if (await new TransactionService().verifyTransaction(body)) {
2✔
299
        res.status(200).json(true);
1✔
300
      } else  {
301
        res.status(400).json('Transaction is invalid');
1✔
302
        return;
1✔
303
      }
304
    } catch (error) {
305
      this.logger.error('Could not validate transaction:', error);
×
306
      res.status(500).json('Internal server error');
×
307
    }
308
  }
309

310

311
  /**
312
   * Determines the relation between the user and the transaction (by filters in the request).
313
   * - Returns 'own' if user is from, to, or createdBy.
314
   * - Returns 'organ' if user shares an organ with any of those users.
315
   * - Returns 'all' otherwise.
316
   *
317
   * @param req - Express request with user token and filters in query params.
318
   * @returns 'own' | 'organ' | 'all'
319
   */
320
  static async filterRelation(
321
    req: RequestWithToken,
322
  ): Promise<'own' | 'organ' | 'all'> {
323
    try {
32✔
324
      const userId = req.token.user.id;
32✔
325
      const { fromId, toId, createdById } = parseGetTransactionsFilters(req);
32✔
326

327
      // Check direct involvement
328
      if (fromId === userId || toId === userId || createdById === userId) {
24✔
329
        return 'own';
4✔
330
      }
331

332
      // Check organ relation
333
      if (
20!
334
        (fromId && await UserService.areInSameOrgan(userId, fromId)) ||
65✔
335
          (toId && await UserService.areInSameOrgan(userId, toId)) ||
336
          (createdById && await UserService.areInSameOrgan(userId, createdById))
337
      ) {
NEW
338
        return 'organ';
×
339
      }
340

341
      return 'all';
20✔
342
    } catch (error) {
343
      return 'all';
8✔
344
    }
345
  }
346
  
347
  /**
348
   * Function to determine which credentials are needed to post transaction
349
   *    all if user is not connected to transaction
350
   *    other if transaction createdby is and linked via organ
351
   *    own if user is connected to transaction
352
   * @param req - Request with TransactionRequest in the body
353
   * @return whether transaction is connected to user token
354
   */
355
  static async postRelation(req: RequestWithToken): Promise<string> {
356
    const request = req.body as TransactionRequest;
10✔
357
    if (request.createdBy !== req.token.user.id) {
10✔
358
      if (await UserService.areInSameOrgan(request.createdBy, req.token.user.id)) {
4✔
359
        return 'organ';
1✔
360
      }
361
      return 'all';
3✔
362
    }
363
    if (request.from === req.token.user.id) return 'own';
6✔
364
    return 'all';
3✔
365
  }
366

367
  /**
368
   * Function to determine which credentials are needed to get transactions
369
   *    all if user is not connected to transaction
370
   *    organ if user is not connected to transaction via organ
371
   *    own if user is connected to transaction
372
   * @param req - Request with transaction id as param
373
   * @return whether transaction is connected to user token
374
   */
375
  static async getRelation(req: RequestWithToken): Promise<string> {
376
    const transaction = await Transaction.findOne({
12✔
377
      where: { id: asNumber(req.params.id) },
378
      relations: ['from', 'createdBy', 'pointOfSale', 'pointOfSale.pointOfSale', 'pointOfSale.pointOfSale.owner'],
379
    });
380
    if (!transaction) return 'all';
12✔
381
    if (userTokenInOrgan(req, transaction.from.id)
10✔
382
        || userTokenInOrgan(req, transaction.createdBy.id)
383
        || userTokenInOrgan(req, transaction.pointOfSale.pointOfSale.owner.id)) return 'organ';
1✔
384
    if (transaction.from.id === req.token.user.id || transaction.createdBy.id === req.token.user.id) return 'own';
9✔
385
    return 'all';
4✔
386
  }
387
}
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