- 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.
116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
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;
|
|
});
|