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