From 8734489cf014aedaca3f325e689493e4fe0b71ca Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Tue, 14 Jun 2022 09:12:41 +0000 Subject: [PATCH 1/8] fix: forcing utf 8 encoding. Closes #76 --- api/src/controllers/internal/Session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index b83d70e..72c8907 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -93,6 +93,7 @@ ${autoExecContent}` session.path, '-AUTOEXEC', autoExecPath, + '-ENCODING UTF-8', process.platform === 'win32' ? '-nosplash' : '' ]) .then(() => { From e059bee7dcf7df8c1bf7031fd48056671d3a2b6b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 Jun 2022 09:20:37 +0000 Subject: [PATCH 2/8] chore(release): 0.3.9 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ded935..f0712db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From 32d372b42fbf56b6c0779e8f704164eaae1c7548 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Tue, 14 Jun 2022 09:49:05 +0000 Subject: [PATCH 3/8] fix: correct syntax for encoding option --- api/src/controllers/internal/Session.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index 72c8907..a120128 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -93,7 +93,8 @@ ${autoExecContent}` session.path, '-AUTOEXEC', autoExecPath, - '-ENCODING UTF-8', + '-ENCODING', + 'UTF-8', process.platform === 'win32' ? '-nosplash' : '' ]) .then(() => { From 806ea4cb5c76aff1e0120cafe9e70f326f0ac280 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 Jun 2022 09:53:53 +0000 Subject: [PATCH 4/8] chore(release): 0.3.10 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0712db..d3ee046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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) From aef411a0eac625c33274dfe3e88b6f75115c44d8 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 14 Jun 2022 22:08:56 +0500 Subject: [PATCH 5/8] feat: new APIs added for GET|PATCH|DELETE of user by username --- api/public/swagger.yaml | 88 +++++++ api/src/controllers/user.ts | 96 ++++++-- api/src/middlewares/verifyAdminIfNeeded.ts | 17 +- api/src/routes/api/spec/user.spec.ts | 259 +++++++++++++++++++++ api/src/routes/api/user.ts | 76 ++++++ api/src/utils/validation.ts | 5 + 6 files changed, 525 insertions(+), 16 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 1f76c10..7e761fe 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -985,6 +985,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/user.ts b/api/src/controllers/user.ts index cfbce9d..99a8f9b 100644 --- a/api/src/controllers/user.ts +++ b/api/src/controllers/user.ts @@ -77,6 +77,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 +114,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 +164,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 +192,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 +233,16 @@ 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) if (!user) throw new Error('User is not found.') @@ -201,7 +265,7 @@ const getDesktopAutoExec = async () => { } const updateUser = async ( - id: number, + findBy: GetUserBy, data: Partial ): Promise => { const { displayName, username, password, isAdmin, isActive, autoExec } = data @@ -211,8 +275,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 +290,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 +315,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 +327,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/routes/api/spec/user.spec.ts b/api/src/routes/api/spec/user.spec.ts index 4e5e2bf..36913a3 100644 --- a/api/src/routes/api/spec/user.spec.ts +++ b/api/src/routes/api/spec/user.spec.ts @@ -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', () => { @@ -455,6 +634,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(), From eef3cb270dd7a3fbbc0a96d933cf9a297baf1fc4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 Jun 2022 17:28:50 +0000 Subject: [PATCH 6/8] chore(release): 0.4.0 [skip ci] # [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ee046..19abfbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [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) From e08bbcc5435cbabaee40a41a7fb667d4a1f078e6 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 15 Jun 2022 15:18:42 +0500 Subject: [PATCH 7/8] fix: add/remove group to User when adding/removing user from group and return group membership on getting user --- api/public/swagger.yaml | 34 ++++++++++++++++------------ api/src/controllers/group.ts | 5 +++- api/src/controllers/user.ts | 13 +++++++++-- api/src/model/User.ts | 24 ++++++++++++++++++++ api/src/routes/api/spec/user.spec.ts | 32 +++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 7e761fe..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: 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/user.ts b/api/src/controllers/user.ts index 99a8f9b..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') @@ -242,7 +244,13 @@ const getUser = async ( findBy: GetUserBy, getAutoExec: boolean ): Promise => { - const user = await User.findOne(findBy) + 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.') @@ -252,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 } } 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 36913a3..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' @@ -571,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 () => { @@ -588,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 () => { @@ -610,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 () => { From c08cfcbc38c2f873934bfea3c4fbd287ca6ca437 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 15 Jun 2022 10:38:22 +0000 Subject: [PATCH 8/8] chore(release): 0.4.1 [skip ci] ## [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)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19abfbb..f3a72b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [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)