Add backend services for password reset
This commit is contained in:
parent
4259778224
commit
fc2666364d
121
app/src/api/services/passwordReset.ts
Normal file
121
app/src/api/services/passwordReset.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import url, { URL } from 'url';
|
||||
import { errors } from 'pg-promise';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import db from '../../db';
|
||||
import * as userService from './user';
|
||||
import { transporter } from './email';
|
||||
|
||||
|
||||
/**
|
||||
* Generate a password reset token for the specified account and send the password reset link by email
|
||||
* @param email the email address for which to generate a password reset token
|
||||
* @param siteOrigin the origin of the website, without a path element - e.g. https://beta.colouring.london
|
||||
*/
|
||||
export async function sendPasswordResetToken(email: string, siteOrigin: string): Promise<void> {
|
||||
const user = await userService.getUserByEmail(email);
|
||||
|
||||
// if no user found for email, do nothing
|
||||
if (user == undefined) return;
|
||||
|
||||
const token = await createPasswordResetToken(user.user_id);
|
||||
const message = getPasswordResetEmail(email, token, siteOrigin);
|
||||
await transporter.sendMail(message);
|
||||
}
|
||||
|
||||
export async function resetPassword(passwordResetToken: string, newPassword: string): Promise<void> {
|
||||
const userId = await verifyPasswordResetToken(passwordResetToken);
|
||||
if (userId != undefined) {
|
||||
await updatePasswordForUser(userId, newPassword);
|
||||
//TODO: expire other tokens of the user?
|
||||
} else {
|
||||
throw new TokenVerificationError('Password reset token could not be verified');
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenVerificationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "TokenVerificationError";
|
||||
}
|
||||
}
|
||||
|
||||
function getPasswordResetEmail(email: string, token: string, siteOrigin: string): nodemailer.SendMailOptions {
|
||||
const linkUrl = new URL(siteOrigin);
|
||||
linkUrl.pathname = '/password-reset.html';
|
||||
linkUrl.search = `?token=${token}`;
|
||||
|
||||
const linkString = url.format(linkUrl);
|
||||
|
||||
const messageBody = `Hi there,
|
||||
|
||||
Someone has requested a password reset for the Colouring London account associated with this email address.
|
||||
Click on the following link within the next 24 hours to reset your password:
|
||||
|
||||
${linkString}
|
||||
`;
|
||||
|
||||
return {
|
||||
text: messageBody,
|
||||
subject: 'Reset your Colouring London password',
|
||||
to: email,
|
||||
from: 'no-reply@colouring.london'
|
||||
};
|
||||
}
|
||||
|
||||
async function createPasswordResetToken(userId: string) {
|
||||
const { token } = await db.one(
|
||||
`INSERT INTO
|
||||
user_password_reset_tokens (user_id, expires_on)
|
||||
VALUES
|
||||
($1::uuid, now() at time zone 'utc' + INTERVAL '$2 day')
|
||||
RETURNING
|
||||
token
|
||||
`, [userId, 1]
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the password reset token is valid and expire the token
|
||||
* @param token password reset token to verify and use
|
||||
* @returns the UUID of the user whose token was used
|
||||
*/
|
||||
async function verifyPasswordResetToken(token: string): Promise<string> {
|
||||
try {
|
||||
// verify and deactivate the token in one operation
|
||||
const usedToken = await db.one(
|
||||
`UPDATE
|
||||
user_password_reset_tokens
|
||||
SET used = true
|
||||
WHERE
|
||||
token = $1::uuid
|
||||
AND NOT used
|
||||
AND now() at time zone 'utc' < expires_on
|
||||
RETURNING *`, [token]
|
||||
);
|
||||
console.log('verified');
|
||||
|
||||
return usedToken.user_id;
|
||||
} catch (err) {
|
||||
if (err instanceof errors.QueryResultError) {
|
||||
console.log(err.code);
|
||||
if (err.code === errors.queryResultErrorCode.noData) return undefined;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePasswordForUser(userId: string, newPassword: string): Promise<null> {
|
||||
return db.none(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
pass = crypt($1, gen_salt('bf'))
|
||||
WHERE
|
||||
user_id = $2::uuid
|
||||
`, [newPassword, userId]);
|
||||
}
|
||||
|
@ -86,6 +86,21 @@ function getUserById(id) {
|
||||
});
|
||||
}
|
||||
|
||||
function getUserByEmail(email: string) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id, username, email
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1
|
||||
`, [email]
|
||||
).catch(function(error) {
|
||||
console.error('Error:', error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getNewUserAPIKey(id) {
|
||||
return db.one(
|
||||
`UPDATE
|
||||
@ -152,6 +167,7 @@ function logout(session: Express.Session) {
|
||||
|
||||
export {
|
||||
getUserById,
|
||||
getUserByEmail,
|
||||
createUser,
|
||||
authUser,
|
||||
getNewUserAPIKey,
|
||||
|
Loading…
Reference in New Issue
Block a user