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 filesParents
766a1eaChanged 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 +
};