Skip to content

Commit 5f96dc8

Browse files
authored
Merge pull request #129 from HackRU/teams-read
feat: teams read endpoint
2 parents f7d9fa6 + a39bdf6 commit 5f96dc8

10 files changed

Lines changed: 480 additions & 72 deletions

File tree

serverless.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import deleteUser from '@functions/delete';
2121
import userExists from '@functions/user-exists';
2222
import interestForm from '@functions/interest-form';
2323
import teamsJoin from '@functions/teams/join';
24+
import teamsRead from '@functions/teams/read';
2425

2526
import * as path from 'path';
2627
import * as dotenv from 'dotenv';
@@ -68,6 +69,7 @@ const serverlessConfiguration: AWS = {
6869
userExists,
6970
interestForm,
7071
teamsJoin,
72+
teamsRead,
7173
},
7274
package: { individually: true, patterns: ['!.env*', '.env.vault'] },
7375
custom: {

src/functions/attend-event/handler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { middyfy } from '@libs/lambda';
44

55
import schema from './schema';
66

7-
import { MongoDB, validateToken, ensureRoles, UserDoc } from '../../util';
7+
import { MongoDB, validateToken, ensureRoles } from '../../util';
8+
import type { UserDocument } from 'src/types';
89
import * as path from 'path';
910
import * as dotenv from 'dotenv';
1011
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
@@ -26,7 +27,7 @@ const attendEvent: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (ev
2627
// Connect to MongoDB
2728
const db = MongoDB.getInstance(process.env.MONGO_URI);
2829
await db.connect();
29-
const users = db.getCollection<UserDoc>('users');
30+
const users = db.getCollection<UserDocument>('users');
3031

3132
const attendEvent = await users.findOne({ email: event.body.qr });
3233

src/functions/delete/handler.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway';
22
import { middyfy } from '@libs/lambda';
33
import schema from './schema';
4-
import { MongoDB, validateToken, ensureRoles, UserDoc } from '../../util';
4+
5+
import { MongoDB, validateToken, ensureRoles } from '../../util';
6+
import type { UserDocument } from 'src/types';
7+
58
import * as path from 'path';
69
import * as dotenv from 'dotenv';
710

@@ -24,7 +27,7 @@ const deleteUser: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (eve
2427
// 2. Connect to MongoDB
2528
const db = MongoDB.getInstance(process.env.MONGO_URI);
2629
await db.connect();
27-
const users = db.getCollection<UserDoc>('users');
30+
const users = db.getCollection<UserDocument>('users');
2831

2932
// 3. Check target user exists
3033
const target = await users.findOne({ email: user_email });

src/functions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { default as delete } from './delete';
1919
export { default as userExists } from './user-exists';
2020
export { default as interestForm } from './interest-form';
2121
export { default as teamsJoin } from './teams/join';
22+
export { default as teamsRead } from './teams/read';
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway';
2+
import { middyfy } from '@libs/lambda';
3+
4+
import schema from './schema';
5+
6+
import { ensureRoles, MongoDB, validateToken } from '../../../util';
7+
import type { UserDocument, TeamDocument } from '../../../types';
8+
9+
import * as path from 'path';
10+
import * as dotenv from 'dotenv';
11+
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
12+
13+
const teamsRead: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (event) => {
14+
try {
15+
// Validate auth token
16+
const isValidToken = validateToken(event.body.auth_token, process.env.JWT_SECRET, event.body.auth_email);
17+
if (!isValidToken) {
18+
return {
19+
statusCode: 401,
20+
body: JSON.stringify({
21+
statusCode: 401,
22+
message: 'Unauthorized',
23+
}),
24+
};
25+
}
26+
27+
// Connect to database
28+
const db = MongoDB.getInstance(process.env.MONGO_URI);
29+
await db.connect();
30+
const users = db.getCollection<UserDocument>('users');
31+
const teams = db.getCollection<TeamDocument>('teams');
32+
33+
// Check if auth user exists
34+
const authUser = await users.findOne({ email: event.body.auth_email.toLowerCase() });
35+
if (!authUser) {
36+
return {
37+
statusCode: 404,
38+
body: JSON.stringify({
39+
statusCode: 404,
40+
message: 'Auth user not found',
41+
}),
42+
};
43+
}
44+
45+
let teamId = event.body.team_id;
46+
47+
// Determine auth user permissions
48+
const isOrganizer = ensureRoles(authUser.role, ['director', 'organizer']);
49+
if (
50+
((teamId && authUser.team_info?.team_id !== teamId) ||
51+
(!teamId && event.body.auth_email !== event.body.member_email)) &&
52+
!isOrganizer
53+
) {
54+
return {
55+
statusCode: 401,
56+
body: JSON.stringify({
57+
statusCode: 401,
58+
message: 'Unauthorized',
59+
}),
60+
};
61+
}
62+
63+
// Fetch team id if member_email specified
64+
if (!teamId) {
65+
const teamUser = await users.findOne({ email: event.body.member_email.toLowerCase() });
66+
if (!teamUser) {
67+
return {
68+
statusCode: 404,
69+
body: JSON.stringify({
70+
statusCode: 404,
71+
message: 'Team user not found',
72+
}),
73+
};
74+
}
75+
76+
if (!teamUser.team_info?.team_id) {
77+
return {
78+
statusCode: 404,
79+
body: JSON.stringify({
80+
statusCode: 404,
81+
message: `User (${event.body.member_email}) is not in an active team`,
82+
}),
83+
};
84+
}
85+
86+
teamId = teamUser.team_info.team_id;
87+
}
88+
89+
// Fetch team and validate status
90+
const team = await teams.findOne({ team_id: teamId });
91+
if (!team) {
92+
return {
93+
statusCode: 404,
94+
body: JSON.stringify({
95+
statusCode: 404,
96+
message: 'Team not found',
97+
}),
98+
};
99+
}
100+
101+
if (team.status !== 'Active') {
102+
return {
103+
statusCode: 400,
104+
body: JSON.stringify({
105+
statusCode: 400,
106+
message: 'Team is not active',
107+
}),
108+
};
109+
}
110+
111+
// Return team data
112+
return {
113+
statusCode: 200,
114+
body: JSON.stringify({
115+
statusCode: 200,
116+
message: 'Successfully read team',
117+
team,
118+
}),
119+
};
120+
} catch (error) {
121+
console.error('Error reading team:', error);
122+
123+
return {
124+
statusCode: 500,
125+
body: JSON.stringify({
126+
statusCode: 500,
127+
message: 'Internal Server Error',
128+
error: error.message,
129+
}),
130+
};
131+
}
132+
};
133+
134+
export const main = middyfy(teamsRead);

src/functions/teams/read/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { handlerPath } from '@libs/handler-resolver';
2+
import schema from './schema';
3+
4+
export default {
5+
handler: `${handlerPath(__dirname)}/handler.main`,
6+
events: [
7+
{
8+
http: {
9+
method: 'post',
10+
path: 'teams/read',
11+
cors: true,
12+
request: {
13+
schemas: {
14+
'application/json': schema,
15+
},
16+
},
17+
},
18+
},
19+
],
20+
};

src/functions/teams/read/schema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default {
2+
type: 'object',
3+
properties: {
4+
auth_token: { type: 'string' },
5+
auth_email: { type: 'string', format: 'email' },
6+
team_id: { type: 'string' },
7+
member_email: { type: 'string', format: 'email' },
8+
},
9+
oneOf: [
10+
{ required: ['auth_token', 'auth_email', 'team_id'] },
11+
{ required: ['auth_token', 'auth_email', 'member_email'] },
12+
],
13+
} as const;

src/types.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ export interface UserTeamInfo {
1111
pending_invites: TeamInvite[];
1212
}
1313

14-
export interface UserDocument {
15-
email: string;
16-
confirmed_team?: boolean;
17-
team_info?: UserTeamInfo;
18-
[key: string]: unknown;
19-
}
20-
2114
export interface TeamDocument {
2215
team_id: string;
2316
leader_email: string;
@@ -27,3 +20,66 @@ export interface TeamDocument {
2720
created: Date;
2821
updated: Date;
2922
}
23+
24+
export interface UserDocument {
25+
first_name: string;
26+
last_name: string;
27+
email: string;
28+
email_verified: boolean;
29+
password: string;
30+
role: {
31+
hacker: boolean;
32+
volunteer: boolean;
33+
judge: boolean;
34+
sponsor: boolean;
35+
mentor: boolean;
36+
organizer: boolean;
37+
director: boolean;
38+
};
39+
votes: 0;
40+
github: string;
41+
major: string;
42+
short_answer: string;
43+
shirt_size: string;
44+
dietary_restrictions: string;
45+
special_needs: string;
46+
date_of_birth: string;
47+
school: string;
48+
grad_year: string;
49+
gender: string;
50+
level_of_study: string;
51+
ethnicity: string;
52+
phone_number: string;
53+
registration_status: RegistrationStatus;
54+
day_of: {
55+
event: Record<
56+
string,
57+
{
58+
attend: number;
59+
time: string[];
60+
}
61+
>;
62+
};
63+
discord: {
64+
user_id: string;
65+
username: string;
66+
access_token: string;
67+
refresh_token: string;
68+
expires_at: number;
69+
};
70+
confirmed_team?: boolean;
71+
team_info?: UserTeamInfo;
72+
created_at: string;
73+
registered_at: string;
74+
}
75+
76+
type RegistrationStatus =
77+
| 'unregistered'
78+
| 'registered'
79+
| 'rejected'
80+
| 'confirmation'
81+
| 'waitlist'
82+
| 'coming'
83+
| 'not_coming'
84+
| 'confirmed'
85+
| 'checked_in';

src/util.ts

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -163,64 +163,3 @@ export async function userExistsLogic(
163163
};
164164
}
165165
}
166-
167-
export interface UserDoc {
168-
first_name: string;
169-
last_name: string;
170-
email: string;
171-
email_verified: boolean;
172-
password: string;
173-
role: {
174-
hacker: boolean;
175-
volunteer: boolean;
176-
judge: boolean;
177-
sponsor: boolean;
178-
mentor: boolean;
179-
organizer: boolean;
180-
director: boolean;
181-
};
182-
votes: 0;
183-
github: string;
184-
major: string;
185-
short_answer: string;
186-
shirt_size: string;
187-
dietary_restrictions: string;
188-
special_needs: string;
189-
date_of_birth: string;
190-
school: string;
191-
grad_year: string;
192-
gender: string;
193-
level_of_study: string;
194-
ethnicity: string;
195-
phone_number: string;
196-
registration_status: RegistrationStatus;
197-
day_of: {
198-
event: Record<
199-
string,
200-
{
201-
attend: number;
202-
time: string[];
203-
}
204-
>;
205-
};
206-
discord: {
207-
user_id: string;
208-
username: string;
209-
access_token: string;
210-
refresh_token: string;
211-
expires_at: number;
212-
};
213-
created_at: string;
214-
registered_at: string;
215-
}
216-
217-
type RegistrationStatus =
218-
| 'unregistered'
219-
| 'registered'
220-
| 'rejected'
221-
| 'confirmation'
222-
| 'waitlist'
223-
| 'coming'
224-
| 'not_coming'
225-
| 'confirmed'
226-
| 'checked_in';

0 commit comments

Comments
 (0)