Initial commit of Step Admin panel
This commit is contained in:
54
src/routes/certificates/create/+page.server.ts
Normal file
54
src/routes/certificates/create/+page.server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { createCertificate, createP12 } from '$lib/server/step';
|
||||
import { logAudit } from '$lib/server/audit';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export const actions = {
|
||||
default: async (event: import('@sveltejs/kit').RequestEvent) => {
|
||||
const data = await event.request.formData();
|
||||
const deviceName = data.get('device_name')?.toString() || '';
|
||||
const subject = data.get('subject')?.toString() || '';
|
||||
const hours = parseInt(data.get('hours')?.toString() || '8760', 10);
|
||||
const exportP12 = data.get('export_p12') === 'on';
|
||||
const p12Password = data.get('p12_password')?.toString() || '';
|
||||
|
||||
if (!deviceName || !subject || isNaN(hours)) {
|
||||
return fail(400, { error: 'Missing required fields' });
|
||||
}
|
||||
if (exportP12 && !p12Password) {
|
||||
return fail(400, { error: 'p12 password required if exporting p12' });
|
||||
}
|
||||
|
||||
try {
|
||||
// generate serial number roughly matching the cert if step output differs, or just a unique ID.
|
||||
// step ca doesn't easily output the exact serial in simple exec without inspect command,
|
||||
// so we will just create a unique internal reference for our DB tracking if necessary,
|
||||
// or parse it from `step certificate inspect`
|
||||
// For this basic MVP, we generate a UUID for the db relation to file.
|
||||
const serialNumber = crypto.randomUUID();
|
||||
|
||||
const { crtFile } = await createCertificate(deviceName, subject, hours);
|
||||
if (exportP12) {
|
||||
await createP12(deviceName, p12Password);
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + hours);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO certificates (device_name, subject, serial_number, status, expires_at, file_path, created_by)
|
||||
VALUES (?, ?, ?, 'active', ?, ?, 'admin')
|
||||
`);
|
||||
stmt.run(deviceName, subject, serialNumber, expiresAt.toISOString(), crtFile);
|
||||
|
||||
logAudit('create_cert', `Created device cert ${deviceName}`, 'admin', event.getClientAddress());
|
||||
|
||||
} catch (err: any) {
|
||||
return fail(500, { error: err.message || 'Failed to create certificate' });
|
||||
}
|
||||
|
||||
throw redirect(303, '/certificates');
|
||||
}
|
||||
};
|
||||
59
src/routes/certificates/create/+page.svelte
Normal file
59
src/routes/certificates/create/+page.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
let { form } = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="md:grid md:grid-cols-3 md:gap-6 mb-6">
|
||||
<div class="md:col-span-1">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">Create Certificate</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Generate a new device certificate using step-ca.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-5 md:col-span-2 md:mt-0">
|
||||
<form method="POST" class="shadow sm:overflow-hidden sm:rounded-md bg-white">
|
||||
<div class="space-y-6 bg-white px-4 py-5 sm:p-6">
|
||||
|
||||
{#if form?.error}
|
||||
<div class="text-sm text-red-600 bg-red-50 p-3 rounded">{form.error}</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<label for="device_name" class="block text-sm font-medium text-gray-700">Device Name (Identifier)</label>
|
||||
<input type="text" name="device_name" id="device_name" required pattern="[a-zA-Z0-9_-]+" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" placeholder="client-001">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="subject" class="block text-sm font-medium text-gray-700">Subject Name</label>
|
||||
<input type="text" name="subject" id="subject" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border" placeholder="admin@example.com">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="hours" class="block text-sm font-medium text-gray-700">Validity (Hours)</label>
|
||||
<input type="number" name="hours" id="hours" required min="1" value="8760" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex h-5 items-center">
|
||||
<input id="export_p12" name="export_p12" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="export_p12" class="font-medium text-gray-700">Export p12</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="p12_password" class="block text-sm font-medium text-gray-700">p12 Password (required if exporting p12)</label>
|
||||
<input type="password" name="p12_password" id="p12_password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2 border">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 text-right sm:px-6">
|
||||
<button type="submit" class="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user