Skip to content

Commit

Permalink
Merge pull request #260 from center-for-threat-informed-defense/featu…
Browse files Browse the repository at this point in the history
…re/get-teams-by-user

Feature/get teams by user
  • Loading branch information
ElJocko authored Jun 1, 2023
2 parents 3878fa2 + d300d5d commit 063c435
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/api/definitions/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
51 changes: 51 additions & 0 deletions app/api/definitions/paths/user-accounts-paths.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
27 changes: 27 additions & 0 deletions app/controllers/user-accounts-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
};
7 changes: 7 additions & 0 deletions app/routes/user-accounts-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions app/services/user-accounts-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
}
}
});
};
52 changes: 52 additions & 0 deletions app/tests/api/user-accounts/user-accounts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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();
Expand Down Expand Up @@ -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')
Expand All @@ -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();
});
Expand Down

0 comments on commit 063c435

Please sign in to comment.