From 495ac3593784ffcd4f28c32edebf5886f51d2df4 Mon Sep 17 00:00:00 2001 From: legonzaur Date: Sun, 1 Jun 2025 13:07:45 +0200 Subject: [PATCH] add invite code support --- src/app.module.ts | 6 ++---- src/vote/vote.controller.ts | 37 +++++++++++++++++++++++++++++++++---- src/vote/vote.entity.ts | 28 ++++++++++++++++++++++++++++ src/vote/vote.module.ts | 4 ++-- src/vote/vote.service.ts | 22 ++++++++++++++++++++-- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 5ae1151..0a09154 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,9 +3,7 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { VoteController } from './vote/vote.controller'; -import { VoteService } from './vote/vote.service'; -import { Answer, Vote } from './vote/vote.entity'; +import { Answer, InviteCode, Vote } from './vote/vote.entity'; import { VoteModule } from './vote/vote.module'; void ConfigModule.forRoot({ @@ -24,7 +22,7 @@ const configService = new ConfigService(); username: configService.get('PG_USER'), database: configService.get('PG_DATABASE'), synchronize: configService.get('DEV_MODE') == 'true', - entities: [Vote, Answer], + entities: [Vote, Answer, InviteCode], }), VoteModule, ], diff --git a/src/vote/vote.controller.ts b/src/vote/vote.controller.ts index 0a293d0..fd167e5 100644 --- a/src/vote/vote.controller.ts +++ b/src/vote/vote.controller.ts @@ -1,4 +1,13 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + ParseUUIDPipe, + Post, +} from '@nestjs/common'; import { VoteCreateDTO } from './vote.dto'; import { VoteService } from './vote.service'; import { ApiOperation } from '@nestjs/swagger'; @@ -7,18 +16,38 @@ import { ApiOperation } from '@nestjs/swagger'; export class VoteController { constructor(private readonly voteService: VoteService) {} + @Get('check/:accessCode') + @ApiOperation({ summary: 'Checks if a code has been used' }) + async check(@Param('accessCode', new ParseUUIDPipe()) accessCode: string) { + const value = await this.voteService.check(accessCode); + if (value === null) { + throw new HttpException('Invalid accessCode', HttpStatus.NOT_FOUND); + } + if (value === false) { + throw new HttpException('AccessCode expired', HttpStatus.FORBIDDEN); + } + return; + } + @Post('vote/:accessCode') @ApiOperation({ summary: 'Vote for a poll' }) async vote( - @Param('accessCode') accessCode: string, + @Param('accessCode', new ParseUUIDPipe()) accessCode: string, @Body() answer: VoteCreateDTO, ) { - await this.voteService.create(answer); + const code = await this.voteService.check(accessCode); + if (code === null) { + throw new HttpException('Invalid accessCode', HttpStatus.NOT_FOUND); + } + if (code === false) { + throw new HttpException('AccessCode expired', HttpStatus.FORBIDDEN); + } + await this.voteService.create(answer, code); } @Get('results/:accessCode') @ApiOperation({ summary: 'Get results' }) - async results(@Param('accessCode') accessCode: string) { + async results(@Param('accessCode', new ParseUUIDPipe()) accessCode: string) { return await this.voteService.getResults(); } } diff --git a/src/vote/vote.entity.ts b/src/vote/vote.entity.ts index db429b4..7dae3ee 100644 --- a/src/vote/vote.entity.ts +++ b/src/vote/vote.entity.ts @@ -4,6 +4,8 @@ import { OneToMany, ManyToOne, Column, + OneToOne, + Generated, } from 'typeorm'; @Entity() @@ -16,6 +18,13 @@ export class Vote { eager: true, }) answers: Answer[]; + + @OneToOne(() => InviteCode, (code) => code.vote, { + eager: false, + onDelete: 'CASCADE', + nullable: false, + }) + code: Promise; } @Entity() @@ -35,3 +44,22 @@ export class Answer { @Column() answer: string; } + +@Entity() +export class InviteCode { + @PrimaryGeneratedColumn({}) + id: number; + + @OneToOne(() => Vote, (vote) => vote.code, { + eager: false, + onDelete: 'CASCADE', + }) + vote: Promise; + + @Column({ default: 1 }) + usages_left: number; + + @Column() + @Generated('uuid') + value: string; +} diff --git a/src/vote/vote.module.ts b/src/vote/vote.module.ts index a3ac3fb..4ac45a3 100644 --- a/src/vote/vote.module.ts +++ b/src/vote/vote.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; -import { Answer, Vote } from './vote.entity'; +import { InviteCode, Vote } from './vote.entity'; import { VoteController } from './vote.controller'; import { VoteService } from './vote.service'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ - imports: [TypeOrmModule.forFeature([Vote, Answer])], + imports: [TypeOrmModule.forFeature([Vote, InviteCode])], controllers: [VoteController], providers: [VoteService], }) diff --git a/src/vote/vote.service.ts b/src/vote/vote.service.ts index 81a0361..0bca31b 100644 --- a/src/vote/vote.service.ts +++ b/src/vote/vote.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { VoteCreateDTO } from './vote.dto'; -import { Vote } from './vote.entity'; +import { InviteCode, Vote } from './vote.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; @@ -17,8 +17,13 @@ interface IVote { export class VoteService { constructor( @InjectRepository(Vote) private voteRepository: Repository, + @InjectRepository(InviteCode) + private inviteCodeRepository: Repository, ) {} - async create(value: VoteCreateDTO) { + async create(value: VoteCreateDTO, accessCode: InviteCode) { + accessCode.usages_left--; + await this.inviteCodeRepository.save(accessCode); + const vote = Object.entries(value).reduce( (acc, [question_id, a]) => { const answer = String(a); @@ -51,4 +56,17 @@ export class VoteService { } return compiled_results; } + + async check(inviteCode: string) { + const code = await this.inviteCodeRepository.findOneBy({ + value: inviteCode, + }); + if (!code) { + return null; + } + if (code.usages_left > 0) { + return code; + } + return false; + } }