eletrotupi / tcc/ commit / 6031ca7

api: add activation code endpoints

Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago 6031ca741218094f5603e9c635a0becdb7aab39d Browse files
Parents a41d791
Changed files
10
Additions
+93
Deletions
-14
api/src/controllers/auth.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
  login,
4 4
  generateToken,
5 5
  requestPasswordReset,
6 6
  resetPassword,
7 +
  activateUser
7 8
} from '@app/services/auth.service';
8 9

9 10
import {
10 11
  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 = {
24 27
  login: async (req: Request, res: Response, next: NextFunction) => {
25 28
    try {
117 120
      });
118 121
    } 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
  }
125 151
};
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 3
  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
10 11
} from '@app/schemas';
11 12

13 14
  createUser,
14 15
  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
21 26
} from '@app/middleware/auth';
22 27

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 {
29 36
  file?: any;
30 37
}
41 48
      }
42 49

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
51 67
      });
52 68
    } catch (err) {
146 162
    return res.json({
147 163
      user
148 164
    })
149 165
  }
150 166
}
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,
15 15
  switch (job.name as MailJobName) {
16 16
    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
    }
24 24

25 25
    case MailJobName.PasswordReset: {
33 33

34 34
    default:
35 35
      throw new Error(`Unknown mail job: ${job.name}`);
36 36
  }
37 37
}
api/src/lib/queue/types.ts
1 1
// Mail queue
2 2
export enum MailJobName {
3 3
  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 =
11 11
  | { name: MailJobName.WelcomeEmail;  data: WelcomeEmailPayload }
12 12
  | { name: MailJobName.PasswordReset; data: PasswordResetPayload };
api/src/routes/auth.ts
1 1
import { Router } from 'express';
2 2
import { AuthController } from '@app/controllers/auth';
3 3

4 4
const router = Router();
5 5

6 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
1 1
import { z } from 'zod';
2 2

3 3
export const LoginSchema = z.object({
4 4
  email: z.string().email(),
5 5
  password: z.string().min(1)
10 10
});
11 11

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
1 1
export {
2 2
  CreateMoodSchema,
3 3
  MoodComponentSchema
4 4
} from '@app/schemas/mood.schema';
5 5

16 16

17 17
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 {
27 29
  type CreateSleepRecordInput,
28 30
  CreateSleepRecordSchema
29 31
} from '@app/schemas/sleepRecord.schema';
api/src/services/auth.service.ts
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 {
5 6
  generateToken as createJWT,
6 7
  generatePasswordResetToken,
13 14
} from '@app/services/user.service'; // Replace with a barrel import
14 15

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';
22 24

23 25
class InvalidCredentialsError extends Error {
99 101
};
100 102

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 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) {
26 26
    console.error("Error sending email: ", error);
27 27
  }
28 28

29 29
  console.log("Email sent successfully!");
30 30
  console.log("Email ID:", data?.id);
31 31
}
api/src/services/user.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { User } from '@prisma/client';
3 3
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> => {
11 11
  const { password, ...data } = input;
12 12
  const encryptedPassword = bcrypt.hashSync(password, 10);
47 47
      id: input.id
48 48
    },
49 49
    data: updateData
50 50
  })
51 51
}