diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ded935..f3a72b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15) + + +### Bug Fixes + +* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](https://github.com/sasjs/server/commit/e08bbcc5435cbabaee40a41a7fb667d4a1f078e6)) + +# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14) + + +### Features + +* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](https://github.com/sasjs/server/commit/aef411a0eac625c33274dfe3e88b6f75115c44d8)) + +## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14) + + +### Bug Fixes + +* correct syntax for encoding option ([32d372b](https://github.com/sasjs/server/commit/32d372b42fbf56b6c0779e8f704164eaae1c7548)) + +## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14) + + +### Bug Fixes + +* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](https://github.com/sasjs/server/commit/8734489cf014aedaca3f325e689493e4fe0b71ca)) + ## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 1f76c10..dd34dd8 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -310,6 +310,21 @@ components: - displayName type: object additionalProperties: false + GroupResponse: + properties: + groupId: + type: number + format: double + name: + type: string + description: + type: string + required: + - groupId + - name + - description + type: object + additionalProperties: false UserDetailsResponse: properties: id: @@ -325,6 +340,10 @@ components: type: boolean autoExec: type: string + groups: + items: + $ref: '#/components/schemas/GroupResponse' + type: array required: - id - displayName @@ -364,21 +383,6 @@ components: - password type: object additionalProperties: false - GroupResponse: - properties: - groupId: - type: number - format: double - name: - type: string - description: - type: string - required: - - groupId - - name - - description - type: object - additionalProperties: false GroupDetailsResponse: properties: groupId: @@ -985,6 +989,94 @@ paths: application/json: schema: $ref: '#/components/schemas/UserPayload' + '/SASjsApi/user/by/username/{username}': + get: + operationId: GetUserByUsername + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + description: 'Only Admin or user itself will get user autoExec code.' + summary: 'Get user properties - such as group memberships, userName, displayName.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + patch: + operationId: UpdateUserByUsername + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + delete: + operationId: DeleteUserByUsername + responses: + '204': + description: 'No content' + summary: 'Delete a user. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + requestBody: + required: true + content: + application/json: + schema: + properties: + password: + type: string + type: object '/SASjsApi/user/{userId}': get: operationId: GetUser diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 44adef2..b3cbb0f 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group' import User from '../model/User' import { UserResponse } from './user' -interface GroupResponse { +export interface GroupResponse { groupId: number name: string description: string @@ -210,6 +210,9 @@ const updateUsersListInGroup = async ( if (!updatedGroup) throw new Error('Unable to update group') + if (action === 'addUser') user.addGroup(group._id) + else user.removeGroup(group._id) + return { groupId: updatedGroup.groupId, name: updatedGroup.name, diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index c1d5d40..3f69a95 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -97,6 +97,8 @@ ${autoExecContent}` session.path, '-AUTOEXEC', autoExecPath, + '-ENCODING', + 'UTF-8', process.platform === 'win32' ? '-nosplash' : '' ]) .then(() => { diff --git a/api/src/controllers/user.ts b/api/src/controllers/user.ts index cfbce9d..cbb3d81 100644 --- a/api/src/controllers/user.ts +++ b/api/src/controllers/user.ts @@ -18,6 +18,7 @@ import { desktopUser } from '../middlewares' import User, { UserPayload } from '../model/User' import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils' +import { GroupResponse } from './group' export interface UserResponse { id: number @@ -32,6 +33,7 @@ interface UserDetailsResponse { isActive: boolean isAdmin: boolean autoExec?: string + groups?: GroupResponse[] } @Security('bearerAuth') @@ -77,6 +79,26 @@ export class UserController { return createUser(body) } + /** + * Only Admin or user itself will get user autoExec code. + * @summary Get user properties - such as group memberships, userName, displayName. + * @param username The User's username + * @example username "johnSnow01" + */ + @Get('by/username/{username}') + public async getUserByUsername( + @Request() req: express.Request, + @Path() username: string + ): Promise { + const { MODE } = process.env + + if (MODE === ModeType.Desktop) return getDesktopAutoExec() + + const { user } = req + const getAutoExec = user!.isAdmin || user!.username == username + return getUser({ username }, getAutoExec) + } + /** * Only Admin or user itself will get user autoExec code. * @summary Get user properties - such as group memberships, userName, displayName. @@ -94,7 +116,32 @@ export class UserController { const { user } = req const getAutoExec = user!.isAdmin || user!.userId == userId - return getUser(userId, getAutoExec) + return getUser({ id: userId }, getAutoExec) + } + + /** + * @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question. + * @param username The User's username + * @example username "johnSnow01" + */ + @Example({ + id: 1234, + displayName: 'John Snow', + username: 'johnSnow01', + isAdmin: false, + isActive: true + }) + @Patch('by/username/{username}') + public async updateUserByUsername( + @Path() username: string, + @Body() body: UserPayload + ): Promise { + const { MODE } = process.env + + if (MODE === ModeType.Desktop) + return updateDesktopAutoExec(body.autoExec ?? '') + + return updateUser({ username }, body) } /** @@ -119,7 +166,21 @@ export class UserController { if (MODE === ModeType.Desktop) return updateDesktopAutoExec(body.autoExec ?? '') - return updateUser(userId, body) + return updateUser({ id: userId }, body) + } + + /** + * @summary Delete a user. Can be performed either by admins, or the user in question. + * @param username The User's username + * @example username "johnSnow01" + */ + @Delete('by/username/{username}') + public async deleteUserByUsername( + @Path() username: string, + @Body() body: { password?: string }, + @Query() @Hidden() isAdmin: boolean = false + ) { + return deleteUser({ username }, isAdmin, body) } /** @@ -133,7 +194,7 @@ export class UserController { @Body() body: { password?: string }, @Query() @Hidden() isAdmin: boolean = false ) { - return deleteUser(userId, isAdmin, body) + return deleteUser({ id: userId }, isAdmin, body) } } @@ -174,11 +235,22 @@ const createUser = async (data: UserPayload): Promise => { } } +interface GetUserBy { + id?: number + username?: string +} + const getUser = async ( - id: number, + findBy: GetUserBy, getAutoExec: boolean ): Promise => { - const user = await User.findOne({ id }) + const user = (await User.findOne( + findBy, + `id displayName username isActive isAdmin autoExec -_id` + ).populate( + 'groups', + 'groupId name description -_id' + )) as unknown as UserDetailsResponse if (!user) throw new Error('User is not found.') @@ -188,7 +260,8 @@ const getUser = async ( username: user.username, isActive: user.isActive, isAdmin: user.isAdmin, - autoExec: getAutoExec ? user.autoExec ?? '' : undefined + autoExec: getAutoExec ? user.autoExec ?? '' : undefined, + groups: user.groups } } @@ -201,7 +274,7 @@ const getDesktopAutoExec = async () => { } const updateUser = async ( - id: number, + findBy: GetUserBy, data: Partial ): Promise => { const { displayName, username, password, isAdmin, isActive, autoExec } = data @@ -211,8 +284,13 @@ const updateUser = async ( if (username) { // Checking if user is already in the database const usernameExist = await User.findOne({ username }) - if (usernameExist && usernameExist.id != id) - throw new Error('Username already exists.') + if (usernameExist) { + if ( + (findBy.id && usernameExist.id != findBy.id) || + (findBy.username && usernameExist.username != findBy.username) + ) + throw new Error('Username already exists.') + } params.username = username } @@ -221,9 +299,10 @@ const updateUser = async ( params.password = User.hashPassword(password) } - const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true }) + const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true }) - if (!updatedUser) throw new Error(`Unable to find user with id: ${id}`) + if (!updatedUser) + throw new Error(`Unable to find user with ${findBy.id || findBy.username}`) return { id: updatedUser.id, @@ -245,11 +324,11 @@ const updateDesktopAutoExec = async (autoExec: string) => { } const deleteUser = async ( - id: number, + findBy: GetUserBy, isAdmin: boolean, { password }: { password?: string } ) => { - const user = await User.findOne({ id }) + const user = await User.findOne(findBy) if (!user) throw new Error('User is not found.') if (!isAdmin) { @@ -257,5 +336,5 @@ const deleteUser = async ( if (!validPass) throw new Error('Invalid password.') } - await User.deleteOne({ id }) + await User.deleteOne(findBy) } diff --git a/api/src/middlewares/verifyAdminIfNeeded.ts b/api/src/middlewares/verifyAdminIfNeeded.ts index d126f3c..c9246f6 100644 --- a/api/src/middlewares/verifyAdminIfNeeded.ts +++ b/api/src/middlewares/verifyAdminIfNeeded.ts @@ -1,11 +1,22 @@ import { RequestHandler } from 'express' +// This middleware checks if a non-admin user trying to +// access information of other user export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => { const { user } = req - const userId = parseInt(req.params.userId) - if (!user?.isAdmin && user?.userId !== userId) { - return res.status(401).send('Admin account required') + if (!user?.isAdmin) { + let adminAccountRequired: boolean = true + + if (req.params.userId) { + adminAccountRequired = user?.userId !== parseInt(req.params.userId) + } else if (req.params.username) { + adminAccountRequired = user?.username !== req.params.username + } + + if (adminAccountRequired) + return res.status(401).send('Admin account required') } + next() } diff --git a/api/src/model/User.ts b/api/src/model/User.ts index 04d550e..b70807d 100644 --- a/api/src/model/User.ts +++ b/api/src/model/User.ts @@ -45,6 +45,8 @@ interface IUserDocument extends UserPayload, Document { interface IUser extends IUserDocument { comparePassword(password: string): boolean + addGroup(groupObjectId: Schema.Types.ObjectId): Promise + removeGroup(groupObjectId: Schema.Types.ObjectId): Promise } interface IUserModel extends Model { hashPassword(password: string): string @@ -106,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean { if (bcrypt.compareSync(password, this.password)) return true return false }) +userSchema.method( + 'addGroup', + async function (groupObjectId: Schema.Types.ObjectId) { + const groupIdIndex = this.groups.indexOf(groupObjectId) + if (groupIdIndex === -1) { + this.groups.push(groupObjectId) + } + this.markModified('groups') + return this.save() + } +) +userSchema.method( + 'removeGroup', + async function (groupObjectId: Schema.Types.ObjectId) { + const groupIdIndex = this.groups.indexOf(groupObjectId) + if (groupIdIndex > -1) { + this.groups.splice(groupIdIndex, 1) + } + this.markModified('groups') + return this.save() + } +) export const User: IUserModel = model('User', userSchema) diff --git a/api/src/routes/api/spec/user.spec.ts b/api/src/routes/api/spec/user.spec.ts index 4e5e2bf..1c4fd99 100644 --- a/api/src/routes/api/spec/user.spec.ts +++ b/api/src/routes/api/spec/user.spec.ts @@ -3,7 +3,7 @@ import mongoose, { Mongoose } from 'mongoose' import { MongoMemoryServer } from 'mongodb-memory-server' import request from 'supertest' import appPromise from '../../../app' -import { UserController } from '../../../controllers/' +import { UserController, GroupController } from '../../../controllers/' import { generateAccessToken, saveTokensInDB } from '../../../utils' const clientId = 'someclientID' @@ -270,6 +270,102 @@ describe('user', () => { expect(res.text).toEqual('Error: Username already exists.') expect(res.body).toEqual({}) }) + + describe('by username', () => { + it('should respond with updated user when admin user requests', async () => { + const dbUser = await controller.createUser(user) + const newDisplayName = 'My new display Name' + + const res = await request(app) + .patch(`/SASjsApi/user/by/username/${user.username}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send({ ...user, displayName: newDisplayName }) + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(newDisplayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + }) + + it('should respond with updated user when user himself requests', async () => { + const dbUser = await controller.createUser(user) + const accessToken = await generateAndSaveToken(dbUser.id) + const newDisplayName = 'My new display Name' + + const res = await request(app) + .patch(`/SASjsApi/user/by/username/${user.username}`) + .auth(accessToken, { type: 'bearer' }) + .send({ + displayName: newDisplayName, + username: user.username, + password: user.password + }) + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(newDisplayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + }) + + it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => { + const dbUser = await controller.createUser(user) + const accessToken = await generateAndSaveToken(dbUser.id) + const newDisplayName = 'My new display Name' + + await request(app) + .patch(`/SASjsApi/user/by/username/${user.username}`) + .auth(accessToken, { type: 'bearer' }) + .send({ ...user, displayName: newDisplayName }) + .expect(400) + }) + + it('should respond with Unauthorized if access token is not present', async () => { + const res = await request(app) + .patch('/SASjsApi/user/by/username/1234') + .send(user) + .expect(401) + + expect(res.text).toEqual('Unauthorized') + expect(res.body).toEqual({}) + }) + + it('should respond with Unauthorized when access token is not of an admin account or himself', async () => { + const dbUser1 = await controller.createUser(user) + const dbUser2 = await controller.createUser({ + ...user, + username: 'randomUser' + }) + const accessToken = await generateAndSaveToken(dbUser2.id) + + const res = await request(app) + .patch(`/SASjsApi/user/${dbUser1.id}`) + .auth(accessToken, { type: 'bearer' }) + .send(user) + .expect(401) + + expect(res.text).toEqual('Admin account required') + expect(res.body).toEqual({}) + }) + + it('should respond with Forbidden if username is already present', async () => { + const dbUser1 = await controller.createUser(user) + const dbUser2 = await controller.createUser({ + ...user, + username: 'randomuser' + }) + + const res = await request(app) + .patch(`/SASjsApi/user/by/username/${dbUser1.username}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send({ username: dbUser2.username }) + .expect(403) + + expect(res.text).toEqual('Error: Username already exists.') + expect(res.body).toEqual({}) + }) + }) }) describe('delete', () => { @@ -363,6 +459,89 @@ describe('user', () => { expect(res.text).toEqual('Error: Invalid password.') expect(res.body).toEqual({}) }) + + describe('by username', () => { + it('should respond with OK when admin user requests', async () => { + const dbUser = await controller.createUser(user) + + const res = await request(app) + .delete(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body).toEqual({}) + }) + + it('should respond with OK when user himself requests', async () => { + const dbUser = await controller.createUser(user) + const accessToken = await generateAndSaveToken(dbUser.id) + + const res = await request(app) + .delete(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(accessToken, { type: 'bearer' }) + .send({ password: user.password }) + .expect(200) + + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request when user himself requests and password is missing', async () => { + const dbUser = await controller.createUser(user) + const accessToken = await generateAndSaveToken(dbUser.id) + + const res = await request(app) + .delete(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(400) + + expect(res.text).toEqual(`"password" is required`) + expect(res.body).toEqual({}) + }) + + it('should respond with Unauthorized when access token is not present', async () => { + const res = await request(app) + .delete('/SASjsApi/user/by/username/RandomUsername') + .send(user) + .expect(401) + + expect(res.text).toEqual('Unauthorized') + expect(res.body).toEqual({}) + }) + + it('should respond with Unauthorized when access token is not of an admin account or himself', async () => { + const dbUser1 = await controller.createUser(user) + const dbUser2 = await controller.createUser({ + ...user, + username: 'randomUser' + }) + const accessToken = await generateAndSaveToken(dbUser2.id) + + const res = await request(app) + .delete(`/SASjsApi/user/by/username/${dbUser1.username}`) + .auth(accessToken, { type: 'bearer' }) + .send(user) + .expect(401) + + expect(res.text).toEqual('Admin account required') + expect(res.body).toEqual({}) + }) + + it('should respond with Forbidden when user himself requests and password is incorrect', async () => { + const dbUser = await controller.createUser(user) + const accessToken = await generateAndSaveToken(dbUser.id) + + const res = await request(app) + .delete(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(accessToken, { type: 'bearer' }) + .send({ password: 'incorrectpassword' }) + .expect(403) + + expect(res.text).toEqual('Error: Invalid password.') + expect(res.body).toEqual({}) + }) + }) }) describe('get', () => { @@ -392,6 +571,7 @@ describe('user', () => { expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isActive).toEqual(user.isActive) expect(res.body.autoExec).toEqual(user.autoExec) + expect(res.body.groups).toEqual([]) }) it('should respond with user autoExec when admin user requests', async () => { @@ -409,6 +589,7 @@ describe('user', () => { expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isActive).toEqual(user.isActive) expect(res.body.autoExec).toEqual(user.autoExec) + expect(res.body.groups).toEqual([]) }) it('should respond with user when access token is not of an admin account', async () => { @@ -431,6 +612,34 @@ describe('user', () => { expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isActive).toEqual(user.isActive) expect(res.body.autoExec).toBeUndefined() + expect(res.body.groups).toEqual([]) + }) + + it('should respond with user along with associated groups', async () => { + const dbUser = await controller.createUser(user) + const userId = dbUser.id + const accessToken = await generateAndSaveToken(userId) + + const group = { + name: 'DCGroup1', + description: 'DC group for testing purposes.' + } + const groupController = new GroupController() + const dbGroup = await groupController.createGroup(group) + await groupController.addUserToGroup(dbGroup.groupId, dbUser.id) + + const res = await request(app) + .get(`/SASjsApi/user/${userId}`) + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(user.displayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + expect(res.body.autoExec).toEqual(user.autoExec) + expect(res.body.groups.length).toBeGreaterThan(0) }) it('should respond with Unauthorized if access token is not present', async () => { @@ -455,6 +664,86 @@ describe('user', () => { expect(res.text).toEqual('Error: User is not found.') expect(res.body).toEqual({}) }) + + describe('by username', () => { + it('should respond with user autoExec when same user requests', async () => { + const dbUser = await controller.createUser(user) + const userId = dbUser.id + const accessToken = await generateAndSaveToken(userId) + + const res = await request(app) + .get(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(user.displayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + expect(res.body.autoExec).toEqual(user.autoExec) + }) + + it('should respond with user autoExec when admin user requests', async () => { + const dbUser = await controller.createUser(user) + + const res = await request(app) + .get(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(user.displayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + expect(res.body.autoExec).toEqual(user.autoExec) + }) + + it('should respond with user when access token is not of an admin account', async () => { + const accessToken = await generateSaveTokenAndCreateUser({ + ...user, + username: 'randomUser' + }) + + const dbUser = await controller.createUser(user) + + const res = await request(app) + .get(`/SASjsApi/user/by/username/${dbUser.username}`) + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body.username).toEqual(user.username) + expect(res.body.displayName).toEqual(user.displayName) + expect(res.body.isAdmin).toEqual(user.isAdmin) + expect(res.body.isActive).toEqual(user.isActive) + expect(res.body.autoExec).toBeUndefined() + }) + + it('should respond with Unauthorized if access token is not present', async () => { + const res = await request(app) + .get('/SASjsApi/user/by/username/randomUsername') + .send() + .expect(401) + + expect(res.text).toEqual('Unauthorized') + expect(res.body).toEqual({}) + }) + + it('should respond with Forbidden if username is incorrect', async () => { + await controller.createUser(user) + + const res = await request(app) + .get('/SASjsApi/user/by/username/randomUsername') + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(403) + + expect(res.text).toEqual('Error: User is not found.') + expect(res.body).toEqual({}) + }) + }) }) describe('getAll', () => { diff --git a/api/src/routes/api/user.ts b/api/src/routes/api/user.ts index fcd6d5e..20ce88c 100644 --- a/api/src/routes/api/user.ts +++ b/api/src/routes/api/user.ts @@ -7,6 +7,7 @@ import { } from '../../middlewares' import { deleteUserValidation, + getUserValidation, registerUserValidation, updateUserValidation } from '../../utils' @@ -36,6 +37,25 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => { } }) +userRouter.get( + '/by/username/:username', + authenticateAccessToken, + async (req, res) => { + const { error, value: params } = getUserValidation(req.params) + if (error) return res.status(400).send(error.details[0].message) + + const { username } = params + + const controller = new UserController() + try { + const response = await controller.getUserByUsername(req, username) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + userRouter.get('/:userId', authenticateAccessToken, async (req, res) => { const { userId } = req.params @@ -48,6 +68,34 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => { } }) +userRouter.patch( + '/by/username/:username', + authenticateAccessToken, + verifyAdminIfNeeded, + async (req, res) => { + const { user } = req + const { error: errorUsername, value: params } = getUserValidation( + req.params + ) + if (errorUsername) + return res.status(400).send(errorUsername.details[0].message) + + const { username } = params + + // only an admin can update `isActive` and `isAdmin` fields + const { error, value: body } = updateUserValidation(req.body, user!.isAdmin) + if (error) return res.status(400).send(error.details[0].message) + + const controller = new UserController() + try { + const response = await controller.updateUserByUsername(username, body) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + userRouter.patch( '/:userId', authenticateAccessToken, @@ -70,6 +118,34 @@ userRouter.patch( } ) +userRouter.delete( + '/by/username/:username', + authenticateAccessToken, + verifyAdminIfNeeded, + async (req, res) => { + const { user } = req + const { error: errorUsername, value: params } = getUserValidation( + req.params + ) + if (errorUsername) + return res.status(400).send(errorUsername.details[0].message) + + const { username } = params + + // only an admin can delete user without providing password + const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin) + if (error) return res.status(400).send(error.details[0].message) + + const controller = new UserController() + try { + await controller.deleteUserByUsername(username, data, user!.isAdmin) + res.status(200).send('Account Deleted!') + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + userRouter.delete( '/:userId', authenticateAccessToken, diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 33a403f..3499715 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -5,6 +5,11 @@ const passwordSchema = Joi.string().min(6).max(1024) export const blockFileRegex = /\.(exe|sh|htaccess)$/i +export const getUserValidation = (data: any): Joi.ValidationResult => + Joi.object({ + username: usernameSchema.required() + }).validate(data) + export const loginWebValidation = (data: any): Joi.ValidationResult => Joi.object({ username: usernameSchema.required(),