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:
115
server/api/designs/index.post.ts
Normal file
115
server/api/designs/index.post.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user