From a95601bc34ce2008236eb753f8ff7d0703af5f32 Mon Sep 17 00:00:00 2001 From: MiguelMLorente Date: Wed, 3 Dec 2025 22:33:10 +0100 Subject: [PATCH] Implement token controll to access sessions --- src/app.module.ts | 17 ++++++++- src/controller/buy.controller.ts | 5 +++ src/controller/tokens.controller.ts | 17 +++++++++ src/dto/session-status.ts | 6 +++ src/dto/session.ts | 4 ++ src/dto/token.ts | 25 +++++++++++++ src/dto/user.ts | 5 +++ src/service/session.service.ts | 23 +++++++++++- src/service/token.service.ts | 57 +++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/controller/tokens.controller.ts create mode 100644 src/dto/session-status.ts create mode 100644 src/dto/token.ts create mode 100644 src/service/token.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 00b3751..20aca7a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,8 @@ import { Purchase } from './dto/purchase'; import { SessionService } from './service/session.service'; import { SessionController } from './controller/session.controller'; import { Session } from './dto/session'; +import { TokensController } from './controller/tokens.controller'; +import { TokenService } from './service/token.service'; @Module({ imports: [ @@ -26,7 +28,18 @@ import { Session } from './dto/session'; }), TypeOrmModule.forFeature([User, Purchase, Session]), ], - controllers: [AccessController, BuyController, SessionController], - providers: [UserService, PurchaseService, StripeService, SessionService], + controllers: [ + AccessController, + BuyController, + SessionController, + TokensController, + ], + providers: [ + UserService, + PurchaseService, + StripeService, + SessionService, + TokenService, + ], }) export class AppModule {} diff --git a/src/controller/buy.controller.ts b/src/controller/buy.controller.ts index b188367..da138e4 100644 --- a/src/controller/buy.controller.ts +++ b/src/controller/buy.controller.ts @@ -4,6 +4,7 @@ import { UserService } from '../service/user.service'; import { PurchaseItem } from '../dto/purchase-item'; import { StripeService } from '../service/stripe.service'; import { PurchaseStatus } from 'src/dto/purchase-status'; +import { TokenService } from 'src/service/token.service'; @Controller('/buy') export class BuyController { @@ -16,6 +17,7 @@ export class BuyController { constructor( private readonly purchaseService: PurchaseService, private readonly userService: UserService, + private readonly tokenService: TokenService, private readonly stripeService: StripeService, ) {} @@ -54,6 +56,9 @@ export class BuyController { ? PurchaseStatus.COMPLETED : PurchaseStatus.IN_PROGRESS; this.purchaseService.recordPurchase(purchase); + if (purchase.status === PurchaseStatus.COMPLETED) { + this.tokenService.addTokens(user, purchase); + } }); return { url: '/signup' }; diff --git a/src/controller/tokens.controller.ts b/src/controller/tokens.controller.ts new file mode 100644 index 0000000..b6e3bf8 --- /dev/null +++ b/src/controller/tokens.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { TokenService } from 'src/service/token.service'; +import { UserService } from 'src/service/user.service'; + +@Controller('/tokens') +export class TokensController { + constructor( + private readonly userService: UserService, + private readonly tokenService: TokenService, + ) {} + + @Get() + public async getTokenCount(@Query('userId') userId: string) { + const user = await this.userService.getUserById(userId); + return await this.tokenService.getAvailableTokens(user); + } +} diff --git a/src/dto/session-status.ts b/src/dto/session-status.ts new file mode 100644 index 0000000..a19af5a --- /dev/null +++ b/src/dto/session-status.ts @@ -0,0 +1,6 @@ +export enum SessionStatus { + SCHEDULED = 'SCHEDULED', + OPEN = 'OPEN', + CLOSED = 'CLOSED', + CANCELLED = 'CANCELLED', +} diff --git a/src/dto/session.ts b/src/dto/session.ts index 860634f..5029d3c 100644 --- a/src/dto/session.ts +++ b/src/dto/session.ts @@ -1,5 +1,6 @@ import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './user'; +import { SessionStatus } from './session-status'; @Entity('SESSIONS') export class Session { @@ -14,4 +15,7 @@ export class Session { @Column() date: Date; + + @Column() + status: SessionStatus; } diff --git a/src/dto/token.ts b/src/dto/token.ts new file mode 100644 index 0000000..be61849 --- /dev/null +++ b/src/dto/token.ts @@ -0,0 +1,25 @@ +import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; +import { User } from './user'; + +@Entity('TOKENS') +export class Token { + @PrimaryColumn() + userId: string; + + @OneToOne(() => User, (user) => user.token) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column() + purchasedTokens: number; + + @Column() + consumedTokens: number; + + @Column() + lockedTokens: number; + + get availableTokens() { + return this.purchasedTokens - this.consumedTokens - this.lockedTokens; + } +} diff --git a/src/dto/user.ts b/src/dto/user.ts index c4cbf25..ad59c1e 100644 --- a/src/dto/user.ts +++ b/src/dto/user.ts @@ -4,10 +4,12 @@ import { JoinTable, ManyToMany, OneToMany, + OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; import { Purchase } from './purchase'; import { Session } from './session'; +import { Token } from './token'; @Entity('USERS') export class User { @@ -26,4 +28,7 @@ export class User { @ManyToMany(() => Session, (session) => session.users) @JoinTable() joinedSessions: Session[]; + + @OneToOne(() => Token, (token) => token.user) + token: Token; } diff --git a/src/service/session.service.ts b/src/service/session.service.ts index aa3393a..db1714d 100644 --- a/src/service/session.service.ts +++ b/src/service/session.service.ts @@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Session } from 'src/dto/session'; import { User } from 'src/dto/user'; import { Repository } from 'typeorm'; +import { TokenService } from './token.service'; +import { SessionStatus } from 'src/dto/session-status'; @Injectable() export class SessionService { @@ -10,6 +12,7 @@ export class SessionService { constructor( @InjectRepository(Session) private sessionRepo: Repository, + private tokenService: TokenService, ) {} public createSession(date: Date, size: number) { @@ -17,10 +20,24 @@ export class SessionService { users: [], date, size, + status: SessionStatus.OPEN, }); this.sessionRepo.save(session); } + public async closeSession(sessionId: string) { + const session = await this.sessionRepo.findOneOrFail({ + where: { id: sessionId }, + }); + Promise.all( + session.users.map( + async (user) => await this.tokenService.consumeToken(user), + ), + ); + session.status = SessionStatus.CLOSED; + await this.sessionRepo.save(session); + } + public getAllSessions() { return this.sessionRepo.find({ relations: { users: true }, @@ -29,6 +46,7 @@ export class SessionService { } public async joinSession(user: User, sessionId: string) { + this.logger.debug(`User: ${user.id} tries to join session: ${sessionId}`); const session = await this.sessionRepo.findOneOrFail({ where: { id: sessionId, @@ -46,11 +64,13 @@ export class SessionService { ) { return; } + await this.tokenService.lockToken(user); session.users.push(user); await this.sessionRepo.save(session); } public async leaveSession(user: User, sessionId: string) { + this.logger.debug(`User: ${user.id} tries to leave session: ${sessionId}`); const session = await this.sessionRepo.findOneOrFail({ where: { id: sessionId, @@ -59,9 +79,10 @@ export class SessionService { users: true, }, }); - if (!session) { + if (!session || !session.users.find((u) => u.id === user.id)) { throw new NotFoundException(); } + this.tokenService.unlockToken(user); session.users = session.users.filter((u) => u.id !== user.id); await this.sessionRepo.save(session); } diff --git a/src/service/token.service.ts b/src/service/token.service.ts new file mode 100644 index 0000000..6672d84 --- /dev/null +++ b/src/service/token.service.ts @@ -0,0 +1,57 @@ +import { + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Purchase } from 'src/dto/purchase'; +import { Token } from 'src/dto/token'; +import { User } from 'src/dto/user'; +import { Repository } from 'typeorm'; + +@Injectable() +export class TokenService { + private readonly logger = new Logger(TokenService.name); + + constructor(@InjectRepository(Token) private tokenRepo: Repository) {} + + public async addTokens(user: User, purchase: Purchase) { + const token = await this.tokenRepo.findOneOrFail({ where: { user: user } }); + token.purchasedTokens += purchase.purchasedUnits; + await this.tokenRepo.save(token); + } + + public async getAvailableTokens(user: User) { + const token = await this.tokenRepo.findOneOrFail({ where: { user: user } }); + return token.availableTokens; + } + + public async consumeToken(user: User) { + const token = await this.tokenRepo.findOneOrFail({ where: { user: user } }); + if (token.lockedTokens <= 0) { + throw new InternalServerErrorException(); + } + token.lockedTokens -= 1; + token.consumedTokens += 1; + await this.tokenRepo.save(token); + } + + public async lockToken(user: User) { + const token = await this.tokenRepo.findOneOrFail({ where: { user: user } }); + if (token.availableTokens <= 0) { + throw new NotFoundException(); + } + token.lockedTokens += 1; + await this.tokenRepo.save(token); + } + + public async unlockToken(user: User) { + const token = await this.tokenRepo.findOneOrFail({ where: { user: user } }); + if (token.lockedTokens <= 0) { + throw new NotFoundException(); + } + token.lockedTokens -= 1; + await this.tokenRepo.save(token); + } +}