From 36412c9f580d9794dd72804f0417b3d6f55ca5cc Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 3 Apr 2023 13:04:32 -0600 Subject: [PATCH] Add timer REST stuff --- server/.eslintrc.js | 9 +++ .../migration.sql | 39 ++++++++++ .../migration.sql | 2 + .../migration.sql | 2 + server/prisma/schema.prisma | 28 ++++--- server/src/auth/auth.controller.ts | 16 +--- server/src/auth/auth.module.ts | 2 +- server/src/dto/dtos.ts | 16 ++++ server/src/timer/timer.controller.ts | 77 ++++++++++++++++++- server/src/timer/timer.module.ts | 5 +- server/src/timer/timer.service.ts | 70 ++++++++++++++++- 11 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 server/prisma/migrations/20230403185142_add_created_by_timer_friend/migration.sql create mode 100644 server/prisma/migrations/20230403185304_set_end_to_now_default/migration.sql create mode 100644 server/prisma/migrations/20230403185404_make_datetime_start_nullable/migration.sql create mode 100644 server/src/dto/dtos.ts diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 259de13..7c9045b 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -21,5 +21,14 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', // or "error" + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], }, }; diff --git a/server/prisma/migrations/20230403185142_add_created_by_timer_friend/migration.sql b/server/prisma/migrations/20230403185142_add_created_by_timer_friend/migration.sql new file mode 100644 index 0000000..8fae128 --- /dev/null +++ b/server/prisma/migrations/20230403185142_add_created_by_timer_friend/migration.sql @@ -0,0 +1,39 @@ +/* + Warnings: + + - You are about to drop the `_FriendToTimer` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `created_by_id` to the `Timer` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "_FriendToTimer" DROP CONSTRAINT "_FriendToTimer_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_FriendToTimer" DROP CONSTRAINT "_FriendToTimer_B_fkey"; + +-- AlterTable +ALTER TABLE "Timer" ADD COLUMN "created_by_id" INTEGER NOT NULL; + +-- DropTable +DROP TABLE "_FriendToTimer"; + +-- CreateTable +CREATE TABLE "_referenced_friend_fk" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_referenced_friend_fk_AB_unique" ON "_referenced_friend_fk"("A", "B"); + +-- CreateIndex +CREATE INDEX "_referenced_friend_fk_B_index" ON "_referenced_friend_fk"("B"); + +-- AddForeignKey +ALTER TABLE "Timer" ADD CONSTRAINT "Timer_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "Friend"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_referenced_friend_fk" ADD CONSTRAINT "_referenced_friend_fk_A_fkey" FOREIGN KEY ("A") REFERENCES "Friend"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_referenced_friend_fk" ADD CONSTRAINT "_referenced_friend_fk_B_fkey" FOREIGN KEY ("B") REFERENCES "Timer"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20230403185304_set_end_to_now_default/migration.sql b/server/prisma/migrations/20230403185304_set_end_to_now_default/migration.sql new file mode 100644 index 0000000..3c2ac12 --- /dev/null +++ b/server/prisma/migrations/20230403185304_set_end_to_now_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Timer" ALTER COLUMN "start" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/server/prisma/migrations/20230403185404_make_datetime_start_nullable/migration.sql b/server/prisma/migrations/20230403185404_make_datetime_start_nullable/migration.sql new file mode 100644 index 0000000..bba7e5a --- /dev/null +++ b/server/prisma/migrations/20230403185404_make_datetime_start_nullable/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Timer" ALTER COLUMN "start" DROP NOT NULL; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 07d16cd..a720ca7 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -19,30 +19,34 @@ model GodToken { } model Friend { - id Int @id @default(autoincrement()) - name String @unique @db.Citext - public_key String @db.Text + id Int @id @default(autoincrement()) + name String @unique @db.Citext + public_key String @db.Text - timers Timer[] + referenced_in Timer[] @relation(name: "referenced_friend_fk") + created_timers Timer[] @relation(name: "created_by_fk") god_tokens GodToken[] TimerRefreshes TimerRefreshes[] } model Timer { - id Int @id @default(autoincrement()) - start DateTime - name String @unique + id Int @id @default(autoincrement()) + start DateTime? @default(now()) + name String @unique + + created_by Friend @relation(name: "created_by_fk", fields: [created_by_id], references: [id]) + created_by_id Int timer_refreshes TimerRefreshes[] - referenced_friends Friend[] + referenced_friends Friend[] @relation(name: "referenced_friend_fk") } model TimerRefreshes { - id Int @id @default(autoincrement()) - start DateTime - end DateTime + id Int @id @default(autoincrement()) + start DateTime + end DateTime - refreshed_by Friend @relation(fields: [refreshed_by_id], references: [id]) + refreshed_by Friend @relation(fields: [refreshed_by_id], references: [id]) refreshed_by_id Int timer Timer @relation(fields: [timer_id], references: [id]) diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index d04939d..3f5648d 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -10,20 +10,12 @@ import { UseGuards, Req, } from '@nestjs/common'; -import { IsNotEmpty } from 'class-validator'; + import { readKey, readCleartextMessage, verify } from 'openpgp'; import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; -export class LoginUserDTO { - @IsNotEmpty() - signature: string; -} - -export class RetrieveTokenDTO { - @IsNotEmpty() - name: string; -} +import { RetrieveFriendDTO, SignedGodTokenDTO } from '../dto/dtos'; @Controller('/auth') export class AuthController { @@ -40,7 +32,7 @@ export class AuthController { } @Get('/') - async retrieveGodToken(@Query() query: RetrieveTokenDTO) { + async makeGodToken(@Query() query: RetrieveFriendDTO) { const friend = await this.authService.findFriendByName(query.name); if (!friend) throw new NotFoundException('Friend not found with that name'); @@ -50,7 +42,7 @@ export class AuthController { @Post() async verifyFriend( @Res({ passthrough: true }) res, - @Body() { signature }: LoginUserDTO, + @Body() { signature }: SignedGodTokenDTO, ) { let signatureObj; try { diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index 59345b6..cf4edde 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -7,7 +7,7 @@ import { AuthService } from './auth.service'; @Module({ imports: [PrismaModule], controllers: [AuthController], - exports: [AuthGuard], + exports: [AuthGuard, AuthService], providers: [AuthService, AuthGuard], }) export class AuthModule {} diff --git a/server/src/dto/dtos.ts b/server/src/dto/dtos.ts new file mode 100644 index 0000000..826fc23 --- /dev/null +++ b/server/src/dto/dtos.ts @@ -0,0 +1,16 @@ +import { IsNotEmpty } from 'class-validator'; + +export class SignedGodTokenDTO { + @IsNotEmpty() + signature: string; +} + +export class RetrieveFriendDTO { + @IsNotEmpty() + name: string; +} + +export class CreateTimerDTO { + @IsNotEmpty() + name: string; +} diff --git a/server/src/timer/timer.controller.ts b/server/src/timer/timer.controller.ts index b9eb21f..03b9790 100644 --- a/server/src/timer/timer.controller.ts +++ b/server/src/timer/timer.controller.ts @@ -1,4 +1,75 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Req, + Query, + NotFoundException, + UseGuards, + BadRequestException, +} from '@nestjs/common'; +import { TimerService } from './timer.service'; +import { AuthService } from '../auth/auth.service'; +import { CreateTimerDTO, RetrieveFriendDTO } from '../dto/dtos'; +import { AuthGuard } from 'src/auth/auth.guard'; +import { Prisma } from '@prisma/client'; -@Controller('timer') -export class TimerController {} +@Controller('timers') +@UseGuards(AuthGuard) +export class TimerController { + constructor( + private readonly timerService: TimerService, + private readonly authService: AuthService, + ) {} + + @Get() + public async getAllTimers() { + return this.timerService.getAll(); + } + + @Get('/friend') + public async getFriendTimers(@Query() { name }: RetrieveFriendDTO) { + const friend = await this.authService.findFriendByName(name); + if (!friend) throw new NotFoundException('Friend not found with that name'); + return this.timerService.friendTimers(friend); + } + + @Post() + public async createTimer(@Body() { name }: CreateTimerDTO, @Req() req) { + const referencedFriendIds = Array.from( + new Set( + [...name.matchAll(/\@<(\d+)>/g)].map(([_match, id]) => + parseInt(id, 10), + ), + ), + ); + + if (referencedFriendIds.length > 10) + throw new BadRequestException( + 'Can link no more than 10 unique friends to timer', + ); + + try { + return await this.timerService.createTimerWithFriends( + { + name, + created_by: { + connect: { + id: req.friend.id, + }, + }, + }, + referencedFriendIds, + ); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) + throw new BadRequestException('Timer with name already exists'); + + throw e; + } + } +} diff --git a/server/src/timer/timer.module.ts b/server/src/timer/timer.module.ts index d0eb8f1..e13da96 100644 --- a/server/src/timer/timer.module.ts +++ b/server/src/timer/timer.module.ts @@ -2,9 +2,12 @@ import { Module } from '@nestjs/common'; import { TimerGateway } from './timer.gateway'; import { TimerController } from './timer.controller'; import { TimerService } from './timer.service'; +import { PrismaModule } from '../prisma/prisma.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ + imports: [PrismaModule, AuthModule], providers: [TimerGateway, TimerService], - controllers: [TimerController] + controllers: [TimerController], }) export class TimerModule {} diff --git a/server/src/timer/timer.service.ts b/server/src/timer/timer.service.ts index 0052261..25f6cbe 100644 --- a/server/src/timer/timer.service.ts +++ b/server/src/timer/timer.service.ts @@ -1,4 +1,72 @@ +import { Friend, Prisma } from '@prisma/client'; import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + @Injectable() -export class TimerService {} +export class TimerService { + constructor(private readonly prismaService: PrismaService) {} + + public getAll() { + return this.prismaService.timer.findMany({ + select: { + id: true, + name: true, + start: true, + referenced_friends: { + select: { + name: true, + id: true, + }, + }, + created_by: { + select: { + name: true, + id: true, + }, + }, + }, + }); + } + + public friendTimers(friend: Friend) { + return this.prismaService.timer.findMany({ + select: { + id: true, + name: true, + start: true, + referenced_friends: { + where: { + id: friend.id, + }, + select: { + id: true, + name: true, + }, + }, + created_by: { + select: { + name: true, + id: true, + }, + }, + }, + }); + } + + public createTimerWithFriends( + timer: Prisma.TimerCreateInput, + friendIds: number[], + ) { + if (friendIds.length > 0) + return this.prismaService.timer.create({ + data: { + ...timer, + referenced_friends: { + connect: friendIds.map((id) => ({ id })), + }, + }, + }); + return this.prismaService.timer.create({ data: timer }); + } +}