- Extract KEBAB_RE, SESSION_COOKIE, SESSION_MAX_AGE_MS, COOKIE_MAX_AGE,
authRequest(), and sessionResponse() to auth.ts — eliminating 4 copies
of the auth() wrapper and 3 copies of KEBAB_RE across admin API routes
- Remove timing side-channel: drop a.length !== b.length early-return
before timingSafeEqual in verifySession (SHA-256 HMAC always 64 chars)
- Fix YOLO localStorage write-before-read race using initialization guard
- Fix admin layout /api/ bypass to return {} instead of {authenticated:true}
- Extract extractError() helper in admin page — removes 4 inline patterns
- Parallel Promise.all in admin loadData() — halves round-trip latency
- Parallel Promise.all in moveCheckedUnits() + surface per-request errors
- Hoist Big(inputValue) above .map() in converter — one alloc per recompute
- O(n) single-pass orderedResults using Map instead of O(n*groups) filters
- Fix expandedGroups.add() mutation — use Set assignment for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import { json } from '@sveltejs/kit';
|
|
import { loadData, saveData } from '$lib/server/data';
|
|
import { authRequest } from '$lib/server/auth';
|
|
import type { RequestHandler } from './$types';
|
|
import type { Group } from '$lib/types';
|
|
|
|
export const PUT: RequestHandler = async ({ request, params }) => {
|
|
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
|
|
|
const { id } = params;
|
|
const body = await request.json().catch(() => null);
|
|
if (!body) return json({ error: 'Invalid JSON' }, { status: 400 });
|
|
|
|
const data = loadData();
|
|
const gIdx = data.groups.findIndex((g) => g.id === id);
|
|
if (gIdx === -1) return json({ error: 'Group not found' }, { status: 404 });
|
|
|
|
const patch = body as Partial<Group>;
|
|
const currentGroup = data.groups[gIdx];
|
|
|
|
// Special case: changing baseUnitId requires recalculating all unit toBase values
|
|
if (patch.baseUnitId !== undefined && patch.baseUnitId !== currentGroup.baseUnitId) {
|
|
const newBaseId = patch.baseUnitId;
|
|
const newBaseUnit = data.units.find((u) => u.id === newBaseId && u.group === id);
|
|
if (!newBaseUnit) {
|
|
return json({ error: `baseUnitId '${newBaseId}' not found in this group` }, { status: 400 });
|
|
}
|
|
|
|
const X = newBaseUnit.toBase; // Current toBase of the new base unit
|
|
// Recalculate all units in this group: unit.toBase = unit.toBase / X
|
|
// Then set new base unit toBase = 1.0
|
|
for (const unit of data.units) {
|
|
if (unit.group === id) {
|
|
unit.toBase = unit.id === newBaseId ? 1.0 : unit.toBase / X;
|
|
}
|
|
}
|
|
}
|
|
|
|
const updated: Group = { ...currentGroup, ...patch };
|
|
data.groups[gIdx] = updated;
|
|
saveData(data);
|
|
|
|
return json(updated);
|
|
};
|
|
|
|
export const DELETE: RequestHandler = async ({ request, params }) => {
|
|
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
|
|
|
const { id } = params;
|
|
const body = await request.json().catch(() => ({}));
|
|
const { action, targetGroupId, toBaseAction } = body as {
|
|
action?: 'reassign' | 'orphan';
|
|
targetGroupId?: string;
|
|
toBaseAction?: 'recalculate' | 'reset' | 'keep';
|
|
};
|
|
|
|
if (!action || (action !== 'reassign' && action !== 'orphan')) {
|
|
return json({ error: "action must be 'reassign' or 'orphan'" }, { status: 400 });
|
|
}
|
|
|
|
const data = loadData();
|
|
const gIdx = data.groups.findIndex((g) => g.id === id);
|
|
if (gIdx === -1) return json({ error: 'Group not found' }, { status: 404 });
|
|
|
|
const fromGroup = data.groups[gIdx];
|
|
const unitsInGroup = data.units.filter((u) => u.group === id);
|
|
|
|
if (action === 'reassign') {
|
|
if (!targetGroupId) {
|
|
return json({ error: 'targetGroupId required for reassign' }, { status: 400 });
|
|
}
|
|
if (targetGroupId === id) {
|
|
return json({ error: 'Cannot reassign to the same group' }, { status: 400 });
|
|
}
|
|
const toGroup = data.groups.find((g) => g.id === targetGroupId);
|
|
if (!toGroup) {
|
|
return json({ error: `Target group '${targetGroupId}' not found` }, { status: 404 });
|
|
}
|
|
|
|
for (const unit of data.units) {
|
|
if (unit.group === id) {
|
|
if (toBaseAction === 'recalculate') {
|
|
unit.toBase = (unit.toBase * fromGroup.toUniversal) / toGroup.toUniversal;
|
|
} else if (toBaseAction === 'reset') {
|
|
unit.toBase = 1.0;
|
|
}
|
|
// 'keep' or default: no change to toBase
|
|
unit.group = targetGroupId;
|
|
}
|
|
}
|
|
} else {
|
|
// orphan
|
|
for (const unit of data.units) {
|
|
if (unit.group === id) unit.group = null;
|
|
}
|
|
}
|
|
|
|
data.groups.splice(gIdx, 1);
|
|
saveData(data);
|
|
|
|
return json({ ok: true, unitsAffected: unitsInGroup.length });
|
|
};
|