102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
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<string, string> | 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<AuthResponse> {
|
||
|
|
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<AuthResponse> {
|
||
|
|
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 {}
|