import { Body, Controller, Get, HttpCode, Module, Post, Req, Res } from "@nestjs/common"; import { JwtModule, type JwtSignOptions } from "@nestjs/jwt"; import { Throttle } from "@nestjs/throttler"; import { type AuthResponse, type ChangePasswordInput, changePasswordSchema, type LoginInput, loginSchema, } from "@imen/shared"; import type { Request, Response } from "express"; import { env, isProd } from "../config/env.js"; import { AdminAuth, type AuthUser, CurrentUser } from "../common/auth.js"; import { ZodPipe } from "../common/zod.pipe.js"; import { AuthService, type IssuedTokens } from "./auth.service.js"; const COOKIE = "imen_rt"; const COOKIE_PATH = "/api/admin/auth"; const ua = (req: Request) => req.headers["user-agent"] ?? ""; const readCookie = (req: Request): string | undefined => (req.cookies as Record | undefined)?.[COOKIE]; @Controller("admin/auth") export class AuthController { constructor(private readonly auth: AuthService) {} @Post("login") @HttpCode(200) @Throttle({ default: { limit: 8, ttl: 60_000 } }) async login( @Body(new ZodPipe(loginSchema)) body: LoginInput, @Req() req: Request, @Res({ passthrough: true }) res: Response, ): Promise { return this.respond(res, await this.auth.login(body, ua(req))); } @Post("refresh") @HttpCode(200) @Throttle({ default: { limit: 30, ttl: 60_000 } }) async refresh( @Req() req: Request, @Res({ passthrough: true }) res: Response, ): Promise { try { return this.respond(res, await this.auth.refresh(readCookie(req), ua(req))); } catch (e) { res.clearCookie(COOKIE, { path: COOKIE_PATH }); throw e; } } @Post("logout") @HttpCode(204) async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) { await this.auth.logout(readCookie(req)); res.clearCookie(COOKIE, { path: COOKIE_PATH }); } @Get("me") @AdminAuth() me(@CurrentUser() user: AuthUser) { return this.auth.me(user.id); } @Post("change-password") @HttpCode(204) @AdminAuth() async changePassword( @Body(new ZodPipe(changePasswordSchema)) body: ChangePasswordInput, @CurrentUser() user: AuthUser, ) { await this.auth.changePassword(user.id, body); } private respond(res: Response, t: IssuedTokens): AuthResponse { res.cookie(COOKIE, t.refreshToken, { httpOnly: true, secure: isProd && env.COOKIE_SECURE, sameSite: "strict", path: COOKIE_PATH, expires: t.refreshExpiresAt, }); return { accessToken: t.accessToken, user: t.user }; } } @Module({ imports: [ JwtModule.register({ global: true, secret: env.JWT_SECRET, signOptions: { expiresIn: env.JWT_ACCESS_TTL as JwtSignOptions["expiresIn"] }, }), ], controllers: [AuthController], providers: [AuthService], exports: [AuthService], }) export class AuthModule {}