Compare commits
No commits in common. "dev" and "prod" have entirely different histories.
@ -4,7 +4,7 @@
|
|||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"performance": {
|
"performance": {
|
||||||
@ -21,10 +21,5 @@
|
|||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
"lineWidth": 80,
|
"lineWidth": 80,
|
||||||
"formatWithErrors": false
|
"formatWithErrors": false
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"parser": {
|
|
||||||
"unsafeParameterDecoratorsEnabled": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,31 +0,0 @@
|
|||||||
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
11
init.sql
@ -1,11 +0,0 @@
|
|||||||
-- 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);
|
|
17
package.json
17
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "bookmarks-back",
|
"name": "template",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Mathis Herriot",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "biome format src test",
|
"format": "biome --apply 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,17 +21,8 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@ -43,10 +34,8 @@
|
|||||||
"@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",
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
-- 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;
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
@ -1,3 +0,0 @@
|
|||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (i.e. Git)
|
|
||||||
provider = "mysql"
|
|
@ -1,44 +0,0 @@
|
|||||||
// 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")
|
|
||||||
}
|
|
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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
12
src/app.controller.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AppController } from './app.controller';
|
||||||
import { UserModule } from './user/user.module';
|
import { AppService } from './app.service';
|
||||||
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: [],
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
controllers: [AppController],
|
||||||
AuthModule,
|
providers: [AppService],
|
||||||
PrismaModule,
|
|
||||||
UserModule,
|
|
||||||
BookmarkModule,
|
|
||||||
],
|
|
||||||
providers: [AuthService],
|
|
||||||
controllers: [AuthController],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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 { }
|
|
@ -1,80 +0,0 @@
|
|||||||
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`,
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
export * from './auth.dto';
|
|
@ -1,4 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class BookmarkModule {}
|
|
@ -1,14 +1,8 @@
|
|||||||
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);
|
||||||
app.useGlobalPipes(
|
await app.listen(3000);
|
||||||
new ValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
await app.listen(3333);
|
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [PrismaService],
|
|
||||||
exports: [PrismaService]
|
|
||||||
})
|
|
||||||
export class PrismaModule {}
|
|
@ -1,16 +0,0 @@
|
|||||||
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"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class UserModule {}
|
|
Loading…
x
Reference in New Issue
Block a user