diff --git a/packages/backend/src/graphql/mutation-resolvers.ts b/packages/backend/src/graphql/mutation-resolvers.ts index ffd2cb55..5d8b8615 100644 --- a/packages/backend/src/graphql/mutation-resolvers.ts +++ b/packages/backend/src/graphql/mutation-resolvers.ts @@ -15,6 +15,7 @@ import deleteStep from './mutations/delete-step'; import createUser from './mutations/create-user.ee'; import updateUser from './mutations/update-user'; import forgotPassword from './mutations/forgot-password.ee'; +import resetPassword from './mutations/reset-password.ee'; import login from './mutations/login'; const mutationResolvers = { @@ -35,6 +36,7 @@ const mutationResolvers = { createUser, updateUser, forgotPassword, + resetPassword, login, }; diff --git a/packages/backend/src/graphql/mutations/reset-password.ee.ts b/packages/backend/src/graphql/mutations/reset-password.ee.ts new file mode 100644 index 00000000..c8a350bc --- /dev/null +++ b/packages/backend/src/graphql/mutations/reset-password.ee.ts @@ -0,0 +1,30 @@ +import User from '../../models/user'; + +type Params = { + input: { + token: string; + password: string; + }; +}; + +const resetPassword = async (_parent: unknown, params: Params) => { + const { token, password } = params.input; + + if (!token) { + throw new Error('Reset password token is required!'); + } + + const user = await User.query().findOne({ reset_password_token: token }); + + if (!user || !user.isResetPasswordTokenValid()) { + throw new Error( + 'Reset password link is not valid or expired. Try generating a new link.' + ); + } + + await user.resetPassword(password); + + return; +}; + +export default resetPassword; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 07b8b9a0..e60add0a 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -51,6 +51,7 @@ type Mutation { createUser(input: CreateUserInput): User updateUser(input: UpdateUserInput): User forgotPassword(input: ForgotPasswordInput): Boolean + resetPassword(input: ResetPasswordInput): Boolean login(input: LoginInput): Auth } @@ -313,7 +314,12 @@ input UpdateUserInput { } input ForgotPasswordInput { - email: String + email: String! +} + +input ResetPasswordInput { + token: String! + password: String! } input LoginInput { diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts index 5fe668c9..7dc2935b 100644 --- a/packages/backend/src/models/user.ts +++ b/packages/backend/src/models/user.ts @@ -87,6 +87,26 @@ class User extends Base { await this.$query().patch({ resetPasswordToken, resetPasswordTokenSentAt }); } + async resetPassword(password: string) { + return await this.$query().patch({ + resetPasswordToken: null, + resetPasswordTokenSentAt: null, + password, + }); + } + + async isResetPasswordTokenValid() { + if (!this.resetPasswordTokenSentAt) { + return false; + } + + const sentAt = new Date(this.resetPasswordTokenSentAt); + const now = new Date(); + const fourHoursInMilliseconds = 1000 * 60 * 60 * 4; + + return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds; + } + async generateHash() { this.password = await bcrypt.hash(this.password, 10); }