Skip to content

Commit 078887f

Browse files
committed
refactor(backend): replace Zod schemas with JSON schema for settings routes
1 parent 0c816f5 commit 078887f

File tree

31 files changed

+7633
-8976
lines changed

31 files changed

+7633
-8976
lines changed

services/backend/api-spec.json

Lines changed: 3433 additions & 4438 deletions
Large diffs are not rendered by default.

services/backend/api-spec.yaml

Lines changed: 2582 additions & 3407 deletions
Large diffs are not rendered by default.

services/backend/src/routes/auth/adminResetPassword.ts

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,72 @@
11
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2-
import { AdminResetPasswordSchema, type AdminResetPasswordInput } from './schemas';
2+
import { type AdminResetPasswordInput } from './schemas';
33
import { PasswordResetService } from '../../services/passwordResetService';
44
import { requireGlobalAdmin } from '../../middleware/roleMiddleware';
5-
import { z } from 'zod';
6-
import { createSchema } from 'zod-openapi';
75

8-
// Response schemas
9-
const adminResetPasswordSuccessResponseSchema = z.object({
10-
success: z.boolean().describe('Indicates if the request was processed successfully'),
11-
message: z.string().describe('Success message')
12-
});
6+
const SUCCESS_RESPONSE_SCHEMA = {
7+
type: 'object',
8+
properties: {
9+
success: { type: 'boolean' },
10+
message: { type: 'string' }
11+
},
12+
required: ['success', 'message']
13+
} as const;
1314

14-
const adminResetPasswordErrorResponseSchema = z.object({
15-
success: z.boolean().describe('Indicates if the operation was successful (false for errors)').default(false),
16-
error: z.string().describe('Error message describing what went wrong')
17-
});
15+
const ERROR_RESPONSE_SCHEMA = {
16+
type: 'object',
17+
properties: {
18+
success: { type: 'boolean', default: false },
19+
error: { type: 'string' }
20+
},
21+
required: ['success', 'error']
22+
} as const;
1823

1924
// Route schema for OpenAPI documentation
2025
const adminResetPasswordRouteSchema = {
2126
tags: ['Authentication', 'Admin'],
2227
summary: 'Admin-initiated password reset',
2328
description: 'Allows global administrators to initiate password reset for users with email authentication. The admin cannot reset their own password. Requires global_send_mail setting to be enabled. The user will receive an email with a reset link that works the same as self-initiated password resets.',
2429
security: [{ cookieAuth: [] }],
25-
body: createSchema(AdminResetPasswordSchema),
30+
body: {
31+
type: 'object',
32+
properties: {
33+
email: { type: 'string', format: 'email' }
34+
},
35+
required: ['email'],
36+
additionalProperties: false
37+
},
2638
response: {
27-
200: createSchema(adminResetPasswordSuccessResponseSchema.describe('Password reset email sent successfully')),
28-
400: createSchema(adminResetPasswordErrorResponseSchema.describe('Bad Request - Invalid email, user not found, or user not eligible')),
29-
401: createSchema(adminResetPasswordErrorResponseSchema.describe('Unauthorized - Authentication required')),
30-
403: createSchema(adminResetPasswordErrorResponseSchema.describe('Forbidden - Insufficient permissions or self-reset attempt')),
31-
503: createSchema(adminResetPasswordErrorResponseSchema.describe('Service Unavailable - Email functionality disabled')),
32-
500: createSchema(adminResetPasswordErrorResponseSchema.describe('Internal Server Error - Password reset failed'))
39+
200: {
40+
...SUCCESS_RESPONSE_SCHEMA,
41+
description: 'Password reset email sent successfully'
42+
},
43+
400: {
44+
...ERROR_RESPONSE_SCHEMA,
45+
description: 'Bad Request - Invalid email, user not found, or user not eligible'
46+
},
47+
401: {
48+
...ERROR_RESPONSE_SCHEMA,
49+
description: 'Unauthorized - Authentication required'
50+
},
51+
403: {
52+
...ERROR_RESPONSE_SCHEMA,
53+
description: 'Forbidden - Insufficient permissions or self-reset attempt'
54+
},
55+
503: {
56+
...ERROR_RESPONSE_SCHEMA,
57+
description: 'Service Unavailable - Email functionality disabled'
58+
},
59+
500: {
60+
...ERROR_RESPONSE_SCHEMA,
61+
description: 'Internal Server Error - Password reset failed'
62+
}
3363
}
3464
};
3565

36-
export default async function adminResetPasswordRoute(fastify: FastifyInstance) {
37-
fastify.post<{ Body: AdminResetPasswordInput }>(
66+
export default async function adminResetPasswordRoute(server: FastifyInstance) {
67+
server.post<{ Body: AdminResetPasswordInput }>(
3868
'/admin/reset-password',
39-
{
69+
{
4070
schema: adminResetPasswordRouteSchema,
4171
preValidation: requireGlobalAdmin()
4272
},
@@ -45,9 +75,9 @@ export default async function adminResetPasswordRoute(fastify: FastifyInstance)
4575
// Check if password reset is available (email sending enabled)
4676
const isResetAvailable = await PasswordResetService.isPasswordResetAvailable();
4777
if (!isResetAvailable) {
48-
return reply.status(503).send({
49-
success: false,
50-
error: 'Password reset is currently disabled. Email functionality is not enabled.'
78+
return reply.status(503).send({
79+
success: false,
80+
error: 'Password reset is currently disabled. Email functionality is not enabled.'
5181
});
5282
}
5383

@@ -62,58 +92,58 @@ export default async function adminResetPasswordRoute(fastify: FastifyInstance)
6292
});
6393
}
6494

65-
fastify.log.info(`Admin-initiated password reset requested by admin ${adminUserId} for email: ${email}`);
95+
server.log.info(`Admin-initiated password reset requested by admin ${adminUserId} for email: ${email}`);
6696

6797
// Queue admin-initiated reset email as background job
6898
try {
69-
const result = await PasswordResetService.prepareAdminResetEmail(email, adminUserId, fastify.log);
99+
const result = await PasswordResetService.prepareAdminResetEmail(email, adminUserId, server.log);
70100

71101
if (!result.success) {
72-
fastify.log.error(`Admin password reset preparation failed for ${email} by admin ${adminUserId}: ${result.error}`);
73-
102+
server.log.error(`Admin password reset preparation failed for ${email} by admin ${adminUserId}: ${result.error}`);
103+
74104
// Determine appropriate status code based on error
75105
if (result.error && result.error.includes('not found') || result.error && result.error.includes('not eligible')) {
76-
return reply.status(400).send({
77-
success: false,
78-
error: result.error
106+
return reply.status(400).send({
107+
success: false,
108+
error: result.error
79109
});
80110
}
81-
111+
82112
if (result.error && result.error.includes('cannot reset their own password')) {
83-
return reply.status(403).send({
84-
success: false,
85-
error: result.error
113+
return reply.status(403).send({
114+
success: false,
115+
error: result.error
86116
});
87117
}
88-
118+
89119
if (result.error && result.error.includes('disabled')) {
90-
return reply.status(503).send({
91-
success: false,
92-
error: result.error
120+
return reply.status(503).send({
121+
success: false,
122+
error: result.error
93123
});
94124
}
95-
96-
return reply.status(500).send({
97-
success: false,
125+
126+
return reply.status(500).send({
127+
success: false,
98128
error: result.error || 'An error occurred during password reset request.'
99129
});
100130
}
101131

102132
// Queue email as background job if token and emailData were prepared
103133
if (result.emailData) {
104134
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105-
const jobQueueService = (fastify as any).jobQueueService;
135+
const jobQueueService = (server as any).jobQueueService;
106136
if (jobQueueService) {
107137
await jobQueueService.createJob('send_email', result.emailData);
108-
fastify.log.info(`Admin password reset email queued for ${email} by admin ${adminUserId}`);
138+
server.log.info(`Admin password reset email queued for ${email} by admin ${adminUserId}`);
109139
} else {
110-
fastify.log.warn('Job queue service not available, admin password reset email not sent');
140+
server.log.warn('Job queue service not available, admin password reset email not sent');
111141
}
112142
}
113143
} catch (error) {
114-
fastify.log.error(error, `Error queueing admin password reset email for ${email}:`);
115-
return reply.status(500).send({
116-
success: false,
144+
server.log.error(error, `Error queueing admin password reset email for ${email}:`);
145+
return reply.status(500).send({
146+
success: false,
117147
error: 'An unexpected error occurred during password reset request.'
118148
});
119149
}
@@ -124,10 +154,10 @@ export default async function adminResetPasswordRoute(fastify: FastifyInstance)
124154
});
125155

126156
} catch (error) {
127-
fastify.log.error(error, 'Error during admin-initiated password reset request:');
128-
return reply.status(500).send({
129-
success: false,
130-
error: 'An unexpected error occurred during password reset request.'
157+
server.log.error(error, 'Error during admin-initiated password reset request:');
158+
return reply.status(500).send({
159+
success: false,
160+
error: 'An unexpected error occurred during password reset request.'
131161
});
132162
}
133163
}

services/backend/src/routes/auth/changePassword.ts

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
22
import { verify, hash } from '@node-rs/argon2';
33
import { getDb, getSchema } from '../../db';
44
import { eq } from 'drizzle-orm';
5-
import { ChangePasswordSchema, type ChangePasswordInput } from './schemas';
5+
import { type ChangePasswordInput } from './schemas';
66
import { requireAuthHook } from '../../hooks/authHook';
7-
import { z } from 'zod';
8-
import { createSchema } from 'zod-openapi';
97
import { EmailService } from '../../email';
108
import { GlobalSettingsService } from '../../services/globalSettingsService';
119

12-
// Response schemas
13-
const changePasswordSuccessResponseSchema = z.object({
14-
success: z.boolean().describe('Indicates if the password change was successful'),
15-
message: z.string().describe('Success message')
16-
});
10+
const SUCCESS_RESPONSE_SCHEMA = {
11+
type: 'object',
12+
properties: {
13+
success: { type: 'boolean' },
14+
message: { type: 'string' }
15+
},
16+
required: ['success', 'message']
17+
} as const;
1718

18-
const changePasswordErrorResponseSchema = z.object({
19-
success: z.boolean().describe('Indicates if the operation was successful (false for errors)').default(false),
20-
error: z.string().describe('Error message describing what went wrong')
21-
});
19+
const ERROR_RESPONSE_SCHEMA = {
20+
type: 'object',
21+
properties: {
22+
success: { type: 'boolean', default: false },
23+
error: { type: 'string' }
24+
},
25+
required: ['success', 'error']
26+
} as const;
2227

2328
// Route schema for OpenAPI documentation
2429
const changePasswordRouteSchema = {
@@ -34,28 +39,35 @@ const changePasswordRouteSchema = {
3439
required: ['current_password', 'new_password'],
3540
additionalProperties: false
3641
},
37-
requestBody: {
38-
required: true,
39-
content: {
40-
'application/json': {
41-
schema: createSchema(ChangePasswordSchema)
42-
}
43-
}
44-
},
4542
security: [{ cookieAuth: [] }],
4643
response: {
47-
200: createSchema(changePasswordSuccessResponseSchema.describe('Password changed successfully')),
48-
400: createSchema(changePasswordErrorResponseSchema.describe('Bad Request - Invalid input, incorrect current password, or missing Content-Type header')),
49-
401: createSchema(changePasswordErrorResponseSchema.describe('Unauthorized - Authentication required')),
50-
403: createSchema(changePasswordErrorResponseSchema.describe('Forbidden - Cannot change password for non-email users')),
51-
500: createSchema(changePasswordErrorResponseSchema.describe('Internal Server Error - Password change failed'))
44+
200: {
45+
...SUCCESS_RESPONSE_SCHEMA,
46+
description: 'Password changed successfully'
47+
},
48+
400: {
49+
...ERROR_RESPONSE_SCHEMA,
50+
description: 'Bad Request - Invalid input, incorrect current password, or missing Content-Type header'
51+
},
52+
401: {
53+
...ERROR_RESPONSE_SCHEMA,
54+
description: 'Unauthorized - Authentication required'
55+
},
56+
403: {
57+
...ERROR_RESPONSE_SCHEMA,
58+
description: 'Forbidden - Cannot change password for non-email users'
59+
},
60+
500: {
61+
...ERROR_RESPONSE_SCHEMA,
62+
description: 'Internal Server Error - Password change failed'
63+
}
5264
}
5365
};
5466

55-
export default async function changePasswordRoute(fastify: FastifyInstance) {
56-
fastify.put<{ Body: ChangePasswordInput }>(
67+
export default async function changePasswordRoute(server: FastifyInstance) {
68+
server.put<{ Body: ChangePasswordInput }>(
5769
'/change-password',
58-
{
70+
{
5971
schema: changePasswordRouteSchema,
6072
preValidation: requireAuthHook // Require authentication
6173
},
@@ -80,7 +92,7 @@ export default async function changePasswordRoute(fastify: FastifyInstance) {
8092
const authUserTable = schema.authUser;
8193

8294
if (!authUserTable) {
83-
fastify.log.error('AuthUser table not found in schema');
95+
server.log.error('AuthUser table not found in schema');
8496
const errorResponse = {
8597
success: false,
8698
error: 'Internal server error: User table configuration missing.'
@@ -164,13 +176,13 @@ export default async function changePasswordRoute(fastify: FastifyInstance) {
164176
// eslint-disable-next-line @typescript-eslint/no-explicit-any
165177
await (db as any)
166178
.update(authUserTable)
167-
.set({
179+
.set({
168180
hashed_password: hashedNewPassword,
169181
updated_at: new Date()
170182
})
171183
.where(eq(authUserTable.id, userId));
172184

173-
fastify.log.info(`Password changed successfully for user: ${userId}`);
185+
server.log.info(`Password changed successfully for user: ${userId}`);
174186

175187
// Send password change notification email if email sending is enabled
176188
try {
@@ -210,22 +222,20 @@ export default async function changePasswordRoute(fastify: FastifyInstance) {
210222
}, request.log);
211223

212224
if (emailResult.success) {
213-
fastify.log.info(`Password change notification email sent to: ${user.email}`);
225+
server.log.info(`Password change notification email sent to: ${user.email}`);
214226
} else {
215-
fastify.log.warn(`Failed to send password change notification email: ${emailResult.error}`);
227+
server.log.warn(`Failed to send password change notification email: ${emailResult.error}`);
216228
}
217229
} else {
218-
fastify.log.debug('Email sending is disabled, skipping password change notification');
230+
server.log.debug('Email sending is disabled, skipping password change notification');
219231
}
220232
} catch (emailError) {
221233
// Don't fail the password change if email fails
222-
fastify.log.warn({ error: emailError }, 'Failed to send password change notification email:');
234+
server.log.warn({ error: emailError }, 'Failed to send password change notification email:');
223235
}
224236

225237
// Optional: Invalidate all other sessions for security
226-
// This would require additional implementation to track and invalidate sessions
227-
// For now, we'll just log this as a security consideration
228-
fastify.log.info(`Consider invalidating other sessions for user: ${userId} after password change`);
238+
server.log.info(`Consider invalidating other sessions for user: ${userId} after password change`);
229239

230240
// Send success response
231241
const successResponse = {
@@ -236,7 +246,7 @@ export default async function changePasswordRoute(fastify: FastifyInstance) {
236246
return reply.status(200).type('application/json').send(jsonString);
237247

238248
} catch (error) {
239-
fastify.log.error(error, 'Error during password change:');
249+
server.log.error(error, 'Error during password change:');
240250
const errorResponse = {
241251
success: false,
242252
error: 'An unexpected error occurred during password change.'

0 commit comments

Comments
 (0)