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,115 @@
import { Buffer } from "node:buffer";
import { randomUUID } from "node:crypto";
import { createError, readBody } from "h3";
import { z } from "zod";
import { requireAuth0User } from "../../utils/auth0";
import { getSupabaseServiceClient } from "../../utils/supabase";
import { useRuntimeConfig } from "#imports";
const requestSchema = z.object({
id: z.string().uuid().optional(),
name: z.string().min(1).max(120),
templateId: z.string().min(1).max(64),
previewDataUrl: z.string().regex(/^data:image\/png;base64,/),
productionDataUrl: z.string().regex(/^data:image\/png;base64,/).optional(),
designJson: z.record(z.any()),
notes: z.string().max(640).optional(),
});
type RequestPayload = z.infer<typeof requestSchema>;
type DesignRecord = {
id: string;
user_id: string;
name: string;
template_id: string;
preview_path: string;
preview_url: string | null;
design_json: unknown;
notes?: string | null;
created_at?: string;
updated_at?: string;
};
const dataUrlToBuffer = (dataUrl: string): Buffer => {
const [, base64Data] = dataUrl.split(",");
if (!base64Data) {
throw createError({ statusCode: 400, statusMessage: "Invalid image data URI." });
}
return Buffer.from(base64Data, "base64");
};
export default defineEventHandler(async (event) => {
const user = await requireAuth0User(event);
const rawBody = await readBody<RequestPayload>(event);
const parsed = requestSchema.safeParse(rawBody);
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: "Invalid request body.",
data: parsed.error.flatten(),
});
}
const body = parsed.data;
const config = useRuntimeConfig();
const bucket = config.public.supabase?.storageBucket;
if (!bucket) {
throw createError({
statusCode: 500,
statusMessage: "Supabase storage bucket is not configured.",
});
}
const supabase = getSupabaseServiceClient();
const designId = body.id ?? randomUUID();
const filePath = `previews/${user.sub}/${designId}.png`;
const uploadResult = await supabase.storage
.from(bucket)
.upload(filePath, dataUrlToBuffer(body.previewDataUrl), {
contentType: "image/png",
upsert: true,
});
if (uploadResult.error) {
throw createError({
statusCode: 500,
statusMessage: `Failed to upload preview: ${uploadResult.error.message}`,
});
}
const { data: publicUrlData } = supabase.storage.from(bucket).getPublicUrl(filePath);
const previewUrl = publicUrlData.publicUrl ?? null;
const record: DesignRecord = {
id: designId,
user_id: user.sub,
name: body.name,
template_id: body.templateId,
preview_path: filePath,
preview_url: previewUrl,
design_json: body.designJson,
notes: body.notes ?? null,
updated_at: new Date().toISOString(),
};
const upsertResult = await supabase
.from("designs")
.upsert(record, { onConflict: "id" })
.select()
.single();
if (upsertResult.error) {
throw createError({
statusCode: 500,
statusMessage: `Failed to save design: ${upsertResult.error.message}`,
});
}
return upsertResult.data;
});