api: add activation code endpoints
Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago
6031ca741218094f5603e9c635a0becdb7aab39d Browse filesParents
a41d791Changed 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
}