Files
backend/src/modules/auth/presentation/auth.controller.ts
T
ksv741 e7f75ebda1
Backend verify / commits (pull_request) Successful in 1m28s
Backend verify / security (pull_request) Canceled after 0s
Backend verify / verify (pull_request) Canceled after 0s
refactor: replace deep imports with native node aliases (WealthLedger/backend#165)
2026-11-04 14:28:52 +03:00

142 lines
3.6 KiB
TypeScript

import type { Request, Response } from 'express';
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiNoContentResponse,
ApiOkResponse,
ApiProperty,
ApiTags,
} from '@nestjs/swagger';
import { ApiProblemResponses } from '#src/common/decorators/api-safe-error-response.decorator';
import { HttpErrorCode } from '#src/common/error-code';
import { AuthService, REFRESH_TTL_MS } from '../application/auth.service';
import { AccessPayload, AuthenticationResult } from '../application/auth.types';
import { LoginDto, RegisterDto } from './auth.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
const REFRESH_COOKIE = 'refresh_token';
const COOKIE_PATH = '/api/v1/auth';
class UserDto {
@ApiProperty()
declare id: string;
@ApiProperty()
declare email: string;
}
class AuthenticationDto {
@ApiProperty()
declare accessToken: string;
@ApiProperty({ type: UserDto })
declare user: UserDto;
}
@ApiTags('auth')
@ApiProblemResponses()
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Post('register')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: AuthenticationDto })
async register(
@Body() credentials: RegisterDto,
@Res({ passthrough: true }) response: Response,
) {
return this.respond(
response,
await this.auth.register(credentials.email, credentials.password),
);
}
@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: AuthenticationDto })
async login(
@Body() credentials: LoginDto,
@Res({ passthrough: true }) response: Response,
) {
return this.respond(
response,
await this.auth.login(credentials.email, credentials.password),
);
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: AuthenticationDto })
async refresh(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
) {
const token = this.refreshToken(request);
if (!token) throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
return this.respond(response, await this.auth.refresh(token));
}
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiNoContentResponse()
async logout(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
await this.auth.logout(this.refreshToken(request));
this.clearCookie(response);
}
@Get('me')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserDto })
async me(@Req() request: Request & { user: AccessPayload }) {
const user = await this.auth.userById(request.user.sub);
return this.auth.toUser(user);
}
private respond(
response: Response,
result: AuthenticationResult,
) {
response.cookie(REFRESH_COOKIE, result.refreshToken, this.cookieOptions());
return { user: result.user, accessToken: result.accessToken };
}
private clearCookie(response: Response): void {
response.clearCookie(REFRESH_COOKIE, this.cookieOptions());
}
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;
}
private cookieOptions() {
return {
httpOnly: true,
sameSite: 'lax' as const,
secure: process.env['NODE_ENV'] !== 'development',
path: COOKIE_PATH,
maxAge: REFRESH_TTL_MS,
};
}
}