testing all features

This commit is contained in:
Kevsl 2024-06-06 16:31:31 +02:00
commit 9f5c23c7c9
74 changed files with 18541 additions and 0 deletions

25
.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# 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

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: npm run start:prod

73
README.md Normal file
View File

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

25
docker-compose.yaml Normal file
View File

@ -0,0 +1,25 @@
version: '3.8'
services:
cryptcoin:
image: postgres:13
ports:
- 5432:5432
environment:
POSTGRES_USER: uDr4KjGcpEm6N5F3BqhxAT
POSTGRES_PASSWORD: 12xt6h4fWf4QL9WiP5syZ887S8Si54giDizdB8L685aBiBkBVaW87MS3bWc22ba3L9YT3
POSTGRES_DB: cW76664NDPkmc
networks:
- CRY
test-db:
image: postgres:13
ports:
- 5434:5434
environment:
POSTGRES_USER: uDr4KjGcpEm6N5F3BqhxAT
POSTGRES_PASSWORD: 12xt6h4fWf4QL9WiP5syZ887S8Si54giDizdB8L685aBiBkBVaW87MS3bWc22ba3L9YT3
POSTGRES_DB: cW76664NDPkmc
networks:
- CRY
networks:
CRY:
external: true

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

16268
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

96
package.json Normal file
View File

@ -0,0 +1,96 @@
{
"name": "cryptcoin",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"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-modules/mailer": "^1.11.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.17",
"@nestjs/throttler": "^5.1.1",
"@prisma/client": "^5.8.1",
"@prisma/studio": "^0.497.0",
"@types/nodemailer": "^6.4.14",
"argon2": "^0.31.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"css-inline": "^0.11.2",
"ejs": "^3.1.9",
"handlebars": "^4.7.8",
"helmet": "^7.1.0",
"nodemailer": "^6.9.10",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"prisma": "^5.8.0",
"pug": "^3.0.2",
"reflect-metadata": "^0.2.1",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"save": "^2.9.0",
"swagger": "^0.7.5",
"swagger-ui-express": "^5.0.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.11.5",
"@types/passport-jwt": "^3.0.6",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"dotenv-cli": "^4.1.1",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.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"
}
}

View File

@ -0,0 +1,88 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"pseudo" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"email" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL,
"city" TEXT NOT NULL,
"dollarAvailables" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Crypto" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" DOUBLE PRECISION NOT NULL,
"image" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Crypto_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserHasCrypto" (
"id_user" TEXT NOT NULL,
"id_crypto" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserHasCrypto_pkey" PRIMARY KEY ("id_user","id_crypto")
);
-- CreateTable
CREATE TABLE "Trade" (
"id" TEXT NOT NULL,
"id_giver" TEXT NOT NULL,
"id_receiver" TEXT NOT NULL,
"id_crypto" TEXT NOT NULL,
"value_while_trade" DOUBLE PRECISION NOT NULL,
"amount_traded" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Trade_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Role" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PromoCode" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "PromoCode_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserHasCrypto" ADD CONSTRAINT "UserHasCrypto_id_user_fkey" FOREIGN KEY ("id_user") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserHasCrypto" ADD CONSTRAINT "UserHasCrypto_id_crypto_fkey" FOREIGN KEY ("id_crypto") REFERENCES "Crypto"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Trade" ADD CONSTRAINT "Trade_id_giver_fkey" FOREIGN KEY ("id_giver") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Trade" ADD CONSTRAINT "Trade_id_receiver_fkey" FOREIGN KEY ("id_receiver") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Trade" ADD CONSTRAINT "Trade_id_crypto_fkey" FOREIGN KEY ("id_crypto") REFERENCES "Crypto"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `value` to the `PromoCode` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PromoCode" ADD COLUMN "value" INTEGER NOT NULL;

View File

@ -0,0 +1,26 @@
/*
Warnings:
- You are about to alter the column `value` on the `Crypto` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.
- You are about to alter the column `value_while_trade` on the `Trade` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.
- You are about to alter the column `amount_traded` on the `Trade` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.
- You are about to alter the column `dollarAvailables` on the `User` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.
- You are about to alter the column `amount` on the `UserHasCrypto` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Crypto" ALTER COLUMN "value" SET DATA TYPE DECIMAL(65,30);
-- AlterTable
ALTER TABLE "Trade" ALTER COLUMN "value_while_trade" SET DATA TYPE DECIMAL(65,30),
ALTER COLUMN "amount_traded" SET DATA TYPE DECIMAL(65,30);
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "dollarAvailables" SET DATA TYPE DECIMAL(65,30);
-- AlterTable
ALTER TABLE "UserHasCrypto" ALTER COLUMN "amount" SET DATA TYPE DECIMAL(65,30);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

View File

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `value_while_trade` on the `Trade` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Trade" DROP COLUMN "value_while_trade";

View File

@ -0,0 +1,16 @@
/*
Warnings:
- You are about to alter the column `value` on the `Crypto` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Integer`.
- You are about to alter the column `amount_traded` on the `Trade` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Integer`.
- You are about to alter the column `amount` on the `UserHasCrypto` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Integer`.
*/
-- AlterTable
ALTER TABLE "Crypto" ALTER COLUMN "value" SET DATA TYPE INTEGER;
-- AlterTable
ALTER TABLE "Trade" ALTER COLUMN "amount_traded" SET DATA TYPE INTEGER;
-- AlterTable
ALTER TABLE "UserHasCrypto" ALTER COLUMN "amount" SET DATA TYPE INTEGER;

View File

@ -0,0 +1,11 @@
/*
Warnings:
- The primary key for the `UserHasCrypto` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The required column `id` was added to the `UserHasCrypto` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
*/
-- AlterTable
ALTER TABLE "UserHasCrypto" DROP CONSTRAINT "UserHasCrypto_pkey",
ADD COLUMN "id" TEXT NOT NULL,
ADD CONSTRAINT "UserHasCrypto_pkey" PRIMARY KEY ("id");

View File

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to alter the column `dollarAvailables` on the `User` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `DoublePrecision`.
*/
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "dollarAvailables" SET DATA TYPE DOUBLE PRECISION;

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Crypto" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION;
-- AlterTable
ALTER TABLE "Trade" ALTER COLUMN "amount_traded" SET DATA TYPE DOUBLE PRECISION;

View File

@ -0,0 +1,47 @@
/*
Warnings:
- Added the required column `id_offer` to the `Trade` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Crypto" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "PromoCode" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Trade" ADD COLUMN "id_offer" TEXT NOT NULL,
ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "UserHasCrypto" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
-- CreateTable
CREATE TABLE "Offer" (
"id" TEXT NOT NULL,
"id_crypto" TEXT NOT NULL,
"id_user" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Offer_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Offer" ADD CONSTRAINT "Offer_id_crypto_fkey" FOREIGN KEY ("id_crypto") REFERENCES "Crypto"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Offer" ADD CONSTRAINT "Offer_id_user_fkey" FOREIGN KEY ("id_user") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Trade" ADD CONSTRAINT "Trade_id_offer_fkey" FOREIGN KEY ("id_offer") REFERENCES "Offer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

110
prisma/schema.prisma Normal file
View File

@ -0,0 +1,110 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Crypto {
id String @id @default(uuid())
name String
value Float
image String
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
UserHasCrypto UserHasCrypto[]
Trade Trade[]
Offer Offer[]
}
model Offer {
id String @id @default(uuid())
id_crypto String
id_user String
amount Float
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
Crypto Crypto @relation(fields: [id_crypto], references: [id])
User User @relation(fields: [id_user], references: [id])
Trade Trade[]
}
model PromoCode {
id String @id @default(uuid())
name String
value Int
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
}
model Role {
id String @id @default(uuid())
name String
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
User User[]
}
model Trade {
id String @id @default(uuid())
id_giver String
id_receiver String
id_crypto String
amount_traded Float
id_offer String
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
Offer Offer @relation(fields: [id_offer], references: [id])
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
city String
dollarAvailables Float
created_at DateTime @default(now())
updated_at DateTime @updatedAt @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 @updatedAt @default(now())
User User @relation(fields: [id_user], references: [id])
Crypto Crypto @relation(fields: [id_crypto], references: [id])
}

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 this.appService.getHello();
}
}

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

@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { PrismaModule } from './prisma/prisma.module';
import { RoleModule } from './role/role.module';
import { PromoCodeModule } from './promoCode/promoCode.module';
import { CryptoModule } from './crypto/crypto.module';
import { TradeModule } from './trade/trade.module';
import { OfferModule } from './offer/offer.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
AuthModule,
PrismaModule,
RoleModule,
PromoCodeModule,
CryptoModule,
TradeModule,
OfferModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

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

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

View File

@ -0,0 +1,21 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthLoginDto, AuthRegisterDto } from './dto';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signup')
signup(@Body() dto: AuthRegisterDto) {
return this.authService.signup(dto);
}
@HttpCode(HttpStatus.OK)
@Post('signin')
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 {}

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

@ -0,0 +1,92 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { AuthLoginDto, AuthRegisterDto } from './dto';
import * as argon from 'argon2';
import { Prisma } from '@prisma/client';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
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.prisma.promoCode.findFirst({
where: {
name: dto.promoCode,
},
});
const userRole = await this.prisma.role.findFirst({
where: {
name: 'user',
},
});
let balance = 1000;
if (promoCode && promoCode.value) {
balance = promoCode.value;
}
try {
const user = await this.prisma.user.create({
data: {
firstName: dto.firstName,
lastName: dto.lastName,
pseudo: dto.pseudo,
city: dto.city,
email: dto.email,
hash,
roleId: userRole.id,
isActive: true,
dollarAvailables: balance,
},
});
return this.signToken(user.id, user.email);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ForbiddenException('Credentials taken');
}
}
throw error;
}
}
async signin(dto: AuthLoginDto) {
const user = await this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
if (!user) throw new ForbiddenException('Credentials incorrect');
const pwMatches = await argon.verify(user.hash, dto.password);
if (!pwMatches) throw new ForbiddenException('Credentials incorrect');
return this.signToken(user.id, user.email);
}
async signToken(
userId: string,
email: string,
): Promise<{ access_token: string }> {
const payload = {
sub: userId,
email: email,
};
const secret = this.config.get('JWT_SECRET');
const token = await this.jwt.signAsync(payload, {
expiresIn: '30d',
secret: secret,
});
return {
access_token: token,
};
}
}

View File

@ -0,0 +1,11 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const GetUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request: Express.Request = ctx.switchToHttp().getRequest();
if (data) {
return request.user[data];
}
return request.user;
},
);

View File

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

View File

@ -0,0 +1,13 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AuthLoginDto {
@IsEmail()
@IsNotEmpty()
@ApiProperty({ type: String, description: 'email' })
email: string;
@ApiProperty({ type: String, description: 'password' })
@IsString()
@IsNotEmpty()
password: string;
}

View File

@ -0,0 +1,65 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class AuthRegisterDto {
@ApiProperty({
type: String,
description: 'FirstName',
example: 'Thomas',
})
@IsNotEmpty()
@IsString()
firstName: string;
@ApiProperty({
type: String,
description: 'Last Name',
example: 'Anderson',
})
@IsNotEmpty()
@IsString()
lastName: string;
@ApiProperty({
type: String,
description: 'Pseudo',
example: 'Néo',
})
@IsNotEmpty()
@IsString()
pseudo: string;
@ApiProperty({
type: String,
description: 'User city',
example: 'Aix les bains',
})
@IsNotEmpty()
@IsString()
city: string;
@ApiProperty({
type: String,
description: 'email',
example: 'neo@matrix.fr',
})
@IsEmail()
@IsNotEmpty()
email: string;
@ApiProperty({
type: String,
description: 'password',
example: 'AAaa11&&&&',
})
@IsString()
@IsNotEmpty()
password: string;
@ApiProperty({
type: String,
description: 'promoCode',
example: 'FILOU20',
})
@IsOptional()
promoCode: string;
}

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

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
config: ConfigService,
private prisma: PrismaService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_SECRET'),
});
}
async validate(payload: { sub: string; email: string }) {
const user = await this.prisma.user.findUnique({
where: {
id: payload.sub,
},
});
delete user.hash;
return user;
}
}

View File

@ -0,0 +1,69 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { GetUser } from '../auth/decorator';
import { ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { CryptoService } from './crypto.service';
import { CryptoDto } from './dto';
import { JwtGuard } from 'src/auth/guard';
import { BuyCryptoDto } from './dto/buy.crypto.dto';
@UseGuards(JwtGuard)
@ApiTags('crypto')
@Controller('crypto')
export class CryptoController {
constructor(private promoService: CryptoService) {}
@Get('/all')
getAllPromoCodes(@GetUser() user: User) {
return this.promoService.getCryptos(user.id);
}
@Get('/search/:id')
searchCrypto(@GetUser() user: User, @Param('id') cryptoName: string) {
return this.promoService.searchCryptos(user.id, cryptoName);
}
@HttpCode(HttpStatus.CREATED)
@Post('/create')
createPromoCode(
@Body()
dto: CryptoDto,
@GetUser() user: User,
) {
return this.promoService.createCrypto(user.id, dto);
}
@Post('/buy')
buyCrypto(
@Body()
dto: BuyCryptoDto,
@GetUser() user: User,
) {
return this.promoService.buyCrypto(user.id, dto);
}
@HttpCode(HttpStatus.OK)
@Patch('/update/:id')
editPromoCodeById(
@Param('id') cryptoId: string,
@Body() dto: CryptoDto,
@GetUser() user: User,
) {
return this.promoService.editCryptoById(user.id, cryptoId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete('/delete/:id')
deletePromoCodeById(@Param('id') cryptoId: string, @GetUser() user: User) {
return this.promoService.deleteCryptoById(user.id, cryptoId);
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CryptoService } from './crypto.service';
import { CryptoController } from './crypto.controller';
@Module({
providers: [CryptoService],
controllers: [CryptoController],
})
export class CryptoModule {}

View File

@ -0,0 +1,148 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { checkUserHasAccount, checkuserIsAdmin } from 'src/utils/checkUser';
import { CryptoDto } from './dto';
import { BuyCryptoDto } from './dto/buy.crypto.dto';
@Injectable()
export class CryptoService {
constructor(private prisma: PrismaService) {}
async getCryptos(userId: string) {
await checkUserHasAccount(userId);
return this.prisma.crypto.findMany({
orderBy: {
name: 'asc',
},
});
}
async searchCryptos(userId: string, cryptoName: string) {
await checkUserHasAccount(userId);
return this.prisma.crypto.findMany({
where: {
name: {
contains: cryptoName,
mode: 'insensitive',
},
},
orderBy: {
name: 'asc',
},
});
}
async createCrypto(userId: string, dto: CryptoDto) {
await checkuserIsAdmin(userId);
const crypto = await this.prisma.crypto.create({
data: {
name: dto.name,
image: dto.image,
value: dto.value,
},
});
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,
},
});
const necessaryAmount = crypto.value * dto.amount;
console.log(necessaryAmount, user.dollarAvailables);
if (necessaryAmount > user.dollarAvailables) {
throw new ForbiddenException('Make money first :) ');
} else {
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,
},
});
}
}
return crypto;
}
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,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class BuyCryptoDto {
@ApiProperty({
type: String,
description: 'Cryptocurrency UUID',
example: '12121-DSZD-E221212-2121221',
})
@IsString()
id_crypto: string;
@ApiProperty({
type: Number,
description: 'Amount of token traded',
example: 2,
})
@IsNumber()
amount: number;
}

View File

@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class CryptoDto {
@ApiProperty({
type: String,
description: 'Cryptocurrency name',
example: 'BTC',
})
@IsString()
name: string;
@ApiProperty({
type: Number,
description: 'Value for the cryptocurrency in $',
example: 1,
})
@IsNumber()
value: number;
@ApiProperty({
type: String,
description: 'Image for the cryptocurrency in ',
example: 'https://myImage/com',
})
@IsString()
image: string;
}

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

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

27
src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
const config = new DocumentBuilder()
.setTitle('CryptCoin APi')
.setDescription('The CryptCoin api swagger ')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
// eslint-disable-next-line prettier/prettier
})
);
await app.listen(3000);
}
bootstrap();

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

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

View File

@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class OfferDto {
@ApiProperty({
type: String,
description: 'Cryptocurrency UUID',
example: '12121-DSZD-E221212-2121221',
})
@IsString()
id_crypto: string;
@ApiProperty({
type: 'number',
description: 'Amount traded ',
example: 21,
})
@IsNumber()
amount: number;
}

View File

@ -0,0 +1,59 @@
/* eslint-disable prettier/prettier */
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
// UseGuards,
} from '@nestjs/common';
import { GetUser } from '../auth/decorator';
// import { JwtGuard } from '../auth/guard';
import { ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { JwtGuard } from 'src/auth/guard';
import { OfferService } from './offer.service';
import { OfferDto } from './dto';
@UseGuards(JwtGuard)
@ApiTags('offer')
@Controller('offer')
export class OfferController {
constructor(private offerService: OfferService) {}
@Get('/all')
getAllRoles(@GetUser() user: User) {
return this.offerService.getOffers(user.id);
}
@HttpCode(HttpStatus.CREATED)
@Post('/create')
createRole(
@Body()
dto: OfferDto,
@GetUser() user: User
) {
return this.offerService.createOffer(user.id, dto);
}
@HttpCode(HttpStatus.OK)
@Patch('/update/:id')
editOfferById(
@Param('id') offerId: string,
@Body() dto: OfferDto,
@GetUser() user: User
) {
return this.offerService.editOfferById(user.id, offerId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete('/delete/:id')
deleteOfferById(@Param('id') roleId: string, @GetUser() user: User) {
return this.offerService.deleteOfferById(user.id, roleId);
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { OfferService } from './offer.service';
import { OfferController } from './offer.controller';
@Module({
providers: [OfferService],
controllers: [OfferController],
})
export class OfferModule {}

View File

@ -0,0 +1,76 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { checkUserHasAccount, checkuserIsAdmin } from 'src/utils/checkUser';
import { OfferDto } from './dto';
// import { checkRoleLevel, checkUserIsStaff } from 'src/utils/checkUser';
@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);
const offer = await this.prisma.offer.create({
data: {
id_crypto: dto.id_crypto,
id_user: userId,
amount: dto.amount,
},
});
return offer;
}
async editOfferById(userId: string, offerId: string, dto: OfferDto) {
await checkUserHasAccount(userId);
const offer = await this.prisma.offer.findUnique({
where: {
id: offerId,
},
});
if (!offer || offer.id !== offerId)
throw new ForbiddenException('Access to resources denied');
return this.prisma.role.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,23 @@
import { Injectable } from '@nestjs/common';
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'),
},
},
});
}
// cleanDb() {
// return this.$transaction([
// this.bookmark.deleteMany(),
// this.user.deleteMany(),
// ])
// }
}

View File

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

View File

@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
export class PromoCodeDto {
@ApiProperty({
type: String,
description: 'Name of the PromoCOde',
example: 'FILOU10',
})
@IsString()
name: string;
@ApiProperty({
type: Number,
description: 'Dollars given for account creation when promoCode applied',
example: 100,
})
@IsNumber()
value: number;
}

View File

@ -0,0 +1,57 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { GetUser } from '../auth/decorator';
import { ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { PromoCodeDto } from './dto';
import { PromoCodeService } from './promoCode.service';
import { JwtGuard } from 'src/auth/guard';
@UseGuards(JwtGuard)
@ApiTags('promoCode')
@Controller('promoCode')
export class PromoCodeController {
constructor(private promoService: PromoCodeService) {}
@Get('/all')
getAllPromoCodes(@GetUser() user: User) {
return this.promoService.getPromoCodes(user.id);
}
@HttpCode(HttpStatus.CREATED)
@Post('/create')
createPromoCode(
// @GetUser() user: User,
@Body()
dto: PromoCodeDto,
@GetUser() user: User,
) {
return this.promoService.createPromoCode(user.id, dto);
}
@HttpCode(HttpStatus.OK)
@Patch('/update/:id')
editPromoCodeById(
@Param('id') promoCodeId: string,
@Body() dto: PromoCodeDto,
@GetUser() user: User,
) {
return this.promoService.editPromoCodeById(user.id, promoCodeId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete('/delete/:id')
deletePromoCodeById(@Param('id') promoCodeId: string, @GetUser() user: User) {
return this.promoService.deletePromoCodeById(user.id, promoCodeId);
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PromoCodeController } from './promoCode.controller';
import { PromoCodeService } from './promoCode.service';
@Module({
providers: [PromoCodeService],
controllers: [PromoCodeController],
})
export class PromoCodeModule {}

View File

@ -0,0 +1,79 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { PromoCodeDto } from './dto';
import { checkuserIsAdmin } from 'src/utils/checkUser';
@Injectable()
export class PromoCodeService {
constructor(private prisma: PrismaService) {}
async getPromoCodes(userId: string) {
await checkuserIsAdmin(userId);
return this.prisma.promoCode.findMany({
orderBy: {
name: 'asc',
},
select: {
id: true,
name: true,
value: true,
},
});
}
async createPromoCode(userId: string, dto: PromoCodeDto) {
await checkuserIsAdmin(userId);
const promoCode = await this.prisma.promoCode.create({
data: {
name: dto.name,
value: dto.value,
},
});
return promoCode;
}
async editPromoCodeById(
userId: string,
promoCodeId: string,
dto: PromoCodeDto,
) {
await checkuserIsAdmin(userId);
const promoCode = await this.prisma.promoCode.findUnique({
where: {
id: promoCodeId,
},
});
if (!promoCode || promoCode.id !== promoCodeId)
throw new ForbiddenException('Access to resources denied');
return this.prisma.promoCode.update({
where: {
id: promoCode.id,
},
data: {
...dto,
},
});
}
async deletePromoCodeById(userId: string, id: string) {
await checkuserIsAdmin(userId);
const promoCode = await this.prisma.promoCode.findUnique({
where: {
id: id,
},
});
if (!promoCode || promoCode.id !== id)
throw new ForbiddenException('Access to resources denied');
await this.prisma.promoCode.delete({
where: {
id: promoCode.id,
},
});
}
}

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

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

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

@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class RoleDto {
@ApiProperty({
type: String,
description: 'Role Name',
example: 'user',
})
@IsString()
name: string;
}

View File

@ -0,0 +1,63 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
// UseGuards,
} from '@nestjs/common';
import { GetUser } from '../auth/decorator';
// import { JwtGuard } from '../auth/guard';
import { RoleDto } from './dto';
import { RoleService } from './role.service';
import { ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { JwtGuard } from 'src/auth/guard';
@UseGuards(JwtGuard)
@ApiTags('role')
@Controller('role')
export class RoleController {
constructor(private roleService: RoleService) {}
@Get('/all')
getAllRoles(@GetUser() user: User) {
return this.roleService.getRolesAdmin(user.id);
}
// @Get('/cm/all')
// getRolesCm(@GetUser() user: User) {
// return this.roleService.getRolesCm(user)
// }
@HttpCode(HttpStatus.CREATED)
@Post('/create')
createRole(
// @GetUser() user: User,
@Body()
dto: RoleDto,
@GetUser() user: User,
) {
return this.roleService.createRole(user.id, dto);
}
@HttpCode(HttpStatus.OK)
@Patch('/update/:id')
editRoleById(
@Param('id') roleId: string,
@Body() dto: RoleDto,
@GetUser() user: User,
) {
return this.roleService.editRoleById(user.id, roleId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete('/delete/:id')
deleteRoleById(@Param('id') roleId: string, @GetUser() user: User) {
return this.roleService.deleteRoleById(user.id, roleId);
}
}

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

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { RoleController } from './role.controller';
import { RoleService } from './role.service';
@Module({
providers: [RoleService],
controllers: [RoleController],
})
export class RoleModule {}

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

@ -0,0 +1,72 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RoleDto } from './dto';
import { checkuserIsAdmin } from 'src/utils/checkUser';
// import { checkRoleLevel, checkUserIsStaff } from 'src/utils/checkUser';
@Injectable()
export class RoleService {
constructor(private prisma: PrismaService) {}
async getRolesAdmin(userId: string) {
await checkuserIsAdmin(userId);
return this.prisma.role.findMany({
orderBy: {
name: 'asc',
},
select: {
id: true,
name: true,
},
});
}
async createRole(userId: string, dto: RoleDto) {
await checkuserIsAdmin(userId);
const role = await this.prisma.role.create({
data: {
name: dto.name,
},
});
return role;
}
async editRoleById(userId: string, roleId: string, dto: RoleDto) {
await checkuserIsAdmin(userId);
const role = await this.prisma.role.findUnique({
where: {
id: roleId,
},
});
if (!role || role.id !== roleId)
throw new ForbiddenException('Access to resources denied');
return this.prisma.role.update({
where: {
id: roleId,
},
data: {
...dto,
},
});
}
async deleteRoleById(userId: string, id: string) {
await checkuserIsAdmin(userId);
const role = await this.prisma.role.findUnique({
where: {
id: id,
},
});
if (!role || role.id !== id)
throw new ForbiddenException('Access to resources denied');
await this.prisma.role.delete({
where: {
id: id,
},
});
}
}

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

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

View File

@ -0,0 +1,48 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDecimal, IsNotEmpty, IsString } from 'class-validator';
export class TradeDto {
@ApiProperty({
type: String,
description: 'UUID of the user that hold tokens before trade',
example: '121212-DSDZ1-21212DJDZ-31313',
})
@IsNotEmpty()
@IsString()
id_giver: string;
@ApiProperty({
type: String,
description: 'UUID of the user that will receive tokens after trade',
example: '121212-DSDZ1-21212DJDZ-31313',
})
@IsNotEmpty()
@IsString()
id_receiver: string;
@ApiProperty({
type: String,
description: 'UUID of the crypto traded',
example: '121212-DSDZ1-21212DJDZ-31313',
})
@IsNotEmpty()
@IsString()
id_crypto: string;
@ApiProperty({
type: Number,
description: 'Amount of tokens traded ',
example: 2,
})
@IsNotEmpty()
@IsDecimal()
amount_traded: number;
@ApiProperty({
type: String,
description: 'Offer UUID ',
example: '121212-DSDZ1-21212DJDZ-31313',
})
@IsNotEmpty()
@IsDecimal()
id_offer: string;
}

View File

@ -0,0 +1,58 @@
/* eslint-disable prettier/prettier */
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { GetUser } from '../auth/decorator';
import { ApiTags } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { TradeService } from './trade.service';
import { TradeDto } from './dto';
import { JwtGuard } from 'src/auth/guard';
@UseGuards(JwtGuard)
@ApiTags('trade')
@Controller('trade')
export class TradeController {
constructor(private tradeService: TradeService) {}
@Get('/all')
getAllPromoCodes(@GetUser() user: User) {
return this.tradeService.getTrades(user.id);
}
@HttpCode(HttpStatus.CREATED)
@Post('/create')
createPromoCode(
// @GetUser() user: User,
@Body()
dto: TradeDto,
@GetUser() user: User
) {
return this.tradeService.createTrade(user.id, dto);
}
@HttpCode(HttpStatus.OK)
@Patch('/update/:id')
editPromoCodeById(
@Param('id') promoCodeId: string,
@Body() dto: TradeDto,
@GetUser() user: User
) {
return this.tradeService.editTradeById(user.id, promoCodeId, dto);
}
@HttpCode(HttpStatus.NO_CONTENT)
@Delete('/delete/:id')
deletePromoCodeById(@Param('id') promoCodeId: string, @GetUser() user: User) {
return this.tradeService.deleteTradeById(user.id, promoCodeId);
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TradeService } from './trade.service';
import { TradeController } from './trade.controller';
@Module({
providers: [TradeService],
controllers: [TradeController],
})
export class TradeModule {}

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

@ -0,0 +1,177 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { checkUserHasAccount, checkuserIsAdmin } from 'src/utils/checkUser';
import { TradeDto } from './dto';
@Injectable()
export class TradeService {
constructor(private prisma: PrismaService) {}
async getTrades(userId: string) {
await checkuserIsAdmin(userId);
return this.prisma.trade.findMany({
orderBy: {
created_at: 'desc',
},
include: {
Giver: true,
Receiver: true,
Crypto: true,
},
});
}
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 crypto = await this.prisma.crypto.findFirst({
where: {
id: dto.id_crypto,
},
});
const buyer = await this.prisma.user.findFirst({
where: {
id: dto.id_receiver,
},
});
const price = crypto.value * dto.amount_traded;
if (buyer.dollarAvailables < price) {
throw new ForbiddenException(
// eslint-disable-next-line prettier/prettier
`Acqueror ${buyer.pseudo} doesnt have enough money to make this trade`
);
}
const asset = await this.prisma.userHasCrypto.findFirst({
where: {
id_crypto: dto.id_crypto,
id_user: dto.id_giver,
},
});
if (!asset || asset.amount < dto.amount_traded) {
throw new ForbiddenException(`Seller doesnt have enough ${crypto.name} `);
}
const trade = await this.prisma.trade.create({
data: {
id_giver: dto.id_giver,
id_receiver: dto.id_receiver,
id_crypto: dto.id_crypto,
amount_traded: dto.amount_traded,
id_offer: dto.id_offer,
},
});
const newBalanceGiver = (asset.amount -= dto.amount_traded);
await this.prisma.userHasCrypto.update({
where: {
id: asset.id,
},
data: {
amount: newBalanceGiver,
},
});
const receiverAssets = await this.prisma.userHasCrypto.findFirst({
where: {
id_user: dto.id_receiver,
id_crypto: dto.id_crypto,
},
});
if (!receiverAssets) {
await this.prisma.userHasCrypto.create({
data: {
id_user: dto.id_receiver,
id_crypto: dto.id_crypto,
amount: dto.amount_traded,
createdAt: new Date(),
},
});
} else {
const newBalanceReceiver = receiverAssets.amount + dto.amount_traded;
await this.prisma.userHasCrypto.update({
where: {
id: receiverAssets.id,
},
data: {
amount: newBalanceReceiver,
},
});
}
const newValue = crypto.value * 1.1;
await this.prisma.crypto.update({
where: {
id: crypto.id,
},
data: {
value: newValue,
},
});
const prevAmount = buyer.dollarAvailables;
await this.prisma.user.update({
where: {
id: dto.id_receiver,
},
data: {
dollarAvailables: prevAmount - price,
},
});
return trade;
}
async editTradeById(userId: string, tradeId: string, dto: TradeDto) {
await checkuserIsAdmin(userId);
const trade = await this.prisma.trade.findUnique({
where: {
id: tradeId,
},
});
if (!trade || trade.id !== tradeId)
throw new ForbiddenException('Access to resources denied');
return this.prisma.trade.update({
where: {
id: trade.id,
},
data: {
...dto,
},
});
}
async deleteTradeById(userId: string, id: string) {
await checkuserIsAdmin(userId);
const trade = await this.prisma.trade.findUnique({
where: {
id: id,
},
});
if (!trade || trade.id !== id)
throw new ForbiddenException('Access to resources denied');
await this.prisma.trade.delete({
where: {
id: trade.id,
},
});
}
}

View File

@ -0,0 +1,31 @@
/* eslint-disable prettier/prettier */
import { Body, Controller, Get, UseGuards } from '@nestjs/common';
import { GetUser } from '../auth/decorator';
import { JwtGuard } from '../auth/guard';
import { ApiTags } from '@nestjs/swagger';
import { UserService } from './user.service';
import { User } from '@prisma/client';
@ApiTags('user')
@UseGuards(JwtGuard)
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
@Get('/my-assets')
GetMyAssets(
@Body()
@GetUser()
user: User
) {
return this.userService.GetMyAssets(user.id);
}
@Get('/my-trades')
GetMyTrades(
@Body()
@GetUser()
user: User
) {
return this.userService.GetMyTrades(user.id);
}
}

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

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
providers: [UserService],
controllers: [UserController],
})
export class UserModule {}

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

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async GetMyAssets(userId: string) {
const user = await this.prisma.user.findUnique({
where: {
id: userId,
},
include: {
UserHasCrypto: true,
},
});
return user;
}
async GetMyTrades(userId: string) {
const user = await this.prisma.trade.findMany({
where: {
OR: [{ id_giver: userId }, { id_receiver: userId }],
},
include: {
Crypto: true,
},
});
return user;
}
}

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 && user.roleId) {
const role = await prisma.role.findFirst({
where: {
id: user.roleId,
},
});
if (role && 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',
};

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"]
}

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"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
}
}