feat(node): package Ubuntu desktop setup and trusted access
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Button, ConfirmationModal, Icon, IconButton, ResourceList, ResourceRow, Select, StatusBadge, TextAreaField, TextField, Window, WindowFooterActions } from "@nodedc/ui-react";
|
||||
import { request } from "./api";
|
||||
import { useAccess, type AccessKey } from "./useAccess";
|
||||
|
||||
export function SystemAccess({ revision, failure, success, adding, closeAdd }: { revision: string; failure: (error: unknown) => void; success: (message: string) => void; adding: boolean; closeAdd: () => void }) {
|
||||
const { access, loading, refresh } = useAccess(revision, failure);
|
||||
const [user, setUser] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
const [key, setKey] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const [remove, setRemove] = useState<AccessKey | null>(null);
|
||||
const [detail, setDetail] = useState<AccessKey | null>(null);
|
||||
useEffect(() => { if (access) setUser(current => access.users.includes(current) ? current : access.users[0] ?? ""); }, [access]);
|
||||
useEffect(() => { if (!adding) { setLabel(""); setKey(""); } }, [adding]);
|
||||
async function add(event: React.FormEvent) {
|
||||
event.preventDefault(); if (pending) return; setPending(true);
|
||||
try { await request("/api/access", "POST", { user, label, key: key.trim() }); await refresh(); closeAdd(); success("Доступ устройства добавлен"); }
|
||||
catch (error) { failure(error); } finally { setPending(false); }
|
||||
}
|
||||
return <div className="node-content">
|
||||
<div className="node-section-heading"><p className="node-note">Компьютеры, которым разрешён вход по SSH через Node.</p><StatusBadge tone={access?.ssh_ready ? "success" : "neutral"}>{loading ? "Проверяем SSH" : !access ? "Нет сведений" : access.ssh_ready ? "SSH отвечает локально" : "SSH не отвечает"}</StatusBadge></div>
|
||||
{loading && !access ? <ActivityIndicator label="Получение доверенных устройств" /> : !access ? <p className="node-note">Не удалось загрузить список. Повторите обновление.</p> : <>
|
||||
{access.keys.length === 0 ? <p className="node-note">Доверенных устройств пока нет. Нажмите плюс в шапке, чтобы добавить компьютер.</p> : <ResourceList aria-label="Доверенные SSH-устройства">{access.keys.map(item => <li key={`${item.user}:${item.id}`}><ResourceRow icon={<Icon name="key" />} title={item.label} description={`Пользователь Ubuntu: ${item.user}`} metadata={<span title={item.id}>{item.id}</span>} status={<StatusBadge>Доступ разрешён</StatusBadge>} actions={<><IconButton label={`Сведения: ${item.label}`} onClick={() => setDetail(item)}><Icon name="eye" /></IconButton><IconButton label={`Отозвать доступ: ${item.label}`} onClick={() => setRemove(item)}><Icon name="trash" /></IconButton></>} /></li>)}</ResourceList>}
|
||||
<p className="node-note">Разрешённый ключ не означает, что компьютер сейчас подключён. Отзыв закрывает новые подключения через Node; открытые сеансы и отдельно настроенные способы входа Ubuntu сохраняются.</p>
|
||||
</>}
|
||||
<Window open={adding} title="Добавить доверенное устройство" subtitle="Разрешить компьютеру подключаться к этому борту по SSH" size="md" closeOnBackdrop={false} closeOnEscape={!pending} onClose={() => { if (!pending) closeAdd(); }} footer={<WindowFooterActions><Button disabled={pending} onClick={closeAdd}>Отмена</Button><Button type="submit" form="node-add-ssh" disabled={pending || !access || !key.trim() || !label.trim() || !user}>{pending ? "Добавляем…" : "Разрешить доступ"}</Button></WindowFooterActions>}>
|
||||
{!access ? <p className="node-note">{loading ? "Получаем пользователей Ubuntu…" : "Не удалось получить пользователей. Закройте окно и обновите список."}</p> : access.users.length === 0 ? <p className="node-note">Не найдены администраторы Ubuntu. Добавьте пользователя в настройках системы.</p> : <form id="node-add-ssh" className="node-form" onSubmit={add} aria-busy={pending}>
|
||||
<TextField label="Название устройства" placeholder="Например, ноутбук оператора" value={label} maxLength={64} disabled={pending} onChange={event => setLabel(event.target.value)} autoComplete="off" />
|
||||
<Select label="Пользователь Ubuntu" value={user} options={access.users.map(value => ({ value, label: value }))} onChange={setUser} disabled={pending} />
|
||||
<TextAreaField label="Публичный SSH-ключ Ed25519" placeholder="ssh-ed25519 …" value={key} rows={4} disabled={pending} onChange={event => setKey(event.target.value)} autoComplete="off" spellCheck={false} />
|
||||
<p className="node-note">Вставьте содержимое публичного файла .pub с доверенного компьютера. Приватный ключ остаётся на том компьютере. Этот доступ действует в частной сети.</p>
|
||||
</form>}
|
||||
</Window>
|
||||
<Window open={detail !== null} title={detail?.label ?? "Доверенное устройство"} subtitle="SSH-доступ к этому борту" onClose={() => setDetail(null)}>
|
||||
{detail && <dl className="node-facts"><div><dt>Пользователь Ubuntu</dt><dd>{detail.user}</dd></div><div><dt>Отпечаток</dt><dd>{detail.id}</dd></div><div><dt>Публичный ключ</dt><dd>{detail.public_key}</dd></div></dl>}
|
||||
</Window>
|
||||
<ConfirmationModal open={remove !== null} title="Отозвать доступ устройства?" description={`Новые подключения через Node с устройства «${remove?.label ?? ""}» станут недоступны. Открытые сеансы продолжат работать.`} confirmLabel="Отозвать" cancelLabel="Отмена" pendingLabel="Отзываем…" danger onClose={() => setRemove(null)} onConfirm={async () => {
|
||||
if (!remove) return;
|
||||
try { await request("/api/access", "DELETE", { user: remove.user, id: remove.id }); await refresh(); setRemove(null); success("Доступ устройства отозван"); } catch (error) { failure(error); throw error; }
|
||||
}} />
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user