Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API

Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each
admin route now owns its own form/modal logic. Add DataTable.svelte,
superform-errors.ts, and profile-specific modal/field components.

Flatten proclamation categories/groups and drop subscription levels; add the
v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated
Prisma client with the new migrations.

Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
This commit is contained in:
NeilClack
2026-09-10 09:38:01 -05:00
parent f4e736c92d
commit 91d162c746
127 changed files with 7182 additions and 9785 deletions
@@ -8,38 +8,44 @@ const { sendMail } = vi.hoisted(() => ({
vi.mock('$lib/server/resend', () => ({ sendMail }));
vi.mock('$lib/svelte-admin/registry', () => ({
getModelConfig: vi.fn(() => ({ slug: 'license-keys', listFields: [], searchFields: [] }))
}));
vi.mock('$lib/svelte-admin/crud', () => ({
loadRecords: vi.fn().mockResolvedValue({ records: [], relationOptionsData: {} })
}));
vi.mock('$lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
profile: { findUnique: vi.fn() },
licenseKey: { create: vi.fn() },
subscriptionLevel: { findMany: vi.fn() }
profile: { findFirst: vi.fn() },
licenseKey: { findMany: vi.fn(), findUnique: vi.fn(), create: vi.fn(), delete: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { actions } from './+page.server';
import { load, actions } from './+page.server';
const mockFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockProfileFindUnique = prisma.profile.findUnique as ReturnType<typeof vi.fn>;
const mockProfileFindFirst = prisma.profile.findFirst as ReturnType<typeof vi.fn>;
const mockLicenseKeyCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
const mockLicenseKeyFindUnique = prisma.licenseKey.findUnique as ReturnType<typeof vi.fn>;
const mockLicenseKeyDelete = prisma.licenseKey.delete as ReturnType<typeof vi.fn>;
describe('admin license-keys createKey action', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns records via load', async () => {
const records = [{ id: 'k1', key: 'AAAA-BBBB-CCCC-DDDD' }];
(prisma.licenseKey.findMany as ReturnType<typeof vi.fn>).mockResolvedValue(records);
const result = await load({} as any);
expect(prisma.licenseKey.findMany).toHaveBeenCalledWith({
include: { user: true },
orderBy: { key: 'asc' }
});
expect(result).toEqual({ records });
});
it('rejects unauthenticated requests', async () => {
const locals = createLocals();
const req = createFormRequest({ email: 'jane@example.com', level: 'standard' });
const req = createFormRequest({ email: 'jane@example.com' });
const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
expect(isHttpError(err, 401)).toBe(true);
@@ -47,27 +53,36 @@ describe('admin license-keys createKey action', () => {
it('returns error when email is missing', async () => {
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ level: 'standard' });
const req = createFormRequest({});
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'Email is required' });
expect(result).toEqual({ success: false, action: 'createKey', error: 'Email is required' });
});
it('returns error when level is missing', async () => {
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ email: 'jane@example.com' });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'Subscription level is required' });
});
it('returns error when user is not found', async () => {
it('creates a key with only an email when no user exists yet', async () => {
mockFindUnique.mockResolvedValue(null);
mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ email: 'nobody@example.com', level: 'standard' });
const req = createFormRequest({ email: 'nobody@example.com' });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: false, error: 'No user found with that email' });
expect(result).toEqual({ success: true, action: 'createKey' });
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
expect(createCall.data.active).toBe(true);
expect(createCall.data.userId).toBeUndefined();
expect(createCall.data.email).toBe('nobody@example.com');
expect(sendMail).toHaveBeenCalledOnce();
const mailCall = sendMail.mock.calls[0][0];
expect(mailCall.to).toBe('nobody@example.com');
expect(mailCall.templateId).toBe('license-key-delivery');
expect(mailCall.data.firstName).toBe('Valued Customer');
});
it('creates a license key and sends email with license_key variable', async () => {
@@ -75,22 +90,21 @@ describe('admin license-keys createKey action', () => {
const profile = { firstName: 'Jane' };
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(profile);
mockProfileFindFirst.mockResolvedValue(profile);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'standard' });
const req = createFormRequest({ email: user.email });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: true, error: undefined });
expect(result).toEqual({ success: true, action: 'createKey' });
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/);
expect(createCall.data.active).toBe(true);
expect(createCall.data.level).toBe('standard');
expect(createCall.data.userId).toBe('user-1');
expect(sendMail).toHaveBeenCalledOnce();
@@ -106,12 +120,12 @@ describe('admin license-keys createKey action', () => {
const user = mockUser();
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue(null);
mockProfileFindFirst.mockResolvedValue(null);
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockResolvedValue({ id: 'email-1' });
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'premium' });
const req = createFormRequest({ email: user.email });
await actions.createKey({ request: req, locals } as any);
@@ -125,19 +139,76 @@ describe('admin license-keys createKey action', () => {
const user = mockUser();
mockFindUnique.mockResolvedValue(user);
mockProfileFindUnique.mockResolvedValue({ firstName: 'Jane' });
mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
mockLicenseKeyCreate.mockResolvedValue({});
sendMail.mockRejectedValue(new Error('Resend API down'));
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const locals = createLocals(user as any);
const req = createFormRequest({ email: user.email, level: 'standard' });
const req = createFormRequest({ email: user.email });
const result = await actions.createKey({ request: req, locals } as any);
expect(result).toEqual({ success: true, error: undefined });
expect(result).toEqual({ success: true, action: 'createKey' });
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
consoleSpy.mockRestore();
});
});
describe('admin license-keys replace action', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('rejects unauthenticated requests', async () => {
const locals = createLocals();
const req = createFormRequest({ id: 'k1' });
const err = await actions.replace({ request: req, locals } as any).catch((e) => e);
expect(isHttpError(err, 401)).toBe(true);
});
it('returns error when id is missing', async () => {
const locals = createLocals(mockUser() as any);
const req = createFormRequest({});
const result = await actions.replace({ request: req, locals } as any);
expect(result).toEqual({ success: false, action: 'replace', error: 'Missing key id' });
});
it('returns error when the key does not exist', async () => {
mockLicenseKeyFindUnique.mockResolvedValue(null);
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ id: 'missing' });
const result = await actions.replace({ request: req, locals } as any);
expect(result).toEqual({ success: false, action: 'replace', error: 'License key not found' });
});
it('deletes the old key and creates a replacement preserving the owner', async () => {
mockLicenseKeyFindUnique.mockResolvedValue({
id: 'k1',
key: 'AAAA-BBBB-CCCC-DDDD',
userId: 'user-1',
email: null
});
mockLicenseKeyDelete.mockResolvedValue({});
mockLicenseKeyCreate.mockResolvedValue({});
const locals = createLocals(mockUser() as any);
const req = createFormRequest({ id: 'k1' });
const result = await actions.replace({ request: req, locals } as any);
expect(result).toEqual({ success: true, action: 'replace' });
expect(mockLicenseKeyDelete).toHaveBeenCalledWith({ where: { id: 'k1' } });
expect(mockLicenseKeyCreate).toHaveBeenCalledOnce();
const createCall = mockLicenseKeyCreate.mock.calls[0][0];
expect(createCall.data.key).toMatch(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/);
expect(createCall.data.active).toBe(true);
expect(createCall.data.userId).toBe('user-1');
expect(createCall.data.email).toBeNull();
});
});