Delete "API Key" page
This commit is contained in:
@@ -50,4 +50,3 @@ CRUD layer has been removed; see `docs/ADMIN_OVERHAUL.md` for the migration note
|
||||
| `/admin/proclamation-categories` | Draggable reorder |
|
||||
| `/admin/proclamation-groups` | Draggable reorder |
|
||||
| `/admin/license-keys` | List-only + rotate |
|
||||
| `/admin/api-keys` | Super-only key rotation |
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { prisma } from '$lib/db';
|
||||
import { rotateApiKey } from '$lib/server/api-key';
|
||||
import { isSuper } from '$lib/server/roles';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!isSuper(locals)) {
|
||||
throw error(403, 'Only Super admins can manage API keys');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const activeKey = await prisma.apiKey.findFirst({
|
||||
where: {
|
||||
revokedAt: null,
|
||||
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }]
|
||||
},
|
||||
select: { keyPrefix: true, createdAt: true, expiresAt: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const expiredCount = await prisma.apiKey.count({
|
||||
where: {
|
||||
OR: [{ revokedAt: { not: null, lte: now } }, { expiresAt: { not: null, lte: now } }]
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
activeKey: activeKey
|
||||
? {
|
||||
prefix: activeKey.keyPrefix,
|
||||
createdAt: activeKey.createdAt.toISOString(),
|
||||
expiresAt: activeKey.expiresAt?.toISOString() ?? null
|
||||
}
|
||||
: null,
|
||||
totalRotated: expiredCount
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
rotate: async ({ locals }) => {
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Not authorized' };
|
||||
}
|
||||
|
||||
const result = await rotateApiKey(false);
|
||||
return {
|
||||
success: true,
|
||||
newKey: result.plaintext,
|
||||
mode: 'normal' as const,
|
||||
expiresAt: result.previousExpiresAt?.toISOString() ?? null
|
||||
};
|
||||
},
|
||||
rotateEmergency: async ({ locals }) => {
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Not authorized' };
|
||||
}
|
||||
|
||||
const result = await rotateApiKey(true);
|
||||
return { success: true, newKey: result.plaintext, mode: 'emergency' as const };
|
||||
}
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
let newKey = $state('');
|
||||
let mode = $state<'normal' | 'emergency' | null>(null);
|
||||
let expiryDate = $state<string | null>(null);
|
||||
let toastMessage = $state('');
|
||||
|
||||
function handleRotate(emergency: boolean): SubmitFunction {
|
||||
return ({ cancel }) => {
|
||||
const msg = emergency
|
||||
? 'EMERGENCY ROTATION: The current key will be revoked IMMEDIATELY. Continue?'
|
||||
: 'Rotate the API key? The current key will remain valid for 48 hours.';
|
||||
if (!confirm(msg)) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' && result.status === 200) {
|
||||
if (result.data?.newKey) {
|
||||
newKey = result.data.newKey;
|
||||
mode = result.data.mode ?? null;
|
||||
expiryDate = result.data.expiresAt ?? null;
|
||||
}
|
||||
} else {
|
||||
toastMessage = 'Rotation failed';
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKey);
|
||||
toastMessage = 'Copied to clipboard';
|
||||
}
|
||||
|
||||
function dismissKey() {
|
||||
newKey = '';
|
||||
mode = null;
|
||||
expiryDate = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-full">
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 class="text-xl font-semibold text-slate-900">API Key Management</h2>
|
||||
{#if data.activeKey}
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if newKey}
|
||||
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-900">
|
||||
{mode === 'emergency' ? 'Emergency Rotation Complete' : 'New API Key Generated'}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-slate-600">This key will not be shown again. Copy it now.</p>
|
||||
<div class="mt-3 rounded-md bg-slate-100 px-3 py-2">
|
||||
<code class="text-sm break-all text-slate-800">{newKey}</code>
|
||||
</div>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={copyKey}
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={dismissKey}
|
||||
class="inline-flex h-10 items-center justify-center rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 shadow-sm transition hover:bg-slate-50"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
{#if mode === 'normal' && expiryDate}
|
||||
<p class="mt-3 text-xs text-slate-500">
|
||||
The previous key will remain valid until {new Date(expiryDate).toLocaleString()}
|
||||
to give in-field devices time to update.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toastMessage}
|
||||
<div
|
||||
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
>
|
||||
{toastMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-4 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
|
||||
<h3 class="text-base font-semibold text-slate-900">Current Key Status</h3>
|
||||
{#if data.activeKey}
|
||||
<div class="mt-3 space-y-1 text-sm text-slate-700">
|
||||
<div>
|
||||
Prefix: <code class="rounded bg-slate-100 px-1.5 py-0.5">{data.activeKey.prefix}</code>
|
||||
</div>
|
||||
<div>Created: {new Date(data.activeKey.createdAt).toLocaleString()}</div>
|
||||
{#if data.activeKey.expiresAt}
|
||||
<div>Expires: {new Date(data.activeKey.expiresAt).toLocaleString()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="mt-2 text-sm text-slate-600">No active API key. Generate one below.</p>
|
||||
{/if}
|
||||
{#if data.totalRotated > 0}
|
||||
<p class="mt-2 text-sm text-slate-600">
|
||||
{data.totalRotated} previous key(s) have been rotated.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<form method="POST" action="?/rotate" use:enhance={handleRotate(false)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-indigo-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-indigo-700"
|
||||
>
|
||||
Generate New API Key
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="?/rotateEmergency" use:enhance={handleRotate(true)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md bg-red-600 px-4 text-sm font-medium text-white shadow-sm transition hover:bg-red-700"
|
||||
>
|
||||
Emergency Rotation
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,17 +165,6 @@
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={() => (adminOpen = false)}
|
||||
class={`block rounded-md px-3 py-2 text-sm transition ${
|
||||
isActive('/admin/api-keys')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -317,13 +306,6 @@
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/api-keys')}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user