From 3ec1ce9bba81579f2ea7efb1100e173b3be98aa0 Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:52:28 +0900 Subject: [PATCH 01/11] comment out the prisima config about logging the sql --- src/settings.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 127e3a5..2fd8af0 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -58,10 +58,10 @@ const getPrismaConfig = (): Prisma.PrismaClientOptions => { }, errorFormat: 'pretty', log: [ - { - emit: 'event', - level: 'query', - }, + // { + // emit: 'event', + // level: 'query', + // }, { emit: 'stdout', level: 'error', @@ -70,10 +70,10 @@ const getPrismaConfig = (): Prisma.PrismaClientOptions => { emit: 'stdout', level: 'info', }, - { - emit: 'stdout', - level: 'warn', - }, + // { + // emit: 'stdout', + // level: 'warn', + // }, ], }; }; From 994cf5b0efc4c2734703a519fad2a92d73b1db8d Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 14 Sep 2024 00:58:23 +0900 Subject: [PATCH 02/11] Issue/136/planner update delete (#138) * Add: implement patch planner and refactor post planner * implement delete Planner * test simple cases and pass * reflect review --- src/common/entities/EPlanners.ts | 11 +- src/common/interfaces/IPlanner.ts | 28 ++ src/common/pipe/planner.pipe.ts | 29 ++ src/modules/planners/planners.controller.ts | 39 +- src/modules/planners/planners.service.ts | 197 ++++++--- src/modules/semesters/semesters.controller.ts | 2 + src/modules/tracks/tracks.controller.ts | 2 + src/prisma/repositories/planner.repository.ts | 408 +++++++++++++----- src/prisma/repositories/user.repository.ts | 16 + 9 files changed, 555 insertions(+), 177 deletions(-) create mode 100644 src/common/pipe/planner.pipe.ts diff --git a/src/common/entities/EPlanners.ts b/src/common/entities/EPlanners.ts index 6b091c0..8dde968 100644 --- a/src/common/entities/EPlanners.ts +++ b/src/common/entities/EPlanners.ts @@ -36,8 +36,8 @@ export namespace EPlanners { export namespace Taken { export const Basic = - Prisma.validator()({}); - export type Basic = Prisma.planner_futureplanneritemGetPayload< + Prisma.validator()({}); + export type Basic = Prisma.planner_takenplanneritemGetPayload< typeof Basic >; @@ -67,6 +67,13 @@ export namespace EPlanners { >; } export namespace Arbitrary { + export const CreateInput = + Prisma.validator()( + {}, + ); + export type CreateInput = + Prisma.planner_arbitraryplanneritemUncheckedCreateInput; + export const Basic = Prisma.validator()({}); export type Basic = Prisma.planner_arbitraryplanneritemGetPayload< diff --git a/src/common/interfaces/IPlanner.ts b/src/common/interfaces/IPlanner.ts index d3084b3..df643a2 100644 --- a/src/common/interfaces/IPlanner.ts +++ b/src/common/interfaces/IPlanner.ts @@ -233,4 +233,32 @@ export namespace IPlanner { @Type(() => Number) credit_au!: number; } + + export class UpdateBodyDto { + // @Todo start_year cannot be greater than end_year, also start_year cannot be greater than now + @IsInt() + @Type(() => Number) + start_year!: number; + + @IsInt() + @Type(() => Number) + end_year!: number; + + @IsInt() + @Type(() => Number) + general_track!: number; + + @IsInt() + @Type(() => Number) + major_track!: number; + + @Transform(({ value }) => (typeof value === 'number' ? [value] : value)) + @IsArray() + @IsInt({ each: true }) + additional_tracks!: number[]; + + @IsOptional() + @IsBoolean() + should_update_taken_semesters?: boolean; + } } diff --git a/src/common/pipe/planner.pipe.ts b/src/common/pipe/planner.pipe.ts new file mode 100644 index 0000000..02806f4 --- /dev/null +++ b/src/common/pipe/planner.pipe.ts @@ -0,0 +1,29 @@ +import { + BadRequestException, + Injectable, + PipeTransform, + ArgumentMetadata, +} from '@nestjs/common'; +import { PrismaService } from '@src/prisma/prisma.service'; + +@Injectable() +export class PlannerPipe implements PipeTransform { + constructor(private prismaService: PrismaService) {} + + async transform(value: any, metadata: ArgumentMetadata): Promise { + const plannerId = parseInt(value, 10); + if (isNaN(plannerId)) { + throw new BadRequestException('Invalid planner ID'); + } + + const planner = await this.prismaService.planner_planner.findUnique({ + where: { id: plannerId }, + }); + + if (!planner) { + throw new BadRequestException('Planner not found'); + } + + return plannerId; + } +} diff --git a/src/modules/planners/planners.controller.ts b/src/modules/planners/planners.controller.ts index d964590..3b55cfe 100644 --- a/src/modules/planners/planners.controller.ts +++ b/src/modules/planners/planners.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, + Delete, Get, Param, + Patch, Post, Query, UnauthorizedException, @@ -10,8 +12,10 @@ import { import { session_userprofile } from '@prisma/client'; import { GetUser } from 'src/common/decorators/get-user.decorator'; import { IPlanner } from 'src/common/interfaces/IPlanner'; -import { toJsonPlannerItem } from '../../common/interfaces/serializer/planner.item.serializer'; +import { toJsonPlannerItem } from '@src/common/interfaces/serializer/planner.item.serializer'; import { PlannersService } from './planners.service'; +import { toJsonPlanner } from '@src/common/interfaces/serializer/planner.serializer'; +import { PlannerPipe } from '@src/common/pipe/planner.pipe'; @Controller('api/users/:id/planners') export class PlannersController { @@ -30,6 +34,37 @@ export class PlannersController { return planners; } + @Patch(':plannerId') + async updatePlanner( + @Param('plannerId', PlannerPipe) plannerId: number, + @Body() planner: IPlanner.UpdateBodyDto, + @GetUser() user: session_userprofile, + ) { + if (planner.should_update_taken_semesters) { + await this.plannersService.updateTakenLectures( + user, + plannerId, + planner.start_year, + planner.end_year, + ); + } + const updatedPlanner = await this.plannersService.updatePlanner( + plannerId, + planner, + user, + ); + return toJsonPlanner(updatedPlanner); + } + + @Delete(':plannerId') + async deletePlanner( + @Param('plannerId', PlannerPipe) plannerId: number, + @GetUser() user: session_userprofile, + ) { + await this.plannersService.deletePlanner(plannerId); + return { message: 'Planner deleted' }; + } + @Post() async postPlanner( @Body() planner: IPlanner.CreateBodyDto, @@ -107,7 +142,7 @@ export class PlannersController { } @Post(':plannerId/update-item') - async updatePlanner( + async updatePlannerItem( @Param('id') userId: number, @Param('plannerId') plannerId: number, @GetUser() user: session_userprofile, diff --git a/src/modules/planners/planners.service.ts b/src/modules/planners/planners.service.ts index 922e63b..eef4ffd 100644 --- a/src/modules/planners/planners.service.ts +++ b/src/modules/planners/planners.service.ts @@ -19,6 +19,8 @@ import { PlannerRepository } from 'src/prisma/repositories/planner.repository'; import { EPlanners } from '../../common/entities/EPlanners'; import { CourseRepository } from './../../prisma/repositories/course.repository'; import { Transactional } from '@nestjs-cls/transactional'; +import { SemesterRepository } from '@src/prisma/repositories/semester.repository'; +import { UserRepository } from '@src/prisma/repositories/user.repository'; @Injectable() export class PlannersService { @@ -27,6 +29,8 @@ export class PlannersService { private readonly LectureRepository: LectureRepository, private readonly DepartmentRepository: DepartmentRepository, private readonly CourseRepository: CourseRepository, + private readonly SemesterRepository: SemesterRepository, + private readonly UserRepository: UserRepository, ) {} public async getPlannerByUser( @@ -71,50 +75,52 @@ export class PlannersService { const validEndYear = lecture.year <= body.end_year; return validStartYear && validEndYear; }); - valid_takenLectures.forEach(async (lecture) => { - await this.PlannerRepository.createTakenPlannerItem(planner, lecture); - }); + await this.PlannerRepository.createTakenPlannerItem( + planner.id, + valid_takenLectures.map((lecture) => { + return { lectureId: lecture.id, isExcluded: false }; + }), + ); } - body.taken_items_to_copy.forEach(async (item) => { - const targetItem = await this.PlannerRepository.getTakenPlannerItemById( - user, - item, + const takenTargetItems = + await this.PlannerRepository.getTakenPlannerItemByIds( + body.taken_items_to_copy, ); - if (!targetItem) { - return; // ignore non-existing items during copy - } - await this.PlannerRepository.createTakenPlannerItem( - planner, - targetItem.subject_lecture, - targetItem.is_excluded, - ); - }); + await this.PlannerRepository.createTakenPlannerItem( + planner.id, + takenTargetItems?.map((item) => { + return { lectureId: item.lecture_id, isExcluded: item.is_excluded }; + }) || [], + ); - body.future_items_to_copy.forEach(async (item) => { - const targetItem = await this.PlannerRepository.getFuturePlannerItemById( - user, - item, + const futureTargetItems = + await this.PlannerRepository.getFuturePlannerItemById( + body.future_items_to_copy, ); - if (!targetItem) { - return; // ignore non-existing items during copy - } - await this.PlannerRepository.createFuturePlannerItem(planner, targetItem); - }); + await this.PlannerRepository.createFuturePlannerItem( + planner.id, + futureTargetItems || [], + ); - body.arbitrary_items_to_copy.forEach(async (item) => { - const targetItem = - await this.PlannerRepository.getArbitraryPlannerItemById(user, item); - if (!targetItem) { - return; // ignore non-existing items during copy - } - await this.PlannerRepository.createArbitraryPlannerItem( - planner, - targetItem, + const targetItems = + await this.PlannerRepository.getArbitraryPlannerItemById( + body.arbitrary_items_to_copy, ); - }); + await this.PlannerRepository.createArbitraryPlannerItem( + planner.id, + targetItems || [], + ); - return toJsonPlanner(planner); + const newPlanner = await this.PlannerRepository.getPlannerById( + user, + planner.id, + ); + if (!newPlanner) { + throw new NotFoundException(); + } + + return toJsonPlanner(newPlanner); } @Transactional() @@ -131,17 +137,20 @@ export class PlannersService { body.department, ); + const data = { + planner_id: planner.id, + year: body.year, + semester: body.semester, + department_id: department?.id || null, + type: body.type, + type_en: body.type_en, + credit: body.credit, + credit_au: body.credit_au, + is_excluded: false, + }; + const arbitraryItem = - await this.PlannerRepository.createArbitraryPlannerItem(planner, { - year: body.year, - semester: body.semester, - department_id: department?.id || null, - type: body.type, - type_en: body.type_en, - credit: body.credit, - credit_au: body.credit_au, - is_excluded: false, - }); + await this.PlannerRepository.createArbitraryPlannerItem(planner.id, data); return toJsonArbitraryItem(arbitraryItem); } @@ -158,26 +167,26 @@ export class PlannersService { ); case 'FUTURE': { const futureItem = - await this.PlannerRepository.getFuturePlannerItemById( - user, + await this.PlannerRepository.getFuturePlannerItemById([ removeItem.item, - ); - if (!futureItem || futureItem.planner_id !== plannerId) { + ]); + if (!futureItem || futureItem[0].planner_id !== plannerId) { throw new NotFoundException(); } - await this.PlannerRepository.deleteFuturePlannerItem(futureItem); + await this.PlannerRepository.deleteFuturePlannerItem(futureItem[0]); break; } case 'ARBITRARY': { const arbitraryItem = - await this.PlannerRepository.getArbitraryPlannerItemById( - user, + await this.PlannerRepository.getArbitraryPlannerItemById([ removeItem.item, - ); - if (!arbitraryItem || arbitraryItem.planner_id !== plannerId) { + ]); + if (!arbitraryItem || arbitraryItem[0].planner_id !== plannerId) { throw new NotFoundException(); } - await this.PlannerRepository.deleteArbitraryPlannerItem(arbitraryItem); + await this.PlannerRepository.deleteArbitraryPlannerItem( + arbitraryItem[0], + ); break; } } @@ -282,4 +291,80 @@ export class PlannersService { updatedFields, ); } + + @Transactional() + async updateTakenLectures( + user: session_userprofile, + plannerId: number, + start_year: number, + end_year: number, + ) { + const notWritableSemester = + await this.SemesterRepository.getNotWritableSemester(); + const takenLectures = await this.UserRepository.getTakenLecturesByYear( + user.id, + start_year, + end_year, + notWritableSemester, + ); + const existedTakenItems = + await this.PlannerRepository.getTakenPlannerItemByLectures( + plannerId, + takenLectures.map((takenLecture) => takenLecture.lecture_id), + ); + const needToAddTakenLectures = takenLectures.filter( + (takenLecture) => + !existedTakenItems.find( + (existedTakenItem) => + existedTakenItem.lecture_id === takenLecture.lecture_id, + ), + ); + await this.PlannerRepository.createTakenPlannerItem( + plannerId, + needToAddTakenLectures.map((lecture) => { + return { lectureId: lecture.lecture_id, isExcluded: false }; + }), + ); + await this.PlannerRepository.deleteFuturePlannerItemsWithWhere( + plannerId, + start_year, + end_year, + ); + await this.PlannerRepository.deleteArbitraryPlannerItemsWithWhere( + plannerId, + start_year, + end_year, + ); + await this.PlannerRepository.deleteTakenPlannerItemsWithWhere(plannerId, { + year: { + lte: start_year, + gte: end_year, + }, + }); + } + + @Transactional() + async updatePlanner( + plannerId: number, + plannerDto: IPlanner.UpdateBodyDto, + user: session_userprofile, + ) { + const planner = this.PlannerRepository.getPlannerById(user, plannerId); + if (!planner) { + throw new NotFoundException(); + } + const updateFields = { + start_year: plannerDto.start_year, + end_year: plannerDto.end_year, + general_track_id: plannerDto.general_track, + major_track_id: plannerDto.major_track, + additional_track_ids: plannerDto.additional_tracks ?? [], + }; + return await this.PlannerRepository.updatePlanner(plannerId, updateFields); + } + + @Transactional() + async deletePlanner(plannerId: number) { + await this.PlannerRepository.deletePlanner(plannerId); + } } diff --git a/src/modules/semesters/semesters.controller.ts b/src/modules/semesters/semesters.controller.ts index 8ddaa0e..4e3dae9 100644 --- a/src/modules/semesters/semesters.controller.ts +++ b/src/modules/semesters/semesters.controller.ts @@ -2,12 +2,14 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ISemester } from 'src/common/interfaces/ISemester'; import { toJsonSemester } from '../../common/interfaces/serializer/semester.serializer'; import { SemestersService } from './semesters.service'; +import { Public } from '@src/common/decorators/skip-auth.decorator'; @Controller('api/semesters') export class SemestersController { constructor(private readonly semestersService: SemestersService) {} @Get() + @Public() async getSemesters(@Query() query: ISemester.QueryDto) { const semesters = await this.semestersService.getSemesters(query); return semesters.map((semester) => toJsonSemester(semester)); diff --git a/src/modules/tracks/tracks.controller.ts b/src/modules/tracks/tracks.controller.ts index bcde01c..3542acf 100644 --- a/src/modules/tracks/tracks.controller.ts +++ b/src/modules/tracks/tracks.controller.ts @@ -1,11 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { TracksService } from './tracks.service'; +import { Public } from '@src/common/decorators/skip-auth.decorator'; @Controller('/api/tracks') export class TracksController { constructor(private readonly tracksService: TracksService) {} @Get() + @Public() async getTracks() { return await this.tracksService.getAllTrack(); } diff --git a/src/prisma/repositories/planner.repository.ts b/src/prisma/repositories/planner.repository.ts index 011e25a..8d37dd1 100644 --- a/src/prisma/repositories/planner.repository.ts +++ b/src/prisma/repositories/planner.repository.ts @@ -1,11 +1,12 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { session_userprofile } from '@prisma/client'; -import { ELecture } from 'src/common/entities/ELecture'; +import { Prisma, session_userprofile } from '@prisma/client'; import { IPlanner } from 'src/common/interfaces/IPlanner'; import { orderFilter } from 'src/common/utils/search.utils'; import { EPlanners } from '../../common/entities/EPlanners'; import { PlannerItemType } from '../../common/interfaces/constants/planner'; import { PrismaService } from '../prisma.service'; +import CreateInput = EPlanners.EItems.Arbitrary.CreateInput; +import { Transactional } from '@nestjs-cls/transactional'; @Injectable() export class PlannerRepository { @@ -164,96 +165,81 @@ export class PlannerRepository { }); } - public async getTakenPlannerItemById( - user: session_userprofile, - id: number, - ): Promise { - const planner = await this.prisma.planner_planner.findMany({ - include: { - planner_takenplanneritem: { - ...EPlanners.EItems.Taken.Details, - }, - }, + public async getTakenPlannerItemByIds( + plannerItemIds: number[], + ): Promise { + const plannerItems = await this.prisma.planner_takenplanneritem.findMany({ + include: EPlanners.EItems.Taken.Details.include, where: { - user_id: user.id, - planner_takenplanneritem: { - some: { - id: id, - }, + id: { + in: plannerItemIds, }, }, }); - const candidates = planner.map((p) => p.planner_takenplanneritem).flat(); - return candidates.find((c) => c.id === id) ?? null; + return plannerItems.length > 0 ? plannerItems : null; } public async createTakenPlannerItem( - planner: EPlanners.Basic, - lecture: ELecture.Details, - isExcluded: boolean = false, - ) { - return await this.prisma.planner_takenplanneritem.create({ - data: { - planner_planner: { - connect: { - id: planner.id, - }, - }, - is_excluded: isExcluded, - subject_lecture: { - connect: { - id: lecture.id, - }, + plannerId: number, + lectures: { + lectureId: number; + isExcluded: boolean; + }[], + ): Promise { + const datas = lectures.map((lecture) => { + return { + planner_id: plannerId, + is_excluded: lecture.isExcluded, + lecture_id: lecture.lectureId, + }; + }); + await this.prisma.planner_takenplanneritem.createMany({ + data: datas, + }); + return this.prisma.planner_takenplanneritem.findMany({ + where: { + planner_id: plannerId, + lecture_id: { + in: lectures.map((lecture) => lecture.lectureId), }, }, }); } public async getFuturePlannerItemById( - user: session_userprofile, - id: number, - ): Promise { - const planner = await this.prisma.planner_planner.findMany({ - include: { - planner_futureplanneritem: { - ...EPlanners.EItems.Future.Extended, - }, - }, - where: { - user_id: user.id, - planner_futureplanneritem: { - some: { - id: id, + futureItemIds: number[], + ): Promise { + const futurePlannerItems = + await this.prisma.planner_futureplanneritem.findMany({ + include: EPlanners.EItems.Future.Extended.include, + where: { + id: { + in: futureItemIds, }, }, - }, - }); - const candidates = planner.map((p) => p.planner_futureplanneritem).flat(); - return candidates.find((c) => c.id === id) ?? null; + }); + return futurePlannerItems.length > 0 ? futurePlannerItems : null; } + @Transactional() public async createFuturePlannerItem( - planner: EPlanners.Basic, - target_item: EPlanners.EItems.Future.Extended, + plannerId: number, + targetItems: EPlanners.EItems.Future.Extended[], ) { - return await this.prisma.planner_futureplanneritem.create({ - data: { - planner_planner: { - connect: { - id: planner.id, - }, - }, - subject_course: { - connect: { - id: target_item.course_id, - }, - }, + const createDatas = targetItems.map((target_item) => { + return { + planner_id: plannerId, + course_id: target_item.course_id, is_excluded: target_item.is_excluded, year: target_item.year, semester: target_item.semester, - }, + }; + }); + return await this.prisma.planner_futureplanneritem.createMany({ + data: createDatas, }); } + @Transactional() public async deleteFuturePlannerItem( target_item: EPlanners.EItems.Future.Extended, ) { @@ -265,61 +251,67 @@ export class PlannerRepository { } public async getArbitraryPlannerItemById( - user: session_userprofile, - id: number, - ): Promise { - const planner = await this.prisma.planner_planner.findMany({ - include: { - planner_arbitraryplanneritem: { - ...EPlanners.EItems.Arbitrary.Extended, - }, - }, - where: { - user_id: user.id, - planner_arbitraryplanneritem: { - some: { - id: id, + arbitraryItemIds: number[], + ): Promise { + const arbitraryItems = + await this.prisma.planner_arbitraryplanneritem.findMany({ + include: EPlanners.EItems.Arbitrary.Extended.include, + where: { + id: { + in: arbitraryItemIds, }, }, - }, - }); - const candidates = planner - .map((p) => p.planner_arbitraryplanneritem) - .flat(); - return candidates.find((c) => c.id === id) ?? null; + }); + return arbitraryItems.length > 0 ? arbitraryItems : null; } public async createArbitraryPlannerItem( - planner: EPlanners.Basic, - target_item: Omit< - EPlanners.EItems.Arbitrary.Extended, - 'id' | 'planner_id' | 'subject_department' - >, - ): Promise { - return await this.prisma.planner_arbitraryplanneritem.create({ - data: { - planner_planner: { - connect: { - id: planner.id, - }, + plannerId: number, + target_items: CreateInput[], + ): Promise; + + public async createArbitraryPlannerItem( + plannerId: number, + target_items: CreateInput, + ): Promise; + + @Transactional() + public async createArbitraryPlannerItem< + T extends CreateInput | CreateInput[], + >(plannerId: number, target_items: T): Promise { + if (Array.isArray(target_items)) { + const createDatas = target_items.map((targetItem: CreateInput) => ({ + planner_id: plannerId, + department_id: targetItem.department_id, + is_excluded: targetItem.is_excluded, + year: targetItem.year, + semester: targetItem.semester, + type: targetItem.type, + type_en: targetItem.type_en, + credit: targetItem.credit, + credit_au: targetItem.credit_au, + })); + + return await this.prisma.planner_arbitraryplanneritem.createMany({ + data: createDatas, + }); + } else { + const targetItem = target_items as CreateInput; + return await this.prisma.planner_arbitraryplanneritem.create({ + include: EPlanners.EItems.Arbitrary.Extended.include, + data: { + planner_id: plannerId, + department_id: targetItem.department_id, + is_excluded: targetItem.is_excluded, + year: targetItem.year, + semester: targetItem.semester, + type: targetItem.type, + type_en: targetItem.type_en, + credit: targetItem.credit, + credit_au: targetItem.credit_au, }, - subject_department: target_item.department_id - ? { - connect: { - id: target_item.department_id, - }, - } - : undefined, - is_excluded: target_item.is_excluded, - year: target_item.year, - semester: target_item.semester, - type: target_item.type, - type_en: target_item.type_en, - credit: target_item.credit, - credit_au: target_item.credit_au, - }, - include: EPlanners.EItems.Arbitrary.Extended.include, - }); + }); + } } public async deleteArbitraryPlannerItem( @@ -367,6 +359,7 @@ export class PlannerRepository { }); } + @Transactional() async updatePlannerItem( item_type: string, item: number, @@ -412,4 +405,185 @@ export class PlannerRepository { throw new BadRequestException('Invalid Planner Item Type'); } } + + async getTakenPlannerItemByLecture( + plannerId: number, + lectureId: number, + ): Promise { + return await this.prisma.planner_takenplanneritem.findFirst({ + where: { + planner_id: plannerId, + lecture_id: lectureId, + }, + }); + } + + async getTakenPlannerItemByLectures( + plannerId: number, + lectureIds: number[], + ): Promise { + return await this.prisma.planner_takenplanneritem.findMany({ + where: { + planner_id: plannerId, + lecture_id: { + in: lectureIds, + }, + }, + }); + } + + @Transactional() + async updatePlanner( + plannerId: number, + updateFields: { + additional_track_ids: number[]; + start_year: number; + major_track_id: number; + general_track_id: number; + end_year: number; + }, + ): Promise { + const existedAdditonalTracks = + await this.prisma.planner_planner_additional_tracks.findMany({ + where: { + planner_id: plannerId, + }, + }); + + const disconnectAdditionalTracks = existedAdditonalTracks.filter( + (track) => + !updateFields.additional_track_ids.includes(track.additionaltrack_id), + ); + const connectAdditionalTrackIds = updateFields.additional_track_ids.filter( + (track) => + !existedAdditonalTracks + .map((t) => t.additionaltrack_id) + .includes(track), + ); + + await this.prisma.planner_planner_additional_tracks.deleteMany({ + where: { + planner_id: plannerId, + additionaltrack_id: { + in: disconnectAdditionalTracks.map( + (track) => track.additionaltrack_id, + ), + }, + }, + }); + + await this.prisma.planner_planner_additional_tracks.createMany({ + data: connectAdditionalTrackIds.map((track) => { + return { + planner_id: plannerId, + additionaltrack_id: track, + }; + }), + }); + + return this.prisma.planner_planner.update({ + where: { + id: plannerId, + }, + data: { + start_year: updateFields.start_year, + end_year: updateFields.end_year, + graduation_majortrack: { + connect: { + id: updateFields.major_track_id, + }, + }, + graduation_generaltrack: { + connect: { + id: updateFields.general_track_id, + }, + }, + }, + include: EPlanners.Details.include, + }); + } + + @Transactional() + async deleteFuturePlannerItemsWithWhere( + plannerId: number, + startYear: number, + endYear: number, + ) { + return await this.prisma.planner_futureplanneritem.deleteMany({ + where: { + year: { + lte: startYear, + gte: endYear, + }, + planner_id: plannerId, + }, + }); + } + + @Transactional() + async deleteArbitraryPlannerItemsWithWhere( + plannerId: number, + startYear: number, + endYear: number, + ) { + return await this.prisma.planner_arbitraryplanneritem.deleteMany({ + where: { + year: { + lte: startYear, + gte: endYear, + }, + planner_id: plannerId, + }, + }); + } + + @Transactional() + async deleteTakenPlannerItemsWithWhere( + plannerId: number, + where: Prisma.XOR< + Prisma.Subject_lectureRelationFilter, + Prisma.subject_lectureWhereInput + >, + ) { + return this.prisma.planner_takenplanneritem.deleteMany({ + where: { + planner_id: plannerId, + subject_lecture: { + ...where, + }, + }, + }); + } + + @Transactional() + async deletePlanner(plannerId: number) { + const planner = await this.prisma.planner_planner.findUniqueOrThrow({ + where: { + id: plannerId, + }, + }); + const userId = planner.user_id; + const planners = await this.prisma.planner_planner.findMany({ + where: { + user_id: userId, + }, + orderBy: { + arrange_order: 'asc', + }, + }); + const deletedPlanner = await this.prisma.planner_planner.delete({ + where: { + id: plannerId, + }, + }); + const needToUpdatePlanners = planners.filter( + (p) => p.arrange_order > deletedPlanner.arrange_order, + ); + await this.decrementOrders( + needToUpdatePlanners.map((p) => p.id), + planner.arrange_order + 1, + planners.length, + ); + return deletedPlanner; + } } diff --git a/src/prisma/repositories/user.repository.ts b/src/prisma/repositories/user.repository.ts index c058878..deac80d 100644 --- a/src/prisma/repositories/user.repository.ts +++ b/src/prisma/repositories/user.repository.ts @@ -63,4 +63,20 @@ export class UserRepository { }, }); } + + async getTakenLecturesByYear( + userId: number, + from: number, + to: number, + notWritableSemester?: subject_semester | null, + ) { + const reviewWritableLecture = await this.getTakenLectures( + userId, + notWritableSemester, + ); + return reviewWritableLecture.filter( + (takenLecture) => + takenLecture.lecture.year >= from && takenLecture.lecture.year <= to, + ); + } } From fd4aec5e2fc01d1886a9118e3194661daaf6b3fd Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sat, 14 Sep 2024 01:02:40 +0900 Subject: [PATCH 03/11] add cors seeting for production --- src/settings.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/settings.ts b/src/settings.ts index 2fd8af0..83c4a38 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -23,7 +23,12 @@ const getCorsConfig = () => { const { NODE_ENV } = process.env; if (NODE_ENV === 'prod') { return { - origin: 'https://otl.kaist.ac.kr:5173', + origin: [ + 'https://otl.kaist.ac.kr', + 'http://otl.kaist.ac.kr', + 'https://otl.sparcs.org', + 'http://otl.sparcs.org', + ], methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, preflightContinue: false, From 9fc34b2dc68f553ad3d29ecd832cca38fbdf7fa5 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 02:59:26 +0900 Subject: [PATCH 04/11] remove console.log --- package-lock.json | 3 ++- package.json | 2 +- src/modules/auth/auth.controller.ts | 1 - src/modules/auth/command/jwt.command.ts | 2 +- src/modules/auth/utils/sparcs-sso.ts | 5 ----- src/modules/courses/courses.controller.ts | 1 - src/settings.ts | 1 - 7 files changed, 4 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e33741..ab402d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "date-fns": "^3.6.0", - "dotenv": "^16.0.3", + "dotenv": "^16.4.5", "dotenv-cli": "^7.2.1", "enquirer": "^2.4.1", "express-session": "^1.17.3", @@ -4091,6 +4091,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 64b7c94..62a1ac9 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "date-fns": "^3.6.0", - "dotenv": "^16.0.3", + "dotenv": "^16.4.5", "dotenv-cli": "^7.2.1", "enquirer": "^2.4.1", "express-session": "^1.17.3", diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 48a8a95..6205d15 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -119,7 +119,6 @@ export class AuthController { res.clearCookie('accessToken', { path: '/', maxAge: 0, httpOnly: true }); res.clearCookie('refreshToken', { path: '/', maxAge: 0, httpOnly: true }); - console.log(logoutUrl); return res.redirect(logoutUrl); } diff --git a/src/modules/auth/command/jwt.command.ts b/src/modules/auth/command/jwt.command.ts index 2c70a5c..42d0384 100644 --- a/src/modules/auth/command/jwt.command.ts +++ b/src/modules/auth/command/jwt.command.ts @@ -77,7 +77,7 @@ export class JwtCommand implements AuthCommand { } return prevResult; } catch (e) { - console.error('error'); + console.error(e); return prevResult; } } diff --git a/src/modules/auth/utils/sparcs-sso.ts b/src/modules/auth/utils/sparcs-sso.ts index 0fc8b4f..7cdf450 100644 --- a/src/modules/auth/utils/sparcs-sso.ts +++ b/src/modules/auth/utils/sparcs-sso.ts @@ -113,7 +113,6 @@ export class Client { } const result = r.data; - console.log(result); result.kaist_info = result.kaist_info ? JSON.parse(result.kaist_info) : {}; @@ -136,13 +135,9 @@ export class Client { */ const state: string = crypto.randomBytes(10).toString('hex'); const params: Params = { client_id: this.client_id, state: state }; - console.log(this.client_id); - console.log(state); - console.log(this.URLS['token_require']); const url: string = `${this.URLS['token_require']}?${querystring.stringify( params, )}`; - console.log('url', url); return { url, state }; } diff --git a/src/modules/courses/courses.controller.ts b/src/modules/courses/courses.controller.ts index f754c7f..99a56b2 100644 --- a/src/modules/courses/courses.controller.ts +++ b/src/modules/courses/courses.controller.ts @@ -50,7 +50,6 @@ export class CourseController { @Query() query: LectureQueryDto, @Param('id', CourseIdPipe) id: number, ) { - console.log(query); return await this.coursesService.getLecturesByCourseId(query, id); } diff --git a/src/settings.ts b/src/settings.ts index 83c4a38..cabf34c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -35,7 +35,6 @@ const getCorsConfig = () => { optionsSuccessStatus: 204, }; } else if (NODE_ENV === 'dev') { - console.log('dev'); return { origin: 'http://3.37.146.183', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', From 5d5e4c06947d145bcdcf2d9da0ede4f0300e4574 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 03:45:49 +0900 Subject: [PATCH 05/11] resolve domain error when using sso --- src/bootstrap/bootstrap.ts | 2 +- src/modules/auth/auth.controller.ts | 5 ++++- src/modules/auth/auth.service.ts | 4 ++++ src/modules/auth/utils/sparcs-sso.ts | 17 +++++++++++++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index aa2454f..b9abfd3 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -54,7 +54,7 @@ async function bootstrap() { ); app.enableShutdownHooks(); - return app.listen(8000); + return app.listen(58000); } bootstrap() diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 6205d15..cdfb4fa 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -9,6 +9,7 @@ import settings from '../../settings'; import { UserService } from '../user/user.service'; import { AuthService } from './auth.service'; import { Client } from './utils/sparcs-sso'; +import { request } from 'http'; @Controller('session') export class AuthController { @@ -39,7 +40,9 @@ export class AuthController { return res.redirect(next ?? '/'); } req.session['next'] = next ?? '/'; - const { url, state } = this.ssoClient.get_login_params(); + const request_url = req.get('host') ?? 'otl.kaist.ac.kr'; + console.log(request_url); + const { url, state } = this.ssoClient.get_login_params(request_url); req.session['sso_state'] = state; if (social_login === '0') { return res.redirect(url + '&social_enabled=0&show_disabled_button=0'); diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 1fa03a9..a684951 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -82,7 +82,9 @@ export class AuthService { accessToken: token, path: '/', httpOnly: true, + sameSite: 'none' as const, maxAge: Number(jwtConfig.signOptions.expiresIn) * 1000, + secure: true, }; } @@ -100,7 +102,9 @@ export class AuthService { refreshToken: refreshToken, path: '/', httpOnly: true, + sameSite: 'none' as const, maxAge: Number(jwtConfig.signOptions.refreshExpiresIn) * 1000, + secure: true, }; } diff --git a/src/modules/auth/utils/sparcs-sso.ts b/src/modules/auth/utils/sparcs-sso.ts index 7cdf450..ce2019e 100644 --- a/src/modules/auth/utils/sparcs-sso.ts +++ b/src/modules/auth/utils/sparcs-sso.ts @@ -123,7 +123,7 @@ export class Client { } } - public get_login_params(): { url: string; state: string } { + public get_login_params(request_url: string): { url: string; state: string } { /* Get login parameters for SPARCS SSO login :returns: [url, state] where url is a url to redirect user, @@ -134,7 +134,20 @@ export class Client { * randomBytes에 10? 5? 둘중 어떤걸 넘겨줄지. gpt는 5라고 하고 파이썬은 token_hex(10) 10 같긴 한데....혹시나 해서 */ const state: string = crypto.randomBytes(10).toString('hex'); - const params: Params = { client_id: this.client_id, state: state }; + const allowedPreferredUris: { [key: string]: string } = { + 'otl.sparcs.org': 'https://otl.sparcs.org/session/login/callback/', + 'otl.kaist.ac.kr': 'https://otl.kaist.ac.kr/session/login/callback/', + 'otl-stage.sparcsandbox.com': + 'https://otl-stage.sparcsandbox.com/session/login/callback/', + }; + const preferred_url = + allowedPreferredUris[request_url] || + 'https://otl.sparcs.org/session/login/callback/'; + const params: Params = { + client_id: this.client_id, + state: state, + preferred_url: preferred_url, + }; const url: string = `${this.URLS['token_require']}?${querystring.stringify( params, )}`; From 7fcb38d08052a6cfd29ae439af700639eafd6fa4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 05:20:59 +0900 Subject: [PATCH 06/11] try to resolve app error but fail --- src/bootstrap/bootstrap.ts | 2 +- src/common/interfaces/serializer/course.serializer.ts | 4 ++-- src/common/interfaces/serializer/lecture.serializer.ts | 2 +- src/common/interfaces/serializer/professor.serializer.ts | 2 +- src/prisma/middleware/prisma.timetablelecture.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index b9abfd3..aa2454f 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -54,7 +54,7 @@ async function bootstrap() { ); app.enableShutdownHooks(); - return app.listen(58000); + return app.listen(8000); } bootstrap() diff --git a/src/common/interfaces/serializer/course.serializer.ts b/src/common/interfaces/serializer/course.serializer.ts index 2adfdc5..a2fcb6a 100644 --- a/src/common/interfaces/serializer/course.serializer.ts +++ b/src/common/interfaces/serializer/course.serializer.ts @@ -23,7 +23,7 @@ export function toJsonFeedBasic(course: subject_course): ICourse.FeedBasic { grade_sum: course.grade_sum, load_sum: course.load_sum, speech_sum: course.speech_sum, - review_total_weight: course.review_total_weight, + review_total_weight: course.review_total_weight + 0.000001, grade: course.grade, load: course.load, speech: course.speech, @@ -53,7 +53,7 @@ export function toJsonCourseBasic( title: course.title, title_en: course.title_en, summary: course.summury, // Todo: fix summury typo in db. - review_total_weight: course.review_total_weight, + review_total_weight: course.review_total_weight + 0.000001, credit: lecture.credit ?? 0, credit_au: lecture.credit_au ?? 0, num_classes: lecture.num_classes ?? 0, diff --git a/src/common/interfaces/serializer/lecture.serializer.ts b/src/common/interfaces/serializer/lecture.serializer.ts index 3988d2a..e84e8bf 100644 --- a/src/common/interfaces/serializer/lecture.serializer.ts +++ b/src/common/interfaces/serializer/lecture.serializer.ts @@ -37,7 +37,7 @@ export function toJsonLectureBasic(lecture: ELecture.Extended): ILecture.Basic { common_title_en: lecture.common_title_en ?? '', class_title: lecture.class_title ?? '', class_title_en: lecture.class_title_en ?? '', - review_total_weight: lecture.review_total_weight, + review_total_weight: lecture.review_total_weight + 0.000001, professors: toJsonProfessors(ordered_professors), }; } diff --git a/src/common/interfaces/serializer/professor.serializer.ts b/src/common/interfaces/serializer/professor.serializer.ts index dba2d75..283d078 100644 --- a/src/common/interfaces/serializer/professor.serializer.ts +++ b/src/common/interfaces/serializer/professor.serializer.ts @@ -10,7 +10,7 @@ export const toJsonProfessors = ( name: professor.professor_name, name_en: professor.professor_name_en ?? '', professor_id: professor.professor_id, - review_total_weight: professor.review_total_weight, + review_total_weight: professor.review_total_weight + 0.000001, }; }); diff --git a/src/prisma/middleware/prisma.timetablelecture.ts b/src/prisma/middleware/prisma.timetablelecture.ts index bd52723..da8bd93 100644 --- a/src/prisma/middleware/prisma.timetablelecture.ts +++ b/src/prisma/middleware/prisma.timetablelecture.ts @@ -54,8 +54,8 @@ export class TimetableLectureMiddleware } throw new Error("can't find user"); } else if (operations === 'delete') { - const timetableId = args?.where?.timetable_id; // todo : args에 where이 들거가나? - const lectureId = args?.where?.lecture_id; + const timetableId = args?.where?.timetable_id_lecture_id?.timetable_id; // todo : args에 where이 들거가나? + const lectureId = args?.where?.timetable_id_lecture_id?.lecture_id; const userId: number | undefined = ( await this.prisma.timetable_timetable.findUnique({ where: { id: timetableId }, From 87665d5c7867432363d69766d3481d3faffadf34 Mon Sep 17 00:00:00 2001 From: larrykwon Date: Sat, 14 Sep 2024 14:35:47 +0900 Subject: [PATCH 07/11] resolve app error --- package-lock.json | 114 ++++++++++++++++++ package.json | 2 + src/bootstrap/bootstrap.ts | 2 + .../serializer/course.serializer.ts | 18 +-- .../serializer/lecture.serializer.ts | 6 +- .../serializer/review.serializer.ts | 10 +- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab402d0..fde61e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "csurf": "^1.10.0", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "dotenv-cli": "^7.2.1", @@ -50,6 +51,7 @@ "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", "@types/bcrypt": "^5.0.0", + "@types/csurf": "^1.11.5", "@types/express": "^4.17.13", "@types/inquirer": "^9.0.7", "@types/jest": "28.1.4", @@ -2068,6 +2070,16 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/csurf": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", + "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", @@ -3903,6 +3915,93 @@ "node": ">= 8" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.10.0.tgz", + "integrity": "sha512-fh725p0R83wA5JukCik5hdEko/LizW/Vl7pkKDa1WJUVCosg141mqaAWCScB+nkEaRMFMGbutHMOr6oBNc/j9A==", + "license": "MIT", + "dependencies": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -8373,6 +8472,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -9430,6 +9535,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", diff --git a/package.json b/package.json index 62a1ac9..e137f2e 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "csurf": "^1.10.0", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "dotenv-cli": "^7.2.1", @@ -83,6 +84,7 @@ "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", "@types/bcrypt": "^5.0.0", + "@types/csurf": "^1.11.5", "@types/express": "^4.17.13", "@types/inquirer": "^9.0.7", "@types/jest": "28.1.4", diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index aa2454f..a6086da 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -5,6 +5,7 @@ import session from 'express-session'; import { AppModule } from '../app.module'; import settings from '../settings'; import morgan = require('morgan'); +import csrf from 'csurf'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -29,6 +30,7 @@ async function bootstrap() { }), ); app.use(cookieParser()); + app.use('/', csrf({ cookie: { key: 'csrftoken' } })); // Logs requests // app.use( // morgan(':method :url OS/:req[client-os] Ver/:req[client-api-version]', { diff --git a/src/common/interfaces/serializer/course.serializer.ts b/src/common/interfaces/serializer/course.serializer.ts index a2fcb6a..464574d 100644 --- a/src/common/interfaces/serializer/course.serializer.ts +++ b/src/common/interfaces/serializer/course.serializer.ts @@ -20,13 +20,13 @@ export function toJsonFeedBasic(course: subject_course): ICourse.FeedBasic { title: course.title, title_en: course.title_en, summary: course.summury, - grade_sum: course.grade_sum, - load_sum: course.load_sum, - speech_sum: course.speech_sum, + grade_sum: course.grade_sum + 0.00001, + load_sum: course.load_sum + 0.00001, + speech_sum: course.speech_sum + 0.000001, review_total_weight: course.review_total_weight + 0.000001, - grade: course.grade, - load: course.load, - speech: course.speech, + grade: course.grade + 0.000001, + load: course.load + 0.00001, + speech: course.speech + 0.00001, title_en_no_space: course.title_en_no_space, title_no_space: course.title_no_space, }; @@ -74,9 +74,9 @@ export function toJsonCourseDetail( related_courses_prior: [], related_courses_posterior: [], professors: professorSorted, - grade: course.grade, - load: course.load, - speech: course.speech, + grade: course.grade + 0.000001, + load: course.load + 0.000001, + speech: course.speech + 0.000001, }; } diff --git a/src/common/interfaces/serializer/lecture.serializer.ts b/src/common/interfaces/serializer/lecture.serializer.ts index e84e8bf..f55b988 100644 --- a/src/common/interfaces/serializer/lecture.serializer.ts +++ b/src/common/interfaces/serializer/lecture.serializer.ts @@ -50,9 +50,9 @@ export function toJsonLectureDetail( throw new Error("Lecture is not of type 'ELecture.Details'"); return Object.assign(basic, { - grade: lecture.grade, - load: lecture.load, - speech: lecture.speech, + grade: lecture.grade + 0.000001, + load: lecture.load + 0.000001, + speech: lecture.speech + 0.000001, classtimes: lecture.subject_classtime.map((classtime) => toJsonClasstime(classtime), ), diff --git a/src/common/interfaces/serializer/review.serializer.ts b/src/common/interfaces/serializer/review.serializer.ts index fc5934a..e5e092c 100644 --- a/src/common/interfaces/serializer/review.serializer.ts +++ b/src/common/interfaces/serializer/review.serializer.ts @@ -31,11 +31,11 @@ export const toJsonReview = ( content: review.is_deleted ? '관리자에 의해 삭제된 코멘트입니다.' : review.content, - like: review.like, - is_deleted: review.is_deleted, - grade: review.grade, - load: review.load, - speech: review.speech, + like: Math.round(review.like), + is_deleted: Math.round(review.is_deleted), + grade: Math.round(review.grade), + load: Math.round(review.load), + speech: Math.round(review.speech), userspecific_is_liked: isLiked, }; return result; From f5f19983cc10b063a0fdfb97894735082e67df09 Mon Sep 17 00:00:00 2001 From: larrykwon Date: Sun, 15 Sep 2024 02:47:39 +0900 Subject: [PATCH 08/11] add ignoremethods option into csrf --- src/bootstrap/bootstrap.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index a6086da..9d30e05 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -1,11 +1,11 @@ import { ValidationPipe, VersioningType } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; +import csrf from 'csurf'; import session from 'express-session'; import { AppModule } from '../app.module'; import settings from '../settings'; import morgan = require('morgan'); -import csrf from 'csurf'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -30,7 +30,21 @@ async function bootstrap() { }), ); app.use(cookieParser()); - app.use('/', csrf({ cookie: { key: 'csrftoken' } })); + app.use( + '/', + csrf({ + cookie: { key: 'csrftoken' }, + ignoreMethods: [ + 'GET', + 'HEAD', + 'OPTIONS', + 'DELETE', + 'PATCH', + 'PUT', + 'POST', + ], + }), + ); // Logs requests // app.use( // morgan(':method :url OS/:req[client-os] Ver/:req[client-api-version]', { From c2bd62487c267004c1a6c8e5d725b3a3a4524658 Mon Sep 17 00:00:00 2001 From: larrykwon Date: Sun, 15 Sep 2024 03:29:42 +0900 Subject: [PATCH 09/11] add delete cascade option to planners table --- src/prisma/schema.prisma | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 3e964e6..d6a4952 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -743,8 +743,8 @@ model planner_arbitraryplanneritem { credit_au Int department_id Int? planner_id Int - subject_department subject_department? @relation(fields: [department_id], references: [id], onDelete: Restrict, onUpdate: Restrict, map: "planner_arbitrarypla_department_id_0dc7ce25_fk_subject_d") - planner_planner planner_planner @relation(fields: [planner_id], references: [id], onUpdate: Restrict, map: "planner_arbitrarypla_planner_id_d6069d2c_fk_planner_p") + subject_department subject_department? @relation(fields: [department_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_arbitrarypla_department_id_0dc7ce25_fk_subject_d") + planner_planner planner_planner @relation(fields: [planner_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_arbitrarypla_planner_id_d6069d2c_fk_planner_p") @@index([department_id], map: "planner_arbitrarypla_department_id_0dc7ce25_fk_subject_d") @@index([planner_id], map: "planner_arbitrarypla_planner_id_d6069d2c_fk_planner_p") @@ -759,8 +759,8 @@ model planner_futureplanneritem { semester Int course_id Int planner_id Int - subject_course subject_course @relation(fields: [course_id], references: [id], onUpdate: Restrict, map: "planner_futureplanne_course_id_b1a06444_fk_subject_c") - planner_planner planner_planner @relation(fields: [planner_id], references: [id], onUpdate: Restrict, map: "planner_futureplanne_planner_id_dfd70193_fk_planner_p") + subject_course subject_course @relation(fields: [course_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_futureplanne_course_id_b1a06444_fk_subject_c") + planner_planner planner_planner @relation(fields: [planner_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_futureplanne_planner_id_dfd70193_fk_planner_p") @@index([course_id], map: "planner_futureplanne_course_id_b1a06444_fk_subject_c") @@index([planner_id], map: "planner_futureplanne_planner_id_dfd70193_fk_planner_p") @@ -796,8 +796,8 @@ model planner_planner_additional_tracks { id Int @id @default(autoincrement()) planner_id Int additionaltrack_id Int - graduation_additionaltrack graduation_additionaltrack @relation(fields: [additionaltrack_id], references: [id], onUpdate: Restrict, map: "planner_planner_addi_additionaltrack_id_c46b8c4e_fk_graduatio") - planner_planner planner_planner @relation(fields: [planner_id], references: [id], onUpdate: Restrict, map: "planner_planner_addi_planner_id_e439a309_fk_planner_p") + graduation_additionaltrack graduation_additionaltrack @relation(fields: [additionaltrack_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_planner_addi_additionaltrack_id_c46b8c4e_fk_graduatio") + planner_planner planner_planner @relation(fields: [planner_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_planner_addi_planner_id_e439a309_fk_planner_p") @@unique([planner_id, additionaltrack_id], map: "planner_planner_addition_planner_id_additionaltra_2298c5cd_uniq") @@index([additionaltrack_id], map: "planner_planner_addi_additionaltrack_id_c46b8c4e_fk_graduatio") @@ -808,8 +808,8 @@ model planner_takenplanneritem { is_excluded Boolean lecture_id Int planner_id Int - subject_lecture subject_lecture @relation(fields: [lecture_id], references: [id], onUpdate: Restrict, map: "planner_takenplanner_lecture_id_9b2d30d8_fk_subject_l") - planner_planner planner_planner @relation(fields: [planner_id], references: [id], onUpdate: Restrict, map: "planner_takenplanner_planner_id_b725ff83_fk_planner_p") + subject_lecture subject_lecture @relation(fields: [lecture_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_takenplanner_lecture_id_9b2d30d8_fk_subject_l") + planner_planner planner_planner @relation(fields: [planner_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "planner_takenplanner_planner_id_b725ff83_fk_planner_p") @@unique([planner_id, lecture_id], map: "planner_takenplanneritem_planner_id_lecture_id_4b39b432_uniq") @@index([lecture_id], map: "planner_takenplanner_lecture_id_9b2d30d8_fk_subject_l") From 4cd1d20b072ccfdef634a0076619714cfab6c210 Mon Sep 17 00:00:00 2001 From: LarryKwon <65128957+LarryKwon@users.noreply.github.com> Date: Sun, 15 Sep 2024 12:15:43 +0900 Subject: [PATCH 10/11] add localhost to preferred url --- src/modules/auth/auth.controller.ts | 2 +- src/modules/auth/utils/sparcs-sso.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index cdfb4fa..624f395 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -40,7 +40,7 @@ export class AuthController { return res.redirect(next ?? '/'); } req.session['next'] = next ?? '/'; - const request_url = req.get('host') ?? 'otl.kaist.ac.kr'; + const request_url = req.get('host') ?? 'localhost:8000'; console.log(request_url); const { url, state } = this.ssoClient.get_login_params(request_url); req.session['sso_state'] = state; diff --git a/src/modules/auth/utils/sparcs-sso.ts b/src/modules/auth/utils/sparcs-sso.ts index ce2019e..2ebe365 100644 --- a/src/modules/auth/utils/sparcs-sso.ts +++ b/src/modules/auth/utils/sparcs-sso.ts @@ -142,7 +142,7 @@ export class Client { }; const preferred_url = allowedPreferredUris[request_url] || - 'https://otl.sparcs.org/session/login/callback/'; + 'http:localhost:8000/session/login/callback/'; const params: Params = { client_id: this.client_id, state: state, From 2dbd5008e30ad420cdf5ff557b24c7a9c2326101 Mon Sep 17 00:00:00 2001 From: jihyeon Date: Wed, 18 Sep 2024 20:07:54 -0400 Subject: [PATCH 11/11] fix: search filter issue (#140) --- src/prisma/repositories/course.repository.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/prisma/repositories/course.repository.ts b/src/prisma/repositories/course.repository.ts index 29020a4..ab7736f 100644 --- a/src/prisma/repositories/course.repository.ts +++ b/src/prisma/repositories/course.repository.ts @@ -46,6 +46,9 @@ export class CourseRepository { 'AE', 'CH', 'TS', + 'BTM', + 'BCS', + 'SS', ]; public async getCourseById(id: number): Promise { @@ -300,10 +303,17 @@ export class CourseRepository { }, }, }; + + const old_code_filter = { + old_code: { + contains: keyword_space_removed, + }, + }; return { OR: [ title_filter, en_title_filter, + old_code_filter, department_name_filter, department_name_en_filter, professors_professor_name_filter,