Compare commits

..

No commits in common. "a102b4a21bb6942faaadcacde929bd461791f8c7" and "10e481669e40f25d9149ff4ca112d4dd8fd3950a" have entirely different histories.

19 changed files with 27 additions and 156 deletions

View File

@ -9,7 +9,7 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME} POSTGRES_DB: ${DB_NAME}
ports: ports:
- "${DB_PORT}:${DB_PORT}" - "${DB_PORT}:5432"
pgadmin: pgadmin:
container_name: pgadmin4_container container_name: pgadmin4_container
image: dpage/pgadmin4 image: dpage/pgadmin4

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

@ -1,48 +0,0 @@
{
"$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": "web-api",
"inputSpec": "http://localhost:3000/api-json",
"additionalProperties": {
"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
}
}
}
}
}

View File

@ -17,8 +17,7 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"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"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
@ -29,7 +28,6 @@
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1", "@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^10.0.2",
"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", "install": "^0.13.0",

View File

@ -29,9 +29,6 @@ dependencies:
'@nestjs/typeorm': '@nestjs/typeorm':
specifier: ^10.0.2 specifier: ^10.0.2
version: 10.0.2(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20) version: 10.0.2(@nestjs/common@10.3.7)(@nestjs/core@10.3.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20)
argon2:
specifier: ^0.40.1
version: 0.40.1
class-transformer: class-transformer:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1 version: 0.5.1
@ -1185,6 +1182,7 @@ packages:
/@phc/format@1.0.0: /@phc/format@1.0.0:
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true
/@pkgjs/parseargs@0.11.0: /@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@ -1825,6 +1823,7 @@ packages:
'@phc/format': 1.0.0 '@phc/format': 1.0.0
node-addon-api: 7.1.0 node-addon-api: 7.1.0
node-gyp-build: 4.8.1 node-gyp-build: 4.8.1
dev: true
/argparse@1.0.10: /argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@ -4231,6 +4230,7 @@ packages:
/node-addon-api@7.1.0: /node-addon-api@7.1.0:
resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
engines: {node: ^16 || ^18 || >= 20} engines: {node: ^16 || ^18 || >= 20}
dev: true
/node-emoji@1.11.0: /node-emoji@1.11.0:
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
@ -4252,6 +4252,7 @@ packages:
/node-gyp-build@4.8.1: /node-gyp-build@4.8.1:
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
hasBin: true hasBin: true
dev: true
/node-int64@0.4.0: /node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}

View File

@ -24,7 +24,7 @@ import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
export class AppModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer consumer
// TODO Redirect via Reverse Proxy all HTTP requests to HTTPS // TODO: Redirect via Reverse Proxy all HTTP requests to HTTPS
.apply( .apply(
CspMiddleware, CspMiddleware,
SecurityHeadersMiddleware, SecurityHeadersMiddleware,

View File

@ -1 +0,0 @@
export * from './user-credentials.entity';

View File

@ -4,11 +4,7 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
async function setupSwagger(app) { async function setupSwagger(app) {
const config = new DocumentBuilder() const config = new DocumentBuilder().build();
.setTitle('Tickets API')
.setDescription('Description of the API')
.setVersion('0.0.0')
.build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
} }

View File

@ -7,56 +7,39 @@ import {
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
import { TokensDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsDto } from '../models/dto';
import { Tokens } from '../models/types';
import { RefreshTokenGuard } from '../common/guards'; 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';
@ApiTags('Authentication')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) {} constructor(private readonly authService: AuthService) {}
@ApiCreatedResponse({
description: 'User signed up successfully',
type: TokensDto,
})
@Public() @Public()
@Post('signup') @Post('signup')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
public async signup( public async signup(
@Body() userCredentials: UserCredentialsDto @Body() userCredentials: UserCredentialsDto
): Promise<TokensDto> { ): Promise<Tokens> {
return this.authService.signup(userCredentials); return this.authService.signup(userCredentials);
} }
@ApiCreatedResponse({
description: 'User signin successfully',
type: TokensDto,
})
@Public() @Public()
@Post('signin') @Post('signin')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
public async signin( public async signin(
@Body() userCredentials: UserCredentialsDto @Body() userCredentials: UserCredentialsDto
): Promise<TokensDto> { ): Promise<Tokens> {
return this.authService.signin(userCredentials); return this.authService.signin(userCredentials);
} }
@ApiCreatedResponse({
description: 'User signed out successfully',
type: Boolean,
})
@Post('logout') @Post('logout')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
public async logout(@GetCurrentUserId() userId: number): Promise<boolean> { public async logout(@GetCurrentUserId() userId: number): Promise<boolean> {
return this.authService.logout(userId); return this.authService.logout(userId);
} }
@ApiCreatedResponse({
description: 'User tokens refreshed successfully',
type: TokensDto,
})
@Public() @Public()
@UseGuards(RefreshTokenGuard) @UseGuards(RefreshTokenGuard)
@Post('refresh') @Post('refresh')
@ -64,7 +47,7 @@ export class AuthController {
public async refresh( public async refresh(
@GetCurrentUserId() userId: number, @GetCurrentUserId() userId: number,
@GetCurrentUser('refresh_token') refresh_token: string @GetCurrentUser('refresh_token') refresh_token: string
): Promise<TokensDto> { ): Promise<Tokens> {
return this.authService.refresh(userId, refresh_token); return this.authService.refresh(userId, refresh_token);
} }
} }

View File

@ -1,2 +1 @@
export * from './user-credentials.dto'; export * from './user-credentials.dto';
export * from './tokens.dto';

View File

@ -1,22 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class TokensDto {
@ApiProperty({
title: 'Access token',
description: 'Access token',
example: 'eyJhbGci',
})
@IsNotEmpty()
@IsString()
public access_token: string;
@ApiProperty({
title: 'Refresh token',
description: 'Refresh token',
example: 'eyJhbGci',
})
@IsNotEmpty()
@IsString()
public refresh_token: string;
}

View File

@ -1,22 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class UserCredentialsDto { export class UserCredentialsDto {
@ApiProperty({
title: 'E-Mail',
description: 'User email',
example: 'foo@bar.com',
})
@IsNotEmpty() @IsNotEmpty()
@IsEmail() @IsEmail()
public email: string; public email: string;
@ApiProperty({
title: 'Password',
description: 'User password',
example: '$tr0ngP@$$w0rd',
minLength: 8,
})
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@MinLength(8) @MinLength(8)

View File

@ -1,2 +1,3 @@
export * from './tokens.type';
export * from './jwt-payload.type'; export * from './jwt-payload.type';
export * from './jwt-payload-with-refresh-token.type'; export * from './jwt-payload-with-refresh-token.type';

View File

@ -0,0 +1,4 @@
export type Tokens = {
access_token: string;
refresh_token: string;
};

View File

@ -1,5 +1,6 @@
import { ForbiddenException, Injectable } from '@nestjs/common'; import { ForbiddenException, Injectable } from '@nestjs/common';
import { TokensDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsDto } from '../models/dto';
import { Tokens } from '../models/types';
import { EncryptionService } from './encryption.service'; import { EncryptionService } from './encryption.service';
import { UserRepository } from '../repositories/user.repository'; import { UserRepository } from '../repositories/user.repository';
import { TokenManagementService } from './token-management.service'; import { TokenManagementService } from './token-management.service';
@ -12,7 +13,7 @@ export class AuthService {
private readonly encryptionService: EncryptionService private readonly encryptionService: EncryptionService
) {} ) {}
public async signup(userCredentials: UserCredentialsDto): Promise<TokensDto> { public async signup(userCredentials: UserCredentialsDto): Promise<Tokens> {
const passwordHashed = await this.encryptionService.hashData( const passwordHashed = await this.encryptionService.hashData(
userCredentials.password userCredentials.password
); );
@ -23,7 +24,7 @@ export class AuthService {
return this.generateAndPersistTokens(user.id, user.email); return this.generateAndPersistTokens(user.id, user.email);
} }
public async signin(userCredentials: UserCredentialsDto): Promise<TokensDto> { public async signin(userCredentials: UserCredentialsDto): Promise<Tokens> {
const user = await this.userRepository.findUserByEmail( const user = await this.userRepository.findUserByEmail(
userCredentials.email userCredentials.email
); );
@ -42,10 +43,7 @@ export class AuthService {
return this.generateAndPersistTokens(user.id, user.email); return this.generateAndPersistTokens(user.id, user.email);
} }
public async refresh( public async refresh(userId: number, refreshToken: string): Promise<Tokens> {
userId: number,
refreshToken: string
): 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');
@ -73,7 +71,7 @@ export class AuthService {
private async generateAndPersistTokens( private async generateAndPersistTokens(
userId: number, userId: number,
email: string email: string
): Promise<TokensDto> { ): Promise<Tokens> {
const tokens = await this.tokenManagementService.generateTokens( const tokens = await this.tokenManagementService.generateTokens(
userId, userId,
email email

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Tokens } from '../models/types';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { TokensDto } from '../models/dto';
@Injectable() @Injectable()
export class TokenManagementService { export class TokenManagementService {
@ -24,10 +24,7 @@ export class TokenManagementService {
this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT'); this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT');
} }
public async generateTokens( public async generateTokens(userId: number, email: string): Promise<Tokens> {
userId: number,
email: string
): 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,6 +1,6 @@
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { UserCredentials } from 'src/entities'; import { UserCredentials } from '../../entities/user-credentials.entity';
export const databaseConfigFactory = ( export const databaseConfigFactory = (
configService: ConfigService configService: ConfigService

View File

@ -31,7 +31,6 @@
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dompurify": "^3.1.3", "dompurify": "^3.1.3",
"primeicons": "^7.0.0",
"primeng": "^17.11.0", "primeng": "^17.11.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@ -38,9 +38,6 @@ dependencies:
dompurify: dompurify:
specifier: ^3.1.3 specifier: ^3.1.3
version: 3.1.3 version: 3.1.3
primeicons:
specifier: ^7.0.0
version: 7.0.0
primeng: primeng:
specifier: ^17.11.0 specifier: ^17.11.0
version: 17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4) version: 17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4)
@ -8567,10 +8564,6 @@ packages:
react-is: 18.2.0 react-is: 18.2.0
dev: true dev: true
/primeicons@7.0.0:
resolution: {integrity: sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==}
dev: false
/primeng@17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4): /primeng@17.11.0(@angular/common@17.3.0)(@angular/core@17.3.0)(@angular/forms@17.3.0)(rxjs@7.8.1)(zone.js@0.14.4):
resolution: {integrity: sha512-EtkXnE7I6gcrqjNDvUi4qvwTvwL/m3hwPJImoO/s088HDOBerwjN1c2Pq3bwGRG8Eg4Ga9nXdE/IrYj2mbAZLw==} resolution: {integrity: sha512-EtkXnE7I6gcrqjNDvUi4qvwTvwL/m3hwPJImoO/s088HDOBerwjN1c2Pq3bwGRG8Eg4Ga9nXdE/IrYj2mbAZLw==}
peerDependencies: peerDependencies: