diff --git a/app/api/definitions/openapi.yml b/app/api/definitions/openapi.yml index b211d0ea..9b9c1c44 100644 --- a/app/api/definitions/openapi.yml +++ b/app/api/definitions/openapi.yml @@ -280,6 +280,9 @@ paths: /api/user-accounts/{id}: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}' + + /api/user-accounts/{id}/teams: + $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}~1teams' /api/user-accounts/register: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1register' diff --git a/app/api/definitions/paths/user-accounts-paths.yml b/app/api/definitions/paths/user-accounts-paths.yml index 9ed932cc..268240ab 100644 --- a/app/api/definitions/paths/user-accounts-paths.yml +++ b/app/api/definitions/paths/user-accounts-paths.yml @@ -210,3 +210,54 @@ paths: $ref: '../components/user-accounts.yml#/components/schemas/user-account' '400': description: 'Missing or invalid parameters were provided. The user account was not created.' + + /api/user-accounts/{id}/teams: + get: + summary: 'Get a list of teams the user account is associated with' + operationId: 'user-account-get-teams' + description: | + This endpoint gets gets a list of teams that a user is associated with, identified by the user account id. + tags: + - 'User Accounts' + parameters: + - name: id + in: path + description: 'User account id of the user account to retrieve a list of associated teams for' + required: true + schema: + type: string + - name: limit + in: query + description: | + The number of user accounts to retrieve. + The default (0) will retrieve all user accounts. + schema: + type: number + default: 0 + - name: offset + in: query + description: | + The number of user accounts to skip. + The default (0) will start with the first user account. + schema: + type: number + default: 0 + - name: includePagination + in: query + description: | + Whether to include pagination data in the returned value. + Wraps returned objects in a larger object. + schema: + type: boolean + default: false + responses: + '200': + description: 'A list of teams which the user is associated with.' + content: + application/json: + schema: + type: array + items: + $ref: '../components/teams.yml#/components/schemas/team' + '404': + description: 'A user account with the requested user account id was not found.' diff --git a/app/controllers/user-accounts-controller.js b/app/controllers/user-accounts-controller.js index 3bccf47f..976b8d30 100644 --- a/app/controllers/user-accounts-controller.js +++ b/app/controllers/user-accounts-controller.js @@ -171,3 +171,30 @@ exports.register = async function(req, res) { } } }; + +exports.retrieveTeamsByUserId = function(req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + status: req.query.status, + includePagination: req.query.includePagination, + }; + + const userId = req.params.id; + + userAccountsService.retrieveTeamsByUserId(userId, options, function(err, results) { + if (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get teams. Server error.'); + } + else { + if (options.includePagination) { + logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total team(s)`); + } + else { + logger.debug(`Success: Retrieved ${ results.length } team(s)`); + } + return res.status(200).send(results); + } + }); +}; \ No newline at end of file diff --git a/app/routes/user-accounts-routes.js b/app/routes/user-accounts-routes.js index 11558b5e..c26185d5 100644 --- a/app/routes/user-accounts-routes.js +++ b/app/routes/user-accounts-routes.js @@ -37,6 +37,13 @@ router.route('/user-accounts/:id') userAccountsController.delete ); +router.route('/user-accounts/:id/teams') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + userAccountsController.retrieveTeamsByUserId + ); + router.route('/user-accounts/register') .post( // authn and authz handled in controller diff --git a/app/services/user-accounts-service.js b/app/services/user-accounts-service.js index 46e29db3..3a910926 100644 --- a/app/services/user-accounts-service.js +++ b/app/services/user-accounts-service.js @@ -2,6 +2,7 @@ const uuid = require('uuid'); const UserAccount = require('../models/user-account-model'); +const Team = require('../models/team-model'); const regexValidator = require('../lib/regex'); const errors = { @@ -334,3 +335,69 @@ exports.addCreatedByUserAccountToAll = async function(attackObjects) { await addCreatedByUserAccount(attackObject); } } + +exports.retrieveTeamsByUserId = function(userAccountId, options, callback) { + if (!userAccountId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'userId'; + return callback(error); + } + + // Build the aggregation + const aggregation = [ + { $sort: { 'name': 1 } }, + ]; + + const match = { + $match: { + userIDs: { $in: [userAccountId] } + } + }; + + aggregation.push(match); + + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [] + } + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } + else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + Team.aggregate(aggregation, function (err, results) { + if (err) { + return callback(err); + } + else { + const teams = results[0].documents; + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const returnValue = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit + }, + data: teams + }; + return callback(null, returnValue); + } + else { + return callback(null, teams); + } + } + }); +}; diff --git a/app/tests/api/user-accounts/user-accounts.spec.js b/app/tests/api/user-accounts/user-accounts.spec.js index 311c25a7..7a156a54 100644 --- a/app/tests/api/user-accounts/user-accounts.spec.js +++ b/app/tests/api/user-accounts/user-accounts.spec.js @@ -7,6 +7,8 @@ logger.level = 'debug'; const database = require('../../../lib/database-in-memory'); const databaseConfiguration = require('../../../lib/database-configuration'); const UserAccount = require('../../../models/user-account-model'); +const Team = require('../../../models/team-model'); +const teamsService = require('../../../services/teams-service'); const login = require('../../shared/login'); @@ -33,6 +35,7 @@ describe('User Accounts API', function () { // Wait until the indexes are created await UserAccount.init(); + await Team.init(); // Initialize the express app app = await require('../../../index').initializeApp(); @@ -293,6 +296,7 @@ describe('User Accounts API', function () { }); }); + let anonymousUserId = null; it('GET /api/user-accounts returns an array with the anonymous user account', function (done) { request(app) .get('/api/user-accounts') @@ -309,11 +313,59 @@ describe('User Accounts API', function () { expect(userAccount).toBeDefined(); expect(Array.isArray(userAccount)).toBe(true); expect(userAccount.length).toBe(1); + anonymousUserId = userAccount[0].id; done(); } }); }); + it('GET /api/user-accounts/{id}/teams should return an empty array when no teams have been associated with a user', function (done) { + request(app) + .get(`/api/user-accounts/${ anonymousUserId }/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(0); + done(); + } + }); + }); + + it('GET /api/user-accounts/{id}/teams should return an array with the list of teams a user is associated with', async function () { + // Add a new team to the database with the anonymous user as a member + const timestamp = new Date().toISOString(); + const teamData = { + userIDs: [ anonymousUserId ], + name: 'Example Team', + created: timestamp, + modified: timestamp + }; + await teamsService.create(teamData); + + const res = await request(app) + .get(`/api/user-accounts/${ anonymousUserId }/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + expect(teams[0].name).toBe(teamData.name); + expect(teams[0].userIDs.length).toBe(1); + expect(teams[0].userIDs[0]).toBe(anonymousUserId); + }); + after(async function() { await database.closeConnection(); });