Feature: Use Api generated by swagger #5

Merged
igorpropisnov merged 12 commits from feature/use-generated-api into main 2024-05-24 20:04:54 +02:00
50 changed files with 12264 additions and 3677 deletions

210
.eslintrc Normal file
View File

@ -0,0 +1,210 @@
{
"root": true,
"plugins": ["import", "prettier", "@stylistic/eslint-plugin-ts", "@stylistic/eslint-plugin", "sort-class-members", "unused-imports"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:prettier/recommended"
],
"rules": {
"@stylistic/ts/lines-between-class-members": [
"error",
{
"enforce": [
{ "blankLine": "always", "prev": "*", "next": "method" },
{ "blankLine": "always", "prev": "method", "next": "*" },
{ "blankLine": "never", "prev": "field", "next": "field" }
]
}
],
"@stylistic/ts/block-spacing": ["error"],
"@stylistic/ts/brace-style": "off",
"@stylistic/ts/key-spacing": ["error", { "afterColon": true }],
"@stylistic/ts/keyword-spacing": ["error", { "before": true }],
"@stylistic/ts/no-extra-parens": ["error", "all", {
"nestedBinaryExpressions": false,
"ternaryOperandBinaryExpressions": false
}],
"@stylistic/ts/no-extra-semi": ["error"],
"@stylistic/ts/object-curly-spacing": ["error", "always"],
"@stylistic/ts/quotes": ["error", "single"],
"@stylistic/ts/semi": ["error", "always"],
"@stylistic/ts/space-before-blocks": ["error"],
"@stylistic/ts/space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}],
"@stylistic/ts/space-infix-ops": ["error"],
// "@stylistic/ts/max-statements-per-line": ["error", { "max": 1 }],
// "@stylistic/ts/multiline-ternary": ["error", "always"],
// "@stylistic/ts/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
// "@stylistic/ts/no-confusing-arrow": ["error"],
// "@stylistic/ts/no-floating-decimal": ["error"],
// "@stylistic/ts/no-mixed-operators": ["error"],
// "@stylistic/ts/no-mixed-spaces-and-tabs": ["error"],/
// "@stylistic/ts/no-multi-spaces": ["error"],
// "@stylistic/ts/no-multiple-empty-lines": ["error", { "max": 2 }],
// "@stylistic/ts/no-tabs": ["error", { "allowIndentationTabs": true }],
// "@stylistic/ts/no-whitespace-before-property": ["error"],
// "@stylistic/ts/nonblock-statement-body-position": ["error", "below"],
// "@stylistic/ts/object-curly-newline": ["error", "always"],
// "@stylistic/ts/object-property-newline": ["error"],
// "@stylistic/ts/one-var-declaration-per-line": ["error", "always"],
// "@stylistic/ts/operator-linebreak": ["error", "before"],
// "@stylistic/ts/padded-blocks": ["error", "never"],
// "@stylistic/ts/rest-spread-spacing": ["error", "never"],
// "@stylistic/ts/semi-spacing": ["error"],
// "@stylistic/ts/semi-style": ["error", "last"],
// "@stylistic/ts/space-in-parens": ["error", "never"],
// "@stylistic/ts/space-unary-ops": ["error"],
// "@stylistic/ts/template-curly-spacing": ["error"],
// "@stylistic/ts/template-tag-spacing": ["error"],
// "@stylistic/ts/wrap-regex": ["error"],
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"no-var": ["error"],
"eqeqeq": ["error", "always"],
"no-eval": "error",
"prefer-const": ["error", { "destructuring": "all", "ignoreReadBeforeAssign": true }],
"prettier/prettier": ["error", { "printWidth": 80 }],
"no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": false,
"allowTypedFunctionExpressions": true,
"allowHigherOrderFunctions": false,
"allowDirectConstAssertionInArrowFunctions": false,
"allowConciseArrowFunctionExpressionsStartingWithVoid": false
}
],
"@typescript-eslint/member-ordering": [
"error",
{
"default": [
"public-static-field",
"protected-static-field",
"private-static-field",
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"public-constructor",
"protected-constructor",
"private-constructor",
"public-static-method",
"protected-static-method",
"private-static-method",
"public-instance-method",
"protected-instance-method",
"private-instance-method"
]
}
],
// https://github.com/bryanrsmith/eslint-plugin-sort-class-members -> Read Docs and replace @typescript-eslint/member-ordering
// "sort-class-members/sort-class-members": [
// 2,
// {
// "order": [
// "[public-properties]",
// "[protected-properties]",
// "[private-properties]",
// "everything-else"
// ],
// "groups": {
// "public-properties": [{ "type": "property", "accessibility": "public" }],
// "protected-properties": [{ "type": "property", "accessibility": "protected" }],
// "private-properties": [{ "type": "property", "accessibility": "private" }]
// }
// }
// ],
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "const", "next": "*" },
{ "blankLine": "always", "prev": "let", "next": "*" },
{ "blankLine": "always", "prev": "var", "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] }
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": false
}
],
"@typescript-eslint/typedef": [
"error",
{
"arrayDestructuring": true,
"arrowParameter": false,
"memberVariableDeclaration": true,
"objectDestructuring": true,
"parameter": true,
"propertyDeclaration": true,
"variableDeclaration": false,
"variableDeclarationIgnoreFunction": true
}
],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"pathGroups": [
{
"pattern": "@angular/**",
"group": "external",
"position": "before"
},
{
"pattern": "@app/**",
"group": "internal",
"position": "before"
},
{
"pattern": "@env/**",
"group": "internal",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
},
{
"files": ["*.dto.ts", "*.entity.ts"],
"rules": {
"@stylistic/ts/lines-between-class-members": "off"
}
}
]
}

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
.env

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
coverage
dist
node_modules
pnpm-lock.yaml
/frontend/pnpm-lock.yaml
/backend/pnpm-lock.yaml

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"npm.packageManager": "pnpm"
}

8
backend/.eslintrc Normal file
View File

@ -0,0 +1,8 @@
{
"extends": ["./../.eslintrc"],
"parser": "@typescript-eslint/parser",
"env": {
"node": true,
"jest": true
}
}

View File

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

1
backend/.gitignore vendored
View File

@ -2,6 +2,7 @@
/dist /dist
/node_modules /node_modules
/build /build
/docs
# Logs # Logs
logs logs

View File

@ -1,3 +0,0 @@
coverage
dist
node_modules

View File

@ -1,7 +0,0 @@
{
"singleQuote": true,
"trailingComma": "es5",
"semi": true,
"endOfLine": "auto",
"bracketSameLine": true
}

View File

@ -1,15 +0,0 @@
{
"npmName": "Ticket-API-Services",
"npmVersion": "0.0.0",
"providedIn": "root",
"withInterfaces": true,
"enumNameSuffix": "Enum",
"supportsES6": true,
"ngVersion": "17.0.0",
"modelSuffix": "Model",
"stringEnums": true,
"enumPropertyNaming": "PascalCase",
"modelPropertyNaming": "original",
"fileNaming": "camelCase",
"paramNaming": "camelCase"
}

View File

@ -14,7 +14,7 @@
"ngVersion": "17.0.0", "ngVersion": "17.0.0",
"npmRepository": null, "npmRepository": null,
"configurationPrefix": null, "configurationPrefix": null,
"apiModulePrefix" : "TicketApi", "apiModulePrefix": "TicketApi",
"providedIn": "any", "providedIn": "any",
"fileNaming": "camelCase", "fileNaming": "camelCase",
"paramNaming": "camelCase", "paramNaming": "camelCase",

View File

@ -18,7 +18,9 @@
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "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", "test:e2e": "jest --config ./test/jest-e2e.json",
"build:api": "pnpm openapi-generator-cli generate" "prettier:fix": "prettier --write .",
"prettier:check": "prettier --check .",
"foramt": "pnpm lint && prettier:fix"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
@ -32,10 +34,8 @@
"argon2": "^0.40.1", "argon2": "^0.40.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"install": "^0.13.0",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.11.5", "pg": "^8.11.5",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@ -51,11 +51,8 @@
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0", "eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';

View File

@ -1,12 +1,13 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
@Controller() @Controller()
export class AppController { export class AppController {
constructor(private readonly appService: AppService) {} public constructor(private readonly appService: AppService) {}
@Get() @Get()
getHello(): string { public getHello(): string {
return this.appService.getHello(); return this.appService.getHello();
} }
} }

View File

@ -1,14 +1,15 @@
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config'; import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
import { DatabaseModule } from './modules/database-module/database.module';
import { CspMiddleware } from './middleware/csp-middleware/csp.middleware'; import { CspMiddleware } from './middleware/csp-middleware/csp.middleware';
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware'; import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
import { AuthModule } from './modules/auth-module/auth.module'; import { AuthModule } from './modules/auth-module/auth.module';
import { AccessTokenGuard } from './modules/auth-module/common/guards'; import { AccessTokenGuard } from './modules/auth-module/common/guards';
import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware'; import { DatabaseModule } from './modules/database-module/database.module';
@Module({ @Module({
imports: [ imports: [
@ -22,7 +23,7 @@ import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }], providers: [AppService, { provide: 'APP_GUARD', useClass: AccessTokenGuard }],
}) })
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { public configure(consumer: MiddlewareConsumer): void {
consumer consumer
// TODO Redirect via Reverse Proxy all HTTP requests to HTTPS // TODO Redirect via Reverse Proxy all HTTP requests to HTTPS
.apply( .apply(

View File

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

View File

@ -9,20 +9,20 @@ import {
@Entity() @Entity()
export class UserCredentials { export class UserCredentials {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: number; public id: number;
@Column({ unique: true }) @Column({ unique: true })
email: string; public email: string;
@Column() @Column()
hash: string; public hash: string;
@Column({ nullable: true }) @Column({ nullable: true })
hashedRt?: string; public hashedRt?: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; public createdAt: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; public updatedAt: Date;
} }

View File

@ -1,28 +1,46 @@
import { NestFactory } from '@nestjs/core'; import * as fs from 'fs';
import { AppModule } from './app.module'; import { join } from 'path';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
async function setupSwagger(app) { import { INestApplication, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function setupSwagger(app: INestApplication): Promise<void> {
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Tickets API') .setTitle('Tickets API')
.setDescription('Description of the API') .setDescription('Description of the API')
.setVersion('0.0.0') .setVersion('0.0.0')
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
const docsDir = join(process.cwd(), 'docs');
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir);
}
fs.writeFileSync(
join(docsDir, 'swagger.json'),
JSON.stringify(document, null, 2)
);
} }
async function setupPrefix(app) { async function setupPrefix(app: INestApplication): Promise<void> {
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
} }
async function setupClassValidator(app) { async function setupClassValidator(app: INestApplication): Promise<void> {
app.useGlobalPipes(new ValidationPipe()); app.useGlobalPipes(new ValidationPipe());
} }
async function bootstrap() { async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await setupSwagger(app); await setupSwagger(app);
await setupPrefix(app); await setupPrefix(app);
await setupClassValidator(app); await setupClassValidator(app);

View File

@ -4,7 +4,7 @@ import { Request, Response, NextFunction } from 'express';
@Injectable() @Injectable()
export class CorsMiddleware implements NestMiddleware { export class CorsMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {} public constructor(private readonly configService: ConfigService) {}
public use(req: Request, res: Response, next: NextFunction): void { public use(req: Request, res: Response, next: NextFunction): void {
if (this.configService.get<string>('NODE_ENV') === 'development') { if (this.configService.get<string>('NODE_ENV') === 'development') {

View File

@ -1,13 +1,14 @@
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Request, Response, NextFunction } from 'express';
@Injectable() @Injectable()
export class CspMiddleware implements NestMiddleware { export class CspMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {} public constructor(private readonly configService: ConfigService) {}
public use(req: Request, res: Response, next: NextFunction): void { public use(req: Request, res: Response, next: NextFunction): void {
const cspDirectives = this.configService.get<string>('CSP_DIRECTIVES'); const cspDirectives = this.configService.get<string>('CSP_DIRECTIVES');
if (cspDirectives) { if (cspDirectives) {
res.setHeader('Content-Security-Policy', cspDirectives); res.setHeader('Content-Security-Policy', cspDirectives);
} }

View File

@ -4,12 +4,13 @@ import { NextFunction, Request, Response } from 'express';
@Injectable() @Injectable()
export class HttpsRedirectMiddleware implements NestMiddleware { export class HttpsRedirectMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {} public constructor(private readonly configService: ConfigService) {}
public use(req: Request, res: Response, next: NextFunction) { public use(req: Request, res: Response, next: NextFunction): void {
if (this.configService.get<string>('NODE_ENV') === 'production') { if (this.configService.get<string>('NODE_ENV') === 'production') {
if (req.protocol === 'http') { if (req.protocol === 'http') {
const httpsUrl = `https://${req.headers.host}${req.url}`; const httpsUrl = `https://${req.headers.host}${req.url}`;
res.redirect(httpsUrl); res.redirect(httpsUrl);
} else { } else {
next(); next();

View File

@ -1,10 +1,10 @@
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Request, Response, NextFunction } from 'express';
@Injectable() @Injectable()
export class SecurityHeadersMiddleware implements NestMiddleware { export class SecurityHeadersMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {} public constructor(private readonly configService: ConfigService) {}
public use(req: Request, res: Response, next: NextFunction): void { public use(req: Request, res: Response, next: NextFunction): void {
if (this.configService.get<string>('NODE_ENV') === 'production') { if (this.configService.get<string>('NODE_ENV') === 'production') {

View File

@ -1,13 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthService } from './services/auth.service';
import { AuthController } from './controller/auth.controller';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UserCredentials } from 'src/entities/user-credentials.entity'; import { UserCredentials } from 'src/entities/user-credentials.entity';
import { AuthController } from './controller/auth.controller';
import { UserRepository } from './repositories/user.repository'; import { UserRepository } from './repositories/user.repository';
import { AuthService } from './services/auth.service';
import { EncryptionService } from './services/encryption.service'; import { EncryptionService } from './services/encryption.service';
import { TokenManagementService } from './services/token-management.service'; import { TokenManagementService } from './services/token-management.service';
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
@Module({ @Module({
imports: [ imports: [

View File

@ -5,6 +5,7 @@ export const GetCurrentUserId = createParamDecorator(
(_: undefined, context: ExecutionContext): number => { (_: undefined, context: ExecutionContext): number => {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const user = request.user as JwtPayload; const user = request.user as JwtPayload;
return user.sub; return user.sub;
} }
); );

View File

@ -7,6 +7,7 @@ export const GetCurrentUser = createParamDecorator(
context: ExecutionContext context: ExecutionContext
) => { ) => {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
if (!data) return request.user; if (!data) return request.user;
return request.user[data]; return request.user[data];
} }

View File

@ -1,3 +1,4 @@
import { SetMetadata } from '@nestjs/common'; import { CustomDecorator, SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true); export const Public = (): CustomDecorator<string> =>
SetMetadata('isPublic', true);

View File

@ -1,11 +1,11 @@
import { Injectable, ExecutionContext } from '@nestjs/common'; import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable() @Injectable()
export class AccessTokenGuard extends AuthGuard('jwt-access-token') { export class AccessTokenGuard extends AuthGuard('jwt-access-token') {
constructor(private readonly reflector: Reflector) { public constructor(private readonly reflector: Reflector) {
super(); super();
} }

View File

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

View File

@ -6,16 +6,17 @@ import {
HttpStatus, HttpStatus,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthService } from '../services/auth.service'; import { ApiCreatedResponse, ApiHeader, ApiTags } from '@nestjs/swagger';
import { TokensDto, UserCredentialsDto } from '../models/dto';
import { RefreshTokenGuard } from '../common/guards';
import { GetCurrentUser, GetCurrentUserId, Public } from '../common/decorators'; import { GetCurrentUser, GetCurrentUserId, Public } from '../common/decorators';
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; import { RefreshTokenGuard } from '../common/guards';
import { TokensDto, UserCredentialsDto } from '../models/dto';
import { AuthService } from '../services/auth.service';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) {} public constructor(private readonly authService: AuthService) {}
@ApiCreatedResponse({ @ApiCreatedResponse({
description: 'User signed up successfully', description: 'User signed up successfully',
@ -53,6 +54,13 @@ export class AuthController {
return this.authService.logout(userId); return this.authService.logout(userId);
} }
@ApiHeader({
name: 'Authorization',
required: true,
schema: {
example: 'Bearer <refresh_token>',
},
})
@ApiCreatedResponse({ @ApiCreatedResponse({
description: 'User tokens refreshed successfully', description: 'User tokens refreshed successfully',
type: TokensDto, type: TokensDto,

View File

@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
@Injectable() @Injectable()
export class UserRepository { export class UserRepository {
constructor( public constructor(
@InjectRepository(UserCredentials) @InjectRepository(UserCredentials)
private readonly repository: Repository<UserCredentials> private readonly repository: Repository<UserCredentials>
) {} ) {}
@ -15,6 +15,7 @@ export class UserRepository {
hash: string hash: string
): Promise<UserCredentials> { ): Promise<UserCredentials> {
const user = this.repository.create({ email, hash }); const user = this.repository.create({ email, hash });
return this.repository.save(user); return this.repository.save(user);
} }
@ -35,6 +36,7 @@ export class UserRepository {
hashedRt: string | null hashedRt: string | null
): Promise<number> { ): Promise<number> {
const result = await this.repository.update(userId, { hashedRt }); const result = await this.repository.update(userId, { hashedRt });
return result.affected ?? 0; return result.affected ?? 0;
} }
} }

View File

@ -1,12 +1,14 @@
import { ForbiddenException, Injectable } from '@nestjs/common'; import { ForbiddenException, Injectable } from '@nestjs/common';
import { TokensDto, UserCredentialsDto } from '../models/dto'; import { TokensDto, UserCredentialsDto } from '../models/dto';
import { EncryptionService } from './encryption.service';
import { UserRepository } from '../repositories/user.repository'; import { UserRepository } from '../repositories/user.repository';
import { EncryptionService } from './encryption.service';
import { TokenManagementService } from './token-management.service'; import { TokenManagementService } from './token-management.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( public constructor(
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
private readonly tokenManagementService: TokenManagementService, private readonly tokenManagementService: TokenManagementService,
private readonly encryptionService: EncryptionService private readonly encryptionService: EncryptionService
@ -20,6 +22,7 @@ export class AuthService {
userCredentials.email, userCredentials.email,
passwordHashed passwordHashed
); );
return this.generateAndPersistTokens(user.id, user.email); return this.generateAndPersistTokens(user.id, user.email);
} }
@ -27,6 +30,7 @@ export class AuthService {
const user = await this.userRepository.findUserByEmail( const user = await this.userRepository.findUserByEmail(
userCredentials.email userCredentials.email
); );
if (!user) { if (!user) {
throw new ForbiddenException('Access Denied'); throw new ForbiddenException('Access Denied');
} }
@ -35,6 +39,7 @@ export class AuthService {
userCredentials.password, userCredentials.password,
user.hash user.hash
); );
if (!passwordMatch) { if (!passwordMatch) {
throw new ForbiddenException('Access Denied'); throw new ForbiddenException('Access Denied');
} }
@ -47,6 +52,7 @@ export class AuthService {
refreshToken: string refreshToken: string
): Promise<TokensDto> { ): Promise<TokensDto> {
const user = await this.userRepository.findUserById(userId); const user = await this.userRepository.findUserById(userId);
if (!user || !user.hashedRt) { if (!user || !user.hashedRt) {
throw new ForbiddenException('Access Denied'); throw new ForbiddenException('Access Denied');
} }
@ -55,6 +61,7 @@ export class AuthService {
refreshToken, refreshToken,
user.hashedRt user.hashedRt
); );
if (!refreshTokenMatch) { if (!refreshTokenMatch) {
throw new ForbiddenException('Access Denied'); throw new ForbiddenException('Access Denied');
} }
@ -67,6 +74,7 @@ export class AuthService {
userId, userId,
null null
); );
return affected > 0; return affected > 0;
} }
@ -81,6 +89,7 @@ export class AuthService {
const hashedRefreshToken = await this.encryptionService.hashData( const hashedRefreshToken = await this.encryptionService.hashData(
tokens.refresh_token tokens.refresh_token
); );
await this.userRepository.updateUserTokenHash(userId, hashedRefreshToken); await this.userRepository.updateUserTokenHash(userId, hashedRefreshToken);
return tokens; return tokens;
} }

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { TokensDto } from '../models/dto'; import { TokensDto } from '../models/dto';
@Injectable() @Injectable()
@ -10,7 +11,7 @@ export class TokenManagementService {
private readonly JWT_SECRET_AT: string; private readonly JWT_SECRET_AT: string;
private readonly JWT_SECRET_RT: string; private readonly JWT_SECRET_RT: string;
constructor( public constructor(
private readonly jwt: JwtService, private readonly jwt: JwtService,
private readonly configService: ConfigService private readonly configService: ConfigService
) { ) {
@ -30,6 +31,7 @@ export class TokenManagementService {
): Promise<TokensDto> { ): Promise<TokensDto> {
const access_token: string = await this.createAccessToken(userId, email); const access_token: string = await this.createAccessToken(userId, email);
const refresh_token: string = await this.createRefreshToken(userId, email); const refresh_token: string = await this.createRefreshToken(userId, email);
return { access_token, refresh_token }; return { access_token, refresh_token };
} }

View File

@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt'; import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { JwtPayload } from '../models/types'; import { JwtPayload } from '../models/types';
@Injectable() @Injectable()
@ -9,7 +10,7 @@ export class AccessTokenStrategy extends PassportStrategy(
Strategy, Strategy,
'jwt-access-token' 'jwt-access-token'
) { ) {
constructor(private readonly configService: ConfigService) { public constructor(private readonly configService: ConfigService) {
super(AccessTokenStrategy.getJwtConfig(configService)); super(AccessTokenStrategy.getJwtConfig(configService));
} }

View File

@ -1,15 +1,15 @@
import { Injectable, ForbiddenException } from '@nestjs/common'; import { Injectable, ForbiddenException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express'; import { Request } from 'express';
import { Strategy, ExtractJwt } from 'passport-jwt';
@Injectable() @Injectable()
export class RefreshTokenStrategy extends PassportStrategy( export class RefreshTokenStrategy extends PassportStrategy(
Strategy, Strategy,
'jwt-refresh-token' 'jwt-refresh-token'
) { ) {
constructor(private readonly configService: ConfigService) { public constructor(private readonly configService: ConfigService) {
super(RefreshTokenStrategy.createJwtStrategyOptions(configService)); super(RefreshTokenStrategy.createJwtStrategyOptions(configService));
} }

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config'; import { ConfigService, ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { databaseConfigFactory } from './database-config'; import { databaseConfigFactory } from './database-config';
@Module({ @Module({

View File

@ -1,6 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest'; import * as request from 'supertest';
import { AppModule } from './../src/app.module'; import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => { describe('AppController (e2e)', () => {

View File

@ -1,17 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
trim_trailing_whitespace = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -1,87 +1,21 @@
{ {
"root": true, "extends": ["./../.eslintrc"],
"ignorePatterns": ["projects/**/*"], "ignorePatterns": ["projects/**/*", "src/app/api"],
"plugins": ["import", "prettier", "@stylistic/eslint-plugin-ts", "sort-class-members", "unused-imports"], "plugins": [
"import",
"prettier",
"@stylistic/eslint-plugin-ts",
"sort-class-members",
"unused-imports"
],
"overrides": [ "overrides": [
{ {
"files": ["*.ts"], "files": ["*.ts"],
"extends": [ "extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended", "plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates", "plugin:@angular-eslint/template/process-inline-templates"
"prettier",
"plugin:prettier/recommended"
], ],
"rules": { "rules": {
"@stylistic/ts/lines-between-class-members": [
"error",
{
"enforce": [
{ "blankLine": "always", "prev": "*", "next": "method" },
{ "blankLine": "always", "prev": "method", "next": "*" },
{ "blankLine": "never", "prev": "field", "next": "field" }
]
}
],
"@stylistic/ts/block-spacing": ["error"],
"@stylistic/ts/brace-style": "off",
"@stylistic/ts/key-spacing": ["error", { "afterColon": true }],
"@stylistic/ts/keyword-spacing": ["error", { "before": true }],
"@stylistic/ts/no-extra-parens": ["error", "all", {
"nestedBinaryExpressions": false,
"ternaryOperandBinaryExpressions": false
}],
"@stylistic/ts/no-extra-semi": ["error"],
"@stylistic/ts/object-curly-spacing": ["error", "always"],
"@stylistic/ts/quotes": ["error", "single"],
"@stylistic/ts/semi": ["error", "always"],
"@stylistic/ts/space-before-blocks": ["error"],
"@stylistic/ts/space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}],
"@stylistic/ts/space-infix-ops": ["error"],
// "@stylistic/ts/max-statements-per-line": ["error", { "max": 1 }],
// "@stylistic/ts/multiline-ternary": ["error", "always"],
// "@stylistic/ts/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
// "@stylistic/ts/no-confusing-arrow": ["error"],
// "@stylistic/ts/no-floating-decimal": ["error"],
// "@stylistic/ts/no-mixed-operators": ["error"],
// "@stylistic/ts/no-mixed-spaces-and-tabs": ["error"],/
// "@stylistic/ts/no-multi-spaces": ["error"],
// "@stylistic/ts/no-multiple-empty-lines": ["error", { "max": 2 }],
// "@stylistic/ts/no-tabs": ["error", { "allowIndentationTabs": true }],
// "@stylistic/ts/no-whitespace-before-property": ["error"],
// "@stylistic/ts/nonblock-statement-body-position": ["error", "below"],
// "@stylistic/ts/object-curly-newline": ["error", "always"],
// "@stylistic/ts/object-property-newline": ["error"],
// "@stylistic/ts/one-var-declaration-per-line": ["error", "always"],
// "@stylistic/ts/operator-linebreak": ["error", "before"],
// "@stylistic/ts/padded-blocks": ["error", "never"],
// "@stylistic/ts/rest-spread-spacing": ["error", "never"],
// "@stylistic/ts/semi-spacing": ["error"],
// "@stylistic/ts/semi-style": ["error", "last"],
// "@stylistic/ts/space-in-parens": ["error", "never"],
// "@stylistic/ts/space-unary-ops": ["error"],
// "@stylistic/ts/template-curly-spacing": ["error"],
// "@stylistic/ts/template-tag-spacing": ["error"],
// "@stylistic/ts/wrap-regex": ["error"],
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "error",
"no-var": ["error"],
"eqeqeq": ["error", "always"],
"no-eval": "error",
"prefer-const": ["error", { "destructuring": "all", "ignoreReadBeforeAssign": true }],
"prettier/prettier": ["error", { "printWidth": 80 }],
"no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
@ -97,125 +31,6 @@
"prefix": "app", "prefix": "app",
"style": "kebab-case" "style": "kebab-case"
} }
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": false,
"allowTypedFunctionExpressions": true,
"allowHigherOrderFunctions": false,
"allowDirectConstAssertionInArrowFunctions": false,
"allowConciseArrowFunctionExpressionsStartingWithVoid": false
}
],
"@typescript-eslint/member-ordering": [
"error",
{
"default": [
"public-static-field",
"protected-static-field",
"private-static-field",
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"public-constructor",
"protected-constructor",
"private-constructor",
"public-static-method",
"protected-static-method",
"private-static-method",
"public-instance-method",
"protected-instance-method",
"private-instance-method"
]
}
],
// https://github.com/bryanrsmith/eslint-plugin-sort-class-members -> Read Docs and replace @typescript-eslint/member-ordering
// "sort-class-members/sort-class-members": [
// 2,
// {
// "order": [
// "[public-properties]",
// "[protected-properties]",
// "[private-properties]",
// "everything-else"
// ],
// "groups": {
// "public-properties": [{ "type": "property", "accessibility": "public" }],
// "protected-properties": [{ "type": "property", "accessibility": "protected" }],
// "private-properties": [{ "type": "property", "accessibility": "private" }]
// }
// }
// ],
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "const", "next": "*" },
{ "blankLine": "always", "prev": "let", "next": "*" },
{ "blankLine": "always", "prev": "var", "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] }
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": false
}
],
"@typescript-eslint/typedef": [
"error",
{
"arrayDestructuring": true,
"arrowParameter": false,
"memberVariableDeclaration": true,
"objectDestructuring": true,
"parameter": true,
"propertyDeclaration": true,
"variableDeclaration": false,
"variableDeclarationIgnoreFunction": true
}
],
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"pathGroups": [
{
"pattern": "@angular/**",
"group": "external",
"position": "before"
},
{
"pattern": "@app/**",
"group": "internal",
"position": "before"
},
{
"pattern": "@env/**",
"group": "internal",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
] ]
} }
}, },

1
frontend/.gitignore vendored
View File

@ -5,6 +5,7 @@
/tmp /tmp
/out-tsc /out-tsc
/bazel-out /bazel-out
/src/app/api
# Node # Node
/node_modules /node_modules

View File

@ -1,4 +0,0 @@
coverage
dist
node_modules
pnpm-lock.yaml

View File

@ -46,9 +46,6 @@
"@angular-eslint/template-parser": "17.2.1", "@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^17.3.0", "@angular/cli": "^17.3.0",
"@angular/compiler-cli": "^17.3.0", "@angular/compiler-cli": "^17.3.0",
"@stylistic/eslint-plugin": "^2.1.0",
"@stylistic/eslint-plugin-migrate": "^2.1.0",
"@stylistic/eslint-plugin-ts": "^2.1.0",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/eslint-plugin": "6.19.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, tap } from 'rxjs'; import { BehaviorSubject, Observable, tap } from 'rxjs';
import { environment } from '../../../environments/environment'; import { AuthenticationApiService } from '../../api/api/authentication.api.service';
import { LoginCredentials, Tokens } from '../types'; import { LoginCredentials, Tokens } from '../types';
import { LocalStorageService } from './local-storage.service'; import { LocalStorageService } from './local-storage.service';
@ -26,35 +26,51 @@ export class AuthService {
public constructor( public constructor(
private readonly httpClient: HttpClient, private readonly httpClient: HttpClient,
private readonly localStorageService: LocalStorageService, private readonly localStorageService: LocalStorageService,
private readonly sessionStorageService: SessionStorageService private readonly sessionStorageService: SessionStorageService,
private readonly authenticationApiService: AuthenticationApiService
) { ) {
this.autoLogin(); this.autoLogin();
} }
public signin(credentials: LoginCredentials): void { public signin(credentials: LoginCredentials): void {
this.httpClient this.authenticationApiService
.post<Tokens>(environment.api.base + `${this._path}/signin`, credentials) .authControllerSignin(credentials)
.subscribe((response: Tokens) => { .subscribe((response: Tokens) => {
this.handleSuccess(response); this.handleSuccess(response);
}); });
} }
public signup(credentials: LoginCredentials): void { public signup(credentials: LoginCredentials): void {
this.httpClient this.authenticationApiService
.post<Tokens>(environment.api.base + `${this._path}/signup`, credentials) .authControllerSignup(credentials)
.subscribe((response: Tokens) => { .subscribe((response: Tokens) => {
//TODO The checked accept terms should be saved with a timestamp in the db
this.handleSuccess(response); this.handleSuccess(response);
}); });
} }
public refreshToken(): Observable<Tokens> {
if (this._refresh_token) {
return this.authenticationApiService
.authControllerRefresh(this._refresh_token)
.pipe(tap((response: Tokens) => this.handleSuccess(response)));
} else {
throw new Error('Refresh token is missing');
}
}
public signout(): void { public signout(): void {
this.authenticationApiService
.authControllerLogout()
.subscribe((response: boolean) => {
if (response) {
this._access_token = null; this._access_token = null;
this._refresh_token = null; this._refresh_token = null;
this.localStorageService.removeItem('access_token'); this.localStorageService.removeItem('access_token');
this.sessionStorageService.removeItem('refresh_token'); this.sessionStorageService.removeItem('refresh_token');
this._isAuthenticated$.next(false); this._isAuthenticated$.next(false);
} }
});
}
public autoLogin(): void { public autoLogin(): void {
const storedAccessToken: string | null = const storedAccessToken: string | null =
@ -71,25 +87,6 @@ export class AuthService {
} }
} }
public refreshToken(): Observable<Tokens> {
const headers = new HttpHeaders().set(
'Authorization',
'Bearer ' + this._refresh_token
);
return this.httpClient
.post<Tokens>(
environment.api.base + `${this._path}/refresh`,
{},
{ headers: headers }
)
.pipe(
tap((response: Tokens) => {
this.handleSuccess(response);
})
);
}
private handleSuccess(tokens: Tokens): void { private handleSuccess(tokens: Tokens): void {
this._access_token = tokens.access_token; this._access_token = tokens.access_token;
this._refresh_token = tokens.refresh_token; this._refresh_token = tokens.refresh_token;

49
openapitools.json Normal file
View File

@ -0,0 +1,49 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.6.0",
"generators": {
"typescript-angular": {
"generatorName": "typescript-angular",
"output": "frontend/src/app/api",
"inputSpec": "backend/docs/swagger.json",
"additionalProperties": {
"basePath": "http://localhost:3000/api",
"npmName": "Ticket-API-Services",
"npmVersion": "0.0.0",
"ngVersion": "17.0.0",
"npmRepository": null,
"configurationPrefix": null,
"apiModulePrefix" : "TicketApi",
"providedIn": "any",
"fileNaming": "camelCase",
"paramNaming": "camelCase",
"enumPropertyNamingReplaceSpecialChar": "false",
"enumUnknownDefaultCase": "false",
"enumNameSuffix": "ApiEnum",
"enumPropertyNaming": "PascalCase",
"modelPropertyNaming": "original",
"modelSuffix": "ApiModel",
"modelFileSuffix": ".api.model",
"serviceSuffix": "ApiService",
"serviceFileSuffix": ".api.service",
"withInterfaces": true,
"supportsES6": true,
"stringEnums": true,
"sortParamsByRequiredFlag": true,
"sortModelPropertiesByRequiredFlag": true,
"useSingleRequestParameter": false,
"taggedUnions": false,
"snapshot": false,
"prependFormOrBodyParameters": false,
"nullSafeAdditionalProps": false,
"legacyDiscriminatorBehavior": true,
"ensureUniqueParams": true,
"disallowAdditionalPropertiesIfNotPresent": true,
"allowUnicodeIdentifiers": false
}
}
}
}
}

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "mvp-ticket",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start:frontend:dev": "cd frontend && pnpm run start:dev",
"start:backend:dev": "cd backend && pnpm run start:dev",
"start:all:dev": "concurrently --kill-others --prefix-colors \"bgBlue.bold,bgMagenta.bold\" \"pnpm run start:frontend:dev\" \"pnpm run start:backend:dev\" --open \"terminal\"",
"install:all": "pnpm install && pnpm run install:frontend && pnpm run install:backend",
"install:frontend": "cd frontend && pnpm install",
"install:backend": "cd backend && pnpm install",
"format:frontend": "cd frontend && pnpm run format",
"format:backend": "cd backend && concurrently \"pnpm run lint\" \"pnpm run prettier:fix\"",
"format:all": "concurrently \"pnpm run format:frontend\" \"pnpm run format:backend\"",
"build:api": "openapi-generator-cli generate"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.13.4",
"@stylistic/eslint-plugin": "^2.1.0",
"@stylistic/eslint-plugin-ts": "^2.1.0",
"@typescript-eslint/eslint-plugin": "6.19.0",
"concurrently": "^8.2.2",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-sort-class-members": "^1.20.0",
"eslint-plugin-unused-imports": "^3.2.0",
"prettier": "^3.2.5"
}
}

2785
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff