diff --git a/src/alokasi-topik/alokasi-topik.controller.ts b/src/alokasi-topik/alokasi-topik.controller.ts index 6fa4e26ac50478a46780cab0a29e11ccda0d7035..7de2c763247d9e7461c75f01d084f9ae74d1fc0b 100644 --- a/src/alokasi-topik/alokasi-topik.controller.ts +++ b/src/alokasi-topik/alokasi-topik.controller.ts @@ -9,12 +9,15 @@ import { Post, Put, Query, + Req, UseGuards, } from "@nestjs/common"; import { ApiBearerAuth, ApiCookieAuth, + ApiCreatedResponse, ApiOkResponse, + ApiOperation, ApiTags, } from "@nestjs/swagger"; import { RoleEnum } from "src/entities/pengguna.entity"; @@ -24,7 +27,7 @@ import { Roles } from "src/middlewares/roles.decorator"; import { RolesGuard } from "src/middlewares/roles.guard"; import { CreateBulkTopikDto, - CreateRespDto, + TopikIdRespDto, CreateTopikDto, GetAllRespDto, OmittedTopik, @@ -34,6 +37,9 @@ import { createBulkRespDto, } from "./alokasi-topik.dto"; import { AlokasiTopikService } from "./alokasi-topik.service"; +import { Request } from "express"; +import { AuthDto } from "src/auth/auth.dto"; +import { HIGH_AUTHORITY_ROLES, isHighAuthority } from "src/helper/roles"; @ApiTags("Alokasi Topik") @ApiCookieAuth() @@ -46,53 +52,66 @@ export class AlokasiTopikController { private konfService: KonfigurasiService, ) {} - @ApiOkResponse({ type: CreateRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Create new topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiCreatedResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Post() - async create(@Body() createDto: CreateTopikDto) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); + async create( + @Body() createDto: CreateTopikDto, + @Req() req: Request, + ): Promise<TopikIdRespDto> { + const periode = await this.konfService.getPeriodeOrFail(); - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles) && createDto.idPengaju !== id) { + throw new BadRequestException("Pengaju ID harus sama dengan user ID"); + } return await this.alokasiTopikService.create({ ...createDto, periode }); } + @ApiOperation({ + summary: "Create multiple topik. Roles: S2_TIM_TESIS, ADMIN", + }) @ApiOkResponse({ type: createBulkRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @Roles(...HIGH_AUTHORITY_ROLES) @Post("/bulk") async createBulk(@Body() createDto: CreateBulkTopikDto) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); + const periode = await this.konfService.getPeriodeOrFail(); return await this.alokasiTopikService.createBulk(createDto, periode); } + @ApiOperation({ + summary: + "Get topik by ID. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING, S2_MAHASISWA", + }) @ApiOkResponse({ type: OmittedTopik }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING, RoleEnum.S2_MAHASISWA) @Get("/:id") async getById(@Param() params: TopikParamDto) { - const res = await this.alokasiTopikService.findById(params.id); + const periode = await this.konfService.getPeriodeOrFail(); + + const res = await this.alokasiTopikService.findById(params.id, periode); if (!res) throw new NotFoundException(); return res as OmittedTopik; } + @ApiOperation({ + summary: + "Get all topik. Roles: S2_TIM_TESIS, ADMIN, S2_MAHASISWA, S2_PEMBIMBING", + }) @ApiOkResponse({ type: GetAllRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN, RoleEnum.S2_MAHASISWA) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_MAHASISWA, RoleEnum.S2_PEMBIMBING) @Get() async getAll( @Query() query: TopikQueryDto, ) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); + const periode = await this.konfService.getPeriodeOrFail(); return await this.alokasiTopikService.findAllCreatedByPembimbing({ page: query.page || 1, @@ -101,22 +120,75 @@ export class AlokasiTopikController { }); } - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Update topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiOkResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Put("/:id") async update( @Param() params: TopikParamDto, @Body() updateDto: UpdateTopikDto, - ) { - const res = await this.alokasiTopikService.update(params.id, updateDto); - if (!res.affected) throw new NotFoundException(); - return res; + @Req() req: Request, + ): Promise<TopikIdRespDto> { + const periode = await this.konfService.getPeriodeOrFail(); + + let idPengaju = undefined; + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles)) { + if (updateDto.idPengaju !== id) { + throw new BadRequestException("Pengaju ID harus sama dengan user ID"); + } + idPengaju = id; + } + + const res = await this.alokasiTopikService.update( + params.id, + updateDto, + periode, + idPengaju, + ); + if (!res.affected) + throw new NotFoundException( + "Topik tidak ditemukan di antara topik yang dapat Anda akses", + ); + + const resp: TopikIdRespDto = { id: params.id }; + + return resp; } - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Delete topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiOkResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Delete("/:id") - async delete(@Param() params: TopikParamDto) { - const res = await this.alokasiTopikService.remove(params.id); - if (!res.affected) throw new NotFoundException(); - return res; + async delete( + @Param() params: TopikParamDto, + @Req() req: Request, + ): Promise<TopikIdRespDto> { + const periode = await this.konfService.getPeriodeOrFail(); + + let idPengaju = undefined; + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles)) { + idPengaju = id; + } + + const res = await this.alokasiTopikService.remove( + params.id, + periode, + idPengaju, + ); + if (!res.affected) + throw new NotFoundException( + "Topik tidak ditemukan di antara topik yang dapat Anda akses", + ); + + const resp: TopikIdRespDto = { id: params.id }; + return resp; } } diff --git a/src/alokasi-topik/alokasi-topik.dto.ts b/src/alokasi-topik/alokasi-topik.dto.ts index 93e7dd82db19970390d4cef921e1a774ed2ffabc..27973c53bc5de171f0766565900b649a54d027be 100644 --- a/src/alokasi-topik/alokasi-topik.dto.ts +++ b/src/alokasi-topik/alokasi-topik.dto.ts @@ -81,7 +81,7 @@ export class createBulkRespDto { ids: string[]; } -export class CreateRespDto { +export class TopikIdRespDto { @ApiProperty() id: string; } diff --git a/src/alokasi-topik/alokasi-topik.service.ts b/src/alokasi-topik/alokasi-topik.service.ts index b9056ab744f4cb42d6fc970e629cf3f189afcaec..2bc2ecb800445a77649f4130804cb14a32bc744f 100644 --- a/src/alokasi-topik/alokasi-topik.service.ts +++ b/src/alokasi-topik/alokasi-topik.service.ts @@ -5,7 +5,7 @@ import { Topik } from "src/entities/topik.entity"; import { ArrayContains, Like, Repository } from "typeorm"; import { CreateBulkTopikDto, - CreateRespDto, + TopikIdRespDto, CreateTopikDto, GetAllRespDto, UpdateTopikDto, @@ -18,7 +18,7 @@ export class AlokasiTopikService { async create( createDto: CreateTopikDto & { periode: string }, - ): Promise<CreateRespDto> { + ): Promise<TopikIdRespDto> { const ids = (await this.topikRepo.insert(createDto)).identifiers; return { id: ids[0].id }; @@ -37,8 +37,7 @@ export class AlokasiTopikService { return { ids: ids.map(({ id }) => id) }; } - async findById(id: string) { - // not periode-protected + async findById(id: string, periode: string) { return await this.topikRepo.findOne({ select: { id: true, @@ -54,6 +53,7 @@ export class AlokasiTopikService { }, where: { id, + periode, }, relations: { pengaju: true, @@ -142,13 +142,18 @@ export class AlokasiTopikService { } } - async update(id: string, updateDto: UpdateTopikDto) { - // not periode-protected - return await this.topikRepo.update(id, updateDto); + async update( + id: string, + updateDto: UpdateTopikDto, + periode: string, + idPengaju?: string, + ) { + const findOpt = idPengaju ? { id, periode, idPengaju } : { id, periode }; + return await this.topikRepo.update(findOpt, updateDto); } - async remove(id: string) { - // not periode-protected - return await this.topikRepo.delete({ id }); // TODO: manage relation cascading option + async remove(id: string, periode: string, idPengaju?: string) { + const findOpt = idPengaju ? { id, periode, idPengaju } : { id, periode }; + return await this.topikRepo.delete(findOpt); } } diff --git a/src/helper/roles.ts b/src/helper/roles.ts new file mode 100644 index 0000000000000000000000000000000000000000..690f29dd520df4c348a4520cfc75aee48540dca5 --- /dev/null +++ b/src/helper/roles.ts @@ -0,0 +1,7 @@ +import { RoleEnum } from "src/entities/pengguna.entity"; + +export const HIGH_AUTHORITY_ROLES = [RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS]; + +export function isHighAuthority(roles: RoleEnum[]) { + return roles.some((role) => HIGH_AUTHORITY_ROLES.includes(role)); +} diff --git a/src/helper/validation.ts b/src/helper/validation.ts deleted file mode 100644 index cfc65eb722c4859bd1569749bccb0e8785721b77..0000000000000000000000000000000000000000 --- a/src/helper/validation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NotFoundException } from "@nestjs/common"; -import { validate as uuidValidate } from "uuid"; - -interface ID { - id: string; - object: string; -} - -export function validateId(items: ID[]) { - for (const item of items) { - const isValidUUID = uuidValidate(item.id); - - if (!isValidUUID) { - throw new NotFoundException(`${item.object} not found.`); - } - } -}