Compare commits

...

4 Commits

Author SHA1 Message Date
8ea217fe9f
Normalize quote usage in imports
Standardized the quote style to double quotes across all TypeScript files for consistency. This includes ".ts" and ".dto" files.
2024-11-12 13:37:29 +01:00
2fdc16e003
Add initial seeding script for roles and promo codes
This commit introduces a new seed script located at prisma/seed.ts. The script seeds the database with default roles ('user' and 'admin') and a sample promo code ('PROMO1000' with a value of 1000). This setup helps initialize essential data for application functionality.
2024-11-12 13:30:21 +01:00
fa91630a29
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.
2024-11-12 13:30:05 +01:00
313a51f8db
Add various UI components including ToggleGroup and RadioGroup
This commit introduces multiple new UI components for the project. The added components include ToggleGroup, RadioGroup, Pagination, ToastBox, Textarea, Breadcrumb, Skeleton, Collapsible, Checkbox, Calendar, Accordion, Sonner, and CopyButton. These components enhance the flexibility and functionality of the UI, providing essential elements for better user interaction.
2024-11-08 14:37:06 +01:00
57 changed files with 1880 additions and 112 deletions

14
.env.example Normal file
View File

@ -0,0 +1,14 @@
PORT=3333
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5434
POSTGRES_DATABASE=
POSTGRES_USER=
POSTGRES_PASSWORD=
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}?schema=public&connection_limit=1
SMTP_PASSWORD=""
SMTP_EMAIL=""
SMTP_HOST=""
JWT_SECRET=

34
prisma/seed.ts Normal file
View File

@ -0,0 +1,34 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.role.create({
data: {
name: 'user',
},
});
await prisma.role.create({
data: {
name: 'admin',
},
});
await prisma.promoCode.create({
data: {
name: 'PROMO1000',
value: 1000,
},
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

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 { AppController } from "@/app.controller";
import { AppService } from "@/app.service";
import { AuthModule } from "@/auth/auth.module";
import { CryptoModule } from "@/crypto/crypto.module";
import { OfferModule } from "@/offer/offer.module";
import { PrismaModule } from "@/prisma/prisma.module";
import { PromoCodeModule } from "@/promoCode/promoCode.module";
import { RoleModule } from "@/role/role.module";
import { TradeModule } from "@/trade/trade.module";
import { UserModule } from "@/user/user.module";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
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 { ApiTags } from "@nestjs/swagger";
import { AuthService } from "./auth.service";
import { AuthLoginDto, AuthRegisterDto } from "./dto";
@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

@ -1,10 +1,10 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthLoginDto, AuthRegisterDto } from './dto';
import * as argon from 'argon2';
import { Prisma, User } from '@prisma/client';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ForbiddenException, Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Prisma, User } from "@prisma/client";
import * as argon from "argon2";
import { PrismaService } from "../prisma/prisma.service";
import { AuthLoginDto, AuthRegisterDto } from "./dto";
@Injectable()
export class AuthService {
@ -19,7 +19,7 @@ export class AuthService {
async signup(dto: AuthRegisterDto) {
const hash = await argon.hash(dto.password);
const promoCode = await this.getPromoCode(dto.promoCode);
const userRole = await this.getUserRole('user');
const userRole = await this.getUserRole("user");
const balance = this.calculateBalance(promoCode);
try {
@ -50,16 +50,19 @@ export class AuthService {
return balance;
}
private async createUser(dto: AuthRegisterDto, hash: string, roleId: string, balance: number) {
private async createUser(
dto: AuthRegisterDto,
hash: string,
roleId: string,
balance: number,
) {
return this.prisma.user.create({
data: {
firstName: dto.firstName,
lastName: dto.lastName,
pseudo: dto.pseudo,
city: dto.city,
email: dto.email,
hash,
age: dto.age,
roleId,
isActive: true,
dollarAvailables: balance,
@ -69,8 +72,8 @@ export class AuthService {
private handleSignupError(error: any) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ForbiddenException('Credentials taken');
if (error.code === "P2002") {
throw new ForbiddenException("Credentials taken");
}
}
throw error;
@ -85,10 +88,10 @@ export class AuthService {
},
});
if (!userDatas) throw new ForbiddenException('Credentials incorrect');
if (!userDatas) throw new ForbiddenException("Credentials incorrect");
const pwMatches = await argon.verify(userDatas.hash, dto.password);
if (!pwMatches) throw new ForbiddenException('Credentials incorrect');
if (!pwMatches) throw new ForbiddenException("Credentials incorrect");
return this.signToken(userDatas);
}
@ -96,9 +99,9 @@ export class AuthService {
async signToken(user: any): Promise<{ access_token: string; user: User }> {
const payload = { sub: user.id, email: user.email };
user.hash = null;
const secret = this.config.get('JWT_SECRET');
const secret = this.config.get("JWT_SECRET");
const token = await this.jwt.signAsync(payload, {
expiresIn: '30d',
expiresIn: "30d",
secret: secret,
});

View File

@ -0,0 +1,11 @@
import { ExecutionContext, createParamDecorator } 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 { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
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 { PrismaService } from "@/prisma/prisma.service";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
@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 { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { JwtGuard } from "src/auth/guard";
import { GetUser } from "../auth/decorator";
import { CryptoService } from "./crypto.service";
import { CryptoDto } from "./dto";
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 { CryptoController } from "./crypto.controller";
import { CryptoService } from "./crypto.service";
@Module({
providers: [CryptoService],
controllers: [CryptoController],
})
export class CryptoModule {}

View File

@ -0,0 +1,192 @@
import { ForbiddenException, Injectable } from "@nestjs/common";
import { checkUserHasAccount, checkUserIsAdmin } from "src/utils/checkUser";
import { PrismaService } from "../prisma/prisma.service";
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";

View File

@ -1,19 +1,19 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
const config = new DocumentBuilder()
.setTitle('Neptune API')
.setDescription('A fictive app')
.setVersion('1.0')
.setTitle("Neptune API")
.setDescription("A fictive app")
.setVersion("1.0")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
SwaggerModule.setup("api", app, document);
app.useGlobalPipes(
new ValidationPipe({

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

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

View File

@ -0,0 +1,32 @@
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 { JwtGuard } from '../auth/guard';
import { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { JwtGuard } from "src/auth/guard";
import { GetUser } from "../auth/decorator";
import { OfferDto } from "./dto";
import { OfferService } from "./offer.service";
@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 { OfferController } from "./offer.controller";
import { OfferService } from "./offer.service";
@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,84 @@
import { PrismaService } from "@/prisma/prisma.service";
import { ForbiddenException, Injectable } from "@nestjs/common";
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,17 @@
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 { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { JwtGuard } from "src/auth/guard";
import { GetUser } from "../auth/decorator";
import { PromoCodeDto } from "./dto";
import { PromoCodeService } from "./promoCode.service";
@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,70 @@
import { PrismaService } from "@/prisma/prisma.service";
import { checkUserIsAdmin } from "@/utils/checkUser";
import { ForbiddenException } from "@nestjs/common";
// biome-ignore lint/style/useImportType:
import { Test, TestingModule } from "@nestjs/testing";
import { PromoCodeService } from "./promoCode.service";
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,75 @@
import { ForbiddenException, Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { checkUserIsAdmin } from "../utils/checkUser";
import { PromoCodeDto } from "./dto";
@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,59 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
// UseGuards,
} from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { JwtGuard } from "src/auth/guard";
import { GetUser } from "../auth/decorator";
// import { JwtGuard } from '../auth/guard';
import { RoleDto } from "./dto";
import { RoleService } from "./role.service";
@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 { checkUserIsAdmin } from "src/utils/checkUser";
import { PrismaService } from "../prisma/prisma.service";
import { RoleDto } from "./dto";
// 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 { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { JwtGuard } from "src/auth/guard";
import { GetUser } from "../auth/decorator";
import { TradeDto } from "./dto";
import { TradeService } from "./trade.service";
@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 { TradeController } from "./trade.controller";
import { TradeService } from "./trade.service";
@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 { checkUserHasAccount, checkUserIsAdmin } from "src/utils/checkUser";
import { PrismaService } from "../prisma/prisma.service";
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 { ApiTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { GetUser } from "../auth/decorator";
import { JwtGuard } from "../auth/guard";
import { UserService } from "./user.service";
@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 { UserController } from "./user.controller";
import { UserService } from "./user.service";
@Module({
providers: [UserService],
controllers: [UserController],
})
export class UserModule {}

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

@ -0,0 +1,80 @@
import { PrismaService } from "@/prisma/prisma.service";
import { Injectable } from "@nestjs/common";
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