eletrotupi / tcc/ commit / c50ef5d

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 · 5 months ago c50ef5d75b90563a411d2101772aeb3b363609ef Browse files
Parents 766a1ea
Changed files
4
Additions
+165
Deletions
-59
api/src/controllers/auth.ts
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') {
47 42
        return res.status(401).json({ error: 'Invalid credentials' });
48 43
      }
50 45
    }
51 46
  },
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
    }
82 64
  },
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 });
105 81
      }
106 82
      next(err);
136 112
      }
137 113
      next(err);
138 114
    }
139 115
  }
140 116
};
/dev/null → api/src/schemas/auth.schema.ts
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
1 1
export {
2 2
  CreateMoodSchema,
3 3
  MoodComponentSchema
4 4
} from '@app/schemas/mood.schema';
5 5

6 6
export type {
7 7
  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';
/dev/null → api/src/services/auth.service.ts
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 +
};