feat(task-35): admin People section — vets, sponsors, mentors, volunteers, users

Full CRUD for vets, sponsors, mentors, volunteer applications, and users.
Includes approve/deny flows for pending users, role management, and password reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:38:48 -05:00
co-authored by Claude Opus 4.6
parent 63f127dfca
commit 416b25fca9
26 changed files with 1788 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { users } from '$lib/server/schema';
import { error, fail } from '@sveltejs/kit';
import { hasPermission } from '$lib/roles';
import type { Role } from '$lib/roles';
import { eq, desc, count } from 'drizzle-orm';
import { logAudit } from '$lib/server/audit';
import { sendRegistrationApproved, sendRegistrationDenied } from '$lib/server/email';
export const load: PageServerLoad = async ({ locals }) => {
if (!hasPermission(locals.user!.role as Role, 'users')) error(403, 'You do not have permission to manage users');
const userList = await db
.select({
id: users.id,
name: users.name,
email: users.email,
role: users.role,
active: users.active,
createdAt: users.createdAt
})
.from(users)
.orderBy(desc(users.createdAt));
const [pendingRow] = await db.select({ total: count() }).from(users).where(eq(users.active, false));
const pendingCount = pendingRow?.total ?? 0;
return { users: userList, pendingCount };
};
export const actions: Actions = {
approve: async ({ request, locals }) => {
if (!hasPermission(locals.user!.role as Role, 'users')) error(403, 'Access denied');
const fd = await request.formData();
const userId = parseInt(fd.get('userId')?.toString() ?? '', 10);
const role = fd.get('role')?.toString() ?? 'viewer';
if (isNaN(userId)) return fail(400, { error: 'Invalid user ID' });
const validRoles = ['sysadmin', 'director', 'foster_coordinator', 'volunteer_manager', 'vet_liaison', 'content_editor', 'applications_manager', 'viewer', 'foster'];
if (!validRoles.includes(role)) return fail(400, { error: 'Invalid role' });
const [user] = await db.select({ id: users.id, name: users.name, email: users.email }).from(users).where(eq(users.id, userId)).limit(1);
if (!user) return fail(404, { error: 'User not found' });
await db.update(users).set({ active: true, role: role as typeof users.role.enumValues[number] }).where(eq(users.id, userId));
await logAudit({ userId: locals.user!.id, action: 'approve_user', entity: 'user', entityId: userId, diff: { role } });
try {
await sendRegistrationApproved(process.env.ORG_NAME ?? 'Rescue', user.email, user.name, `${process.env.SITE_URL ?? ''}/login`);
} catch { /* noop */ }
return { success: true, action: 'approve' };
},
deny: async ({ request, locals }) => {
if (!hasPermission(locals.user!.role as Role, 'users')) error(403, 'Access denied');
const fd = await request.formData();
const userId = parseInt(fd.get('userId')?.toString() ?? '', 10);
if (isNaN(userId)) return fail(400, { error: 'Invalid user ID' });
const [user] = await db.select({ id: users.id, name: users.name, email: users.email, active: users.active }).from(users).where(eq(users.id, userId)).limit(1);
if (!user) return fail(404, { error: 'User not found' });
if (user.active) return fail(400, { error: 'Cannot deny an active user. Use the edit page to deactivate.' });
await db.delete(users).where(eq(users.id, userId));
await logAudit({ userId: locals.user!.id, action: 'deny_user', entity: 'user', entityId: userId });
try {
await sendRegistrationDenied(process.env.ORG_NAME ?? 'Rescue', user.email, user.name);
} catch { /* noop */ }
return { success: true, action: 'deny' };
}
};