diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 88eead7..89992b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,4 +56,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy build --project-name=ramolibrelab --branch=main + command: pages deploy --project-name=ramolibrelab --branch=main diff --git a/functions/api/lab/[id].js b/functions/api/lab/[id].js new file mode 100644 index 0000000..2af9357 --- /dev/null +++ b/functions/api/lab/[id].js @@ -0,0 +1,28 @@ +const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días + +export async function onRequestGet(context) { + const { params, env } = context; + const id = params.id; + + if (!id || !/^[A-Za-z0-9]{4,12}$/.test(id)) { + return Response.json({ error: 'invalid_id' }, { status: 400 }); + } + + const raw = await env.LAB_KV.get(id); + if (raw === null) { + return Response.json({ error: 'not_found' }, { status: 404 }); + } + + const response = new Response(raw, { + status: 200, + headers: { 'content-type': 'application/json' } + }); + + context.waitUntil( + env.LAB_KV.put(id, raw, { expirationTtl: TTL_SECONDS }).catch((err) => { + console.error('[lab] TTL renewal failed', err); + }) + ); + + return response; +} diff --git a/functions/api/lab/index.js b/functions/api/lab/index.js new file mode 100644 index 0000000..10b7359 --- /dev/null +++ b/functions/api/lab/index.js @@ -0,0 +1,52 @@ +const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; +const ID_LENGTH = 6; +const MAX_PAYLOAD_BYTES = 100_000; +const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días + +function generateId() { + const bytes = new Uint8Array(ID_LENGTH); + crypto.getRandomValues(bytes); + let id = ''; + for (let i = 0; i < ID_LENGTH; i++) { + id += ALPHABET[bytes[i] % ALPHABET.length]; + } + return id; +} + +export async function onRequestPost(context) { + const { request, env } = context; + + let payload; + try { + payload = await request.json(); + } catch { + return Response.json({ error: 'invalid_json' }, { status: 400 }); + } + + const serialized = JSON.stringify(payload); + if (serialized.length > MAX_PAYLOAD_BYTES) { + return Response.json({ error: 'payload_too_large' }, { status: 413 }); + } + + let id; + for (let attempt = 0; attempt < 3; attempt++) { + const candidate = generateId(); + const existing = await env.LAB_KV.get(candidate); + if (!existing) { + id = candidate; + break; + } + } + + if (!id) { + return Response.json({ error: 'id_generation_failed' }, { status: 500 }); + } + + try { + await env.LAB_KV.put(id, serialized, { expirationTtl: TTL_SECONDS }); + } catch { + return Response.json({ error: 'kv_write_failed' }, { status: 507 }); + } + + return Response.json({ id }, { status: 201 }); +} diff --git a/src/lib/share.ts b/src/lib/share.ts new file mode 100644 index 0000000..de7bcc0 --- /dev/null +++ b/src/lib/share.ts @@ -0,0 +1,51 @@ +import { decodeUrlData, encodeUrlData } from '$lib/utils/url_data'; +import type { Simulacion } from '$lib/state/simulaciones.svelte'; + +export async function shareLab(labData: unknown): Promise { + const origin = window.location.origin; + + try { + const res = await fetch('/api/lab', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(labData) + }); + + if (!res.ok) throw new Error(`status ${res.status}`); + + const { id } = (await res.json()) as { id: string }; + return `${origin}/?s=${id}`; + } catch (err) { + console.warn('[share] fallback a Base64 por fallo de red/KV', err); + const payload = encodeUrlData(labData); + return `${origin}/?share=${payload}`; + } +} + +export async function hydrateFromUrl(): Promise { + const params = new URLSearchParams(window.location.search); + + const shortId = params.get('s'); + if (shortId) { + try { + const res = await fetch(`/api/lab/${shortId}`); + if (res.ok) { + return (await res.json()) as Simulacion; + } + } catch (err) { + console.warn('[share] error hidratando desde enlace corto', err); + } + return null; + } + + const shared = params.get('share'); + if (shared) { + try { + return decodeUrlData(shared); + } catch (err) { + console.warn('[share] enlace share inválido', err); + } + } + + return null; +} diff --git a/src/routes/(app)/_components/Sidebar.svelte b/src/routes/(app)/_components/Sidebar.svelte index 8ffa07e..0265f8d 100644 --- a/src/routes/(app)/_components/Sidebar.svelte +++ b/src/routes/(app)/_components/Sidebar.svelte @@ -1,8 +1,7 @@