feat: add design persistence functionality with Auth0 and Supabase integration

- Implemented `useDesignPersistence` composable for managing design records.
- Enhanced `useSlipmatDesigner` to support loading designs from JSON.
- Created global authentication middleware for route protection.
- Added Supabase client plugin for database interactions.
- Developed API endpoints for fetching, saving, and retrieving designs.
- Introduced utility functions for Auth0 token verification and Supabase client retrieval.
- Updated Nuxt configuration to include Auth0 and Supabase environment variables.
- Added necessary dependencies for Auth0 and Supabase.
- Enhanced TypeScript configuration for improved type support.
This commit is contained in:
Frank John Begornia
2025-11-07 00:01:52 +08:00
parent e2955debb7
commit 4d91925fad
20 changed files with 1242 additions and 19 deletions

View File

@@ -0,0 +1,153 @@
import { computed, ref } from "vue";
import { useAuth0 } from "@auth0/auth0-vue";
import { useNuxtApp, useRuntimeConfig } from "nuxt/app";
import type { $Fetch } from "ofetch";
import type { ExportedDesign } from "./useSlipmatDesigner";
type DesignRecord = {
id: string;
name: string;
template_id: string;
preview_url: string | null;
preview_path?: string | null;
design_json?: unknown;
notes?: string | null;
created_at?: string | null;
updated_at?: string | null;
};
export interface SavedDesign {
id: string;
name: string;
templateId: string;
previewUrl: string | null;
notes?: string | null;
createdAt: string | null;
updatedAt: string | null;
designJson?: unknown;
}
type SaveOptions = {
id?: string;
name: string;
design: ExportedDesign;
notes?: string;
};
const mapDesignRecord = (record: DesignRecord): SavedDesign => ({
id: record.id,
name: record.name,
templateId: record.template_id,
previewUrl: record.preview_url ?? null,
notes: record.notes ?? undefined,
createdAt: record.created_at ?? null,
updatedAt: record.updated_at ?? null,
designJson: record.design_json,
});
export const useDesignPersistence = () => {
const runtime = useRuntimeConfig();
const audience = runtime.public.auth0?.audience;
const auth0 = process.client ? useAuth0() : null;
const nuxtApp = useNuxtApp();
const fetcher = nuxtApp.$fetch as $Fetch;
const designs = ref<SavedDesign[]>([]);
const isLoading = ref(false);
const isSaving = ref(false);
const lastError = ref<string | null>(null);
const isAuthenticated = computed(() => auth0?.isAuthenticated.value ?? false);
const acquireToken = async (): Promise<string> => {
if (!auth0) {
throw new Error("Auth0 client is not available.");
}
return auth0.getAccessTokenSilently({
authorizationParams: audience
? {
audience,
}
: undefined,
});
};
const fetchDesigns = async () => {
if (!auth0 || !isAuthenticated.value) {
return;
}
isLoading.value = true;
lastError.value = null;
try {
const token = await acquireToken();
const response = await fetcher<DesignRecord[]>("/api/designs", {
headers: {
Authorization: `Bearer ${token}`,
},
});
designs.value = Array.isArray(response)
? response.map(mapDesignRecord)
: [];
} catch (error) {
lastError.value = error instanceof Error ? error.message : String(error);
throw error;
} finally {
isLoading.value = false;
}
};
const saveDesign = async ({ id, name, design, notes }: SaveOptions) => {
if (!auth0 || !isAuthenticated.value) {
throw new Error("User must be authenticated before saving a design.");
}
isSaving.value = true;
lastError.value = null;
try {
const token = await acquireToken();
const payload = {
id,
name,
templateId: design.templateId,
previewDataUrl: design.previewUrl,
productionDataUrl: design.productionUrl,
designJson: design.canvasJson,
notes,
};
const response = await fetcher<DesignRecord>("/api/designs", {
method: "POST",
body: payload,
headers: {
Authorization: `Bearer ${token}`,
},
});
const saved = mapDesignRecord(response);
const next = designs.value.filter((item) => item.id !== saved.id);
designs.value = [saved, ...next];
return saved;
} catch (error) {
lastError.value = error instanceof Error ? error.message : String(error);
throw error;
} finally {
isSaving.value = false;
}
};
return {
designs,
isLoading,
isSaving,
lastError,
isAuthenticated,
fetchDesigns,
saveDesign,
};
};

View File

@@ -20,6 +20,7 @@ export interface ExportedDesign {
productionBlob: Blob;
templateId: string;
createdAt: string;
canvasJson: FabricCanvasJSON;
}
const DISPLAY_SIZE = 720;
@@ -63,6 +64,7 @@ type FabricCircle = FabricNamespace.Circle;
type FabricRect = FabricNamespace.Rect;
type FabricTextbox = FabricNamespace.Textbox;
type FabricObject = FabricNamespace.Object;
type FabricCanvasJSON = ReturnType<FabricCanvas["toJSON"]>;
type CanvasReadyPayload = {
canvas: FabricCanvas;
@@ -638,6 +640,8 @@ export const useSlipmatDesigner = () => {
}
productionObjectUrl.value = URL.createObjectURL(productionDataBlob);
const canvasJson = currentCanvas.toJSON() as FabricCanvasJSON;
return {
previewUrl: previewDataUrl,
previewBlob: previewDataBlob,
@@ -645,12 +649,30 @@ export const useSlipmatDesigner = () => {
productionBlob: productionDataBlob,
templateId: selectedTemplate.value.id,
createdAt: new Date().toISOString(),
canvasJson,
};
} finally {
isExporting.value = false;
}
};
const loadDesignFromJson = async (designJson: FabricCanvasJSON) => {
const fabric = fabricApi.value;
const currentCanvas = canvas.value;
if (!fabric || !currentCanvas) {
return;
}
await new Promise<void>((resolve) => {
currentCanvas.loadFromJSON(designJson, () => {
currentCanvas.renderAll();
maintainStaticLayerOrder();
schedulePreviewRefresh();
resolve();
});
});
};
const downloadPreview = async () => {
if (!previewUrl.value) {
await refreshPreview();
@@ -717,6 +739,7 @@ export const useSlipmatDesigner = () => {
resetZoom,
clearDesign,
exportDesign,
loadDesignFromJson,
downloadPreview,
downloadProduction,
refreshPreview,