diff --git a/packages/backend/src/controllers/api/v1/users/update-current-user-password.js b/packages/backend/src/controllers/api/v1/users/update-current-user-password.js new file mode 100644 index 00000000..b3727a20 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/update-current-user-password.js @@ -0,0 +1,9 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const user = await request.currentUser + .$query() + .patchAndFetch({ password: request.body.password }); + + renderObject(response, user); +}; diff --git a/packages/backend/src/controllers/api/v1/users/update-current-user-password.test.js b/packages/backend/src/controllers/api/v1/users/update-current-user-password.test.js new file mode 100644 index 00000000..fc5b0305 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/update-current-user-password.test.js @@ -0,0 +1,41 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import updateCurrentUserPasswordMock from '../../../../../test/mocks/rest/api/v1/users/update-current-user-password.js'; + +describe('PATCH /api/v1/users/:userId/password', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return updated user with valid password', async () => { + const response = await request(app) + .patch(`/api/v1/users/${currentUser.id}/password`) + .set('Authorization', token) + .send({ password: 'new-password' }) + .expect(200); + + const refetchedCurrentUser = await currentUser.$query(); + const expectedPayload = updateCurrentUserPasswordMock(refetchedCurrentUser); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return HTTP 422 with invalid password', async () => { + const response = await request(app) + .patch(`/api/v1/users/${currentUser.id}/password`) + .set('Authorization', token) + .send({ password: '' }) + .expect(422); + + expect(response.body.meta.type).toEqual('ModelValidation'); + expect(response.body.errors).toMatchObject({ + password: ['must NOT have fewer than 6 characters'], + }); + }); +}); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 3b92c99b..1f8a5da9 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -42,7 +42,7 @@ class User extends Base { id: { type: 'string', format: 'uuid' }, fullName: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, - password: { type: 'string' }, + password: { type: 'string', minLength: 6 }, status: { type: 'string', enum: ['active', 'invited'], diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 2f42aeb5..672b7d35 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -4,6 +4,7 @@ import { authorizeUser } from '../../../helpers/authorization.js'; import checkIsCloud from '../../../helpers/check-is-cloud.js'; import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; import updateCurrentUserAction from '../../../controllers/api/v1/users/update-current-user.js'; +import updateCurrentUserPasswordAction from '../../../controllers/api/v1/users/update-current-user-password.js'; import deleteCurrentUserAction from '../../../controllers/api/v1/users/delete-current-user.js'; import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; import getAppsAction from '../../../controllers/api/v1/users/get-apps.js'; @@ -18,6 +19,13 @@ const router = Router(); router.get('/me', authenticateUser, getCurrentUserAction); router.patch('/:userId', authenticateUser, updateCurrentUserAction); + +router.patch( + '/:userId/password', + authenticateUser, + updateCurrentUserPasswordAction +); + router.get('/:userId/apps', authenticateUser, authorizeUser, getAppsAction); router.get('/invoices', authenticateUser, checkIsCloud, getInvoicesAction); router.delete('/:userId', authenticateUser, deleteCurrentUserAction); diff --git a/packages/backend/test/mocks/rest/api/v1/users/update-current-user-password.js b/packages/backend/test/mocks/rest/api/v1/users/update-current-user-password.js new file mode 100644 index 00000000..e0e65221 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/update-current-user-password.js @@ -0,0 +1,22 @@ +const updateCurrentUserPasswordMock = (currentUser) => { + return { + data: { + createdAt: currentUser.createdAt.getTime(), + email: currentUser.email, + fullName: currentUser.fullName, + id: currentUser.id, + status: currentUser.status, + updatedAt: currentUser.updatedAt.getTime(), + trialExpiryDate: currentUser.trialExpiryDate?.toISOString(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'User', + }, + }; +}; + +export default updateCurrentUserPasswordMock;