- health schema: vaccinations, body_measurements, workouts, nutrition_daily, sleep (generated hours), medications, hair_progress, appointments - migration with updated_at triggers; grants to personal_app only - /trackers/health overview with add/edit/delete, owner-gated (404 otherwise), noindex + private no-store, hidden from listings for non-owners - owner-only JSON/CSV import endpoint and scripts/health-import.ts
52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
import { db } from '#lib/server/db/index.js';
|
|
import { requireOwner } from '#lib/server/owner.js';
|
|
import { parseCsv, toPayload, validatePayload, writeRows } from '#lib/server/health-import.js';
|
|
import { error, json } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
|
|
const MAX_BYTES = 2_000_000;
|
|
|
|
// Owner-only import. Same formats as scripts/health-import.ts:
|
|
// POST application/json {"vaccinations":[...], "sleep":[...]} or [...] with ?table=
|
|
// POST text/csv header row + rows, with ?table=
|
|
// ?dry_run=1 validate only
|
|
// Only JSON/CSV content types are accepted, so a cross-site form can't post here
|
|
// (browsers preflight these types, and we never send CORS headers).
|
|
export const POST: RequestHandler = async (event) => {
|
|
requireOwner(event);
|
|
const type = (event.request.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
|
|
if (type !== 'application/json' && type !== 'text/csv')
|
|
error(415, 'Use application/json or text/csv');
|
|
const text = await event.request.text();
|
|
if (text.length > MAX_BYTES) error(413, 'Body too large');
|
|
const table = event.url.searchParams.get('table');
|
|
let payload;
|
|
try {
|
|
payload =
|
|
type === 'text/csv' ? toPayload(parseCsv(text), table) : toPayload(JSON.parse(text), table);
|
|
} catch (e) {
|
|
return json(
|
|
{ ok: false, error: e instanceof Error ? e.message : 'Bad input' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
const { clean, errors } = validatePayload(payload);
|
|
if (errors.length) return json({ ok: false, errors: errors.slice(0, 100) }, { status: 400 });
|
|
const rows = Object.fromEntries(Object.entries(clean).map(([t, r]) => [t, r.length]));
|
|
if (event.url.searchParams.get('dry_run') === '1') return json({ ok: true, dryRun: true, rows });
|
|
try {
|
|
return json({ ok: true, counts: await writeRows(db, clean) });
|
|
} catch (e) {
|
|
return json(
|
|
{ ok: false, error: e instanceof Error ? e.message : 'Import failed' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
};
|
|
|
|
// Any other method: still 404 for non-owners (no 405 that would reveal the endpoint).
|
|
export const fallback: RequestHandler = (event) => {
|
|
requireOwner(event);
|
|
error(405, 'Use POST');
|
|
};
|