From 4d21288511c3be84aea5d3734e5c7e27d220bcea Mon Sep 17 00:00:00 2001 From: NeilClack Date: Wed, 8 Jul 2026 14:11:01 -0500 Subject: [PATCH] api: switch license endpoint to POST/JSON, fix active check, add key copy button - Change /api/v1/license from GET with query params to POST with JSON body - Request body uses license_key and email_address - Response returns key, email_address, subscription_level - Add active check (reject deactivated license keys) - Exclude /api/v1/automations/ from API key auth (GHL webhook) - Add copyableFields prop to AdminTable with clipboard icon - Enable copy button on license key field in license-keys admin page --- src/hooks.server.ts | 5 ++- src/lib/components/admin/AdminTable.svelte | 34 ++++++++++++++++- .../admin/license-keys/+page.svelte | 1 + src/routes/api/v1/license/+server.ts | 37 ++++++++++--------- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e1d56d5..bde0b72 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,7 +4,10 @@ import { building } from '$app/environment'; import { validateApiKey } from '$lib/server/api-key'; export async function handle({ event, resolve }) { - if (event.url.pathname.startsWith('/api/v1/')) { + if ( + event.url.pathname.startsWith('/api/v1/') && + !event.url.pathname.startsWith('/api/v1/automations/') + ) { const authHeader = event.request.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { return new Response(JSON.stringify({ error: 'API key required' }), { diff --git a/src/lib/components/admin/AdminTable.svelte b/src/lib/components/admin/AdminTable.svelte index d522f4e..64526d0 100644 --- a/src/lib/components/admin/AdminTable.svelte +++ b/src/lib/components/admin/AdminTable.svelte @@ -9,6 +9,7 @@ fieldTypes = {} as Record, singular = 'record', draggable = false, + copyableFields = [] as string[], onRowClick, onNew, onReorder @@ -19,6 +20,7 @@ fieldTypes?: Record; singular?: string; draggable?: boolean; + copyableFields?: string[]; onRowClick?: (record: Record) => void; onNew?: () => void; onReorder?: (ids: (string | number)[]) => void; @@ -109,6 +111,10 @@ hoverIndex = null; } + function copyValue(v: unknown) { + navigator.clipboard.writeText(String(v ?? '')); + } + function formatCellValue(v: unknown, field: string): string { if (v == null) return ''; const ft = fieldTypes[field]; @@ -196,7 +202,33 @@ {/if} {#each listFields as field (field)} - {formatCellValue(resolveFieldValue(record, field), field)} + + {formatCellValue(resolveFieldValue(record, field), field)} + {#if copyableFields.includes(field)} + + {/if} + {/each} diff --git a/src/routes/(protected)/admin/license-keys/+page.svelte b/src/routes/(protected)/admin/license-keys/+page.svelte index 90cbf10..1cbf182 100644 --- a/src/routes/(protected)/admin/license-keys/+page.svelte +++ b/src/routes/(protected)/admin/license-keys/+page.svelte @@ -68,6 +68,7 @@ searchFields={config.searchFields} fieldTypes={config.fieldTypes} singular={config.displayNameSingular} + copyableFields={['key']} onRowClick={openReplace} /> diff --git a/src/routes/api/v1/license/+server.ts b/src/routes/api/v1/license/+server.ts index 89053db..e9c7282 100644 --- a/src/routes/api/v1/license/+server.ts +++ b/src/routes/api/v1/license/+server.ts @@ -2,29 +2,31 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { prisma } from '$lib/db'; -// Get the key and email form the URL -// First, check if the key even exists, throw error if not -// Second, check that the email is attached to a real user, throw error -// Third, check that the key retrieved from the database belongs to the user, throw error -// Return response -export const GET: RequestHandler = async ({ url }) => { - const key = url.searchParams.get('key') ?? ''; - const email = url.searchParams.get('email') ?? ''; - if (key === '') { - throw error(400, 'license key required'); +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json(); + const licenseKey = body.license_key ?? ''; + const email = body.email_address ?? ''; + + if (!licenseKey) { + throw error(400, 'license_key is required'); + } + if (!email) { + throw error(400, 'email_address is required'); } const storedKey = await prisma.licenseKey.findFirst({ - where: { - key: key - } + where: { key: licenseKey } }); if (!storedKey) { throw error(400, 'license key not found'); } - const userAccount = await prisma.user.findUniqueOrThrow({ where: { email: email } }); + if (!storedKey.active) { + throw error(400, 'license key is not active'); + } + + const userAccount = await prisma.user.findUnique({ where: { email } }); if (!userAccount) { throw error(400, 'user does not exist'); @@ -37,8 +39,9 @@ export const GET: RequestHandler = async ({ url }) => { return new Response( JSON.stringify({ key: storedKey.key, - active: storedKey.active, - level: storedKey.level - }) + email_address: email, + subscription_level: storedKey.level + }), + { headers: { 'content-type': 'application/json' } } ); };