api: add activation code endpoints

Pedro Lucas Porcellis porcellis@eletrotupi.com 21 days ago 6031ca741218094f5603e9c635a0becdb7aab39d
Parents: a41d791
10 file(s) changed
  • api/src/controllers/auth.ts +28 -2
  • api/src/controllers/users.ts +17 -1
  • api/src/lib/queue/processors/mail.ts +1 -1
  • api/src/lib/queue/types.ts +1 -1
  • api/src/routes/auth.ts +2 -0
  • api/src/schemas/auth.schema.ts +7 -1
  • api/src/schemas/index.ts +3 -1
  • api/src/services/auth.service.ts +28 -1
  • api/src/services/mail/welcome.ts +5 -5
  • api/src/services/user.service.ts +1 -1
api/src/controllers/auth.ts
@@ -4,6 +4,7 @@ login,
4 4 generateToken,
5 5 requestPasswordReset,
6 6 resetPassword,
7 + activateUser
7 8 } from '@app/services/auth.service';
8 9
9 10 import {
@@ -11,13 +12,15 @@ verifyToken
11 12 } from '@app/lib/jwt';
12 13
13 14 import {
14 - findUserByEmail
15 + findUserByEmail,
16 + findUserById,
15 17 } from '@app/services/user.service'
16 18
17 19 import {
18 20 LoginSchema,
19 21 PasswordResetSchema,
20 - PasswordResetRequestSchema
22 + PasswordResetRequestSchema,
23 + ActivateUserSchema
21 24 } from '@app/schemas';
22 25
23 26 export const AuthController = {
@@ -119,6 +122,29 @@ } catch (err: any) {
119 122 if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
120 123 return res.status(401).json({ error: 'Invalid or expired token' });
121 124 }
125 + next(err);
126 + }
127 + },
128 +
129 + activate: async (req: Request, res: Response, next: NextFunction) => {
130 + try {
131 + const parsed = ActivateUserSchema.safeParse(req.body);
132 +
133 + if (!parsed.success) {
134 + return res.status(400).json({
135 + errors: parsed.error!.issues
136 + });
137 + }
138 +
139 + const user = await findUserById(parsed.data.userId);
140 +
141 + if (!user) {
142 + return res.status(403).json({ error: "Usuario não encontrado" })
143 + }
144 +
145 + const result = await activateUser(user, parsed.data);
146 + return res.status(200).json(result);
147 + } catch (err: any) {
122 148 next(err);
123 149 }
124 150 }
api/src/controllers/users.ts
@@ -4,6 +4,7 @@ generateToken
4 4 } from '@app/services/auth.service';
5 5
6 6 import { getQueue, MailJobName } from '@app/lib/queue';
7 +
7 8 import {
8 9 CreateUserSchema,
9 10 UpdateUserSchema
@@ -15,6 +16,10 @@ findUserById,
15 16 findUserByEmail,
16 17 updateUser
17 18 } from '@app/services/user.service';
19 +
20 + import {
21 + storeActivationCode
22 + } from '@app/services/auth.service';
18 23
19 24 import {
20 25 AuthenticatedRequest
@@ -23,6 +28,8 @@
23 28 import { prisma } from '@app/lib/prisma';
24 29 import s3 from '@app/lib/s3';
25 30 import { DeleteObjectCommand } from '@aws-sdk/client-s3';
31 + import crypto from 'crypto';
32 + import { addMinutes } from 'date-fns';
26 33
27 34 // XXX: I've regreted already
28 35 interface SingleFileRequest extends AuthenticatedRequest {
@@ -43,8 +50,17 @@
43 50 const user = await createUser(parsed.data);
44 51 const jwtToken = generateToken(user.id, user.email);
45 52
53 + const code = [...Array(6)].map(() => crypto.randomInt(100))
54 + .join(" ");
55 +
56 + const expiresAt = addMinutes(new Date(), 5);
57 + await storeActivationCode(user.id, code, expiresAt);
58 +
46 59 const mailQueue = getQueue('mail');
47 - const job = await mailQueue.add(MailJobName.WelcomeEmail, { userId: user.id });
60 + const job = await mailQueue.add(MailJobName.WelcomeEmail, {
61 + userId: user.id,
62 + code: code
63 + });
48 64
49 65 res.status(201).json({
50 66 token: jwtToken
api/src/lib/queue/processors/mail.ts
@@ -17,7 +17,7 @@ case MailJobName.WelcomeEmail: {
17 17 const data = job.data as WelcomeEmailPayload;
18 18 console.log("Dispatching an welcome email", data);
19 19
20 - sendWelcomeEmail(Number(data.userId));
20 + sendWelcomeEmail(Number(data.userId), data.code);
21 21
22 22 break;
23 23 }
api/src/lib/queue/types.ts
@@ -4,7 +4,7 @@ WelcomeEmail = 'mail:welcome',
4 4 PasswordReset = 'mail:password-reset',
5 5 }
6 6
7 - export type WelcomeEmailPayload = { userId: number };
7 + export type WelcomeEmailPayload = { userId: number; code: string };
8 8 export type PasswordResetPayload = { userId: number; token: string };
9 9
10 10 export type MailJobData =
api/src/routes/auth.ts
@@ -7,5 +7,6 @@ router.post('/login', AuthController.login);
7 7 router.post('/forgot-password', AuthController.forgotPassword);
8 8 router.post('/reset-password', AuthController.resetPassword);
9 9 router.get('/verify', AuthController.verify);
10 + router.post('/activate', AuthController.activate);
10 11
11 - export default router;
12 + export default router;
api/src/schemas/auth.schema.ts
@@ -12,8 +12,14 @@
12 12 export const PasswordResetSchema = z.object({
13 13 token: z.string(),
14 14 newPassword: z.string().min(6)
15 - })
15 + });
16 +
17 + export const ActivateUserSchema = z.object({
18 + userId: z.number(),
19 + code: z.number().max(6)
20 + });
16 21
17 22 export type LoginInput = z.infer<typeof LoginSchema>;
18 23 export type PasswordResetRequestInput = z.infer<typeof PasswordResetRequestSchema>;
19 24 export type PasswordResetInput = z.infer<typeof PasswordResetSchema>;
25 + export type ActivateUserInput = z.infer<typeof ActivateUserSchema>;
api/src/schemas/index.ts
@@ -18,9 +18,11 @@ export {
18 18 LoginSchema,
19 19 PasswordResetRequestSchema,
20 20 PasswordResetSchema,
21 + ActivateUserSchema,
21 22 type LoginInput,
22 23 type PasswordResetRequestInput,
23 - type PasswordResetInput
24 + type PasswordResetInput,
25 + type ActivateUserInput
24 26 } from '@app/schemas/auth.schema';
25 27
26 28 export {
api/src/services/auth.service.ts
@@ -1,4 +1,5 @@
1 1 import { prisma } from '@app/lib/prisma';
2 + import { User } from '@prisma/client';
2 3 import bcrypt from "bcryptjs";
3 4
4 5 import {
@@ -15,7 +16,8 @@
15 16 import {
16 17 LoginInput,
17 18 PasswordResetRequestInput,
18 - PasswordResetInput
19 + PasswordResetInput,
20 + ActivateUserInput
19 21 } from '@app/schemas'
20 22
21 23 import { getQueue, MailJobName } from '@app/lib/queue';
@@ -101,3 +103,28 @@
101 103 export const generateToken = (userId: number, email: string) => {
102 104 return createJWT(userId, email);
103 105 };
106 +
107 + export const activateUser = async(user: User, input: ActivateUserInput): Promise<User> => {
108 + return await prisma.user.update({
109 + where: {
110 + id: user.id
111 + },
112 + data: {
113 + isActive: true,
114 + activationCode: null,
115 + activationCodeExpiresAt: null
116 + }
117 + });
118 + }
119 +
120 + export const storeActivationCode = async(userId: number, code: string, expiresAt: Date): Promise<User> => {
121 + return await prisma.user.update({
122 + where: {
123 + id: userId
124 + },
125 + data: {
126 + activationCode: code,
127 + activationCodeExpiresAt: expiresAt
128 + }
129 + })
130 + }
api/src/services/mail/welcome.ts
@@ -1,25 +1,25 @@
1 1 import { findUserById } from '@app/services/user.service';
2 2 import { sendEmail } from '@app/lib/mail';
3 3
4 - const email = (firstName: string) => {
4 + const email = (firstName: string, code: string) => {
5 5 return (`
6 6 Oi! Seja bem vindo, ${firstName}.
7 7
8 - Você criou sua conta com sucesso no orbit!
8 + Você criou sua conta com sucesso no orbit! Aqui seu código de ativação: ${code}.
9 9
10 10 Qualquer coisa prende o grito!
11 11 `)
12 12 }
13 13
14 - export async function sendWelcomeEmail(userId: number): Promise<void> {
14 + export async function sendWelcomeEmail(userId: number, code: string): Promise<void> {
15 15 const user = await findUserById(userId)!;
16 16
17 - console.log("Email/welcome: ", user);
17 + console.log("Email/welcome: ", user, code);
18 18
19 19 const { data, error } = await sendEmail({
20 20 to: user!.email,
21 21 subject: "Bem vindo ao orbit!",
22 - text: email(user!.firstName)
22 + text: email(user!.firstName, code)
23 23 });
24 24
25 25 if (error) {
api/src/services/user.service.ts
@@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs';
4 4
5 5 import {
6 6 CreateUserInput,
7 - UpdateUserInput,
7 + UpdateUserInput
8 8 } from "@app/schemas";
9 9
10 10 export const createUser = async(input: CreateUserInput): Promise<User> => {