eletrotupi / tcc/ commit / 23c0d37

api: migrate to service + schemas and ditch user model

finally, it was such a fucking mess
Pedro Lucas Porcellis porcellis@eletrotupi.com · 5 months ago 23c0d375c6007bb31f664fa3b208e18023c04ba7 Browse files
Parents df2ccb0
Changed files
4
Additions
+65
Deletions
-191
api/src/controllers/users.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import {
3 -
  findUserById,
4 -
  findUserByEmail,
5 -
  updateUser,
6 3
  generateToken
7 -
} from '@app/models/user';
8 -

9 -
import {
10 -
  validateRequiredFields
11 -
} from '@app/utils/validators'
4 +
} from '@app/services/auth.service';
12 5

13 6
import { getQueue, MailJobName } from '@app/lib/queue';
14 7
import {
15 -
  CreateUserSchema
8 +
  CreateUserSchema,
9 +
  UpdateUserSchema
16 10
} from '@app/schemas';
17 11

18 12
import {
19 -
  createUser
13 +
  createUser,
14 +
  findUserById,
15 +
  findUserByEmail,
16 +
  updateUser
20 17
} from '@app/services/user.service';
21 18

22 19
export const UsersController = {
23 20
  create: async (req: Request, res: Response, next: NextFunction) => {
24 21
    try {
44 41
    }
45 42
  },
46 43

47 44
  update: async (req: Request, res: Response, next: NextFunction) => {
48 45
    try {
49 -
      const { id } = req.params
50 -
      const { user: userParams } = req.body;
46 +
      const { id } = req.params;
51 47

52 -
      if (userParams) {
53 -
        const { email, password, firstName, lastName } = userParams;
48 +
      const parsed = UpdateUserSchema.safeParse(req.body);
49 +
      if (!parsed.success) {
50 +
        return res.status(400).json({
51 +
          errors: parsed.error!.issues
52 +
        });
53 +
      }
54 54

55 -
        const user = await findUserById(Number(id));
55 +
      const user = await findUserById(Number(id));
56 56

57 -
        if (!user) {
58 -
          return res.status(404).json({
59 -
            error: "Usuário não encontrado"
60 -
          })
61 -
        }
57 +
      if (!user) {
58 +
        return res.status(404).json({
59 +
          error: "Usuário não encontrado"
60 +
        });
61 +
      }
62 62

63 -
        if (email || password || firstName || lastName) {
64 -
          const response = await updateUser(user, {
65 -
            email, firstName, lastName, password
66 -
          })
67 -

68 -
          const updated = await findUserById(Number(id))
63 +
      const updated = await updateUser(parsed.data);
69 64

70 -
          // TODO: Drop encrypted password & token here
71 -
          return res.status(200).json({
72 -
            user: updated
73 -
          })
74 -
        } else {
75 -
          return res.status(200).json({
76 -
            user
77 -
          })
78 -
        }
79 -
      }
65 +
      // TODO: Drop encrypted password & token here
66 +
      return res.status(200).json({
67 +
        user: updated
68 +
      });
80 69
    } catch (err) {
81 70
      next(err);
82 71
    }
83 72
  }
84 73
};
api/src/models/user.ts → /dev/null
1 -
import bcrypt from "bcryptjs";
2 -
import { prisma } from '../lib/prisma'
3 -
import {
4 -
  generateToken as createJWT,
5 -
  generatePasswordResetToken,
6 -
  getPasswordResetExpiration,
7 -
  isTokenExpired
8 -
} from '@app/lib/jwt';
9 -

10 -
import {
11 -
  isValidEmail,
12 -
  isValidPassword,
13 -
  sanitizeString,
14 -
  validateRequiredFields
15 -
} from "@app/utils/validators";
16 -

17 -
import { InvalidEmailError, ShortPasswordError } from '@app/lib/errors/UserErrors'
18 -

19 -
type UserOptions = {
20 -
  email: string;
21 -
  firstName: string;
22 -
  lastName: string;
23 -
  password?: string;
24 -
  encryptedPassword?: string;
25 -
};
26 -

27 -
const generateToken = (userId: number, email: string) => {
28 -
  return createJWT(userId, email);
29 -
}
30 -

31 -
const findUserById = async (id: number) => {
32 -
  return await prisma.user.findUnique({
33 -
    where: { id }
34 -
  });
35 -
};
36 -

37 -
const findUserByEmail = async (email: string) => {
38 -
  return await prisma.user.findUnique({
39 -
    where: { email }
40 -
  });
41 -
};
42 -

43 -
const authenticateUser = async (email: string, password: string) => {
44 -
  const user = await findUserByEmail(email);
45 -

46 -
  if (!user) {
47 -
    throw new Error('Invalid credentials');
48 -
  }
49 -

50 -
  const isPasswordValid = bcrypt.compareSync(password, user.encryptedPassword);
51 -

52 -
  if (!isPasswordValid) {
53 -
    throw new Error('Invalid credentials');
54 -
  }
55 -

56 -
  return user;
57 -
};
58 -

59 -
const initiatePasswordReset = async (email: string) => {
60 -
  const user = await findUserByEmail(email);
61 -

62 -
  if (!user) {
63 -
    // Don't reveal whether email exists
64 -
    return { success: true };
65 -
  }
66 -

67 -
  const resetToken = generatePasswordResetToken();
68 -
  const resetExpires = getPasswordResetExpiration();
69 -

70 -
  await prisma.user.update({
71 -
    where: { id: user.id },
72 -
    data: {
73 -
      passwordResetToken: resetToken,
74 -
      passwordResetExpires: resetExpires
75 -
    }
76 -
  });
77 -

78 -
  // In production, send email with reset link
79 -
  // For now, return token for testing
80 -
  return {
81 -
    success: true,
82 -
    token: resetToken,
83 -
    email: user.email,
84 -
    id: user.id
85 -
  };
86 -
};
87 -

88 -
const resetPassword = async (token: string, newPassword: string) => {
89 -
  const user = await prisma.user.findFirst({
90 -
    where: {
91 -
      passwordResetToken: token,
92 -
      passwordResetExpires: { not: null }
93 -
    }
94 -
  });
95 -

96 -
  if (!user || !user.passwordResetExpires) {
97 -
    throw new Error('Invalid or expired reset token');
98 -
  }
99 -

100 -
  if (isTokenExpired(user.passwordResetExpires)) {
101 -
    throw new Error('Reset token has expired');
102 -
  }
103 -

104 -
  if (!isValidPassword(newPassword)) {
105 -
    throw new ShortPasswordError();
106 -
  }
107 -

108 -
  const encryptedPassword = bcrypt.hashSync(newPassword, 10);
109 -

110 -
  await prisma.user.update({
111 -
    where: { id: user.id },
112 -
    data: {
113 -
      encryptedPassword,
114 -
      passwordResetToken: null,
115 -
      passwordResetExpires: null
116 -
    }
117 -
  });
118 -

119 -
  return { success: true };
120 -
};
121 -

122 -
const updateUser = async (user: any, data: UserOptions) => {
123 -
  const { password } = data;
124 -

125 -
  if (password) {
126 -
    const encryptedPassword = bcrypt.hashSync(password, 10);
127 -

128 -
    delete data.password;
129 -
    data = { ...data, encryptedPassword }
130 -
  }
131 -

132 -
  return await prisma.user.update({
133 -
    where: { id: user.id },
134 -
    data
135 -
  });
136 -
}
137 -

138 -
export {
139 -
  findUserById,
140 -
  generateToken,
141 -
  findUserByEmail,
142 -
  authenticateUser,
143 -
  initiatePasswordReset,
144 -
  resetPassword,
145 -
  updateUser
146 -
}
api/src/schemas/user.schema.ts
1 1
import { z } from 'zod';
2 2

3 3
export const CreateUserSchema = z.object({
4 4
  firstName: z.string(),
5 5
  lastName: z.string().optional(),
6 6
  email: z.string().email(),
7 7
  password: z.string().min(6)
8 8
});
9 9

10 -
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
11 -

12 -
export const UpdateUserSchema = z.object({
13 -
  firstName: z.string(),
14 -
  lastName: z.string().optional(),
15 -
  email: z.string().email(),
16 -
  password: z.string().min(6)
17 -
});
10 +
export const UpdateUserSchema = CreateUserSchema
11 +
  .partial()
12 +
  .extend({
13 +
    id: z.number()
14 +
  });
18 15

16 +
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
19 17
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
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 -
  CreateUserInput
6 +
  CreateUserInput,
7 +
  UpdateUserInput,
7 8
} from "@app/schemas";
8 9

9 10
export const createUser = async(input: CreateUserInput): Promise<User> => {
10 11
  const { password, ...data } = input;
11 12
  const encryptedPassword = bcrypt.hashSync(password, 10);
12 13

13 14
  return await prisma.user.create({
14 15
    data: {
15 16
      ...data, encryptedPassword
16 17
    }
18 +
  })
19 +
};
20 +

21 +
export const findUserById = async(id: number): Promise<User | null> => {
22 +
  return await prisma.user.findUnique({
23 +
    where: {
24 +
      id
25 +
    }
26 +
  });
27 +
};
28 +

29 +
export const findUserByEmail = async(email: string): Promise<User | null> => {
30 +
  return await prisma.user.findUnique({
31 +
    where: {
32 +
      email
33 +
    }
34 +
  });
35 +
};
36 +

37 +
export const updateUser = async (input: UpdateUserInput): Promise<User> => {
38 +
  const { password, ...data } = input;
39 +

40 +
  const updateData = {
41 +
    ...data,
42 +
    ...(password && { encryptedPassword: bcrypt.hashSync(password, 10) })
43 +
  };
44 +

45 +
  return await prisma.user.update({
46 +
    where: {
47 +
      id: input.id
48 +
    },
49 +
    data: updateData
17 50
  })
18 51
}