diff --git a/docker-compose.yml b/docker-compose.yml index 0b9200d..72460ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ version: '3.8' services: api: container_name: ionicapp_api + restart: unless-stopped depends_on: - postgres build: @@ -22,6 +23,7 @@ services: postgres: container_name: ionicapp_postgres + restart: unless-stopped build: dockerfile: docker/postgres/Dockerfile environment: @@ -48,6 +50,7 @@ services: redis: container_name: ionicapp_redis + restart: unless-stopped image: redis:alpine ports: - ${REDIS_PORT}:6379 diff --git a/src/dto/id-number.dto.ts b/src/dto/id-number.dto.ts index 4146d54..5a75923 100644 --- a/src/dto/id-number.dto.ts +++ b/src/dto/id-number.dto.ts @@ -8,5 +8,5 @@ export class IdNumberDto { @IsPositive() @Expose() @ApiProperty({ example: 1 }) - id: number; + id!: number; } diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index ba99467..22d6415 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -10,26 +10,26 @@ export class User { unique: true, generated: 'uuid', }) - id: string; + id!: string; @Column({ type: 'varchar', length: 255, nullable: false, }) - password: string; + password!: string; @Column({ type: 'varchar', length: 255, nullable: false, }) - name: string; + name!: string; @Column({ type: 'varchar', length: 255, nullable: false, }) - email: string; + email!: string; } diff --git a/src/helpers/system.ts b/src/helpers/system.ts index 9807aa7..d283d84 100644 --- a/src/helpers/system.ts +++ b/src/helpers/system.ts @@ -5,3 +5,10 @@ export const encodePassword = async (passwordRaw: string): Promise => { const salt = await bcrypt.genSalt(Number(b)); return await bcrypt.hash(passwordRaw, salt); }; + +export const checkPassword = async ( + password: string, + hashedPassword: string, +): Promise => { + return bcrypt.compare(password, hashedPassword); +}; diff --git a/src/main.ts b/src/main.ts index 22affc9..088521f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ async function bootstrap(): Promise { const corsOrigins = configService.get('SITE_ORIGIN'); const corsOriginsArray = corsOrigins?.split(',').filter(Boolean) ?? []; + app.setGlobalPrefix('api'); app.useGlobalPipes(new ValidationPipe(validationPipeConfig)); app.enableShutdownHooks(); @@ -33,7 +34,7 @@ async function bootstrap(): Promise { .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup('api/docs', app, document, { + SwaggerModule.setup('docs', app, document, { swaggerOptions: { persistAuthorization: true }, }); } diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index cab7df3..231ae61 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -12,6 +12,7 @@ import { AccessForbiddenException, AccessTokenGenerationException, } from '../../exceptions/access-exceptions'; +import { checkPassword } from '../../helpers/system'; @Injectable() export class AuthService { @@ -27,33 +28,24 @@ export class AuthService { return this.jwtService.signAsync(payload, options); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async validateUser(email: string, _password: string): Promise { + async validateUser(email: string, password: string): Promise { const user: User | null = await this.usersRepository.findOneBy({ email, }); - if (!user) { throw new AccessForbiddenException('Invalid login or password'); } - - // use mocked (simplified) auth - allow all users regardles of password - // TODO: remove this after real auth implementation - - // const passwordIsValid = await this.hashService.compareHashed( - // password, - // user.password, - // ); - // if (!passwordIsValid) { - // throw new AccessForbiddenException('Invalid login or password'); - // } + const passwordIsValid = await checkPassword(password, user.password); + if (!passwordIsValid) { + throw new AccessForbiddenException('Invalid login or password'); + } return user; } async signin(payloadUser: JwtUserPayloadDto): Promise { const dbUser: User | null = await this.usersRepository.findOneBy({ - id: payloadUser._id, + id: payloadUser.id, }); if (!dbUser) { @@ -67,11 +59,11 @@ export class AuthService { userPayload: JwtUserPayloadDto, ): Promise { const accessToken = await this.generateAccessToken({ - sub: userPayload._id, + sub: userPayload.id, }); const refreshToken = await this.generateRefreshToken({ - sub: userPayload._id, + sub: userPayload.id, }); return { accessToken, refreshToken }; @@ -112,13 +104,13 @@ export class AuthService { expiresIn, }); } catch (e) { - throw new AccessTokenGenerationException(e.message); + throw new AccessTokenGenerationException((e as Error)?.message); } } async refresh(payloadUser: JwtUserPayloadDto): Promise { const dbUser: User | null = await this.usersRepository.findOneBy({ - id: payloadUser._id, + id: payloadUser.id, }); if (!dbUser) { @@ -126,7 +118,7 @@ export class AuthService { } const updatedPayload: JwtUserPayloadDto = { - _id: String(dbUser.id), + id: String(dbUser.id), }; return this.generateTokens(updatedPayload); diff --git a/src/modules/auth/dto/auth.dto.ts b/src/modules/auth/dto/auth.dto.ts index cede264..496ba64 100644 --- a/src/modules/auth/dto/auth.dto.ts +++ b/src/modules/auth/dto/auth.dto.ts @@ -4,9 +4,9 @@ import { IsString } from 'class-validator'; export class AuthDto { @ApiProperty({ example: 'admin@test.com' }) @IsString() - email: string; + email!: string; @ApiProperty({ example: 'asdfasdf' }) @IsString() - password: string; + password!: string; } diff --git a/src/modules/auth/dto/jwt-user-payload.dto.ts b/src/modules/auth/dto/jwt-user-payload.dto.ts index aaad27f..36ad444 100644 --- a/src/modules/auth/dto/jwt-user-payload.dto.ts +++ b/src/modules/auth/dto/jwt-user-payload.dto.ts @@ -1,3 +1,3 @@ export class JwtUserPayloadDto { - _id: string; + id!: string; } diff --git a/src/modules/auth/dto/response-token.dto.ts b/src/modules/auth/dto/response-token.dto.ts index 1277b92..3c8fb6f 100644 --- a/src/modules/auth/dto/response-token.dto.ts +++ b/src/modules/auth/dto/response-token.dto.ts @@ -6,10 +6,10 @@ export class ResponseTokenDto { @ApiProperty() @IsString() @Expose() - accessToken: string; + accessToken!: string; @ApiProperty() @IsString() @Expose() - refreshToken: string; + refreshToken!: string; } diff --git a/src/modules/auth/strategy/jwt-refresh.strategy.ts b/src/modules/auth/strategy/jwt-refresh.strategy.ts index 82eea89..9b3ba87 100644 --- a/src/modules/auth/strategy/jwt-refresh.strategy.ts +++ b/src/modules/auth/strategy/jwt-refresh.strategy.ts @@ -23,6 +23,6 @@ export class JwtRefreshStrategy extends PassportStrategy( } validate(req: Request, payload: JwtPayload): JwtUserPayloadDto { - return { _id: payload.sub }; + return { id: payload.sub }; } } diff --git a/src/modules/auth/strategy/jwt.strategy.ts b/src/modules/auth/strategy/jwt.strategy.ts index f159eca..3ec8617 100644 --- a/src/modules/auth/strategy/jwt.strategy.ts +++ b/src/modules/auth/strategy/jwt.strategy.ts @@ -20,6 +20,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } validate(payload: JwtPayload): JwtUserPayloadDto { - return { _id: payload.sub }; + return { id: payload.sub }; } } diff --git a/src/modules/auth/strategy/local.strategy.ts b/src/modules/auth/strategy/local.strategy.ts index 469bec3..fc4863b 100644 --- a/src/modules/auth/strategy/local.strategy.ts +++ b/src/modules/auth/strategy/local.strategy.ts @@ -1,10 +1,11 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { AuthService } from '../auth.service'; import { JwtUserPayloadDto } from '../dto/jwt-user-payload.dto'; import { User } from '../../../entities/user.entity'; +import { AccessUnauthorizedException } from '../../../exceptions/access-exceptions'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { @@ -20,13 +21,13 @@ export class LocalStrategy extends PassportStrategy(Strategy) { try { user = await this.authService.validateUser(email, password); } catch (e) { - throw new UnauthorizedException(e.message); + throw new AccessUnauthorizedException((e as Error)?.message); } if (!user) { - throw new UnauthorizedException(); + throw new AccessUnauthorizedException(); } - return { _id: String(user.id) }; + return { id: String(user.id) }; } } diff --git a/src/modules/users/dto/user.dto.ts b/src/modules/users/dto/user.dto.ts index 55654f8..6eb770c 100644 --- a/src/modules/users/dto/user.dto.ts +++ b/src/modules/users/dto/user.dto.ts @@ -3,20 +3,20 @@ import { IsEmail, IsString, IsUUID, MaxLength } from 'class-validator'; export class UserDto { @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' }) @IsUUID() - id: string; + id!: string; @ApiProperty({ example: 'hashedPassword123' }) @IsString() @MaxLength(255, { message: 'Password too long. Maximum is 255 symbols.' }) - password: string; + password!: string; @ApiProperty({ example: 'Max' }) @IsString() @MaxLength(255, { message: 'Name too long. Maximum is 255 symbols.' }) - name: string; + name!: string; @ApiProperty({ example: 'someemail@test.com' }) @IsEmail() @MaxLength(255, { message: 'Email too long. Maximum is 255 symbols.' }) - email: string; + email!: string; } diff --git a/src/modules/users/responses/user.response.ts b/src/modules/users/responses/user.response.ts index aa47581..db83326 100644 --- a/src/modules/users/responses/user.response.ts +++ b/src/modules/users/responses/user.response.ts @@ -5,13 +5,13 @@ import { Exclude, Expose } from 'class-transformer'; export class UserResponse { @Expose() @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' }) - id: string; + id!: string; @Expose() @ApiProperty({ example: 'Max' }) - name: string; + name!: string; @Expose() @ApiProperty({ example: 'someemail@test.com' }) - email: string; + email!: string; } diff --git a/src/responses/typed-list.response.factory.ts b/src/responses/typed-list.response.factory.ts index d023237..f5436b6 100644 --- a/src/responses/typed-list.response.factory.ts +++ b/src/responses/typed-list.response.factory.ts @@ -8,12 +8,12 @@ export function TypedListResponseFactory(ListClass: Constructor) { class TypedList implements IListResponse { @Expose() @ApiProperty({ example: 1 }) - total: number; + total!: number; @Expose() @Type(() => ListClass) @ApiProperty({ type: ListClass, isArray: true }) - data: T[]; + data!: T[]; } return TypedList; } diff --git a/tsconfig.json b/tsconfig.json index e3cd1b5..6160166 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "strict": true }, "exclude": [ "node_modules",