Restructure the repo into a pnpm + Turborepo monorepo: - apps/web: Nuxt 4 site, now server-rendered and reading all content from the API; pages cached with SWR and purged on content changes - apps/api: NestJS 12 + Prisma 7 + PostgreSQL. Auth with short-lived access tokens and rotating httpOnly refresh cookies, roles (ADMIN, EDITOR), catalog, editable site sections, media upload to S3-compatible storage (converted to WebP), contact messages, audit log - apps/admin: Vue 3 admin panel (Ant Design Vue, Pinia, Tailwind) for products, categories, site sections, messages, media and users - packages/shared: Zod schemas and types shared by API and admin Also: - seed script that loads the original 8 categories / 89 products and section content - Docker images for api (with a one-off migrate service), web, admin, and an nginx gateway for both domains - dev and production compose files, daily database backup service - API e2e tests Removes the committed static out/ build and the old root Dockerfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 {}
|