Compare commits

..

17 Commits

Author SHA1 Message Date
cc296d739b Update IDE config and seed file with additional data
Adjusted .idea/workspace.xml for correct recent project path and tasks. Updated seed.ts to include new cryptocurrency records and modified existing promo code details.
2024-11-27 10:55:28 +01:00
55ec13c0a7 Fix offer retrieval method and add GDPR acknowledgment
Return the result from `getOffersById` method to fix route handling. Add `id` field in offers selection query and introduce `gdpr_acknowledgement` to the user model and mock data for GDPR compliance.
2024-11-24 23:49:59 +01:00
ebdc3d0109 Enable user crypto amount selection and update deployment docs
Added the ability to select user crypto amounts in the user service. Removed commented-out code in the offer controller for clarity. Updated the README with deployment and production instructions.
2024-11-22 11:06:50 +01:00
ecba9a3377 Add E2E tests and update dependencies
Implemented end-to-end tests for the app and introduced path aliases in `tsconfig.json`. Updated `pnpm-lock.yaml` with the latest dependency versions. Added a new service method to fetch offers by user ID and crypto ID.
2024-11-14 11:11:21 +01:00
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
d73aecd2ab Add initial Prisma schema
Set up Prisma models for Crypto, CryptoHistory, Offer, PromoCode, Role, Trade, User, and UserHasCrypto. This includes defining fields, relationships, and default values for each model, and configuring PostgreSQL as the database provider.
2024-11-08 09:35:32 +01:00
cfcc998271 Add AuthService for user registration and login
Implement user signup and signin functionality with password hashing, role assignment, and JWT token generation. Integrate promo code for initial balance adjustment, and handle unique credential errors.
2024-10-31 11:49:03 +01:00
747b4357f9 Remove trailing slash from .gitignore prisma/migrations entry
The trailing slash was removed to accurately match the directory during ignore operations. This ensures that the .gitignore works as expected for the prisma migrations folder.
2024-10-31 11:45:49 +01:00
1f7ed6e03c Add .gitignore file
Created a .gitignore file to exclude compiled output, logs, OS-specific files, test directories, editor settings, environment variable files, temporary directories, runtime data, and diagnostic reports from version control. This helps maintain a cleaner repository and prevents the inclusion of unnecessary or sensitive files.
2024-10-31 11:44:39 +01:00
5666c16d00 Add main application bootstrap logic
Introduce the main.ts file to set up the NestJS application. This includes enabling CORS, configuring Swagger documentation, and setting up global validation pipes.
2024-10-31 11:43:49 +01:00
272d3ec50a Add nest-cli.json configuration file
This adds a new configuration file for the Nest CLI to the project. The file specifies the schema, schematics collection, source root directory, and compiler options, including deleting the output directory before building.
2024-10-31 11:43:38 +01:00
57d9845d19 Add pnpm-lock.yaml file
Introduce a new pnpm-lock.yaml file to manage dependency versions and settings. This change ensures consistency across installations by locking the specific versions of dependencies.
2024-10-31 11:43:27 +01:00
80c8662ed1 Add tsconfig and tsconfig.build configuration files
Introduced `tsconfig.json` with TypeScript compiler options tailored for the project. Additionally, created `tsconfig.build.json` to extend the main config for build-specific settings and exclusions. This setup ensures a streamlined compilation process and cleaner build output.
2024-10-31 11:42:56 +01:00
0fb7c3e288 Add docker-compose file for PostgreSQL service
Introduces a docker-compose.yaml file to set up a PostgreSQL container named "neptune-db". Configures the container with environment variables and volumes for persistent data storage.
2024-10-31 11:41:42 +01:00
69 changed files with 8347 additions and 1 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=

57
.gitignore vendored Normal file
View File

@@ -0,0 +1,57 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
/prisma/migrations

View File

@@ -1,3 +1,25 @@
# neptune-back
The project to validate my DWWM diploma on the back end.
The project to validate my DWWM diploma on the back end.
## Déploiement
- Configurer les variables d'environnements
- Mettre en ligne la base de donnée
> Si en production lancée avec le reste de l'application via docker-compose.yml
```shell
pnpm install
pnpm exec prisma generate
pnpm exec prisma migrate deploy
```
### En dèveloppement
```shell
pnpm start:dev
```
### En mode production
```shell
pnpm build
pnpm start:prod
```

45
biome.json Normal file
View File

@@ -0,0 +1,45 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
},
"vcs": {
"enabled": true,
"clientKind": "git"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"performance": {
"recommended": true,
"noDelete": "off"
},
"suspicious": {
"noExplicitAny": "warn"
},
"complexity": {
"useLiteralKeys": "off"
},
"style": {
"useImportType": "off"
}
}
},
"formatter": {
"indentStyle": "tab",
"indentWidth": 2,
"lineWidth": 90
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
}
}
}

14
docker-compose.yaml Normal file
View File

@@ -0,0 +1,14 @@
services:
database:
container_name: "neptune-db"
image: 'postgres:latest'
env_file:
- .env
ports:
- "${POSTGRES_PORT}:5432"
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DATABASE}
volumes:
- '${PWD}/db-data/:/var/lib/postgresql/data/'

8
nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

89
package.json Normal file
View File

@@ -0,0 +1,89 @@
{
"name": "neptune-back",
"version": "0.0.1",
"description": "",
"author": "Mathis HERRIOT",
"private": true,
"license": "MIT",
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"scripts": {
"check": "biome check --skip-errors src",
"fix": "biome check --skip-errors --write src",
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/src/main",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.4.7",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.7",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.7",
"@nestjs/swagger": "^7.4.2",
"@nestjs/throttler": "^5.2.0",
"@prisma/client": "^5.22.0",
"@prisma/studio": "^0.497.0",
"@types/nodemailer": "^6.4.16",
"argon2": "^0.31.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ejs": "^3.1.10",
"handlebars": "^4.7.8",
"helmet": "^7.2.0",
"nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"prisma": "^5.22.0",
"reflect-metadata": "^0.2.2",
"rimraf": "^5.0.10",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@nestjs/cli": "^10.4.7",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.7",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.14",
"@types/node": "^20.17.6",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^6.0.2",
"dotenv-cli": "^4.1.1",
"jest": "^29.7.0",
"jest-mock-extended": "4.0.0-beta1",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

5986
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

107
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,107 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Crypto {
id String @id @default(uuid())
name String @unique
value Float
image String
quantity Float @default(1000)
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
UserHasCrypto UserHasCrypto[]
Trade Trade[]
Offer Offer[]
CryptoHistory CryptoHistory[]
}
model CryptoHistory {
id String @id @default(uuid())
id_crypto String
value Float
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
Crypto Crypto @relation(fields: [id_crypto], references: [id])
}
model Offer {
id String @id @default(uuid())
id_crypto String
id_user String
amount Float
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
Crypto Crypto @relation(fields: [id_crypto], references: [id])
User User @relation(fields: [id_user], references: [id])
}
model PromoCode {
id String @id @default(uuid())
name String
value Int
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
}
model Role {
id String @id @default(uuid())
name String
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
User User[]
}
model Trade {
id String @id @default(uuid())
id_giver String
id_receiver String
id_crypto String
amount_traded Float
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
Giver User @relation("Giver", fields: [id_giver], references: [id])
Receiver User @relation("Receiver", fields: [id_receiver], references: [id])
Crypto Crypto @relation(fields: [id_crypto], references: [id])
}
model User {
id String @id @default(uuid())
firstName String
lastName String
pseudo String
hash String
email String @unique
roleId String
isActive Boolean
dollarAvailables Float
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
gdpr_acknowledgement DateTime @default(now())
Role Role @relation(fields: [roleId], references: [id])
UserHasCrypto UserHasCrypto[]
TradeGiven Trade[] @relation("Giver")
TradeReceived Trade[] @relation("Receiver")
Offer Offer[]
}
model UserHasCrypto {
id String @id @default(uuid())
id_user String
id_crypto String
amount Int
createdAt DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
User User @relation(fields: [id_user], references: [id])
Crypto Crypto @relation(fields: [id_crypto], references: [id])
}

60
prisma/seed.ts Normal file
View File

@@ -0,0 +1,60 @@
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: 'YOLO',
value: 8192,
},
});
await prisma.crypto.create({
data: {
name: 'BTC',
value: 456,
quantity: 2400,
image: "",
}
});
await prisma.crypto.create({
data: {
name: 'ETH',
value: 947,
quantity: 163,
image: "",
}
});
await prisma.crypto.create({
data: {
name: 'DOGE',
value: 1749,
quantity: 482,
image: "",
}
});
}
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 {}

113
src/auth/auth.service.ts Normal file
View File

@@ -0,0 +1,113 @@
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 {
private readonly initialBalance = 1000;
constructor(
private prisma: PrismaService,
private jwt: JwtService,
private config: ConfigService,
) {}
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 balance = this.calculateBalance(promoCode);
try {
const user = await this.createUser(dto, hash, userRole.id, balance);
return this.signToken(user);
} catch (error) {
this.handleSignupError(error);
}
}
private async getPromoCode(promoCode: string) {
return this.prisma.promoCode.findFirst({
where: { name: promoCode },
});
}
private async getUserRole(roleName: string) {
return this.prisma.role.findFirst({
where: { name: roleName },
});
}
private calculateBalance(promoCode: any) {
let balance = this.initialBalance;
if (promoCode && promoCode.value) {
balance += promoCode.value;
}
return balance;
}
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,
email: dto.email,
hash,
roleId,
isActive: true,
dollarAvailables: balance,
},
});
}
private handleSignupError(error: any) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
throw new ForbiddenException("Credentials taken");
}
}
throw error;
}
async signin(dto: AuthLoginDto) {
const userDatas = await this.prisma.user.findUnique({
where: { email: dto.email },
include: {
UserHasCrypto: { include: { Crypto: true } },
Role: true,
},
});
if (!userDatas) throw new ForbiddenException("Credentials incorrect");
const pwMatches = await argon.verify(userDatas.hash, dto.password);
if (!pwMatches) throw new ForbiddenException("Credentials incorrect");
return this.signToken(userDatas);
}
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 token = await this.jwt.signAsync(payload, {
expiresIn: "30d",
secret: secret,
});
return {
access_token: token,
user,
};
}
}

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";

26
src/main.ts Normal file
View File

@@ -0,0 +1,26 @@
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")
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api", app, document);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
await app.listen(process.env.PORT || 3000);
}
bootstrap();

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,63 @@
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 { 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);
}
@HttpCode(HttpStatus.FOUND)
@Get("crypto/:cryptoId")
getOffersById(@GetUser() user: User,@Param("cryptoId") cryptoId: string) {
return this.offerService.getOffersById(user.id, cryptoId)
}
}

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

104
src/offer/offer.service.ts Normal file
View File

@@ -0,0 +1,104 @@
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 getOffersById(userId: string, cryptoId: string) {
await checkUserHasAccount(userId);
return this.prisma.offer.findMany({
where: {
id_crypto: cryptoId,
},
orderBy: {
created_at: "desc",
},
select: {
id: true,
amount: true,
created_at: true,
id_user: true,
Crypto: true,
},
});
}
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 {}

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

@@ -0,0 +1,81 @@
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,
amount: 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,17 @@
// 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(),
gdpr_acknowledgement: new Date(),
roleId: "user",
dollarAvailables: 1000,
});

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

24
test/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@/*": ["./src/*"]
}
}
}