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
This commit is contained in:
+4
-1
@@ -4,7 +4,10 @@ import { building } from '$app/environment';
|
|||||||
import { validateApiKey } from '$lib/server/api-key';
|
import { validateApiKey } from '$lib/server/api-key';
|
||||||
|
|
||||||
export async function handle({ event, resolve }) {
|
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');
|
const authHeader = event.request.headers.get('authorization');
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
return new Response(JSON.stringify({ error: 'API key required' }), {
|
return new Response(JSON.stringify({ error: 'API key required' }), {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
fieldTypes = {} as Record<string, PrismaScalarType>,
|
fieldTypes = {} as Record<string, PrismaScalarType>,
|
||||||
singular = 'record',
|
singular = 'record',
|
||||||
draggable = false,
|
draggable = false,
|
||||||
|
copyableFields = [] as string[],
|
||||||
onRowClick,
|
onRowClick,
|
||||||
onNew,
|
onNew,
|
||||||
onReorder
|
onReorder
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
fieldTypes?: Record<string, PrismaScalarType>;
|
fieldTypes?: Record<string, PrismaScalarType>;
|
||||||
singular?: string;
|
singular?: string;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
|
copyableFields?: string[];
|
||||||
onRowClick?: (record: Record<string, unknown>) => void;
|
onRowClick?: (record: Record<string, unknown>) => void;
|
||||||
onNew?: () => void;
|
onNew?: () => void;
|
||||||
onReorder?: (ids: (string | number)[]) => void;
|
onReorder?: (ids: (string | number)[]) => void;
|
||||||
@@ -109,6 +111,10 @@
|
|||||||
hoverIndex = null;
|
hoverIndex = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyValue(v: unknown) {
|
||||||
|
navigator.clipboard.writeText(String(v ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
function formatCellValue(v: unknown, field: string): string {
|
function formatCellValue(v: unknown, field: string): string {
|
||||||
if (v == null) return '';
|
if (v == null) return '';
|
||||||
const ft = fieldTypes[field];
|
const ft = fieldTypes[field];
|
||||||
@@ -196,7 +202,33 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#each listFields as field (field)}
|
{#each listFields as field (field)}
|
||||||
<td class="px-4 py-2 text-sm">
|
<td class="px-4 py-2 text-sm">
|
||||||
{formatCellValue(resolveFieldValue(record, field), field)}
|
<span class="inline-flex items-center gap-1.5">
|
||||||
|
{formatCellValue(resolveFieldValue(record, field), field)}
|
||||||
|
{#if copyableFields.includes(field)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
copyValue(resolveFieldValue(record, field));
|
||||||
|
}}
|
||||||
|
title="Copy to clipboard"
|
||||||
|
class="shrink-0 leading-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="size-3.5"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3.5" y="2.5" width="11" height="11" rx="1" />
|
||||||
|
<path d="M5.5 2.5V1.5a1 1 0 011-1h3a1 1 0 011 1v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
searchFields={config.searchFields}
|
searchFields={config.searchFields}
|
||||||
fieldTypes={config.fieldTypes}
|
fieldTypes={config.fieldTypes}
|
||||||
singular={config.displayNameSingular}
|
singular={config.displayNameSingular}
|
||||||
|
copyableFields={['key']}
|
||||||
onRowClick={openReplace}
|
onRowClick={openReplace}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,29 +2,31 @@ import { error } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
|
||||||
// Get the key and email form the URL
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
// First, check if the key even exists, throw error if not
|
const body = await request.json();
|
||||||
// Second, check that the email is attached to a real user, throw error
|
const licenseKey = body.license_key ?? '';
|
||||||
// Third, check that the key retrieved from the database belongs to the user, throw error
|
const email = body.email_address ?? '';
|
||||||
// Return response
|
|
||||||
export const GET: RequestHandler = async ({ url }) => {
|
if (!licenseKey) {
|
||||||
const key = url.searchParams.get('key') ?? '';
|
throw error(400, 'license_key is required');
|
||||||
const email = url.searchParams.get('email') ?? '';
|
}
|
||||||
if (key === '') {
|
if (!email) {
|
||||||
throw error(400, 'license key required');
|
throw error(400, 'email_address is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedKey = await prisma.licenseKey.findFirst({
|
const storedKey = await prisma.licenseKey.findFirst({
|
||||||
where: {
|
where: { key: licenseKey }
|
||||||
key: key
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!storedKey) {
|
if (!storedKey) {
|
||||||
throw error(400, 'license key not found');
|
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) {
|
if (!userAccount) {
|
||||||
throw error(400, 'user does not exist');
|
throw error(400, 'user does not exist');
|
||||||
@@ -37,8 +39,9 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
key: storedKey.key,
|
key: storedKey.key,
|
||||||
active: storedKey.active,
|
email_address: email,
|
||||||
level: storedKey.level
|
subscription_level: storedKey.level
|
||||||
})
|
}),
|
||||||
|
{ headers: { 'content-type': 'application/json' } }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user