import { json, type Handle, type HandleServerError } from '@sveltejs/kit'; import { stringify } from 'devalue'; import { randomUUID } from 'node:crypto'; import { getSessionUser } from '$lib/server/auth/session'; import { getSiteAccessBlock, getSiteAccessSettings } from '$lib/server/site-access'; import { DATA_MODULES, proposalActionResult, queueDataProposal, shouldModerateDataMutation, type DataModule } from '$lib/server/data-moderation'; export const handle: Handle = async ({ event, resolve }) => { if (event.url.pathname === '/admin' || event.url.pathname.startsWith('/admin/')) { const location = `/isac${event.url.pathname.slice('/admin'.length)}${event.url.search}`; return new Response(null, { status: 308, headers: { location } }); } const siteSettings = await getSiteAccessSettings(); const sessionUser = await getSessionUser(event.cookies); const accessBlock = getSiteAccessBlock(siteSettings, event.url.pathname, Boolean(sessionUser)); if (accessBlock) { const target = new URL('/coming-soon', event.url); if (accessBlock.kind === 'section') target.searchParams.set('unavailable', accessBlock.section); if (event.request.method === 'GET' || event.request.method === 'HEAD') { return new Response(null, { status: 303, headers: { location: `${target.pathname}${target.search}` } }); } return json( { error: accessBlock.kind === 'section' ? 'This section is not available.' : 'The public site is not available yet.' }, { status: 404 } ); } if (event.request.method === 'POST') { const match = event.url.pathname.match(/^\/isac\/([^/]+)$/); const module = match?.[1] as DataModule | undefined; const action = event.url.search.startsWith('?/') ? event.url.search.slice(2) : ''; if (module && DATA_MODULES.includes(module) && action) { // Publishing calculation rules is never a contributor proposal. The // action has its own dedicated permission and confirmation contract. if (module === 'calculations' && action === 'publishRevision') return resolve(event); const user = sessionUser; const access = await shouldModerateDataMutation(user); if (!access.canManage && access.canPropose && user) { if (!access.contributorEnabled) { if (event.request.headers.get('accept')?.includes('application/json')) { return json({ type: 'failure', status: 403, data: { contributorModeRequired: true } }, { status: 403 }); } return new Response('Contributor mode must be enabled before submitting data changes.', { status: 403 }); } const submittedForm = await event.request.formData(); const proposal = await queueDataProposal(user, module, action, submittedForm); if (event.request.headers.get('accept')?.includes('application/json')) { const result = proposalActionResult(module, action, Boolean(proposal.entityId), proposal.id); if (module === 'attributes' && action === 'save') { Object.assign(result, { savedItem: { id: proposal.entityId ?? proposal.draftEntityId, name: String(submittedForm.get('name') ?? ''), type: String(submittedForm.get('type') ?? ''), category: String(submittedForm.get('category') ?? ''), calculation_type: String(submittedForm.get('calculationType') ?? ''), unit: String(submittedForm.get('unit') ?? ''), active: 1, pending: true, proposalId: proposal.id, proposerName: user.displayName } }); } return json({ type: 'success', status: 200, data: stringify(result) }); } const target = new URL(event.url.pathname, event.url); target.searchParams.set('proposal', proposal.id); return new Response(null, { status: 303, headers: { location: `${target.pathname}${target.search}` } }); } } } return resolve(event); }; export const handleError: HandleServerError = ({ error, event, status, message }) => { const errorId = randomUUID().slice(0, 8).toUpperCase(); console.error(`[${errorId}] ${event.request.method} ${event.url.pathname} -> ${status}`, error); return { message: status >= 500 ? 'An unexpected fault interrupted this operation. No further action was applied.' : message, errorId }; };