diff --git a/packages/web/src/App.jsx b/packages/web/src/App.jsx index c191915..351f59f 100644 --- a/packages/web/src/App.jsx +++ b/packages/web/src/App.jsx @@ -69,12 +69,20 @@ export function App() { } /> } /> } /> + } /> + } /> } /> + } /> + } /> } /> {user.role === "admin" ? ( <> } /> + } /> + } /> } /> + } /> + } /> } /> } /> } /> @@ -82,7 +90,9 @@ export function App() { ) : ( <> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/packages/web/src/components/SecretEditorModal.jsx b/packages/web/src/components/SecretEditorModal.jsx new file mode 100644 index 0000000..3d1644e --- /dev/null +++ b/packages/web/src/components/SecretEditorModal.jsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { errorMessage } from "../api/client.js"; +import { useOwners, useUpsertSecret } from "../api/hooks.js"; + +/** + * Add / replace an encrypted secret. + * Reusable: mount when open; parent supplies mode + initial fields. + * + * @param {"add" | "replace"} mode + * @param {{ owner: string, name?: string }} initial + * @param {() => void} onClose + * @param {(saved: unknown) => void} [onSaved] + * @param {boolean} [lockOwner] + */ +export function SecretEditorModal({ + mode, + initial, + onClose, + onSaved, + lockOwner = false, +}) { + const { data: owners = [] } = useOwners(); + const upsert = useUpsertSecret(); + const [form, setForm] = useState(() => ({ + owner: initial.owner || owners[0] || "default", + name: initial.name || "", + value: "", + })); + + function onSubmit(e) { + e.preventDefault(); + upsert.mutate( + { owner: form.owner, name: form.name, value: form.value }, + { + onSuccess: (data) => { + onSaved?.(data?.secret ?? data); + onClose(); + }, + }, + ); + } + + const title = mode === "add" ? "New secret" : `Replace ${form.owner}/${form.name}`; + + return ( + +
+

{title}

+
+ {mode === "add" ? ( + <> + + + + ) : null} + +

+ Values are encrypted at rest and never shown again after save. Values shorter than 8 + characters are not redacted from logs. +

+ {upsert.isError ? ( +

{errorMessage(upsert.error)}

+ ) : null} +
+ + +
+
+
+
+ +
+
+ ); +} diff --git a/packages/web/src/components/UserEditorModal.jsx b/packages/web/src/components/UserEditorModal.jsx new file mode 100644 index 0000000..6a1d828 --- /dev/null +++ b/packages/web/src/components/UserEditorModal.jsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { errorMessage } from "../api/client.js"; +import { useCreateUser, useUpdateUser } from "../api/hooks.js"; + +/** + * Add / edit a local user. + * Reusable: mount when open; pass `user` for edit. + * + * @param {"add" | "edit"} mode + * @param {{ id: string, username: string, role: string }} [user] + * @param {() => void} onClose + * @param {(saved: unknown) => void} [onSaved] + */ +export function UserEditorModal({ mode, user, onClose, onSaved }) { + const create = useCreateUser(); + const update = useUpdateUser(); + const [form, setForm] = useState(() => ({ + username: user?.username || "", + password: "", + role: user?.role || "operator", + id: user?.id || null, + })); + + const mutation = mode === "add" ? create : update; + + function onSubmit(e) { + e.preventDefault(); + if (mode === "add") { + create.mutate( + { + username: form.username, + password: form.password, + role: form.role, + }, + { + onSuccess: (data) => { + onSaved?.(data?.user ?? data); + onClose(); + }, + }, + ); + return; + } + const body = { id: form.id, role: form.role }; + if (form.password) body.password = form.password; + update.mutate(body, { + onSuccess: (data) => { + onSaved?.(data?.user ?? data); + onClose(); + }, + }); + } + + const title = mode === "add" ? "New user" : form.username; + + return ( + +
+

{title}

+
+ {mode === "add" ? ( + + ) : null} + + + {mutation.isError ? ( +

{errorMessage(mutation.error)}

+ ) : null} +
+ + +
+
+
+
+ +
+
+ ); +} diff --git a/packages/web/src/components/VariableEditorModal.jsx b/packages/web/src/components/VariableEditorModal.jsx new file mode 100644 index 0000000..4f54ea3 --- /dev/null +++ b/packages/web/src/components/VariableEditorModal.jsx @@ -0,0 +1,191 @@ +import { useState } from "react"; +import { errorMessage } from "../api/client.js"; +import { useOwners, useUpsertVariable } from "../api/hooks.js"; + +const TYPES = ["string", "number", "boolean"]; + +function defaultValue(type) { + if (type === "boolean") return false; + if (type === "number") return ""; + return ""; +} + +/** + * Add / edit a plaintext workflow variable. + * Reusable: mount when open; parent supplies mode + initial fields. + * + * @param {"add" | "edit"} mode + * @param {{ owner: string, name?: string, type?: string, value?: string | number | boolean }} initial + * @param {() => void} onClose + * @param {(saved: unknown) => void} [onSaved] + * @param {boolean} [lockOwner] When true, owner cannot be changed (add mode). + */ +export function VariableEditorModal({ + mode, + initial, + onClose, + onSaved, + lockOwner = false, +}) { + const { data: owners = [] } = useOwners(); + const upsert = useUpsertVariable(); + const [form, setForm] = useState(() => ({ + owner: initial.owner || owners[0] || "default", + name: initial.name || "", + type: initial.type || "string", + value: + initial.type === "number" + ? String(initial.value ?? "") + : initial.value !== undefined + ? initial.value + : defaultValue(initial.type || "string"), + })); + const [formError, setFormError] = useState(null); + + function onTypeChange(type) { + setForm({ ...form, type, value: defaultValue(type) }); + } + + function onSubmit(e) { + e.preventDefault(); + let value = form.value; + if (form.type === "number") { + value = Number(form.value); + if (!Number.isFinite(value)) { + setFormError("value must be a finite number"); + return; + } + } + if (form.type === "boolean") { + value = form.value === true; + } + setFormError(null); + upsert.mutate( + { owner: form.owner, name: form.name, type: form.type, value }, + { + onSuccess: (data) => { + onSaved?.(data?.variable ?? data); + onClose(); + }, + }, + ); + } + + const title = mode === "add" ? "New variable" : `Edit ${form.owner}/${form.name}`; + + return ( + +
+

{title}

+
+ {mode === "add" ? ( + <> + + + + ) : null} + +