Skip to content

Commit 51ab4b7

Browse files
authored
feat: bring your own token (#386)
Add UI to set a token for a given domain.
1 parent cefeb16 commit 51ab4b7

6 files changed

Lines changed: 147 additions & 11 deletions

File tree

src/components/collections/href.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function CollectionsHref({ href }: { href: string }) {
4242
queryFn: async ({ pageParam }) => {
4343
if (pageParam) {
4444
const headers: Record<string, string> = {};
45-
const token = getAccessToken();
45+
const token = getAccessToken(pageParam);
4646
if (token) headers["Authorization"] = `Bearer ${token}`;
4747
return await fetch(pageParam, { headers }).then((response) => {
4848
if (response.ok) return response.json();

src/components/ui/settings.tsx

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import type { IconButtonProps } from "@chakra-ui/react";
22
import {
3+
Box,
34
CloseButton,
45
Dialog,
56
Field,
7+
Flex,
8+
HStack,
69
IconButton,
10+
Input,
711
Portal,
12+
Separator,
813
Switch,
914
Text,
1015
} from "@chakra-ui/react";
1116
import * as React from "react";
12-
import { LuSettings } from "react-icons/lu";
17+
import { LuPlus, LuSettings, LuTrash2 } from "react-icons/lu";
1318
import { useStore } from "../../store";
19+
import { authConfig } from "./auth";
1420

1521
interface SettingsButtonProps extends Omit<IconButtonProps, "aria-label"> {}
1622

@@ -79,6 +85,12 @@ export const SettingsButton = React.forwardRef<
7985
</Text>
8086
</Field.HelperText>
8187
</Field.Root>
88+
{!authConfig && (
89+
<>
90+
<Separator my={4} />
91+
<TokensSection />
92+
</>
93+
)}
8294
</Dialog.Body>
8395
<Dialog.CloseTrigger asChild>
8496
<CloseButton size="sm" />
@@ -89,3 +101,86 @@ export const SettingsButton = React.forwardRef<
89101
</Dialog.Root>
90102
);
91103
});
104+
105+
function TokensSection() {
106+
const tokens = useStore((store) => store.tokens);
107+
const setToken = useStore((store) => store.setToken);
108+
const removeToken = useStore((store) => store.removeToken);
109+
const href = useStore((store) => store.href);
110+
111+
const defaultBaseUri = React.useMemo(() => {
112+
if (!href) return "";
113+
try {
114+
return new URL(href).origin;
115+
} catch {
116+
return "";
117+
}
118+
}, [href]);
119+
120+
const [baseUri, setBaseUri] = React.useState("");
121+
const [tokenValue, setTokenValue] = React.useState("");
122+
123+
const handleAdd = () => {
124+
const uri = baseUri.trim() || defaultBaseUri;
125+
const val = tokenValue.trim();
126+
if (!uri || !val) return;
127+
setToken(uri, val);
128+
setBaseUri("");
129+
setTokenValue("");
130+
};
131+
132+
const entries = Object.entries(tokens);
133+
134+
return (
135+
<Box>
136+
<Text fontWeight="medium" mb={2}>
137+
Access tokens
138+
</Text>
139+
<Text fontSize="sm" color="fg.muted" mb={3}>
140+
Provide Bearer tokens for authenticated STAC APIs.
141+
</Text>
142+
{entries.length > 0 && (
143+
<Box mb={3}>
144+
{entries.map(([uri]) => (
145+
<Flex key={uri} align="center" justify="space-between" py={1}>
146+
<Text fontSize="sm" truncate>
147+
{uri}
148+
</Text>
149+
<IconButton
150+
aria-label={`Remove token for ${uri}`}
151+
size="xs"
152+
variant="ghost"
153+
onClick={() => removeToken(uri)}
154+
>
155+
<LuTrash2 />
156+
</IconButton>
157+
</Flex>
158+
))}
159+
</Box>
160+
)}
161+
<HStack>
162+
<Input
163+
placeholder={defaultBaseUri || "https://api.example.com"}
164+
size="sm"
165+
value={baseUri}
166+
onChange={(e) => setBaseUri(e.target.value)}
167+
/>
168+
<Input
169+
placeholder="Token"
170+
size="sm"
171+
type="password"
172+
value={tokenValue}
173+
onChange={(e) => setTokenValue(e.target.value)}
174+
/>
175+
<IconButton
176+
aria-label="Add token"
177+
size="sm"
178+
variant="outline"
179+
onClick={handleAdd}
180+
>
181+
<LuPlus />
182+
</IconButton>
183+
</HStack>
184+
</Box>
185+
);
186+
}

src/store/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
createStacGeoparquetState,
1717
type StacGeoparquetState,
1818
} from "./stac-geoparquet";
19+
import { createTokensSlice, type TokensState } from "./tokens";
1920
import {
2021
createUploadedFileSlice,
2122
type UploadedFileState,
@@ -40,6 +41,7 @@ export interface State
4041
MapState,
4142
SettingsState,
4243
StacGeoparquetState,
44+
TokensState,
4345
WebMapLinksState {
4446
fillColor: [number, number, number, number];
4547
lineColor: [number, number, number, number];
@@ -64,6 +66,7 @@ export const useStore = create<State>()(
6466
...createDatetimeSlice(...a),
6567
...createMapSlice(...a),
6668
...createSettingsSlice(...a),
69+
...createTokensSlice(...a),
6770
...createWebMapLinksSlice(...a),
6871
fillColor: [207, 63, 2, 50] as [number, number, number, number],
6972
lineColor: [207, 63, 2, 100] as [number, number, number, number],
@@ -74,6 +77,7 @@ export const useStore = create<State>()(
7477
partialize: (state) => ({
7578
restrictToThreeBandCogs: state.restrictToThreeBandCogs,
7679
hivePartitioning: state.hivePartitioning,
80+
tokens: state.tokens,
7781
}),
7882
}
7983
)

src/store/tokens.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { StateCreator } from "zustand";
2+
import type { State } from ".";
3+
4+
export interface TokensState {
5+
tokens: Record<string, string>;
6+
setToken: (baseUri: string, token: string) => void;
7+
removeToken: (baseUri: string) => void;
8+
}
9+
10+
export const createTokensSlice: StateCreator<State, [], [], TokensState> = (
11+
set,
12+
get
13+
) => ({
14+
tokens: {},
15+
setToken: (baseUri, token) =>
16+
set({ tokens: { ...get().tokens, [baseUri]: token } }),
17+
removeToken: (baseUri) => {
18+
const tokens = { ...get().tokens };
19+
delete tokens[baseUri];
20+
set({ tokens });
21+
},
22+
});

src/utils/auth.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import { User } from "oidc-client-ts";
2+
import { useStore } from "../store";
23

3-
export function getAccessToken(): string | null {
4+
export function getAccessToken(href?: string | URL): string | null {
45
const authority = import.meta.env.VITE_AUTH_AUTHORITY;
56
const clientId = import.meta.env.VITE_AUTH_CLIENT_ID;
6-
if (!authority || !clientId) return null;
77

8-
const oidcStorage = sessionStorage.getItem(
9-
`oidc.user:${authority}:${clientId}`
10-
);
11-
if (oidcStorage) {
12-
const user = User.fromStorageString(oidcStorage);
13-
return user?.access_token ?? null;
8+
if (authority && clientId) {
9+
const oidcStorage = sessionStorage.getItem(
10+
`oidc.user:${authority}:${clientId}`
11+
);
12+
if (oidcStorage) {
13+
const user = User.fromStorageString(oidcStorage);
14+
return user?.access_token ?? null;
15+
}
16+
return null;
1417
}
18+
19+
if (href) {
20+
try {
21+
const url = new URL(href.toString());
22+
const baseUri = url.origin;
23+
const tokens = useStore.getState().tokens;
24+
return tokens[baseUri] ?? null;
25+
} catch {
26+
return null;
27+
}
28+
}
29+
1530
return null;
1631
}

src/utils/stac.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export async function fetchStac({
8383
body?: string;
8484
}): Promise<StacValue> {
8585
const headers: Record<string, string> = { Accept: "application/json" };
86-
const token = getAccessToken();
86+
const token = getAccessToken(href);
8787
if (token) headers["Authorization"] = `Bearer ${token}`;
8888

8989
return await fetch(href, {

0 commit comments

Comments
 (0)