Implement confirm user functionality

This commit adds the necessary functionality to confirm a user's account.

It includes the addition of a new column in the user table to track whether an account is confirmed or not, and the implementation of JWT for confirmation tokens.

This commit integrates the SparkPost API as well as React Email to send dynamic emails for whatever purpose.

Upon user registration, a confirmation email will be sent to the user.
This commit is contained in:
Aaron William Po
2023-03-05 14:07:19 -05:00
parent f576f515a1
commit 584e3b349f
21 changed files with 5352 additions and 145 deletions

View File

@@ -9,24 +9,24 @@ import { z } from 'zod';
import { MAX_AGE, setTokenCookie, getTokenCookie } from './cookie';
import ServerError from '../util/ServerError';
const { TOKEN_SECRET } = process.env;
const { SESSION_SECRET } = process.env;
export async function setLoginSession(
res: NextApiResponse,
session: z.infer<typeof BasicUserInfoSchema>,
) {
if (!TOKEN_SECRET) {
if (!SESSION_SECRET) {
throw new ServerError('Authentication is not configured.', 500);
}
const createdAt = Date.now();
const obj = { ...session, createdAt, maxAge: MAX_AGE };
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
const token = await Iron.seal(obj, SESSION_SECRET, Iron.defaults);
setTokenCookie(res, token);
}
export async function getLoginSession(req: SessionRequest) {
if (!TOKEN_SECRET) {
if (!SESSION_SECRET) {
throw new ServerError('Authentication is not configured.', 500);
}
@@ -35,7 +35,7 @@ export async function getLoginSession(req: SessionRequest) {
throw new ServerError('You are not logged in.', 401);
}
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
const session = await Iron.unseal(token, SESSION_SECRET, Iron.defaults);
const parsed = UserSessionSchema.safeParse(session);

28
config/jwt/index.ts Normal file
View File

@@ -0,0 +1,28 @@
import { BasicUserInfoSchema } from '@/config/auth/types';
import jwt from 'jsonwebtoken';
import { z } from 'zod';
const { CONFIRMATION_TOKEN_SECRET } = process.env;
if (!CONFIRMATION_TOKEN_SECRET) {
throw new Error('CONFIRMATION_TOKEN_SECRET is not defined');
}
type User = z.infer<typeof BasicUserInfoSchema>;
export const generateConfirmationToken = (user: User) => {
const token = jwt.sign(user, CONFIRMATION_TOKEN_SECRET, { expiresIn: '30m' });
return token;
};
export const verifyConfirmationToken = (token: string) => {
const decoded = jwt.verify(token, CONFIRMATION_TOKEN_SECRET);
const parsed = BasicUserInfoSchema.safeParse(decoded);
if (!parsed.success) {
throw new Error('Invalid token');
}
return parsed.data;
};

View File

@@ -15,7 +15,7 @@ const getCurrentUser = async (
const user = await findUserById(session?.id);
if (!user) {
throw new ServerError('Could not get user.', 401);
throw new ServerError('User is not logged in.', 401);
}
req.user = user;

View File

@@ -0,0 +1,11 @@
import SparkPost from 'sparkpost';
const { SPARKPOST_API_KEY } = process.env;
if (!SPARKPOST_API_KEY) {
throw new Error('SPARKPOST_API_KEY is not defined');
}
const client = new SparkPost(SPARKPOST_API_KEY);
export default client;

View File

@@ -0,0 +1,25 @@
import client from './client';
interface EmailParams {
address: string;
text: string;
html: string;
subject: string;
}
const { SPARKPOST_SENDER_ADDRESS } = process.env;
if (!SPARKPOST_SENDER_ADDRESS) {
throw new Error('SPARKPOST_SENDER_ADDRESS env variable is not set.');
}
const sendEmail = async ({ address, text, html, subject }: EmailParams) => {
const from = SPARKPOST_SENDER_ADDRESS;
await client.transmissions.send({
content: { from, html, subject, text },
recipients: [{ address }],
});
};
export default sendEmail;