api: move auth to service + schemas

It does look way better, now that I'm getting the hang of it
Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago c50ef5d75b90563a411d2101772aeb3b363609ef
Parents: 766a1ea
4 file(s) changed
  • api/src/controllers/auth.ts +31 -55
  • api/src/schemas/auth.schema.ts +19 -0
  • api/src/schemas/index.ts +12 -4
  • api/src/services/auth.service.ts +103 -0
api/src/controllers/auth.ts
@@ -1,46 +1,41 @@
1 1 import { Request, Response, NextFunction } from 'express';
2 2 import {
3 - authenticateUser,
3 + login,
4 4 generateToken,
5 - initiatePasswordReset,
5 + requestPasswordReset,
6 6 resetPassword,
7 - findUserByEmail
8 - } from '@app/models/user';
7 + } from '@app/services/auth.service';
9 8
10 9 import {
11 - validateRequiredFields
12 - } from '@app/utils/validators';
10 + verifyToken
11 + } from '@app/lib/jwt';
13 12
14 - import { verifyToken } from '@app/lib/jwt';
15 - import { getQueue, MailJobName } from '@app/lib/queue';
13 + import {
14 + findUserByEmail
15 + } from '@app/services/user.service'
16 +
17 + import {
18 + LoginSchema,
19 + PasswordResetSchema,
20 + PasswordResetRequestSchema
21 + } from '@app/schemas';
16 22
17 23 export const AuthController = {
18 24 login: async (req: Request, res: Response, next: NextFunction) => {
19 25 try {
20 - const { email, password } = req.body;
26 + const parsed = LoginSchema.safeParse(req.body);
21 27
22 - const requiredValidation = validateRequiredFields(req.body, [
23 - 'email', 'password'
24 - ]);
25 -
26 - if (!requiredValidation.valid) {
28 + if (!parsed.success) {
27 29 return res.status(400).json({
28 - error: 'Missing required fields',
29 - missing: requiredValidation.missing
30 + errors: parsed.error!.issues
30 31 });
31 32 }
32 33
33 - const user = await authenticateUser(email, password);
34 - const jwtToken = generateToken(user.id, user.email);
34 + const { user, token } = await login(parsed.data);
35 35
36 - res.json({
37 - token: jwtToken,
38 - user: {
39 - id: user.id,
40 - email: user.email,
41 - firstName: user.firstName,
42 - lastName: user.lastName
43 - }
36 + return res.json({
37 + token,
38 + user
44 39 });
45 40 } catch (err: any) {
46 41 if (err.message === 'Invalid credentials') {
@@ -52,30 +47,17 @@ },
52 47
53 48 forgotPassword: async (req: Request, res: Response, next: NextFunction) => {
54 49 try {
55 - const { email } = req.body;
56 50
57 - const requiredValidation = validateRequiredFields(req.body, ['email']);
51 + const parsed = PasswordResetRequestSchema.safeParse(req.body);
58 52
59 - if (!requiredValidation.valid) {
53 + if (!parsed.success) {
60 54 return res.status(400).json({
61 - error: 'Email is required',
62 - missing: requiredValidation.missing
55 + errors: parsed.error!.issues
63 56 });
64 57 }
65 58
66 - const result = await initiatePasswordReset(email);
67 - const mailQueue = getQueue('mail');
68 - const job = await mailQueue.add(MailJobName.PasswordReset, {
69 - userId: result.id,
70 - token: result.token
71 - });
72 -
73 - // In production, don't send token in response
74 - // Instead, send email with reset link
75 - res.json({
76 - message: 'Password reset instructions sent to your email',
77 - ...(!process.env.NODE_ENV || process.env.NODE_ENV === 'development' ? { token: result.token } : {})
78 - });
59 + const result = await requestPasswordReset(parsed.data);
60 + return res.status(200).json(result)
79 61 } catch (err) {
80 62 next(err);
81 63 }
@@ -83,22 +65,16 @@ },
83 65
84 66 resetPassword: async (req: Request, res: Response, next: NextFunction) => {
85 67 try {
86 - const { token, password } = req.body;
68 + const parsed = PasswordResetSchema.safeParse(req.body);
87 69
88 - const requiredValidation = validateRequiredFields(req.body, [
89 - 'token', 'password'
90 - ]);
91 -
92 - if (!requiredValidation.valid) {
70 + if (!parsed.success) {
93 71 return res.status(400).json({
94 - error: 'Missing required fields',
95 - missing: requiredValidation.missing
72 + errors: parsed.error!.issues
96 73 });
97 74 }
98 75
99 - await resetPassword(token, password);
100 -
101 - res.json({ message: 'Password successfully reset' });
76 + const result = await resetPassword(parsed.data);
77 + return res.status(200).json(result);
102 78 } catch (err: any) {
103 79 if (err.message.includes('token')) {
104 80 return res.status(400).json({ error: err.message });
api/src/schemas/auth.schema.ts
@@ -0,0 +1,19 @@
1 + import { z } from 'zod';
2 +
3 + export const LoginSchema = z.object({
4 + email: z.string().email(),
5 + password: z.string().min(1)
6 + });
7 +
8 + export const PasswordResetRequestSchema = z.object({
9 + email: z.string().email()
10 + });
11 +
12 + export const PasswordResetSchema = z.object({
13 + token: z.string(),
14 + newPassword: z.string().min(8)
15 + })
16 +
17 + export type LoginInput = z.infer<typeof LoginSchema>;
18 + export type PasswordResetRequestInput = z.infer<typeof PasswordResetRequestSchema>;
19 + export type PasswordResetInput = z.infer<typeof PasswordResetSchema>;
api/src/schemas/index.ts
@@ -8,9 +8,17 @@ CreateMoodInput
8 8 } from '@app/schemas/mood.schema';
9 9
10 10 export {
11 - CreateUserSchema
11 + CreateUserSchema,
12 + UpdateUserSchema,
13 + type CreateUserInput,
14 + type UpdateUserInput,
12 15 } from '@app/schemas/user.schema';
13 16
14 - export type {
15 - CreateUserInput
16 - } from '@app/schemas/user.schema';
17 + export {
18 + LoginSchema,
19 + PasswordResetRequestSchema,
20 + PasswordResetSchema,
21 + type LoginInput,
22 + type PasswordResetRequestInput,
23 + type PasswordResetInput
24 + } from '@app/schemas/auth.schema';
api/src/services/auth.service.ts
@@ -0,0 +1,103 @@
1 + import { prisma } from '@app/lib/prisma';
2 + import bcrypt from "bcryptjs";
3 +
4 + import {
5 + generateToken as createJWT,
6 + generatePasswordResetToken,
7 + getPasswordResetExpiration,
8 + isTokenExpired
9 + } from '@app/lib/jwt';
10 +
11 + import {
12 + findUserByEmail
13 + } from '@app/services/user.service'; // Replace with a barrel import
14 +
15 + import {
16 + LoginInput,
17 + PasswordResetRequestInput,
18 + PasswordResetInput
19 + } from '@app/schemas'
20 +
21 + import { getQueue, MailJobName } from '@app/lib/queue';
22 +
23 + class InvalidCredentialsError extends Error {
24 + constructor() { super('Invalid credentials') }
25 + }
26 +
27 + class InvalidOrExpiredTokenError extends Error {
28 + constructor() { super('Invalid or Expired reset token') }
29 + }
30 +
31 + export const login = async({ email, password }: LoginInput) => {
32 + const user = await findUserByEmail(email);
33 +
34 + if (!user || !bcrypt.compareSync(password, user.encryptedPassword)) {
35 + throw new InvalidCredentialsError();
36 + }
37 +
38 + const token = generateToken(user.id, user.email);
39 +
40 + return { user, token };
41 + };
42 +
43 + export const requestPasswordReset = async ({ email }: PasswordResetRequestInput) => {
44 + const user = await findUserByEmail(email);
45 +
46 + if (!user) {
47 + return { success: true };
48 + }
49 +
50 + const resetToken = generatePasswordResetToken();
51 + const resetExpires = getPasswordResetExpiration();
52 +
53 + await prisma.user.update({
54 + where: {
55 + id: user.id
56 + },
57 + data: {
58 + passwordResetToken: resetToken,
59 + passwordResetExpires: resetExpires
60 + }
61 + });
62 +
63 + const mailQueue = getQueue('mail');
64 + const job = await mailQueue.add(MailJobName.PasswordReset, {
65 + userId: user.id,
66 + token: resetToken
67 + });
68 +
69 + return {
70 + success: true,
71 + token: resetToken
72 + };
73 + };
74 +
75 + export const resetPassword = async ({ token, newPassword }: PasswordResetInput) => {
76 + const user = await prisma.user.findFirst({
77 + where: {
78 + passwordResetToken: token,
79 + passwordResetExpires: { not: null }
80 + }
81 + });
82 +
83 + if (!user || !user.passwordResetExpires || isTokenExpired(user.passwordResetExpires)) {
84 + throw new InvalidOrExpiredTokenError();
85 + }
86 +
87 + await prisma.user.update({
88 + where: { id: user.id },
89 + data: {
90 + encryptedPassword: bcrypt.hashSync(newPassword, 10),
91 + passwordResetToken: null,
92 + passwordResetExpires: null
93 + }
94 + });
95 +
96 + return {
97 + success: true
98 + };
99 + };
100 +
101 + export const generateToken = (userId: number, email: string) => {
102 + return createJWT(userId, email);
103 + };