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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user