History

Refactor secret storage to use providers

Rhys Sullivan committed 8 months ago

Commit 22e370c1 parent 6e204f010 files · +1066 −263Browse files

Showing the changes to apps (3 of 10 files). Show all

Unified · Split

3 files
  1. apps/web/src/views/add-source.tsx +58 −5
  2. apps/web/src/views/secrets.tsx +68 −7
  3. apps/web/src/views/source-editor.tsx +50 −7
apps/web/src/views/add-source.tsx+58−5
@@ -1,10 +1,11 @@
1−import { useState, type ReactNode } from "react";
1+import { useEffect, useState, type ReactNode } from "react";
22 import { Link, useNavigate } from "@tanstack/react-router";
33 import {
44 type CompleteSourceOAuthResult,
55 type ConnectSourcePayload,
66 type ConnectSourceResult,
77 type DiscoverSourcePayload,
8+ type InstanceConfig,
89 type Loadable,
910 type SecretListItem,
1011 type Source,
@@ -12,6 +13,7 @@ import { · 1 unmodified line
1213 useConnectSource,
1314 useCreateSecret,
1415 useDiscoverSource,
16+ useInstanceConfig,
1517 useRefreshSecrets,
1618 useSecrets,
1719 useStartSourceOAuth,
@@ -590,6 +592,7 @@ export function AddSourcePage() { · 572 unmodified lines
590592 const navigate = useNavigate();
591593 const discoverSource = useDiscoverSource();
592594 const connectSource = useConnectSource();
595+ const instanceConfig = useInstanceConfig();
593596 const secrets = useSecrets();
594597 const refreshSecrets = useRefreshSecrets();
595598 const startSourceOAuth = useStartSourceOAuth();
@@ -1209,6 +1212,7 @@ export function AddSourcePage() { · 613 unmodified lines
12091212 {connectForm.authKind === "bearer" && (
12101213 <Field label="Token" className="sm:col-span-2">
12111214 <SecretOrTokenInput
1215+ instanceConfig={instanceConfig}
12121216 secrets={secrets}
12131217 providerId={connectForm.bearerProviderId}
12141218 handle={connectForm.bearerHandle}
@@ -1408,6 +1412,7 @@ const CREATE_NEW_VALUE = "__create_new__"; · 193 unmodified lines
14081412 const INLINE_TOKEN_VALUE = "__inline_token__";
14091413
14101414 function SecretOrTokenInput(props: {
1415+ instanceConfig: Loadable<InstanceConfig>;
14111416 secrets: Loadable<ReadonlyArray<SecretListItem>>;
14121417 providerId: string;
14131418 handle: string;
@@ -1415,13 +1420,35 @@ function SecretOrTokenInput(props: { · 1 unmodified line
14151420 onSelectSecret: (providerId: string, handle: string) => void;
14161421 onChangeToken: (token: string) => void;
14171422 }) {
1418− const { secrets, providerId, handle, inlineToken, onSelectSecret, onChangeToken } = props;
1423+ const {
1424+ instanceConfig,
1425+ secrets,
1426+ providerId,
1427+ handle,
1428+ inlineToken,
1429+ onSelectSecret,
1430+ onChangeToken,
1431+ } = props;
14191432 const createSecret = useCreateSecret();
14201433 const [showCreate, setShowCreate] = useState(false);
14211434 const [useInline, setUseInline] = useState(false);
14221435 const [newName, setNewName] = useState("");
14231436 const [newValue, setNewValue] = useState("");
1437+ const [newProviderId, setNewProviderId] = useState("");
14241438 const [createError, setCreateError] = useState<string | null>(null);
1439+ const storableProviders =
1440+ instanceConfig.status === "ready"
1441+ ? instanceConfig.data.secretProviders.filter((provider) => provider.canStore)
1442+ : [];
1443+
1444+ useEffect(() => {
1445+ if (newProviderId.length > 0) {
1446+ return;
1447+ }
1448+ if (instanceConfig.status === "ready") {
1449+ setNewProviderId(instanceConfig.data.defaultSecretStoreProvider);
1450+ }
1451+ }, [instanceConfig, newProviderId]);
14251452
14261453 const selectedValue = showCreate ? CREATE_NEW_VALUE : useInline ? INLINE_TOKEN_VALUE : (handle || "");
14271454
@@ -1431,6 +1458,11 @@ function SecretOrTokenInput(props: { · 3 unmodified lines
14311458 setUseInline(false);
14321459 setNewName("");
14331460 setNewValue("");
1461+ setNewProviderId(
1462+ instanceConfig.status === "ready"
1463+ ? instanceConfig.data.defaultSecretStoreProvider
1464+ : "",
1465+ );
14341466 setCreateError(null);
14351467 return;
14361468 }
@@ -1448,7 +1480,10 @@ function SecretOrTokenInput(props: { · 11 unmodified lines
14481480 }
14491481 setUseInline(false);
14501482 setShowCreate(false);
1451− onSelectSecret("postgres", value);
1483+ const matchedSecret = secrets.status === "ready"
1484+ ? secrets.data.find((secret) => secret.id === value)
1485+ : null;
1486+ onSelectSecret(matchedSecret?.providerId ?? "local", value);
14521487 };
14531488
14541489 const handleCreate = async () => {
@@ -1464,7 +1499,11 @@ function SecretOrTokenInput(props: { · 9 unmodified lines
14641499 }
14651500
14661501 try {
1467− const result = await createSecret.mutateAsync({ name: trimmedName, value: newValue });
1502+ const result = await createSecret.mutateAsync({
1503+ name: trimmedName,
1504+ value: newValue,
1505+ ...(newProviderId ? { providerId: newProviderId } : {}),
1506+ });
14681507 onSelectSecret(result.providerId, result.id);
14691508 setShowCreate(false);
14701509 } catch (err) {
@@ -1495,7 +1534,7 @@ function SecretOrTokenInput(props: { · 24 unmodified lines
14951534 <option value="">Select a secret...</option>
14961535 {items.map((secret) => (
14971536 <option key={secret.id} value={secret.id}>
1498− {secret.name || secret.id}
1537+ {secret.name || secret.id} ({secret.providerId})
14991538 </option>
15001539 ))}
15011540 <option value={INLINE_TOKEN_VALUE}>Paste token directly</option>
@@ -1541,6 +1580,20 @@ function SecretOrTokenInput(props: { · 39 unmodified lines
15411580 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
15421581 />
15431582 </label>
1583+ <label className="block space-y-1">
1584+ <span className="text-[11px] font-medium text-muted-foreground">Store in</span>
1585+ <select
1586+ value={newProviderId}
1587+ onChange={(e) => setNewProviderId(e.target.value)}
1588+ className="h-8 w-full rounded-lg border border-input bg-background px-3 text-[12px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
1589+ >
1590+ {storableProviders.map((provider) => (
1591+ <option key={provider.id} value={provider.id}>
1592+ {provider.name}
1593+ </option>
1594+ ))}
1595+ </select>
1596+ </label>
15441597 </div>
15451598 <div className="flex items-center justify-end gap-2">
15461599 <button
apps/web/src/views/secrets.tsx+68−7
@@ -1,13 +1,14 @@
1−import { useState, type ReactNode } from "react";
1+import { useEffect, useState, type ReactNode } from "react";
22 import { Link } from "@tanstack/react-router";
33 import {
4+ type InstanceConfig,
45 type SecretListItem,
56 type Loadable,
67 useSecrets,
78 useCreateSecret,
89 useUpdateSecret,
910 useDeleteSecret,
10− useRefreshSecrets,
11+ useInstanceConfig,
1112 } from "@executor/react";
1213 import { LoadableBlock } from "../components/loadable";
1314 import { Button } from "../components/ui/button";
@@ -19,10 +20,26 @@ import { cn } from "../lib/utils"; · 5 unmodified lines
1920 // Page
2021 // ---------------------------------------------------------------------------
2122
23+const storableProvidersFromConfig = (config: Loadable<InstanceConfig>) =>
24+ config.status === "ready"
25+ ? config.data.secretProviders.filter((provider) => provider.canStore)
26+ : [];
27+
28+const providerLabelForId = (config: Loadable<InstanceConfig>, providerId: string): string =>
29+ config.status === "ready"
30+ ? config.data.secretProviders.find((provider) => provider.id === providerId)?.name ?? providerId
31+ : providerId;
32+
2233 export function SecretsPage() {
34+ const instanceConfig = useInstanceConfig();
2335 const secrets = useSecrets();
2436 const [showCreate, setShowCreate] = useState(false);
2537 const [editingId, setEditingId] = useState<string | null>(null);
38+ const storableProviders = storableProvidersFromConfig(instanceConfig);
39+ const defaultProviderLabel =
40+ instanceConfig.status === "ready"
41+ ? providerLabelForId(instanceConfig, instanceConfig.data.defaultSecretStoreProvider)
42+ : null;
2643
2744 return (
2845 <div className="flex-1 overflow-y-auto">
@@ -34,7 +51,8 @@ export function SecretsPage() { · 5 unmodified lines
3451 Secrets
3552 </h1>
3653 <p className="mt-1.5 text-[14px] text-muted-foreground">
37− Tokens and credentials stored in this instance.
54+ Tokens and credentials stored by executor.
55+ {defaultProviderLabel ? ` New secrets default to ${defaultProviderLabel}.` : ""}
3856 </p>
3957 </div>
4058 <Button size="sm" onClick={() => { setShowCreate(true); setEditingId(null); }}>
@@ -48,6 +66,12 @@ export function SecretsPage() { · 7 unmodified lines
4866 <CreateSecretForm
4967 onClose={() => setShowCreate(false)}
5068 className="mb-6"
69+ providerOptions={storableProviders}
70+ defaultProviderId={
71+ instanceConfig.status === "ready"
72+ ? instanceConfig.data.defaultSecretStoreProvider
73+ : null
74+ }
5175 />
5276 )}
5377
@@ -62,6 +86,7 @@ export function SecretsPage() { · 8 unmodified lines
6286 <SecretRow
6387 key={secret.id}
6488 secret={secret}
89+ providerLabel={providerLabelForId(instanceConfig, secret.providerId)}
6590 isEditing={editingId === secret.id}
6691 onEdit={() => setEditingId(editingId === secret.id ? null : secret.id)}
6792 onCancelEdit={() => setEditingId(null)}
@@ -95,7 +120,7 @@ function EmptyState(props: { onAdd: () => void }) { · 27 unmodified lines
95120 No secrets stored
96121 </p>
97122 <p className="text-[13px] text-muted-foreground/60 mb-5">
98− Secrets let you securely provide tokens for source authentication.
123+ Secrets let you securely provide tokens for source authentication without putting them in workspace config.
99124 </p>
100125 <Button size="sm" onClick={props.onAdd}>
101126 <IconPlus className="size-3.5" />
@@ -109,12 +134,27 @@ function EmptyState(props: { onAdd: () => void }) { · 7 unmodified lines
109134 // Create form
110135 // ---------------------------------------------------------------------------
111136
112−function CreateSecretForm(props: { onClose: () => void; className?: string }) {
137+function CreateSecretForm(props: {
138+ onClose: () => void;
139+ className?: string;
140+ providerOptions: ReadonlyArray<{ id: string; name: string }>;
141+ defaultProviderId: string | null;
142+}) {
113143 const createSecret = useCreateSecret();
114144 const [name, setName] = useState("");
115145 const [value, setValue] = useState("");
146+ const [providerId, setProviderId] = useState(props.defaultProviderId ?? "");
116147 const [error, setError] = useState<string | null>(null);
117148
149+ useEffect(() => {
150+ if (providerId.length > 0) {
151+ return;
152+ }
153+ if (props.defaultProviderId) {
154+ setProviderId(props.defaultProviderId);
155+ }
156+ }, [props.defaultProviderId, providerId]);
157+
118158 const handleSubmit = async () => {
119159 setError(null);
120160 const trimmedName = name.trim();
@@ -128,7 +168,11 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 7 unmodified lines
128168 }
129169
130170 try {
131− await createSecret.mutateAsync({ name: trimmedName, value });
171+ await createSecret.mutateAsync({
172+ name: trimmedName,
173+ value,
174+ ...(providerId ? { providerId } : {}),
175+ });
132176 props.onClose();
133177 } catch (err) {
134178 setError(err instanceof Error ? err.message : "Failed creating secret.");
@@ -172,6 +216,19 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 37 unmodified lines
172216 className="h-9 w-full rounded-lg border border-input bg-background px-3 font-mono text-[12px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
173217 />
174218 </FieldLabel>
219+ <FieldLabel label="Store in">
220+ <select
221+ value={providerId}
222+ onChange={(e) => setProviderId(e.target.value)}
223+ className="h-9 w-full rounded-lg border border-input bg-background px-3 text-[13px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
224+ >
225+ {props.providerOptions.map((provider) => (
226+ <option key={provider.id} value={provider.id}>
227+ {provider.name}
228+ </option>
229+ ))}
230+ </select>
231+ </FieldLabel>
175232 </div>
176233 <div className="flex justify-end">
177234 <Button
@@ -194,11 +251,12 @@ function CreateSecretForm(props: { onClose: () => void; className?: string }) { · 16 unmodified lines
194251
195252 function SecretRow(props: {
196253 secret: SecretListItem;
254+ providerLabel: string;
197255 isEditing: boolean;
198256 onEdit: () => void;
199257 onCancelEdit: () => void;
200258 }) {
201− const { secret, isEditing } = props;
259+ const { secret, providerLabel, isEditing } = props;
202260 const deleteSecret = useDeleteSecret();
203261 const [isDeleting, setIsDeleting] = useState(false);
204262
@@ -239,6 +297,9 @@ function SecretRow(props: { · 34 unmodified lines
239297 <Badge variant="outline" className="text-[9px] shrink-0">
240298 {secret.purpose.replace(/_/g, " ")}
241299 </Badge>
300+ <Badge variant="outline" className="text-[9px] shrink-0">
301+ {providerLabel}
302+ </Badge>
242303 </div>
243304 <div className="flex items-center gap-2 mt-0.5">
244305 <span className="text-[11px] font-mono text-muted-foreground/50 truncate">
apps/web/src/views/source-editor.tsx+50−7
@@ -1,4 +1,4 @@
1−import { useState, type ReactNode } from "react";
1+import { useEffect, useState, type ReactNode } from "react";
22 import { Link, useNavigate } from "@tanstack/react-router";
33 import {
44 type CompleteSourceOAuthResult,
@@ -1012,6 +1012,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1007 unmodified lines
10121012 {formState.authKind === "bearer" && (
10131013 <Field label="Token" className="sm:col-span-2">
10141014 <SecretPicker
1015+ instanceConfig={instanceConfig}
10151016 secrets={secrets}
10161017 providerId={formState.bearerProviderId}
10171018 handle={formState.bearerHandle}
@@ -1030,6 +1031,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 12 unmodified lines
10301031 <>
10311032 <Field label="Access token" className="sm:col-span-2">
10321033 <SecretPicker
1034+ instanceConfig={instanceConfig}
10331035 secrets={secrets}
10341036 providerId={formState.oauthAccessProviderId}
10351037 handle={formState.oauthAccessHandle}
@@ -1041,6 +1043,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
10411043 </Field>
10421044 <Field label="Refresh token (optional)" className="sm:col-span-2">
10431045 <SecretPicker
1046+ instanceConfig={instanceConfig}
10441047 secrets={secrets}
10451048 providerId={formState.oauthRefreshProviderId}
10461049 handle={formState.oauthRefreshHandle}
@@ -1218,18 +1221,33 @@ function StatusBanner(props: { state: StatusBannerState; className?: string }) { · 171 unmodified lines
12181221 const CREATE_NEW_VALUE = "__create_new__";
12191222
12201223 function SecretPicker(props: {
1224+ instanceConfig: Loadable<InstanceConfig>;
12211225 secrets: Loadable<ReadonlyArray<SecretListItem>>;
12221226 providerId: string;
12231227 handle: string;
12241228 onSelect: (providerId: string, handle: string) => void;
12251229 allowEmpty?: boolean;
12261230 }) {
1227− const { secrets, providerId, handle, onSelect, allowEmpty } = props;
1231+ const { instanceConfig, secrets, providerId, handle, onSelect, allowEmpty } = props;
12281232 const createSecret = useCreateSecret();
12291233 const [showCreate, setShowCreate] = useState(false);
12301234 const [newName, setNewName] = useState("");
12311235 const [newValue, setNewValue] = useState("");
1236+ const [newProviderId, setNewProviderId] = useState("");
12321237 const [createError, setCreateError] = useState<string | null>(null);
1238+ const storableProviders =
1239+ instanceConfig.status === "ready"
1240+ ? instanceConfig.data.secretProviders.filter((provider) => provider.canStore)
1241+ : [];
1242+
1243+ useEffect(() => {
1244+ if (newProviderId.length > 0) {
1245+ return;
1246+ }
1247+ if (instanceConfig.status === "ready") {
1248+ setNewProviderId(instanceConfig.data.defaultSecretStoreProvider);
1249+ }
1250+ }, [instanceConfig, newProviderId]);
12331251
12341252 // Build the selected value key: if handle is set, use it (it's the secret ID)
12351253 const selectedValue = handle || "";
@@ -1239,6 +1257,11 @@ function SecretPicker(props: { · 3 unmodified lines
12391257 setShowCreate(true);
12401258 setNewName("");
12411259 setNewValue("");
1260+ setNewProviderId(
1261+ instanceConfig.status === "ready"
1262+ ? instanceConfig.data.defaultSecretStoreProvider
1263+ : "",
1264+ );
12421265 setCreateError(null);
12431266 return;
12441267 }
@@ -1246,8 +1269,10 @@ function SecretPicker(props: { · 1 unmodified line
12461269 onSelect("", "");
12471270 return;
12481271 }
1249− // Selecting an existing secret: providerId is always "postgres"
1250− onSelect("postgres", value);
1272+ const matchedSecret = secrets.status === "ready"
1273+ ? secrets.data.find((secret) => secret.id === value)
1274+ : null;
1275+ onSelect(matchedSecret?.providerId ?? "local", value);
12511276 };
12521277
12531278 const handleCreate = async () => {
@@ -1263,7 +1288,11 @@ function SecretPicker(props: { · 9 unmodified lines
12631288 }
12641289
12651290 try {
1266− const result = await createSecret.mutateAsync({ name: trimmedName, value: newValue });
1291+ const result = await createSecret.mutateAsync({
1292+ name: trimmedName,
1293+ value: newValue,
1294+ ...(newProviderId ? { providerId: newProviderId } : {}),
1295+ });
12671296 onSelect(result.providerId, result.id);
12681297 setShowCreate(false);
12691298 } catch (err) {
@@ -1286,7 +1315,7 @@ function SecretPicker(props: { · 16 unmodified lines
12861315
12871316 // Check if the current handle matches a known secret
12881317 const matchedSecret = items.find((s) => s.id === handle);
1289− const isExternalRef = handle && !matchedSecret && providerId !== "postgres";
1318+ const isExternalRef = handle && !matchedSecret && providerId !== "local";
12901319
12911320 return (
12921321 <div className="space-y-2">
@@ -1299,7 +1328,7 @@ function SecretPicker(props: { · 6 unmodified lines
12991328 {!allowEmpty && !selectedValue && <option value="">Select a secret…</option>}
13001329 {items.map((secret) => (
13011330 <option key={secret.id} value={secret.id}>
1302− {secret.name || secret.id}
1331+ {secret.name || secret.id} ({secret.providerId})
13031332 </option>
13041333 ))}
13051334 {isExternalRef && (
@@ -1338,6 +1367,20 @@ function SecretPicker(props: { · 32 unmodified lines
13381367 className="h-8 w-full rounded-lg border border-input bg-background px-3 font-mono text-[11px] text-foreground outline-none transition-colors placeholder:text-muted-foreground/35 focus:border-ring focus:ring-1 focus:ring-ring/25"
13391368 />
13401369 </label>
1370+ <label className="block space-y-1">
1371+ <span className="text-[11px] font-medium text-muted-foreground">Store in</span>
1372+ <select
1373+ value={newProviderId}
1374+ onChange={(e) => setNewProviderId(e.target.value)}
1375+ className="h-8 w-full rounded-lg border border-input bg-background px-3 text-[12px] text-foreground outline-none transition-colors focus:border-ring focus:ring-1 focus:ring-ring/25"
1376+ >
1377+ {storableProviders.map((provider) => (
1378+ <option key={provider.id} value={provider.id}>
1379+ {provider.name}
1380+ </option>
1381+ ))}
1382+ </select>
1383+ </label>
13411384 </div>
13421385 <div className="flex items-center justify-end gap-2">
13431386 <button