feat(server): introduce admin reset functionality and enhance user management

- Added a new `reset-admin` script to reset or create the admin username and password.
- Updated the README to include instructions for resetting the admin login.
- Enhanced user management by allowing username updates during admin reset.
- Defaulted new resources to the internal namespace `local` for better organization.
- Removed unused owner selection from various components to streamline the UI.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
2026-08-22 19:08:52 +07:00
parent 811890443b
commit 71cc705cd2
19 changed files with 271 additions and 484 deletions
@@ -1,22 +1,19 @@
import { useState } from "react";
import { errorMessage } from "../api/client.js";
import { useDuplicateWorkflow, useOwners } from "../api/hooks.js";
import { useDuplicateWorkflow } from "../api/hooks.js";
import { useNotifications } from "../notifications.jsx";
export function DuplicateWorkflowDialog({ source, warnUnsaved, onClose, onDuplicated }) {
const { notify } = useNotifications();
const { data: owners = [] } = useOwners();
const duplicate = useDuplicateWorkflow();
const [destOwner, setDestOwner] = useState(source.owner);
function onSubmit(e) {
e.preventDefault();
if (destOwner === source.owner && duplicate.isPending) return;
if (duplicate.isPending) return;
duplicate.mutate(
{
owner: source.owner,
file: source.file,
destOwner,
destOwner: source.owner,
},
{
onSuccess: (data) => {
@@ -33,7 +30,7 @@ export function DuplicateWorkflowDialog({ source, warnUnsaved, onClose, onDuplic
<h3 className="font-bold">Duplicate {source.key}?</h3>
<p className="mt-2 text-sm opacity-70">
A new UUID filename is assigned automatically. The copy starts disabled. HTTP paths are
rewritten when staying under the same owner so triggers do not collide.
rewritten so triggers do not collide.
</p>
{warnUnsaved ? (
<p className="text-warning mt-2 text-sm">
@@ -41,21 +38,6 @@ export function DuplicateWorkflowDialog({ source, warnUnsaved, onClose, onDuplic
</p>
) : null}
<form className="mt-4 space-y-3" onSubmit={onSubmit}>
<label className="form-control w-full">
<span className="label py-0 text-sm">Owner</span>
<input
className="input input-sm w-full"
list="duplicate-workflow-owners"
value={destOwner}
onChange={(e) => setDestOwner(e.target.value)}
required
/>
<datalist id="duplicate-workflow-owners">
{owners.map((o) => (
<option key={o} value={o} />
))}
</datalist>
</label>
{duplicate.isError ? (
<p className="text-error text-sm">{errorMessage(duplicate.error)}</p>
) : null}
-2
View File
@@ -17,7 +17,6 @@ import {
LuShield,
LuSun,
LuTags,
LuUsers,
} from "react-icons/lu";
import { useLogout, useOpsStatus } from "../api/hooks.js";
import { brandMark } from "../theme/brand.js";
@@ -52,7 +51,6 @@ const navSections = [
items: [
{ to: "/manage", label: "Manage", icon: LuServer },
{ to: "/backup", label: "Backup", icon: LuArchive },
{ to: "/users", label: "Users", icon: LuUsers },
],
},
];
@@ -1,8 +1,9 @@
import { useState } from "react";
import { errorMessage } from "../api/client.js";
import { useOwners, useScripts, useUpsertProfile } from "../api/hooks.js";
import { useScripts, useUpsertProfile } from "../api/hooks.js";
import { FormInput, FormSelect } from "./FormControls.jsx";
import { ConfigFields } from "./workflow/ConfigFields.jsx";
import { DEFAULT_OWNER } from "../lib/tenant.js";
/**
* @param {"add" | "edit"} mode
@@ -16,11 +17,10 @@ import { ConfigFields } from "./workflow/ConfigFields.jsx";
* @param {number} [usageCount]
*/
export function ProfileEditorModal({ mode, initial, onClose, onSaved, usageCount = 0 }) {
const { data: owners = [] } = useOwners();
const { data: scripts = [] } = useScripts();
const upsert = useUpsertProfile();
const [form, setForm] = useState(() => ({
owner: initial.owner || owners[0] || "default",
owner: initial.owner || DEFAULT_OWNER,
name: initial.name || "",
script: initial.script || "",
config:
@@ -72,7 +72,7 @@ export function ProfileEditorModal({ mode, initial, onClose, onSaved, usageCount
submit();
}
const title = mode === "add" ? "New profile" : `Edit ${form.owner}/${form.name}`;
const title = mode === "add" ? "New profile" : `Edit ${form.name}`;
return (
<dialog className="modal modal-open">
@@ -80,49 +80,23 @@ export function ProfileEditorModal({ mode, initial, onClose, onSaved, usageCount
<h3 className="font-bold">{title}</h3>
<form className="mt-3 space-y-2" onSubmit={onSubmit}>
{mode === "add" ? (
<>
<label className="form-control w-full">
<span className="label py-0 text-sm">Owner</span>
{owners.length > 0 ? (
<FormSelect
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
>
{owners.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</FormSelect>
) : (
<FormInput
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
/>
)}
</label>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens. Cannot be changed later."
/>
<span className="label-text-alt opacity-60">
YAML id. Locked after create so live workflow refs stay valid.
</span>
</label>
</>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens. Cannot be changed later."
/>
<span className="label-text-alt opacity-60">
YAML id. Locked after create so live workflow refs stay valid.
</span>
</label>
) : (
<p className="text-sm opacity-70">
<span className="font-mono">{form.owner}/{form.name}</span>
<span className="font-mono">{form.name}</span>
<span className="ml-2 opacity-60">(name cannot be changed)</span>
</p>
)}
@@ -1,30 +1,22 @@
import { useState } from "react";
import { errorMessage } from "../api/client.js";
import { useOwners, useUpsertSecret } from "../api/hooks.js";
import { FormInput, FormSelect } from "./FormControls.jsx";
import { useUpsertSecret } from "../api/hooks.js";
import { FormInput } from "./FormControls.jsx";
import { Modal } from "./Modal.jsx";
import { DEFAULT_OWNER } from "../lib/tenant.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();
export function SecretEditorModal({ mode, initial, onClose, onSaved }) {
const upsert = useUpsertSecret();
const [form, setForm] = useState(() => ({
owner: initial.owner || owners[0] || "default",
owner: initial.owner || DEFAULT_OWNER,
name: initial.name || "",
value: "",
}));
@@ -42,52 +34,24 @@ export function SecretEditorModal({
);
}
const title = mode === "add" ? "New secret" : `Replace ${form.owner}/${form.name}`;
const title = mode === "add" ? "New secret" : `Replace ${form.name}`;
return (
<Modal open onClose={onClose} boxClassName="max-w-md" aria-label={title}>
<h3 className="font-bold">{title}</h3>
<form className="mt-3 space-y-2" onSubmit={onSubmit}>
{mode === "add" ? (
<>
<label className="form-control w-full">
<span className="label py-0 text-sm">Owner</span>
{owners.length > 0 ? (
<FormSelect
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
disabled={lockOwner}
>
{owners.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</FormSelect>
) : (
<FormInput
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
disabled={lockOwner}
/>
)}
</label>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens"
/>
</label>
</>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens"
/>
</label>
) : null}
<label className="form-control w-full">
<span className="label py-0 text-sm">Value</span>
@@ -1,7 +1,8 @@
import { useState } from "react";
import { errorMessage } from "../api/client.js";
import { useOwners, useUpsertVariable } from "../api/hooks.js";
import { useUpsertVariable } from "../api/hooks.js";
import { FormInput, FormSelect, FormTextarea } from "./FormControls.jsx";
import { DEFAULT_OWNER } from "../lib/tenant.js";
const TYPES = ["string", "number", "boolean"];
@@ -13,25 +14,16 @@ function defaultValue(type) {
/**
* 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();
export function VariableEditorModal({ mode, initial, onClose, onSaved }) {
const upsert = useUpsertVariable();
const [form, setForm] = useState(() => ({
owner: initial.owner || owners[0] || "default",
owner: initial.owner || DEFAULT_OWNER,
name: initial.name || "",
type: initial.type || "string",
value:
@@ -72,7 +64,7 @@ export function VariableEditorModal({
);
}
const title = mode === "add" ? "New variable" : `Edit ${form.owner}/${form.name}`;
const title = mode === "add" ? "New variable" : `Edit ${form.name}`;
return (
<dialog className="modal modal-open">
@@ -80,45 +72,17 @@ export function VariableEditorModal({
<h3 className="font-bold">{title}</h3>
<form className="mt-3 space-y-2" onSubmit={onSubmit}>
{mode === "add" ? (
<>
<label className="form-control w-full">
<span className="label py-0 text-sm">Owner</span>
{owners.length > 0 ? (
<FormSelect
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
disabled={lockOwner}
>
{owners.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</FormSelect>
) : (
<FormInput
className="w-full"
value={form.owner}
onChange={(e) => setForm({ ...form, owner: e.target.value })}
required
disabled={lockOwner}
/>
)}
</label>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens"
/>
</label>
</>
<label className="form-control w-full">
<span className="label py-0 text-sm">Name</span>
<FormInput
className="w-full font-mono"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
pattern="[A-Za-z0-9._-]+"
title="Letters, numbers, dots, underscores, hyphens"
/>
</label>
) : null}
<label className="form-control w-full">
<span className="label py-0 text-sm">Type</span>