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,67 @@
|
||||
import type { AdminUserDto, AuthResponse, LoginInput } from "@imen/shared";
|
||||
import { defineStore } from "pinia";
|
||||
import { rawApi, setAuthHooks } from "@/api/http";
|
||||
import { router } from "@/router";
|
||||
|
||||
let refreshing: Promise<boolean> | null = null;
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
/** short-lived access token, kept in memory only */
|
||||
token: "",
|
||||
user: null as AdminUserDto | null,
|
||||
/** true once the initial session check has run */
|
||||
ready: false,
|
||||
}),
|
||||
getters: {
|
||||
isAdmin: (s) => s.user?.role === "ADMIN",
|
||||
},
|
||||
actions: {
|
||||
setSession(r: AuthResponse) {
|
||||
this.token = r.accessToken;
|
||||
this.user = r.user;
|
||||
},
|
||||
clear() {
|
||||
this.token = "";
|
||||
this.user = null;
|
||||
},
|
||||
async login(input: LoginInput) {
|
||||
this.setSession(await rawApi<AuthResponse>("/admin/auth/login", { method: "POST", body: input }));
|
||||
},
|
||||
/** Exchanges the refresh cookie for a new access token; concurrent calls share one request */
|
||||
refresh(): Promise<boolean> {
|
||||
refreshing ??= rawApi<AuthResponse>("/admin/auth/refresh", { method: "POST" })
|
||||
.then((r) => {
|
||||
this.setSession(r);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
this.clear();
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
refreshing = null;
|
||||
});
|
||||
return refreshing;
|
||||
},
|
||||
/** Restores the session after a page load (the refresh cookie survives reloads) */
|
||||
async init() {
|
||||
if (this.ready) return;
|
||||
setAuthHooks({
|
||||
token: () => this.token,
|
||||
refresh: () => this.refresh(),
|
||||
onExpired: () => {
|
||||
this.clear();
|
||||
void router.push({ name: "login", query: { redirect: router.currentRoute.value.fullPath } });
|
||||
},
|
||||
});
|
||||
await this.refresh();
|
||||
this.ready = true;
|
||||
},
|
||||
async logout() {
|
||||
await rawApi("/admin/auth/logout", { method: "POST" }).catch(() => undefined);
|
||||
this.clear();
|
||||
await router.push({ name: "login" });
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user