add invite code support

This commit is contained in:
2025-06-01 13:07:45 +02:00
parent 7383ce6977
commit 495ac35937
5 changed files with 85 additions and 12 deletions
+2 -4
View File
@@ -3,9 +3,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { VoteController } from './vote/vote.controller'; import { Answer, InviteCode, Vote } from './vote/vote.entity';
import { VoteService } from './vote/vote.service';
import { Answer, Vote } from './vote/vote.entity';
import { VoteModule } from './vote/vote.module'; import { VoteModule } from './vote/vote.module';
void ConfigModule.forRoot({ void ConfigModule.forRoot({
@@ -24,7 +22,7 @@ const configService = new ConfigService();
username: configService.get('PG_USER'), username: configService.get('PG_USER'),
database: configService.get('PG_DATABASE'), database: configService.get('PG_DATABASE'),
synchronize: configService.get('DEV_MODE') == 'true', synchronize: configService.get('DEV_MODE') == 'true',
entities: [Vote, Answer], entities: [Vote, Answer, InviteCode],
}), }),
VoteModule, VoteModule,
], ],
+33 -4
View File
@@ -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 { VoteCreateDTO } from './vote.dto';
import { VoteService } from './vote.service'; import { VoteService } from './vote.service';
import { ApiOperation } from '@nestjs/swagger'; import { ApiOperation } from '@nestjs/swagger';
@@ -7,18 +16,38 @@ import { ApiOperation } from '@nestjs/swagger';
export class VoteController { export class VoteController {
constructor(private readonly voteService: VoteService) {} 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') @Post('vote/:accessCode')
@ApiOperation({ summary: 'Vote for a poll' }) @ApiOperation({ summary: 'Vote for a poll' })
async vote( async vote(
@Param('accessCode') accessCode: string, @Param('accessCode', new ParseUUIDPipe()) accessCode: string,
@Body() answer: VoteCreateDTO, @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') @Get('results/:accessCode')
@ApiOperation({ summary: 'Get results' }) @ApiOperation({ summary: 'Get results' })
async results(@Param('accessCode') accessCode: string) { async results(@Param('accessCode', new ParseUUIDPipe()) accessCode: string) {
return await this.voteService.getResults(); return await this.voteService.getResults();
} }
} }
+28
View File
@@ -4,6 +4,8 @@ import {
OneToMany, OneToMany,
ManyToOne, ManyToOne,
Column, Column,
OneToOne,
Generated,
} from 'typeorm'; } from 'typeorm';
@Entity() @Entity()
@@ -16,6 +18,13 @@ export class Vote {
eager: true, eager: true,
}) })
answers: Answer[]; answers: Answer[];
@OneToOne(() => InviteCode, (code) => code.vote, {
eager: false,
onDelete: 'CASCADE',
nullable: false,
})
code: Promise<InviteCode>;
} }
@Entity() @Entity()
@@ -35,3 +44,22 @@ export class Answer {
@Column() @Column()
answer: string; answer: string;
} }
@Entity()
export class InviteCode {
@PrimaryGeneratedColumn({})
id: number;
@OneToOne(() => Vote, (vote) => vote.code, {
eager: false,
onDelete: 'CASCADE',
})
vote: Promise<Vote>;
@Column({ default: 1 })
usages_left: number;
@Column()
@Generated('uuid')
value: string;
}
+2 -2
View File
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { Answer, Vote } from './vote.entity'; import { InviteCode, Vote } from './vote.entity';
import { VoteController } from './vote.controller'; import { VoteController } from './vote.controller';
import { VoteService } from './vote.service'; import { VoteService } from './vote.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Vote, Answer])], imports: [TypeOrmModule.forFeature([Vote, InviteCode])],
controllers: [VoteController], controllers: [VoteController],
providers: [VoteService], providers: [VoteService],
}) })
+20 -2
View File
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { VoteCreateDTO } from './vote.dto'; import { VoteCreateDTO } from './vote.dto';
import { Vote } from './vote.entity'; import { InviteCode, Vote } from './vote.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
@@ -17,8 +17,13 @@ interface IVote {
export class VoteService { export class VoteService {
constructor( constructor(
@InjectRepository(Vote) private voteRepository: Repository<Vote>, @InjectRepository(Vote) private voteRepository: Repository<Vote>,
@InjectRepository(InviteCode)
private inviteCodeRepository: Repository<InviteCode>,
) {} ) {}
async create(value: VoteCreateDTO) { async create(value: VoteCreateDTO, accessCode: InviteCode) {
accessCode.usages_left--;
await this.inviteCodeRepository.save(accessCode);
const vote = Object.entries(value).reduce<IVote>( const vote = Object.entries(value).reduce<IVote>(
(acc, [question_id, a]) => { (acc, [question_id, a]) => {
const answer = String(a); const answer = String(a);
@@ -51,4 +56,17 @@ export class VoteService {
} }
return compiled_results; 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;
}
} }