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:
co-authored by
Claude Opus 5.5
parent
31076174e2
commit
789d224ea0
@@ -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 {}
|
||||
Reference in New Issue
Block a user