diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 62dd4b63b..0eb2583d7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -42,5 +42,7 @@ https://youtu.be/lngaePO96tM - Status checks triggering Fail2Ban bans while a session was open - Too many database writes from session activity - Host editor tabs being hidden when they did not fit +- SSH host keys being accepted without a check when the host was missing from the database +- The "last login failed" host status showing in English or mistranslated in several languages diff --git a/src/backend/hosts/host-key-verifier.ts b/src/backend/hosts/host-key-verifier.ts index fb7158f25..375260568 100644 --- a/src/backend/hosts/host-key-verifier.ts +++ b/src/backend/hosts/host-key-verifier.ts @@ -84,18 +84,47 @@ export class SSHHostKeyVerifier { hostId, ); + // No row means nothing to compare against and nowhere to save the + // key, so it is never accepted silently (#1397): the user decides + // when there is a socket to ask on, otherwise it is refused. if (!host) { + if (!ws) { + sshLogger.error( + "Host not found in database during key verification, rejecting", + { + operation: "host_key_no_host", + hostId, + ip, + port, + userId, + }, + ); + verify(false); + return; + } + const accepted = await this.promptUserForNewKey( + ws, + ip, + port, + undefined, + fingerprint, + keyType, + algorithm, + ); sshLogger.warn( - "Host not found in database during key verification", + accepted + ? "Key for a host missing from the database accepted for this session only" + : "Key for a host missing from the database rejected", { operation: "host_key_no_host", hostId, ip, port, + fingerprint, userId, }, ); - verify(true); + verify(accepted); return; } diff --git a/src/backend/tests/hosts/host-key-recovery.test.ts b/src/backend/tests/hosts/host-key-recovery.test.ts index 61e0b0ea7..1a09f8269 100644 --- a/src/backend/tests/hosts/host-key-recovery.test.ts +++ b/src/backend/tests/hosts/host-key-recovery.test.ts @@ -76,3 +76,47 @@ describe("host key recovery", () => { expect(accepted).toEqual([]); }); }); + +describe("a host missing from the database", () => { + function answeringSocket(action: "accept" | "reject") { + const socket = new EventEmitter() as EventEmitter & { + send: (data: string) => void; + }; + socket.send = () => + socket.emit( + "message", + Buffer.from( + JSON.stringify({ + type: "host_key_verification_response", + data: { action }, + }), + ), + ); + return socket as unknown as WebSocket; + } + + async function verifyMissingHost(ws: WebSocket | null) { + const verifier = await SSHHostKeyVerifier.createHostVerifier( + hostId, + "127.0.0.1", + 22, + ws, + "user", + false, + null, + ); + return new Promise((resolve) => + verifier(Buffer.from("any-key"), resolve), + ); + } + + it("refuses the key when nobody can be asked", async () => { + expect(await verifyMissingHost(null)).toBe(false); + }); + + it("asks the user and saves nothing", async () => { + expect(await verifyMissingHost(answeringSocket("accept"))).toBe(true); + expect(await verifyMissingHost(answeringSocket("reject"))).toBe(false); + expect(updateHostKey).not.toHaveBeenCalled(); + }); +}); diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 4b987d315..7173f984c 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -359,7 +359,7 @@ "serverStats": "Host Metrics", "status": { "available": "Available", - "reachable": "Up, but the last sign in failed", + "reachable": "Available, but login failed", "offline": "Offline", "monitoringDisabled": "Monitoring disabled", "label": "Status" @@ -1516,8 +1516,7 @@ "sslManualTitle": "Upload a certificate", "updateExternalAutoProvisionFailed": "Failed to update the auto-create setting", "hostTags": "Predefined host tags", - "hostTagsDesc": "One tag per line, available to all users. Editing this list changes suggestions only; tags already assigned to hosts are kept.", - "hostTagsSaved": "Host tag suggestions saved.", + "hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.", "hostTagsError": "Could not load or save host tag suggestions." }, "newUi": { diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index f151f20e7..07c75a5e4 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -353,7 +353,7 @@ "serverStats": "Gasheermetrieke", "status": { "available": "Beskikbaar", - "reachable": "Aan, maar die laaste aanmelding het misluk", + "reachable": "Beskikbaar, maar aanmelding het misluk", "offline": "Vanlyn", "monitoringDisabled": "Monitering gedeaktiveer", "label": "Status" diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index 11f4890b3..191030fe8 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -353,7 +353,7 @@ "serverStats": "مقاييس المضيف", "status": { "available": "متاح", - "reachable": "تم تسجيل الدخول، لكن آخر تسجيل دخول فشل", + "reachable": "متاح، لكن تسجيل الدخول فشل", "offline": "غير متصل بالإنترنت", "monitoringDisabled": "تم تعطيل المراقبة", "label": "حالة" diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index d58b17d2f..428add439 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -353,7 +353,7 @@ "serverStats": "Метрики на хоста", "status": { "available": "Налично", - "reachable": "Включено, но последното влизане не беше успешно", + "reachable": "Достъпен, но влизането е неуспешно", "offline": "Офлайн", "monitoringDisabled": "Мониторингът е деактивиран", "label": "Статус" diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index 979da651d..7ad67fe56 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -353,7 +353,7 @@ "serverStats": "হোস্ট মেট্রিক্স", "status": { "available": "উপলব্ধ", - "reachable": "Up, but the last sign in failed", + "reachable": "উপলব্ধ, কিন্তু লগইন ব্যর্থ হয়েছে", "offline": "অফলাইন", "monitoringDisabled": "পর্যবেক্ষণ অক্ষম করা হয়েছে", "label": "অবস্থা" diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 9e9208f0d..b99e0ef49 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -353,7 +353,7 @@ "serverStats": "Mètriques de l'amfitrió", "status": { "available": "Disponible", - "reachable": "Activat, però l'últim inici de sessió ha fallat", + "reachable": "Disponible, però l'inici de sessió ha fallat", "offline": "Fora de línia", "monitoringDisabled": "Monitorització desactivada", "label": "Estat" diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 7a3a86d94..a5c3049e4 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -353,7 +353,7 @@ "serverStats": "Metriky hostitele", "status": { "available": "K dispozici", - "reachable": "Nahoře, ale poslední přihlášení se nezdařilo", + "reachable": "Dostupný, ale přihlášení selhalo", "offline": "Offline", "monitoringDisabled": "Monitorování zakázáno", "label": "Postavení" diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index 096277810..0f0788076 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -353,7 +353,7 @@ "serverStats": "Værtsmålinger", "status": { "available": "Tilgængelig", - "reachable": "Oppe, men den sidste login mislykkedes", + "reachable": "Tilgængelig, men login mislykkedes", "offline": "Offline", "monitoringDisabled": "Overvågning deaktiveret", "label": "Status" diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index dd7f54a24..9a342f14d 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -353,7 +353,7 @@ "serverStats": "Host-Metriken", "status": { "available": "Verfügbar", - "reachable": "Oben angekommen, aber die letzte Anmeldung schlug fehl.", + "reachable": "Erreichbar, aber Anmeldung fehlgeschlagen", "offline": "Offline", "monitoringDisabled": "Überwachung deaktiviert", "label": "Status" diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index fa69cbbed..f51781cf9 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -353,7 +353,7 @@ "serverStats": "Μετρήσεις κεντρικού υπολογιστή", "status": { "available": "Διαθέσιμος", - "reachable": "Ενεργοποιημένο, αλλά η τελευταία σύνδεση απέτυχε", + "reachable": "Διαθέσιμος, αλλά η σύνδεση απέτυχε", "offline": "Εκτός σύνδεσης", "monitoringDisabled": "Η παρακολούθηση απενεργοποιήθηκε", "label": "Κατάσταση" diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index 035694dd8..900365dbb 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -353,7 +353,7 @@ "serverStats": "Métricas del host", "status": { "available": "Disponible", - "reachable": "Arriba, pero el último cartel falló.", + "reachable": "Disponible, pero el inicio de sesión falló", "offline": "Desconectado", "monitoringDisabled": "Monitoreo desactivado", "label": "Estado" diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index 4beec5464..fb334c2f1 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -353,7 +353,7 @@ "serverStats": "Isännän mittarit", "status": { "available": "Saatavilla", - "reachable": "Ylös, mutta viimeisin kirjautuminen epäonnistui", + "reachable": "Saatavilla, mutta kirjautuminen epäonnistui", "offline": "Offline-tilassa", "monitoringDisabled": "Valvonta poistettu käytöstä", "label": "Status" diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index 1785cfbc8..6b98f553a 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -353,7 +353,7 @@ "serverStats": "Métriques de l'hôte", "status": { "available": "Disponible", - "reachable": "J'ai réussi, mais la dernière connexion a échoué.", + "reachable": "Disponible, mais la connexion a échoué", "offline": "Hors ligne", "monitoringDisabled": "Surveillance désactivée", "label": "Statut" diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index 34463b428..8b3bcef9f 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -353,7 +353,7 @@ "serverStats": "מדדי מארח", "status": { "available": "זָמִין", - "reachable": "למעלה, אבל הכניסה האחרונה נכשלה", + "reachable": "זמין, אך ההתחברות נכשלה", "offline": "לא מקוון", "monitoringDisabled": "ניטור מושבת", "label": "סטָטוּס" diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index c62d7f565..826ef0acb 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -353,7 +353,7 @@ "serverStats": "होस्ट मेट्रिक्स", "status": { "available": "उपलब्ध", - "reachable": "Up, but the last sign in failed", + "reachable": "उपलब्ध, लेकिन लॉगिन विफल रहा", "offline": "ऑफलाइन", "monitoringDisabled": "निगरानी अक्षम है", "label": "स्थिति" diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 28f0357d4..74d89d91f 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -353,7 +353,7 @@ "serverStats": "Gazdagép-metrikák", "status": { "available": "Elérhető", - "reachable": "Felment, de az utolsó bejelentkezés sikertelen", + "reachable": "Elérhető, de a bejelentkezés sikertelen", "offline": "Offline", "monitoringDisabled": "Monitorozás letiltva", "label": "Állapot" diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index 1892d67bf..8c1f8d26d 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -353,7 +353,7 @@ "serverStats": "Metrik Host", "status": { "available": "Tersedia", - "reachable": "Up, but the last sign in failed", + "reachable": "Tersedia, tetapi login gagal", "offline": "Offline", "monitoringDisabled": "Pemantauan dinonaktifkan", "label": "Status" diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index 9825cc897..42c351faa 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -353,7 +353,7 @@ "serverStats": "Metriche host", "status": { "available": "Disponibile", - "reachable": "Su, ma l'ultimo accesso non è riuscito", + "reachable": "Disponibile, ma l'accesso non è riuscito", "offline": "Offline", "monitoringDisabled": "Monitoraggio disabilitato", "label": "Stato" diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 02ef1ad5b..5a51a2d64 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -353,7 +353,7 @@ "serverStats": "ホストメトリクス", "status": { "available": "利用可能", - "reachable": "上昇したが、最後のサインインが失敗した", + "reachable": "利用可能ですが、ログインに失敗しました", "offline": "オフライン", "monitoringDisabled": "監視が無効になっています", "label": "状態" diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 972689ef5..443e3e6c0 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -353,7 +353,7 @@ "serverStats": "호스트 메트릭", "status": { "available": "사용 가능", - "reachable": "올라갔지만 마지막 로그인 시도가 실패했습니다.", + "reachable": "사용 가능하지만 로그인에 실패했습니다", "offline": "오프라인", "monitoringDisabled": "모니터링이 비활성화되었습니다.", "label": "상태" diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index e5d683167..14e38301d 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -353,7 +353,7 @@ "serverStats": "Hoststatistieken", "status": { "available": "Beschikbaar", - "reachable": "Omhoog, maar de laatste aanmelding mislukte.", + "reachable": "Beschikbaar, maar inloggen mislukt", "offline": "Offline", "monitoringDisabled": "Monitoring uitgeschakeld", "label": "Status" diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index ce46321de..917b43790 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -353,7 +353,7 @@ "serverStats": "Vertsmålinger", "status": { "available": "Tilgjengelig", - "reachable": "Up, but the last sign in failed", + "reachable": "Tilgjengelig, men innlogging mislyktes", "offline": "Frakoblet", "monitoringDisabled": "Overvåking deaktivert", "label": "Status" diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index 378e7647d..b6b44cc1f 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -353,7 +353,7 @@ "serverStats": "Metryki hosta", "status": { "available": "Dostępny", - "reachable": "Up, but the last sign in failed", + "reachable": "Dostępny, ale logowanie nie powiodło się", "offline": "Niedostępny", "monitoringDisabled": "Monitorowanie wyłączone", "label": "Status" diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index efdc25027..fdd390bd3 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -353,7 +353,7 @@ "serverStats": "Métricas do host", "status": { "available": "Disponível", - "reachable": "Up, but the last sign in failed", + "reachable": "Disponível, mas o login falhou", "offline": "Offline", "monitoringDisabled": "Monitoramento desativado", "label": "Status" diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index b53f73f3a..71937473a 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -353,7 +353,7 @@ "serverStats": "Métricas do Host", "status": { "available": "Disponível", - "reachable": "Up, but the last sign in failed", + "reachable": "Disponível, mas o início de sessão falhou", "offline": "Offline", "monitoringDisabled": "Monitorização desativado", "label": "Estatuto" diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index f5bd69c8d..0a71d029a 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -353,7 +353,7 @@ "serverStats": "Metrici ale gazdei", "status": { "available": "Disponibil", - "reachable": "Activ, dar ultima conectare a eșuat", + "reachable": "Disponibil, dar autentificarea a eșuat", "offline": "Offline", "monitoringDisabled": "Monitorizarea este dezactivată", "label": "Stare" diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index 175166ecf..6461c31a0 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -353,7 +353,7 @@ "serverStats": "Метрики хоста", "status": { "available": "Доступный", - "reachable": "Up, but the last sign in failed", + "reachable": "Доступен, но вход не выполнен", "offline": "Офлайн", "monitoringDisabled": "Мониторинг отключен", "label": "Статус" diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 8ad60ed59..9673a8d70 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -353,7 +353,7 @@ "serverStats": "Метрике хоста", "status": { "available": "Доступно", - "reachable": "Up, but the last sign in failed", + "reachable": "Доступан, али пријава није успела", "offline": "Офлајн", "monitoringDisabled": "Праћење је онемогућено", "label": "Статус" diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index 0cfa9a83c..4849a4899 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -353,7 +353,7 @@ "serverStats": "Värdstatistik", "status": { "available": "Tillgänglig", - "reachable": "Up, but the last sign in failed", + "reachable": "Tillgänglig, men inloggningen misslyckades", "offline": "Off-line", "monitoringDisabled": "Övervakning inaktiverad", "label": "Status" diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 431e183b1..1c8517e4f 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -353,7 +353,7 @@ "serverStats": "เมตริกโฮสต์", "status": { "available": "มีอยู่", - "reachable": "Up, but the last sign in failed", + "reachable": "พร้อมใช้งาน แต่เข้าสู่ระบบไม่สำเร็จ", "offline": "ออฟไลน์", "monitoringDisabled": "ปิดใช้งานการตรวจสอบ", "label": "สถานะ" diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index 5153d45f0..7a292e6ab 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -353,7 +353,7 @@ "serverStats": "Sunucu Metrikleri", "status": { "available": "Mevcut", - "reachable": "Up, but the last sign in failed", + "reachable": "Erişilebilir, ancak oturum açma başarısız oldu", "offline": "Çevrimdışı", "monitoringDisabled": "İzleme devre dışı bırakıldı", "label": "Durum" diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index d325f9690..2c4647588 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -353,7 +353,7 @@ "serverStats": "Метрики хоста", "status": { "available": "Доступно", - "reachable": "Up, but the last sign in failed", + "reachable": "Доступний, але вхід не виконано", "offline": "Офлайн", "monitoringDisabled": "Моніторинг вимкнено", "label": "Статус" diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index 4608abfb8..66cbd672d 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -353,7 +353,7 @@ "serverStats": "Số liệu máy chủ", "status": { "available": "Có sẵn", - "reachable": "Up, but the last sign in failed", + "reachable": "Khả dụng, nhưng đăng nhập thất bại", "offline": "Ngoại tuyến", "monitoringDisabled": "Chức năng giám sát đã bị vô hiệu hóa.", "label": "Trạng thái" diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index 554fbd2f4..07153f1f6 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -359,7 +359,7 @@ "serverStats": "主机指标", "status": { "available": "可用的", - "reachable": "在线,但上次登录失败", + "reachable": "可用,但登录失败", "offline": "离线", "monitoringDisabled": "监控已禁用", "label": "地位" diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 2a86062ad..bd1f32327 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -353,7 +353,7 @@ "serverStats": "主機指標", "status": { "available": "可用的", - "reachable": "Up, but the last sign in failed", + "reachable": "可用,但登入失敗", "offline": "離線", "monitoringDisabled": "監控已停用", "label": "地位" diff --git a/src/ui/sidebar/AdminHostTags.tsx b/src/ui/sidebar/AdminHostTags.tsx index 9bb344384..90736c975 100644 --- a/src/ui/sidebar/AdminHostTags.tsx +++ b/src/ui/sidebar/AdminHostTags.tsx @@ -1,80 +1,99 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Button } from "@/components/button"; +import { toast } from "sonner"; +import { X } from "lucide-react"; import { getHostTags, saveHostTags } from "@/api/host-tags-api"; export function AdminHostTags() { const { t } = useTranslation(); - const [value, setValue] = useState(""); + const [tags, setTags] = useState([]); + const [input, setInput] = useState(""); const [loaded, setLoaded] = useState(false); const [saving, setSaving] = useState(false); - const [message, setMessage] = useState(""); useEffect(() => { let cancelled = false; getHostTags() - .then((tags) => { + .then((list) => { if (cancelled) return; - setValue(tags.join("\n")); + setTags(list); setLoaded(true); }) .catch(() => { - if (!cancelled) setMessage(t("admin.hostTagsError")); + if (!cancelled) toast.error(t("admin.hostTagsError")); }); return () => { cancelled = true; }; }, [t]); - async function save() { + async function save(next: string[]) { + const previous = tags; + setTags(next); setSaving(true); - setMessage(""); try { - const tags = await saveHostTags( - value - .split("\n") - .map((tag) => tag.trim()) - .filter(Boolean), - ); - setValue(tags.join("\n")); - setMessage(t("admin.hostTagsSaved")); + setTags(await saveHostTags(next)); window.dispatchEvent(new Event("termix:host-tags-changed")); } catch { - setMessage(t("admin.hostTagsError")); + setTags(previous); + toast.error(t("admin.hostTagsError")); } finally { setSaving(false); } } + function add(raw: string) { + const tag = raw.trim(); + setInput(""); + if (!tag || tags.includes(tag)) return; + void save([...tags, tag]); + } + return ( -
+
-

+ {t("admin.hostTagsDesc")} -

-