UI: Admin shh module

This commit is contained in:
Johannes Kirschbauer
2024-09-03 10:55:04 +02:00
parent 71705f8a51
commit 2e4aca9c40
4 changed files with 123 additions and 42 deletions

View File

@@ -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 # @wraps preserves all metadata of fn
# we need to update the annotation, because our wrapper changes the return type # we need to update the annotation, because our wrapper changes the return type

View File

@@ -1,5 +1,3 @@
from pathlib import Path
from clan_cli.inventory import ( from clan_cli.inventory import (
AdminConfig, AdminConfig,
ServiceAdmin, ServiceAdmin,
@@ -27,7 +25,7 @@ def get_admin_service(base_url: str) -> ServiceAdmin | None:
@API.register @API.register
def set_admin_service( def set_admin_service(
base_url: str, base_url: str,
allowed_keys: dict[str, Path], allowed_keys: dict[str, str],
instance_name: str = "admin", instance_name: str = "admin",
extra_machines: list[str] | None = None, extra_machines: list[str] | None = None,
) -> None: ) -> None:
@@ -43,24 +41,15 @@ def set_admin_service(
msg = "At least one key must be provided to ensure access" msg = "At least one key must be provided to ensure access"
raise ValueError(msg) 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( instance = ServiceAdmin(
meta=ServiceMeta(name=instance_name), meta=ServiceMeta(name=instance_name),
roles=ServiceAdminRole( roles=ServiceAdminRole(
default=ServiceAdminRoleDefault( default=ServiceAdminRoleDefault(
config=AdminConfig(allowedKeys=keys),
machines=extra_machines, machines=extra_machines,
tags=["all"], tags=["all"],
) )
), ),
config=AdminConfig(allowedKeys=allowed_keys),
) )
inventory.services.admin[instance_name] = instance inventory.services.admin[instance_name] = instance

View File

@@ -1,6 +1,7 @@
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid"; 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 cx from "classnames";
import { createECDH } from "crypto";
interface TextInputProps<T extends FieldValues, R extends ResponseData> { interface TextInputProps<T extends FieldValues, R extends ResponseData> {
formStore: FormStore<T, R>; 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>( export function TextInput<T extends FieldValues, R extends ResponseData>(
props: TextInputProps<T, R>, props: TextInputProps<T, R>,
) { ) {
const value = () => props.value;
createEffect(() => {
console.log("rendering text input", props.value);
});
return ( return (
<label <label
class={cx("form-control w-full", props.class)} class={cx("form-control w-full", props.class)}
@@ -46,7 +53,7 @@ export function TextInput<T extends FieldValues, R extends ResponseData>(
{props.inlineLabel} {props.inlineLabel}
<input <input
{...props.inputProps} {...props.inputProps}
value={props.value} value={value()}
type={props.type ? props.type : "text"} type={props.type ? props.type : "text"}
class="grow" class="grow"
classList={{ classList={{

View File

@@ -2,25 +2,23 @@ import { callApi, SuccessQuery } from "@/src/api";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { createQuery } from "@tanstack/solid-query"; 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 { Show } from "solid-js";
import { DiskView } from "../disk/view";
import { Accessor } from "solid-js";
import { import {
createForm, createForm,
FieldArray,
FieldValues, FieldValues,
getValue, getValue,
getValues, getValues,
insert,
setValue, setValue,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/components/TextInput";
import toast from "solid-toast";
type AdminData = SuccessQuery<"get_admin_service">["data"]; type AdminData = SuccessQuery<"get_admin_service">["data"];
interface ClanDetailsProps { interface ClanDetailsProps {
admin: AdminData; admin: AdminData;
base_url: string;
} }
interface AdminSettings extends FieldValues { interface AdminSettings extends FieldValues {
allowedKeys: { name: string; value: string }[]; 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) => { const handleSubmit = async (values: AdminSettings) => {
console.log("submitting", values, getValues(formStore)); 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 ( return (
<div> <div>
<span class="text-xl text-primary">Clan Settings</span> <span class="text-xl text-primary">Clan Admin Settings</span>
<br></br>
<span class="text-lg text-neutral">
Each of the following keys can be used to authenticate on any machine
</span>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<div class="grid grid-cols-12 gap-2"> <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()}> <For each={keys()}>
{(name, idx) => ( {(name, idx) => (
<> <>
@@ -59,7 +74,13 @@ const ClanDetails = (props: ClanDetailsProps) => {
<TextInput <TextInput
formStore={formStore} formStore={formStore}
inputProps={props} 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 ?? ""} value={field.value ?? ""}
error={field.error} error={field.error}
class="col-span-4" class="col-span-4"
@@ -69,36 +90,88 @@ const ClanDetails = (props: ClanDetailsProps) => {
</Field> </Field>
<Field name={`allowedKeys.${idx()}.value`}> <Field name={`allowedKeys.${idx()}.value`}>
{(field, props) => ( {(field, props) => (
<TextInput <>
formStore={formStore} <TextInput
inputProps={props} formStore={formStore}
label="Value" inputProps={props}
value={field.value ?? ""} label={"Value"}
error={field.error} value={field.value ?? ""}
class="col-span-7" error={field.error}
required 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> </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> <span class="material-icons">delete</span>
</button> </button>
</> </>
)} )}
</For> </For>
</div> </div>
<div class="flex w-full my-2 gap-2"> <div class="my-2 flex w-full gap-2">
<button <button
class="btn btn-ghost btn-square" class="btn btn-square btn-ghost"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setKeys((c) => [...c, 1]); setKeys((c) => [...c, 1]);
console.log(keys());
}} }}
> >
<span class="material-icons">add</span> <span class="material-icons">add</span>
</button> </button>
<button class="btn">Submit</button> <button class="btn" type="submit">
Submit
</button>
</div> </div>
</Form> </Form>
</div> </div>
@@ -128,7 +201,7 @@ export const Details = () => {
> >
<Switch> <Switch>
<Match when={query.data}> <Match when={query.data}>
{(d) => <ClanDetails admin={query.data} />} {(d) => <ClanDetails admin={query.data} base_url={clan_dir} />}
</Match> </Match>
</Switch> </Switch>
</Show> </Show>