Skip to content

Commit 1ae96ef

Browse files
author
Lasim
committed
Refactor team member management routes to improve schema validation and error handling
- Updated removeTeamMemberRoute to use constant schemas for request parameters and responses. - Enhanced transferOwnershipRoute with improved request body validation and error responses. - Refactored updateMemberRoleRoute to utilize TypeScript interfaces for better type safety and validation. - Consolidated team schemas in schemas.ts for consistency and clarity, replacing Zod with JSON Schema-like structures. - Improved updateTeamRoute to handle errors more gracefully and ensure proper type assertions.
1 parent f5420cc commit 1ae96ef

File tree

14 files changed

+2750
-2449
lines changed

14 files changed

+2750
-2449
lines changed

services/backend/api-spec.json

Lines changed: 850 additions & 1035 deletions
Large diffs are not rendered by default.

services/backend/api-spec.yaml

Lines changed: 687 additions & 818 deletions
Large diffs are not rendered by default.
Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,120 @@
1-
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2-
import { ZodError } from 'zod';
3-
import { createSchema } from 'zod-openapi';
1+
import type { FastifyInstance } from 'fastify';
42
import { TeamService } from '../../services/teamService';
53
import { requirePermission } from '../../middleware/roleMiddleware';
6-
import { CreateTeamSchema, TeamResponseSchema, ErrorResponseSchema, type CreateTeamInput } from './schemas';
4+
import {
5+
CREATE_TEAM_REQUEST_SCHEMA,
6+
TEAM_SUCCESS_RESPONSE_SCHEMA,
7+
ERROR_RESPONSE_SCHEMA,
8+
type CreateTeamRequest,
9+
type TeamSuccessResponse,
10+
type ErrorResponse
11+
} from './schemas';
712

8-
export default async function createTeamRoute(fastify: FastifyInstance) {
13+
export default async function createTeamRoute(server: FastifyInstance) {
914
// POST /teams - Create a new team
10-
fastify.post<{ Body: CreateTeamInput }>('/teams', {
15+
server.post('/teams', {
16+
preValidation: requirePermission('teams.create'),
1117
schema: {
1218
tags: ['Teams'],
1319
summary: 'Create new team',
14-
description: 'Creates a new team with the specified name and description. Users can create up to 3 teams maximum. The slug is automatically generated from the team name and made unique.',
20+
description: 'Creates a new team with the specified name and description. Users can create up to 3 teams maximum. The slug is automatically generated from the team name and made unique. Requires Content-Type: application/json header when sending request body.',
1521
security: [{ cookieAuth: [] }],
16-
body: createSchema(CreateTeamSchema),
22+
23+
// Fastify validation schema
24+
body: CREATE_TEAM_REQUEST_SCHEMA,
25+
26+
// OpenAPI documentation (same schema, reused)
27+
requestBody: {
28+
required: true,
29+
content: {
30+
'application/json': {
31+
schema: CREATE_TEAM_REQUEST_SCHEMA
32+
}
33+
}
34+
},
35+
1736
response: {
18-
201: createSchema(TeamResponseSchema.describe('Team created successfully')),
19-
400: createSchema(ErrorResponseSchema.describe('Bad Request - Validation error or team limit reached')),
20-
401: createSchema(ErrorResponseSchema.describe('Unauthorized - Authentication required')),
21-
403: createSchema(ErrorResponseSchema.describe('Forbidden - Insufficient permissions')),
22-
500: createSchema(ErrorResponseSchema.describe('Internal Server Error'))
37+
201: {
38+
...TEAM_SUCCESS_RESPONSE_SCHEMA,
39+
description: 'Team created successfully'
40+
},
41+
400: {
42+
...ERROR_RESPONSE_SCHEMA,
43+
description: 'Bad Request - Validation error or team limit reached'
44+
},
45+
401: {
46+
...ERROR_RESPONSE_SCHEMA,
47+
description: 'Unauthorized - Authentication required'
48+
},
49+
403: {
50+
...ERROR_RESPONSE_SCHEMA,
51+
description: 'Forbidden - Insufficient permissions'
52+
},
53+
500: {
54+
...ERROR_RESPONSE_SCHEMA,
55+
description: 'Internal Server Error'
56+
}
2357
}
2458
},
25-
preValidation: requirePermission('teams.create'),
26-
}, async (request: FastifyRequest<{ Body: CreateTeamInput }>, reply: FastifyReply) => {
59+
}, async (request, reply) => {
2760
try {
2861
if (!request.user) {
29-
return reply.status(401).send({
62+
const errorResponse: ErrorResponse = {
3063
success: false,
31-
error: 'Authentication required',
32-
});
64+
error: 'Authentication required'
65+
};
66+
const jsonString = JSON.stringify(errorResponse);
67+
return reply.status(401).type('application/json').send(jsonString);
3368
}
3469

35-
// Validate request body
36-
const validatedData = CreateTeamSchema.parse(request.body);
70+
// TypeScript type assertion (Fastify has already validated)
71+
const { name, description } = request.body as CreateTeamRequest;
3772

3873
// Check if user can create more teams (3 team limit)
3974
const canCreate = await TeamService.canUserCreateTeam(request.user.id);
4075
if (!canCreate) {
41-
return reply.status(400).send({
76+
const errorResponse: ErrorResponse = {
4277
success: false,
43-
error: 'You have reached the maximum limit of 3 teams',
44-
});
78+
error: 'You have reached the maximum limit of 3 teams'
79+
};
80+
const jsonString = JSON.stringify(errorResponse);
81+
return reply.status(400).type('application/json').send(jsonString);
4582
}
4683

4784
// Create the team
4885
const team = await TeamService.createTeam({
49-
name: validatedData.name,
50-
description: validatedData.description,
86+
name,
87+
description,
5188
owner_id: request.user.id,
5289
});
5390

54-
return reply.status(201).send({
91+
const successResponse: TeamSuccessResponse = {
5592
success: true,
5693
data: team,
57-
message: 'Team created successfully',
58-
});
94+
message: 'Team created successfully'
95+
};
96+
const jsonString = JSON.stringify(successResponse);
97+
return reply.status(201).type('application/json').send(jsonString);
5998
} catch (error) {
60-
if (error instanceof ZodError) {
61-
return reply.status(400).send({
62-
success: false,
63-
error: 'Validation error',
64-
details: error.issues,
65-
});
66-
}
67-
6899
if (error instanceof Error) {
69100
// Handle specific team creation errors
70101
if (error.message.includes('slug')) {
71-
return reply.status(400).send({
102+
const errorResponse: ErrorResponse = {
72103
success: false,
73-
error: 'Team name conflicts with existing team',
74-
});
104+
error: 'Team name conflicts with existing team'
105+
};
106+
const jsonString = JSON.stringify(errorResponse);
107+
return reply.status(400).type('application/json').send(jsonString);
75108
}
76109
}
77110

78-
fastify.log.error(error, 'Error creating team');
79-
return reply.status(500).send({
111+
server.log.error(error, 'Error creating team');
112+
const errorResponse: ErrorResponse = {
80113
success: false,
81-
error: 'Failed to create team',
82-
});
114+
error: 'Failed to create team'
115+
};
116+
const jsonString = JSON.stringify(errorResponse);
117+
return reply.status(500).type('application/json').send(jsonString);
83118
}
84119
});
85120
}
Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,118 @@
1-
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2-
import { createSchema } from 'zod-openapi';
1+
import type { FastifyInstance, FastifyRequest } from 'fastify';
32
import { TeamService } from '../../services/teamService';
43
import { requirePermission } from '../../middleware/roleMiddleware';
5-
import { ErrorResponseSchema } from './schemas';
4+
import {
5+
TEAM_ID_PARAMS_SCHEMA,
6+
DELETE_SUCCESS_RESPONSE_SCHEMA,
7+
ERROR_RESPONSE_SCHEMA,
8+
type TeamIdParams,
9+
type DeleteSuccessResponse,
10+
type ErrorResponse
11+
} from './schemas';
612

7-
export default async function deleteTeamRoute(fastify: FastifyInstance) {
13+
export default async function deleteTeamRoute(server: FastifyInstance) {
814
// DELETE /teams/:id - Delete team
9-
fastify.delete<{ Params: { id: string } }>('/teams/:id', {
15+
server.delete('/teams/:id', {
16+
preValidation: requirePermission('teams.delete'),
1017
schema: {
1118
tags: ['Teams'],
1219
summary: 'Delete team',
1320
description: 'Deletes a team from the system. Only team owners can delete teams. Default teams cannot be deleted.',
1421
security: [{ cookieAuth: [] }],
15-
params: {
16-
type: 'object',
17-
properties: {
18-
id: { type: 'string' }
19-
},
20-
required: ['id']
21-
},
22+
23+
// Fastify validation schema
24+
params: TEAM_ID_PARAMS_SCHEMA,
25+
2226
response: {
2327
200: {
24-
type: 'object',
25-
properties: {
26-
success: { type: 'boolean' },
27-
message: { type: 'string' }
28-
},
29-
required: ['success', 'message']
28+
...DELETE_SUCCESS_RESPONSE_SCHEMA,
29+
description: 'Team deleted successfully'
30+
},
31+
400: {
32+
...ERROR_RESPONSE_SCHEMA,
33+
description: 'Bad Request - Cannot delete default team or team has active resources'
34+
},
35+
401: {
36+
...ERROR_RESPONSE_SCHEMA,
37+
description: 'Unauthorized - Authentication required'
38+
},
39+
403: {
40+
...ERROR_RESPONSE_SCHEMA,
41+
description: 'Forbidden - Insufficient permissions or not team owner'
3042
},
31-
400: createSchema(ErrorResponseSchema.describe('Bad Request - Cannot delete default team or team has active resources')),
32-
401: createSchema(ErrorResponseSchema.describe('Unauthorized - Authentication required')),
33-
403: createSchema(ErrorResponseSchema.describe('Forbidden - Insufficient permissions')),
34-
404: createSchema(ErrorResponseSchema.describe('Not Found - Team not found')),
35-
500: createSchema(ErrorResponseSchema.describe('Internal Server Error'))
43+
404: {
44+
...ERROR_RESPONSE_SCHEMA,
45+
description: 'Not Found - Team not found'
46+
},
47+
500: {
48+
...ERROR_RESPONSE_SCHEMA,
49+
description: 'Internal Server Error'
50+
}
3651
}
3752
},
38-
preValidation: requirePermission('teams.delete'),
39-
}, async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
53+
}, async (request: FastifyRequest<{ Params: TeamIdParams }>, reply) => {
4054
try {
41-
if (!request.user) {
42-
return reply.status(401).send({
43-
success: false,
44-
error: 'Authentication required',
45-
});
46-
}
47-
48-
const teamId = request.params.id;
55+
// TypeScript types are now properly inferred from route definition
56+
const { id: teamId } = request.params;
4957

5058
// Check if team exists
5159
const existingTeam = await TeamService.getTeamById(teamId);
5260
if (!existingTeam) {
53-
return reply.status(404).send({
61+
const errorResponse: ErrorResponse = {
5462
success: false,
55-
error: 'Team not found',
56-
});
63+
error: 'Team not found'
64+
};
65+
const jsonString = JSON.stringify(errorResponse);
66+
return reply.status(404).type('application/json').send(jsonString);
5767
}
5868

59-
// Check if user is team owner
60-
if (existingTeam.owner_id !== request.user.id) {
61-
return reply.status(403).send({
69+
// Check if user is team owner (user is guaranteed to exist due to preValidation)
70+
if (existingTeam.owner_id !== request.user!.id) {
71+
const errorResponse: ErrorResponse = {
6272
success: false,
63-
error: 'Only team owners can delete teams',
64-
});
73+
error: 'Only team owners can delete teams'
74+
};
75+
const jsonString = JSON.stringify(errorResponse);
76+
return reply.status(403).type('application/json').send(jsonString);
6577
}
6678

6779
// Check if it's a default team
6880
if (existingTeam.is_default) {
69-
return reply.status(400).send({
81+
const errorResponse: ErrorResponse = {
7082
success: false,
71-
error: 'Default teams cannot be deleted',
72-
});
83+
error: 'Default teams cannot be deleted'
84+
};
85+
const jsonString = JSON.stringify(errorResponse);
86+
return reply.status(400).type('application/json').send(jsonString);
7387
}
7488

7589
// Delete the team
7690
await TeamService.deleteTeam(teamId);
7791

78-
return reply.status(200).send({
92+
const successResponse: DeleteSuccessResponse = {
7993
success: true,
80-
message: 'Team deleted successfully',
81-
});
94+
message: 'Team deleted successfully'
95+
};
96+
const jsonString = JSON.stringify(successResponse);
97+
return reply.status(200).type('application/json').send(jsonString);
8298
} catch (error) {
8399
if (error instanceof Error && error.message.includes('active resources')) {
84-
return reply.status(400).send({
100+
const errorResponse: ErrorResponse = {
85101
success: false,
86-
error: 'Cannot delete team with active resources',
87-
});
102+
error: 'Cannot delete team with active resources'
103+
};
104+
const jsonString = JSON.stringify(errorResponse);
105+
return reply.status(400).type('application/json').send(jsonString);
88106
}
89107

90-
fastify.log.error(error, 'Error deleting team');
91-
return reply.status(500).send({
108+
server.log.error(error, 'Error deleting team');
109+
const errorResponse: ErrorResponse = {
92110
success: false,
93111
error: 'Failed to delete team',
94-
details: error instanceof Error ? error.message : 'Unknown error'
95-
});
112+
details: [error instanceof Error ? error.message : 'Unknown error']
113+
};
114+
const jsonString = JSON.stringify(errorResponse);
115+
return reply.status(500).type('application/json').send(jsonString);
96116
}
97117
});
98118
}

0 commit comments

Comments
 (0)