Skip to content

Commit

Permalink
implement password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
jilv220 committed Nov 10, 2023
1 parent 7c74dca commit 0f6238b
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 64 deletions.
3 changes: 2 additions & 1 deletion app.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="lucia" />
declare namespace Lucia {
type Auth = import("./lucia.js").Auth;
// this has to point to the lucia config file
type Auth = import('./src/db/lucia.ts').Auth;
type DatabaseUserAttributes = {
username: string;
email: string;
Expand Down
4 changes: 2 additions & 2 deletions src/constant/error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const UNKNOWN_ERR = 'Unknown error';
export const INCORRECT_USERNAME_OR_PASSWORD = 'Incorrect username or password';
export const INCORRECT_EMAIL_OR_PASSWORD = 'Incorrect email or password';
export const INVALID_EMAIL_VERIFICATION_TOKEN = 'Invalid email verification token';
export const EXPIRED_EMAIL_VERIFICATION_TOKEN = 'Expired email verification token';
export const INVALID_TOKEN = 'Invalid token';
export const EXPIRED_TOKEN = 'Expired token';

export class EmailVerificationTokenError extends Error {}
15 changes: 3 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import express from 'express';
import signupRouter from './routes/signup.ts';
import loginRouter from './routes/login.ts';
import userRouter from './routes/user.ts';
import logoutRouter from './routes/logout.ts';
import emailVerificationRouter from './routes/resendVerification.ts';
import rooterRouter from './routes/root.ts';
import 'dotenv/config';

const app = express();
Expand All @@ -13,17 +9,12 @@ const port = 3000;
app.use(
express.urlencoded({
extended: true,
}),
})
);
app.use(express.json());

// Routes
app
.use('/', signupRouter)
.use('/', loginRouter)
.use('/', userRouter)
.use('/', logoutRouter)
.use('/', emailVerificationRouter);
app.use('/', rooterRouter);

app.listen(port, () => {
console.log(`Http server listening on port ${port}`);
Expand Down
39 changes: 33 additions & 6 deletions src/lib/email.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import { transporter } from '../config/email.ts';
import { TOKEN_TYPE } from '../models/tokenManger.ts';

function buildTokenOpts(email: string, token: string, tokenType: TOKEN_TYPE) {
let endPoint;
let msg;
let subject;

export const sendEmailVerificationLink = (email: string, token: string) => {
const domainURL = process.env.DOMAIN_URL!;
const url = `${domainURL}/email-verification/${token}`;
const msg = 'Click Here to Verify Your Account';

const mailOpts = {
// eslint-disable-next-line default-case
switch (tokenType) {
case 'email_verification':
endPoint = 'email-verification';
msg = 'Click Here to Verify Your Account';
subject = 'Verify Your Account Now';
break;
case 'password_reset':
endPoint = 'password-reset';
msg = 'Click Here to Reset Your Password';
subject = 'Reset Your Password';
break;
}
const url = `${domainURL}/${endPoint}/${token}`;

return {
from: process.env.SMTP_USER!,
to: email,
subject: 'Verify Your Account Now',
subject,
text: `${url} ${msg}`,
html: `<a href=${url}>${msg}</a>`,
};
}

const sendTokenLink = (email: string, token: string, tokenType: TOKEN_TYPE) => {
const emailOpts = buildTokenOpts(email, token, tokenType);
transporter
.sendMail(mailOpts)
.sendMail(emailOpts)
.then((info) => {
console.log(`Email sent: ${info.messageId}`);
})
.catch((err) => {
console.error(err);
});
};

export const sendEmailVerificationLink = (email: string, token: string) =>
sendTokenLink(email, token, 'email_verification');

export const sendPasswordResetLink = (email: string, token: string) =>
sendTokenLink(email, token, 'password_reset');
11 changes: 4 additions & 7 deletions src/models/tokenManger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import { db } from '../db/db.ts';
import { emailVerificationToken } from '../schema/email_verification_token.ts';
import { IToken } from '../types/token.ts';
import { passwordResetToken } from '../schema/password_reset_token.ts';
import {
INVALID_EMAIL_VERIFICATION_TOKEN,
EXPIRED_EMAIL_VERIFICATION_TOKEN,
} from '../constant/error.ts';
import { EXPIRED_TOKEN, INVALID_TOKEN } from '../constant/error.ts';

type TOKEN_TYPE = 'email_verification' | 'password_reset';
export type TOKEN_TYPE = 'email_verification' | 'password_reset';

function buildTokenDAO(tokenType: TOKEN_TYPE) {
let token;
Expand Down Expand Up @@ -81,15 +78,15 @@ class TokenManager implements IToken {
const tokenInDB = await this.dbQuery.findFirst({
where: eq(this.tokenDAO.id, token),
});
if (!tokenInDB) throw new Error(INVALID_EMAIL_VERIFICATION_TOKEN);
if (!tokenInDB) throw new Error(INVALID_TOKEN);
await tx
.delete(this.tokenDAO)
.where(eq(this.tokenDAO.userId, tokenInDB.userId));
return tokenInDB;
});
const tokenExpires = Number(storedToken.expires);
if (!isWithinExpiration(tokenExpires)) {
throw new Error(EXPIRED_EMAIL_VERIFICATION_TOKEN);
throw new Error(EXPIRED_TOKEN);
}
return storedToken.userId;
}
Expand Down
15 changes: 7 additions & 8 deletions src/routes/login.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Router } from 'express';
import { userSelectSchema } from '../validator/user.ts';
import { userLoginSchema } from '../validator/user.ts';
import {
buildClientErrorResponse,
buildUnknownErrorResponse,
Expand All @@ -10,13 +10,10 @@ import { INCORRECT_EMAIL_OR_PASSWORD } from '../constant/error.ts';

const router = Router();
router.post('/login', async (req, res) => {
const userParseRes = userSelectSchema.safeParse(req.body);
const userParseRes = userLoginSchema.safeParse(req.body);

if (!userParseRes.success) {
const formatted = userParseRes.error.format();
if (formatted.username) {
return buildClientErrorResponse(res, 'Invalid username');
}
if (formatted.password) {
return buildClientErrorResponse(res, 'Invalid password');
}
Expand All @@ -43,9 +40,11 @@ router.post('/login', async (req, res) => {
userId: key.unwrap().userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(session);
res.setHeader('Set-Cookie', sessionCookie.serialize());
return res.sendStatus(302);
const authRequest = auth.handleRequest(req, res);
authRequest.setSession(session);
return res.json({
redirectTo: '/',
});
});

export default router;
5 changes: 4 additions & 1 deletion src/routes/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { auth } from '../db/lucia.ts';
const router = Router();
router.post('/logout', async (req, res) => {
const authRequest = auth.handleRequest(req, res);
const session = await authRequest.validateBearerToken();
let session = await authRequest.validate();
if (!session) {
session = await authRequest.validateBearerToken();
}
if (!session) {
return res.sendStatus(401);
}
Expand Down
89 changes: 89 additions & 0 deletions src/routes/passwordReset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,93 @@
import { Router } from 'express';
import { eq } from 'drizzle-orm';
import {
passwordResetSchema,
requestPasswordResetSchema,
} from '../validator/user.ts';
import {
buildClientErrorResponse,
buildUnknownErrorResponse,
} from '../lib/utils.ts';
import { db } from '../db/db.ts';
import { user } from '../schema/user.ts';
import { passwordResetTM } from '../models/tokenManger.ts';
import { sendPasswordResetLink } from '../lib/email.ts';
import { auth } from '../db/lucia.ts';
import { EXPIRED_TOKEN, INVALID_TOKEN } from '../constant/error.ts';

const router = Router();
router.post('/password-reset', async (req, res) => {
const parseRes = requestPasswordResetSchema.safeParse(req.body);
if (!parseRes.success) {
const formatted = parseRes.error.format();
if (formatted.email) {
return buildClientErrorResponse(res, 'Invalid Email');
}
return buildUnknownErrorResponse(res);
}
const { email } = parseRes.data;

try {
const storedUser = await db.query.user.findFirst({
where: eq(user.email, email),
});

if (!storedUser) {
return buildClientErrorResponse(res, 'User does not exist');
}
const token = await passwordResetTM.generate(storedUser.id);
sendPasswordResetLink(email, token);

return res.send();
} catch (e) {
if (e instanceof Error) {
console.error(e);
}
return buildUnknownErrorResponse(res);
}
});

router.post('/password-reset/:token', async (req, res) => {
const parseRes = passwordResetSchema.safeParse(req.body);
if (!parseRes.success) {
const formatted = parseRes.error.format();
if (formatted.password) {
return buildClientErrorResponse(res, formatted.password._errors);
}
return buildUnknownErrorResponse(res);
}

const { password } = parseRes.data;
try {
const { token } = req.params;
const userId = await passwordResetTM.validate(token);
const userFound = await auth.getUser(userId);

await auth.invalidateAllUserSessions(userFound.userId);
await auth.updateKeyPassword('email', userFound.email, password);
const session = await auth.createSession({
userId: userFound.userId,
attributes: {},
});
const authRequest = auth.handleRequest(req, res);
authRequest.setSession(session);
return res.json({
redirectTo: '/',
});
} catch (e) {
if (e instanceof Error) {
switch (e.message) {
case INVALID_TOKEN:
return buildClientErrorResponse(res, INVALID_TOKEN);
case EXPIRED_TOKEN:
return buildClientErrorResponse(res, EXPIRED_TOKEN);
default:
return buildUnknownErrorResponse(res);
}
}
console.error(e);
return buildUnknownErrorResponse(res);
}
});

export default router;
13 changes: 6 additions & 7 deletions src/routes/resendVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import {
buildUnknownErrorResponse,
} from '../lib/utils.ts';
import { emailVerificationTM } from '../models/tokenManger.ts';
import {
EXPIRED_EMAIL_VERIFICATION_TOKEN,
INVALID_EMAIL_VERIFICATION_TOKEN,
} from '../constant/error.ts';
import { EXPIRED_TOKEN, INVALID_TOKEN } from '../constant/error.ts';

const router = Router();

Expand Down Expand Up @@ -50,16 +47,18 @@ router.get('/email-verification/:token', async (req, res) => {
});
const authRequest = auth.handleRequest(req, res);
authRequest.setSession(session);
return res.sendStatus(302);
return res.json({
redirectTo: '/',
});
} catch (e) {
if (e instanceof Error) {
switch (e.message) {
case INVALID_EMAIL_VERIFICATION_TOKEN:
case INVALID_TOKEN:
return buildClientErrorResponse(
res,
'Invalid Email Verification Link'
);
case EXPIRED_EMAIL_VERIFICATION_TOKEN:
case EXPIRED_TOKEN:
return buildClientErrorResponse(
res,
'Invalid Email Verification Link'
Expand Down
18 changes: 18 additions & 0 deletions src/routes/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from 'express';
import signupRouter from './signup.ts';
import loginRouter from './login.ts';
import logoutRouter from './logout.ts';
import emailVerificationRouter from './resendVerification.ts';
import passwordResetRouter from './passwordReset.ts';

const router = Router();
const ROOT = '/';

router
.use(ROOT, signupRouter)
.use(ROOT, loginRouter)
.use(ROOT, logoutRouter)
.use(ROOT, emailVerificationRouter)
.use(ROOT, passwordResetRouter);

export default router;
8 changes: 5 additions & 3 deletions src/routes/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ router.post('/signup', async (req, res) => {
const token = await emailVerificationTM.generate(user.userId);
sendEmailVerificationLink(user.email, token);

const sessionCookie = auth.createSessionCookie(session);
res.setHeader('Set-Cookie', sessionCookie.serialize());
return res.sendStatus(302);
const authRequest = auth.handleRequest(req, res);
authRequest.setSession(session);
return res.json({
redirectTo: '/',
});
});

export default router;
15 changes: 0 additions & 15 deletions src/routes/user.ts

This file was deleted.

14 changes: 12 additions & 2 deletions src/validator/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ export const userInsertSchemaOverride = z.object({
});

export const userInsertSchema = baseUserInsertSchema.merge(
userInsertSchemaOverride,
userInsertSchemaOverride
);

export const userSelectSchema = userInsertSchema;
export const userLoginSchema = userInsertSchema.omit({
username: true,
});

export const requestPasswordResetSchema = userInsertSchema.pick({
email: true,
});

export const passwordResetSchema = userInsertSchema.pick({
password: true,
});

0 comments on commit 0f6238b

Please sign in to comment.