Files
imen_pishghaman/apps/api/src/auth/auth.controller.ts
T
S. EsmaeiliandClaude Opus 5.5 789d224ea0 feat: monorepo with NestJS API, admin panel and DB-driven website
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>
2026-10-21 20:31:05 +03:30

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 {}