|
1 | | -// src/functions/teams/invite/handler.ts |
2 | 1 | import * as dotenv from 'dotenv'; |
3 | 2 | import * as path from 'path'; |
4 | 3 | import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; |
5 | 4 | import { middyfy } from '@libs/lambda'; |
6 | 5 | import schema from './schema'; |
7 | | -import type { Failure, TeamInvite, TeamDocument, UserDocument } from '../../../types'; |
8 | | -import { MongoDB, validateToken, userExistsLogic } from '../../../util'; |
| 6 | +import { teamInviteLogic } from '../../../util'; |
9 | 7 |
|
10 | 8 | dotenv.config({ path: path.resolve(process.cwd(), '.env') }); |
11 | 9 |
|
12 | | -const MAX_TEAM_SIZE = 4; |
13 | | - |
14 | 10 | const teamInvite: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (event) => { |
15 | | - // Alias snake_case request fields to camelCase locals to satisfy naming-convention |
16 | 11 | const { auth_token: authToken, auth_email: authEmail, team_id: teamId, emails } = event.body; |
17 | | - |
18 | | - // auth check |
19 | | - if (!validateToken(authToken, process.env.JWT_SECRET!, authEmail)) { |
20 | | - return { |
21 | | - statusCode: 401, |
22 | | - body: JSON.stringify({ message: 'Unauthorized' }), |
23 | | - }; |
24 | | - } |
25 | | - |
26 | | - // DB setup |
27 | | - const db = MongoDB.getInstance(process.env.MONGO_URI!); |
28 | | - await db.connect(); |
29 | | - const client = db.getClient(); |
30 | | - const users = db.getCollection<UserDocument>('users'); |
31 | | - const teams = db.getCollection<TeamDocument>('teams'); |
32 | | - |
33 | | - // verify auth user |
34 | | - const authUser = await users.findOne({ email: authEmail }); |
35 | | - if (!authUser) { |
36 | | - return { |
37 | | - statusCode: 404, |
38 | | - body: JSON.stringify({ message: 'Auth user not found' }), |
39 | | - }; |
40 | | - } |
41 | | - |
42 | | - // verify team & leadership |
43 | | - const team = await teams.findOne({ team_id: teamId }); |
44 | | - if (!team) { |
45 | | - return { |
46 | | - statusCode: 404, |
47 | | - body: JSON.stringify({ message: 'Team not found' }), |
48 | | - }; |
49 | | - } |
50 | | - if (team.leader_email !== authEmail) { |
51 | | - return { |
52 | | - statusCode: 403, |
53 | | - body: JSON.stringify({ message: 'Auth user is not the team leader' }), |
54 | | - }; |
55 | | - } |
56 | | - |
57 | | - // check team status |
58 | | - if (team.status !== 'Active') { |
59 | | - return { |
60 | | - statusCode: 400, |
61 | | - body: JSON.stringify({ message: 'Team is not active' }), |
62 | | - }; |
63 | | - } |
64 | | - |
65 | | - // capacity check |
66 | | - const confirmedCount = (Array.isArray(team.members) ? team.members.length : 0) + 1; // + 1 for the leader |
67 | | - const pendingCount = await users.countDocuments({ |
68 | | - 'team_info.pending_invites.team_id': teamId, |
69 | | - }); |
70 | | - const availableSlots = MAX_TEAM_SIZE - confirmedCount - pendingCount; |
71 | | - if (availableSlots <= 0) { |
72 | | - return { |
73 | | - statusCode: 400, |
74 | | - body: JSON.stringify({ message: 'Team is already full' }), |
75 | | - }; |
76 | | - } |
77 | | - |
78 | | - const invited: string[] = []; |
79 | | - const failed: Failure[] = []; |
80 | | - |
81 | | - // use a transaction for all updates |
82 | | - const session = client.startSession(); |
83 | | - try { |
84 | | - await session.withTransaction(async () => { |
85 | | - const uniqueEmails = Array.from(new Set(emails)); |
86 | | - |
87 | | - for (const email of uniqueEmails) { |
88 | | - // stop when no slots remain |
89 | | - if (invited.length >= availableSlots) { |
90 | | - failed.push({ email, reason: 'No slots remaining' }); |
91 | | - continue; |
92 | | - } |
93 | | - |
94 | | - // check user exists via util |
95 | | - const { statusCode: uxStatus } = await userExistsLogic(authEmail, authToken, email); |
96 | | - if (uxStatus !== 200) { |
97 | | - failed.push({ email, reason: 'User does not exist or unauthorized' }); |
98 | | - continue; |
99 | | - } |
100 | | - |
101 | | - // load user within transaction |
102 | | - const user = await users.findOne({ email }, { session }); |
103 | | - if (!user) { |
104 | | - failed.push({ email, reason: 'User record not found' }); |
105 | | - continue; |
106 | | - } |
107 | | - |
108 | | - // prevent users already on a team |
109 | | - if (user.confirmed_team) { |
110 | | - failed.push({ email, reason: 'Already a confirmed team member' }); |
111 | | - continue; |
112 | | - } |
113 | | - |
114 | | - // prevent duplicate invites (type the pending list instead of using any) |
115 | | - const pending = (user.team_info?.pending_invites ?? []) as TeamInvite[]; |
116 | | - if (pending.some((inv) => inv.team_id === teamId)) { |
117 | | - failed.push({ email, reason: 'Already invited to this team' }); |
118 | | - continue; |
119 | | - } |
120 | | - |
121 | | - // all checks pass -> enqueue invite |
122 | | - await users.updateOne( |
123 | | - { email }, |
124 | | - { |
125 | | - $push: { |
126 | | - 'team_info.pending_invites': { |
127 | | - team_id: teamId, |
128 | | - invited_by: authEmail, |
129 | | - invited_at: new Date(), |
130 | | - team_name: team.team_name, |
131 | | - } as TeamInvite, |
132 | | - }, |
133 | | - }, |
134 | | - { session } |
135 | | - ); |
136 | | - invited.push(email); |
137 | | - } |
138 | | - }); |
139 | | - } catch (err) { |
140 | | - // eslint-disable-next-line no-console |
141 | | - console.error('Transaction aborted:', err); |
142 | | - return { |
143 | | - statusCode: 500, |
144 | | - body: JSON.stringify({ |
145 | | - message: 'Internal server error during invitation processing', |
146 | | - }), |
147 | | - }; |
148 | | - } finally { |
149 | | - await session.endSession(); |
150 | | - } |
151 | | - |
152 | | - return { |
153 | | - statusCode: 200, |
154 | | - body: JSON.stringify({ |
155 | | - message: 'Invitations sent successfully', |
156 | | - invited, |
157 | | - failed, |
158 | | - }), |
159 | | - }; |
| 12 | + return teamInviteLogic(authEmail, authToken, teamId, emails); |
160 | 13 | }; |
161 | 14 |
|
162 | 15 | export const main = middyfy(teamInvite); |
0 commit comments