Merge pull request 'UI: Admin shh module' (#2031) from hsjobeki/clan-core:hsjobeki-main into main
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
|
# @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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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
|
<TextInput
|
||||||
formStore={formStore}
|
formStore={formStore}
|
||||||
inputProps={props}
|
inputProps={props}
|
||||||
label="Value"
|
label={"Value"}
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
class="col-span-7"
|
class="col-span-6"
|
||||||
required
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user