-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64 from aweolumidedavid/feature/wager-invitation
feat:add wager invitation system
- Loading branch information
Showing
14 changed files
with
305 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
backend/prisma/migrations/20250211092149_fix_invitation_username_type/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
-- CreateTable | ||
CREATE TABLE "invitations" ( | ||
"id" TEXT NOT NULL, | ||
"wager_id" TEXT NOT NULL, | ||
"invited_by_id" TEXT NOT NULL, | ||
"invited_username" CITEXT NOT NULL, | ||
"status" TEXT NOT NULL DEFAULT 'pending', | ||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updated_at" TIMESTAMP(3) NOT NULL, | ||
|
||
CONSTRAINT "invitations_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "invitations" ADD CONSTRAINT "invitations_wager_id_fkey" FOREIGN KEY ("wager_id") REFERENCES "wagers"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "invitations" ADD CONSTRAINT "invitations_invited_by_id_fkey" FOREIGN KEY ("invited_by_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "invitations" ADD CONSTRAINT "invitations_invited_username_fkey" FOREIGN KEY ("invited_username") REFERENCES "users"("username") ON DELETE RESTRICT ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
backend/src/wagerInvitations/controllers/wagerInvitation.controller.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { WagerInvitationService } from '../services/wagerInvitation.service'; | ||
import { WagerInvitationController } from './wagerInvitation.controller'; | ||
|
||
describe('WagerInvitationController', () => { | ||
let controller: WagerInvitationController; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
controllers: [WagerInvitationController], | ||
providers: [WagerInvitationService], | ||
}).compile(); | ||
|
||
controller = module.get<WagerInvitationController>( | ||
WagerInvitationController, | ||
); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(controller).toBeDefined(); | ||
}); | ||
}); |
71 changes: 71 additions & 0 deletions
71
backend/src/wagerInvitations/controllers/wagerInvitation.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { | ||
Controller, | ||
Get, | ||
Post, | ||
Body, | ||
Param, | ||
UseGuards, | ||
ConflictException, | ||
Inject, | ||
Req, | ||
} from '@nestjs/common'; | ||
import { JwtService } from '@nestjs/jwt'; | ||
import { WagerInvitationService } from '../services/wagerInvitation.service'; | ||
import { CreateWagerInvitationDto } from '../dtos/wagerInvitations.dto'; | ||
import { CreateInvitationGuard } from '../guards/wagerInvitation.guard'; | ||
import { ConfigType } from '@nestjs/config'; | ||
import { AppConfig } from 'src/config'; | ||
|
||
@Controller('invitations') | ||
export class WagerInvitationController { | ||
constructor( | ||
@Inject(AppConfig.KEY) | ||
private appConfig: ConfigType<typeof AppConfig>, | ||
private invitationService: WagerInvitationService, | ||
private jwtService: JwtService, | ||
) {} | ||
|
||
@Post('create') | ||
@UseGuards(CreateInvitationGuard) | ||
async createInvitation( | ||
@Body() data: CreateWagerInvitationDto, | ||
@Req() req: Request, | ||
) { | ||
const invitedByUserId = req['user'].sub; | ||
// Ensure user is not invited more than once to the same wager | ||
const existingInvitation = | ||
await this.invitationService.findExistingInvitation( | ||
data.wagerId, | ||
data.invitedUsername, | ||
); | ||
if (existingInvitation) { | ||
throw new ConflictException( | ||
'User has already been invited to this wager', | ||
); | ||
} | ||
const invitation = await this.invitationService.createInvitation( | ||
data, | ||
invitedByUserId, | ||
); | ||
|
||
// Generate JWT token for invitation | ||
const invitationToken = await this.jwtService.signAsync( | ||
{ invitationId: invitation.id }, | ||
{ | ||
expiresIn: this.appConfig.invitationTokenExpiry, | ||
secret: this.appConfig.secret, | ||
}, | ||
); | ||
|
||
return { invitation, invitationToken }; | ||
} | ||
|
||
@Get('wager/:wagerId') | ||
async getInvitationsForWager( | ||
@Param('wagerId') wagerId: string, | ||
@Req() req: Request, | ||
) { | ||
const userId = req['user'].sub; | ||
return await this.invitationService.getInvitationsForWager(wagerId, userId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { IsNotEmpty, IsString } from 'class-validator'; | ||
|
||
export class CreateWagerInvitationDto { | ||
@IsString() | ||
@IsNotEmpty() | ||
wagerId: string; | ||
|
||
@IsString() | ||
@IsNotEmpty() | ||
invitedUsername: string; | ||
} |
73 changes: 73 additions & 0 deletions
73
backend/src/wagerInvitations/guards/wagerInvitation.guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { | ||
BadRequestException, | ||
CanActivate, | ||
ConflictException, | ||
ExecutionContext, | ||
Injectable, | ||
NotFoundException, | ||
} from '@nestjs/common'; | ||
import { Request } from 'express'; | ||
import { validate } from 'class-validator'; | ||
import { plainToInstance } from 'class-transformer'; | ||
import { WagerService } from 'src/wager/services/wager.service'; | ||
import { UsersService } from 'src/users/users.service'; | ||
import { CreateWagerInvitationDto } from '../dtos/wagerInvitations.dto'; | ||
|
||
@Injectable() | ||
export class CreateInvitationGuard implements CanActivate { | ||
constructor( | ||
private readonly wagerService: WagerService, | ||
private readonly userService: UsersService, | ||
) {} | ||
|
||
async canActivate(context: ExecutionContext) { | ||
const req: Request = context.switchToHttp().getRequest(); | ||
const invitedById = req['user'].sub; | ||
|
||
// Convert the raw body to an instance of CreateWagerDto | ||
const dto = plainToInstance(CreateWagerInvitationDto, req.body); | ||
|
||
// Validate the DTO | ||
const errors = await validate(dto); | ||
if (errors.length > 0) { | ||
const formattedErrors = this.formatValidationErrors(errors); | ||
throw new BadRequestException(formattedErrors); | ||
} | ||
|
||
// Validate wager exists | ||
const wager = await this.wagerService.findOneById(dto.wagerId); | ||
if (!wager) { | ||
throw new NotFoundException('Wager not found'); | ||
} | ||
|
||
// Validate invited user exists | ||
const invitedUser = await this.userService.findOneByUsername( | ||
dto.invitedUsername, | ||
); | ||
|
||
if (!invitedUser) { | ||
throw new NotFoundException('Invited user not found'); | ||
} | ||
|
||
// Ensure user is not inviting themselves | ||
if (invitedUser.id === invitedById) { | ||
throw new ConflictException('You cannot invite yourself'); | ||
} | ||
|
||
// Attach the validated DTO to the request for later use | ||
req.body = dto; | ||
|
||
return true; | ||
} | ||
private formatValidationErrors(errors: any[]) { | ||
const messages = errors.flatMap((error) => { | ||
return Object.values(error.constraints); | ||
}); | ||
|
||
return { | ||
message: messages, | ||
error: 'Bad Request', | ||
statusCode: 400, | ||
}; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
backend/src/wagerInvitations/services/wager.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { WagerInvitationService } from './wagerInvitation.service'; | ||
|
||
describe('WagerInvitationService', () => { | ||
let service: WagerInvitationService; | ||
|
||
beforeEach(async () => { | ||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [WagerInvitationService], | ||
}).compile(); | ||
|
||
service = module.get<WagerInvitationService>(WagerInvitationService); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(service).toBeDefined(); | ||
}); | ||
}); |
42 changes: 42 additions & 0 deletions
42
backend/src/wagerInvitations/services/wagerInvitation.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { PrismaService } from 'nestjs-prisma'; | ||
import { CreateWagerInvitationDto } from '../dtos/wagerInvitations.dto'; | ||
import { Injectable } from '@nestjs/common'; | ||
|
||
@Injectable() | ||
export class WagerInvitationService { | ||
constructor(private prisma: PrismaService) {} | ||
|
||
async createInvitation( | ||
data: CreateWagerInvitationDto, | ||
invitedByUserId: string, | ||
) { | ||
const invitation = await this.prisma.invitation.create({ | ||
data: { | ||
wagerId: data.wagerId, | ||
invitedById: invitedByUserId, | ||
invitedUsername: data.invitedUsername, | ||
status: 'pending', | ||
}, | ||
}); | ||
return invitation; | ||
} | ||
|
||
async getInvitationsForWager(wagerId: string, invitedById: string) { | ||
const invitations = await this.prisma.invitation.findMany({ | ||
where: { wagerId, invitedById }, | ||
include: { | ||
invitedUser: true, | ||
wager: true, | ||
}, | ||
}); | ||
|
||
return invitations; | ||
} | ||
|
||
async findExistingInvitation(wagerId: string, invitedUsername: string) { | ||
const invitation = await this.prisma.invitation.findFirst({ | ||
where: { wagerId, invitedUsername }, | ||
}); | ||
return invitation; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Global, Module } from '@nestjs/common'; | ||
import { UsersService } from 'src/users/users.service'; | ||
import { WagerService } from 'src/wager/services/wager.service'; | ||
import { WagerInvitationController } from './controllers/wagerInvitation.controller'; | ||
import { WagerInvitationService } from './services/wagerInvitation.service'; | ||
import { JwtModule } from '@nestjs/jwt'; | ||
|
||
@Global() | ||
@Module({ | ||
controllers: [WagerInvitationController], | ||
providers: [WagerInvitationService, UsersService, WagerService], | ||
imports: [JwtModule], | ||
}) | ||
export class InvitationModule {} |