Compare commits

...

9 Commits
prod ... dev

Author SHA1 Message Date
78807ffd61
Update 2024-03-04 15:24:20 +01:00
d4a9519cbf
feat: useGlobalValidationPipe with whitelist 2024-02-27 16:46:11 +01:00
577d96d68c
feat: Auth module 2024-02-27 16:45:22 +01:00
73ee4e2894
build: argon2 ... 2024-02-27 16:45:03 +01:00
f62fb6d687
biomejs conf 2024-02-27 16:44:31 +01:00
5619ebfc17
AuthDTO update 2024-02-27 16:44:20 +01:00
7f1440ed65
prisma schema 2024-02-27 16:44:03 +01:00
5a6470a3ce
prisma migration 2024-02-27 16:43:31 +01:00
3f61a16324
Tuto part 53-58 2024-02-23 16:08:59 +01:00
23 changed files with 366 additions and 59 deletions

View File

@ -1,2 +1,3 @@
# template-nestjs # template-nestjs
53:58

View File

@ -4,7 +4,7 @@
"enabled": false "enabled": false
}, },
"linter": { "linter": {
"enabled": true, "enabled": false,
"rules": { "rules": {
"recommended": true, "recommended": true,
"performance": { "performance": {
@ -21,5 +21,10 @@
"indentWidth": 2, "indentWidth": 2,
"lineWidth": 80, "lineWidth": 80,
"formatWithErrors": false "formatWithErrors": false
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
}
} }
} }

31
docker-compose.yaml Normal file
View File

@ -0,0 +1,31 @@
version: '3'
services:
database:
image: 'postgres:latest'
ports:
- 15432:5432
env_file:
- .env
networks:
- postgres-network
volumes:
- ./db-data/:/var/lib/postgresql/data/
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
pgadmin:
image: dpage/pgadmin4
ports:
- 15433:80
env_file:
- .env
depends_on:
- database
networks:
- postgres-network
volumes:
- ./pgadmin-data/:/var/lib/pgadmin/
networks:
postgres-network:
driver: bridge

11
init.sql Normal file
View File

@ -0,0 +1,11 @@
-- create a table
CREATE TABLE test(
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
archived BOOLEAN NOT NULL DEFAULT FALSE
);
-- add test data
INSERT INTO test (name, archived)
VALUES ('test row 1', true),
('test row 2', false);

View File

@ -1,13 +1,13 @@
{ {
"name": "template", "name": "bookmarks-back",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"author": "", "author": "Mathis Herriot",
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"format": "biome --apply src test", "format": "biome format src test",
"start": "nest start", "start": "nest start",
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
@ -21,8 +21,17 @@
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^5.10.2",
"argon2": "^0.40.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
@ -34,8 +43,10 @@
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"prisma": "^5.10.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",

View File

@ -0,0 +1,24 @@
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`hash` VARCHAR(191) NOT NULL,
`firstName` VARCHAR(191) NULL,
`lastName` VARCHAR(191) NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Bookmark` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`title` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
`link` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -0,0 +1,43 @@
/*
Warnings:
- You are about to drop the `Bookmark` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE `Bookmark`;
-- DropTable
DROP TABLE `User`;
-- CreateTable
CREATE TABLE `users` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`hash` VARCHAR(191) NOT NULL,
`firstName` VARCHAR(191) NULL,
`lastName` VARCHAR(191) NULL,
UNIQUE INDEX `users_id_key`(`id`),
UNIQUE INDEX `users_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `bookmarks` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
`title` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
`link` VARCHAR(191) NOT NULL,
`userId` INTEGER NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `bookmarks` ADD CONSTRAINT `bookmarks_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`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 = "mysql"

44
prisma/schema.prisma Normal file
View File

@ -0,0 +1,44 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @unique @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
hash String
firstName String?
lastName String?
@@map("users")
bookmarks Bookmark[]
}
model Bookmark {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
description String?
link String
userId Int
user User @relation(fields: [userId], references: [id])
@@map("bookmarks")
}

View File

@ -1,22 +0,0 @@
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!');
});
});
});

View File

@ -1,12 +0,0 @@
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();
}
}

View File

@ -1,10 +1,21 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AuthModule } from './auth/auth.module';
import { AppService } from './app.service'; import { UserModule } from './user/user.module';
import { BookmarkModule } from './bookmark/bookmark.module';
import { AuthService } from './auth/auth.service';
import { AuthController } from './auth/auth.controller';
import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from "@nestjs/config";
@Module({ @Module({
imports: [], imports: [
controllers: [AppController], ConfigModule.forRoot({ isGlobal: true }),
providers: [AppService], AuthModule,
PrismaModule,
UserModule,
BookmarkModule,
],
providers: [AuthService],
controllers: [AuthController],
}) })
export class AppModule {} export class AppModule {}

View File

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

View File

@ -0,0 +1,18 @@
import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthDto } from "./dto";
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post("register")
async signup(@Body() dto: AuthDto) {
return await this.authService.register(dto);
}
@Post("login")
async signin(@Body() dto: AuthDto) {
return await this.authService.login(dto);
}
}

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

@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { PrismaModule } from "src/prisma/prisma.module";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [PrismaModule, JwtModule.register({})],
controllers: [AuthController],
providers: [AuthService]
})
export class AuthModule { }

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

@ -0,0 +1,80 @@
import { ForbiddenException, Injectable } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { AuthDto } from "./dto";
import * as argon from "argon2";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { JwtService } from "@nestjs/jwt";
import { User } from "@prisma/client";
import { time } from "console";
@Injectable({})
export class AuthService {
constructor(
private prisma: PrismaService,
private jwt: JwtService,
) {}
async login(dto: AuthDto) {
const User = await this.prisma.user.findUnique({
where: {
email: dto.email,
},
});
if (!User) {
console.warn(`ACCESS: Refused login for "${dto.email}" (email not used)`);
throw new ForbiddenException("Credential(s) invalid.");
}
const pwMatches: boolean = await argon.verify(User.hash, dto.password);
if (!pwMatches) {
console.warn(
`ACCESS: Refused login for "${dto.email}" (invalid password)`,
);
throw new ForbiddenException("Credential(s) invalid.");
}
delete User.hash;
console.info(`ACCESS: Granted login for "${dto.email}"`);
return User;
}
async register(dto: AuthDto) {
const userPasswordHash = await argon.hash(dto.password);
try {
const User = await this.prisma.user.create({
data: {
email: dto.email,
hash: userPasswordHash,
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
},
});
//delete User.hash;
return User;
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === "P2002") {
throw new ForbiddenException("Credential(s) taken.");
}
}
}
}
private async generateAuthToken(targetUser: User, dayToLive: number) {
const timestamp = Date.now();
const jwtPayload = {
sub: targetUser.id,
iat: timestamp,
};
return this.jwt.signAsync(jwtPayload, {
expiresIn: `${dayToLive}d`,
})
}
}

16
src/auth/dto/auth.dto.ts Normal file
View File

@ -0,0 +1,16 @@
import { IsEmail, IsNotEmpty, IsStrongPassword } from "class-validator";
export class AuthDto {
@IsEmail()
@IsNotEmpty()
email: string;
@IsStrongPassword({
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
})
password: string;
}

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

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

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class BookmarkModule {}

View File

@ -1,8 +1,14 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await app.listen(3000); app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
await app.listen(3333);
} }
bootstrap(); bootstrap();

View File

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

View File

@ -0,0 +1,16 @@
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"),
},
},
});
}
}

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

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class UserModule {}