From aa67719a5c7278946e19a5933fdfa84f6049f745 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 8 Apr 2026 09:12:00 -0600 Subject: [PATCH] feat: bring your own token --- src/components/collections/href.tsx | 2 +- src/components/ui/settings.tsx | 97 ++++++++++++++++++++++++++++- src/store/index.ts | 4 ++ src/store/tokens.ts | 22 +++++++ src/utils/auth.ts | 31 ++++++--- src/utils/stac.ts | 2 +- 6 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 src/store/tokens.ts diff --git a/src/components/collections/href.tsx b/src/components/collections/href.tsx index 2f5ee72..762418c 100644 --- a/src/components/collections/href.tsx +++ b/src/components/collections/href.tsx @@ -42,7 +42,7 @@ export default function CollectionsHref({ href }: { href: string }) { queryFn: async ({ pageParam }) => { if (pageParam) { const headers: Record = {}; - const token = getAccessToken(); + const token = getAccessToken(pageParam); if (token) headers["Authorization"] = `Bearer ${token}`; return await fetch(pageParam, { headers }).then((response) => { if (response.ok) return response.json(); diff --git a/src/components/ui/settings.tsx b/src/components/ui/settings.tsx index 9c2fb93..397ee29 100644 --- a/src/components/ui/settings.tsx +++ b/src/components/ui/settings.tsx @@ -1,16 +1,22 @@ import type { IconButtonProps } from "@chakra-ui/react"; import { + Box, CloseButton, Dialog, Field, + Flex, + HStack, IconButton, + Input, Portal, + Separator, Switch, Text, } from "@chakra-ui/react"; import * as React from "react"; -import { LuSettings } from "react-icons/lu"; +import { LuPlus, LuSettings, LuTrash2 } from "react-icons/lu"; import { useStore } from "../../store"; +import { authConfig } from "./auth"; interface SettingsButtonProps extends Omit {} @@ -79,6 +85,12 @@ export const SettingsButton = React.forwardRef< + {!authConfig && ( + <> + + + + )} @@ -89,3 +101,86 @@ export const SettingsButton = React.forwardRef< ); }); + +function TokensSection() { + const tokens = useStore((store) => store.tokens); + const setToken = useStore((store) => store.setToken); + const removeToken = useStore((store) => store.removeToken); + const href = useStore((store) => store.href); + + const defaultBaseUri = React.useMemo(() => { + if (!href) return ""; + try { + return new URL(href).origin; + } catch { + return ""; + } + }, [href]); + + const [baseUri, setBaseUri] = React.useState(""); + const [tokenValue, setTokenValue] = React.useState(""); + + const handleAdd = () => { + const uri = baseUri.trim() || defaultBaseUri; + const val = tokenValue.trim(); + if (!uri || !val) return; + setToken(uri, val); + setBaseUri(""); + setTokenValue(""); + }; + + const entries = Object.entries(tokens); + + return ( + + + Access tokens + + + Provide Bearer tokens for authenticated STAC APIs. + + {entries.length > 0 && ( + + {entries.map(([uri]) => ( + + + {uri} + + removeToken(uri)} + > + + + + ))} + + )} + + setBaseUri(e.target.value)} + /> + setTokenValue(e.target.value)} + /> + + + + + + ); +} diff --git a/src/store/index.ts b/src/store/index.ts index 19b5bd2..4c8425b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -16,6 +16,7 @@ import { createStacGeoparquetState, type StacGeoparquetState, } from "./stac-geoparquet"; +import { createTokensSlice, type TokensState } from "./tokens"; import { createUploadedFileSlice, type UploadedFileState, @@ -40,6 +41,7 @@ export interface State MapState, SettingsState, StacGeoparquetState, + TokensState, WebMapLinksState { fillColor: [number, number, number, number]; lineColor: [number, number, number, number]; @@ -64,6 +66,7 @@ export const useStore = create()( ...createDatetimeSlice(...a), ...createMapSlice(...a), ...createSettingsSlice(...a), + ...createTokensSlice(...a), ...createWebMapLinksSlice(...a), fillColor: [207, 63, 2, 50] as [number, number, number, number], lineColor: [207, 63, 2, 100] as [number, number, number, number], @@ -74,6 +77,7 @@ export const useStore = create()( partialize: (state) => ({ restrictToThreeBandCogs: state.restrictToThreeBandCogs, hivePartitioning: state.hivePartitioning, + tokens: state.tokens, }), } ) diff --git a/src/store/tokens.ts b/src/store/tokens.ts new file mode 100644 index 0000000..fdf824d --- /dev/null +++ b/src/store/tokens.ts @@ -0,0 +1,22 @@ +import type { StateCreator } from "zustand"; +import type { State } from "."; + +export interface TokensState { + tokens: Record; + setToken: (baseUri: string, token: string) => void; + removeToken: (baseUri: string) => void; +} + +export const createTokensSlice: StateCreator = ( + set, + get +) => ({ + tokens: {}, + setToken: (baseUri, token) => + set({ tokens: { ...get().tokens, [baseUri]: token } }), + removeToken: (baseUri) => { + const tokens = { ...get().tokens }; + delete tokens[baseUri]; + set({ tokens }); + }, +}); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 28f2193..e8a69ff 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,16 +1,31 @@ import { User } from "oidc-client-ts"; +import { useStore } from "../store"; -export function getAccessToken(): string | null { +export function getAccessToken(href?: string | URL): string | null { const authority = import.meta.env.VITE_AUTH_AUTHORITY; const clientId = import.meta.env.VITE_AUTH_CLIENT_ID; - if (!authority || !clientId) return null; - const oidcStorage = sessionStorage.getItem( - `oidc.user:${authority}:${clientId}` - ); - if (oidcStorage) { - const user = User.fromStorageString(oidcStorage); - return user?.access_token ?? null; + if (authority && clientId) { + const oidcStorage = sessionStorage.getItem( + `oidc.user:${authority}:${clientId}` + ); + if (oidcStorage) { + const user = User.fromStorageString(oidcStorage); + return user?.access_token ?? null; + } + return null; } + + if (href) { + try { + const url = new URL(href.toString()); + const baseUri = url.origin; + const tokens = useStore.getState().tokens; + return tokens[baseUri] ?? null; + } catch { + return null; + } + } + return null; } diff --git a/src/utils/stac.ts b/src/utils/stac.ts index 69c3b99..407b201 100644 --- a/src/utils/stac.ts +++ b/src/utils/stac.ts @@ -83,7 +83,7 @@ export async function fetchStac({ body?: string; }): Promise { const headers: Record = { Accept: "application/json" }; - const token = getAccessToken(); + const token = getAccessToken(href); if (token) headers["Authorization"] = `Bearer ${token}`; return await fetch(href, {