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.
This commit is contained in:
Mathis H (Avnyr) 2024-11-12 13:30:05 +01:00
parent 313a51f8db
commit fa91630a29
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
54 changed files with 1733 additions and 2 deletions

View File

@ -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>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View File

@ -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';
}
}

38
src/app.module.ts Normal file
View File

@ -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 {}

8
src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -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);
}
}

12
src/auth/auth.module.ts Normal file
View File

@ -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 {}

View File

@ -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,

View File

@ -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;
},
);

View File

@ -0,0 +1 @@
export * from './get-user.decorator';

View File

@ -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;
}

View File

@ -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;
}

2
src/auth/dto/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './auth.register.dto';
export * from './auth.login.dto';

1
src/auth/guard/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './jwt.guard';

View File

@ -0,0 +1,7 @@
import { AuthGuard } from '@nestjs/passport';
export class JwtGuard extends AuthGuard('jwt') {
constructor() {
super();
}
}

View File

@ -0,0 +1 @@
export * from './jwt.strategy';

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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,
},
});
}
}

View File

@ -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;
}

View File

@ -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;
}

1
src/crypto/dto/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './crypto.dto';

1
src/offer/dto/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './offer.dto';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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';

View File

@ -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,
},
});
}
}

View File

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@ -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'),
},
},
});
}
}

View File

@ -0,0 +1 @@
export * from './promoCode.dto';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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>(PromoCodeService);
prisma = module.get<PrismaService>(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();
});
});
});

View File

@ -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,
},
});
}
}

1
src/role/dto/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './role.dto';

13
src/role/dto/role.dto.ts Normal file
View File

@ -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;
}

View File

@ -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);
}
}

8
src/role/role.module.ts Normal file
View File

@ -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 {}

72
src/role/role.service.ts Normal file
View File

@ -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,
},
});
}
}

1
src/trade/dto/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './trade.dto';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

173
src/trade/trade.service.ts Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

8
src/user/user.module.ts Normal file
View File

@ -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 {}

81
src/user/user.service.ts Normal file
View File

@ -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,
},
});
}
}

86
src/utils/checkUser.ts Normal file
View File

@ -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');
}
}

4
src/utils/const/const.ts Normal file
View File

@ -0,0 +1,4 @@
export const Roles = {
ADMIN: 'admin',
USER: 'user',
};

0
src/utils/styles.ts Normal file
View File

View File

@ -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,
});

0
src/utils/types.ts Normal file
View File