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:
153
composables/useDesignPersistence.ts
Normal file
153
composables/useDesignPersistence.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user