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>
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
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" });
|
|
},
|
|
},
|
|
});
|