mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Merge the Nightscout connect registration
This commit is contained in:
commit
e06c42d261
3 files changed
+157
-1
No files matched your search
@@ -16640,6 +16640,57 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
/api/nightscout/connect:
|
||||
post:
|
||||
tags:
|
||||
- Integrations
|
||||
summary: Connect (or update) the caller's Nightscout instance
|
||||
description: Validates the URL and token with a live single-entry probe BEFORE storing anything, so a wrong token, an
|
||||
unreachable instance or an unapproved private origin surfaces here rather than on the first cron tick. On
|
||||
success the URL and token are encrypted at rest, the `nightscout` ledger is reset to connected, and a first sync
|
||||
is enqueued best-effort. Re-sending replaces the stored values; there is no partial update. Body capped at 16
|
||||
KiB, rate-limited 10 requests / 60 s per user. Auth via cookie or Bearer.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NightscoutConnectRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: The instance answered the probe and was stored.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NightscoutConnectEnvelope"
|
||||
"400":
|
||||
description: Body is not parseable JSON.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"401": *a1
|
||||
"413":
|
||||
description: Body exceeds 16 KiB.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422":
|
||||
description: "The URL failed validation, the origin is private and the operator has not approved it (`meta.errorCode` =
|
||||
`private_origin_not_approved` or `invalid_origin`), Nightscout rejected the token, or the instance could not
|
||||
be reached. Nothing was written. The message is fixed per class and never the upstream error's own: a
|
||||
Nightscout base URL carries its API token as a query parameter."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429":
|
||||
description: More than 10 connection attempts in 60 s from this user.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
/api/nightscout/status:
|
||||
get:
|
||||
tags:
|
||||
@@ -27453,6 +27504,26 @@ components:
|
||||
- entries
|
||||
description: Merge-by-`id` update of the HealthKit metric config. Unknown ids are silently ignored; omitted fields fall
|
||||
back to the stored (or default) value for that entry. Up to 50 entries per call.
|
||||
NightscoutConnectRequest:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 2048
|
||||
token:
|
||||
default: ""
|
||||
type: string
|
||||
maxLength: 512
|
||||
allowPrivateHost:
|
||||
default: false
|
||||
type: boolean
|
||||
required:
|
||||
- url
|
||||
description: "Instance URL and optional API token. Both are trimmed before use — a trailing space or newline from a copy
|
||||
button reaches the instance verbatim and answers as an opaque DNS failure. `token` may be omitted or empty for a
|
||||
fully public instance (`AUTH_DEFAULT_ROLES=readable`). `allowPrivateHost` is accepted for older clients and
|
||||
ignored: only the server-side `NIGHTSCOUT_PRIVATE_ORIGINS` allowlist can authorise a private origin."
|
||||
WhoopCredentialsRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -62778,6 +62849,30 @@ components:
|
||||
- truncated
|
||||
- reasonCode
|
||||
additionalProperties: false
|
||||
NightscoutConnectEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
connected:
|
||||
type: boolean
|
||||
const: true
|
||||
required:
|
||||
- connected
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
NightscoutStatusEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -293,7 +293,15 @@ const UNPUBLISHED: Readonly<Record<string, Exemption>> = {
|
||||
},
|
||||
"/api/fitbit/connect": { kind: "browserHandoff", methods: ["GET"] },
|
||||
"/api/google-health/connect": { kind: "browserHandoff", methods: ["GET"] },
|
||||
"/api/nightscout/connect": { kind: "browserHandoff", methods: ["POST"] },
|
||||
// `/api/nightscout/connect` sat here as a `browserHandoff` and is published
|
||||
// now. Nightscout has no OAuth step and no authorise page: the route is an
|
||||
// authenticated JSON POST that Zod-parses a URL and a token, probes the
|
||||
// instance and encrypts the pair onto the row. It was the only POST among
|
||||
// eight `connect` entries and the only one whose siblings — status, test,
|
||||
// disconnect — were all published, which is what makes it read as an entry
|
||||
// that inherited a neighbour's reason rather than earned its own. The seven
|
||||
// GET handoffs below keep it: each of those really does end at a provider's
|
||||
// authorise page.
|
||||
"/api/oura/connect": { kind: "browserHandoff", methods: ["GET"] },
|
||||
"/api/polar/connect": { kind: "browserHandoff", methods: ["GET"] },
|
||||
"/api/strava/connect": { kind: "browserHandoff", methods: ["GET"] },
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ouraCredentialsSchema } from "@/lib/validations/oura";
|
||||
import { polarCredentialsSchema } from "@/lib/validations/polar";
|
||||
import { stravaCredentialsSchema } from "@/lib/validations/strava";
|
||||
import { whoopCredentialsSchema } from "@/lib/validations/whoop";
|
||||
import { nightscoutConnectSchema } from "@/lib/validations/nightscout";
|
||||
import { withingsCredentialsSchema } from "@/lib/validations/withings";
|
||||
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
|
||||
@@ -330,6 +331,12 @@ const integrationLedgerState = z
|
||||
"Last recorded ledger state for this provider. `connected` is also the value a user who has never synced sees — the ledger row is created on the first attempt, and its absence reads as `connected`, not as an error.",
|
||||
);
|
||||
|
||||
const nightscoutConnectRequest = nightscoutConnectSchema.meta({
|
||||
id: "NightscoutConnectRequest",
|
||||
description:
|
||||
"Instance URL and optional API token. Both are trimmed before use — a trailing space or newline from a copy button reaches the instance verbatim and answers as an opaque DNS failure. `token` may be omitted or empty for a fully public instance (`AUTH_DEFAULT_ROLES=readable`). `allowPrivateHost` is accepted for older clients and ignored: only the server-side `NIGHTSCOUT_PRIVATE_ORIGINS` allowlist can authorise a private origin.",
|
||||
});
|
||||
|
||||
const nightscoutStatusResponse = z
|
||||
.object({
|
||||
connected: z
|
||||
@@ -1101,6 +1108,52 @@ export const integrationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/nightscout/connect": {
|
||||
post: {
|
||||
tags: ["Integrations"],
|
||||
summary: "Connect (or update) the caller's Nightscout instance",
|
||||
description:
|
||||
"Validates the URL and token with a live single-entry probe BEFORE storing anything, so a wrong token, an unreachable instance or an unapproved private origin surfaces here rather than on the first cron tick. On success the URL and token are encrypted at rest, the `nightscout` ledger is reset to connected, and a first sync is enqueued best-effort. Re-sending replaces the stored values; there is no partial update. Body capped at 16 KiB, rate-limited 10 requests / 60 s per user. Auth via cookie or Bearer.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: nightscoutConnectRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "The instance answered the probe and was stored.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
z.object({ connected: z.literal(true) }),
|
||||
"NightscoutConnectEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
"400": {
|
||||
description: "Body is not parseable JSON.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
"413": {
|
||||
description: "Body exceeds 16 KiB.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
...stdResponses,
|
||||
"422": {
|
||||
description:
|
||||
"The URL failed validation, the origin is private and the operator has not approved it (`meta.errorCode` = `private_origin_not_approved` or `invalid_origin`), Nightscout rejected the token, or the instance could not be reached. Nothing was written. The message is fixed per class and never the upstream error's own: a Nightscout base URL carries its API token as a query parameter.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
"429": {
|
||||
description:
|
||||
"More than 10 connection attempts in 60 s from this user.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/nightscout/status": {
|
||||
get: {
|
||||
tags: ["Integrations"],
|
||||
|
||||
Reference in new issue
Block a user