mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Adds seven read/write endpoints that aggregate or adapt existing data
into shapes more practical for non-browser consumers. None of the
pre-existing endpoints are touched.
• `GET /api/dashboard/summary` — greeting + streak + today compliance
+ per-metric latest value with 7-day sparkline. Single round-trip
for a "home" view instead of fanning out across 6 endpoints.
• `GET /api/measurements/series?kind=&days=` — pre-aggregated series +
stats (mean, min, max, stdDev, count) reusing
`src/lib/analytics/trends.ts`.
• `GET/PATCH /api/integrations/healthkit` — opt-in per-metric sync
config (entries[] of `{ id, kind, direction, enabled }`). Stored in
the new `users.healthkit_config_json` column.
• `POST /api/devices` — APNs/FCM token registration only; no push
sender wired up here. Upsert on `token`.
• `GET /api/medications/intake?scope=today|compliance` — top-level
aggregator over all of a user's medications, plus a sibling
`POST /api/medications/intake` accepting `{ intakeId, status, takenAt }`
for clients that don't want to maintain medication-id state.
• `GET /api/insights/cards` — adapter that maps the rule-based alerts
from `/api/insights/comprehensive` into card-shaped `Insight[]`.
• `GET /api/insights/correlations` — placeholder array today; flagged
in audit log as `insights.correlations.empty` until a real engine
lands.
All endpoints honour `requireAuth()` (cookie or bearer) and the existing
audit/Wide-Event pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
/**
|
|
* POST /api/devices
|
|
*
|
|
* APNs device registration. Native clients call this on login and on
|
|
* APNs token rotation. We upsert by `token`; sending the same token on a
|
|
* different account simply transfers ownership.
|
|
*
|
|
* No APNs send-side logic lives here — that ships in Phase 8.
|
|
*/
|
|
import { NextRequest } from "next/server";
|
|
import { z } from "zod/v4";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { apiError, apiSuccess, safeJson } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { prisma } from "@/lib/db";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
|
|
const deviceSchema = z.object({
|
|
token: z
|
|
.string()
|
|
.min(8)
|
|
.max(512)
|
|
.regex(/^[A-Za-z0-9+/=._:-]+$/, "Invalid token format"),
|
|
bundleId: z.string().min(1).max(128),
|
|
locale: z.string().min(2).max(16).optional(),
|
|
appVersion: z.string().min(1).max(32).optional(),
|
|
model: z.string().min(1).max(64).optional(),
|
|
});
|
|
|
|
export const POST = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
|
|
const { data: body, error } = await safeJson(request);
|
|
if (error) return error;
|
|
|
|
const parsed = deviceSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
return apiError(parsed.error.issues[0].message, 422);
|
|
}
|
|
|
|
const { token, bundleId, locale, appVersion, model } = parsed.data;
|
|
|
|
const existing = await prisma.device.findUnique({ where: { token } });
|
|
let id: string;
|
|
if (existing) {
|
|
const updated = await prisma.device.update({
|
|
where: { token },
|
|
data: {
|
|
userId: user.id,
|
|
platform: "ios",
|
|
bundleId,
|
|
locale: locale ?? null,
|
|
appVersion: appVersion ?? null,
|
|
model: model ?? null,
|
|
lastSeen: new Date(),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
id = updated.id;
|
|
} else {
|
|
const created = await prisma.device.create({
|
|
data: {
|
|
userId: user.id,
|
|
platform: "ios",
|
|
token,
|
|
bundleId,
|
|
locale: locale ?? null,
|
|
appVersion: appVersion ?? null,
|
|
model: model ?? null,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
id = created.id;
|
|
}
|
|
|
|
await auditLog("devices.register", {
|
|
userId: user.id,
|
|
details: { deviceId: id, bundleId, model: model ?? null },
|
|
});
|
|
|
|
annotate({
|
|
action: { name: "devices.register", entity_type: "device", entity_id: id },
|
|
});
|
|
|
|
return apiSuccess({ id }, 201);
|
|
});
|