Skip to content

Commit 0d500f4

Browse files
committed
#795 Create new router for all deadline notifications, more abstraction
1 parent 5c3d00c commit 0d500f4

11 files changed

Lines changed: 171 additions & 157 deletions

.github/workflows/task-deadline-notifications.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ jobs:
1010
steps:
1111
- name: Send notifications
1212
run: |
13-
curl -X GET https://api.finishlinebyner.com/tasks/sendTaskDeadlineSlackNotifications \
13+
curl -X GET https://api.finishlinebyner.com/deadline-notifications/sendTaskDeadlineSlackNotifications \
1414
-H 'Authorization: ${{ secrets.NOTIFICATION_ENDPOINT_SECRET }}'

src/backend/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import descriptionBulletsRouter from './src/routes/description-bullets.routes';
1212
import tasksRouter from './src/routes/tasks.routes';
1313
import reimbursementRequestsRouter from './src/routes/reimbursement-requests.routes';
1414
import designReviewRouter from './src/routes/design-review.routes';
15+
import deadlineNotificationsRouter from './src/routes/deadline-notifications.routes';
1516

1617
const app = express();
1718
const port = process.env.PORT || 3001;
@@ -54,6 +55,7 @@ app.use('/description-bullets', descriptionBulletsRouter);
5455
app.use('/tasks', tasksRouter);
5556
app.use('/reimbursement-requests', reimbursementRequestsRouter);
5657
app.use('/design-reviews', designReviewRouter);
58+
app.use('/deadline-notifications', deadlineNotificationsRouter);
5759
app.use('/', (_req, res) => {
5860
res.json('Welcome to FinishLine');
5961
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
import DeadlineNotificationsService from '../services/deadline-notifications.services';
3+
4+
export default class DeadlineNotificationsController {
5+
static async sendTaskDeadlineSlackNotifications(_req: Request, res: Response, next: NextFunction) {
6+
try {
7+
const tomorrow = new Date(new Date().setHours(24, 0, 0, 0));
8+
await DeadlineNotificationsService.sendTaskDeadlineSlackNotifications(tomorrow);
9+
10+
res.status(200).json({ message: 'Successfully sent task deadline notifications!' });
11+
} catch (error: unknown) {
12+
next(error);
13+
}
14+
}
15+
}

src/backend/src/controllers/tasks.controllers.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,4 @@ export default class TasksController {
9191
next(error);
9292
}
9393
}
94-
95-
static async sendTaskDeadlineSlackNotifications(_req: Request, res: Response, next: NextFunction) {
96-
try {
97-
const tomorrow = new Date().setHours(24, 0, 0, 0);
98-
await TasksService.sendTaskDeadlineSlackNotifications(tomorrow);
99-
100-
res.status(200).json({ success: 'true' });
101-
} catch (error: unknown) {
102-
next(error);
103-
}
104-
}
10594
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import express from 'express';
2+
import DeadlineNotificationsController from '../controllers/deadline-notifications.controllers';
3+
4+
const deadlineNotificationsRouter = express.Router();
5+
6+
deadlineNotificationsRouter.get(
7+
'/sendTaskDeadlineSlackNotifications',
8+
DeadlineNotificationsController.sendTaskDeadlineSlackNotifications
9+
);
10+
11+
export default deadlineNotificationsRouter;

src/backend/src/routes/tasks.routes.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,4 @@ tasksRouter.post(
3838

3939
tasksRouter.post('/:taskId/delete', TasksController.deleteTask);
4040

41-
tasksRouter.get('/sendTaskDeadlineSlackNotifications', TasksController.sendTaskDeadlineSlackNotifications);
42-
4341
export default tasksRouter;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import prisma from '../prisma/prisma';
2+
import { TaskWithAssignees, getTeamFromTaskAssignees, usersToSlackPings } from '../utils/deadline-notifications.utils';
3+
import { sendMessage } from '../integrations/slack';
4+
5+
export default class DeadlineNotificationsService {
6+
/**
7+
* Sends the task deadline slack notifications for all tasks with a deadline of the given date
8+
* @param deadline the beginning of the deadline date (at 12am)
9+
*/
10+
static async sendTaskDeadlineSlackNotifications(deadline: Date) {
11+
const startOfDay = deadline;
12+
const endOfDay = new Date(startOfDay);
13+
endOfDay.setDate(startOfDay.getDate() + 1);
14+
15+
const tasks = await prisma.task.findMany({
16+
where: {
17+
deadline: {
18+
gte: startOfDay,
19+
lt: endOfDay
20+
},
21+
status: {
22+
not: 'DONE'
23+
}
24+
},
25+
include: {
26+
assignees: {
27+
include: {
28+
userSecureSettings: true,
29+
userSettings: true,
30+
teamAsHead: true,
31+
teamsAsLead: true,
32+
teamsAsMember: true
33+
}
34+
},
35+
wbsElement: {
36+
include: {
37+
project: { include: { teams: true } }
38+
}
39+
}
40+
}
41+
});
42+
43+
const teamTaskMap = new Map<string, TaskWithAssignees[]>();
44+
45+
// group tasks due by team in a map
46+
tasks.forEach((task) => {
47+
const teamSlackId = getTeamFromTaskAssignees(task.assignees);
48+
49+
const currentTasks = teamTaskMap.get(teamSlackId);
50+
if (currentTasks) {
51+
currentTasks.push(task);
52+
teamTaskMap.set(teamSlackId, currentTasks);
53+
} else {
54+
teamTaskMap.set(teamSlackId, [task]);
55+
}
56+
});
57+
58+
// send the notifications to each team for their respective tasks
59+
teamTaskMap.forEach((tasks, slackId) => {
60+
const messageBlock = tasks
61+
.map(
62+
(task) =>
63+
`${usersToSlackPings(task.assignees ?? [])} Reminder: ${task.title} due tomorrow in project ${
64+
task.wbsElement?.name
65+
}`
66+
)
67+
.join('\n\n');
68+
69+
sendMessage(slackId, messageBlock);
70+
});
71+
}
72+
}

src/backend/src/services/tasks.services.ts

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,10 @@ import teamQueryArgs from '../prisma-query-args/teams.query-args';
66
import prisma from '../prisma/prisma';
77
import taskTransformer from '../transformers/tasks.transformer';
88
import { NotFoundException, AccessDeniedException, HttpException, DeletedException } from '../utils/errors.utils';
9-
import {
10-
TaskWithAssignees,
11-
getTeamFromTaskAssignees,
12-
hasPermissionToEditTask,
13-
sendSlackTaskAssignedNotificationToUsers,
14-
usersToSlackIds
15-
} from '../utils/tasks.utils';
9+
import { hasPermissionToEditTask, sendSlackTaskAssignedNotificationToUsers } from '../utils/tasks.utils';
1610
import { areUsersPartOfTeams, isUserOnTeam } from '../utils/teams.utils';
1711
import { getUsers } from '../utils/users.utils';
1812
import { wbsNumOf } from '../utils/utils';
19-
import { sendMessage } from '../integrations/slack';
2013

2114
export default class TasksService {
2215
/**
@@ -265,69 +258,4 @@ export default class TasksService {
265258

266259
return deletedTask.taskId;
267260
}
268-
269-
/**
270-
* Sends the task deadline slack notifications for all tasks with a deadline of the given date
271-
* @param deadline the deadline date as a number
272-
*/
273-
static async sendTaskDeadlineSlackNotifications(deadline: number) {
274-
const startOfDay = new Date(deadline);
275-
const endOfDay = new Date(startOfDay);
276-
endOfDay.setDate(startOfDay.getDate() + 1);
277-
278-
const tasks = await prisma.task.findMany({
279-
where: {
280-
deadline: {
281-
gte: startOfDay,
282-
lt: endOfDay
283-
},
284-
status: {
285-
not: 'DONE'
286-
}
287-
},
288-
include: {
289-
assignees: {
290-
include: {
291-
userSecureSettings: true,
292-
userSettings: true,
293-
teamAsHead: true,
294-
teamsAsLead: true,
295-
teamsAsMember: true
296-
}
297-
},
298-
wbsElement: {
299-
include: {
300-
project: { include: { teams: true } }
301-
}
302-
}
303-
}
304-
});
305-
306-
const teamTaskMap = new Map<string, TaskWithAssignees[]>();
307-
308-
tasks.forEach((task) => {
309-
const teamSlackId = getTeamFromTaskAssignees(task.assignees);
310-
311-
const currentTasks = teamTaskMap.get(teamSlackId);
312-
if (currentTasks) {
313-
currentTasks.push(task);
314-
teamTaskMap.set(teamSlackId, currentTasks);
315-
} else {
316-
teamTaskMap.set(teamSlackId, [task]);
317-
}
318-
});
319-
320-
teamTaskMap.forEach((tasks, slackId) => {
321-
const messageBlock = tasks
322-
.map(
323-
(task) =>
324-
`${usersToSlackIds(task.assignees ?? [])} Reminder: ${task.title} due tomorrow in project ${
325-
task.wbsElement?.name
326-
}`
327-
)
328-
.join('\n\n');
329-
330-
sendMessage(slackId, messageBlock);
331-
});
332-
}
333261
}

src/backend/src/utils/auth.utils.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken';
22
import { Request, Response, NextFunction } from 'express';
33
import { JwtPayload, VerifyErrors } from 'jsonwebtoken';
44
import prisma from '../prisma/prisma';
5-
import { NotFoundException } from './errors.utils';
5+
import { HttpException, NotFoundException } from './errors.utils';
66
import { User, User_Secure_Settings, User_Settings } from '@prisma/client';
77

88
const TOKEN_SECRET = process.env.TOKEN_SECRET || 'i<3security';
@@ -33,17 +33,9 @@ export const requireJwtProd = (req: Request, res: Response, next: NextFunction)
3333
) {
3434
next();
3535
} else if (
36-
req.path === '/tasks/sendTaskDeadlineSlackNotifications' // task deadline notification endpoint
36+
req.path.startsWith('/deadline-notifications') // task deadline notification endpoint
3737
) {
38-
const { authorization } = req.headers;
39-
const { NOTIFICATION_ENDPOINT_SECRET } = process.env;
40-
41-
if (!authorization) return res.status(401).json({ message: 'Authentication Failed: Secret not found!' });
42-
43-
if (authorization !== NOTIFICATION_ENDPOINT_SECRET)
44-
return res.status(401).json({ message: 'Authentication Failed: Invalid secret!' });
45-
46-
next();
38+
notificationEndpointAuth(req, res, next);
4739
} else {
4840
const { token } = req.cookies;
4941

@@ -72,17 +64,9 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) =
7264
) {
7365
next();
7466
} else if (
75-
req.path === '/tasks/sendTaskDeadlineSlackNotifications' // task deadline notification endpoint
67+
req.path.startsWith('/deadline-notifications') // task deadline notification endpoint
7668
) {
77-
const { authorization } = req.headers;
78-
const { NOTIFICATION_ENDPOINT_SECRET } = process.env;
79-
80-
if (!authorization) return res.status(401).json({ message: 'Authentication Failed: Secret not found!' });
81-
82-
if (authorization !== NOTIFICATION_ENDPOINT_SECRET)
83-
return res.status(401).json({ message: 'Authentication Failed: Invalid secret!' });
84-
85-
next();
69+
notificationEndpointAuth(req, res, next);
8670
} else {
8771
const devUserId = req.headers.authorization;
8872

@@ -94,6 +78,20 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) =
9478
}
9579
};
9680

81+
const notificationEndpointAuth = (req: Request, res: Response, next: NextFunction) => {
82+
const { authorization } = req.headers;
83+
const { NOTIFICATION_ENDPOINT_SECRET } = process.env;
84+
85+
if (!NOTIFICATION_ENDPOINT_SECRET) throw new HttpException(500, 'Notification endpoint secret not found!');
86+
87+
if (!authorization) return res.status(401).json({ message: 'Authentication Failed: Secret not found!' });
88+
89+
if (authorization !== NOTIFICATION_ENDPOINT_SECRET)
90+
return res.status(401).json({ message: 'Authentication Failed: Invalid secret!' });
91+
92+
next();
93+
};
94+
9795
/**
9896
* get the user making the request.
9997
* @param res - we use the response because that's where we stored the userId data during jwt validation
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Team, Task as Prisma_Task, WBS_Element } from '@prisma/client';
2+
import { UserWithSettings } from './auth.utils';
3+
import { HttpException } from './errors.utils';
4+
5+
export type UserWithTeams = UserWithSettings & {
6+
teamAsHead: Team | null;
7+
teamsAsLead: Team[] | null;
8+
teamsAsMember: Team[] | null;
9+
};
10+
11+
export type TaskWithAssignees = Prisma_Task & {
12+
assignees: UserWithSettings[] | null;
13+
wbsElement: WBS_Element | null;
14+
};
15+
16+
export const usersToSlackPings = (users: UserWithSettings[]) => {
17+
// https://api.slack.com/reference/surfaces/formatting#mentioning-users
18+
return users.map((user) => `<@${user.userSettings?.slackId}>`).join(' ');
19+
};
20+
21+
/**
22+
* Gets the team of a task's assignees
23+
* @param users the users of the task
24+
* @returns the slack id of the team assigned to the task
25+
*/
26+
export const getTeamFromTaskAssignees = (users: UserWithTeams[]): string => {
27+
const allTeams = users.map((user) => {
28+
const teams = [];
29+
if (user.teamAsHead) teams.push(user.teamAsHead);
30+
if (user.teamsAsLead) teams.push(...user.teamsAsLead);
31+
if (user.teamsAsMember) teams.push(...user.teamsAsMember);
32+
return teams;
33+
});
34+
35+
// Find common teams across all users
36+
const commonTeams = allTeams.reduce((acc, teams, index) => {
37+
if (index === 0) {
38+
return teams;
39+
}
40+
return acc.filter((team) => teams.some((t) => t.teamId === team.teamId));
41+
}, allTeams[0] || []);
42+
43+
// Assuming we return the Slack ID of the first common team if there are any
44+
if (commonTeams.length > 0) {
45+
return commonTeams[0].slackId; // Return the slackId of the first common team
46+
}
47+
48+
throw new HttpException(400, 'All of the users do not share a team!');
49+
};

0 commit comments

Comments
 (0)