From f1730190ddd370e1af3ce1450c395ef072c9ea72 Mon Sep 17 00:00:00 2001 From: kevinand11 Date: Sun, 3 Dec 2023 20:23:29 +0100 Subject: [PATCH] feat: manage access --- .../application/controllers/study/quizzes.ts | 19 +++++++++++++++++++ .../src/application/routes/study/quizzes.ts | 12 ++++++++++++ .../domain/types/notifications.ts | 6 +++++- .../study/data/repositories/quizzes.ts | 17 +++++++++++++++++ .../study/domain/irepositories/quizzes.ts | 1 + .../modules/study/domain/useCases/quizzes.ts | 4 ++++ .../modules/study/utils/changes/quizzes.ts | 17 ++++++++++++++++- 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/services/api/src/application/controllers/study/quizzes.ts b/services/api/src/application/controllers/study/quizzes.ts index f61742d8..e2bdbec9 100644 --- a/services/api/src/application/controllers/study/quizzes.ts +++ b/services/api/src/application/controllers/study/quizzes.ts @@ -146,4 +146,23 @@ export class QuizController { if (updatedQuiz) return updatedQuiz throw new NotAuthorizedError() } + + static async addMembers (req: Request) { + const { userIds, grant } = validate({ + userIds: Schema.array(Schema.string().min(1)).min(1), + grant: Schema.boolean() + }, req.body) + + if (grant) { + const users = await UsersUseCases.get({ + where: [{ field: 'id', value: userIds, condition: Conditions.in }] + }) + const activeUsers = users.results.filter((u) => !u.isDeleted()) + if (userIds.length !== activeUsers.length) throw new BadRequestError('some users not found') + } + + const updatedQuiz = await QuizzesUseCases.addMembers({ id: req.params.id, ownerId: req.authUser!.id, userIds, grant }) + if (updatedQuiz) return updatedQuiz + throw new NotAuthorizedError() + } } \ No newline at end of file diff --git a/services/api/src/application/routes/study/quizzes.ts b/services/api/src/application/routes/study/quizzes.ts index 3c20c5b4..d4f6a4a2 100644 --- a/services/api/src/application/routes/study/quizzes.ts +++ b/services/api/src/application/routes/study/quizzes.ts @@ -132,5 +132,17 @@ export const quizzesRoutes = groupRoutes('/quizzes', [ } }) ] + }, { + path: '/:id/access/members/manage', + method: 'post', + controllers: [ + isAuthenticated, + makeController(async (req) => { + return { + status: StatusCodes.Ok, + result: await QuizController.addMembers(req) + } + }) + ] } ]) \ No newline at end of file diff --git a/services/api/src/modules/notifications/domain/types/notifications.ts b/services/api/src/modules/notifications/domain/types/notifications.ts index 75778f80..2b041151 100644 --- a/services/api/src/modules/notifications/domain/types/notifications.ts +++ b/services/api/src/modules/notifications/domain/types/notifications.ts @@ -17,6 +17,8 @@ export enum NotificationType { NewQuizAccessRequest = 'NewQuizAccessRequest', QuizAccessRequestGranted = 'QuizAccessRequestGranted', QuizAccessRequestRejected = 'QuizAccessRequestRejected', + QuizAccessMemberGranted = 'QuizAccessMemberGranted', + QuizAccessMemberRebuked = 'QuizAccessMemberRebuked', } export type NotificationData = @@ -35,4 +37,6 @@ export type NotificationData = | { type: NotificationType.SubscriptionFailed, planId: string } | { type: NotificationType.NewQuizAccessRequest, userIds: string[] } | { type: NotificationType.QuizAccessRequestGranted, quizId: string } - | { type: NotificationType.QuizAccessRequestRejected, quizId: string } \ No newline at end of file + | { type: NotificationType.QuizAccessRequestRejected, quizId: string } + | { type: NotificationType.QuizAccessMemberGranted, quizId: string } + | { type: NotificationType.QuizAccessMemberRebuked, quizId: string } \ No newline at end of file diff --git a/services/api/src/modules/study/data/repositories/quizzes.ts b/services/api/src/modules/study/data/repositories/quizzes.ts index d52144f5..7dec02d1 100644 --- a/services/api/src/modules/study/data/repositories/quizzes.ts +++ b/services/api/src/modules/study/data/repositories/quizzes.ts @@ -125,4 +125,21 @@ export class QuizRepository implements IQuizRepository { }) return !!quiz } + + async addMembers(id: string, ownerId: string, userIds: string[], grant: boolean) { + const quiz = await Quiz.findByIdAndUpdate({ + _id: id, 'user.id': ownerId, + }, { + ...(grant ? { + $addToSet: { 'access.members': { '$each': userIds } }, + $pull: { 'access.requests': { '$in': userIds } }, + } : { + $pull: { + 'access.members': { '$in': userIds }, + 'access.requests': { '$in': userIds }, + }, + }) + }) + return !!quiz + } } \ No newline at end of file diff --git a/services/api/src/modules/study/domain/irepositories/quizzes.ts b/services/api/src/modules/study/domain/irepositories/quizzes.ts index df8e908a..bc31ee56 100644 --- a/services/api/src/modules/study/domain/irepositories/quizzes.ts +++ b/services/api/src/modules/study/domain/irepositories/quizzes.ts @@ -17,4 +17,5 @@ export interface IQuizRepository { updateRatings (id: string, ratings: number, add: boolean): Promise requestAccess (id: string, userId: string, add: boolean): Promise grantAccess (id: string, ownerId: string, userId: string, grant: boolean): Promise + addMembers (id: string, ownerId: string, userIds: string[], grant: boolean): Promise } \ No newline at end of file diff --git a/services/api/src/modules/study/domain/useCases/quizzes.ts b/services/api/src/modules/study/domain/useCases/quizzes.ts index b10af4ea..007ba498 100644 --- a/services/api/src/modules/study/domain/useCases/quizzes.ts +++ b/services/api/src/modules/study/domain/useCases/quizzes.ts @@ -61,4 +61,8 @@ export class QuizzesUseCase { async grantAccess (data: { id: string, ownerId: string, userId: string, grant: boolean }) { return await this.repository.grantAccess(data.id, data.ownerId, data.userId, data.grant) } + + async addMembers (data: { id: string, ownerId: string, userIds: string[], grant: boolean }) { + return await this.repository.addMembers(data.id, data.ownerId, data.userIds, data.grant) + } } \ No newline at end of file diff --git a/services/api/src/modules/study/utils/changes/quizzes.ts b/services/api/src/modules/study/utils/changes/quizzes.ts index 3b1ebd5e..eec50de6 100644 --- a/services/api/src/modules/study/utils/changes/quizzes.ts +++ b/services/api/src/modules/study/utils/changes/quizzes.ts @@ -43,11 +43,14 @@ export const QuizDbChangeCallbacks: DbChangeCallbacks property: UserMeta.publishedQuizzes }) - if (changes.access?.requests) { + if (changes.access) { const newRequests = after.access.requests.filter((r) => !before.access.requests.includes(r)) const oldRequests = before.access.requests.filter((r) => !after.access.requests.includes(r)) const accepted = oldRequests.filter((r) => after.access.members.includes(r)) const rejected = oldRequests.filter((r) => !after.access.members.includes(r)) + const added = after.access.members.filter((m) => !before.access.members.includes(m)) + .filter((m) => !accepted.includes(m)) + const removed = before.access.members.filter((m) => !after.access.members.includes(m)) if (newRequests.length) await sendNotification([after.user.id], { title: 'New Quiz Edit Request', body: `Someone just requested access to edit your quiz: ${after.title}`, @@ -66,6 +69,18 @@ export const QuizDbChangeCallbacks: DbChangeCallbacks sendEmail: true, data: { type: NotificationType.QuizAccessRequestRejected, quizId: after.id } }) + if (added.length) await sendNotification(added, { + title: 'Quiz Edit Request Granted', + body: `You have been granted access to edit ${after.title}`, + sendEmail: true, + data: { type: NotificationType.QuizAccessMemberGranted, quizId: after.id } + }) + if (removed.length) await sendNotification(removed, { + title: 'Quiz Edit Request Rebuked', + body: `Your access to edit ${after.title} has been rebuked`, + sendEmail: true, + data: { type: NotificationType.QuizAccessMemberRebuked, quizId: after.id } + }) } }, deleted: async ({ before }) => {