mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(integrations): treat an empty redirect URI variable as unset
The compose file passes every whitelisted variable through, so an unset redirect URI reaches the process as an empty string. Withings then sent redirect_uri= to the provider, and Fitbit and Google Health refused to start the connection as not configured although NEXT_PUBLIC_APP_URL was set. Every integration now treats an empty or blank value as unset and derives the callback from the app URL.
This commit is contained in:
1 parent
c88754a0b5
commit
c366d4946c
8 files changed
+100
-20
No files matched your search
@@ -77,8 +77,11 @@ export interface FitbitCredentials {
|
||||
* - when derived from `NEXT_PUBLIC_APP_URL`, must stay same-origin with it.
|
||||
*/
|
||||
export function getFitbitRedirectUri(): string {
|
||||
const explicit = process.env.FITBIT_REDIRECT_URI;
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises an unset var as an empty string, which must fall through to
|
||||
// the derived URI rather than read as "not configured".
|
||||
const explicit = process.env.FITBIT_REDIRECT_URI?.trim() || undefined;
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL?.trim() || undefined;
|
||||
const raw =
|
||||
explicit ?? (appUrl ? `${appUrl}/api/fitbit/callback` : undefined);
|
||||
|
||||
|
||||
@@ -147,8 +147,11 @@ export interface GoogleHealthCredentials {
|
||||
* - when derived from `NEXT_PUBLIC_APP_URL`, must stay same-origin with it.
|
||||
*/
|
||||
export function getGoogleHealthRedirectUri(): string {
|
||||
const explicit = process.env.GOOGLE_HEALTH_REDIRECT_URI;
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises an unset var as an empty string, which must fall through to
|
||||
// the derived URI rather than read as "not configured".
|
||||
const explicit = process.env.GOOGLE_HEALTH_REDIRECT_URI?.trim() || undefined;
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL?.trim() || undefined;
|
||||
const raw =
|
||||
explicit ?? (appUrl ? `${appUrl}/api/google-health/callback` : undefined);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* The compose file passes every whitelisted variable through, so an unset
|
||||
* redirect URI reaches the process as an empty string. Every integration
|
||||
* treats an empty or blank value as unset and derives the callback from
|
||||
* `NEXT_PUBLIC_APP_URL`: Withings used to send `redirect_uri=` and Fitbit and
|
||||
* Google Health refused to start the handshake as "not configured".
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getWithingsRedirectUri } from "@/lib/withings/client";
|
||||
import { getFitbitRedirectUri } from "@/lib/fitbit/client";
|
||||
import { getGoogleHealthRedirectUri } from "@/lib/google-health/client";
|
||||
import { getPolarRedirectUri } from "@/lib/polar/client";
|
||||
import { getWhoopRedirectUri } from "@/lib/whoop/client";
|
||||
import { getStravaRedirectUri } from "@/lib/strava/client";
|
||||
import { getOuraRedirectUri } from "@/lib/oura/client";
|
||||
|
||||
const APP = "https://health.example.test";
|
||||
|
||||
const CASES: Array<[string, string, () => string, string]> = [
|
||||
["Withings", "WITHINGS_REDIRECT_URI", getWithingsRedirectUri, "withings"],
|
||||
["Fitbit", "FITBIT_REDIRECT_URI", getFitbitRedirectUri, "fitbit"],
|
||||
[
|
||||
"Google Health",
|
||||
"GOOGLE_HEALTH_REDIRECT_URI",
|
||||
getGoogleHealthRedirectUri,
|
||||
"google-health",
|
||||
],
|
||||
["Polar", "POLAR_REDIRECT_URI", getPolarRedirectUri, "polar"],
|
||||
["WHOOP", "WHOOP_REDIRECT_URI", getWhoopRedirectUri, "whoop"],
|
||||
["Strava", "STRAVA_REDIRECT_URI", getStravaRedirectUri, "strava"],
|
||||
["Oura", "OURA_REDIRECT_URI", getOuraRedirectUri, "oura"],
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe.each(CASES)("%s redirect URI", (_name, variable, resolve, path) => {
|
||||
it.each(["", " "])(
|
||||
"derives the callback when the variable is %j",
|
||||
(value) => {
|
||||
vi.stubEnv(variable, value);
|
||||
vi.stubEnv("NEXT_PUBLIC_APP_URL", APP);
|
||||
expect(resolve()).toBe(`${APP}/api/${path}/callback`);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses an explicit value, trimmed", () => {
|
||||
const explicit = `${APP}/api/${path}/callback`;
|
||||
vi.stubEnv(variable, ` ${explicit} `);
|
||||
vi.stubEnv("NEXT_PUBLIC_APP_URL", APP);
|
||||
expect(resolve()).toBe(explicit);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with neither variable set", () => {
|
||||
it.each([
|
||||
["Fitbit", "FITBIT_REDIRECT_URI", getFitbitRedirectUri],
|
||||
["Google Health", "GOOGLE_HEALTH_REDIRECT_URI", getGoogleHealthRedirectUri],
|
||||
] as const)("%s still says it is not configured", (_n, variable, resolve) => {
|
||||
vi.stubEnv(variable, "");
|
||||
vi.stubEnv("NEXT_PUBLIC_APP_URL", " ");
|
||||
expect(() => resolve()).toThrow(/not configured/);
|
||||
});
|
||||
});
|
||||
@@ -42,9 +42,11 @@ export function getOuraCredentials(): OuraCredentials | null {
|
||||
}
|
||||
|
||||
export function getOuraRedirectUri(): string {
|
||||
// An empty or blank value counts as unset (the compose whitelist
|
||||
// materialises an unset var as an empty string).
|
||||
return (
|
||||
process.env.OURA_REDIRECT_URI ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/api/oura/callback`
|
||||
process.env.OURA_REDIRECT_URI?.trim() ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL?.trim()}/api/oura/callback`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,12 @@ export function getPolarCredentials(): PolarCredentials | null {
|
||||
}
|
||||
|
||||
export function getPolarRedirectUri(): string {
|
||||
// `||`, not `??`: the compose whitelist materialises an unset var as an empty
|
||||
// string, which must still fall through to the derived URI.
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises an unset var as an empty string, which must still fall
|
||||
// through to the derived URI.
|
||||
return (
|
||||
process.env.POLAR_REDIRECT_URI ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/api/polar/callback`
|
||||
process.env.POLAR_REDIRECT_URI?.trim() ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL?.trim()}/api/polar/callback`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,12 @@ export function getStravaCredentials(): StravaCredentials | null {
|
||||
}
|
||||
|
||||
export function getStravaRedirectUri(): string {
|
||||
// `||`, not `??`: the compose whitelist materialises an unset var as an empty
|
||||
// string, which must still fall through to the derived URI.
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises an unset var as an empty string, which must still fall
|
||||
// through to the derived URI.
|
||||
return (
|
||||
process.env.STRAVA_REDIRECT_URI ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/api/strava/callback`
|
||||
process.env.STRAVA_REDIRECT_URI?.trim() ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL?.trim()}/api/strava/callback`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,12 @@ export interface WhoopCredentials {
|
||||
}
|
||||
|
||||
export function getWhoopRedirectUri(): string {
|
||||
// `||`, not `??`: the compose whitelist materialises the var as an empty
|
||||
// string when unset, which must still fall through to the derived URI.
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises the var as an empty string when unset, which must still fall
|
||||
// through to the derived URI.
|
||||
return (
|
||||
process.env.WHOOP_REDIRECT_URI ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/api/whoop/callback`
|
||||
process.env.WHOOP_REDIRECT_URI?.trim() ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL?.trim()}/api/whoop/callback`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,12 @@ export interface WithingsCredentials {
|
||||
}
|
||||
|
||||
export function getWithingsRedirectUri(): string {
|
||||
// An empty or blank value counts as unset: the compose whitelist
|
||||
// materialises an unset var as an empty string, which must fall through to
|
||||
// the derived URI rather than go out as `redirect_uri=`.
|
||||
return (
|
||||
process.env.WITHINGS_REDIRECT_URI ??
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/api/withings/callback`
|
||||
process.env.WITHINGS_REDIRECT_URI?.trim() ||
|
||||
`${process.env.NEXT_PUBLIC_APP_URL?.trim()}/api/withings/callback`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user