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>
This commit is contained in:
S. Esmaeili
2026-10-21 20:31:05 +03:30
co-authored by Claude Opus 5.5
parent 31076174e2
commit 789d224ea0
395 changed files with 19070 additions and 11592 deletions
@@ -0,0 +1,49 @@
import { Controller, Get, Module } from "@nestjs/common";
import type { DashboardDto } from "@imen/shared";
import { AdminAuth } from "../common/auth.js";
import { toMessageDto } from "../contact/contact.module.js";
import { PrismaService } from "../prisma/prisma.service.js";
@AdminAuth()
@Controller("admin/dashboard")
export class DashboardController {
constructor(private readonly prisma: PrismaService) {}
@Get()
async get(): Promise<DashboardDto> {
const [categories, products, draftProducts, newMessages, media, messages, activity] =
await Promise.all([
this.prisma.category.count(),
this.prisma.product.count(),
this.prisma.product.count({ where: { isPublished: false } }),
this.prisma.contactMessage.count({ where: { status: "NEW" } }),
this.prisma.media.count(),
this.prisma.contactMessage.findMany({ orderBy: { id: "desc" }, take: 5 }),
this.prisma.auditLog.findMany({
orderBy: { id: "desc" },
take: 10,
include: { user: { select: { name: true } } },
}),
]);
return {
categories,
products,
draftProducts,
newMessages,
media,
recentMessages: messages.map(toMessageDto),
recentActivity: activity.map((a) => ({
id: a.id,
action: a.action,
entity: a.entity,
summary: a.summary,
user: a.user?.name ?? null,
createdAt: a.createdAt.toISOString(),
})),
};
}
}
@Module({ controllers: [DashboardController] })
export class DashboardModule {}