91 lines
4.1 KiB
TypeScript
91 lines
4.1 KiB
TypeScript
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
|
|
};
|
|
};
|