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-categories` | Draggable reorder |
|
||||||
| `/admin/proclamation-groups` | Draggable reorder |
|
| `/admin/proclamation-groups` | Draggable reorder |
|
||||||
| `/admin/license-keys` | List-only + rotate |
|
| `/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}
|
{model.displayName}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -317,13 +306,6 @@
|
|||||||
{model.displayName}
|
{model.displayName}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user