Add private health tracker (owner-only)
- 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
This commit is contained in:
51
src/routes/trackers/health/import/+server.ts
Normal file
51
src/routes/trackers/health/import/+server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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');
|
||||
};
|
||||
Reference in New Issue
Block a user