From 1ccfbb1bcc4754debf97bd4cc2dd0bf491e944a5 Mon Sep 17 00:00:00 2001 From: aloftus23 Date: Wed, 13 Mar 2024 15:58:15 -0400 Subject: [PATCH] revert users.ts and app.ts --- backend/src/api/app.ts | 125 ++++++- backend/src/api/users.ts | 685 +++++++++++++++++++++++++++++++++++---- 2 files changed, 731 insertions(+), 79 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index e60920b6..61025450 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -5,6 +5,8 @@ import * as cors from 'cors'; import * as helmet from 'helmet'; import { handler as healthcheck } from './healthcheck'; import * as auth from './auth'; +import * as cpes from './cpes'; +import * as cves from './cves'; import * as domains from './domains'; import * as search from './search'; import * as vulnerabilities from './vulnerabilities'; @@ -16,6 +18,7 @@ import * as stats from './stats'; import * as apiKeys from './api-keys'; import * as reports from './reports'; import * as savedSearches from './saved-searches'; +import rateLimit from 'express-rate-limit'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { UserType } from '../models'; import logger from '../tools/lambda-logger'; @@ -84,29 +87,60 @@ const logHeaders = (req, res, next) => { const app = express(); -app.use(logHeaders); -app.use(cors()); +app.use( + rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 5000 + }) +); // limit 1000 requests per 15 minutes + app.use(express.json({ strict: false })); -app.use(helmet.hsts({ maxAge: 31536000, preload: true })); -app.use(cookieParser()); app.use( - helmet.contentSecurityPolicy({ - directives: { - defaultSrc: [ - "'self'", - 'https://cognito-idp.us-gov-west-1.amazonaws.com', - 'https://api.crossfeed.cyber.dhs.gov' - ], - scriptSrc: ["'self'", 'https://api.crossfeed.cyber.dhs.gov'] - // Add other directives as needed + cors({ + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + }) +); + +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: [ + "'self'", + 'https://cognito-idp.us-gov-west-1.amazonaws.com', + 'https://api.staging.crossfeed.cyber.dhs.gov' + ], + objectSrc: ["'none'"], + scriptSrc: [ + "'self'", + 'https://api.staging.crossfeed.cyber.dhs.gov' + // Add any other allowed script sources here + ], + frameAncestors: ["'none'"] + // Add other directives as needed + } + }, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true } }) ); +app.use((req, res, next) => { + res.setHeader('X-XSS-Protection', '0'); + next(); +}); + +app.use(cookieParser()); + app.get('/', handlerToExpress(healthcheck)); app.post('/auth/login', handlerToExpress(auth.login)); app.post('/auth/callback', handlerToExpress(auth.callback)); +app.post('/users/register', handlerToExpress(users.register)); const checkUserLoggedIn = async (req, res, next) => { req.requestContext = { @@ -176,10 +210,10 @@ app.get('/index.php', (req, res) => res.redirect('/matomo/index.php')); const matomoProxy = createProxyMiddleware({ target: process.env.MATOMO_URL, headers: { HTTP_X_FORWARDED_URI: '/matomo' }, - pathRewrite: function (path, req) { + pathRewrite: function (path) { return path.replace(/^\/matomo/, ''); }, - onProxyReq: function (proxyReq, req, res) { + onProxyReq: function (proxyReq) { // Only pass the MATOMO_SESSID cookie to Matomo. if (!proxyReq.getHeader('Cookie')) return; const cookies = cookie.parse(proxyReq.getHeader('Cookie')); @@ -189,7 +223,7 @@ const matomoProxy = createProxyMiddleware({ ); proxyReq.setHeader('Cookie', newCookies); }, - onProxyRes: function (proxyRes, req, res) { + onProxyRes: function (proxyRes) { // Remove transfer-encoding: chunked responses, because API Gateway doesn't // support chunked encoding. if (proxyRes.headers['transfer-encoding'] === 'chunked') { @@ -208,7 +242,7 @@ const matomoProxy = createProxyMiddleware({ */ const peProxy = createProxyMiddleware({ target: process.env.PE_API_URL, - pathRewrite: function (path, req) { + pathRewrite: function (path) { return path.replace(/^\/pe/, ''); }, logLevel: 'silent' @@ -295,6 +329,9 @@ authenticatedRoute.delete('/api-keys/:keyId', handlerToExpress(apiKeys.del)); authenticatedRoute.post('/search', handlerToExpress(search.search)); authenticatedRoute.post('/search/export', handlerToExpress(search.export_)); +authenticatedRoute.get('/cpes/:id', handlerToExpress(cpes.get)); +authenticatedRoute.get('/cves/:id', handlerToExpress(cves.get)); +authenticatedRoute.get('/cves/name/:name', handlerToExpress(cves.getByName)); authenticatedRoute.post('/domain/search', handlerToExpress(domains.list)); authenticatedRoute.post('/domain/export', handlerToExpress(domains.export_)); authenticatedRoute.get('/domain/:domainId', handlerToExpress(domains.get)); @@ -361,10 +398,23 @@ authenticatedRoute.get( '/organizations/:organizationId', handlerToExpress(organizations.get) ); +authenticatedRoute.get( + '/organizations/state/:state', + handlerToExpress(organizations.getByState) +); +authenticatedRoute.get( + '/organizations/regionId/:regionId', + handlerToExpress(organizations.getByRegionId) +); authenticatedRoute.post( '/organizations', handlerToExpress(organizations.create) ); +authenticatedRoute.post( + '/organizations_upsert', + handlerToExpress(organizations.upsert_org) +); + authenticatedRoute.put( '/organizations/:organizationId', handlerToExpress(organizations.update) @@ -373,6 +423,10 @@ authenticatedRoute.delete( '/organizations/:organizationId', handlerToExpress(organizations.del) ); +authenticatedRoute.post( + '/v2/organizations/:organizationId/users', + handlerToExpress(organizations.addUserV2) +); authenticatedRoute.post( '/organizations/:organizationId/roles/:roleId/approve', handlerToExpress(organizations.approveRole) @@ -397,6 +451,14 @@ authenticatedRoute.post('/stats', handlerToExpress(stats.get)); authenticatedRoute.post('/users', handlerToExpress(users.invite)); authenticatedRoute.get('/users', handlerToExpress(users.list)); authenticatedRoute.delete('/users/:userId', handlerToExpress(users.del)); +authenticatedRoute.get( + '/users/state/:state', + handlerToExpress(users.getByState) +); +authenticatedRoute.get( + '/users/regionId/:regionId', + handlerToExpress(users.getByRegionId) +); authenticatedRoute.post('/users/search', handlerToExpress(users.search)); authenticatedRoute.post( @@ -409,6 +471,35 @@ authenticatedRoute.post( handlerToExpress(reports.list_reports) ); +//Authenticated Registration Routes +authenticatedRoute.put( + '/users/:userId/register/approve', + handlerToExpress(users.registrationApproval) +); + +authenticatedRoute.put( + '/users/:userId/register/deny', + handlerToExpress(users.registrationDenial) +); + +//************* */ +// V2 Routes // +//************* */ + +// Users +authenticatedRoute.put('/v2/users/:userId', handlerToExpress(users.updateV2)); +authenticatedRoute.get('/v2/users', handlerToExpress(users.getAllV2)); + +// Organizations +authenticatedRoute.put( + '/v2/organizations/:organizationId', + handlerToExpress(organizations.updateV2) +); +authenticatedRoute.get( + '/v2/organizations', + handlerToExpress(organizations.getAllV2) +); + app.use(authenticatedRoute); export default app; diff --git a/backend/src/api/users.ts b/backend/src/api/users.ts index 0836c4a7..a5df83cc 100644 --- a/backend/src/api/users.ts +++ b/backend/src/api/users.ts @@ -7,6 +7,7 @@ import { IsEnum, IsInt, IsIn, + IsNumber, IsObject, IsPositive, ValidateNested, @@ -18,19 +19,23 @@ import { wrapHandler, NotFound, Unauthorized, - sendEmail + sendEmail, + sendUserRegistrationEmail, + sendRegistrationApprovedEmail, + sendRegistrationDeniedEmail } from './helpers'; import { UserType } from '../models/user'; import { getUserId, canAccessUser, isGlobalViewAdmin, + isRegionalAdmin, isOrgAdmin, isGlobalWriteAdmin } from './auth'; import { Type, plainToClass } from 'class-transformer'; import { IsNull } from 'typeorm'; -import logger from '../tools/lambda-logger'; +import { create } from './organizations'; class UserSearch { @IsInt() @@ -47,7 +52,9 @@ class UserSearch { 'userType', 'dateAcceptedTerms', 'lastLoggedIn', - 'acceptedTermsVersion' + 'acceptedTermsVersion', + 'state', + 'regionId' ]) @IsOptional() sort: string = 'fullName'; @@ -69,12 +76,137 @@ class UserSearch { async getResults(event): Promise<[User[], number]> { const pageSize = this.pageSize || 25; const sort = this.sort === 'name' ? 'user.fullName' : 'user.' + this.sort; - const qs = User.createQueryBuilder('user').orderBy(sort, this.order); - const results = await qs.getManyAndCount(); - return results; + const qs = User.createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'roles') // Include the roles relation + .leftJoinAndSelect('roles.organization', 'organization') // Include the organization relation + .orderBy(sort, this.order); + const result = await qs.getMany(); + const count = await qs.getCount(); + return [result, count]; } } +// New User +class NewUser { + @IsString() + firstName: string; + + @IsString() + lastName: string; + + @IsString() + @IsOptional() + state: string; + + @IsString() + @IsOptional() + regionId: string; + + @IsEmail() + @IsOptional() + email: string; + + @IsString() + @IsOptional() + organization: string; + + @IsBoolean() + @IsOptional() + organizationAdmin: string; + + @IsEnum(UserType) + @IsOptional() + userType: UserType; +} + +class UpdateUser { + @IsString() + @IsOptional() + state: string; + + @IsString() + @IsOptional() + regionId: string; + + @IsBoolean() + @IsOptional() + invitePending: boolean; + + @IsEnum(UserType) + @IsOptional() + userType: UserType; + + // @IsString() + @IsEnum(Organization) + @IsOptional() + organization: Organization; + + @IsString() + @IsOptional() + role: string; +} + +const REGION_STATE_MAP = { + Connecticut: '1', + Maine: '1', + Massachusetts: '1', + 'New Hampshire': '1', + 'Rhode Island': '1', + Vermont: '1', + 'New Jersey': '2', + 'New York': '2', + 'Puerto Rico': '2', + 'Virgin Islands': '2', + Delaware: '3', + Maryland: '3', + Pennsylvania: '3', + Virginia: '3', + 'District of Columbia': '3', + 'West Virginia': '3', + Alabama: '4', + Florida: '4', + Georgia: '4', + Kentucky: '4', + Mississippi: '4', + 'North Carolina': '4', + 'South Carolina': '4', + Tennessee: '4', + Illinois: '5', + Indiana: '5', + Michigan: '5', + Minnesota: '5', + Ohio: '5', + Wisconsin: '5', + Arkansas: '6', + Louisiana: '6', + 'New Mexico': '6', + Oklahoma: '6', + Texas: '6', + Iowa: '7', + Kansas: '7', + Missouri: '7', + Nebraska: '7', + Colorado: '8', + Montana: '8', + 'North Dakota': '8', + 'South Dakota': '8', + Utah: '8', + Wyoming: '8', + Arizona: '9', + California: '9', + Hawaii: '9', + Nevada: '9', + Guam: '9', + 'American Samoa': '9', + 'Commonwealth Northern Mariana Islands': '9', + 'Republic of Marshall Islands': '9', + 'Federal States of Micronesia': '9', + Alaska: '10', + Idaho: '10', + Oregon: '10', + Washington: '10' +}; + /** * @swagger * @@ -136,11 +268,13 @@ export const update = wrapHandler(async (event) => { } ); if (user) { + console.log(JSON.stringify({ original_user: user })); user.firstName = body.firstName ?? user.firstName; user.lastName = body.lastName ?? user.lastName; user.fullName = user.firstName + ' ' + user.lastName; user.userType = body.userType ?? user.userType; await User.save(user); + console.log(JSON.stringify({ updated_user: user })); return { statusCode: 200, body: JSON.stringify(user) @@ -149,66 +283,35 @@ export const update = wrapHandler(async (event) => { return NotFound; }); -class NewUser { - @IsString() - firstName: string; - - @IsString() - lastName: string; - - @IsEmail() - @IsOptional() - email: string; - - @IsString() - @IsOptional() - organization: string; - - @IsBoolean() - @IsOptional() - organizationAdmin: string; - - @IsEnum(UserType) - @IsOptional() - userType: UserType; -} - const sendInviteEmail = async (email: string, organization?: Organization) => { const staging = process.env.NODE_ENV !== 'production'; - try { - await sendEmail( - email, - 'Crossfeed Invitation', - `Hi there, - - You've been invited to join ${ - organization?.name ? `the ${organization?.name} organization on ` : '' - }Crossfeed. To accept the invitation and start using Crossfeed, sign on at ${ - process.env.FRONTEND_DOMAIN - }/signup. + await sendEmail( + email, + 'Crossfeed Invitation', + `Hi there, - Crossfeed access instructions: +You've been invited to join ${ + organization?.name ? `the ${organization?.name} organization on ` : '' + }Crossfeed. To accept the invitation and start using Crossfeed, sign on at ${ + process.env.FRONTEND_DOMAIN + }/signup. - 1. Visit ${process.env.FRONTEND_DOMAIN}/signup. - 2. Select "Create Account." - 3. Enter your email address and a new password for Crossfeed. - 4. A confirmation code will be sent to your email. Enter this code when you receive it. - 5. You will be prompted to enable MFA. Scan the QR code with an authenticator app on your phone, such as Microsoft Authenticator. Enter the MFA code you see after scanning. - 6. After configuring your account, you will be redirected to Crossfeed. +Crossfeed access instructions: - For more information on using Crossfeed, view the Crossfeed user guide at https://docs.crossfeed.cyber.dhs.gov/user-guide/quickstart/. +1. Visit ${process.env.FRONTEND_DOMAIN}/signup. +2. Select "Create Account." +3. Enter your email address and a new password for Crossfeed. +4. A confirmation code will be sent to your email. Enter this code when you receive it. +5. You will be prompted to enable MFA. Scan the QR code with an authenticator app on your phone, such as Microsoft Authenticator. Enter the MFA code you see after scanning. +6. After configuring your account, you will be redirected to Crossfeed. - If you encounter any difficulties, please feel free to reply to this email (or send an email to ${ - process.env.CROSSFEED_SUPPORT_EMAIL_REPLYTO - }).` - ); - } catch (error) { - logger.error(`Error sending email: ${error}`); +For more information on using Crossfeed, view the Crossfeed user guide at https://docs.crossfeed.cyber.dhs.gov/user-guide/quickstart/. - // Handle the error or re-throw it if needed - throw error; - } +If you encounter any difficulties, please feel free to reply to this email (or send an email to ${ + process.env.CROSSFEED_SUPPORT_EMAIL_REPLYTO + }).` + ); }; /** @@ -236,7 +339,11 @@ export const invite = wrapHandler(async (event) => { await connectToDatabase(); body.email = body.email.toLowerCase(); - logger.info(body.email); + + if (body.state) { + body.regionId = REGION_STATE_MAP[body.state]; + } + // Check if user already exists let user = await User.findOne({ email: body.email @@ -254,14 +361,17 @@ export const invite = wrapHandler(async (event) => { ...body }); await User.save(user); - await sendInviteEmail(user.email, organization); + if (process.env.IS_LOCAL!) { + console.log('Cannot send invite email while running on local.'); + } else { + await sendInviteEmail(user.email, organization); + } } else if (!user.firstName && !user.lastName) { // Only set the user first name and last name the first time the user is invited. user.firstName = body.firstName; user.lastName = body.lastName; await User.save(user); } - // Always update the userType, if specified in the request. if (body.userType) { user.userType = body.userType; @@ -359,6 +469,7 @@ export const acceptTerms = wrapHandler(async (event) => { * description: List users. * tags: * - Users + * */ export const list = wrapHandler(async (event) => { if (!isGlobalViewAdmin(event)) return Unauthorized; @@ -383,7 +494,7 @@ export const list = wrapHandler(async (event) => { */ export const search = wrapHandler(async (event) => { if (!isGlobalViewAdmin(event)) return Unauthorized; - await connectToDatabase(); + await connectToDatabase(true); const search = await validateBody(UserSearch, event.body); const [result, count] = await search.getResults(event); return { @@ -391,3 +502,453 @@ export const search = wrapHandler(async (event) => { body: JSON.stringify({ result, count }) }; }); + +/** + * @swagger + * + * /users/regionId/{regionId}: + * get: + * description: List users with specific regionId. + * parameters: + * - in: path + * name: regionId + * description: User regionId + * tags: + * - Users + */ +export const getByRegionId = wrapHandler(async (event) => { + if (!isRegionalAdmin(event)) return Unauthorized; + const regionId = event.pathParameters?.regionId; + await connectToDatabase(); + const result = await User.find({ + where: { regionId: regionId }, + relations: ['roles', 'roles.organization'] + }); + if (result) { + return { + statusCode: 200, + body: JSON.stringify(result) + }; + } + return NotFound; +}); + +/** + * @swagger + * + * /users/state/{state}: + * get: + * description: List users with specific state. + * parameters: + * - in: path + * name: state + * description: User state + * tags: + * - Users + */ +export const getByState = wrapHandler(async (event) => { + if (!isRegionalAdmin(event)) return Unauthorized; + const state = event.pathParameters?.state; + await connectToDatabase(); + const result = await User.find({ + where: { state: state }, + relations: ['roles', 'roles.organization'] + }); + if (result) { + return { + statusCode: 200, + body: JSON.stringify(result) + }; + } + return NotFound; +}); + +/** + * @swagger + * + * /users/register: + * post: + * description: New user registration. + * tags: + * - Users + */ +export const register = wrapHandler(async (event) => { + const body = await validateBody(NewUser, event.body); + const newUser = { + firstName: body.firstName, + lastName: body.lastName, + email: body.email.toLowerCase(), + userType: UserType.STANDARD, + state: body.state, + regionId: REGION_STATE_MAP[body.state], + invitePending: true + }; + console.log(JSON.stringify(newUser)); + + await connectToDatabase(); + + // Check if user already exists + const userCheck = await User.findOne({ + where: { email: newUser.email } + }); + + let id = ''; + // Create if user does not exist + if (userCheck) { + console.log('User already exists.'); + return { + statusCode: 422, + body: 'User email already exists. Registration failed.' + }; + } + + const createdUser = await User.create(newUser); + await User.save(createdUser); + id = createdUser.id; + const savedUser = await User.findOne(id, { + relations: ['roles', 'roles.organization'] + }); + if (!savedUser) { + return NotFound; + } + // Send email notification + await sendUserRegistrationEmail( + savedUser.email, + 'Crossfeed Registration Pending', + savedUser.firstName, + savedUser.lastName, + 'crossfeed_registration_notification.html' + ); + + return { + statusCode: 200, + body: JSON.stringify(savedUser) + }; +}); + +/** + * @swagger + * + * /users/{id}/register/approve: + * put: + * description: Approve a particular users registration. + * parameters: + * - in: path + * name: id + * description: User id + * tags: + * - Users + */ +export const registrationApproval = wrapHandler(async (event) => { + // Get the user id from the path + const userId = event.pathParameters?.userId; + + // Confirm that the id is a valid UUID + if (!userId || !isUUID(userId)) { + return NotFound; + } + + // Connect to the database + await connectToDatabase(); + + const user = await User.findOne(userId); + if (!user) { + return NotFound; + } + + // Send email notification + await sendRegistrationApprovedEmail( + user.email, + 'Crossfeed Registration Approved', + user.firstName, + user.lastName, + 'crossfeed_approval_notification.html' + ); + + // TODO: Handle Response Output + return { + statusCode: 200, + body: 'User registration approved.' + }; +}); + +/** + * @swagger + * + * /users/{id}/register/deny: + * put: + * description: Deny a particular users registration. + * parameters: + * - in: path + * name: id + * description: User id + * tags: + * - Users + */ +export const registrationDenial = wrapHandler(async (event) => { + // Get the user id from the path + const userId = event.pathParameters?.userId; + + // Confirm that the id is a valid UUID + if (!userId || !isUUID(userId)) { + return NotFound; + } + + // Connect to the database + await connectToDatabase(); + + const user = await User.findOne(userId); + if (!user) { + return NotFound; + } + + await sendRegistrationDeniedEmail( + user.email, + 'Crossfeed Registration Denied', + user.firstName, + user.lastName, + 'crossfeed_denial_notification.html' + ); + + // TODO: Handle Response Output + return { + statusCode: 200, + body: 'User registration denied.' + }; +}); + +//***************// +// V2 Endpoints // +//***************// + +/** + * @swagger + * + * /v2/users: + * get: + * description: List all users with query parameters. + * tags: + * - Users + * parameters: + * - in: query + * name: state + * required: false + * schema: + * type: array + * items: + * type: string + * - in: query + * name: regionId + * required: false + * schema: + * type: array + * items: + * type: string + * - in: query + * name: invitePending + * required: false + * schema: + * type: array + * items: + * type: string + * + */ +export const getAllV2 = wrapHandler(async (event) => { + if (!isRegionalAdmin(event)) return Unauthorized; + + const filterParams = {}; + + if (event.query && event.query.state) { + filterParams['state'] = event.query.state; + } + if (event.query && event.query.regionId) { + filterParams['regionId'] = event.query.regionId; + } + if (event.query && event.query.invitePending) { + filterParams['invitePending'] = event.query.invitePending; + } + + await connectToDatabase(); + if (Object.entries(filterParams).length === 0) { + const result = await User.find({ + relations: ['roles', 'roles.organization'] + }); + return { + statusCode: 200, + body: JSON.stringify(result) + }; + } else { + const result = await User.find({ + where: filterParams, + relations: ['roles', 'roles.organization'] + }); + return { + statusCode: 200, + body: JSON.stringify(result) + }; + } +}); + +/** + * @swagger + * + * /v2/users: + * post: + * description: Create a new user. + * tags: + * - Users + */ +export const inviteV2 = wrapHandler(async (event) => { + const body = await validateBody(NewUser, event.body); + // Invoker must be either an organization or global admin + if (body.organization) { + if (!isOrgAdmin(event, body.organization)) return Unauthorized; + } else { + if (!isGlobalWriteAdmin(event)) return Unauthorized; + } + if (!isGlobalWriteAdmin(event) && body.userType) { + // Non-global admins can't set userType + return Unauthorized; + } + + await connectToDatabase(); + + body.email = body.email.toLowerCase(); + const userEmail = body.email.toLowerCase(); + + const sendRegisterEmail = async (email: string) => { + const staging = process.env.NODE_ENV !== 'production'; + + await sendEmail( + email, + 'Crossfeed Registration', + `Hello, + Your Crossfeed registration is under review. + You will receive an email when your registration is approved. + + Thank you!` + ); + }; + + // Check if user already exists + let user = await User.findOne({ + email: body.email + }); + + // Handle Organization assignment if provided + let organization: Organization | undefined; + if (body.organization) { + organization = await Organization.findOne(body.organization); + } + + // Create user if not found + if (!user) { + // Create User object + user = await User.create({ + invitePending: true, + ...body + }); + // Save User to DB + await User.save(user); + + // Send Notification Email to user + await sendRegisterEmail(userEmail); + } else if (!user.firstName && !user.lastName) { + // Only set the user first name and last name the first time the user is invited. + user.firstName = body.firstName; + user.lastName = body.lastName; + // Save User to DB + await User.save(user); + } + + // Always update the userType, if specified in the request. + if (body.userType) { + user.userType = body.userType; + await User.save(user); + } + + if (organization) { + // Create approved role if organization supplied + await Role.createQueryBuilder() + .insert() + .values({ + user: user, + organization: { id: body.organization }, + approved: true, + createdBy: { id: event.requestContext.authorizer!.id }, + approvedBy: { id: event.requestContext.authorizer!.id }, + role: body.organizationAdmin ? 'admin' : 'user' + }) + .onConflict( + ` + ("userId", "organizationId") DO UPDATE + SET "role" = excluded."role", + "approved" = excluded."approved", + "approvedById" = excluded."approvedById" + ` + ) + .execute(); + } + + const updated = await User.findOne( + { + id: user.id + }, + { + relations: ['roles', 'roles.organization'] + } + ); + return { + statusCode: 200, + body: JSON.stringify(updated) + }; +}); + +/** + * @swagger + * + * /v2/users/{id}: + * put: + * description: Update a particular user. + * parameters: + * - in: path + * name: id + * description: User id + * tags: + * - Users + */ +export const updateV2 = wrapHandler(async (event) => { + // Get the user id from the path + const userId = event.pathParameters?.userId; + + // Confirm that the id is a valid UUID + if (!userId || !isUUID(userId)) { + return NotFound; + } + + // Validate the body + const body = await validateBody(UpdateUser, event.body); + + // Connect to the database + await connectToDatabase(); + + const user = await User.findOne(userId); + if (!user) { + return NotFound; + } + + // Update the user + const updatedResp = await User.update(userId, body); + + // Handle response + if (updatedResp) { + const updatedUser = await User.findOne(userId, { + relations: ['roles', 'roles.organization'] + }); + return { + statusCode: 200, + body: JSON.stringify(updatedUser) + }; + } + return NotFound; +});