diff --git a/packages/backend/src/controllers/api/v1/users/reset-password.js b/packages/backend/src/controllers/api/v1/users/reset-password.js new file mode 100644 index 00000000..3044e7b0 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/reset-password.js @@ -0,0 +1,23 @@ +import User from '../../../../models/user.js'; +import { renderError } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const { token, password } = request.body; + + const user = await User.query() + .findOne({ + reset_password_token: token, + }) + .throwIfNotFound(); + + if (!user.isResetPasswordTokenValid()) { + return renderError(response, [{ general: [invalidTokenErrorMessage] }]); + } + + await user.resetPassword(password); + + response.status(204).end(); +}; + +const invalidTokenErrorMessage = + 'Reset password link is not valid or expired. Try generating a new link.'; diff --git a/packages/backend/src/controllers/api/v1/users/reset-password.test.js b/packages/backend/src/controllers/api/v1/users/reset-password.test.js new file mode 100644 index 00000000..e36342df --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/reset-password.test.js @@ -0,0 +1,49 @@ +import { describe, it, beforeEach } from 'vitest'; +import request from 'supertest'; +import { DateTime } from 'luxon'; +import app from '../../../../app.js'; +import { createUser } from '../../../../../test/factories/user'; + +describe('POST /api/v1/users/reset-password', () => { + let currentUser; + + beforeEach(async () => { + currentUser = await createUser({ + resetPasswordToken: 'sampleResetPasswordToken', + resetPasswordTokenSentAt: DateTime.now().toISO(), + }); + }); + + it('should respond with no content', async () => { + await request(app) + .post('/api/v1/users/reset-password') + .send({ + token: currentUser.resetPasswordToken, + password: 'newPassword', + }) + .expect(204); + }); + + it('should return not found response for not existing user', async () => { + await request(app) + .post('/api/v1/users/reset-password') + .send({ + token: 'nonExistingResetPasswordToken', + }) + .expect(404); + }); + + it('should return unprocessable entity for existing user with expired reset password token', async () => { + const user = await createUser({ + resetPasswordToken: 'anotherResetPasswordToken', + resetPasswordTokenSentAt: DateTime.now().minus({ days: 2 }).toISO(), + }); + + await request(app) + .post('/api/v1/users/reset-password') + .send({ + token: user.resetPasswordToken, + }) + .expect(422); + }); +}); diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 988d91bc..d9ea9567 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -15,7 +15,6 @@ import executeFlow from './mutations/execute-flow.js'; import generateAuthUrl from './mutations/generate-auth-url.js'; import registerUser from './mutations/register-user.ee.js'; import resetConnection from './mutations/reset-connection.js'; -import resetPassword from './mutations/reset-password.ee.js'; import updateAppAuthClient from './mutations/update-app-auth-client.ee.js'; import updateAppConfig from './mutations/update-app-config.ee.js'; import updateConfig from './mutations/update-config.ee.js'; @@ -34,6 +33,7 @@ import verifyConnection from './mutations/verify-connection.js'; import deleteUser from './mutations/delete-user.ee.js'; import login from './mutations/login.js'; import forgotPassword from './mutations/forgot-password.ee.js'; +import resetPassword from './mutations/reset-password.ee.js'; const mutationResolvers = { createAppAuthClient, diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js index 34e8bb55..7eb4133b 100644 --- a/packages/backend/src/routes/api/v1/users.js +++ b/packages/backend/src/routes/api/v1/users.js @@ -11,6 +11,7 @@ import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscri import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js'; import acceptInvitationAction from '../../../controllers/api/v1/users/accept-invitation.js'; import forgotPasswordAction from '../../../controllers/api/v1/users/forgot-password.js'; +import resetPasswordAction from '../../../controllers/api/v1/users/reset-password.js'; const router = Router(); @@ -54,4 +55,6 @@ router.get( router.post('/invitation', asyncHandler(acceptInvitationAction)); router.post('/forgot-password', asyncHandler(forgotPasswordAction)); +router.post('/reset-password', asyncHandler(resetPasswordAction)); + export default router;