Backend security / security (push) Successful in 8s
Backend security / security (pull_request) Canceled after 0s
Backend verify / verify (push) Canceled after 9s
Backend verify / verify (pull_request) Canceled after 4s
Conventional commits / commits (pull_request) Successful in 31s
Use owner enums for error and T-Bank protocol values with ESLint regression checks. Preserve open wire fallbacks and keep protocol imports independent of DI and Prisma. Verified npm ci, verify, 226 unit tests, 262 isolated e2e tests and unchanged OpenAPI.
149 lines
3.8 KiB
TypeScript
149 lines
3.8 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 '../../../common/decorators/api-safe-error-response.decorator';
|
|
import { HttpErrorCode } from '../../../common/error-code';
|
|
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';
|
|
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: CredentialsDto,
|
|
@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: CredentialsDto,
|
|
@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,
|
|
) {
|
|
try {
|
|
const token = this.refreshToken(request);
|
|
if (!token) throw new UnauthorizedException({ code: HttpErrorCode.Unauthorized });
|
|
return this.respond(response, await this.auth.refresh(token));
|
|
} catch (error) {
|
|
if (error instanceof UnauthorizedException) this.clearCookie(response);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@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: {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
user: { email: string; id: string };
|
|
},
|
|
) {
|
|
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,
|
|
};
|
|
}
|
|
}
|