Skip to content

Apply the new permissions to the UE, comments and annals routes #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 11, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -898,11 +898,13 @@ enum AddressPrivacy {
}

enum Permission {
API_SEE_OPINIONS_UE
API_UPLOAD_ANNAL
API_MODERATE_ANNAL
API_MODERATE_COMMENTS

USER_SEE_DETAILS
USER_UPDATE_DETAILS
API_SEE_OPINIONS_UE // See the rates of an UE
API_GIVE_OPINIONS_UE // Rate an UE you have done or are doing
API_SEE_ANNALS // See and download annals
API_UPLOAD_ANNALS // Upload an annal
API_MODERATE_ANNALS // Moderate annals
API_MODERATE_COMMENTS // Moderate comments

USER_SEE_DETAILS // See personal details about someone, even the ones the user decided to hide
USER_UPDATE_DETAILS // Update personal details about someone
}
4 changes: 2 additions & 2 deletions prisma/seed/modules/ueSubscription.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function ueSubscriptionSeed(
): Promise<RawUserUeSubscription[]> {
console.log('Seeding UE subscriptions...');
const subscriptions: Promise<RawUserUeSubscription>[] = [];
/*for (const user of users) {
for (const user of users) {
const subscribedToUes = faker.helpers.arrayElements(ues, faker.datatype.number({ min: 1, max: 10 }));
for (const ue of subscribedToUes) {
subscriptions.push(
Expand All @@ -24,7 +24,7 @@ export default function ueSubscriptionSeed(
}),
);
}
}*/
}
console.log('WARNING: no ue subscriptions seeded !!');
return Promise.all(subscriptions);
}
2 changes: 1 addition & 1 deletion src/auth/guard/role.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export class RoleGuard implements CanActivate {
// If the user has one of the needed permissions, serve the request
for (const requiredType of requiredTypes) if (user.userType === requiredType) return true;
// The user has none of the required permissions, throw an error
throw new AppException(ERROR_CODE.FORBIDDEN_INVALID_ROLE, requiredTypes[0]);
throw new AppException(ERROR_CODE.FORBIDDEN_INVALID_ROLE, requiredTypes.join(','));
}
}
11 changes: 8 additions & 3 deletions src/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const enum ERROR_CODE {
NO_SUCH_ASSO = 4410,
NO_SUCH_UEOF = 4411,
NO_SUCH_APPLICATION = 4412,
NO_SUCH_UE_AT_SEMESTER = 4413,
ANNAL_ALREADY_UPLOADED = 4901,
RESOURCE_UNAVAILABLE = 4902,
RESOURCE_INVALID_TYPE = 4903,
Expand Down Expand Up @@ -205,11 +206,11 @@ export const ErrorData = Object.freeze({
},
[ERROR_CODE.FORBIDDEN_NOT_ENOUGH_API_PERMISSIONS]: {
message: 'Missing permission %',
httpCode: HttpStatus.FORBIDDEN,
httpCode: HttpStatus.UNAUTHORIZED,
},
[ERROR_CODE.FORBIDDEN_NOT_ENOUGH_USER_PERMISSIONS]: {
message: 'Missing permission % on user %',
httpCode: HttpStatus.FORBIDDEN,
httpCode: HttpStatus.UNAUTHORIZED,
},
[ERROR_CODE.NO_TOKEN]: {
message: 'No token provided',
Expand All @@ -224,7 +225,7 @@ export const ErrorData = Object.freeze({
httpCode: HttpStatus.UNAUTHORIZED,
},
[ERROR_CODE.FORBIDDEN_INVALID_ROLE]: {
message: 'Role % is required to access this resource',
message: 'One of the following roles is required to access this resource: %',
httpCode: HttpStatus.UNAUTHORIZED,
},
[ERROR_CODE.INVALID_CAS_TICKET]: {
Expand Down Expand Up @@ -331,6 +332,10 @@ export const ErrorData = Object.freeze({
message: 'The application % does not exist',
httpCode: HttpStatus.NOT_FOUND,
},
[ERROR_CODE.NO_SUCH_UE_AT_SEMESTER]: {
message: 'UE % does not exist for semester %',
httpCode: HttpStatus.NOT_FOUND,
},
[ERROR_CODE.ANNAL_ALREADY_UPLOADED]: {
message: 'A file has alreay been uploaded for this annal',
httpCode: HttpStatus.CONFLICT,
Expand Down
56 changes: 32 additions & 24 deletions src/ue/annals/annals.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { UeService } from '../ue.service';
import { Response as ExpressResponse } from 'express';
import { UUIDParam } from '../../app.pipe';
import { GetUser, RequireUserType } from '../../auth/decorator';
import { GetUser, RequireApiPermission } from '../../auth/decorator';
import { AppException, ERROR_CODE } from '../../exceptions';
import { FileSize, MulterWithMime, UploadRoute, UserFile } from '../../upload.interceptor';
import { CommentStatus } from '../comments/interfaces/comment.interface';
Expand All @@ -26,7 +26,7 @@
constructor(readonly annalsService: AnnalsService, readonly ueService: UeService) {}

@Get()
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_SEE_ANNALS')
@ApiOperation({ description: 'Get the list of annals of a UE.' })
@ApiOkResponse({ type: UeAnnalResDto, isArray: true })
@ApiAppErrorResponse(ERROR_CODE.NO_SUCH_UE, 'Thrown when there is no UE with code `ueCode`.')
Expand All @@ -36,14 +36,14 @@
@GetPermissions() permissions: PermissionManager,
): Promise<UeAnnalResDto[]> {
if (!(await this.ueService.doesUeExist(ueCode))) throw new AppException(ERROR_CODE.NO_SUCH_UE, ueCode);
return this.annalsService.getUeAnnalsList(user, ueCode, permissions.can(Permission.API_MODERATE_ANNAL));
return this.annalsService.getUeAnnalsList(user, ueCode, permissions.can(Permission.API_MODERATE_ANNALS));
}

@Post()
@RequireUserType('STUDENT')
@RequireApiPermission('API_UPLOAD_ANNALS')
@ApiOperation({
description:
'Create an annal. User must have done the UE, or have the permission `annalUploader`. Metadata of the annal will be created, but the file will not actually exist. To upload the file, see `PUT /v1/ue/annals/:annalId`.',
'Create an annal. User must have done the UE, or have the permission `API_MODERATE_ANNALS`. Metadata of the annal will be created, but the file will not actually exist. To upload the file, see `PUT /v1/ue/annals/:annalId`.',
})
@ApiOkResponse({ type: UeAnnalResDto })
@ApiAppErrorResponse(ERROR_CODE.NO_SUCH_UE, 'Thrown when there is no UE with code `ueCode`.')
Expand All @@ -53,28 +53,30 @@
)
@ApiAppErrorResponse(
ERROR_CODE.NOT_DONE_UE_IN_SEMESTER,
'User has not done the UE and is not an `annalUploader`, and thus cannot upload an annal for this UE.',
'User has not done the UE and is not an `API_MODERATE_ANNALS`, and thus cannot upload an annal for this UE.',
)
async createUeAnnal(
@Body() { ueCode, semester, typeId, ueof }: CreateAnnalReqDto,
@GetUser() user: User,
@GetPermissions() permissions: PermissionManager,
): Promise<UeAnnalResDto> {
if (ueof && permissions.can(Permission.API_UPLOAD_ANNAL))
throw new AppException(ERROR_CODE.FORBIDDEN_NOT_ENOUGH_API_PERMISSIONS, Permission.API_UPLOAD_ANNAL);
if (ueof && !permissions.can(Permission.API_MODERATE_ANNALS))
throw new AppException(ERROR_CODE.FORBIDDEN_NOT_ENOUGH_API_PERMISSIONS, Permission.API_MODERATE_ANNALS);

Check warning on line 64 in src/ue/annals/annals.controller.ts

View check run for this annotation

Codecov / codecov/patch

src/ue/annals/annals.controller.ts#L64

Added line #L64 was not covered by tests
if (!ueof && !(await this.ueService.doesUeExist(ueCode))) throw new AppException(ERROR_CODE.NO_SUCH_UE, ueCode);
if (ueof && !(await this.ueService.doesUeofExist(ueof))) throw new AppException(ERROR_CODE.NO_SUCH_UEOF, ueof);
if (!(await this.annalsService.doesAnnalTypeExist(typeId))) throw new AppException(ERROR_CODE.NO_SUCH_ANNAL_TYPE);
if (
!(await this.ueService.hasUserAttended(ueCode, user.id, semester)) &&
!permissions.can(Permission.API_UPLOAD_ANNAL)
!permissions.can(Permission.API_MODERATE_ANNALS)
)
throw new AppException(ERROR_CODE.NOT_DONE_UE_IN_SEMESTER, ueCode, semester);
if (!(await this.ueService.didUeHappenAtSemester(ueCode, semester)))
throw new AppException(ERROR_CODE.NO_SUCH_UE_AT_SEMESTER, ueCode, semester);

Check warning on line 74 in src/ue/annals/annals.controller.ts

View check run for this annotation

Codecov / codecov/patch

src/ue/annals/annals.controller.ts#L74

Added line #L74 was not covered by tests
return this.annalsService.createAnnalFile(user, { ueCode, semester, typeId, ueof });
}

@Get('metadata')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_SEE_ANNALS')
@ApiOperation({
description:
'Get generic information about annals for a particular UE. User must have already done this UE, or be an `annalUploader`.',
Expand All @@ -92,13 +94,13 @@
@GetPermissions() permissions: PermissionManager,
): Promise<UeAnnalMetadataResDto> {
if (!(await this.ueService.doesUeExist(ueCode))) throw new AppException(ERROR_CODE.NO_SUCH_UE, ueCode);
if (!(await this.ueService.hasUserAttended(ueCode, user.id)) && !permissions.can(Permission.API_UPLOAD_ANNAL))
if (!(await this.ueService.hasUserAttended(ueCode, user.id)) && !permissions.can(Permission.API_UPLOAD_ANNALS))
throw new AppException(ERROR_CODE.NOT_ALREADY_DONE_UE);
return this.annalsService.getUeAnnalMetadata(user, ueCode, permissions.can(Permission.API_UPLOAD_ANNAL));
return this.annalsService.getUeAnnalMetadata(user, ueCode, permissions.can(Permission.API_UPLOAD_ANNALS));
}

@Put(':annalId')
@RequireUserType('STUDENT')
@RequireApiPermission('API_UPLOAD_ANNALS')
@UploadRoute('file')
@ApiOperation({
description:
Expand All @@ -125,15 +127,15 @@
if (!(await this.annalsService.isUeAnnalSender(user.id, annalId)))
throw new AppException(ERROR_CODE.NOT_ANNAL_SENDER);
if (
(await this.annalsService.getUeAnnal(annalId, user.id, permissions.can(Permission.API_MODERATE_ANNAL))).status !==
CommentStatus.PROCESSING
(await this.annalsService.getUeAnnal(annalId, user.id, permissions.can(Permission.API_MODERATE_ANNALS)))
.status !== CommentStatus.PROCESSING
)
throw new AppException(ERROR_CODE.ANNAL_ALREADY_UPLOADED);
return this.annalsService.uploadAnnalFile(await file, annalId, rotate);
}

@Get(':annalId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_SEE_ANNALS')
@ApiOperation({ description: 'Get the file linked to a specific annal.' })
@ApiOkResponse({ description: 'The file is sent back.' })
@ApiAppErrorResponse(
Expand All @@ -146,12 +148,14 @@
@Response() response: ExpressResponse,
@GetPermissions() permissions: PermissionManager,
) {
if (!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNAL))))
if (
!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNALS)))
)
throw new AppException(ERROR_CODE.NO_SUCH_ANNAL, annalId);
const annalFile = await this.annalsService.getUeAnnalFile(
annalId,
user.id,
permissions.can(Permission.API_MODERATE_ANNAL),
permissions.can(Permission.API_MODERATE_ANNALS),
);
if (!annalFile) throw new AppException(ERROR_CODE.NO_SUCH_ANNAL, annalId);
response.setHeader('Content-Type', 'application/pdf');
Expand All @@ -163,7 +167,7 @@
}

@Patch(':annalId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_UPLOAD_ANNALS')
@ApiOperation({
description:
'Modify the metadata of an annal. User must be the original sender of the annal, or be an `annalModerator`.',
Expand All @@ -177,18 +181,20 @@
@GetUser() user: User,
@GetPermissions() permissions: PermissionManager,
): Promise<UeAnnalResDto> {
if (!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNAL))))
if (
!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNALS)))
)
throw new AppException(ERROR_CODE.NO_SUCH_ANNAL, annalId);
if (
!(await this.annalsService.isUeAnnalSender(user.id, annalId)) &&
!permissions.can(Permission.API_MODERATE_ANNAL)
!permissions.can(Permission.API_MODERATE_ANNALS)
)
throw new AppException(ERROR_CODE.NOT_ANNAL_SENDER);
return this.annalsService.updateAnnalMetadata(annalId, body);
}

@Delete(':annalId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_UPLOAD_ANNALS')
@ApiOperation({
description:
'Delete an annal. The file attached to the annal will not actually be deleted. User must be the original sender of the annal, or be an `annalModerator`.',
Expand All @@ -201,11 +207,13 @@
@GetUser() user: User,
@GetPermissions() permissions: PermissionManager,
): Promise<UeAnnalResDto> {
if (!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNAL))))
if (
!(await this.annalsService.isAnnalAccessible(user.id, annalId, permissions.can(Permission.API_MODERATE_ANNALS)))
)
throw new AppException(ERROR_CODE.NO_SUCH_ANNAL, annalId);
if (
!(await this.annalsService.isUeAnnalSender(user.id, annalId)) &&
!permissions.can(Permission.API_MODERATE_ANNAL)
!permissions.can(Permission.API_MODERATE_ANNALS)
)
throw new AppException(ERROR_CODE.NOT_ANNAL_SENDER);
return this.annalsService.deleteAnnal(annalId);
Expand Down
2 changes: 1 addition & 1 deletion src/ue/annals/annals.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class AnnalsService {
},
ueof: {
connect: {
code: subscription.ueofCode ?? params.ueof,
code: subscription?.ueofCode ?? params.ueof,
},
},
},
Expand Down
23 changes: 11 additions & 12 deletions src/ue/comments/comments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Patch, Post, Query } from '@nestjs/common';
import { UUIDParam } from '../../app.pipe';
import { GetUser, RequireApiPermission, RequireUserType } from '../../auth/decorator';
import { GetUser, RequireApiPermission } from '../../auth/decorator';
import { AppException, ERROR_CODE } from '../../exceptions';
import UeCommentPostReqDto from './dto/req/ue-comment-post-req.dto';
import CommentReplyReqDto from './dto/req/ue-comment-reply-req.dto';
Expand All @@ -24,15 +24,14 @@ export class CommentsController {
constructor(readonly commentsService: CommentsService, readonly ueService: UeService) {}

@Get()
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission(Permission.API_SEE_OPINIONS_UE)
@ApiOperation({ description: 'Get the comments of a UE. This route is paginated.' })
@ApiOkResponse({ type: paginatedResponseDto(UeCommentResDto) })
@ApiAppErrorResponse(
ERROR_CODE.NO_SUCH_UE,
'This error is sent back when there is no UE associated with the code provided.',
)
async getUEComments(
async getUeComments(
@GetUser() user: User,
@Query() dto: GetUeCommentsReqDto,
@GetPermissions() permissions: PermissionManager,
Expand All @@ -42,7 +41,7 @@ export class CommentsController {
}

@Post()
@RequireUserType('STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({ description: 'Send a comment for a UE.' })
@ApiOkResponse({ type: UeCommentResDto })
@ApiAppErrorResponse(
Expand All @@ -69,7 +68,7 @@ export class CommentsController {

// TODO : en vrai la route GET /ue/comments renvoie les mêmes infos nan ? :sweat_smile:
@Get(':commentId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_SEE_OPINIONS_UE')
@ApiOperation({ description: 'Fetch a specific comment.' })
@ApiOkResponse({ type: UeCommentResDto })
@ApiAppErrorResponse(ERROR_CODE.NO_SUCH_COMMENT, 'No comment is associated with the given commentId')
Expand All @@ -88,7 +87,7 @@ export class CommentsController {
}

@Patch(':commentId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({ description: 'Edit a comment.' })
@ApiOkResponse({ type: UeCommentResDto })
@ApiAppErrorResponse(ERROR_CODE.NO_SUCH_COMMENT, 'No comment has the given commentId.')
Expand All @@ -111,7 +110,7 @@ export class CommentsController {
}

@Delete(':commentId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({
description: 'Delete a comment. The user must be the author or have the `commentModerator` permission.',
})
Expand All @@ -135,7 +134,7 @@ export class CommentsController {
}

@Post(':commentId/upvote')
@RequireUserType('STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@HttpCode(HttpStatus.OK)
@ApiOperation({
description: 'Give an upvote for a comment. User cannot be the author. Each user can only upvote a comment once.',
Expand Down Expand Up @@ -164,7 +163,7 @@ export class CommentsController {
}

@Delete(':commentId/upvote')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@HttpCode(HttpStatus.OK)
@ApiOperation({ description: 'Remove an upvote for a comment. User' })
@ApiOkResponse({ type: UeCommentUpvoteResDto$False })
Expand All @@ -191,7 +190,7 @@ export class CommentsController {
}

@Post(':commentId/reply')
@RequireUserType('STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({ description: 'Reply to a comment.' })
@ApiOkResponse({ type: UeCommentReplyResDto })
@ApiAppErrorResponse(ERROR_CODE.NO_SUCH_COMMENT, 'There is no comment with the provided commentId.')
Expand All @@ -208,7 +207,7 @@ export class CommentsController {
}

@Patch('reply/:replyId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({
description:
'Edit a reply to a comment. The user must be the author of the reply or have the `commentModerator` permission.',
Expand All @@ -232,7 +231,7 @@ export class CommentsController {
}

@Delete('reply/:replyId')
@RequireUserType('STUDENT', 'FORMER_STUDENT')
@RequireApiPermission('API_GIVE_OPINIONS_UE')
@ApiOperation({
description:
'Delete a reply to a comment. The user must be the author of the reply or have the `commentModerator` permission.',
Expand Down
Loading
Loading