chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 16:37:01 +03:00
committed by ksv741
parent 53b2794963
commit a970eaaf7a
179 changed files with 4485 additions and 2716 deletions
@@ -114,8 +114,8 @@ describe('AuthService', () => {
where,
data,
}: {
where: { id?: string; refreshTokenHash?: string };
data: { refreshTokenHash: string };
where: { id?: string; refreshTokenHash?: string };
}) => {
if (where.id === session.id && where.refreshTokenHash === currentHash) {
currentHash = data.refreshTokenHash;
+21 -12
View File
@@ -6,6 +6,7 @@ import {
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { createHash, randomUUID } from 'crypto';
import { Session, User } from '../../../../generated/prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { AuthUser, RefreshPayload } from './auth.types';
@@ -16,6 +17,7 @@ const REFRESH_TTL_MS = 604_800_000;
const ACCESS_TTL = '15m';
const REFRESH_TTL = '7d';
const DUMMY_PASSWORD_HASH =
// eslint-disable-next-line sonarjs/no-hardcoded-passwords -- Public dummy bcrypt hash preserves login timing for nonexistent users; it is not a credential.
'$2b$12$Z49BwJsyiUqY0ZozFzt9IeYvjhV4kbeLw9KdxOguT5bfhA1RvPCx6';
@Injectable()
@@ -28,12 +30,13 @@ export class AuthService {
async register(
email: string,
password: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
): Promise<{ accessToken: string; refreshToken: string; user: AuthUser }> {
const existing = await this.prisma.user.findUnique({
where: { email },
});
if (existing)
if (existing) {
throw new ConflictException({ code: 'EMAIL_ALREADY_REGISTERED' });
}
let user: User;
try {
@@ -45,8 +48,9 @@ export class AuthService {
},
});
} catch (error) {
if (isUniqueConstraintError(error))
if (isUniqueConstraintError(error)) {
throw new ConflictException({ code: 'EMAIL_ALREADY_REGISTERED' });
}
throw error;
}
return this.createSession(user);
@@ -55,7 +59,7 @@ export class AuthService {
async login(
email: string,
password: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
): Promise<{ accessToken: string; refreshToken: string; user: AuthUser }> {
const user = await this.prisma.user.findUnique({
where: { email },
});
@@ -63,14 +67,15 @@ export class AuthService {
password,
user?.passwordHash ?? DUMMY_PASSWORD_HASH,
);
if (!user || !valid)
if (!user || !valid) {
throw new UnauthorizedException({ code: 'INVALID_CREDENTIALS' });
}
return this.createSession(user);
}
async refresh(
token: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
): Promise<{ accessToken: string; refreshToken: string; user: AuthUser }> {
let payload: RefreshPayload;
try {
payload = await this.jwt.verifyAsync<RefreshPayload>(token, {
@@ -79,8 +84,9 @@ export class AuthService {
} catch {
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
if (payload.type !== 'refresh')
if (payload.type !== 'refresh') {
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
const session = await this.prisma.session.findUnique({
where: { id: payload.sid },
include: { user: true },
@@ -91,12 +97,14 @@ export class AuthService {
session.revokedAt ||
session.expiresAt <= new Date()
) {
if (session && !session.revokedAt)
if (session && !session.revokedAt) {
await this.prisma.session.delete({ where: { id: session.id } });
}
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
if (session.refreshTokenHash !== this.hash(token))
if (session.refreshTokenHash !== this.hash(token)) {
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
return this.rotateSession(session, session.refreshTokenHash);
}
@@ -138,7 +146,7 @@ export class AuthService {
private async createSession(
user: User,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
): Promise<{ accessToken: string; refreshToken: string; user: AuthUser }> {
const session = await this.prisma.session.create({
data: {
id: randomUUID(),
@@ -153,7 +161,7 @@ export class AuthService {
private async rotateSession(
session: Session & { user: User },
expectedRefreshTokenHash?: string,
): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> {
): Promise<{ accessToken: string; refreshToken: string; user: AuthUser }> {
const refreshToken = await this.jwt.signAsync(
{
sub: session.userId,
@@ -172,8 +180,9 @@ export class AuthService {
where: { id: session.id, refreshTokenHash: expectedRefreshTokenHash },
data,
});
if (updated.count !== 1)
if (updated.count !== 1) {
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
} else {
await this.prisma.session.update({ where: { id: session.id }, data });
}
+5 -5
View File
@@ -1,17 +1,17 @@
export interface AuthUser {
id: string;
email: string;
id: string;
}
export interface AccessPayload {
sub: string;
sid: string;
sub: string;
type: 'access';
}
export interface RefreshPayload {
sub: string;
sid: string;
type: 'refresh';
jti: string;
sid: string;
sub: string;
type: 'refresh';
}
+3 -2
View File
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { Environment } from '../../config/env.validation';
import { AuthController } from './presentation/auth.controller';
import { AuthService } from './application/auth.service';
import { JwtAuthGuard } from './presentation/jwt-auth.guard';
import { JwtStrategy } from './infrastructure/jwt.strategy';
import { AuthController } from './presentation/auth.controller';
import { JwtAuthGuard } from './presentation/jwt-auth.guard';
@Module({
imports: [
@@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Environment } from '../../../config/env.validation';
import { AccessPayload } from '../application/auth.types';
@@ -17,8 +18,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
validate(payload: AccessPayload): AccessPayload {
if (payload.type !== 'access')
if (payload.type !== 'access') {
throw new UnauthorizedException({ code: 'UNAUTHORIZED' });
}
return payload;
}
}
@@ -1,3 +1,5 @@
import type { Request, Response } from 'express';
import {
Body,
Controller,
@@ -17,11 +19,11 @@ import {
ApiProperty,
ApiTags,
} from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { ApiProblemResponses } from '../../../common/decorators/api-safe-error-response.decorator';
import { CredentialsDto } from './auth.dto';
import { AuthService } from '../application/auth.service';
import { AccessPayload } from '../application/auth.types';
import { CredentialsDto } from './auth.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
const REFRESH_COOKIE = 'refresh_token';
@@ -115,9 +117,9 @@ export class AuthController {
private respond(
response: Response,
result: {
user: { id: string; email: string };
accessToken: string;
refreshToken: string;
user: { email: string; id: string };
},
) {
response.cookie(REFRESH_COOKIE, result.refreshToken, this.cookieOptions());
@@ -129,6 +131,7 @@ export class AuthController {
}
private refreshToken(request: Request): string | undefined {
// eslint-disable-next-line security/detect-object-injection -- The cookie name is a fixed application constant; its value is narrowed below.
const token: unknown = request.cookies?.[REFRESH_COOKIE];
return typeof token === 'string' ? token : undefined;
}
+3 -1
View File
@@ -28,9 +28,10 @@ class EmailConstraint {
character.charCodeAt(0) < ASCII_SPACE ||
character.charCodeAt(0) === ASCII_DELETE,
) &&
value.split('@').every(Boolean)
value.split('@').every(Boolean)
);
}
defaultMessage(): string {
return 'must be a valid email address';
}
@@ -44,6 +45,7 @@ class PasswordBytesConstraint {
Buffer.byteLength(value, 'utf8') <= MAX_PASSWORD_BYTES
);
}
defaultMessage(): string {
return 'must not exceed 72 UTF-8 bytes';
}
@@ -1,5 +1,6 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { RequestContextService } from '../../../common/logging/request-context.service';
import { AccessPayload } from '../application/auth.types';
@@ -14,8 +15,9 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
const request = context
.switchToHttp()
.getRequest<{ user?: AccessPayload }>();
if (request.user?.sub)
if (request.user?.sub) {
this.requestContext.set({ userId: request.user.sub });
}
return Boolean(allowed);
}
}