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 filesParents
df2ccb0Changed 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
}