From fa91630a2916b9edc1c74b080b986871892195c0 Mon Sep 17 00:00:00 2001 From: Mathis Date: Tue, 12 Nov 2024 13:30:05 +0100 Subject: [PATCH] Add various module, service, and DTO files for new features Integrate modules and services related to crypto, user, role, offer, and promoCode functionalities. Includes DTOs for validating data shapes, and services to handle business logic, with controller endpoints for different operations. --- src/app.controller.spec.ts | 22 +++ src/app.controller.ts | 12 ++ src/app.module.ts | 38 +++++ src/app.service.ts | 8 + src/auth/auth.controller.ts | 21 +++ src/auth/auth.module.ts | 12 ++ src/auth/auth.service.ts | 2 - src/auth/decorator/get-user.decorator.ts | 11 ++ src/auth/decorator/index.ts | 1 + src/auth/dto/auth.login.dto.ts | 13 ++ src/auth/dto/auth.register.dto.ts | 73 +++++++++ src/auth/dto/index.ts | 2 + src/auth/guard/index.ts | 1 + src/auth/guard/jwt.guard.ts | 7 + src/auth/strategy/index.ts | 1 + src/auth/strategy/jwt.strategy.ts | 28 ++++ src/crypto/crypto.controller.ts | 73 +++++++++ src/crypto/crypto.module.ts | 9 ++ src/crypto/crypto.service.ts | 192 +++++++++++++++++++++++ src/crypto/dto/buy.crypto.dto.ts | 32 ++++ src/crypto/dto/crypto.dto.ts | 54 +++++++ src/crypto/dto/index.ts | 1 + src/offer/dto/index.ts | 1 + src/offer/dto/offer.dto.ts | 33 ++++ src/offer/offer.controller.ts | 58 +++++++ src/offer/offer.module.ts | 9 ++ src/offer/offer.service.sql | 35 +++++ src/offer/offer.service.ts | 88 +++++++++++ src/prisma/prisma.module.ts | 9 ++ src/prisma/prisma.service.ts | 18 +++ src/promoCode/dto/index.ts | 1 + src/promoCode/dto/promoCode.dto.ts | 32 ++++ src/promoCode/promoCode.controller.ts | 57 +++++++ src/promoCode/promoCode.module.ts | 9 ++ src/promoCode/promoCode.service.spec.ts | 71 +++++++++ src/promoCode/promoCode.service.ts | 79 ++++++++++ src/role/dto/index.ts | 1 + src/role/dto/role.dto.ts | 13 ++ src/role/role.controller.ts | 63 ++++++++ src/role/role.module.ts | 8 + src/role/role.service.ts | 72 +++++++++ src/trade/dto/index.ts | 1 + src/trade/dto/trade.dto.ts | 13 ++ src/trade/trade.controller.ts | 38 +++++ src/trade/trade.module.ts | 9 ++ src/trade/trade.service.ts | 173 ++++++++++++++++++++ src/user/user.controller.ts | 36 +++++ src/user/user.module.ts | 8 + src/user/user.service.ts | 81 ++++++++++ src/utils/checkUser.ts | 86 ++++++++++ src/utils/const/const.ts | 4 + src/utils/styles.ts | 0 src/utils/tests/user-mock.ts | 16 ++ src/utils/types.ts | 0 54 files changed, 1733 insertions(+), 2 deletions(-) create mode 100644 src/app.controller.spec.ts create mode 100644 src/app.controller.ts create mode 100644 src/app.module.ts create mode 100644 src/app.service.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/decorator/get-user.decorator.ts create mode 100644 src/auth/decorator/index.ts create mode 100644 src/auth/dto/auth.login.dto.ts create mode 100644 src/auth/dto/auth.register.dto.ts create mode 100644 src/auth/dto/index.ts create mode 100644 src/auth/guard/index.ts create mode 100644 src/auth/guard/jwt.guard.ts create mode 100644 src/auth/strategy/index.ts create mode 100644 src/auth/strategy/jwt.strategy.ts create mode 100644 src/crypto/crypto.controller.ts create mode 100644 src/crypto/crypto.module.ts create mode 100644 src/crypto/crypto.service.ts create mode 100644 src/crypto/dto/buy.crypto.dto.ts create mode 100644 src/crypto/dto/crypto.dto.ts create mode 100644 src/crypto/dto/index.ts create mode 100644 src/offer/dto/index.ts create mode 100644 src/offer/dto/offer.dto.ts create mode 100644 src/offer/offer.controller.ts create mode 100644 src/offer/offer.module.ts create mode 100644 src/offer/offer.service.sql create mode 100644 src/offer/offer.service.ts create mode 100644 src/prisma/prisma.module.ts create mode 100644 src/prisma/prisma.service.ts create mode 100644 src/promoCode/dto/index.ts create mode 100644 src/promoCode/dto/promoCode.dto.ts create mode 100644 src/promoCode/promoCode.controller.ts create mode 100644 src/promoCode/promoCode.module.ts create mode 100644 src/promoCode/promoCode.service.spec.ts create mode 100644 src/promoCode/promoCode.service.ts create mode 100644 src/role/dto/index.ts create mode 100644 src/role/dto/role.dto.ts create mode 100644 src/role/role.controller.ts create mode 100644 src/role/role.module.ts create mode 100644 src/role/role.service.ts create mode 100644 src/trade/dto/index.ts create mode 100644 src/trade/dto/trade.dto.ts create mode 100644 src/trade/trade.controller.ts create mode 100644 src/trade/trade.module.ts create mode 100644 src/trade/trade.service.ts create mode 100644 src/user/user.controller.ts create mode 100644 src/user/user.module.ts create mode 100644 src/user/user.service.ts create mode 100644 src/utils/checkUser.ts create mode 100644 src/utils/const/const.ts create mode 100644 src/utils/styles.ts create mode 100644 src/utils/tests/user-mock.ts create mode 100644 src/utils/types.ts diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts new file mode 100644 index 0000000..d22f389 --- /dev/null +++ b/src/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/src/app.controller.ts b/src/app.controller.ts new file mode 100644 index 0000000..52a8d67 --- /dev/null +++ b/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return 'Hello'; + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..c458fa9 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { AppController } from '@/app.controller'; +import { AppService } from '@/app.service'; +import { ConfigModule } from '@nestjs/config'; +import { AuthModule } from '@/auth/auth.module'; +import { PrismaModule } from '@/prisma/prisma.module'; +import { RoleModule } from '@/role/role.module'; +import { PromoCodeModule } from '@/promoCode/promoCode.module'; +import { CryptoModule } from '@/crypto/crypto.module'; +import { TradeModule } from '@/trade/trade.module'; +import { OfferModule } from '@/offer/offer.module'; +import { UserModule } from '@/user/user.module'; +import { ThrottlerModule } from '@nestjs/throttler'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + AuthModule, + PrismaModule, + RoleModule, + PromoCodeModule, + CryptoModule, + TradeModule, + OfferModule, + UserModule, + ThrottlerModule.forRoot([ + { + ttl: 60000, + limit: 40, + }, + ]), + ], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..e479253 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthLoginDto, AuthRegisterDto } from './dto'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('auth') +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Post('sign-up') + signUp(@Body() dto: AuthRegisterDto) { + return this.authService.signup(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('sign-in') + signIn(@Body() dto: AuthLoginDto) { + return this.authService.signin(dto); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..6cc6f73 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './strategy'; + +@Module({ + imports: [JwtModule.register({})], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index c39ee44..13ebb05 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -56,10 +56,8 @@ export class AuthService { firstName: dto.firstName, lastName: dto.lastName, pseudo: dto.pseudo, - city: dto.city, email: dto.email, hash, - age: dto.age, roleId, isActive: true, dollarAvailables: balance, diff --git a/src/auth/decorator/get-user.decorator.ts b/src/auth/decorator/get-user.decorator.ts new file mode 100644 index 0000000..1be614d --- /dev/null +++ b/src/auth/decorator/get-user.decorator.ts @@ -0,0 +1,11 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const GetUser = createParamDecorator( + (data: string | undefined, ctx: ExecutionContext) => { + const request: Express.Request = ctx.switchToHttp().getRequest(); + if (data) { + return request.user[data]; + } + return request.user; + }, +); diff --git a/src/auth/decorator/index.ts b/src/auth/decorator/index.ts new file mode 100644 index 0000000..a7b85aa --- /dev/null +++ b/src/auth/decorator/index.ts @@ -0,0 +1 @@ +export * from './get-user.decorator'; diff --git a/src/auth/dto/auth.login.dto.ts b/src/auth/dto/auth.login.dto.ts new file mode 100644 index 0000000..b14b03e --- /dev/null +++ b/src/auth/dto/auth.login.dto.ts @@ -0,0 +1,13 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +export class AuthLoginDto { + @IsEmail() + @IsNotEmpty() + @ApiProperty({ type: String, description: 'email' }) + email: string; + + @ApiProperty({ type: String, description: 'password' }) + @IsString() + @IsNotEmpty() + password: string; +} diff --git a/src/auth/dto/auth.register.dto.ts b/src/auth/dto/auth.register.dto.ts new file mode 100644 index 0000000..441c810 --- /dev/null +++ b/src/auth/dto/auth.register.dto.ts @@ -0,0 +1,73 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + MaxLength, + Min, + MinLength, +} from 'class-validator'; +export class AuthRegisterDto { + @ApiProperty({ + type: String, + description: 'FirstName', + example: 'Thomas', + }) + @MinLength(1) + @MaxLength(50) + @IsNotEmpty() + @IsString() + firstName: string; + + @ApiProperty({ + type: String, + description: 'Last Name', + example: 'Anderson', + }) + @MinLength(1) + @MaxLength(50) + @IsNotEmpty() + @IsString() + lastName: string; + + @ApiProperty({ + type: String, + description: 'Pseudo', + example: 'Néo', + }) + @MinLength(1) + @MaxLength(50) + @IsNotEmpty() + @IsString() + pseudo: string; + + @ApiProperty({ + type: String, + description: 'email', + example: 'neo@matrix.fr', + }) + @MaxLength(255) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ + type: String, + description: 'password', + example: 'AAaa11&&&&', + }) + @IsString() + @IsNotEmpty() + password: string; + + @ApiProperty({ + type: String, + description: 'promoCode', + example: 'FILOU20', + }) + @IsOptional() + promoCode: string; +} diff --git a/src/auth/dto/index.ts b/src/auth/dto/index.ts new file mode 100644 index 0000000..e07e0a8 --- /dev/null +++ b/src/auth/dto/index.ts @@ -0,0 +1,2 @@ +export * from './auth.register.dto'; +export * from './auth.login.dto'; diff --git a/src/auth/guard/index.ts b/src/auth/guard/index.ts new file mode 100644 index 0000000..3f1c103 --- /dev/null +++ b/src/auth/guard/index.ts @@ -0,0 +1 @@ +export * from './jwt.guard'; diff --git a/src/auth/guard/jwt.guard.ts b/src/auth/guard/jwt.guard.ts new file mode 100644 index 0000000..52bf0bb --- /dev/null +++ b/src/auth/guard/jwt.guard.ts @@ -0,0 +1,7 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class JwtGuard extends AuthGuard('jwt') { + constructor() { + super(); + } +} diff --git a/src/auth/strategy/index.ts b/src/auth/strategy/index.ts new file mode 100644 index 0000000..e7586f7 --- /dev/null +++ b/src/auth/strategy/index.ts @@ -0,0 +1 @@ +export * from './jwt.strategy'; diff --git a/src/auth/strategy/jwt.strategy.ts b/src/auth/strategy/jwt.strategy.ts new file mode 100644 index 0000000..9bde98d --- /dev/null +++ b/src/auth/strategy/jwt.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PrismaService } from "@/prisma/prisma.service"; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor( + config: ConfigService, + private prisma: PrismaService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.get('JWT_SECRET'), + }); + } + + async validate(payload: { sub: string; email: string }) { + const user = await this.prisma.user.findUnique({ + where: { + id: payload.sub, + }, + }); + delete user.hash; + return user; + } +} diff --git a/src/crypto/crypto.controller.ts b/src/crypto/crypto.controller.ts new file mode 100644 index 0000000..20a3316 --- /dev/null +++ b/src/crypto/crypto.controller.ts @@ -0,0 +1,73 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +import { ApiTags } from '@nestjs/swagger'; +import { User } from '@prisma/client'; +import { CryptoService } from './crypto.service'; +import { CryptoDto } from './dto'; +import { JwtGuard } from 'src/auth/guard'; +import { BuyCryptoDto } from './dto/buy.crypto.dto'; + +@UseGuards(JwtGuard) +@ApiTags('crypto') +@Controller('crypto') +export class CryptoController { + constructor(private cryptoService: CryptoService) {} + + @Get('/all') + getAllPromoCodes(@GetUser() user: User) { + return this.cryptoService.getCryptos(user.id); + } + @Get('/search/:name') + searchCrypto(@GetUser() user: User, @Param('name') cryptoName: string) { + return this.cryptoService.searchCryptos(user.id, cryptoName); + } + + @Get('/history/:id') + CryptoHistory(@GetUser() user: User, @Param('id') cryptoId: string) { + return this.cryptoService.getCryptoHistory(user.id, cryptoId); + } + + @HttpCode(HttpStatus.CREATED) + @Post('/create') + createPromoCode( + @Body() + dto: CryptoDto, + @GetUser() user: User, + ) { + return this.cryptoService.createCrypto(user.id, dto); + } + @Post('/buy') + buyCrypto( + @Body() + dto: BuyCryptoDto, + @GetUser() user: User, + ) { + return this.cryptoService.buyCrypto(user.id, dto); + } + @HttpCode(HttpStatus.OK) + @Patch('/update/:id') + editCryptoById( + @Param('id') cryptoId: string, + @Body() dto: CryptoDto, + @GetUser() user: User, + ) { + return this.cryptoService.editCryptoById(user.id, cryptoId, dto); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('/delete/:id') + deleteOfferById(@Param('id') roleId: string, @GetUser() user: User) { + return this.cryptoService.deleteCryptoById(user.id, roleId); + } +} diff --git a/src/crypto/crypto.module.ts b/src/crypto/crypto.module.ts new file mode 100644 index 0000000..301dce5 --- /dev/null +++ b/src/crypto/crypto.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { CryptoService } from './crypto.service'; +import { CryptoController } from './crypto.controller'; + +@Module({ + providers: [CryptoService], + controllers: [CryptoController], +}) +export class CryptoModule {} diff --git a/src/crypto/crypto.service.ts b/src/crypto/crypto.service.ts new file mode 100644 index 0000000..a2ccc9c --- /dev/null +++ b/src/crypto/crypto.service.ts @@ -0,0 +1,192 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { checkUserHasAccount, checkUserIsAdmin } from 'src/utils/checkUser'; +import { CryptoDto } from './dto'; +import { BuyCryptoDto } from './dto/buy.crypto.dto'; +@Injectable() +export class CryptoService { + constructor(private prisma: PrismaService) {} + + async getCryptos(userId: string) { + await checkUserHasAccount(userId); + + return this.prisma.crypto.findMany({ + orderBy: { + name: 'asc', + }, + }); + } + async searchCryptos(userId: string, cryptoName: string) { + await checkUserHasAccount(userId); + + return this.prisma.crypto.findMany({ + where: { + name: { + contains: cryptoName, + mode: 'insensitive', + }, + }, + orderBy: { + name: 'asc', + }, + }); + } + + async getCryptoHistory(userId: string, cryptoId: string) { + await checkUserHasAccount(userId); + + if (cryptoId) { + return this.prisma.crypto.findMany({ + where: { + id: cryptoId, + }, + + orderBy: { + created_at: 'desc', + }, + take: 50, + }); + } + throw new ForbiddenException('Crypto UUID required'); + } + + async createCrypto(userId: string, dto: CryptoDto) { + await checkUserIsAdmin(userId); + + const existingCryptosWithSameName = await this.prisma.crypto.findMany({ + where: { + name: dto.name, + }, + }); + if (existingCryptosWithSameName.length > 0) { + throw new ForbiddenException('Name already taken'); + } + const crypto = await this.prisma.crypto.create({ + data: { + name: dto.name, + image: dto.image, + value: dto.value, + quantity: dto.quantity, + }, + }); + + return crypto; + } + + async buyCrypto(userId: string, dto: BuyCryptoDto) { + const crypto = await this.prisma.crypto.findFirst({ + where: { + id: dto.id_crypto, + }, + }); + + const user = await this.prisma.user.findFirst({ + where: { + id: userId, + }, + }); + if (crypto.quantity < dto.amount) { + throw new ForbiddenException('No more tokens available'); + } + const necessaryAmount = crypto.value * dto.amount; + console.log(necessaryAmount, user.dollarAvailables); + + if (necessaryAmount > user.dollarAvailables) { + throw new ForbiddenException('Make money first :) '); + } + const userAsset = await this.prisma.userHasCrypto.findFirst({ + where: { + id_crypto: dto.id_crypto, + id_user: userId, + }, + }); + const newBalance = user.dollarAvailables - necessaryAmount; + console.log(newBalance); + + await this.prisma.user.update({ + where: { + id: user.id, + }, + data: { + dollarAvailables: newBalance, + }, + }); + if (userAsset) { + const newBalance = userAsset.amount + dto.amount; + await this.prisma.userHasCrypto.update({ + where: { + id: userAsset.id, + }, + data: { + amount: newBalance, + }, + }); + } else { + await this.prisma.userHasCrypto.create({ + data: { + id_crypto: dto.id_crypto, + id_user: userId, + amount: dto.amount, + }, + }); + } + const newCryptoValue = crypto.value * 1.1; + + await this.prisma.cryptoHistory.create({ + data: { + id_crypto: crypto.id, + value: newCryptoValue, + }, + }); + const newQuantity = (crypto.quantity -= dto.amount); + return this.prisma.crypto.update({ + where: { + id: dto.id_crypto, + }, + data: { + value: newCryptoValue, + quantity: newQuantity, + }, + }); + } + async editCryptoById(userId: string, cryptoId: string, dto: CryptoDto) { + await checkUserIsAdmin(userId); + + const crypto = await this.prisma.crypto.findUnique({ + where: { + id: cryptoId, + }, + }); + + if (!crypto || crypto.id !== cryptoId) + throw new ForbiddenException('Access to resources denied'); + + return this.prisma.crypto.update({ + where: { + id: crypto.id, + }, + data: { + ...dto, + }, + }); + } + + async deleteCryptoById(userId: string, id: string) { + await checkUserIsAdmin(userId); + + const crypto = await this.prisma.crypto.findUnique({ + where: { + id: id, + }, + }); + + if (!crypto || crypto.id !== id) + throw new ForbiddenException('Access to resources denied'); + + await this.prisma.crypto.delete({ + where: { + id: crypto.id, + }, + }); + } +} diff --git a/src/crypto/dto/buy.crypto.dto.ts b/src/crypto/dto/buy.crypto.dto.ts new file mode 100644 index 0000000..4947dbc --- /dev/null +++ b/src/crypto/dto/buy.crypto.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumber, + IsString, + IsUUID, + Max, + MaxLength, + Min, + MinLength, +} from 'class-validator'; +export class BuyCryptoDto { + @ApiProperty({ + type: String, + description: 'Cryptocurrency UUID', + example: '12121-DSZD-E221212-2121221', + }) + @MinLength(1) + @MaxLength(50) + @IsString() + @IsUUID() + id_crypto: string; + + @ApiProperty({ + type: Number, + description: 'Amount of token traded', + example: 2, + }) + @Min(1) + @Max(1000) + @IsNumber() + amount: number; +} diff --git a/src/crypto/dto/crypto.dto.ts b/src/crypto/dto/crypto.dto.ts new file mode 100644 index 0000000..5320e3e --- /dev/null +++ b/src/crypto/dto/crypto.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumber, + IsPositive, + IsString, + IsUrl, + Max, + MaxLength, + Min, + MinLength, +} from 'class-validator'; +export class CryptoDto { + @ApiProperty({ + type: String, + description: 'Cryptocurrency name', + example: 'BTC', + }) + @MaxLength(50) + @MinLength(1) + @IsString() + name: string; + + @ApiProperty({ + type: Number, + description: 'Value for the cryptocurrency in $', + example: 1, + }) + @Min(1) + @Max(10000) + @IsPositive() + @IsNumber() + value: number; + + @ApiProperty({ + type: Number, + description: 'Quantity of tokens available on the platform', + example: 100, + }) + @Min(1) + @Max(10000) + @IsPositive() + @IsNumber() + quantity: number; + + @ApiProperty({ + type: String, + description: 'Image for the cryptocurrency in ', + example: 'https://myImage/com', + }) + @MaxLength(255) + @IsUrl() + @IsString() + image: string; +} diff --git a/src/crypto/dto/index.ts b/src/crypto/dto/index.ts new file mode 100644 index 0000000..737fd9c --- /dev/null +++ b/src/crypto/dto/index.ts @@ -0,0 +1 @@ +export * from './crypto.dto'; diff --git a/src/offer/dto/index.ts b/src/offer/dto/index.ts new file mode 100644 index 0000000..18055bf --- /dev/null +++ b/src/offer/dto/index.ts @@ -0,0 +1 @@ +export * from './offer.dto'; diff --git a/src/offer/dto/offer.dto.ts b/src/offer/dto/offer.dto.ts new file mode 100644 index 0000000..6a2a420 --- /dev/null +++ b/src/offer/dto/offer.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumber, + IsPositive, + IsString, + IsUUID, + Max, + MaxLength, + Min, +} from 'class-validator'; +export class OfferDto { + @ApiProperty({ + type: String, + description: 'Cryptocurrency UUID', + example: '12121-DSZD-E221212-6227933', + }) + @IsString() + @IsUUID() + id_crypto: string; + + @ApiProperty({ + type: 'number', + + description: 'Amount traded ', + example: 21, + }) + @Min(1) + @Max(1000) + @IsNumber() + @IsPositive() + amount: number; +} + diff --git a/src/offer/offer.controller.ts b/src/offer/offer.controller.ts new file mode 100644 index 0000000..65eb13f --- /dev/null +++ b/src/offer/offer.controller.ts @@ -0,0 +1,58 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + UseGuards, + // UseGuards, +} from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +// import { JwtGuard } from '../auth/guard'; +import { ApiTags } from '@nestjs/swagger'; +import { User } from '@prisma/client'; +import { JwtGuard } from 'src/auth/guard'; +import { OfferService } from './offer.service'; +import { OfferDto } from './dto'; + +@UseGuards(JwtGuard) +@ApiTags('offer') +@Controller('offer') +export class OfferController { + constructor(private offerService: OfferService) {} + + @Get('/all') + getAllRoles(@GetUser() user: User) { + return this.offerService.getOffers(user.id); + } + + @HttpCode(HttpStatus.CREATED) + @Post('/create') + createRole( + @Body() + dto: OfferDto, + @GetUser() user: User, + ) { + return this.offerService.createOffer(user.id, dto); + } + + @HttpCode(HttpStatus.OK) + @Patch('/update/:id') + editOfferById( + @Param('id') offerId: string, + @Body() dto: OfferDto, + @GetUser() user: User, + ) { + return this.offerService.editOfferById(user.id, offerId, dto); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('/delete/:id') + deleteOfferById(@Param('id') roleId: string, @GetUser() user: User) { + return this.offerService.deleteOfferById(user.id, roleId); + } +} diff --git a/src/offer/offer.module.ts b/src/offer/offer.module.ts new file mode 100644 index 0000000..77c611a --- /dev/null +++ b/src/offer/offer.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { OfferService } from './offer.service'; +import { OfferController } from './offer.controller'; + +@Module({ + providers: [OfferService], + controllers: [OfferController], +}) +export class OfferModule {} diff --git a/src/offer/offer.service.sql b/src/offer/offer.service.sql new file mode 100644 index 0000000..3388017 --- /dev/null +++ b/src/offer/offer.service.sql @@ -0,0 +1,35 @@ +SELECT + o.amount, + o.created_at, + o.id_user, + c.id AS crypto_id, + c.name AS crypto_name, + c.value AS crypto_value, + c.image AS crypto_image, + c.quantity AS crypto_quantity +FROM + "Offer" o + JOIN + "Crypto" c ON o.id_crypto = c.id +ORDER BY + o.created_at DESC; + +INSERT INTO "Offer" (id, id_crypto, id_user, amount, created_at, updated_at) +VALUES (gen_random_uuid(), 'dto.id_crypto', 'userId', 'dto.amount', NOW(), NOW()); + +SELECT * FROM "Offer" WHERE id = 'offerId'; + +SELECT * FROM "Crypto" WHERE id = 'dto.id_crypto'; + +UPDATE "Offer" +SET + id_crypto = 'dto.id_crypto', + amount = 'dto.amount', + updated_at = NOW() +WHERE + id = 'offerId'; + +SELECT * FROM "Offer" WHERE id = 'id'; + +DELETE FROM "Offer" WHERE id = 'id'; + diff --git a/src/offer/offer.service.ts b/src/offer/offer.service.ts new file mode 100644 index 0000000..cb0ec4c --- /dev/null +++ b/src/offer/offer.service.ts @@ -0,0 +1,88 @@ +import {ForbiddenException, Injectable} from "@nestjs/common"; +import {PrismaService} from "@/prisma/prisma.service"; +import {checkUserHasAccount, checkUserIsAdmin} from "src/utils/checkUser"; +import {OfferDto} from "./dto"; + + +@Injectable() +export class OfferService { + constructor(private prisma: PrismaService) {} + + async getOffers(userId: string) { + await checkUserHasAccount(userId); + return this.prisma.offer.findMany({ + orderBy: { + created_at: 'desc', + }, + select: { + amount: true, + created_at: true, + id_user: true, + Crypto: true, + }, + }); + } + + async createOffer(userId: string, dto: OfferDto) { + await checkUserHasAccount(userId); + return this.prisma.offer.create({ + data: { + id_crypto: dto.id_crypto, + id_user: userId, + amount: dto.amount, + }, + }); + } + + async editOfferById(userId: string, offerId: string, dto: OfferDto) { + await checkUserHasAccount(userId); + + const offer = await this.prisma.offer.findUnique({ + where: { + id: offerId, + }, + }); + + const crypto = await this.prisma.crypto.findUnique({ + where: { + id: dto.id_crypto, + }, + }); + if (!crypto || !crypto.id) { + throw new ForbiddenException('Crypto doesnt exist'); + } + + if (!offer || offer.id !== offerId) + throw new ForbiddenException('Offer id mandatory'); + + return this.prisma.offer.update({ + where: { + id: offerId, + }, + data: { + ...dto, + }, + }); + } + async deleteOfferById(userId: string, id: string) { + await checkUserIsAdmin(userId); + + const offer = await this.prisma.offer.findUnique({ + where: { + id: id, + }, + }); + + if (!offer || offer.id !== id) + throw new ForbiddenException('Access to resources denied'); + + await this.prisma.offer.delete({ + where: { + id: id, + }, + }); + } +} + + + diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..8a3c1aa --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +// biome-ignore lint/style/useImportType: +import { ConfigService } from '@nestjs/config'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient { + constructor(config: ConfigService) { + super({ + datasources: { + db: { + url: config.get('DATABASE_URL'), + }, + }, + }); + } +} + diff --git a/src/promoCode/dto/index.ts b/src/promoCode/dto/index.ts new file mode 100644 index 0000000..cab4b5c --- /dev/null +++ b/src/promoCode/dto/index.ts @@ -0,0 +1 @@ +export * from './promoCode.dto'; diff --git a/src/promoCode/dto/promoCode.dto.ts b/src/promoCode/dto/promoCode.dto.ts new file mode 100644 index 0000000..be05dbc --- /dev/null +++ b/src/promoCode/dto/promoCode.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsNumber, + IsPositive, + IsString, + Max, + MaxLength, + Min, + MinLength, +} from 'class-validator'; +export class PromoCodeDto { + @ApiProperty({ + type: String, + description: 'Name of the PromoCOde', + example: 'FILOU10', + }) + @MinLength(1) + @MaxLength(50) + @IsString() + name: string; + + @ApiProperty({ + type: Number, + description: 'Dollars given for account creation when promoCode applied', + example: 100, + }) + @IsPositive() + @Min(1) + @Max(3000) + @IsNumber() + value: number; +} diff --git a/src/promoCode/promoCode.controller.ts b/src/promoCode/promoCode.controller.ts new file mode 100644 index 0000000..41ba56d --- /dev/null +++ b/src/promoCode/promoCode.controller.ts @@ -0,0 +1,57 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +import { ApiTags } from '@nestjs/swagger'; +import { User } from '@prisma/client'; +import { PromoCodeDto } from './dto'; +import { PromoCodeService } from './promoCode.service'; +import { JwtGuard } from 'src/auth/guard'; + +@UseGuards(JwtGuard) +@ApiTags('promoCode') +@Controller('promoCode') +export class PromoCodeController { + constructor(private promoService: PromoCodeService) {} + + @Get('/all') + getAllPromoCodes(@GetUser() user: User) { + return this.promoService.getPromoCodes(user.id); + } + + @HttpCode(HttpStatus.CREATED) + @Post('/create') + createPromoCode( + // @GetUser() user: User, + @Body() + dto: PromoCodeDto, + @GetUser() user: User, + ) { + return this.promoService.createPromoCode(user.id, dto); + } + + @HttpCode(HttpStatus.OK) + @Patch('/update/:id') + editPromoCodeById( + @Param('id') promoCodeId: string, + @Body() dto: PromoCodeDto, + @GetUser() user: User, + ) { + return this.promoService.editPromoCodeById(user.id, promoCodeId, dto); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('/delete/:id') + deletePromoCodeById(@Param('id') promoCodeId: string, @GetUser() user: User) { + return this.promoService.deletePromoCodeById(user.id, promoCodeId); + } +} diff --git a/src/promoCode/promoCode.module.ts b/src/promoCode/promoCode.module.ts new file mode 100644 index 0000000..7f8471f --- /dev/null +++ b/src/promoCode/promoCode.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PromoCodeController } from './promoCode.controller'; +import { PromoCodeService } from './promoCode.service'; + +@Module({ + providers: [PromoCodeService], + controllers: [PromoCodeController], +}) +export class PromoCodeModule {} diff --git a/src/promoCode/promoCode.service.spec.ts b/src/promoCode/promoCode.service.spec.ts new file mode 100644 index 0000000..e5700e8 --- /dev/null +++ b/src/promoCode/promoCode.service.spec.ts @@ -0,0 +1,71 @@ +import { ForbiddenException } from '@nestjs/common'; +// biome-ignore lint/style/useImportType: +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from "@/prisma/prisma.service"; +import { PromoCodeService } from './promoCode.service'; +import { checkUserIsAdmin } from "@/utils/checkUser"; + +jest.mock('../utils/checkUser'); + +describe('PromoCodeService', () => { + let service: PromoCodeService; + let prisma: PrismaService; + + const mockPrismaService = { + promoCode: { + findMany: jest.fn(), + create: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PromoCodeService, + { provide: PrismaService, useValue: mockPrismaService }, + ], + }).compile(); + + service = module.get(PromoCodeService); + prisma = module.get(PrismaService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getPromoCodes', () => { + it('should get promo codes if user is admin', async () => { + (checkUserIsAdmin as jest.Mock).mockResolvedValue(true); + const mockPromoCodes = [ + { id: '1', name: 'PROMO10', value: 10 }, + { id: '2', name: 'PROMO20', value: 20 }, + ]; + mockPrismaService.promoCode.findMany.mockResolvedValue(mockPromoCodes); + + const result = await service.getPromoCodes('user-id'); + + expect(checkUserIsAdmin).toHaveBeenCalledWith('user-id'); + expect(prisma.promoCode.findMany).toHaveBeenCalledWith({ + orderBy: { name: 'asc' }, + select: { id: true, name: true, value: true }, + }); + expect(result).toEqual(mockPromoCodes); + }); + + it('should throw ForbiddenException if user is not admin', async () => { + (checkUserIsAdmin as jest.Mock).mockRejectedValue(new ForbiddenException('Not an admin')); + + await expect(service.getPromoCodes('user-id')).rejects.toThrow( + ForbiddenException, + ); + + expect(checkUserIsAdmin).toHaveBeenCalledWith('user-id'); + expect(prisma.promoCode.findMany).not.toHaveBeenCalled(); + }); + }); +}); + diff --git a/src/promoCode/promoCode.service.ts b/src/promoCode/promoCode.service.ts new file mode 100644 index 0000000..9286c6f --- /dev/null +++ b/src/promoCode/promoCode.service.ts @@ -0,0 +1,79 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { PromoCodeDto } from './dto'; +import { checkUserIsAdmin } from '../utils/checkUser'; +@Injectable() +export class PromoCodeService { + constructor(private prisma: PrismaService) {} + + async getPromoCodes(userId: string) { + await checkUserIsAdmin(userId); + + return this.prisma.promoCode.findMany({ + orderBy: { + name: 'asc', + }, + select: { + id: true, + name: true, + value: true, + }, + }); + } + + async createPromoCode(userId: string, dto: PromoCodeDto) { + await checkUserIsAdmin(userId); + + const promoCode = await this.prisma.promoCode.create({ + data: { + name: dto.name, + value: dto.value, + }, + }); + + return promoCode; + } + async editPromoCodeById( + userId: string, + promoCodeId: string, + dto: PromoCodeDto, + ) { + await checkUserIsAdmin(userId); + + const promoCode = await this.prisma.promoCode.findUnique({ + where: { + id: promoCodeId, + }, + }); + + if (!promoCode || promoCode.id !== promoCodeId) + throw new ForbiddenException('Access to resources denied'); + + return this.prisma.promoCode.update({ + where: { + id: promoCode.id, + }, + data: { + ...dto, + }, + }); + } + async deletePromoCodeById(userId: string, id: string) { + await checkUserIsAdmin(userId); + + const promoCode = await this.prisma.promoCode.findUnique({ + where: { + id: id, + }, + }); + + if (!promoCode || promoCode.id !== id) + throw new ForbiddenException('Access to resources denied'); + + await this.prisma.promoCode.delete({ + where: { + id: promoCode.id, + }, + }); + } +} diff --git a/src/role/dto/index.ts b/src/role/dto/index.ts new file mode 100644 index 0000000..985a22d --- /dev/null +++ b/src/role/dto/index.ts @@ -0,0 +1 @@ +export * from './role.dto'; diff --git a/src/role/dto/role.dto.ts b/src/role/dto/role.dto.ts new file mode 100644 index 0000000..c41fa02 --- /dev/null +++ b/src/role/dto/role.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, MaxLength, MinLength } from 'class-validator'; +export class RoleDto { + @ApiProperty({ + type: String, + description: 'Role Name', + example: 'user', + }) + @MinLength(1) + @MaxLength(50) + @IsString() + name: string; +} diff --git a/src/role/role.controller.ts b/src/role/role.controller.ts new file mode 100644 index 0000000..f0c090f --- /dev/null +++ b/src/role/role.controller.ts @@ -0,0 +1,63 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + UseGuards, + // UseGuards, +} from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +// import { JwtGuard } from '../auth/guard'; +import { RoleDto } from './dto'; +import { RoleService } from './role.service'; +import { ApiTags } from '@nestjs/swagger'; +import { User } from '@prisma/client'; +import { JwtGuard } from 'src/auth/guard'; + +@UseGuards(JwtGuard) +@ApiTags('role') +@Controller('role') +export class RoleController { + constructor(private roleService: RoleService) {} + + @Get('/all') + getAllRoles(@GetUser() user: User) { + return this.roleService.getRolesAdmin(user.id); + } + // @Get('/cm/all') + // getRolesCm(@GetUser() user: User) { + // return this.roleService.getRolesCm(user) + // } + + @HttpCode(HttpStatus.CREATED) + @Post('/create') + createRole( + // @GetUser() user: User, + @Body() + dto: RoleDto, + @GetUser() user: User, + ) { + return this.roleService.createRole(user.id, dto); + } + + @HttpCode(HttpStatus.OK) + @Patch('/update/:id') + editRoleById( + @Param('id') roleId: string, + @Body() dto: RoleDto, + @GetUser() user: User, + ) { + return this.roleService.editRoleById(user.id, roleId, dto); + } + + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('/delete/:id') + deleteRoleById(@Param('id') roleId: string, @GetUser() user: User) { + return this.roleService.deleteRoleById(user.id, roleId); + } +} diff --git a/src/role/role.module.ts b/src/role/role.module.ts new file mode 100644 index 0000000..ca3af9e --- /dev/null +++ b/src/role/role.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { RoleController } from './role.controller'; +import { RoleService } from './role.service'; +@Module({ + providers: [RoleService], + controllers: [RoleController], +}) +export class RoleModule {} diff --git a/src/role/role.service.ts b/src/role/role.service.ts new file mode 100644 index 0000000..01268a7 --- /dev/null +++ b/src/role/role.service.ts @@ -0,0 +1,72 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { RoleDto } from './dto'; +import { checkUserIsAdmin } from 'src/utils/checkUser'; +// import { checkRoleLevel, checkUserIsStaff } from 'src/utils/checkUser'; +@Injectable() +export class RoleService { + constructor(private prisma: PrismaService) {} + + async getRolesAdmin(userId: string) { + await checkUserIsAdmin(userId); + return this.prisma.role.findMany({ + orderBy: { + name: 'asc', + }, + select: { + id: true, + name: true, + }, + }); + } + + async createRole(userId: string, dto: RoleDto) { + await checkUserIsAdmin(userId); + const role = await this.prisma.role.create({ + data: { + name: dto.name, + }, + }); + + return role; + } + async editRoleById(userId: string, roleId: string, dto: RoleDto) { + await checkUserIsAdmin(userId); + + const role = await this.prisma.role.findUnique({ + where: { + id: roleId, + }, + }); + + if (!role || role.id !== roleId) + throw new ForbiddenException('Access to resources denied'); + + return this.prisma.role.update({ + where: { + id: roleId, + }, + data: { + ...dto, + }, + }); + } + async deleteRoleById(userId: string, id: string) { + await checkUserIsAdmin(userId); + + const role = await this.prisma.role.findUnique({ + where: { + id: id, + }, + }); + + if (!role || role.id !== id) + throw new ForbiddenException('Access to resources denied'); + + await this.prisma.role.delete({ + where: { + id: id, + }, + }); + } +} diff --git a/src/trade/dto/index.ts b/src/trade/dto/index.ts new file mode 100644 index 0000000..3943059 --- /dev/null +++ b/src/trade/dto/index.ts @@ -0,0 +1 @@ +export * from './trade.dto'; diff --git a/src/trade/dto/trade.dto.ts b/src/trade/dto/trade.dto.ts new file mode 100644 index 0000000..212c248 --- /dev/null +++ b/src/trade/dto/trade.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; +export class TradeDto { + @ApiProperty({ + type: String, + description: 'Offer UUID ', + example: '121212-DSDZ1-21212DJDZ-31313', + }) + @IsUUID() + @IsNotEmpty() + @IsString() + id_offer: string; +} diff --git a/src/trade/trade.controller.ts b/src/trade/trade.controller.ts new file mode 100644 index 0000000..8c09a35 --- /dev/null +++ b/src/trade/trade.controller.ts @@ -0,0 +1,38 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Post, + UseGuards, +} from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +import { ApiTags } from '@nestjs/swagger'; +import { User } from '@prisma/client'; +import { TradeService } from './trade.service'; +import { TradeDto } from './dto'; +import { JwtGuard } from 'src/auth/guard'; + +@UseGuards(JwtGuard) +@ApiTags('trade') +@Controller('trade') +export class TradeController { + constructor(private tradeService: TradeService) {} + + @Get('/all') + getAllPromoCodes(@GetUser() user: User) { + return this.tradeService.getTrades(user.id); + } + + @HttpCode(HttpStatus.CREATED) + @Post('/create') + createPromoCode( + // @GetUser() user: User, + @Body() + dto: TradeDto, + @GetUser() user: User, + ) { + return this.tradeService.createTrade(user.id, dto); + } +} diff --git a/src/trade/trade.module.ts b/src/trade/trade.module.ts new file mode 100644 index 0000000..ea7c0a9 --- /dev/null +++ b/src/trade/trade.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TradeService } from './trade.service'; +import { TradeController } from './trade.controller'; + +@Module({ + providers: [TradeService], + controllers: [TradeController], +}) +export class TradeModule {} diff --git a/src/trade/trade.service.ts b/src/trade/trade.service.ts new file mode 100644 index 0000000..adce301 --- /dev/null +++ b/src/trade/trade.service.ts @@ -0,0 +1,173 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { checkUserHasAccount, checkUserIsAdmin } from 'src/utils/checkUser'; +import { TradeDto } from './dto'; +@Injectable() +export class TradeService { + constructor(private prisma: PrismaService) {} + + async getTrades(userId: string) { + await checkUserIsAdmin(userId); + + return this.prisma.trade.findMany({ + orderBy: { + created_at: 'desc', + }, + // include: { + // Giver: true, + // Receiver: true, + // Crypto: true, + // }, + + select: { + Giver: { + select: { + firstName: true, + lastName: true, + pseudo: true, + dollarAvailables: true, + }, + }, + Receiver: { + select: { + firstName: true, + lastName: true, + pseudo: true, + dollarAvailables: true, + }, + }, + Crypto: true, + }, + }); + } + async getLastTrades() { + return this.prisma.trade.findMany({ + orderBy: { + created_at: 'desc', + }, + select: { + amount_traded: true, + Crypto: true, + }, + }); + } + + async createTrade(userId: string, dto: TradeDto) { + await checkUserHasAccount(userId); + + const offer = await this.prisma.offer.findUnique({ + where: { + id: dto.id_offer, + }, + }); + const crypto = await this.prisma.crypto.findFirst({ + where: { + id: offer.id_crypto, + }, + }); + + const buyer = await this.prisma.user.findFirst({ + where: { + id: offer.id_user, + }, + }); + + const price = crypto.value * offer.amount; + if (buyer.dollarAvailables < price) { + throw new ForbiddenException( + `Acqueror ${buyer.pseudo} doesnt have enough money to make this trade`, + ); + } + + const asset = await this.prisma.userHasCrypto.findFirst({ + where: { + id_crypto: offer.id_crypto, + id_user: offer.id_user, + }, + }); + + if (!asset || asset.amount < offer.amount) { + throw new ForbiddenException(`Seller doesnt have enough ${crypto.name} `); + } + + const trade = await this.prisma.trade.create({ + data: { + id_giver: offer.id_user, + id_receiver: userId, + id_crypto: offer.id_crypto, + amount_traded: offer.amount, + }, + }); + + const newBalanceGiver = (asset.amount -= offer.amount); + await this.prisma.userHasCrypto.update({ + where: { + id: asset.id, + }, + data: { + amount: newBalanceGiver, + }, + }); + + const receiverAssets = await this.prisma.userHasCrypto.findFirst({ + where: { + id_user: userId, + id_crypto: offer.id_crypto, + }, + }); + if (!receiverAssets) { + await this.prisma.userHasCrypto.create({ + data: { + id_user: userId, + id_crypto: offer.id_crypto, + amount: offer.amount, + createdAt: new Date(), + }, + }); + } else { + const newBalanceReceiver = receiverAssets.amount + offer.amount; + await this.prisma.userHasCrypto.update({ + where: { + id: receiverAssets.id, + }, + data: { + amount: newBalanceReceiver, + }, + }); + } + + const newValue = crypto.value * 1.1; + await this.prisma.cryptoHistory.create({ + data: { + id_crypto: crypto.id, + value: newValue, + }, + }); + + await this.prisma.crypto.update({ + where: { + id: crypto.id, + }, + data: { + value: newValue, + }, + }); + + const prevAmount = buyer.dollarAvailables; + + await this.prisma.user.update({ + where: { + id: userId, + }, + data: { + dollarAvailables: prevAmount - price, + }, + }); + await this.prisma.offer.delete({ + where: { + id: offer.id, + }, + }); + return trade; + } +} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts new file mode 100644 index 0000000..150b915 --- /dev/null +++ b/src/user/user.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { GetUser } from '../auth/decorator'; +import { JwtGuard } from '../auth/guard'; +import { ApiTags } from '@nestjs/swagger'; +import { UserService } from './user.service'; +import { User } from '@prisma/client'; + +@ApiTags('user') +@UseGuards(JwtGuard) +@Controller('user') +export class UserController { + constructor(private userService: UserService) {} + + @Get('/my-assets') + GetMyAssets( + @GetUser() + user: User, + ) { + return this.userService.getMyAssets(user.id); + } + + @Get('/users-assets') + GetAlLAssets( + @GetUser() + user: User, + ) { + return this.userService.getUsersAssets(user.id); + } + @Get('/my-trades') + GetMyTrades( + @GetUser() + user: User, + ) { + return this.userService.getMyTrades(user.id); + } +} diff --git a/src/user/user.module.ts b/src/user/user.module.ts new file mode 100644 index 0000000..33452f9 --- /dev/null +++ b/src/user/user.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +@Module({ + providers: [UserService], + controllers: [UserController], +}) +export class UserModule {} diff --git a/src/user/user.service.ts b/src/user/user.service.ts new file mode 100644 index 0000000..d397021 --- /dev/null +++ b/src/user/user.service.ts @@ -0,0 +1,81 @@ +import {Injectable} from "@nestjs/common"; +import {PrismaService} from "@/prisma/prisma.service"; +import {checkUserHasAccount, checkUserIsAdmin} from "src/utils/checkUser"; + + +@Injectable() +export class UserService { + constructor(private prisma: PrismaService) {} + + /** + * Retrieves the assets of a given user, including their first name, last name, available dollars, + * pseudo, and associated cryptocurrencies. + * + * @param {string} userId - The unique identifier of the user whose assets are being retrieved. + * @return A promise that resolves to an object containing the user's assets and associated data. + */ + async getMyAssets(userId: string) { + await checkUserHasAccount(userId); + + return this.prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + firstName: true, + lastName: true, + dollarAvailables: true, + pseudo: true, + UserHasCrypto: { + select: { + Crypto: true, + }, + }, + }, + }); + } + + /** + * Retrieves the assets of users based on user ID. + * + * @param {string} userId - The ID of the user requesting the assets. + * @return A promise that resolves to an array of user assets. + */ + async getUsersAssets(userId: string) { + await checkUserIsAdmin(userId); + + return this.prisma.user.findMany({ + select: { + firstName: true, + lastName: true, + pseudo: true, + dollarAvailables: true, + UserHasCrypto: { + select: { + Crypto: true, + amount: true, + }, + }, + }, + take: 20, + }); + } + + /** + * Fetches all trades associated with a given user. + * + * @param {string} userId - The unique identifier of the user. + * @return A promise that resolves to an array of trade objects. + */ + async getMyTrades(userId: string) { + await checkUserHasAccount(userId); + return this.prisma.trade.findMany({ + where: { + OR: [{id_giver: userId}, {id_receiver: userId}], + }, + include: { + Crypto: true, + }, + }); + } +} diff --git a/src/utils/checkUser.ts b/src/utils/checkUser.ts new file mode 100644 index 0000000..024c384 --- /dev/null +++ b/src/utils/checkUser.ts @@ -0,0 +1,86 @@ +import { ForbiddenException } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { Roles } from './const/const'; + +const prisma = new PrismaClient(); + +export async function checkRoleLevel(userId: string, level: string) { + if (!userId || !level) { + throw new ForbiddenException('Access to resources denied'); + } + + checkRoleExist(level); + + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + }); + if (user?.roleId) { + const role = await prisma.role.findFirst({ + where: { + id: user.roleId, + }, + }); + + if (role?.id) { + checkRoleExist(role.name); + if (level === Roles.ADMIN && role.name !== Roles.ADMIN) { + throw new ForbiddenException('Access to resources denied'); + } + } else { + throw new ForbiddenException('Access to resources denied'); + } + } else { + throw new ForbiddenException('Access to resources denied'); + } +} + +function checkRoleExist(role: string) { + switch (role) { + case Roles.ADMIN: + case Roles.USER: + break; + default: + throw new ForbiddenException('Access to resources denied'); + } +} + +export async function checkUserHasAccount(jwtId: string) { + if (jwtId) { + const user = await prisma.user.findUnique({ + where: { + id: jwtId, + isActive: true, + }, + }); + if (!user || !user.id) { + throw new ForbiddenException('Access to resources denied'); + } + } else { + throw new ForbiddenException('Access to resources denied'); + } +} + +export async function checkUserIsAdmin(jwtId: string) { + if (jwtId) { + const user = await prisma.user.findUnique({ + where: { + id: jwtId, + isActive: true, + }, + include: { + Role: true, + }, + }); + if (!user || !user.id) { + throw new ForbiddenException('Access to resources denied2'); + } + + if (user.Role.name !== Roles.ADMIN) { + throw new ForbiddenException('Access to resources denied3'); + } + } else { + throw new ForbiddenException('Access to resources denied4'); + } +} diff --git a/src/utils/const/const.ts b/src/utils/const/const.ts new file mode 100644 index 0000000..befe0aa --- /dev/null +++ b/src/utils/const/const.ts @@ -0,0 +1,4 @@ +export const Roles = { + ADMIN: 'admin', + USER: 'user', +}; diff --git a/src/utils/styles.ts b/src/utils/styles.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/tests/user-mock.ts b/src/utils/tests/user-mock.ts new file mode 100644 index 0000000..450d30b --- /dev/null +++ b/src/utils/tests/user-mock.ts @@ -0,0 +1,16 @@ +// auth/test/mocks.ts +import { User } from '@prisma/client'; + +export const getMockUser = (): User => ({ + id: 'user-id', + email: 'test@example.com', + firstName: 'Test User', + lastName: 'Test', + hash: 'test-hash', + pseudo: 'test-pseudo', + isActive: true, + created_at: new Date(), + updated_at: new Date(), + roleId: 'user', + dollarAvailables: 1000, +}); diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..e69de29