api: migrate to service + schemas and ditch user model
finally, it was such a fucking mess
Parents:
df2ccb04 file(s) changed
- api/src/controllers/users.ts +25 -36
- api/src/models/user.ts +0 -146
- api/src/schemas/user.schema.ts +6 -8
- api/src/services/user.service.ts +34 -1
api/src/controllers/users.ts
@@ -1,22 +1,19 @@
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 = {
@@ -46,37 +43,29 @@ },
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 }
@@ -1,146 +0,0 @@
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
@@ -7,13 +7,11 @@ 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
@@ -3,7 +3,8 @@ 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> => {
@@ -14,5 +15,37 @@ 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 }