feat: implement terminal authentication bootstrap and integrate into ScanTerminalDashboard
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
|||||||
mergeAccessStateWithAuthPayload,
|
mergeAccessStateWithAuthPayload,
|
||||||
type AuthProfilePayload,
|
type AuthProfilePayload,
|
||||||
} from "@/components/dashboard/scan-terminal/terminal-access-state";
|
} from "@/components/dashboard/scan-terminal/terminal-access-state";
|
||||||
|
import { loadTerminalAuthProfile } from "@/components/dashboard/scan-terminal/terminal-auth-bootstrap";
|
||||||
import {
|
import {
|
||||||
cityListItemsToScanRows,
|
cityListItemsToScanRows,
|
||||||
mergeScanRowsWithCityFallbackRows,
|
mergeScanRowsWithCityFallbackRows,
|
||||||
@@ -1034,12 +1035,15 @@ function ScanTerminalScreen() {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const sessionPromise = hasSupabasePublicEnv()
|
const supabaseEnabled = hasSupabasePublicEnv();
|
||||||
? getSupabaseBrowserClient().auth.getSession()
|
loadTerminalAuthProfile({
|
||||||
: Promise.resolve({ data: { session: null } });
|
getSession: () =>
|
||||||
|
supabaseEnabled
|
||||||
sessionPromise
|
? getSupabaseBrowserClient().auth.getSession()
|
||||||
.then(({ data: { session } }) => loadAuthProfile(session?.access_token))
|
: Promise.resolve({ data: { session: null } }),
|
||||||
|
hasSupabasePublicEnv: supabaseEnabled,
|
||||||
|
loadAuthProfile,
|
||||||
|
})
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
|
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
|
||||||
|
|||||||
+40
@@ -1332,6 +1332,46 @@ export function runTests() {
|
|||||||
"MGM series should not mix METAR airportCurrent points into the MGM airport-station line",
|
"MGM series should not mix METAR airportCurrent points into the MGM airport-station line",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ankaraMgmWithMetarLabel = __buildTemperatureChartDataForTest(
|
||||||
|
{
|
||||||
|
city: "ankara",
|
||||||
|
local_date: "2026-05-29",
|
||||||
|
local_time: "18:48",
|
||||||
|
tz_offset_seconds: 3 * 60 * 60,
|
||||||
|
current_temp: 14,
|
||||||
|
airport: "LTAC",
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
localTime: "18:48",
|
||||||
|
times: [],
|
||||||
|
temps: [],
|
||||||
|
airportPrimary: {
|
||||||
|
source_code: "mgm",
|
||||||
|
source_label: "METAR",
|
||||||
|
temp: 14,
|
||||||
|
obs_time: "2026-05-29T15:48:00Z",
|
||||||
|
},
|
||||||
|
airportCurrent: {
|
||||||
|
source_code: "metar",
|
||||||
|
source_label: "METAR",
|
||||||
|
temp: 17,
|
||||||
|
obs_time: "18:20",
|
||||||
|
},
|
||||||
|
airportPrimaryTodayObs: [
|
||||||
|
{ time: "2026-05-29T15:00:00Z", temp: 14 },
|
||||||
|
{ time: "2026-05-29T15:30:00Z", temp: 15 },
|
||||||
|
],
|
||||||
|
metarTodayObs: [
|
||||||
|
{ time: "2026-05-29T15:20:00Z", temp: 17 },
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
"1D",
|
||||||
|
);
|
||||||
|
const ankaraMgmSeries = seriesByKey(ankaraMgmWithMetarLabel.series as any, "madis") as any;
|
||||||
|
const ankaraMetarSeries = seriesByKey(ankaraMgmWithMetarLabel.series as any, "metar") as any;
|
||||||
|
assert(ankaraMgmSeries?.label === "MGM", "Ankara MGM airport-primary series should be labeled MGM even if payload source_label says METAR");
|
||||||
|
assert(ankaraMetarSeries?.label === "METAR", "Ankara METAR backup series should keep the METAR label");
|
||||||
|
|
||||||
const chengduMergedHourly = __mergePatchIntoHourlyForTest(
|
const chengduMergedHourly = __mergePatchIntoHourlyForTest(
|
||||||
{
|
{
|
||||||
localTime: "05:25",
|
localTime: "05:25",
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
loadTerminalAuthProfile,
|
||||||
|
type TerminalAuthProfilePayload,
|
||||||
|
} from "@/components/dashboard/scan-terminal/terminal-auth-bootstrap";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred<T>() {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (reason?: unknown) => void;
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, reject, resolve };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushMicrotasks() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runTests() {
|
||||||
|
const slowCookieProfile = deferred<TerminalAuthProfilePayload>();
|
||||||
|
const fastSession = deferred<{ data: { session: { access_token: string } } }>();
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const resultPromise = loadTerminalAuthProfile({
|
||||||
|
hasSupabasePublicEnv: true,
|
||||||
|
getSession: () => {
|
||||||
|
calls.push("getSession");
|
||||||
|
return fastSession.promise;
|
||||||
|
},
|
||||||
|
loadAuthProfile: (accessToken) => {
|
||||||
|
const token = String(accessToken || "");
|
||||||
|
calls.push(token ? `profile:${token}` : "profile:cookie");
|
||||||
|
if (!token) return slowCookieProfile.promise;
|
||||||
|
return Promise.resolve({
|
||||||
|
authenticated: true,
|
||||||
|
user_id: "bearer-user",
|
||||||
|
subscription_active: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushMicrotasks();
|
||||||
|
assert(
|
||||||
|
calls.includes("profile:cookie") && calls.includes("getSession"),
|
||||||
|
"terminal auth bootstrap should start cookie profile and Supabase session in parallel",
|
||||||
|
);
|
||||||
|
|
||||||
|
fastSession.resolve({ data: { session: { access_token: "fast-token" } } });
|
||||||
|
const result = await resultPromise;
|
||||||
|
assert(
|
||||||
|
calls.includes("profile:fast-token"),
|
||||||
|
"terminal auth bootstrap should retry auth profile with the Supabase bearer token",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
result.authenticated === true && result.user_id === "bearer-user",
|
||||||
|
"terminal auth bootstrap should not wait for a slow cookie profile when bearer auth succeeds",
|
||||||
|
);
|
||||||
|
|
||||||
|
const slowSession = deferred<{ data: { session: { access_token: string } | null } }>();
|
||||||
|
const cookieOnlyResult = await loadTerminalAuthProfile({
|
||||||
|
hasSupabasePublicEnv: true,
|
||||||
|
getSession: () => slowSession.promise,
|
||||||
|
loadAuthProfile: (accessToken) => {
|
||||||
|
assert(!accessToken, "authenticated cookie profile should finish without requiring bearer profile");
|
||||||
|
return Promise.resolve({
|
||||||
|
authenticated: true,
|
||||||
|
user_id: "cookie-user",
|
||||||
|
subscription_active: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(
|
||||||
|
cookieOnlyResult.user_id === "cookie-user",
|
||||||
|
"terminal auth bootstrap should accept an authenticated cookie profile immediately",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -627,6 +627,43 @@ function isMgmAirportPrimary(hourly: HourlyForecast) {
|
|||||||
return sourceTokens.some((value) => value === "mgm" || value.includes("turkey_mgm"));
|
return sourceTokens.some((value) => value === "mgm" || value.includes("turkey_mgm"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canonicalAirportPrimarySourceLabel(hourly: HourlyForecast) {
|
||||||
|
const primary = hourly?.airportPrimary;
|
||||||
|
const tokens = [
|
||||||
|
primary?.source_code,
|
||||||
|
(primary as any)?.source,
|
||||||
|
].map((value) => String(value || "").trim().toLowerCase());
|
||||||
|
if (tokens.some((value) => value === "mgm" || value.includes("turkey_mgm"))) return "MGM";
|
||||||
|
if (tokens.some((value) => value.includes("jma"))) return "JMA";
|
||||||
|
if (tokens.some((value) => value.includes("fmi"))) return "FMI";
|
||||||
|
if (tokens.some((value) => value.includes("knmi"))) return "KNMI";
|
||||||
|
if (tokens.some((value) => value.includes("ims"))) return "IMS";
|
||||||
|
if (tokens.some((value) => value.includes("ncm"))) return "NCM";
|
||||||
|
if (tokens.some((value) => value.includes("aeroweb"))) return "AeroWeb";
|
||||||
|
if (tokens.some((value) => value.includes("singapore_mss") || value === "mss")) return "MSS";
|
||||||
|
if (tokens.some((value) => value.includes("madis") || value.includes("noaa"))) return "NOAA MADIS";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGenericAirportPrimaryLabel(label: string) {
|
||||||
|
const normalized = label.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
!normalized ||
|
||||||
|
normalized === "metar" ||
|
||||||
|
normalized === "madis" ||
|
||||||
|
normalized === "noaa madis"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function airportPrimarySeriesLabel(hourly: HourlyForecast, isHKO: boolean) {
|
||||||
|
if (isHKO) return "HKO";
|
||||||
|
const canonicalLabel = canonicalAirportPrimarySourceLabel(hourly);
|
||||||
|
if (canonicalLabel === "MGM") return canonicalLabel;
|
||||||
|
const payloadLabel = String(hourly?.airportPrimary?.source_label || "").trim();
|
||||||
|
if (payloadLabel && !isGenericAirportPrimaryLabel(payloadLabel)) return payloadLabel;
|
||||||
|
return canonicalLabel || payloadLabel || "NOAA MADIS";
|
||||||
|
}
|
||||||
|
|
||||||
function airportPrimaryObservationPoints(hourly: HourlyForecast) {
|
function airportPrimaryObservationPoints(hourly: HourlyForecast) {
|
||||||
return appendLatestAirportObservation(
|
return appendLatestAirportObservation(
|
||||||
hourly?.airportPrimaryTodayObs,
|
hourly?.airportPrimaryTodayObs,
|
||||||
@@ -1800,7 +1837,7 @@ function buildFullDayChartData(
|
|||||||
if (madisVals.some((v) => v !== null)) {
|
if (madisVals.some((v) => v !== null)) {
|
||||||
series.push({
|
series.push({
|
||||||
key: "madis",
|
key: "madis",
|
||||||
label: isHKO ? "HKO" : (hourly?.airportPrimary?.source_label || "NOAA MADIS"),
|
label: airportPrimarySeriesLabel(hourly, isHKO),
|
||||||
source: isHKO ? "HKO" : (hourly?.airportPrimary?.station_code || row?.airport || "MADIS"),
|
source: isHKO ? "HKO" : (hourly?.airportPrimary?.station_code || row?.airport || "MADIS"),
|
||||||
color: "#0284c7",
|
color: "#0284c7",
|
||||||
dashed: isHKO ? true : false,
|
dashed: isHKO ? true : false,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type { AuthProfilePayload } from "@/components/dashboard/scan-terminal/terminal-access-state";
|
||||||
|
|
||||||
|
export type TerminalAuthProfilePayload = AuthProfilePayload;
|
||||||
|
|
||||||
|
type SupabaseSessionResult = {
|
||||||
|
data?: {
|
||||||
|
session?: {
|
||||||
|
access_token?: string | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
} | null | undefined;
|
||||||
|
|
||||||
|
type LoadTerminalAuthProfileOptions = {
|
||||||
|
getSession: () => Promise<SupabaseSessionResult>;
|
||||||
|
hasSupabasePublicEnv: boolean;
|
||||||
|
loadAuthProfile: (accessToken?: string | null) => Promise<TerminalAuthProfilePayload>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettledProfile =
|
||||||
|
| { ok: true; payload: TerminalAuthProfilePayload | null }
|
||||||
|
| { ok: false; error: unknown };
|
||||||
|
|
||||||
|
function settleProfile(
|
||||||
|
promise: Promise<TerminalAuthProfilePayload | null>,
|
||||||
|
): Promise<SettledProfile> {
|
||||||
|
return promise
|
||||||
|
.then((payload) => ({ ok: true as const, payload }))
|
||||||
|
.catch((error) => ({ ok: false as const, error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstKnownProfile(cookieResult: SettledProfile, bearerResult: SettledProfile) {
|
||||||
|
if (bearerResult.ok && bearerResult.payload?.authenticated) return bearerResult.payload;
|
||||||
|
if (cookieResult.ok && cookieResult.payload) return cookieResult.payload;
|
||||||
|
if (bearerResult.ok && bearerResult.payload) return bearerResult.payload;
|
||||||
|
if (!cookieResult.ok) throw cookieResult.error;
|
||||||
|
if (!bearerResult.ok) throw bearerResult.error;
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
subscription_active: false,
|
||||||
|
points: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTerminalAuthProfile({
|
||||||
|
getSession,
|
||||||
|
hasSupabasePublicEnv,
|
||||||
|
loadAuthProfile,
|
||||||
|
}: LoadTerminalAuthProfileOptions) {
|
||||||
|
let resolvedAuthenticated = false;
|
||||||
|
let resolveAuthenticated:
|
||||||
|
| ((payload: TerminalAuthProfilePayload) => void)
|
||||||
|
| null = null;
|
||||||
|
|
||||||
|
const authenticatedProfile = new Promise<TerminalAuthProfilePayload>((resolve) => {
|
||||||
|
resolveAuthenticated = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolveIfAuthenticated = (payload: TerminalAuthProfilePayload | null) => {
|
||||||
|
if (!payload?.authenticated || resolvedAuthenticated) return;
|
||||||
|
resolvedAuthenticated = true;
|
||||||
|
resolveAuthenticated?.(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cookieProfile = settleProfile(
|
||||||
|
loadAuthProfile(null).then((payload) => {
|
||||||
|
resolveIfAuthenticated(payload);
|
||||||
|
return payload;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const bearerProfile = settleProfile(
|
||||||
|
(async () => {
|
||||||
|
if (!hasSupabasePublicEnv) return null;
|
||||||
|
const sessionResult = await getSession();
|
||||||
|
if (resolvedAuthenticated) return null;
|
||||||
|
const accessToken = String(
|
||||||
|
sessionResult?.data?.session?.access_token || "",
|
||||||
|
).trim();
|
||||||
|
if (!accessToken) return null;
|
||||||
|
const payload = await loadAuthProfile(accessToken);
|
||||||
|
resolveIfAuthenticated(payload);
|
||||||
|
return payload;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fallbackProfile = Promise.all([cookieProfile, bearerProfile]).then(
|
||||||
|
([cookieResult, bearerResult]) => firstKnownProfile(cookieResult, bearerResult),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Promise.race([authenticatedProfile, fallbackProfile]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user