Files
shd.build/src/hooks.server.ts
T
JB 5a67b252aa
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 19s
Validate and deploy production / deploy (push) Successful in 1m0s
Validate and deploy staging / deploy (push) Successful in 36s
feat: add prelaunch site controls and refresh branding
2026-07-27 02:24:13 -04:00

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
};
};