eletrotupi / tcc/ commit / 10dda3b

api: split activation code from welcome email

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 10dda3b1236e6affa6cb2722043f04c66876a354 Browse files
Parents e6b6493
Changed files
6
Additions
+108
Deletions
-10
api/src/controllers/auth.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 +
import { AuthenticatedRequest } from '@app/middleware/auth';
3 +

2 4
import {
3 5
  login,
4 6
  generateToken,
5 7
  requestPasswordReset,
6 8
  resetPassword,
7 -
  activateUser
9 +
  activateUser,
10 +
  storeActivationCode
8 11
} from '@app/services/auth.service';
9 12

10 13
import {
11 14
  verifyToken
12 15
} from '@app/lib/jwt';
13 16

14 17
import {
15 18
  findUserByEmail,
16 19
  findUserById,
17 -
  findUserByActivationCode
20 +
  findUserByActivationCode,
18 21
} from '@app/services/user.service'
19 22

20 23
import {
21 24
  LoginSchema,
22 25
  PasswordResetSchema,
23 26
  PasswordResetRequestSchema,
24 27
  ActivateUserSchema
25 28
} from '@app/schemas';
26 29

30 +
import { addMinutes } from 'date-fns';
31 +
import { getQueue, MailJobName } from '@app/lib/queue';
32 +
import crypto from 'crypto';
33 +

27 34
export const AuthController = {
28 35
  login: async (req: Request, res: Response, next: NextFunction) => {
29 36
    try {
30 37
      const parsed = LoginSchema.safeParse(req.body);
31 38

35 42
        });
36 43
      }
37 44

38 45
      const { user, token } = await login(parsed.data);
39 46

40 -
      if (!user.active) {
41 -
        // TODO: generate a new activation code and send an email
42 -
        // if the code has expired
47 +
      if (!user.active && (new Date() > new Date(user.activationCodeExpiresAt!))) {
48 +
        const code = [...Array(6)].map(() => crypto.randomInt(9))
49 +
        .join("");
50 +

51 +
        const expiresAt = addMinutes(new Date(), 5);
52 +
        await storeActivationCode(user.id, code, expiresAt);
53 +

54 +
        const mailQueue = getQueue('mail');
55 +
        const job = await mailQueue.add(MailJobName.ActivateAccountEmail, {
56 +
          userId: user.id,
57 +
          code: code
58 +
        });
43 59
      }
44 60

45 61
      return res.json({
46 62
        token,
47 63
        user
143 159
      }
144 160

145 161
      const user = await findUserByActivationCode(String(parsed.data.code));
146 162

147 163
      if (!user) {
148 -
        return res.status(403).json({ error: "Usuario não encontrado" })
164 +
        return res.status(422).json({
165 +
          error: "Código incorreto. Verifique o código e tente novamente"
166 +
        })
149 167
      }
150 168

151 169
      const result = await activateUser(user);
152 170

153 171
      return res.status(200).json({
154 172
        user: result
155 173
      });
174 +
    } catch (err: any) {
175 +
      next(err);
176 +
    }
177 +
  },
178 +

179 +
  resendActivationCode: async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
180 +
    try {
181 +
      const user = await findUserById(req.userId!);
182 +

183 +
      if (!user) {
184 +
        return res.status(400).json({
185 +
          errors: "Usuário não encontrado"
186 +
        })
187 +
      }
188 +

189 +
      const code = [...Array(6)].map(() => crypto.randomInt(9))
190 +
      .join("");
191 +

192 +
      const expiresAt = addMinutes(new Date(), 5);
193 +
      await storeActivationCode(user.id, code, expiresAt);
194 +

195 +
      //const mailQueue = getQueue('mail');
196 +
      //const job = await mailQueue.add(MailJobName.ActivateAccountEmail, {
197 +
      //  userId: user.id,
198 +
      //  code: code
199 +
      //});
200 +

201 +
      res.json({
202 +
        user
203 +
      })
156 204
    } catch (err: any) {
157 205
      next(err);
158 206
    }
159 207
  }
160 208
};
api/src/lib/queue/processors/mail.ts
1 1
import { Job } from 'bullmq';
2 2
import {
3 3
  MailJobName, MailJobData,
4 4
  WelcomeEmailPayload,
5 5
  PasswordResetPayload,
6 +
  ActivateAccountEmailPayload
6 7
} from '@app/lib/queue/types';
7 8

8 9
import {
9 10
  sendWelcomeEmail,
10 -
  sendResetPasswordEmail
11 +
  sendResetPasswordEmail,
12 +
  sendActivateAccountEmail
11 13
} from '@app/services/mail'
12 14

13 15
export async function mailProcessor(job: Job<MailJobData['data']>): Promise<void> {
14 16
  // job.name is the discriminant
15 17
  switch (job.name as MailJobName) {
16 18
    case MailJobName.WelcomeEmail: {
17 19
      const data = job.data as WelcomeEmailPayload;
18 20
      console.log("Dispatching an welcome email", data);
19 21

20 22
      sendWelcomeEmail(Number(data.userId), data.code);
23 +

24 +
      break;
25 +
    }
26 +

27 +
    case MailJobName.ActivateAccountEmail: {
28 +
      const data = job.data as ActivateAccountEmailPayload;
29 +
      console.log("Dispatching an activate account email", data);
30 +

31 +
      sendActivateAccountEmail(Number(data.userId), data.code);
21 32

22 33
      break;
23 34
    }
24 35

25 36
    case MailJobName.PasswordReset: {
33 44

34 45
    default:
35 46
      throw new Error(`Unknown mail job: ${job.name}`);
36 47
  }
37 48
}
api/src/lib/queue/types.ts
1 1
// Mail queue
2 2
export enum MailJobName {
3 -
  WelcomeEmail    = 'mail:welcome',
4 -
  PasswordReset   = 'mail:password-reset',
3 +
  WelcomeEmail         = 'mail:welcome',
4 +
  PasswordReset        = 'mail:password-reset',
5 +
  ActivateAccountEmail = 'mail:activate-account'
5 6
}
6 7

7 8
export type WelcomeEmailPayload    = { userId: number; code: string };
8 9
export type PasswordResetPayload   = { userId: number; token: string };
10 +
export type ActivateAccountEmailPayload = { userId: number; code: string };
9 11

10 12
export type MailJobData =
11 13
  | { name: MailJobName.WelcomeEmail;  data: WelcomeEmailPayload }
12 -
  | { name: MailJobName.PasswordReset; data: PasswordResetPayload };
14 +
  | { name: MailJobName.PasswordReset; data: PasswordResetPayload }
15 +
  | { name: MailJobName.ActivateAccountEmail; data: ActivateAccountEmailPayload };
api/src/routes/auth.ts
1 1
import { Router } from 'express';
2 2
import { AuthController } from '@app/controllers/auth';
3 +
import { requireAuth } from '@app/middleware/auth';
3 4

4 5
const router = Router();
5 6

6 7
router.post('/login', AuthController.login);
7 8
router.post('/forgot-password', AuthController.forgotPassword);
8 9
router.post('/reset-password', AuthController.resetPassword);
9 10
router.get('/verify', AuthController.verify);
10 11
router.post('/activate', AuthController.activate);
12 +
router.post('/resend_code', requireAuth, AuthController.resendActivationCode);
11 13

12 14
export default router;
api/src/services/mail/index.ts
1 1
export { sendWelcomeEmail } from '@app/services/mail/welcome';
2 2
export { sendResetPasswordEmail } from '@app/services/mail/sendPasswordReset'
3 +
export { sendActivateAccountEmail }  from '@app/services/mail/sendActivateAccountEmail';
/dev/null → api/src/services/mail/sendActivateAccountEmail.ts
1 +
import { findUserById } from '@app/services/user.service';
2 +
import { sendEmail }  from '@app/lib/mail';
3 +

4 +
const email = (firstName: string, code: string) => {
5 +
  return (`
6 +
Oi, ${firstName}.
7 +

8 +
Você criou sua conta no nexo. Porém, precisa ativar sua conta.
9 +

10 +
Aqui seu código de ativação: ${code}.
11 +

12 +
Qualquer coisa prende o grito!
13 +
  `)
14 +
}
15 +

16 +
export async function sendActivateAccountEmail(userId: number, code: string): Promise<void> {
17 +
  const user = await findUserById(userId)!;
18 +

19 +
  console.log("Email/activateAccount: ", user, code);
20 +

21 +
  const { data, error } = await sendEmail({
22 +
    to: user!.email,
23 +
    subject: "Ative sua conta no nexo!",
24 +
    text: email(user!.firstName, code)
25 +
  });
26 +

27 +
  if (error) {
28 +
    console.error("Error sending email: ", error);
29 +
  }
30 +

31 +
  console.log("Email sent successfully!");
32 +
  console.log("Email ID:", data?.id);
33 +
}