CH-J Toolsbainisteoireacht freastalaí SSH
Roghchlár
Foinse foilsithe

CH-J Tools

Brabhsáil comhadlanna agus comhaid chun feidhmchlár áirithe a scaoileadh.

Íosluchtaigh foinse ZIP
CH-J / Server Manager 1704 Comhaid
0.1.2-beta Suimigh: · Tógtha:
index.js Suimigh: Dáta gan taifeadadh 97.8 KB · 1373 líne
Cód Íosluchtaigh comhad
1"use strict";
3const api = window.chjPlugin;
4const i18n = window.chjPluginI18n.createI18n("cs");
5const t = i18n.t;
6let endedSessionId = "";
7let sessionsLoading = false;
9// Guard every continuation, including sudo retries and dialog answers, against a server switch.
10async function sessionResult(promise, token) {
11 const value = await promise;
12 if (token !== state.generation) throw new Error("CHJ_SESSION_CHANGED");
13 return value;
16// Marks a catalog key that is translated with t() only when it is shown.
17const k = (text) => text;
18const el = new Proxy({}, { get: (_target, id) => document.getElementById(id) });
20// Every asynchronous result is checked against these tokens, so data of a previous session or
21// account is never rendered after the user switched.
22const state = {
23 sessions: [],
24 sessionId: "",
25 generation: 0,
26 detailToken: 0,
27 privilege: null,
28 inventory: null,
29 selected: null,
30 detail: null,
31 detailLoading: false,
32 tab: "account",
33 loading: false,
34 mutating: false
35};
37const PRIVILEGE_TEXT = {
38 root: ["root", "ok"],
39 "sudo-nopasswd": [k("sudo bez hesla"), "ok"],
40 "sudo-password": [k("sudo s heslem"), "warn"],
41 none: [k("bez sudo – jen čtení"), "danger"],
42 unknown: [k("neověřeno"), "muted"],
43 "windows-elevated": [k("správce Windows (zvýšený token)"), "ok"],
44 "windows-admin-unelevated": [k("člen Administrators bez zvýšeného tokenu – jen čtení"), "warn"],
45 "windows-standard": [k("standardní účet Windows – jen čtení"), "danger"]
46};
47const SUDO_TEXT = {
48 full: k("plné sudo"),
49 "full-nopasswd": k("plné sudo bez hesla"),
50 limited: k("omezené sudo"),
51 none: k("bez sudo"),
52 unavailable: k("sudo není nainstalováno"),
53 unknown: k("neznámé")
54};
55const PASSWORD_TEXT = {
56 set: [k("heslo nastaveno"), "ok"],
57 locked: [k("heslo zamčeno"), "warn"],
58 none: [k("bez hesla"), "muted"],
59 empty: [k("prázdné heslo!"), "danger"],
60 unknown: [k("stav hesla neznámý"), "muted"]
61};
62const PROTECTION_TEXT = {
63 root: k("Chráněný systémový účet root: nelze ho smazat ani měnit UID/GID, skupiny, shell či domovský adresář. SSH přístup (včetně PermitRootLogin) a SSH klíče lze spravovat s bezpečnostními kontrolami."),
64 system: k("Systémový účet je chráněný. CH-J ho zobrazuje jen pro čtení."),
65 builtin: k("Vestavěný účet Windows (Administrator, Guest, DefaultAccount, WDAGUtilityAccount) je chráněný a zobrazuje se jen pro čtení."),
66 session: k("Tímto účtem je CH-J právě připojeno. Změny, které by mohly přerušit přístup, jsou zablokované; SSH klíče a SSH přístup lze měnit s ochranou proti odříznutí.")
67};
68const ERROR_TEXT = {
69 SUDO_PASSWORD_REQUIRED: k("Operace vyžaduje heslo pro sudo."),
70 SUDO_PASSWORD_INCORRECT: k("Heslo pro sudo bylo odmítnuto."),
71 SUDO_NOT_PERMITTED: k("Účet SSH relace nemá pro tuto operaci oprávnění sudo. Zadání hesla to nezmění."),
72 SUDO_TTY_REQUIRED: k("Politika sudo na serveru vyžaduje terminál (requiretty). CH-J ji neobchází."),
73 SUDO_AUTH_FAILED: k("Ověření sudo selhalo nebo je účet dočasně zablokován."),
74 SUDO_UNAVAILABLE: k("Na serveru není sudo a relace neběží jako root."),
75 SUDO_FAILED: k("sudo operaci nespustilo."),
76 SUDO_PASSWORD_INVALID: k("Heslo pro sudo má neplatný formát."),
77 SESSION_NOT_CONNECTED: k("SSH relace byla odpojena. Připojte se znovu v hlavním okně."),
78 REMOTE_COMMAND_TIMEOUT: k("Server neodpověděl včas."),
79 REMOTE_COMMAND_FAILED: k("Vzdálený příkaz se nepodařilo spustit."),
80 REMOTE_OUTPUT_TOO_LARGE: k("Odpověď serveru je příliš velká."),
81 PLUGIN_PERMISSION_DENIED: k("Modul nemá potřebné oprávnění."),
82 USERS_NAME_INVALID: k("Neplatný název. Povolené jsou malé znaky a-z, číslice, _ a - (max. 32, nezačíná číslicí ani pomlčkou)."),
83 USERS_GROUP_INVALID: k("Neplatný název skupiny."),
84 USERS_SHELL_INVALID: k("Neplatný shell (musí existovat a být v /etc/shells, případně nologin/false)."),
85 USERS_HOME_INVALID: k("Neplatný domovský adresář. Použijte absolutní cestu mimo systémové adresáře."),
86 USERS_HOME_EXISTS: k("Cílový domovský adresář už existuje."),
87 USERS_COMMENT_INVALID: k("Celé jméno může mít nejvýše 100 znaků bez dvojtečky a řídicích znaků."),
88 USERS_PASSWORD_INVALID: k("Heslo musí mít 1–256 znaků a nesmí obsahovat zalomení řádku."),
89 USERS_USER_EXISTS: k("Účet s tímto jménem už existuje."),
90 USERS_GROUP_EXISTS: k("Skupina se stejným názvem už existuje; zvolte primární skupinu."),
91 USERS_USER_NOT_FOUND: k("Účet na serveru neexistuje."),
92 USERS_GROUP_NOT_FOUND: k("Skupina na serveru neexistuje."),
93 USERS_GROUP_NOT_MEMBER: k("Účet není doplňkovým členem skupiny."),
94 USERS_GROUP_PRIMARY: k("Primární skupinu nelze odebrat; nejprve změňte primární skupinu."),
95 USERS_PROTECTED_USER: k("Účet je chráněný."),
96 USERS_LAST_ADMIN: k("Jde o posledního funkčního administrátora; změna byla odmítnuta."),
97 USERS_USER_HAS_PROCESSES: k("Účet má spuštěné procesy. Ukončete je a zkuste to znovu."),
98 USERS_TOOL_MISSING: k("Na serveru chybí potřebný systémový nástroj."),
99 USERS_SUDO_GROUP_UNAVAILABLE: k("Na serveru není skupina sudo/wheel/admin, na kterou by odkazoval sudoers. CH-J sudoers nemění."),
100 USERS_USERADD_FAILED: k("useradd selhal:"),
101 USERS_USERMOD_FAILED: k("usermod selhal:"),
102 USERS_USERDEL_FAILED: k("userdel selhal:"),
103 USERS_PASSWORD_FAILED: k("Nastavení hesla selhalo:"),
104 USERS_GROUP_CHANGE_FAILED: k("Změna skupiny selhala:"),
105 USERS_KEY_INVALID: k("Neplatný veřejný klíč. Vložte přesně jeden klíč OpenSSH bez voleb."),
106 USERS_KEY_PRIVATE: k("Vložili jste soukromý klíč! Na server se smí nahrát jen veřejný klíč (.pub)."),
107 USERS_KEY_TYPE_UNSUPPORTED: k("Přidat lze jen klíče Ed25519."),
108 USERS_KEY_DUPLICATE: k("Tento klíč už je pro účet povolený."),
109 USERS_KEY_NOT_FOUND: k("Klíč se mezitím změnil nebo neexistuje. Obnovte seznam."),
110 USERS_KEY_IN_USE: k("Tento klíč používá aktivní relace CH-J, proto nebyl odebrán."),
111 USERS_KEY_LAST: k("Poslední klíč účtu aktivní relace nelze odebrat."),
112 USERS_KEYS_FILE_UNMANAGEABLE: k("Soubor authorized_keys nelze bezpečně upravit:"),
113 USERS_KEYS_FILE_TOO_LARGE: k("Soubor authorized_keys by přesáhl 256 KiB."),
114 USERS_KEYS_SYMLINK: k("authorized_keys nebo .ssh je symbolický odkaz; soubor nebyl změněn."),
115 USERS_KEYS_CHANGED: k("Soubor s klíči se mezitím změnil. Obnovte seznam a zkuste to znovu."),
116 USERS_KEYS_PATH_UNSUPPORTED: k("Cesta k authorized_keys mimo domovský adresář není podporovaná."),
117 USERS_SSH_POLICY_INVALID: k("Neplatné nastavení SSH."),
118 USERS_SSH_LAYOUT_UNSUPPORTED: k("sshd_config nenačítá sshd_config.d před prvním blokem Match. CH-J hlavní konfiguraci neupravuje."),
119 USERS_SSH_MANAGED_FILE_MODIFIED: k("Soubor spravovaný CH-J byl ručně upraven; CH-J ho nepřepíše."),
120 USERS_SSH_MANAGED_FILE_UNSAFE: k("Soubor spravovaný CH-J není běžný čitelný soubor."),
121 USERS_SSH_CONFIG_ALREADY_INVALID: k("Současná konfigurace SSH je neplatná, změna nebyla provedena:"),
122 USERS_SSH_CONFIG_INVALID: k("Nová konfigurace neprošla sshd -t a nebyla použita:"),
123 USERS_SSH_POLICY_NOT_EFFECTIVE: k("Nastavení by se neprojevilo kvůli existujícím pravidlům s vyšší prioritou; vráceno zpět."),
124 USERS_SSH_SESSION_USES_PASSWORD: k("Jste připojeni heslem. Připojte se SSH klíčem a zkuste to znovu."),
125 USERS_SSH_LOCKOUT_PREVENTED: k("Změna byla zablokována, aby nepřerušila přístup aktivní relace:"),
126 USERS_SSH_PROBE_FAILED: k("Nové testovací přihlášení selhalo; konfigurace byla vrácena:"),
127 USERS_SSH_PROBE_CREDENTIALS_REQUIRED: k("Pro ověření nového přihlášení je potřeba heslo nebo passphrase profilu."),
128 USERS_SSH_RELOAD_FAILED: k("Reload SSH služby selhal; konfigurace byla vrácena:"),
129 USERS_SSH_POLICY_CHANGED: k("Spravovaná SSH konfigurace se mezitím změnila. Obnovte detail a zkuste to znovu."),
130 USERS_SSH_CLIENT_UNKNOWN: k("Adresu klienta SSH relace nelze zjistit, ochranu proti odříznutí proto nelze ověřit."),
131 USERS_SSHD_MISSING: k("Na serveru nebyl nalezen OpenSSH server (sshd)."),
132 USERS_PRIVILEGE_REQUIRED: k("Operace vyžaduje oprávnění root."),
133 USERS_ROOT_PASSWORD_CONFIRMATION_REQUIRED: k("Povolení hesla pro root vyžaduje dodatečné potvrzení."),
134 USERS_NAME_INVALID_WINDOWS: k("Neplatný název účtu Windows: písmena, číslice, mezera, tečka, _ nebo - (max. 20), nesmí končit tečkou ani mezerou."),
135 USERS_SID_INVALID: k("Neplatný identifikátor účtu (SID)."),
136 USERS_WINDOWS_ELEVATION_REQUIRED: k("Operace vyžaduje SSH relaci správce se zvýšeným tokenem. Windows nemá sudo a CH-J neobchází UAC:"),
137 USERS_ACCESS_DENIED: k("Windows operaci zamítl (přístup odepřen bezpečnostní politikou):"),
138 USERS_POWERSHELL_UNAVAILABLE: k("Na serveru nelze spustit Windows PowerShell (powershell.exe)."),
139 USERS_POWERSHELL_RESTRICTED: k("PowerShell je omezen politikou (Constrained Language Mode / blokování skriptů); CH-J ji neobchází."),
140 USERS_LOCALACCOUNTS_UNAVAILABLE: k("Modul Microsoft.PowerShell.LocalAccounts není v PowerShellu na serveru dostupný."),
141 USERS_DOMAIN_READ_ONLY: k("Doménové a cloudové identity jsou jen pro čtení."),
142 USERS_NOT_SUPPORTED_ON_PLATFORM: k("Tato operace na Windows neexistuje (shell, HOME, primární skupina, zamknutí hesla)."),
143 USERS_PASSWORD_POLICY: k("Heslo nesplňuje politiku hesel Windows."),
144 USERS_PROFILE_MISSING: k("Profil účtu zatím neexistuje (účet se ještě nepřihlásil), soubor klíčů nelze vytvořit."),
145 USERS_SSH_NAME_UNSUPPORTED: k("Název účtu nelze bezpečně použít v sshd_config."),
146 USERS_SSH_APPLY_IN_PROGRESS: k("Jiná změna SSH konfigurace ještě probíhá nebo čeká na potvrzení."),
147 USERS_SSH_APPLY_NOT_STARTED: k("Úloha s automatickým rollbackem se nespustila; nic nebylo změněno."),
148 USERS_SSH_ROLLED_BACK: k("Změna SSH nebyla ponechána, původní konfigurace byla obnovena:"),
149 USERS_SSHD_QUERY_FAILED: k("Vyhodnocení sshd -T pod účtem SYSTEM se nepodařilo."),
150 USERS_PLATFORM_UNSUPPORTED: k("Operační systém serveru nelze rozpoznat nebo není podporován:"),
151 USERS_GROUP_MEMBER_EXISTS: k("Účet už je členem skupiny."),
152 USERS_PROFILE_IN_USE: k("Profil účtu je stále načten (uživatel je přihlášen nebo ho Windows po SSH přihlášení ještě neuvolnil). Zkuste to později, nebo odstraňte účet bez profilu."),
153 USERS_WINDOWS_OPERATION_FAILED: k("Operace Windows selhala:")
154};
155const DETAIL_CODES = new Set(["USERS_USERADD_FAILED", "USERS_USERMOD_FAILED", "USERS_USERDEL_FAILED", "USERS_PASSWORD_FAILED", "USERS_GROUP_CHANGE_FAILED",
156 "USERS_KEYS_FILE_UNMANAGEABLE", "USERS_SSH_CONFIG_ALREADY_INVALID", "USERS_SSH_CONFIG_INVALID", "USERS_SSH_POLICY_NOT_EFFECTIVE", "USERS_SSH_LOCKOUT_PREVENTED",
157 "USERS_SSH_PROBE_FAILED", "USERS_SSH_RELOAD_FAILED", "USERS_PROTECTED_USER", "USERS_TOOL_MISSING", "USERS_WINDOWS_ELEVATION_REQUIRED", "USERS_ACCESS_DENIED",
158 "USERS_SSH_ROLLED_BACK", "USERS_PLATFORM_UNSUPPORTED", "USERS_WINDOWS_OPERATION_FAILED", "USERS_PASSWORD_POLICY"]);
159const POLICY_ROWS = [
160 { key: "access", label: k("SSH přístup"), hint: k("Úplné povolení nebo zákaz SSH bez zrušení systémového účtu."), options: [["allow", k("Povolit")], ["deny", k("Zakázat")]] },
161 { key: "password", label: k("Přihlášení heslem"), hint: k("Dědit = platí globální nastavení serveru."), options: [["inherit", k("Dědit")], ["yes", k("Povolit")], ["no", k("Zakázat")]] },
162 { key: "pubkey", label: k("Přihlášení veřejným klíčem"), hint: k("Zákaz zablokuje i Ed25519 klíče."), options: [["inherit", k("Dědit")], ["yes", k("Povolit")], ["no", k("Zakázat")]] },
163 { key: "ipv4", label: k("Přístup z IPv4"), hint: k("Podle IP rodiny zdrojové adresy klienta."), options: [["allow", k("Povolit")], ["deny", k("Zakázat")]] },
164 { key: "ipv6", label: k("Přístup z IPv6"), hint: k("Podle IP rodiny zdrojové adresy klienta."), options: [["allow", k("Povolit")], ["deny", k("Zakázat")]] }
165];
166const ROOT_LOGIN_ROW = { key: "permitRootLogin", label: "PermitRootLogin (root)", hint: k("Doporučeno: Jen klíč (prohibit-password). „Povolit“ dovolí i heslo podle ostatních pravidel."),
167 options: [["inherit", k("Dědit")], ["no", k("Zakázat")], ["prohibit-password", k("Jen klíč (doporučeno)")], ["forced-commands-only", k("Klíč s vynuceným příkazem")], ["yes", k("Povolit")]] };
168const CANCELLED = "CHJ_UI_CANCELLED";
170// ---------- small DOM helpers (text only, never innerHTML with server data) ----------
171function h(tag, props = {}, children = []) {
172 const node = document.createElement(tag);
173 for (const [key, value] of Object.entries(props)) {
174 if (value === undefined || value === null || value === false) continue;
175 if (key === "className") node.className = value;
176 else if (key === "text") node.textContent = String(value);
177 else if (key === "dataset") Object.assign(node.dataset, value);
178 else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2), value);
179 else if (typeof value === "boolean") node[key] = value;
180 else node.setAttribute(key, String(value));
181 }
182 for (const child of [].concat(children)) {
183 if (child === null || child === undefined || child === false) continue;
184 node.append(child instanceof Node ? child : String(child));
185 }
186 return node;
188function badge(text, tone = "muted") { return h("span", { className: `badge ${tone}`, text }); }
189function setStatus(message, error = false) { el.status.textContent = message; el.status.classList.toggle("error", error); }
190function connected() { return Boolean(state.sessionId); }
191function isWindows() { return state.inventory?.platform === "windows" || state.privilege?.platform === "windows"; }
192// Windows accounts are addressed by SID; Linux accounts by name.
193function identFor(username) {
194 const user = state.inventory?.users.find((candidate) => candidate.username === username) || (state.detail?.username === username ? state.detail : null);
195 return user?.sid && isWindows() ? { sid: user.sid, username } : { username };
198function parseError(error) {
199 const raw = String(error?.message || error || "");
200 const match = raw.match(/\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+): ([\s\S]*)$/);
201 if (match) return { code: match[1], detail: match[2].trim() };
202 return { code: error?.code || "UNKNOWN", detail: raw.replace(/^Error invoking remote method '[^']+': (?:\w*Error: )?/, "") };
204function describeError(error) {
205 const { code, detail } = parseError(error);
206 if (code === CANCELLED) return t("Operace byla zrušena.");
207 const base = ERROR_TEXT[code] && t(ERROR_TEXT[code]);
208 if (!base) return detail || code;
209 return DETAIL_CODES.has(code) && detail ? `${base} ${detail}` : base;
211function cancelled() { const error = new Error(`${CANCELLED}: cancelled`); error.code = CANCELLED; return error; }
213// ---------- dialogs ----------
214function runDialog(dialog, { init, submit }) {
215 return new Promise((resolve) => {
216 const form = dialog.querySelector("form");
217 let finished = false;
218 const finish = (value) => {
219 if (finished) return;
220 finished = true;
221 form.removeEventListener("submit", onSubmit);
222 dialog.removeEventListener("close", onClose);
223 for (const button of dialog.querySelectorAll("[data-close]")) button.removeEventListener("click", onClose);
224 if (dialog.open) dialog.close();
225 resolve(value);
226 };
227 const onSubmit = async (event) => {
228 event.preventDefault();
229 const value = await submit();
230 if (value !== undefined) finish(value);
231 };
232 const onClose = () => finish(null);
233 form.addEventListener("submit", onSubmit);
234 dialog.addEventListener("close", onClose);
235 for (const button of dialog.querySelectorAll("[data-close]")) button.addEventListener("click", onClose);
236 init?.();
237 dialog.showModal();
238 });
240function showError(node, message) { node.textContent = message || ""; node.hidden = !message; }
242function askSudo(label, error) {
243 return runDialog(el.sudoDialog, {
244 init: () => {
245 el.sudoMessage.textContent = t("Operace „{label}“ vyžaduje oprávnění správce.", { label });
246 el.sudoUser.textContent = state.privilege?.username || "";
247 el.sudoPassword.value = "";
248 showError(el.sudoError, error);
249 setTimeout(() => el.sudoPassword.focus(), 0);
250 },
251 submit: () => { const value = el.sudoPassword.value; el.sudoPassword.value = ""; return value || undefined; }
252 }).finally(() => { el.sudoPassword.value = ""; });
255function confirmAction({ title, message, details = [], confirmLabel = t("Potvrdit"), danger = false, typed = "", option = "" }) {
256 return runDialog(el.confirmDialog, {
257 init: () => {
258 el.confirmTitle.textContent = `${title} — ${api.sessions.displayName(state.sessions.find((session) => session.sessionId === state.sessionId))}`;
259 el.confirmMessage.textContent = message;
260 el.confirmDetails.replaceChildren(...details.map((item) => h("li", { text: item })));
261 el.confirmDetails.hidden = !details.length;
262 el.confirmTypedLabel.hidden = !typed;
263 el.confirmTypedValue.textContent = typed;
264 el.confirmTyped.value = "";
265 el.confirmOptionLabel.hidden = !option;
266 el.confirmOptionText.textContent = option;
267 el.confirmOption.checked = false;
268 el.confirmSubmit.textContent = confirmLabel;
269 el.confirmSubmit.className = `button ${danger ? "danger" : "primary"}`;
270 },
271 submit: () => {
272 if (typed && el.confirmTyped.value.trim() !== typed) { el.confirmTyped.focus(); return undefined; }
273 return { option: el.confirmOption.checked };
274 }
275 });
278function askValue({ title, message = "", label, value = "", type = "text", label2 = "", option = "", validate }) {
279 return runDialog(el.valueDialog, {
280 init: () => {
281 el.valueTitle.textContent = `${title} — ${api.sessions.displayName(state.sessions.find((session) => session.sessionId === state.sessionId))}`;
282 el.valueMessage.textContent = message;
283 el.valueLabelText.textContent = label;
284 el.valueInput.type = type; el.valueInput.value = value;
285 el.valueLabel2.hidden = !label2;
286 el.valueLabelText2.textContent = label2;
287 el.valueInput2.type = type; el.valueInput2.value = "";
288 el.valueOptionLabel.hidden = !option;
289 el.valueOptionText.textContent = option;
290 el.valueOption.checked = false;
291 showError(el.valueError, "");
292 setTimeout(() => el.valueInput.focus(), 0);
293 },
294 submit: () => {
295 const result = { value: el.valueInput.value, value2: el.valueInput2.value, option: el.valueOption.checked };
296 const problem = validate?.(result);
297 if (problem) { showError(el.valueError, problem); return undefined; }
298 return result;
299 }
300 }).finally(() => { el.valueInput.value = ""; el.valueInput2.value = ""; });
303function askProbeSecret(missing) {
304 const passphrase = missing === "passphrase";
305 return runDialog(el.probeDialog, {
306 init: () => {
307 el.probeLabel.textContent = passphrase ? t("Passphrase soukromého klíče profilu") : t("Heslo SSH profilu");
308 el.probeSecret.value = "";
309 setTimeout(() => el.probeSecret.focus(), 0);
310 },
311 submit: () => { const value = el.probeSecret.value; el.probeSecret.value = ""; return value ? (passphrase ? { passphrase: value } : { password: value }) : undefined; }
312 });
315// Calls Core without a password first. Only if Core reports that sudo needs one, the dialog is shown.
316async function withSudo(label, invoke) {
317 const requestGeneration = state.generation;
318 try {
319 let password = "";
320 let attempts = 0;
321 try {
322 for (;;) {
323 try { return await sessionResult(invoke(password ? { sudoPassword: password } : {}), requestGeneration); }
324 catch (error) { if (requestGeneration !== state.generation) return;
325 const { code } = parseError(error);
326 if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && attempts < 3) {
327 attempts += 1;
328 password = await sessionResult(askSudo(label, code === "SUDO_PASSWORD_INCORRECT" && password ? t("Heslo bylo odmítnuto. Zkuste to znovu.") : ""), requestGeneration);
329 if (!password) throw cancelled();
330 continue;
331 }
332 if (code === "SUDO_NOT_PERMITTED" || code === "SUDO_UNAVAILABLE") void refreshPrivilege();
333 if (code === "SESSION_NOT_CONNECTED") void loadSessions();
334 throw error;
335 }
336 }
337 } finally { password = ""; }
338 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
341// ---------- sessions and privileges ----------
342async function loadSessions() {
343 if (sessionsLoading) return;
344 sessionsLoading = true;
345 try {
346 state.sessions = (await api.sessions.list()).filter((session) => session.state === "connected");
347 } catch (error) {
348 sessionsLoading = false;
349 setStatus(t("Relace nelze načíst: {error}", { error: describeError(error) }), true);
350 return;
351 }
352 sessionsLoading = false;
353 const previous = state.sessionId;
354 el.sessionSelect.replaceChildren(new Option(state.sessions.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""));
355 for (const session of state.sessions) el.sessionSelect.add(new Option(api.sessions.displayName(session), session.sessionId));
356 const { id: next, reason } = api.sessions.reconcile(endedSessionId || previous, state.sessions);
357 if (reason === "gone") endedSessionId ||= previous;
358 el.sessionSelect.value = next;
359 if (reason === "choose") api.sessions.offerChoice(el.sessionSelect, state.sessions, t("Vyberte relaci…"));
360 if (next !== previous) {
361 const selecting = selectSession(next);
362 const token = state.generation;
363 await selecting;
364 if (token !== state.generation) return;
365 }
366 else updateControls();
367 if (reason === "gone") setStatus(t("Relace skončila. Vyberte server."), true);
368 else if (!state.sessions.length) setStatus(t("Nejprve se v hlavním okně připojte k serveru přes SSH."));
371async function selectSession(sessionId) {
372 for (const dialog of document.querySelectorAll("dialog[open]")) dialog.close();
373 state.sessionId = sessionId;
374 state.generation += 1;
375 state.detailToken += 1;
376 Object.assign(state, { privilege: null, inventory: null, selected: null, detail: null, detailLoading: false, loading: false, mutating: false });
377 renderPrivilege(); renderUsers(); renderDetail(); updateControls();
378 if (!sessionId) return;
379 await refreshPrivilege();
380 await loadInventory();
383async function refreshPrivilege() {
384 const requestGeneration = state.generation;
385 const token = state.generation;
386 if (!connected()) return;
387 try {
388 const privilege = await sessionResult(api.users.privileges(state.sessionId, true), requestGeneration);
389 if (token !== state.generation) return;
390 state.privilege = privilege;
391 } catch (error) { if (requestGeneration !== state.generation) return;
392 if (token === state.generation) setStatus(t("Oprávnění nelze zjistit: {error}", { error: describeError(error) }), true);
393 }
394 renderPrivilege(); updateControls();
397function renderPrivilege() {
398 const mode = state.privilege?.mode || "unknown";
399 const [text, tone] = PRIVILEGE_TEXT[mode] || PRIVILEGE_TEXT.unknown;
400 const suffix = mode === "sudo-password" && !state.privilege?.verified ? ` (${t("neověřeno")})` : "";
401 el.privilegeBadge.textContent = state.privilege ? `${state.privilege.username}: ${t(text)}${suffix}` : t("neověřeno");
402 el.privilegeBadge.className = `badge ${tone}`;
403 if (mode.startsWith("windows-")) {
404 el.privilegeBadge.title = mode === "windows-elevated" ? t("Relace má zvýšený token; administrativní operace jsou dostupné.") : t("Windows nemá sudo. Administrativní operace vyžadují zvýšený token správce; CH-J neobchází UAC.");
405 return;
406 }
407 el.privilegeBadge.title = mode === "none" ? t("Účet nemá sudo. Modul funguje v režimu čtení.") : mode === "sudo-password" ? t("Administrativní operace si vyžádají heslo pro sudo.") : "";
410function updateControls() {
411 const busy = state.loading || state.mutating;
412 el.sessionSelect.disabled = state.mutating;
413 el.refreshSessionsButton.disabled = state.mutating;
414 el.reloadButton.disabled = !connected() || busy;
415 const readOnlyWindows = isWindows() && state.privilege?.mode !== "windows-elevated";
416 el.createButton.disabled = !connected() || busy || !state.inventory || state.privilege?.mode === "none" || readOnlyWindows;
417 el.sshPasswordOffButton.disabled = el.createButton.disabled;
418 el.sshPasswordOffButton.hidden = (state.inventory?.users || []).some((account) => account.platform === "windows");
419 const canElevate = !isWindows() && connected() && state.inventory && !state.inventory.privileged && ["sudo-password", "unknown"].includes(state.privilege?.mode || "unknown");
420 el.elevatedButton.hidden = !canElevate;
421 el.elevatedButton.disabled = busy;
422 for (const button of document.querySelectorAll("[data-mutation]")) button.disabled = busy || !connected();
425// ---------- inventory ----------
426async function loadInventory({ elevated = false } = {}) {
427 const requestGeneration = state.generation;
428 if (!connected()) return;
429 const token = state.generation;
430 const sessionId = state.sessionId;
431 state.loading = true; updateControls();
432 setStatus(elevated ? t("Načítám rozšířený stav účtů…") : t("Načítám účty…"));
433 try {
434 const run = (auth) => api.users.inventory(sessionId, { ...auth, privileged: elevated });
435 const inventory = elevated ? await sessionResult(withSudo(t("Načtení rozšířeného stavu účtů"), run), requestGeneration) : await sessionResult(run({}), requestGeneration);
436 if (token !== state.generation) return;
437 applyInventory(inventory);
438 const regular = inventory.users.filter((user) => !user.system).length;
439 setStatus(`${t("Načteno {count} účtů ({regular} běžných)", { count: inventory.users.length, regular })}${inventory.privileged ? "" : ` – ${t("stav hesel a sudo vyžaduje sudo")}`}.`);
440 } catch (error) { if (requestGeneration !== state.generation) return;
441 if (token === state.generation) setStatus(describeError(error), parseError(error).code !== CANCELLED);
442 } finally { if (requestGeneration === state.generation) {
443 if (token === state.generation) { state.loading = false; updateControls(); }
444 } }
447function applyInventory(inventory) {
448 state.inventory = inventory;
449 if (inventory.privilege) state.privilege = inventory.privilege;
450 if (state.selected && !inventory.users.some((user) => user.username === state.selected)) {
451 state.selected = null; state.detail = null; state.detailToken += 1;
452 } else if (state.detail) {
453 const user = inventory.users.find((candidate) => candidate.username === state.detail.username);
454 if (user) mergeAccount(state.detail, user, inventory.privileged);
455 }
456 renderPrivilege(); renderUsers(); renderDetail(); updateControls();
459function mergeAccount(detail, user, privileged) {
460 const account = detail.account;
461 Object.assign(account, { shell: user.shell, home: user.home, comment: user.comment, primaryGroup: user.primaryGroup, groups: user.groups, loginShell: user.loginShell, uid: user.uid, gid: user.gid });
462 if (privileged) Object.assign(account, { password: user.password, expired: user.expired, expireDays: user.expireDays, sudo: user.sudo });
465function filteredUsers() {
466 const users = state.inventory?.users || [];
467 const filter = el.filterSelect.value;
468 const query = el.searchInput.value.trim().toLowerCase();
469 return users.filter((user) => (filter === "all" || (filter === "system" ? user.system : !user.system))
470 && (!query || user.username.includes(query) || user.comment.toLowerCase().includes(query) || String(user.uid) === query));
473function policySummary(policy) {
474 if (!policy) return badge(t("dědí server"), "muted");
475 if (policy.access === "deny") return badge(t("SSH zakázáno"), "danger");
476 const parts = [];
477 if (policy.password !== "inherit") parts.push(policy.password === "no" ? t("heslo ne") : t("heslo ano"));
478 if (policy.pubkey !== "inherit") parts.push(policy.pubkey === "no" ? t("klíč ne") : t("klíč ano"));
479 if (policy.ipv4 === "deny") parts.push(t("bez IPv4"));
480 if (policy.ipv6 === "deny") parts.push(t("bez IPv6"));
481 return badge(parts.join(" · ") || t("dědí server"), parts.length ? "warn" : "muted");
484function accountState(user) {
485 if (user.platform === "windows") {
486 const items = [badge(user.enabled ? t("povolen") : t("zakázán"), user.enabled ? "ok" : "danger")];
487 if (user.source && user.source !== "Local") items.push(badge(user.source === "MicrosoftAccount" ? t("účet Microsoft") : user.source, "muted"));
488 if (user.passwordRequired === false && !user.passwordLastSet) items.push(badge(t("bez hesla"), "muted"));
489 return items;
490 }
491 const items = [];
492 if (user.expired) items.push(badge(t("deaktivován"), "danger"));
493 const [text, tone] = PASSWORD_TEXT[user.password] || PASSWORD_TEXT.unknown;
494 items.push(badge(t(text), tone));
495 if (!user.loginShell) items.push(badge(t("bez shellu"), "muted"));
496 return items;
499function renderTableHead() {
500 const labels = isWindows() ? [t("Účet"), "SID (RID)", t("Stav"), "Administrators", "SSH (CH-J)"] : [t("Účet"), "UID", t("Stav"), "Sudo", "SSH (CH-J)"];
501 el.usersBody.closest("table").querySelector("thead").replaceChildren(h("tr", {}, labels.map((text) => h("th", { scope: "col", text }))));
504function renderUsers() {
505 renderTableHead();
506 const users = filteredUsers();
507 if (!state.inventory) {
508 el.usersBody.replaceChildren(h("tr", {}, h("td", { colspan: 5, className: "empty", text: connected() ? t("Načítám…") : t("Nejsou načtena žádná data.") })));
509 el.inventoryNote.textContent = "";
510 return;
511 }
512 if (!users.length) {
513 el.usersBody.replaceChildren(h("tr", {}, h("td", { colspan: 5, className: "empty", text: t("Žádný účet neodpovídá filtru.") })));
514 } else {
515 el.usersBody.replaceChildren(...users.map((user) => h("tr", { className: user.username === state.selected ? "selected" : "" }, [
516 h("td", {}, h("button", { type: "button", className: "row-button", dataset: { username: user.username }, "aria-pressed": String(user.username === state.selected) }, [
517 h("strong", { text: user.username }),
518 user.protection ? h("small", { text: user.protection === "session" ? ` (${t("tato relace")})` : user.protection === "root" ? " (root)" : user.protection === "builtin" ? ` (${t("vestavěný")})` : "" }) : null,
519 user.comment ? h("span", { className: "sub", text: user.comment }) : null
520 ])),
521 h("td", { title: user.sid || "", text: user.platform === "windows" ? `…-${user.rid}` : String(user.uid) }),
522 h("td", {}, h("div", { className: "badges" }, accountState(user))),
523 user.platform === "windows" ? h("td", { text: user.admin ? t("člen") : "—" }) : h("td", { text: user.sudo === "unknown" ? (user.adminGroups.length ? t("skupina {groups}", { groups: user.adminGroups.join(", ") }) : "—") : SUDO_TEXT[user.sudo] ? t(SUDO_TEXT[user.sudo]) : user.sudo }),
524 h("td", {}, policySummary(user.sshPolicy))
525 ])));
526 }
527 const managed = state.inventory.managedPolicy;
528 const notes = [];
529 if (isWindows()) {
530 if (!state.inventory.privileged) notes.push(t("Režim čtení: správa účtů, SSH a cizích klíčů vyžaduje SSH relaci správce se zvýšeným tokenem (Windows nemá sudo)."));
531 if (state.inventory.server.serverCore) notes.push(t("Windows Server Core (bez GUI)."));
532 if (state.inventory.server.partOfDomain) notes.push(t("Počítač je v doméně {domain}; doménové identity jsou jen pro čtení ({count} v lokálních skupinách).", { domain: state.inventory.server.domain, count: state.inventory.externalIdentities.length }));
533 } else if (!state.inventory.privileged) notes.push(t("Režim čtení: stav hesel, expirace a skutečná sudo práva vyžadují sudo."));
534 if (state.inventory.server.sshLayout !== "ok" && isWindows()) notes.push(t("SSH politiku CH-J nelze v tomto sshd_config bezpečně spravovat (viz Diagnostika)."));
535 else if (state.inventory.server.sshLayout !== "ok") notes.push(t("SSH politiku CH-J nelze na tomto serveru spravovat (sshd_config nenačítá sshd_config.d před Match)."));
536 if (managed.state === "present" && !managed.consistent) notes.push(t("Soubor SSH politiky CH-J byl ručně upraven – CH-J ho nepřepíše."));
537 if (state.inventory.server.os) notes.push(t("Server: {os}.", { os: state.inventory.server.os }));
538 el.inventoryNote.textContent = notes.join(" ");
541// ---------- detail ----------
542async function loadDetail(username, { elevated = false } = {}) {
543 const requestGeneration = state.generation;
544 if (!connected()) return;
545 const token = state.generation;
546 const detailToken = ++state.detailToken;
547 const sessionId = state.sessionId;
548 if (state.selected !== username) { state.detail = null; state.tab = state.tab || "account"; }
549 state.selected = username;
550 state.detailLoading = true;
551 renderUsers(); renderDetail();
552 try {
553 const run = (auth) => api.users.detail(sessionId, username, { ...auth, ...identFor(username), privileged: elevated });
554 const detail = elevated ? await sessionResult(withSudo(t("Detail účtu {username}", { username }), run), requestGeneration) : await sessionResult(run({}), requestGeneration);
555 if (token !== state.generation || detailToken !== state.detailToken) return;
556 state.detail = detail;
557 setStatus(detail.privileged ? t("Detail účtu {username} načten včetně údajů vyžadujících sudo.", { username }) : t("Detail účtu {username} načten (režim čtení).", { username }));
558 } catch (error) { if (requestGeneration !== state.generation) return;
559 if (token !== state.generation || detailToken !== state.detailToken) return;
560 setStatus(describeError(error), parseError(error).code !== CANCELLED);
561 } finally { if (requestGeneration === state.generation) {
562 if (token === state.generation && detailToken === state.detailToken) { state.detailLoading = false; renderDetail(); updateControls(); }
563 } }
566function renderDetail() {
567 const detail = state.detail;
568 el.detailEmpty.hidden = Boolean(state.selected);
569 el.detailContent.hidden = !state.selected;
570 el.detailEmpty.textContent = connected() ? t("Vyberte účet v seznamu.") : t("Nejprve vyberte připojenou relaci.");
571 if (!state.selected) return;
572 el.detailTitle.textContent = state.selected;
573 if (!detail) {
574 el.detailSubtitle.textContent = state.detailLoading ? t("Načítám detail…") : t("Detail se nepodařilo načíst.");
575 el.detailBadges.replaceChildren();
576 el.detailNotice.hidden = true;
577 for (const panel of ["account", "groups", "ssh", "keys", "diagnostics"]) el[`panel-${panel}`].replaceChildren();
578 return;
579 }
580 const account = detail.account;
581 el.detailSubtitle.textContent = (account.platform === "windows" ? [account.fullName, account.sid, account.profilePath] : [account.comment, `UID ${account.uid}`, account.home]).filter(Boolean).join(" · ") + (state.detailLoading ? ` · ${t("obnovuji…")}` : "");
582 el["tab-groups"].textContent = account.platform === "windows" ? t("Skupiny a správci") : t("Skupiny a sudo");
583 el.detailBadges.replaceChildren(...[
584 account.protection ? badge(account.protection === "session" ? t("aktivní relace") : account.protection === "root" ? "root" : account.protection === "builtin" ? t("vestavěný") : t("systémový"), "warn") : null,
585 badge(detail.privileged ? (detail.platform === "windows" ? t("ověřeno se zvýšeným tokenem") : t("ověřeno se sudo")) : t("režim čtení"), detail.privileged ? "ok" : "muted"),
586 ...accountState(account)
587 ].filter(Boolean));
588 const notices = [];
589 if (account.protection) notices.push(t(PROTECTION_TEXT[account.protection]));
590 if (!detail.privileged) notices.push(detail.platform === "windows"
591 ? t("Skutečná SSH konfigurace (sshd -T), klíče jiných účtů a změny vyžadují SSH relaci správce se zvýšeným tokenem. Windows nemá sudo a CH-J neobchází UAC.")
592 : t("Část údajů (stav hesla, sudo, skutečná SSH konfigurace, klíče jiných účtů) vyžaduje sudo."));
593 el.detailNotice.replaceChildren(...notices.map((text) => h("p", { text })));
594 if (!detail.privileged && detail.platform !== "windows") el.detailNotice.append(h("button", { type: "button", className: "button", text: t("Načíst údaje se sudo"), onclick: () => loadDetail(detail.username, { elevated: true }) }));
595 el.detailNotice.hidden = !notices.length;
596 renderAccountTab(detail); renderGroupsTab(detail); renderSshTab(detail); renderKeysTab(detail); renderDiagnosticsTab(detail);
597 selectTab(state.tab, false);
598 updateControls();
601function infoGrid(rows) {
602 return h("dl", { className: "info-grid" }, rows.flatMap(([label, value]) => [h("dt", { text: label }), h("dd", {}, value instanceof Node ? value : String(value ?? "—"))]));
604function actionButton(label, onClick, { danger = false, sudo = true, title = "" } = {}) {
605 return h("button", { type: "button", className: `button${danger ? " danger" : ""}`, dataset: { mutation: "1" }, title: title || (sudo ? t("Vyžaduje oprávnění správce (sudo)") : ""), onclick: onClick }, [label, sudo ? h("span", { className: "sudo-mark", text: " sudo" }) : null]);
607function section(title, children, hint = "") {
608 return h("section", { className: "sub-panel" }, [h("h4", { text: title }), hint ? h("p", { className: "hint", text: hint }) : null, ...[].concat(children)]);
611function renderAccountTab(detail) {
612 if (detail.platform === "windows") { renderWindowsAccountTab(detail); return; }
613 const account = detail.account;
614 const [passwordText] = PASSWORD_TEXT[account.password] || PASSWORD_TEXT.unknown;
615 const expiry = account.expired ? t("deaktivován (expirovaný účet)") : account.expireDays ? t("platný do {date}", { date: new Date(account.expireDays * 86400000).toISOString().slice(0, 10) }) : account.password === "unknown" ? t("neznámý (vyžaduje sudo)") : t("aktivní");
616 const children = [infoGrid([
617 [t("Uživatelské jméno"), account.username],
618 [t("Celé jméno"), account.comment || "—"],
619 ["UID / GID", `${account.uid} / ${account.gid}`],
620 [t("Primární skupina"), account.primaryGroup],
621 [t("Domovský adresář"), `${account.home}${account.homeMeta && account.homeMeta !== "absent" ? ` (${account.homeMeta})` : account.homeMeta === "absent" ? ` (${t("neexistuje")})` : ""}`],
622 ["Shell", `${account.shell}${account.loginShell ? "" : ` – ${t("interaktivní přihlášení vypnuto")}`}`],
623 [t("Heslo"), t(passwordText)],
624 [t("Stav účtu"), expiry],
625 [t("Typ"), account.uid === 0 ? "root" : account.system ? t("systémový účet") : t("běžný účet")],
626 [t("Spuštěné procesy"), account.processes === null ? "—" : String(account.processes)]
627 ]), h("p", { className: "hint", text: t("Zamknutí hesla (passwd -l) nebrání přihlášení SSH klíčem. Úplné zablokování: Deaktivovat účet, nebo SSH přístup → Zakázat.") })];
628 if (!account.protection) {
629 const actions = [actionButton(t("Změnit heslo…"), () => changePassword(account.username))];
630 if (account.password === "locked") actions.push(actionButton(t("Odemknout heslo"), () => simpleAction("unlock", account.username, t("Odemknutí hesla"))));
631 else if (account.password === "set") actions.push(actionButton(t("Zamknout heslo"), () => simpleAction("lock", account.username, t("Zamknutí hesla"))));
632 actions.push(account.expired
633 ? actionButton(t("Aktivovat účet"), () => simpleAction("unexpire", account.username, t("Aktivace účtu")))
634 : actionButton(t("Deaktivovat účet"), () => simpleAction("expire", account.username, t("Deaktivace účtu"), t("Účet se nebude moci přihlásit žádnou metodou (ani SSH klíčem). Data zůstanou zachována."))));
635 actions.push(actionButton(t("Upravit celé jméno…"), () => changeComment(account)));
636 actions.push(actionButton(t("Změnit domovský adresář…"), () => changeHome(account)));
637 actions.push(actionButton(t("Odstranit účet…"), () => deleteAccount(account), { danger: true }));
638 children.push(h("div", { className: "inline-actions" }, actions));
639 const shells = [...new Set([...(state.inventory?.shells || []), "/usr/sbin/nologin", "/bin/false", account.shell])].filter(Boolean);
640 const shellSelect = h("select", { "aria-label": "Shell" }, shells.map((shell) => h("option", { value: shell, text: shell, selected: shell === account.shell })));
641 children.push(section("Shell", h("div", { className: "inline-actions" }, [shellSelect, actionButton(t("Uložit shell"), () => {
642 if (shellSelect.value === account.shell) return;
643 void runMutation(t("Změna shellu"), (sessionId, auth) => api.users.manage(sessionId, { action: "setShell", ...identFor(account.username), shell: shellSelect.value, ...auth }));
644 })]), t("nologin/false zablokuje interaktivní přihlášení, ale ne nutně všechny SSH funkce (např. přesměrování portů).")));
645 }
646 el["panel-account"].replaceChildren(...children);
649function renderGroupsTab(detail) {
650 if (detail.platform === "windows") { renderWindowsGroupsTab(detail); return; }
651 const account = detail.account;
652 const groups = state.inventory?.groups || [];
653 const editable = !account.protection;
654 const children = [];
655 const memberRows = account.groups.length
656 ? account.groups.map((group) => h("li", {}, [h("span", { text: group }), editable && group !== account.primaryGroup ? h("button", { type: "button", className: "mini", dataset: { mutation: "1" }, text: t("Odebrat"), onclick: () => removeGroup(account.username, group) }) : null]))
657 : [h("li", { className: "muted", text: t("Žádné doplňkové skupiny.") })];
658 children.push(section(t("Skupiny"), [
659 infoGrid([[t("Primární skupina"), account.primaryGroup]]),
660 h("ul", { className: "chip-list" }, memberRows)
661 ]));
662 if (editable && groups.length) {
663 const candidates = groups.filter((group) => !account.groups.includes(group.name) && group.name !== account.primaryGroup).sort((left, right) => left.name.localeCompare(right.name));
664 const addSelect = h("select", { "aria-label": t("Přidat do skupiny") }, candidates.map((group) => h("option", { value: group.name, text: `${group.name} (${group.gid})` })));
665 const primarySelect = h("select", { "aria-label": t("Primární skupina") }, groups.slice().sort((left, right) => left.name.localeCompare(right.name)).map((group) => h("option", { value: group.name, text: `${group.name} (${group.gid})`, selected: group.name === account.primaryGroup })));
666 children.push(section(t("Změnit členství"), [
667 h("div", { className: "inline-actions" }, [addSelect, actionButton(t("Přidat do skupiny"), () => addGroup(account.username, addSelect.value))]),
668 h("div", { className: "inline-actions" }, [primarySelect, actionButton(t("Nastavit primární skupinu"), () => setPrimary(account.username, primarySelect.value, account.primaryGroup))])
669 ], t("Ostatní skupiny a nesouvisející oprávnění zůstávají zachované.")));
670 }
671 const sudoState = account.sudo || "unknown";
672 const adminGroups = state.inventory?.adminGroups || [];
673 const sudoChildren = [infoGrid([
674 [t("Skutečný stav (sudo -l)"), sudoState === "unknown" ? t("neznámý – vyžaduje sudo") : SUDO_TEXT[sudoState] ? t(SUDO_TEXT[sudoState]) : sudoState],
675 [t("Členství ve skupinách správců"), ["sudo", "wheel", "admin"].filter((name) => account.groups.includes(name) || account.primaryGroup === name).join(", ") || t("žádné")],
676 [t("Skupiny s pravidlem v sudoers"), adminGroups.length ? adminGroups.join(", ") : state.inventory?.privileged ? t("žádná") : t("neznámé (vyžaduje sudo)")]
677 ]), h("p", { className: "hint", text: t("Členství ve skupině samo nezaručuje funkční sudo. CH-J nikdy nemění sudoers a nenastavuje NOPASSWD.") })];
678 if (editable) {
679 sudoChildren.push(h("div", { className: "inline-actions" }, [
680 actionButton(t("Přidat sudo"), () => simpleAction("grantAdmin", account.username, t("Přidání sudo"), t("Účet bude přidán do skupiny správců, na kterou odkazuje sudoers."))),
681 actionButton(t("Odebrat sudo"), () => simpleAction("revokeAdmin", account.username, t("Odebrání sudo"), t("Účet bude odebrán ze skupin sudo/wheel/admin. Pravidla sudoers pro konkrétní účet zůstanou beze změny.")), { danger: true })
682 ]));
683 }
684 children.push(section("Sudo", sudoChildren));
685 el["panel-groups"].replaceChildren(...children);
688function effectiveCell(detail, family, key) {
689 const effective = detail.ssh.effective[family];
690 if (!effective) return h("span", { className: "muted", text: detail.privileged ? t("nedostupné") : t("vyžaduje sudo") });
691 const yesNo = (value) => value === "yes" ? badge(t("povoleno"), "ok") : value === "no" ? badge(t("zakázáno"), "danger") : badge(value, "muted");
692 if (key === "password") return yesNo(effective.methods.password ? "yes" : "no");
693 if (key === "pubkey") return yesNo(effective.methods.publickey ? "yes" : "no");
694 const listener = detail.ssh.effective.listener;
695 if (listener && listener[family] === false) return badge(t("sshd nenaslouchá"), "danger");
696 if (effective.allowed === true) return badge(t("povoleno"), "ok");
697 if (effective.allowed === null) return badge(t("nejisté"), "warn");
698 const managedOnly = effective.reasons.every((reason) => reason.managed);
699 return h("span", { title: effective.reasons.map((reason) => `${reason.rule} ${reason.entry}`).join(", ") }, badge(managedOnly ? t("zakázáno (CH-J)") : t("zakázáno (pravidla serveru)"), "danger"));
702function renderSshTab(detail) {
703 const ssh = detail.ssh;
704 const account = detail.account;
705 const children = [];
706 if (!ssh.available && detail.privileged) {
707 el["panel-ssh"].replaceChildren(h("p", { className: "notice", text: t("Na serveru nebyl nalezen OpenSSH server (sshd).") }));
708 return;
709 }
710 const windows = detail.platform === "windows";
711 const blocked = windows ? account.protection === "builtin" : account.system;
712 const rows = !windows && account.uid === 0 ? [...POLICY_ROWS, ROOT_LOGIN_ROW] : POLICY_ROWS;
713 const reason = blocked ? (windows ? t("SSH přístup vestavěných účtů Windows CH-J nespravuje.") : t("SSH přístup systémových účtů CH-J nespravuje."))
714 : !detail.privileged ? (windows ? t("Skutečný stav a změny SSH vyžadují SSH relaci správce se zvýšeným tokenem.") : t("Skutečný stav a změny SSH vyžadují sudo."))
715 : windows && !ssh.sshName ? t("Název účtu obsahuje znaky, které nelze bezpečně použít v sshd_config.")
716 : !ssh.layoutSupported ? (windows ? t("sshd_config nelze bezpečně upravit: {problems}", { problems: (ssh.layoutProblems || []).join("; ") }) : t("sshd_config nenačítá sshd_config.d před prvním blokem Match – CH-J hlavní konfiguraci neupravuje."))
717 : ssh.managed.state === "present" && !ssh.managed.consistent ? t("{path} byl ručně upraven – CH-J ho nepřepíše.", { path: ssh.managed.path })
718 : ssh.syntaxOk === false ? t("Současná konfigurace SSH je neplatná: {message}", { message: ssh.syntaxMessage }) : "";
719 const form = h("form", { className: "policy-form" });
720 const table = h("table", { className: "policy-table" }, [
721 h("thead", {}, h("tr", {}, [h("th", { text: t("Nastavení") }), h("th", { text: t("Požadováno (CH-J)") }), h("th", { text: `${t("Skutečně IPv4")}${ssh.effective.ipv4?.sample ? ` (${t("vzorová adresa")})` : ""}` }), h("th", { text: `${t("Skutečně IPv6")}${ssh.effective.ipv6?.sample ? ` (${t("vzorová adresa")})` : ""}` })])),
722 h("tbody", {}, rows.map((row) => {
723 const current = ssh.policy[row.key];
724 const options = h("div", { className: "segmented", role: "radiogroup", "aria-label": t(row.label) }, row.options.map(([value, label]) => h("label", {}, [
725 h("input", { type: "radio", name: `policy-${row.key}`, value, checked: current === value, disabled: Boolean(reason) }), h("span", { text: t(label) })
726 ])));
727 const family = row.key === "ipv4" ? ["ipv4"] : row.key === "ipv6" ? ["ipv6"] : ["ipv4", "ipv6"];
728 const effectiveKey = row.key === "ipv4" || row.key === "ipv6" ? "access" : row.key;
729 if (row.key === "permitRootLogin") {
730 const shown = (family) => {
731 const value = detail.ssh.effective[family]?.permitRootLogin;
732 const text = value === "without-password" ? "prohibit-password" : value;
733 return text ? badge({ no: t("zakázáno"), "prohibit-password": t("jen klíč"), "forced-commands-only": t("klíč s vynuceným příkazem"), yes: t("heslo i klíč") }[text] || text, text === "no" ? "danger" : text === "yes" ? "warn" : "ok") : h("span", { className: "muted", text: detail.privileged ? t("nedostupné") : t("vyžaduje sudo") });
734 };
735 return h("tr", {}, [h("th", { scope: "row" }, [h("span", { text: t(row.label) }), h("small", { className: "sub", text: t(row.hint) })]), h("td", {}, options), h("td", {}, shown("ipv4")), h("td", {}, shown("ipv6"))]);
736 }
737 return h("tr", {}, [
738 h("th", { scope: "row" }, [h("span", { text: t(row.label) }), h("small", { className: "sub", text: t(row.hint) })]),
739 h("td", {}, options),
740 h("td", {}, family.includes("ipv4") ? effectiveCell(detail, "ipv4", effectiveKey) : h("span", { className: "muted", text: "—" })),
741 h("td", {}, family.includes("ipv6") ? effectiveCell(detail, "ipv6", effectiveKey) : h("span", { className: "muted", text: "—" }))
742 ]);
743 }))
744 ]);
745 form.append(table);
746 if (reason) form.append(h("p", { className: "notice", text: reason }));
747 else {
748 if (windows) form.append(h("p", { className: "notice warn", text: t("Windows OpenSSH vyžaduje po změně restart služby sshd. Před restartem se na serveru aktivuje nezávislá úloha (SYSTEM), která do 120 s vrátí původní sshd_config, pokud změnu nepotvrdí nové SSH přihlášení. Původní relace se může přerušit.") }));
749 if (detail.self) form.append(h("p", { className: "notice warn", text: t("Měníte účet aktivní relace. Core ověří přes sshd -T, že vaše současné přihlášení zůstane povolené, a po reloadu zkusí nové SSH přihlášení. Při selhání se konfigurace automaticky vrátí.") }));
750 form.append(h("div", { className: "inline-actions" }, [h("button", { type: "submit", className: "button primary", dataset: { mutation: "1" }, text: windows ? t("Uložit, restartovat sshd a ověřit") : t("Uložit a ověřit (sudo)") })]));
751 form.addEventListener("submit", (event) => { event.preventDefault(); void saveSshPolicy(detail, form); });
752 }
753 children.push(form);
754 if (windows) children.push(h("p", { className: "hint", text: t("Na Windows zapisuje CH-J dvě označené sekce přímo do C:\\ProgramData\\ssh\\sshd_config: DenyUsers před první existující blok Match a vlastní Match User na konec. KbdInteractiveAuthentication se nepoužívá (Windows OpenSSH ji nepodporuje). Před restartem proběhne sshd -t a sshd -T -C nad kandidátní konfigurací.") }));
755 else children.push(h("p", { className: "hint", text: t("Pravidla zapisuje CH-J do vlastního souboru sshd_config.d (DenyUsers user@0.0.0.0/0, user@::/0 a Match User). Před reloadem proběhne sshd -t a kontrola účinného nastavení sshd -T -C; neplatná nebo neúčinná změna se vrátí. Používá se reload, ne restart.") }));
756 for (const warning of ssh.warnings) children.push(h("p", { className: "notice warn", text: translateWarning(warning) }));
757 const reasons = ["ipv4", "ipv6"].flatMap((family) => (ssh.effective[family]?.reasons || []).map((item) => `${family.toUpperCase()}: ${item.rule} ${item.entry}${item.managed ? " (CH-J)" : ""}${item.uncertain ? ` – ${t("nelze vyhodnotit bez DNS")}` : ""}`));
758 if (reasons.length) children.push(section(t("Pravidla omezující přístup"), h("ul", { className: "details-list" }, [...new Set(reasons)].map((text) => h("li", { text })))));
759 el["panel-ssh"].replaceChildren(...children);
762function translateWarning(text) {
763 if (/does not listen on IPv6/.test(text)) return t("sshd nenaslouchá na IPv6 – přihlášení přes IPv6 není možné bez ohledu na nastavení účtu.");
764 if (/does not listen on IPv4/.test(text)) return t("sshd nenaslouchá na IPv4 – přihlášení přes IPv4 není možné bez ohledu na nastavení účtu.");
765 if (/AuthorizedKeysCommand/.test(text)) return t("Je nastaven AuthorizedKeysCommand – klíče mohou pocházet i z externího zdroje, který CH-J nespravuje.");
766 if (/edited manually/.test(text)) return t("Soubor SSH politiky CH-J byl ručně upraven; CH-J ho nepřepíše.");
767 return text;
770function renderKeysTab(detail) {
771 const files = detail.keys.files;
772 const account = detail.account;
773 const children = [];
774 if (files.some((file) => file.shared)) children.push(h("p", { className: "notice warn", text: t("Účet je správce: Windows OpenSSH používá sdílený soubor administrators_authorized_keys. Každý klíč v něm umožní přihlášení ke všem účtům ze skupiny Administrators; klíče proto nelze přiřadit jednomu účtu.") }));
775 if (!files.length || (!detail.privileged && !detail.self)) {
776 children.push(h("p", { className: "notice", text: detail.privileged || detail.self ? t("Soubor authorized_keys nebyl nalezen.") : detail.platform === "windows" ? t("Klíče jiného účtu lze číst jen ze SSH relace správce se zvýšeným tokenem.") : t("Klíče jiného účtu lze číst jen se sudo (soubor má práva 0600).") }));
777 }
778 const reasons = { "needs-elevation": k("vyžaduje zvýšený token správce"), "profile-missing": k("profil účtu zatím neexistuje (účet se ještě nepřihlásil)"), "outside-home": k("mimo domovský adresář"), symlink: k("symbolický odkaz"), "not-regular": k("není běžný soubor"), unreadable: k("nelze přečíst"), "unsupported-token": k("nepodporovaný token v AuthorizedKeysFile"), "too-large": k("příliš velký"), unknown: k("neznámý stav") };
779 for (const [index, file] of files.entries()) {
780 if (!detail.privileged && !detail.self) break;
781 const status = file.type === "file" ? t("počet klíčů: {count}", { count: file.entries.filter((entry) => entry.valid).length }) : file.type === "absent" || file.type === "absent-dir" ? t("zatím neexistuje") : file.type;
782 const rows = file.entries.map((entry) => entry.valid
783 ? h("tr", {}, [
784 h("td", { text: entry.type }), h("td", { className: "mono", text: entry.fingerprint }), h("td", { text: entry.comment || "—" }),
785 h("td", { className: "mono", text: entry.options || "—" }), h("td", { text: String(entry.line) }),
786 h("td", {}, account.protection === "system" || account.protection === "builtin" || !file.manageable ? null
787 : h("button", { type: "button", className: "mini danger", dataset: { mutation: "1" }, text: t("Odebrat"), onclick: () => removeKey(detail, file, entry) }))
788 ])
789 : h("tr", { className: "invalid" }, [h("td", { colspan: 6, text: t("Řádek {line}: nerozpoznaný záznam ({type}) – zůstane beze změny.", { line: entry.line, type: entry.type || entry.reason }) })]));
790 children.push(section(index === 0 ? t("Primární soubor: {path}", { path: file.path }) : t("Další soubor: {path}", { path: file.path }), [
791 h("p", { className: "hint", text: `${status} · ${file.meta || ""}${file.dirMeta ? ` · ${t("adresář {meta}", { meta: file.dirMeta })}` : ""}${file.manageable ? "" : ` · ${t("nelze spravovat: {reason}", { reason: reasons[file.reason] ? t(reasons[file.reason]) : file.reason })}`}` }),
792 rows.length ? h("div", { className: "table-wrap" }, h("table", {}, [h("thead", {}, h("tr", {}, [t("Typ"), t("Otisk SHA256"), t("Komentář"), t("Volby"), t("Řádek"), ""].map((text) => h("th", { text })))), h("tbody", {}, rows)])) : null
793 ]));
794 }
795 const primary = files[0];
796 if (!account.protection || account.protection === "session" || account.protection === "root") {
797 if ((detail.platform === "windows" ? account.protection !== "builtin" : !account.system) && (detail.privileged || detail.self)) {
798 children.push(h("div", { className: "inline-actions" }, [
799 h("button", { type: "button", className: "button primary", dataset: { mutation: "1" }, disabled: !primary?.manageable, text: t("Přidat Ed25519 klíč…"), onclick: () => addKey(detail) })
800 ]));
801 }
802 }
803 if (detail.platform === "windows") children.push(h("p", { className: "hint", text: t("Soukromé klíče se nikdy neodesílají na server. Úprava zachová ostatní řádky beze změny a zachová existující ACL souboru (ReplaceFile). Nový soubor dostane ACL jen pro SYSTEM, Administrators a u běžného účtu jeho vlastníka.") }));
804 else children.push(h("p", { className: "hint", text: t("Soukromé klíče se nikdy neodesílají na server. Úprava zachová všechny ostatní řádky, komentáře a volby klíčů beze změny; zápis probíhá atomicky pod identitou cílového účtu (0700 .ssh, 0600 authorized_keys).") }));
805 el["panel-keys"].replaceChildren(...children);
808function renderDiagnosticsTab(detail) {
809 if (detail.platform === "windows") { renderWindowsDiagnosticsTab(detail); return; }
810 const ssh = detail.ssh;
811 const privilege = state.privilege;
812 const listener = ssh.effective.listener;
813 const files = detail.keys.files;
814 const rows = [
815 [t("Oprávnění relace"), privilege ? `${privilege.username}: ${t((PRIVILEGE_TEXT[privilege.mode] || PRIVILEGE_TEXT.unknown)[0])}${privilege.verified ? "" : ` (${t("neověřeno")})`}` : t("neověřeno")],
816 [t("Adresa klienta (SSH_CLIENT)"), privilege?.clientAddress || t("neznámá")],
817 ["OpenSSH", ssh.version || (ssh.available ? t("neznámá verze") : t("nenalezen"))],
818 [t("Služba SSH"), ssh.service || t("neznámý stav")],
819 [t("Kontrola sshd -t"), ssh.syntaxOk === null ? t("vyžaduje sudo") : ssh.syntaxOk ? t("v pořádku") : t("CHYBA: {message}", { message: ssh.syntaxMessage })],
820 [t("Struktura konfigurace"), { ok: t("Include sshd_config.d před Match – podporováno"), "include-missing": t("sshd_config.d se nenačítá – nepodporováno"), "include-after-match": t("Include až za blokem Match – nepodporováno") }[ssh.layout] || ssh.layout],
821 [t("Soubor spravovaný CH-J"), `${ssh.managed.path} – ${{ absent: t("neexistuje"), present: ssh.managed.consistent ? t("v pořádku") : t("ručně upraven"), unsafe: t("není běžný soubor"), unreadable: t("nečitelný") }[ssh.managed.state] || ssh.managed.state}`],
822 [t("Naslouchání sshd"), listener ? `IPv4: ${listener.ipv4 ? t("ano") : t("ne")}, IPv6: ${listener.ipv6 ? t("ano") : t("ne")}` : t("vyžaduje sudo")],
823 ["AuthorizedKeysCommand", ssh.effective.authorizedKeysCommand || (detail.privileged ? t("není nastaven") : t("vyžaduje sudo"))],
824 [t("Domovský adresář"), detail.account.homeMeta || "—"],
825 ...files.map((file) => [t("Klíče {spec}", { spec: file.spec || file.path }), `${file.path} – ${file.type}${file.meta ? `, ${file.meta}` : ""}${file.dirMeta ? `, ${t("adresář {meta}", { meta: file.dirMeta })}` : ""}`]),
826 [t("Načteno"), new Date(detail.collectedAt).toLocaleString(i18n.locale())]
827 ];
828 const strict = [];
829 for (const file of files) {
830 const [owner, , mode] = String(file.meta || "").split(":");
831 if (file.type === "file" && owner && owner !== detail.username && owner !== "root") strict.push(t("{path} nevlastní {user} (StrictModes může klíče ignorovat).", { path: file.path, user: detail.username }));
832 if (file.type === "file" && mode && /[2367]$|[2367].$/.test(mode)) strict.push(t("{path} je zapisovatelný pro skupinu/ostatní ({mode}); sshd s StrictModes klíče odmítne.", { path: file.path, mode }));
833 }
834 el["panel-diagnostics"].replaceChildren(infoGrid(rows), ...strict.map((text) => h("p", { className: "notice warn", text })), ...ssh.warnings.map((text) => h("p", { className: "notice warn", text: translateWarning(text) })));
837function selectTab(name, focus = true) {
838 state.tab = name;
839 for (const tab of document.querySelectorAll("[role=tab]")) {
840 const selected = tab.id === `tab-${name}`;
841 tab.setAttribute("aria-selected", String(selected));
842 tab.tabIndex = selected ? 0 : -1;
843 el[tab.getAttribute("aria-controls")].hidden = !selected;
844 if (selected && focus) tab.focus();
845 }
848// ---------- Windows variants (SID, Administrators, account state; no UID/shell/sudo) ----------
849function formatDate(value) { return value ? new Date(value).toLocaleString(i18n.locale()) : "—"; }
851function renderWindowsAccountTab(detail) {
852 const account = detail.account;
853 const children = [infoGrid([
854 [t("Uživatelské jméno"), account.username],
855 [t("Celé jméno"), account.fullName || "—"],
856 [t("Popis"), account.description || "—"],
857 ["SID", account.sid],
858 [t("Zdroj"), account.source === "MicrosoftAccount" ? t("lokální účet propojený s účtem Microsoft") : account.source || "—"],
859 [t("Stav účtu"), account.enabled ? t("povolen") : t("zakázán")],
860 [t("Heslo naposledy nastaveno"), formatDate(account.passwordLastSet)],
861 [t("Platnost hesla"), account.passwordExpires ? formatDate(account.passwordExpires) : t("neomezená")],
862 [t("Platnost účtu"), account.accountExpires ? formatDate(account.accountExpires) : t("neomezená")],
863 [t("Poslední přihlášení"), formatDate(account.lastLogon)],
864 [t("Profil"), account.profilePath || t("zatím nevytvořen")],
865 [t("Typ"), account.protection === "builtin" ? t("vestavěný účet") : t("běžný lokální účet")],
866 ["Administrators (S-1-5-32-544)", account.admin ? t("člen") : t("není člen")]
867 ]), h("p", { className: "hint", text: t("Windows účty se identifikují podle SID. UID/GID, shell ani sudo se na Windows nepoužívají. Zakázaný účet se nepřihlásí žádnou metodou včetně SSH klíče.") })];
868 if (!account.protection && detail.privileged) {
869 children.push(h("div", { className: "inline-actions" }, [
870 actionButton(t("Změnit heslo…"), () => changePassword(account.username), { sudo: false }),
871 account.enabled
872 ? actionButton(t("Zakázat účet"), () => simpleAction("disable", account.username, t("Zakázání účtu"), t("Účet se nebude moci přihlásit žádnou metodou. Data a profil zůstanou zachovány.")), { sudo: false, danger: true })
873 : actionButton(t("Povolit účet"), () => simpleAction("enable", account.username, t("Povolení účtu")), { sudo: false }),
874 actionButton(t("Upravit celé jméno…"), () => changeWindowsText(account, "setFullName", t("Celé jméno"), account.fullName, 256), { sudo: false }),
875 actionButton(t("Upravit popis…"), () => changeWindowsText(account, "setDescription", t("Popis"), account.description, 48), { sudo: false }),
876 actionButton(t("Odstranit účet…"), () => deleteAccount(account), { danger: true, sudo: false })
877 ]));
878 } else if (!account.protection) {
879 children.push(h("p", { className: "notice", text: t("Změny účtu vyžadují SSH relaci správce se zvýšeným tokenem.") }));
880 }
881 el["panel-account"].replaceChildren(...children);
884function renderWindowsGroupsTab(detail) {
885 const account = detail.account;
886 const editable = !account.protection && detail.privileged;
887 const groups = state.inventory?.groups || [];
888 const memberSids = new Set(account.groups.map((group) => group.sid));
889 const children = [section(t("Lokální skupiny"), [
890 h("ul", { className: "chip-list" }, account.groups.length ? account.groups.map((group) => h("li", { title: group.sid }, [
891 h("span", { text: group.name }),
892 editable ? h("button", { type: "button", className: "mini", dataset: { mutation: "1" }, text: t("Odebrat"), onclick: () => removeWindowsGroup(account, group) }) : null
893 ])) : [h("li", { className: "muted", text: t("Účet není členem žádné lokální skupiny.") })])
894 ], t("Názvy skupin jsou lokalizované; CH-J pracuje se skupinami podle SID."))];
895 if (editable) {
896 const candidates = groups.filter((group) => !memberSids.has(group.sid) && group.sid !== "S-1-5-32-544").sort((left, right) => left.name.localeCompare(right.name));
897 const select = h("select", { "aria-label": t("Přidat do skupiny") }, candidates.map((group) => h("option", { value: group.sid, text: group.name })));
898 children.push(section(t("Přidat do skupiny"), h("div", { className: "inline-actions" }, [select, actionButton(t("Přidat"), () => {
899 const group = candidates.find((candidate) => candidate.sid === select.value);
900 if (group) void runMutation(t("Přidání do skupiny {group}", { group: group.name }), (sessionId, auth) => api.users.manage(sessionId, { action: "addGroup", ...identFor(account.username), group: group.sid, ...auth }));
901 }, { sudo: false })])));
902 }
903 const tokenMode = t((PRIVILEGE_TEXT[detail.privilege?.mode] || PRIVILEGE_TEXT.unknown)[0]);
904 const adminChildren = [infoGrid([
905 [t("Členství v Administrators"), account.admin ? t("člen (S-1-5-32-544)") : t("není člen")],
906 [t("Token této SSH relace"), tokenMode]
907 ]), h("p", { className: "hint", text: t("Členství ve skupině Administrators neznamená automaticky zvýšený token procesu (UAC). Na Windows se nepoužívá sudo a CH-J neobchází UAC ani bezpečnostní politiku.") })];
908 if (editable) {
909 adminChildren.push(h("div", { className: "inline-actions" }, [
910 account.admin
911 ? actionButton(t("Odebrat z Administrators"), () => simpleAction("revokeAdmin", account.username, t("Odebrání administrátorských oprávnění"), t("Účet bude odebrán ze skupiny Administrators (S-1-5-32-544). Posledního povoleného správce CH-J neodebere.")), { danger: true, sudo: false })
912 : actionButton(t("Přidat do Administrators"), () => simpleAction("grantAdmin", account.username, t("Přidání administrátorských oprávnění"), t("Účet bude přidán do skupiny Administrators (S-1-5-32-544). Jeho SSH klíče se pak čtou ze sdíleného administrators_authorized_keys.")), { sudo: false })
913 ]));
914 }
915 children.push(section(t("Správci"), adminChildren));
916 const external = state.inventory?.externalIdentities || [];
917 if (external.length) {
918 children.push(section(t("Doménové a cloudové identity v lokálních skupinách (jen pro čtení)"), h("ul", { className: "details-list" }, external.map((item) => h("li", { text: `${item.name} (${item.sid}, ${item.source}) – ${item.groups.join(", ")}` })))));
919 }
920 el["panel-groups"].replaceChildren(...children);
923function renderWindowsDiagnosticsTab(detail) {
924 const ssh = detail.ssh;
925 const system = detail.system || {};
926 const privilege = detail.privilege || state.privilege || {};
927 const listener = ssh.effective.listener;
928 const rows = [
929 [t("Systém"), `${system.caption || "Windows"} ${system.version || ""} (build ${system.build || "?"})`],
930 [t("Typ instalace"), system.installationType ? `${system.installationType}${system.serverCore ? ` – ${t("bez GUI")}` : ""}` : "—"],
931 [t("Detekce platformy"), `${system.detectedBy || "?"}${system.unameOutput ? `, uname: ${system.unameOutput}` : ""}`],
932 [t("Výchozí SSH shell"), `${system.defaultShell || "?"}${system.defaultShellPath ? ` (${system.defaultShellPath})` : ` (${t("výchozí cmd.exe")})`}`],
933 [t("PowerShell pro CH-J"), `${system.powershell || "?"}${system.is64BitProcess ? ", 64bit" : ", 32bit!"}, ${system.languageMode || "?"}${system.pwsh ? `; PowerShell 7: ${system.pwsh}` : `; ${t("PowerShell 7 nenalezen")}`}`],
934 ["LocalAccounts", system.localAccounts ? t("dostupný") : t("nedostupný")],
935 [t("Token relace"), `${privilege.username || "?"} – ${t((PRIVILEGE_TEXT[privilege.mode] || PRIVILEGE_TEXT.unknown)[0])}, ${t("integrita {level}", { level: privilege.integrity || "?" })}`],
936 [t("Adresa klienta (SSH_CLIENT)"), privilege.clientAddress || t("neznámá")],
937 ["OpenSSH Server", ssh.version || (ssh.available ? t("neznámá verze") : t("nenainstalován"))],
938 [t("Služba sshd"), ssh.service || "—"],
939 ["sshd_config", ssh.configPath || "—"],
940 [t("Kontrola sshd -t"), ssh.syntaxOk === null ? t("vyžaduje zvýšený token") : ssh.syntaxOk ? t("v pořádku") : t("CHYBA: {message}", { message: ssh.syntaxMessage })],
941 [t("Spravované sekce CH-J"), ssh.layoutSupported ? (ssh.managed.state === "present" ? (ssh.managed.consistent ? t("přítomny, v pořádku") : t("ručně upraveny")) : t("zatím nejsou")) : t("nepodporováno: {problems}", { problems: (ssh.layoutProblems || []).join("; ") })],
942 [t("Naslouchání sshd"), listener ? `IPv4: ${listener.ipv4 ? t("ano") : t("ne")}, IPv6: ${listener.ipv6 ? t("ano") : t("ne")}` : t("vyžaduje zvýšený token")],
943 [t("Aplikace změn"), t("restart služby sshd s nezávislým rollbackem (naplánovaná úloha SYSTEM, 120 s)")],
944 ...detail.keys.files.map((file) => [t("Klíče {spec}", { spec: file.spec || "" }), `${file.path || t("nevyřešeno")} – ${file.type}${file.shared ? ` (${t("sdílený")})` : ""}${file.acl?.length ? `; ACL: ${file.acl.map((entry) => `${entry.sid}:${entry.rights}`).join(", ")}` : ""}`]),
945 [t("Načteno"), new Date(detail.collectedAt).toLocaleString(i18n.locale())]
946 ];
947 el["panel-diagnostics"].replaceChildren(infoGrid(rows), ...ssh.warnings.map((text) => h("p", { className: "notice warn", text: translateWarning(text) })));
950async function changeWindowsText(account, action, label, value, limit) {
951 const requestGeneration = state.generation;
952 try {
953 const answer = await sessionResult(askValue({ title: label, label, value: value || "", validate: ({ value: text }) => text.length > limit || /[\x00-\x1f]/.test(text) ? t("Max. {limit} znaků bez řídicích znaků.", { limit }) : "" }), requestGeneration);
954 if (!answer) return;
955 const field = action === "setDescription" ? "description" : "fullName";
956 await sessionResult(runMutation(t("Úprava: {label}", { label }), (sessionId, auth) => api.users.manage(sessionId, { action, ...identFor(account.username), [field]: answer.value.trim(), ...auth })), requestGeneration);
957 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
960async function removeWindowsGroup(account, group) {
961 const requestGeneration = state.generation;
962 try {
963 const answer = await sessionResult(confirmAction({ title: t("Odebrat ze skupiny {group}", { group: group.name }), message: t("Účet {user} přestane být členem skupiny {group} ({sid}). Ostatní skupiny zůstanou.", { user: account.username, group: group.name, sid: group.sid }), confirmLabel: t("Odebrat"), danger: true }), requestGeneration);
964 if (!answer) return;
965 await sessionResult(runMutation(t("Odebrání ze skupiny {group}", { group: group.name }), (sessionId, auth) => api.users.manage(sessionId, { action: "removeGroup", ...identFor(account.username), group: group.sid, ...auth })), requestGeneration);
966 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
969// ---------- mutations ----------
970async function runMutation(label, invoke, { successMessage } = {}) {
971 const requestGeneration = state.generation;
972 if (state.mutating || !connected()) return null;
973 const token = state.generation;
974 const sessionId = state.sessionId;
975 if (!window.confirm(`${api.sessions.displayName(state.sessions.find((session) => session.sessionId === sessionId))}\n\n${label}`)) return null;
976 state.mutating = true; updateControls();
977 setStatus(`${label}…`);
978 try {
979 const result = await sessionResult(withSudo(label, (auth) => invoke(sessionId, auth)), requestGeneration);
980 if (token !== state.generation) return null;
981 if (result?.inventory) applyInventory(result.inventory);
982 if (result?.sessionLost) { setStatus(t("Změna byla potvrzena, ale původní SSH relace se restartem sshd ukončila. Připojte se znovu v hlavním okně."), true); void loadSessions(); return result; }
983 if (result?.detail) {
984 // Keep the list column in sync with the verified policy returned by Core.
985 const listed = state.inventory?.users.find((user) => user.username === result.detail.username);
986 if (listed) {
987 const current = result.detail.ssh.policy;
988 listed.sshPolicy = POLICY_ROWS.every((row) => current[row.key] === row.options[0][0]) ? null : { ...current };
989 renderUsers();
990 }
991 if (state.selected === result.detail.username) { state.detailToken += 1; state.detail = result.detail; state.detailLoading = false; renderDetail(); }
992 }
993 const failedSteps = (result?.steps || []).filter((step) => step.status === "failed");
994 const warnings = [...(result?.warnings || []), ...failedSteps.map((step) => t("Krok {step} selhal: {message}", { step: step.step, message: step.message }))];
995 const partial = result?.result === "partial" || failedSteps.length > 0;
996 setStatus(`${successMessage || t("{label}: hotovo, stav byl znovu načten ze serveru.", { label })}${partial ? ` ${t("Pouze částečně!")}` : ""}${warnings.length ? ` ${warnings.join(" ")}` : ""}`, partial);
997 return result;
998 } catch (error) { if (requestGeneration !== state.generation) return;
999 if (token === state.generation) setStatus(describeError(error), parseError(error).code !== CANCELLED);
1000 return null;
1001 } finally { if (requestGeneration === state.generation) {
1002 state.mutating = false; updateControls();
1003 } }
1006async function simpleAction(action, username, label, message = "") {
1007 const requestGeneration = state.generation;
1008 try {
1009 const answer = await sessionResult(confirmAction({ title: label, message: message || t("Provést „{label}“ pro účet {user}?", { label, user: username }), confirmLabel: label, danger: ["expire", "revokeAdmin", "lock"].includes(action) }), requestGeneration);
1010 if (!answer) return;
1011 await sessionResult(runMutation(label, (sessionId, auth) => api.users.manage(sessionId, { action, ...identFor(username), ...auth })), requestGeneration);
1012 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1015async function changePassword(username) {
1016 const requestGeneration = state.generation;
1017 try {
1018 const answer = await sessionResult(askValue({
1019 title: t("Nové heslo pro {user}", { user: username }), label: t("Nové heslo"), label2: t("Heslo znovu"), type: "password",
1020 message: t("Heslo se přenese přes stdin SSH kanálu do chpasswd, nikdy v argumentech příkazu."),
1021 validate: ({ value, value2 }) => !value ? t("Zadejte heslo.") : value !== value2 ? t("Hesla se neshodují.") : value.length > 256 || /[\r\n]/.test(value) ? t("Heslo má neplatný formát.") : ""
1022 }), requestGeneration);
1023 if (!answer) return;
1024 let password = answer.value;
1025 answer.value = ""; answer.value2 = "";
1026 await sessionResult(runMutation(t("Změna hesla"), (sessionId, auth) => api.users.manage(sessionId, { action: "setPassword", ...identFor(username), password, ...auth })), requestGeneration);
1027 password = "";
1028 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1031async function changeComment(account) {
1032 const requestGeneration = state.generation;
1033 try {
1034 const answer = await sessionResult(askValue({ title: t("Celé jméno"), label: t("Celé jméno (GECOS)"), value: account.comment, validate: ({ value }) => value.length > 100 || /[:\\]/.test(value) ? t("Max. 100 znaků bez dvojtečky.") : "" }), requestGeneration);
1035 if (!answer) return;
1036 await sessionResult(runMutation(t("Úprava celého jména"), (sessionId, auth) => api.users.manage(sessionId, { action: "setComment", ...identFor(account.username), comment: answer.value.trim(), ...auth })), requestGeneration);
1037 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1040async function changeHome(account) {
1041 const requestGeneration = state.generation;
1042 try {
1043 const answer = await sessionResult(askValue({
1044 title: t("Domovský adresář"), label: t("Nová cesta"), value: account.home, option: t("přesunout obsah (usermod -m, cíl nesmí existovat)"),
1045 validate: ({ value }) => /^\/[A-Za-z0-9._/@+-]+$/.test(value.trim()) ? "" : t("Zadejte absolutní cestu bez mezer a speciálních znaků.")
1046 }), requestGeneration);
1047 if (!answer || answer.value.trim() === account.home) return;
1048 await sessionResult(runMutation(t("Změna domovského adresáře"), (sessionId, auth) => api.users.manage(sessionId, { action: "setHome", ...identFor(account.username), home: answer.value.trim(), moveHome: answer.option, ...auth })), requestGeneration);
1049 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1052async function deleteAccount(account) {
1053 const requestGeneration = state.generation;
1054 try {
1055 const answer = await sessionResult(confirmAction({
1056 title: t("Odstranit účet {user}", { user: account.username }),
1057 message: t("Účet bude odstraněn ze serveru. Operaci nelze vrátit."),
1058 details: account.platform === "windows"
1059 ? [`SID ${account.sid}`, t("Profil {path}", { path: account.profilePath || t("neexistuje") }), t("Pravidla SSH spravovaná CH-J pro tento účet budou také odstraněna (vyžaduje restart sshd a testovací přihlášení).")]
1060 : [t("UID {uid}, domovský adresář {home}", { uid: account.uid, home: account.home }), account.processes ? t("Účet má {count} spuštěných procesů. Před odstraněním budou ukončeny.", { count: account.processes }) : t("Pravidla SSH spravovaná CH-J pro tento účet budou také odstraněna.")],
1061 option: account.platform === "windows" ? (account.profilePath ? t("odstranit také profil {path} (samostatné potvrzení)", { path: account.profilePath }) : "") : t("odstranit také domovský adresář {path} (samostatné potvrzení)", { path: account.home }),
1062 typed: account.username, confirmLabel: t("Odstranit účet"), danger: true
1063 }), requestGeneration);
1064 if (!answer) return;
1065 let removeHome = false;
1066 if (answer.option) {
1067 const second = await sessionResult(confirmAction({ title: account.platform === "windows" ? t("Odstranit profil?") : t("Odstranit domovský adresář?"), message: t("Obsah {path} bude nevratně smazán.", { path: account.platform === "windows" ? account.profilePath : account.home }), confirmLabel: t("Ano, smazat i data"), danger: true }), requestGeneration);
1068 if (!second) { setStatus(t("Odstranění bylo zrušeno.")); return; }
1069 removeHome = true;
1071 const result = await sessionResult(runMutation(t("Odstranění účtu"), (sessionId, auth) => api.users.manage(sessionId, { action: "delete", ...identFor(account.username), removeHome, removeProfile: removeHome, stopProcesses: true, ...auth })), requestGeneration);
1072 if (result && state.selected === account.username) { state.selected = null; state.detail = null; renderUsers(); renderDetail(); }
1073 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1076async function sshPasswordOffAll() {
1077 const requestGeneration = state.generation;
1078 const preview = await sessionResult(withSudo(t("Vypnutí SSH hesla všem účtům"), (auth) => api.users.sshPasswordOffAll(state.sessionId, { dryRun: true, ...auth })), requestGeneration);
1079 if (!preview) return;
1080 const remaining = preview.accounts.filter((account) => !account.alreadyOff);
1081 if (!remaining.length) { setStatus(t("Přihlášení heslem přes SSH už mají vypnuté všechny účty.")); return; }
1082 const withoutKey = preview.accounts.filter((account) => !account.keys).map((account) => account.username);
1083 const answer = await sessionResult(confirmAction({
1084 title: t("SSH přihlášení heslem všem účtům"),
1085 message: t("Přihlášení heslem přes SSH se vypne všem účtům na tomto serveru. Heslo pro sudo a místní přihlášení zůstávají. Změna se ověří, a když něco selže, vrátí se."),
1086 details: [t("Účty: {list}", { list: preview.accounts.map((account) => account.username).join(", ") }), ...(withoutKey.length ? [t("Bez SSH klíče, po změně se přes SSH nepřihlásí: {list}", { list: withoutKey.join(", ") })] : [])],
1087 typed: withoutKey.length ? "SSH" : "", confirmLabel: t("Vypnout heslo"), danger: true
1088 }), requestGeneration);
1089 if (!answer) return;
1090 let probeCredentials = null;
1091 const result = await sessionResult(runMutation(t("Vypnutí SSH hesla všem účtům"), async (sessionId, auth) => {
1092 for (;;) {
1093 try { return await sessionResult(api.users.sshPasswordOffAll(sessionId, { confirm: true, ...auth, ...(probeCredentials ? { probeCredentials } : {}) }), requestGeneration); }
1094 catch (error) {
1095 if (requestGeneration !== state.generation) return null;
1096 const { code, detail } = parseError(error);
1097 if (code !== "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" || probeCredentials) throw error;
1098 probeCredentials = await sessionResult(askProbeSecret(/passphrase/.test(detail) ? "passphrase" : "password"), requestGeneration);
1099 if (!probeCredentials) throw cancelled();
1102 }, { successMessage: t("Přihlášení heslem přes SSH je vypnuté všem účtům.") }), requestGeneration);
1103 if (result) await loadInventory();
1106async function addGroup(username, group) {
1107 const requestGeneration = state.generation;
1108 try {
1109 if (!group) return;
1110 await sessionResult(runMutation(t("Přidání do skupiny {group}", { group }), (sessionId, auth) => api.users.manage(sessionId, { action: "addGroup", ...identFor(username), group, ...auth })), requestGeneration);
1111 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1114async function removeGroup(username, group) {
1115 const requestGeneration = state.generation;
1116 try {
1117 const answer = await sessionResult(confirmAction({ title: t("Odebrat ze skupiny {group}", { group }), message: t("Účet {user} přestane být členem skupiny {group}. Ostatní skupiny zůstanou.", { user: username, group }), confirmLabel: t("Odebrat"), danger: true }), requestGeneration);
1118 if (!answer) return;
1119 await sessionResult(runMutation(t("Odebrání ze skupiny {group}", { group }), (sessionId, auth) => api.users.manage(sessionId, { action: "removeGroup", ...identFor(username), group, ...auth })), requestGeneration);
1120 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1123async function setPrimary(username, group, current) {
1124 const requestGeneration = state.generation;
1125 try {
1126 if (!group || group === current) return;
1127 const answer = await sessionResult(confirmAction({ title: t("Změna primární skupiny"), message: t("Primární skupina účtu {user} bude {group}. Vlastnictví existujících souborů se nemění.", { user: username, group }), confirmLabel: t("Změnit") }), requestGeneration);
1128 if (!answer) return;
1129 await sessionResult(runMutation(t("Změna primární skupiny"), (sessionId, auth) => api.users.manage(sessionId, { action: "setPrimaryGroup", username, group, ...auth })), requestGeneration);
1130 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1133async function saveSshPolicy(detail, form) {
1134 const requestGeneration = state.generation;
1135 try {
1136 const rows = detail.platform !== "windows" && detail.account.uid === 0 ? [...POLICY_ROWS, ROOT_LOGIN_ROW] : POLICY_ROWS;
1137 const policy = Object.fromEntries(rows.map((row) => [row.key, form.querySelector(`input[name="policy-${row.key}"]:checked`)?.value || detail.ssh.policy[row.key] || "inherit"]));
1138 const changes = rows.filter((row) => policy[row.key] !== detail.ssh.policy[row.key]).map((row) => `${t(row.label)}: ${t(row.options.find(([value]) => value === detail.ssh.policy[row.key])?.[1])} → ${t(row.options.find(([value]) => value === policy[row.key])?.[1])}`);
1139 if (!changes.length) { setStatus(t("Nastavení SSH se nezměnilo.")); return; }
1140 let confirmRootPassword = false;
1141 if (detail.account.uid === 0 && detail.platform !== "windows" && (policy.password === "yes" || policy.permitRootLogin === "yes")) {
1142 const warning = await sessionResult(confirmAction({
1143 title: t("Povolit přihlášení root heslem?"),
1144 message: t("Přihlášení root heslem výrazně zvyšuje riziko útoků hádáním hesel. Doporučená volba je „Jen klíč“ (prohibit-password). Pro přihlášení Ed25519 klíčem není heslo root potřeba."),
1145 typed: "root", confirmLabel: t("Rozumím, povolit heslo"), danger: true
1146 }), requestGeneration);
1147 if (!warning) { setStatus(t("Povolení hesla pro root nebylo potvrzeno; nic nebylo změněno.")); return; }
1148 confirmRootPassword = true;
1150 const risky = detail.self || policy.access === "deny" || policy.pubkey === "no" || policy.password === "no";
1151 const answer = await sessionResult(confirmAction({
1152 title: t("SSH přístup účtu {user}", { user: detail.username }),
1153 message: detail.platform === "windows" ? t("sshd bude restartován. Změna se ponechá jen tehdy, když ji do 120 s potvrdí nové SSH přihlášení; jinak ji úloha na serveru sama vrátí.") : detail.self ? t("Měníte SSH přístup účtu této relace. Nechte relaci otevřenou; při neúspěšném testu nového přihlášení se změna vrátí.") : t("Změna se ověří (sshd -t, sshd -T -C) a aplikuje reloadem SSH služby."),
1154 details: changes, confirmLabel: t("Aplikovat a ověřit"), danger: risky
1155 }), requestGeneration);
1156 if (!answer) return;
1157 let probeCredentials = null;
1158 await sessionResult(runMutation(t("Změna SSH přístupu"), async (sessionId, auth) => {
1159 const requestGeneration = state.generation;
1160 try {
1161 for (;;) {
1162 try { return await sessionResult(api.users.sshPolicy(sessionId, { ...identFor(detail.username), policy, confirmRootPassword, ...auth, ...(probeCredentials ? { probeCredentials } : {}) }), requestGeneration); }
1163 catch (error) { if (requestGeneration !== state.generation) return;
1164 const { code, detail: message } = parseError(error);
1165 if (code !== "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" || probeCredentials) throw error;
1166 probeCredentials = await sessionResult(askProbeSecret(/passphrase/.test(message) ? "passphrase" : "password"), requestGeneration);
1167 if (!probeCredentials) throw cancelled();
1170 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1171 }, { successMessage: detail.platform === "windows" ? t("SSH přístup byl změněn, ověřen přes sshd -T, sshd byl restartován a změnu potvrdilo nové SSH přihlášení.") : t("SSH přístup byl změněn, ověřen přes sshd -T a SSH služba byla znovu načtena.") }), requestGeneration);
1172 probeCredentials = null;
1173 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1176async function removeKey(detail, file, entry) {
1177 const requestGeneration = state.generation;
1178 try {
1179 const answer = await sessionResult(confirmAction({
1180 title: t("Odebrat SSH klíč"), message: t("Z {path} bude odebrán pouze tento klíč. Ostatní řádky zůstanou beze změny.", { path: file.path }),
1181 details: [`${entry.type} ${entry.fingerprint}`, entry.comment ? t("Komentář: {comment}", { comment: entry.comment }) : "", t("Řádek {line}", { line: entry.line })].filter(Boolean), confirmLabel: t("Odebrat klíč"), danger: true
1182 }), requestGeneration);
1183 if (!answer) return;
1184 await sessionResult(runMutation(t("Odebrání SSH klíče"), (sessionId, auth) => api.users.keys(sessionId, { action: "remove", ...identFor(detail.username), path: file.path, line: entry.line, fingerprint: entry.fingerprint, ...auth })), requestGeneration);
1185 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1188async function addKey(detail) {
1189 const requestGeneration = state.generation;
1190 try {
1191 const publicKey = await sessionResult(runDialog(el.keyDialog, {
1192 init: () => {
1193 el.keyText.value = ""; el.keyGenComment.value = `${detail.username}@${state.sessions.find((session) => session.sessionId === state.sessionId)?.host || "server"}`;
1194 el.keyGenPassphrase.value = ""; el.keyGenerateFields.hidden = true; el.keyKeychainFields.hidden = true; keychainKeys = [];
1195 showError(el.keyError, ""); el.keyInfo.hidden = true;
1196 },
1197 submit: () => {
1198 const text = el.keyText.value.trim();
1199 if (!/^(ssh-ed25519|sk-ssh-ed25519@openssh\.com) [A-Za-z0-9+/=]+( .*)?$/.test(text)) {
1200 showError(el.keyError, /PRIVATE KEY/.test(text) ? t(ERROR_TEXT.USERS_KEY_PRIVATE) : t("Vložte jeden veřejný klíč Ed25519 ve formátu OpenSSH."));
1201 return undefined;
1203 return text;
1205 }).finally(() => { el.keyGenPassphrase.value = ""; }), requestGeneration);
1206 if (!publicKey) return;
1207 await sessionResult(runMutation(t("Přidání SSH klíče"), (sessionId, auth) => api.users.keys(sessionId, { action: "add", ...identFor(detail.username), publicKey, ...auth })), requestGeneration);
1208 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1211async function generateKeyPair() {
1212 const requestGeneration = state.generation;
1213 showError(el.keyError, ""); el.keyInfo.hidden = true;
1214 el.keyGenRun.disabled = true;
1215 let passphrase = el.keyGenPassphrase.value;
1216 el.keyGenPassphrase.value = "";
1217 try {
1218 const generated = await sessionResult(api.keys.generate({ type: "ed25519", comment: el.keyGenComment.value, passphrase, privateKeyFormat: "openssh" }), requestGeneration);
1219 passphrase = "";
1220 for (const field of ["privateKey", "privateKeyPem", "privateKeyOpenSSH", "privateKeyPpk"]) generated[field] = "";
1221 const saved = await sessionResult(api.keys.save(generated.generationId), requestGeneration);
1222 if (!saved || saved.canceled) { showError(el.keyError, t("Soukromý klíč nebyl uložen, proto se veřejný klíč nepřidá.")); return; }
1223 el.keyText.value = generated.publicKey;
1224 el.keyInfo.textContent = `${t("Soukromý klíč byl uložen lokálně: {path} ({format}).", { path: saved.privatePath, format: saved.privateKeyFormat })} ${generated.notice || ""} ${t("Potvrďte přidání veřejného klíče na server.")}`;
1225 el.keyInfo.hidden = false;
1226 } catch (error) { if (requestGeneration !== state.generation) return;
1227 showError(el.keyError, t("Generování selhalo: {error}", { error: describeError(error) }));
1228 } finally { if (requestGeneration === state.generation) {
1229 passphrase = "";
1230 el.keyGenRun.disabled = false;
1231 } }
1234async function openCreateDialog() {
1235 const requestGeneration = state.generation;
1236 try {
1237 const inventory = state.inventory;
1238 if (!inventory) return;
1239 const windows = inventory.platform === "windows";
1240 const groups = windows
1241 ? inventory.groups.filter((group) => !["S-1-5-32-544", "S-1-5-32-545"].includes(group.sid)).sort((left, right) => left.name.localeCompare(right.name))
1242 : inventory.groups.filter((group) => group.gid >= (inventory.server.uidMin || 1000) || ["sudo", "wheel", "adm", "users", "docker", "www-data"].includes(group.name)).sort((left, right) => left.name.localeCompare(right.name));
1243 const payload = await sessionResult(runDialog(el.createDialog, {
1244 init: () => {
1245 for (const id of ["createUsername", "createComment", "createHome", "createPassword", "createPassword2"]) el[id].value = "";
1246 el.createMakeHome.checked = true; el.createAdmin.checked = false; el.createDescription.value = "";
1247 for (const id of ["createShell", "createHome", "createPrimary"]) el[id].closest("label").hidden = windows;
1248 el.createMakeHomeLabel.hidden = windows;
1249 el.createDescriptionLabel.hidden = !windows;
1250 el.createUsername.pattern = windows ? "[A-Za-z0-9_][A-Za-z0-9._ \\-]{0,19}" : "[a-z_][a-z0-9_\\-]{0,31}";
1251 el.createUsername.maxLength = windows ? 20 : 32;
1252 el.createAdminText.textContent = windows ? t("přidat do skupiny Administrators (S-1-5-32-544)") : t("přidat do administrátorské skupiny (sudo/wheel podle serveru)");
1253 el.createHint.textContent = windows ? t("Účet se přidá do skupiny Users. Bez hesla se lze přihlásit jen SSH klíčem (je-li to povoleno politikou). Profil vznikne při prvním přihlášení.") : t("Bez hesla se účet může přihlásit jen SSH klíčem. Sudo práva se po vytvoření ověří pomocí sudo -l.");
1254 const shells = inventory.shells.length ? inventory.shells : ["/bin/sh"];
1255 el.createShell.replaceChildren(...shells.map((shell) => h("option", { value: shell, text: shell, selected: shell === "/bin/bash" })));
1256 el.createPrimary.replaceChildren(h("option", { value: "", text: t("Vlastní skupina se jménem účtu") }), ...groups.map((group) => h("option", { value: group.name, text: group.name })));
1257 el.createGroups.replaceChildren(...groups.map((group) => h("option", { value: windows ? group.sid : group.name, text: group.name })));
1258 showError(el.createError, "");
1259 setTimeout(() => el.createUsername.focus(), 0);
1260 },
1261 submit: () => {
1262 const username = el.createUsername.value.trim();
1263 const password = el.createPassword.value;
1264 const home = el.createHome.value.trim();
1265 let problem = "";
1266 if (windows ? !/^[A-Za-z0-9_][A-Za-z0-9._ -]{0,19}$/.test(username) || /[. ]$/.test(username) : !/^[a-z_][a-z0-9_-]{0,31}$/.test(username)) problem = windows ? t(ERROR_TEXT.USERS_NAME_INVALID_WINDOWS) : t(ERROR_TEXT.USERS_NAME_INVALID);
1267 else if (inventory.users.some((user) => user.username === username)) problem = t(ERROR_TEXT.USERS_USER_EXISTS);
1268 else if (home && !/^\/[A-Za-z0-9._/@+-]+$/.test(home)) problem = t(ERROR_TEXT.USERS_HOME_INVALID);
1269 else if (password !== el.createPassword2.value) problem = t("Hesla se neshodují.");
1270 else if (/[\r\n]/.test(password)) problem = t(ERROR_TEXT.USERS_PASSWORD_INVALID);
1271 if (problem) { showError(el.createError, problem); return undefined; }
1272 if (windows) {
1273 return { action: "create", username, fullName: el.createComment.value.trim(), description: el.createDescription.value.trim(),
1274 groups: [...el.createGroups.selectedOptions].map((option) => option.value), admin: el.createAdmin.checked, ...(password ? { password } : {}) };
1276 return {
1277 action: "create", username, comment: el.createComment.value.trim(), shell: el.createShell.value, home, createHome: el.createMakeHome.checked,
1278 primaryGroup: el.createPrimary.value, groups: [...el.createGroups.selectedOptions].map((option) => option.value), admin: el.createAdmin.checked,
1279 ...(password ? { password } : {})
1280 };
1282 }).finally(() => { el.createPassword.value = ""; el.createPassword2.value = ""; }), requestGeneration);
1283 if (!payload) return;
1284 const result = await sessionResult(runMutation(t("Vytvoření účtu {user}", { user: payload.username }), (sessionId, auth) => api.users.manage(sessionId, { ...payload, ...auth })), requestGeneration);
1285 payload.password = "";
1286 if (result) {
1287 el.filterSelect.value = "regular"; renderUsers();
1288 void loadDetail(result.username);
1290 } catch (error) { if (requestGeneration !== state.generation) return; throw error; }
1293// ---------- events ----------
1294el.closeButton.addEventListener("click", () => api.close());
1295el.refreshSessionsButton.addEventListener("click", () => { void loadSessions(); });
1296el.sessionSelect.addEventListener("change", () => { endedSessionId = ""; void selectSession(el.sessionSelect.value); });
1297el.reloadButton.addEventListener("click", async () => {
1298 await refreshPrivilege();
1299 await loadInventory();
1300 if (state.selected) void loadDetail(state.selected);
1301});
1302el.elevatedButton.addEventListener("click", () => { void loadInventory({ elevated: true }); });
1303el.searchInput.addEventListener("input", renderUsers);
1304el.filterSelect.addEventListener("change", renderUsers);
1305el.createButton.addEventListener("click", () => { void openCreateDialog(); });
1306el.sshPasswordOffButton.addEventListener("click", () => { void sshPasswordOffAll().catch((error) => setStatus(errorText(error), true)); });
1307el.usersBody.addEventListener("click", (event) => {
1308 const button = event.target.closest("button[data-username]");
1309 if (button) void loadDetail(button.dataset.username);
1310});
1311document.querySelector("[role=tablist]").addEventListener("click", (event) => {
1312 const tab = event.target.closest("[role=tab]");
1313 if (tab) selectTab(tab.id.replace("tab-", ""));
1314});
1315document.querySelector("[role=tablist]").addEventListener("keydown", (event) => {
1316 if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
1317 const tabs = [...document.querySelectorAll("[role=tab]")];
1318 const index = tabs.findIndex((tab) => tab.id === `tab-${state.tab}`);
1319 const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (index + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
1320 event.preventDefault();
1321 selectTab(tabs[next].id.replace("tab-", ""));
1322});
1323el.keyImportButton.addEventListener("click", () => el.keyFile.click());
1324el.keyFile.addEventListener("change", async () => {
1325 const file = el.keyFile.files?.[0];
1326 el.keyFile.value = "";
1327 if (!file) return;
1328 if (file.size > 16 * 1024) { showError(el.keyError, t("Soubor je příliš velký pro veřejný klíč.")); return; }
1329 const text = await file.text();
1330 if (/PRIVATE KEY/.test(text)) { showError(el.keyError, t(ERROR_TEXT.USERS_KEY_PRIVATE)); return; }
1331 el.keyText.value = text.trim();
1332 showError(el.keyError, "");
1333});
1334el.keyGenerateButton.addEventListener("click", () => { el.keyGenerateFields.hidden = !el.keyGenerateFields.hidden; });
1335// Public Ed25519 keys stored in the Keychain of the main window (Core returns public lines only).
1336let keychainKeys = [];
1337async function loadKeychainKeys() {
1338 const requestGeneration = state.generation;
1339 showError(el.keyError, "");
1340 el.keyKeychainFields.hidden = false;
1341 const select = el.keyKeychainSelect;
1342 select.replaceChildren(new Option(t("Načítám…"), ""));
1343 try {
1344 const keys = (await api.users.keychainPublicKeys()).filter((key) => /^(ssh-ed25519|sk-ssh-ed25519@openssh\.com) /.test(key.publicKey || ""));
1345 if (requestGeneration !== state.generation) return;
1346 keychainKeys = keys;
1347 select.replaceChildren(new Option(keys.length ? t("Vyberte klíč…") : t("Keychain neobsahuje žádný veřejný klíč Ed25519."), ""));
1348 for (const key of keys) select.append(new Option(`${key.label || "SSH"} · ${key.fingerprint || ""}`, key.id));
1349 } catch (error) {
1350 if (requestGeneration !== state.generation) return;
1351 select.replaceChildren(); el.keyKeychainFields.hidden = true;
1352 showError(el.keyError, parseError(error).code === "VAULT_LOCKED" ? t("Odemkněte trezor v hlavním okně, aby šlo použít klíče z Keychainu.") : describeError(error));
1355el.keyKeychainButton.addEventListener("click", () => { void loadKeychainKeys(); });
1356el.keyKeychainSelect.addEventListener("change", () => {
1357 const key = keychainKeys.find((item) => item.id === el.keyKeychainSelect.value);
1358 if (key) { el.keyText.value = key.publicKey.trim(); showError(el.keyError, ""); }
1359});
1360el.keyGenRun.addEventListener("click", () => { void generateKeyPair(); });
1362(async () => {
1363 try { i18n.setLanguage(await api.ui.getLanguage()); } catch {}
1364 i18n.apply(document);
1365 api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); i18n.apply(document); renderPrivilege(); renderUsers(); renderDetail(); });
1366 try {
1367 const info = await api.getInfo();
1368 el.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`;
1369 } catch {}
1370 await loadSessions();
1371})().catch((error) => setStatus(describeError(error), true));
1373setInterval(() => { if (!document.hidden) void loadSessions().catch(() => {}); }, 3000);
SHA-256edfbcd8d3141e51a1866d83c25450cabcc815ecafe03f40419bc41bbca18b01f

CH-J Proprietary Software License 1.14 · Tá Foinse ar fáil faoin CH-J Proprietary Software License 1.14. Ní chuireann a infhaighteacht a athrú ar na téarmaí ceadúnais nó cearta breise a dheonú.

Cartlann SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Comhaid