Skip to content

Commit f5420cc

Browse files
author
Lasim
committed
feat(backend): re-implement team management routes for CRUD operations
1 parent 62ba4c0 commit f5420cc

File tree

7 files changed

+548
-504
lines changed

7 files changed

+548
-504
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import { ZodError } from 'zod';
3+
import { createSchema } from 'zod-openapi';
4+
import { TeamService } from '../../services/teamService';
5+
import { requirePermission } from '../../middleware/roleMiddleware';
6+
import { CreateTeamSchema, TeamResponseSchema, ErrorResponseSchema, type CreateTeamInput } from './schemas';
7+
8+
export default async function createTeamRoute(fastify: FastifyInstance) {
9+
// POST /teams - Create a new team
10+
fastify.post<{ Body: CreateTeamInput }>('/teams', {
11+
schema: {
12+
tags: ['Teams'],
13+
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.',
15+
security: [{ cookieAuth: [] }],
16+
body: createSchema(CreateTeamSchema),
17+
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'))
23+
}
24+
},
25+
preValidation: requirePermission('teams.create'),
26+
}, async (request: FastifyRequest<{ Body: CreateTeamInput }>, reply: FastifyReply) => {
27+
try {
28+
if (!request.user) {
29+
return reply.status(401).send({
30+
success: false,
31+
error: 'Authentication required',
32+
});
33+
}
34+
35+
// Validate request body
36+
const validatedData = CreateTeamSchema.parse(request.body);
37+
38+
// Check if user can create more teams (3 team limit)
39+
const canCreate = await TeamService.canUserCreateTeam(request.user.id);
40+
if (!canCreate) {
41+
return reply.status(400).send({
42+
success: false,
43+
error: 'You have reached the maximum limit of 3 teams',
44+
});
45+
}
46+
47+
// Create the team
48+
const team = await TeamService.createTeam({
49+
name: validatedData.name,
50+
description: validatedData.description,
51+
owner_id: request.user.id,
52+
});
53+
54+
return reply.status(201).send({
55+
success: true,
56+
data: team,
57+
message: 'Team created successfully',
58+
});
59+
} 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+
68+
if (error instanceof Error) {
69+
// Handle specific team creation errors
70+
if (error.message.includes('slug')) {
71+
return reply.status(400).send({
72+
success: false,
73+
error: 'Team name conflicts with existing team',
74+
});
75+
}
76+
}
77+
78+
fastify.log.error(error, 'Error creating team');
79+
return reply.status(500).send({
80+
success: false,
81+
error: 'Failed to create team',
82+
});
83+
}
84+
});
85+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import { createSchema } from 'zod-openapi';
3+
import { TeamService } from '../../services/teamService';
4+
import { requirePermission } from '../../middleware/roleMiddleware';
5+
import { ErrorResponseSchema } from './schemas';
6+
7+
export default async function deleteTeamRoute(fastify: FastifyInstance) {
8+
// DELETE /teams/:id - Delete team
9+
fastify.delete<{ Params: { id: string } }>('/teams/:id', {
10+
schema: {
11+
tags: ['Teams'],
12+
summary: 'Delete team',
13+
description: 'Deletes a team from the system. Only team owners can delete teams. Default teams cannot be deleted.',
14+
security: [{ cookieAuth: [] }],
15+
params: {
16+
type: 'object',
17+
properties: {
18+
id: { type: 'string' }
19+
},
20+
required: ['id']
21+
},
22+
response: {
23+
200: {
24+
type: 'object',
25+
properties: {
26+
success: { type: 'boolean' },
27+
message: { type: 'string' }
28+
},
29+
required: ['success', 'message']
30+
},
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'))
36+
}
37+
},
38+
preValidation: requirePermission('teams.delete'),
39+
}, async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
40+
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;
49+
50+
// Check if team exists
51+
const existingTeam = await TeamService.getTeamById(teamId);
52+
if (!existingTeam) {
53+
return reply.status(404).send({
54+
success: false,
55+
error: 'Team not found',
56+
});
57+
}
58+
59+
// Check if user is team owner
60+
if (existingTeam.owner_id !== request.user.id) {
61+
return reply.status(403).send({
62+
success: false,
63+
error: 'Only team owners can delete teams',
64+
});
65+
}
66+
67+
// Check if it's a default team
68+
if (existingTeam.is_default) {
69+
return reply.status(400).send({
70+
success: false,
71+
error: 'Default teams cannot be deleted',
72+
});
73+
}
74+
75+
// Delete the team
76+
await TeamService.deleteTeam(teamId);
77+
78+
return reply.status(200).send({
79+
success: true,
80+
message: 'Team deleted successfully',
81+
});
82+
} catch (error) {
83+
if (error instanceof Error && error.message.includes('active resources')) {
84+
return reply.status(400).send({
85+
success: false,
86+
error: 'Cannot delete team with active resources',
87+
});
88+
}
89+
90+
fastify.log.error(error, 'Error deleting team');
91+
return reply.status(500).send({
92+
success: false,
93+
error: 'Failed to delete team',
94+
details: error instanceof Error ? error.message : 'Unknown error'
95+
});
96+
}
97+
});
98+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import { createSchema } from 'zod-openapi';
3+
import { TeamService } from '../../services/teamService';
4+
import { requireAuthenticationAny, requireOAuthScope } from '../../middleware/oauthMiddleware';
5+
import { TeamResponseSchema, ErrorResponseSchema } from './schemas';
6+
7+
export default async function getDefaultTeamRoute(fastify: FastifyInstance) {
8+
// GET /teams/me/default - Get current user's default team
9+
fastify.get('/teams/me/default', {
10+
schema: {
11+
tags: ['Teams'],
12+
summary: 'Get current user default team',
13+
description: 'Retrieves the default team for the currently authenticated user. Supports both cookie-based authentication (for web users) and OAuth2 Bearer token authentication (for CLI users). Requires teams:read scope for OAuth2 access.',
14+
security: [
15+
{ cookieAuth: [] },
16+
{ bearerAuth: [] }
17+
],
18+
response: {
19+
200: createSchema(TeamResponseSchema.describe('Default team retrieved successfully')),
20+
401: createSchema(ErrorResponseSchema.describe('Unauthorized - Authentication required or invalid token')),
21+
403: createSchema(ErrorResponseSchema.describe('Forbidden - Insufficient permissions or scope')),
22+
404: createSchema(ErrorResponseSchema.describe('Not Found - No default team found')),
23+
500: createSchema(ErrorResponseSchema.describe('Internal Server Error'))
24+
}
25+
},
26+
preValidation: [
27+
requireAuthenticationAny(),
28+
requireOAuthScope('teams:read')
29+
]
30+
}, async (request: FastifyRequest, reply: FastifyReply) => {
31+
try {
32+
if (!request.user) {
33+
return reply.status(401).send({
34+
success: false,
35+
error: 'Authentication required',
36+
});
37+
}
38+
39+
const authType = request.tokenPayload ? 'oauth2' : 'cookie';
40+
const userId = request.user.id;
41+
42+
request.log.debug({
43+
operation: 'get_user_default_team',
44+
userId,
45+
authType,
46+
clientId: request.tokenPayload?.clientId,
47+
scope: request.tokenPayload?.scope,
48+
endpoint: request.url
49+
}, 'Authentication method determined for default team retrieval');
50+
51+
const defaultTeam = await TeamService.getUserDefaultTeam(request.user.id);
52+
53+
if (!defaultTeam) {
54+
return reply.status(404).send({
55+
success: false,
56+
error: 'No default team found',
57+
});
58+
}
59+
60+
return reply.status(200).send({
61+
success: true,
62+
data: defaultTeam,
63+
message: 'Default team retrieved successfully',
64+
});
65+
} catch (error) {
66+
fastify.log.error(error, 'Error fetching user default team');
67+
return reply.status(500).send({
68+
success: false,
69+
error: 'Failed to fetch default team',
70+
});
71+
}
72+
});
73+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import { z } from 'zod';
3+
import { createSchema } from 'zod-openapi';
4+
import { TeamService } from '../../services/teamService';
5+
import { requireAuthenticationAny, requireOAuthScope } from '../../middleware/oauthMiddleware';
6+
import { TeamWithRoleInfoSchema, ErrorResponseSchema } from './schemas';
7+
8+
export default async function getTeamByIdRoute(fastify: FastifyInstance) {
9+
// GET /teams/:id - Get team by ID with user role info
10+
fastify.get<{ Params: { id: string } }>('/teams/:id', {
11+
schema: {
12+
tags: ['Teams'],
13+
summary: 'Get team by ID with user role',
14+
description: 'Retrieves a specific team by its ID with the current user\'s role and permissions within that team. User must be a member of the team. Supports both cookie-based authentication (for web users) and OAuth2 Bearer token authentication (for CLI users). Requires teams:read scope for OAuth2 access.',
15+
security: [
16+
{ cookieAuth: [] },
17+
{ bearerAuth: [] }
18+
],
19+
params: {
20+
type: 'object',
21+
properties: {
22+
id: { type: 'string' }
23+
},
24+
required: ['id']
25+
},
26+
response: {
27+
200: createSchema(z.object({
28+
success: z.boolean().describe('Indicates if the operation was successful'),
29+
data: TeamWithRoleInfoSchema.describe('Team data with user role information')
30+
}).describe('Team retrieved successfully with user role info')),
31+
401: createSchema(ErrorResponseSchema.describe('Unauthorized - Authentication required or invalid token')),
32+
403: createSchema(ErrorResponseSchema.describe('Forbidden - Insufficient permissions or scope')),
33+
404: createSchema(ErrorResponseSchema.describe('Not Found - Team not found')),
34+
500: createSchema(ErrorResponseSchema.describe('Internal Server Error'))
35+
}
36+
},
37+
preValidation: [
38+
requireAuthenticationAny(),
39+
requireOAuthScope('teams:read')
40+
]
41+
}, async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
42+
try {
43+
if (!request.user) {
44+
const errorResponse = {
45+
success: false,
46+
error: 'Authentication required'
47+
};
48+
const jsonString = JSON.stringify(errorResponse);
49+
return reply.status(401).type('application/json').send(jsonString);
50+
}
51+
52+
const authType = request.tokenPayload ? 'oauth2' : 'cookie';
53+
const userId = request.user.id;
54+
const teamId = request.params.id;
55+
56+
request.log.debug({
57+
operation: 'get_team_by_id',
58+
userId,
59+
teamId,
60+
authType,
61+
clientId: request.tokenPayload?.clientId,
62+
scope: request.tokenPayload?.scope,
63+
endpoint: request.url
64+
}, 'Authentication method determined for team retrieval');
65+
66+
const team = await TeamService.getTeamById(teamId);
67+
68+
if (!team) {
69+
const errorResponse = {
70+
success: false,
71+
error: 'Team not found'
72+
};
73+
const jsonString = JSON.stringify(errorResponse);
74+
return reply.status(404).type('application/json').send(jsonString);
75+
}
76+
77+
// Check if user has access to this team
78+
const isTeamMember = await TeamService.isTeamMember(teamId, request.user.id);
79+
80+
if (!isTeamMember) {
81+
const errorResponse = {
82+
success: false,
83+
error: 'You do not have access to this team'
84+
};
85+
const jsonString = JSON.stringify(errorResponse);
86+
return reply.status(403).type('application/json').send(jsonString);
87+
}
88+
89+
// Get user's role and permissions within this team
90+
const membership = await TeamService.getTeamMembership(teamId, request.user.id);
91+
const memberCount = await TeamService.getTeamMemberCount(teamId);
92+
93+
// Build team response with role information
94+
const teamWithRoleInfo = {
95+
...team,
96+
role: membership?.role || 'team_user',
97+
is_admin: membership?.role === 'team_admin',
98+
is_owner: team.owner_id === request.user.id,
99+
member_count: memberCount
100+
};
101+
102+
const successResponse = {
103+
success: true,
104+
data: teamWithRoleInfo
105+
};
106+
const jsonString = JSON.stringify(successResponse);
107+
return reply.status(200).type('application/json').send(jsonString);
108+
} catch (error) {
109+
fastify.log.error(error, 'Error fetching team');
110+
const errorResponse = {
111+
success: false,
112+
error: 'Failed to fetch team'
113+
};
114+
const jsonString = JSON.stringify(errorResponse);
115+
return reply.status(500).type('application/json').send(jsonString);
116+
}
117+
});
118+
}

0 commit comments

Comments
 (0)