UI: Admin shh module
This commit is contained in:
@@ -134,6 +134,18 @@ API.register(open_file)
|
||||
)
|
||||
],
|
||||
)
|
||||
except Exception as e:
|
||||
return ErrorDataClass(
|
||||
op_key=op_key,
|
||||
status="error",
|
||||
errors=[
|
||||
ApiError(
|
||||
message=str(e),
|
||||
description="An unexpected error occurred",
|
||||
location=[fn.__name__],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# @wraps preserves all metadata of fn
|
||||
# we need to update the annotation, because our wrapper changes the return type
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.inventory import (
|
||||
AdminConfig,
|
||||
ServiceAdmin,
|
||||
@@ -27,7 +25,7 @@ def get_admin_service(base_url: str) -> ServiceAdmin | None:
|
||||
@API.register
|
||||
def set_admin_service(
|
||||
base_url: str,
|
||||
allowed_keys: dict[str, Path],
|
||||
allowed_keys: dict[str, str],
|
||||
instance_name: str = "admin",
|
||||
extra_machines: list[str] | None = None,
|
||||
) -> None:
|
||||
@@ -43,24 +41,15 @@ def set_admin_service(
|
||||
msg = "At least one key must be provided to ensure access"
|
||||
raise ValueError(msg)
|
||||
|
||||
keys = {}
|
||||
for name, keyfile in allowed_keys.items():
|
||||
if not keyfile.is_absolute():
|
||||
msg = f"Keyfile '{keyfile}' must be an absolute path"
|
||||
raise ValueError(msg)
|
||||
with keyfile.open() as f:
|
||||
pubkey = f.read()
|
||||
keys[name] = pubkey
|
||||
|
||||
instance = ServiceAdmin(
|
||||
meta=ServiceMeta(name=instance_name),
|
||||
roles=ServiceAdminRole(
|
||||
default=ServiceAdminRoleDefault(
|
||||
config=AdminConfig(allowedKeys=keys),
|
||||
machines=extra_machines,
|
||||
tags=["all"],
|
||||
)
|
||||
),
|
||||
config=AdminConfig(allowedKeys=allowed_keys),
|
||||
)
|
||||
|
||||
inventory.services.admin[instance_name] = instance
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
|
||||
import { Show, type JSX } from "solid-js";
|
||||
import { createEffect, Show, type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { createECDH } from "crypto";
|
||||
|
||||
interface TextInputProps<T extends FieldValues, R extends ResponseData> {
|
||||
formStore: FormStore<T, R>;
|
||||
@@ -22,6 +23,12 @@ interface TextInputProps<T extends FieldValues, R extends ResponseData> {
|
||||
export function TextInput<T extends FieldValues, R extends ResponseData>(
|
||||
props: TextInputProps<T, R>,
|
||||
) {
|
||||
const value = () => props.value;
|
||||
|
||||
createEffect(() => {
|
||||
console.log("rendering text input", props.value);
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
class={cx("form-control w-full", props.class)}
|
||||
@@ -46,7 +53,7 @@ export function TextInput<T extends FieldValues, R extends ResponseData>(
|
||||
{props.inlineLabel}
|
||||
<input
|
||||
{...props.inputProps}
|
||||
value={props.value}
|
||||
value={value()}
|
||||
type={props.type ? props.type : "text"}
|
||||
class="grow"
|
||||
classList={{
|
||||
|
||||
@@ -2,25 +2,23 @@ import { callApi, SuccessQuery } from "@/src/api";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createEffect, createSignal, For, Match, Switch } from "solid-js";
|
||||
import { createSignal, For, Match, Switch } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
import { DiskView } from "../disk/view";
|
||||
import { Accessor } from "solid-js";
|
||||
import {
|
||||
createForm,
|
||||
FieldArray,
|
||||
FieldValues,
|
||||
getValue,
|
||||
getValues,
|
||||
insert,
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import toast from "solid-toast";
|
||||
|
||||
type AdminData = SuccessQuery<"get_admin_service">["data"];
|
||||
|
||||
interface ClanDetailsProps {
|
||||
admin: AdminData;
|
||||
base_url: string;
|
||||
}
|
||||
interface AdminSettings extends FieldValues {
|
||||
allowedKeys: { name: string; value: string }[];
|
||||
@@ -37,20 +35,37 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const [keys, setKeys] = createSignal<1[]>(new Array(items().length).fill(1));
|
||||
const [keys, setKeys] = createSignal<1[]>(
|
||||
new Array(items().length || 1).fill(1),
|
||||
);
|
||||
|
||||
const handleSubmit = async (values: AdminSettings) => {
|
||||
console.log("submitting", values, getValues(formStore));
|
||||
|
||||
const r = await callApi("set_admin_service", {
|
||||
base_url: props.base_url,
|
||||
allowed_keys: values.allowedKeys.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
if (r.status === "success") {
|
||||
toast.success("Successfully updated admin settings");
|
||||
}
|
||||
if (r.status === "error") {
|
||||
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span class="text-xl text-primary">Clan Settings</span>
|
||||
<br></br>
|
||||
<span class="text-lg text-neutral">
|
||||
Each of the following keys can be used to authenticate on any machine
|
||||
</span>
|
||||
<span class="text-xl text-primary">Clan Admin Settings</span>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<span class="col-span-12 text-lg text-neutral">
|
||||
Each of the following keys can be used to authenticate on any
|
||||
machine
|
||||
</span>
|
||||
<For each={keys()}>
|
||||
{(name, idx) => (
|
||||
<>
|
||||
@@ -59,7 +74,13 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={`allowedKeys.${idx()}.name-` + items().length}
|
||||
label={"Name"}
|
||||
adornment={{
|
||||
position: "start",
|
||||
content: (
|
||||
<span class="material-icons text-gray-400">key</span>
|
||||
),
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-4"
|
||||
@@ -69,36 +90,88 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
||||
</Field>
|
||||
<Field name={`allowedKeys.${idx()}.value`}>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label="Value"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-7"
|
||||
required
|
||||
/>
|
||||
<>
|
||||
<TextInput
|
||||
formStore={formStore}
|
||||
inputProps={props}
|
||||
label={"Value"}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-6"
|
||||
required
|
||||
/>
|
||||
<span class="tooltip mt-auto" data-tip="Select file">
|
||||
<label
|
||||
class={"form-control w-full"}
|
||||
aria-disabled={formStore.submitting}
|
||||
>
|
||||
<div class="input input-bordered relative flex items-center gap-2">
|
||||
<input
|
||||
value=""
|
||||
// Disable drag n drop
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
class="absolute -ml-4 size-full cursor-pointer opacity-0"
|
||||
type="file"
|
||||
onInput={async (e) => {
|
||||
console.log(e.target.files);
|
||||
if (!e.target.files) return;
|
||||
|
||||
const content = await e.target.files[0].text();
|
||||
console.log(content);
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.value`,
|
||||
content,
|
||||
);
|
||||
if (
|
||||
!getValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
)
|
||||
) {
|
||||
setValue(
|
||||
formStore,
|
||||
`allowedKeys.${idx()}.name`,
|
||||
e.target.files[0].name,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="material-icons">file_open</span>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<button class="btn btn-ghost col-span-1 self-end">
|
||||
<button
|
||||
class="btn btn-ghost col-span-1 self-end"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => c.filter((_, i) => i !== idx()));
|
||||
setValue(formStore, `allowedKeys.${idx()}.name`, "");
|
||||
setValue(formStore, `allowedKeys.${idx()}.value`, "");
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">delete</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex w-full my-2 gap-2">
|
||||
<div class="my-2 flex w-full gap-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-square"
|
||||
class="btn btn-square btn-ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setKeys((c) => [...c, 1]);
|
||||
console.log(keys());
|
||||
}}
|
||||
>
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
<button class="btn">Submit</button>
|
||||
<button class="btn" type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -128,7 +201,7 @@ export const Details = () => {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={query.data}>
|
||||
{(d) => <ClanDetails admin={query.data} />}
|
||||
{(d) => <ClanDetails admin={query.data} base_url={clan_dir} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user