release-2.9.1 (#1557)

* fix(sso): send the PKCE verifier for GitHub login (#1534)

* fix(remote-desktop): clip cursor overflow without disabling zoom (#1535)

* test(remote-desktop): preserve sessions beyond one hour (#1536)

* fix(hosts): expose controls for overflowing editor tabs (#1537)

* fix(hosts): expose controls for overflowing editor tabs

* test(hosts): mock resize observation in admin panel tests

* fix(status): skip TCP probes while host sessions are active (#1538)

* fix(database): batch session activity persistence (#1539)

* docs(api): describe cookie and API key authentication (#1540)

* fix(snippets): repair missing note column on SQLite upgrades (#1541)

* fix(docker): retain stored SSH credential association (#1543)

* fix(file-manager): render transfer toasts without plugin hooks (#1546)

* feat(hosts): keep compact row actions inline (#1547)

* fix(auth): allow retrying unavailable second-factor interfaces (#1549)

* fix(auth): reject unresolved legacy identity provisioning (#1550)

* fix(desktop): ship the plugin SDK inside Mac App Store builds (#1551)

* fix(tmux): pass full session info to the terminal's session picker (#1552)

The tmux.sessions service reduced detected sessions to bare names, but the
picker reads session.name, so every entry rendered blank and selecting one
sent an empty name, which created a new session instead of attaching.

* fix(ssh): read force keyboard-interactive as a boolean when resolving hosts (#1553)

The column holds the text "true"/"false", and the resolver passed it
through as-is. The string "false" is truthy, so the password provider
treated every saved host as forced keyboard-interactive and left the
password out. Jump hops are built straight from the resolved host, so a
password hop whose server doesn't offer keyboard-interactive failed with
"All configured authentication methods failed".

* fix(database): batch database saves for bulk host writes (#1554)

* fix(database): yield to the event loop between database saves

Each SQLite save serializes and encrypts the whole database into new
buffers, which V8 only releases once the event loop turns. Boot
migrations and bulk host import write plugin settings per host and per
key in an awaited loop, so hundreds of full copies piled up and the
container ran out of memory.

* fix(database): save once per bulk host write instead of once per row

On SQLite every repository write force-saves the whole database. Boot
plugin data migrations, host defaults materialization and the bulk
host routes write one row per host per setting, so they serialized and
encrypted the full database hundreds of times in a row.

DatabaseSaveTrigger.batched wraps a function so force saves made while
it runs collapse into a single save when it finishes. Nested scopes fold
into the outer one, and work that outlives its scope saves normally.

* feat(hosts): add instance-wide predefined tag suggestions (#1548)

* feat(hosts): add shared predefined tag suggestions

* style(hosts): format tag catalog routes

* test(database): advance fake timers past the save yield (#1555)

* fix(i18n): complete Simplified Chinese core and plugin translations (#1542)

* fix(i18n): complete Chinese onboarding and navigation labels

* fix(i18n): translate remaining Chinese core and plugin interfaces

* fix(i18n): translate host editor tab overflow controls

* fix(i18n): use consistent Chinese fleet terminology

* fix(i18n): localize hardcoded controls and plugin views

* fix(i18n): translate built-in homepage widget catalog

* test(homepage): follow translated timezone placeholder

* fix(i18n): translate plugin-provided homepage widgets

* fix(i18n): localize feature settings section titles

* fix: 2.8 oidc accounts unable to sign in after upgrading (#1381)

* fix: plugins losing the saved ssh login when copying a host (#1391)

* fix: fleets, proxmox and automations not getting the host sudo password

* fix: host imports running a defaults pass and metrics restart per host (#1384)

* fix: high cpu from status probes and full database saves on every sample (#1300)

* fix: user data export freezing the server (#1393)

* fix: op:// secret references rejected as ssh keys on credentials (#1394)

* fix: slow file deletes from the trash lookups on every delete (#1390)

* fix: add openapi docs and error handling to host tag routes

* test: compare download stream buffers directly so it stops timing out

* chore: drop em dash from host row comment

* chore: update release notes for 2.9.1

* fix: restore space to add host tags and redesign predefined tags editor

* fix: host key silently accepted when the host is missing from the database (#1397)

* fix: clearer host login failed status label and fix its translations (#1396)

* chore: add host key and status label fixes to release notes

* fix: sso and ldap provider dialogs overflowing the screen and using mismatched toggles

* chore: increment ver

* fix(audit): store plugin entries with no acting user as a null user_id (#1556)

Plugin audit entries written outside a request used the literal "system"
as user_id. That column references users.id, so the insert was refused
(Postgres logs it as an FK violation) and the entry was silently dropped.
Write null instead and keep "system" / plugin:<id> in username.

* chore: add sso/ldap dialog and audit log fixes to release notes

* fix: tunnels and host settings missing from shared hosts on desktop

* fix: remote desktop logins missing from shared hosts on desktop

* fix: simplify host status to online/offline and keep it live without a refresh

* chore: sync Crowdin translations for 2.9.1

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
Luke GustafsonandZacharyZcR authored and GitHub committed 2026-10-04 15:11:41 -05:00
1 parent 5786eaccbb
commit 6b708106e1
912 files changed
+14220 -2200

No files matched your search

+9
View File
@@ -20,6 +20,12 @@
"nameRequired": "A display name is required",
"type": "Provider Type",
"enabled": "Enabled",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Save Provider",
"saving": "Saving...",
"saved": "Saved",
@@ -61,5 +67,8 @@
"label": "Sign in with SSO automatically",
"description": "Send visitors straight to the first SSO provider instead of showing the login form. The OIDC_SILENT_LOGIN_DEFAULT environment variable overrides this."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "'n Vertoonnaam word vereis",
"type": "Verskaffertipe",
"enabled": "Geaktiveer",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Stoor Verskaffer",
"saving": "Stoor...",
"saved": "Gestoor",
@@ -61,5 +67,8 @@
"label": "Meld outomaties aan met SSO",
"description": "Stuur besoekers direk na die eerste SSO-verskaffer in plaas daarvan om die aanmeldvorm te wys. Die OIDC_SILENT_LOGIN_DEFAULT omgewingveranderlike oorskryf dit."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "اسم العرض مطلوب",
"type": "نوع المزود",
"enabled": "مُمكّن",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "حفظ المزود",
"saving": "جارٍ الحفظ...",
"saved": "محفوظ",
@@ -61,5 +67,8 @@
"label": "تسجيل الدخول باستخدام تسجيل الدخول الموحد (SSO) تلقائيًا",
"description": "قم بتوجيه الزوار مباشرةً إلى أول مزود خدمة تسجيل دخول موحد (SSO) بدلاً من عرض نموذج تسجيل الدخول. يُمكن تجاوز هذا الإعداد باستخدام متغير البيئة OIDC_SILENT_LOGIN_DEFAULT."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Изисква се показвано име",
"type": "Тип доставчик",
"enabled": "Активирано",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Запазване на доставчика",
"saving": "Запазване...",
"saved": "Запазено",
@@ -61,5 +67,8 @@
"label": "Влизане с SSO автоматично",
"description": "Изпращайте посетителите директно към първия SSO доставчик, вместо да показвате формата за вход. Променливата на средата OIDC_SILENT_LOGIN_DEFAULT отменя това."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "একটি প্রদর্শনের নাম প্রয়োজন",
"type": "প্রদানকারীর ধরণ",
"enabled": "সক্ষম করা হয়েছে",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "সরবরাহকারীকে সংরক্ষণ করুন",
"saving": "সঞ্চয়...",
"saved": "সংরক্ষিত",
@@ -61,5 +67,8 @@
"label": "SSO দিয়ে স্বয়ংক্রিয়ভাবে সাইন ইন করুন",
"description": "লগইন ফর্ম দেখানোর পরিবর্তে ভিজিটরদের সরাসরি প্রথম SSO প্রোভাইডারের কাছে পাঠান। OIDC_SILENT_LOGIN_DEFAULT এনভায়রনমেন্ট ভেরিয়েবলটি এই প্রক্রিয়াটিকে ওভাররাইড করে।"
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Cal un nom de visualització",
"type": "Tipus de proveïdor",
"enabled": "Activat",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Desa el proveïdor",
"saving": "Desant...",
"saved": "Desat",
@@ -61,5 +67,8 @@
"label": "Inicia la sessió amb SSO automàticament",
"description": "Envia els visitants directament al primer proveïdor d'SSO en comptes de mostrar el formulari d'inici de sessió. La variable d'entorn OIDC_SILENT_LOGIN_DEFAULT ho anul·la."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Je vyžadováno zobrazované jméno",
"type": "Typ poskytovatele",
"enabled": "Povoleno",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Uložit poskytovatele",
"saving": "Ukládání…",
"saved": "Uloženo",
@@ -61,5 +67,8 @@
"label": "Automatické přihlášení pomocí SSO",
"description": "Odesílejte návštěvníky přímo k prvnímu poskytovateli SSO namísto zobrazení přihlašovacího formuláře. Proměnná prostředí OIDC_SILENT_LOGIN_DEFAULT toto přepíše."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Et visningsnavn er påkrævet",
"type": "Udbydertype",
"enabled": "Aktiveret",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Gem udbyder",
"saving": "Gemmer...",
"saved": "Gemt",
@@ -61,5 +67,8 @@
"label": "Log automatisk ind med SSO",
"description": "Send besøgende direkte til den første SSO-udbyder i stedet for at vise loginformularen. Miljøvariablen OIDC_SILENT_LOGIN_DEFAULT tilsidesætter dette."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Ein Anzeigename ist erforderlich.",
"type": "Anbietertyp",
"enabled": "Aktiviert",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Anbieter speichern",
"saving": "Wird gespeichert …",
"saved": "Gespeichert",
@@ -61,5 +67,8 @@
"label": "Automatische Anmeldung mit SSO",
"description": "Besucher werden direkt zum ersten SSO-Anbieter weitergeleitet, anstatt das Anmeldeformular anzuzeigen. Die Umgebungsvariable OIDC_SILENT_LOGIN_DEFAULT überschreibt dies."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Απαιτείται εμφανιζόμενο όνομα",
"type": "Τύπος παρόχου",
"enabled": "Ενεργοποιημένο",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Αποθήκευση παρόχου",
"saving": "Οικονομία...",
"saved": "Αποθηκεύτηκε",
@@ -61,5 +67,8 @@
"label": "Αυτόματη σύνδεση με SSO",
"description": "Στέλνει τους επισκέπτες απευθείας στον πρώτο πάροχο SSO αντί να εμφανίζει τη φόρμα σύνδεσης. Η μεταβλητή περιβάλλοντος OIDC_SILENT_LOGIN_DEFAULT παρακάμπτει αυτό."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Se requiere un nombre para mostrar.",
"type": "Tipo de proveedor",
"enabled": "Activado",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Guardar proveedor",
"saving": "Guardando...",
"saved": "Guardado",
@@ -61,5 +67,8 @@
"label": "Inicia sesión automáticamente con SSO.",
"description": "En lugar de mostrar el formulario de inicio de sesión, redirige a los visitantes directamente al primer proveedor de SSO. La variable de entorno OIDC_SILENT_LOGIN_DEFAULT anula esta configuración."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Näyttönimi on pakollinen",
"type": "Palveluntarjoajan tyyppi",
"enabled": "Käytössä",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Tallenna palveluntarjoaja",
"saving": "Tallennetaan...",
"saved": "Tallennettu",
@@ -61,5 +67,8 @@
"label": "Kirjaudu sisään automaattisesti kertakirjautumisella",
"description": "Ohjaa kävijät suoraan ensimmäiselle SSO-palveluntarjoajalle kirjautumislomakkeen näyttämisen sijaan. OIDC_SILENT_LOGIN_DEFAULT-ympäristömuuttuja korvaa tämän."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Un nom d'affichage est requis.",
"type": "Type de fournisseur",
"enabled": "Activé",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Enregistrer le fournisseur",
"saving": "Enregistrement…",
"saved": "Enregistré",
@@ -61,5 +67,8 @@
"label": "Connectez-vous automatiquement avec l'authentification unique (SSO).",
"description": "Dirigez les visiteurs directement vers le premier fournisseur d'authentification unique (SSO) au lieu d'afficher le formulaire de connexion. La variable d'environnement OIDC_SILENT_LOGIN_DEFAULT remplace ce comportement."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "נדרש שם תצוגה",
"type": "סוג ספק",
"enabled": "מופעל",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "שמור ספק",
"saving": "חִסָכוֹן...",
"saved": "נשמר",
@@ -61,5 +67,8 @@
"label": "כניסה אוטומטית באמצעות SSO",
"description": "שלח מבקרים ישירות לספק ה-SSO הראשון במקום להציג את טופס ההתחברות. משתנה הסביבה OIDC_SILENT_LOGIN_DEFAULT עוקף זאת."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "एक डिस्प्ले नाम आवश्यक है",
"type": "प्रदाता प्रकार",
"enabled": "सक्षम",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "प्रदाता सहेजें",
"saving": "सहेजा जा रहा है...",
"saved": "सहेजा गया",
@@ -61,5 +67,8 @@
"label": "एसएसओ के साथ स्वचालित रूप से साइन इन करें",
"description": "लॉगिन फ़ॉर्म दिखाने के बजाय आगंतुकों को सीधे पहले एसएसओ प्रदाता पर भेजें। OIDC_SILENT_LOGIN_DEFAULT पर्यावरण चर इसे ओवरराइड करता है।"
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Megjelenített név megadása kötelező",
"type": "Szolgáltató típusa",
"enabled": "Engedélyezve",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Szolgáltató mentése",
"saving": "Megtakarítás...",
"saved": "Mentett",
@@ -61,5 +67,8 @@
"label": "Automatikus bejelentkezés egyszeri bejelentkezéssel",
"description": "A látogatókat közvetlenül az első SSO-szolgáltatóhoz irányítsa a bejelentkezési űrlap megjelenítése helyett. Az OIDC_SILENT_LOGIN_DEFAULT környezeti változó felülírja ezt."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Nama tampilan wajib diisi.",
"type": "Jenis Penyedia",
"enabled": "Diaktifkan",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Simpan Penyedia",
"saving": "Penghematan...",
"saved": "Tersimpan",
@@ -61,5 +67,8 @@
"label": "Masuk dengan SSO secara otomatis",
"description": "Arahkan pengunjung langsung ke penyedia SSO pertama alih-alih menampilkan formulir login. Variabel lingkungan OIDC_SILENT_LOGIN_DEFAULT mengesampingkan hal ini."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "È richiesto un nome visualizzato",
"type": "Tipo provider",
"enabled": "Abilitato",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Salva provider",
"saving": "Salvando...",
"saved": "Salvato",
@@ -61,5 +67,8 @@
"label": "Accedi automaticamente con SSO",
"description": "Indirizza i visitatori direttamente al primo provider SSO anziché mostrare il modulo di accesso. La variabile d'ambiente OIDC_SILENT_LOGIN_DEFAULT sovrascrive questa impostazione."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "表示名は必須です",
"type": "プロバイダータイプ",
"enabled": "有効",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "保存",
"saving": "保存中...",
"saved": "保存しました",
@@ -61,5 +67,8 @@
"label": "SSOで自動的にサインイン",
"description": "ログインフォームを表示せずに、訪問者を最初のSSOプロバイダーに直接誘導します。この設定は、環境変数OIDC_SILENT_LOGIN_DEFAULTで上書きされます。"
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "표시 이름은 필수입니다.",
"type": "제공자 유형",
"enabled": "사용",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "제공자 저장",
"saving": "저장 중...",
"saved": "저장됨",
@@ -61,5 +67,8 @@
"label": "SSO로 자동 로그인",
"description": "방문자를 로그인 양식을 표시하는 대신 첫 번째 SSO 제공업체로 바로 보냅니다. OIDC_SILENT_LOGIN_DEFAULT 환경 변수는 이 설정을 재정의합니다."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Een weergavenaam is vereist.",
"type": "Aanbiedertype",
"enabled": "Ingeschakeld",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Aanbieder opslaan",
"saving": "Besparing...",
"saved": "Opgeslagen",
@@ -61,5 +67,8 @@
"label": "Meld u automatisch aan met SSO.",
"description": "Stuur bezoekers direct door naar de eerste SSO-provider in plaats van het inlogformulier weer te geven. De omgevingsvariabele OIDC_SILENT_LOGIN_DEFAULT overschrijft dit."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Et visningsnavn er obligatorisk",
"type": "Leverandørtype",
"enabled": "Aktivert",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Lagre leverandør",
"saving": "Lagrer...",
"saved": "Lagret",
@@ -61,5 +67,8 @@
"label": "Logg inn med SSO automatisk",
"description": "Send besøkende direkte til den første SSO-leverandøren i stedet for å vise innloggingsskjemaet. Miljøvariabelen OIDC_SILENT_LOGIN_DEFAULT overstyrer dette."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Wymagana jest nazwa wyświetlana",
"type": "Typ dostawcy",
"enabled": "Włączona",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Zapisz dostawcę",
"saving": "Zapisywanie...",
"saved": "Zapisano",
@@ -61,5 +67,8 @@
"label": "Zaloguj się automatycznie za pomocą SSO",
"description": "Zamiast wyświetlać formularz logowania, przekieruj odwiedzających bezpośrednio do pierwszego dostawcy SSO. Zmienna środowiskowa OIDC_SILENT_LOGIN_DEFAULT zastępuje to ustawienie."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "É necessário um nome de exibição.",
"type": "Tipo de provedor",
"enabled": "Habilitado",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Salvar provedor",
"saving": "Salvando...",
"saved": "Salvo",
@@ -61,5 +67,8 @@
"label": "Faça login automaticamente com SSO.",
"description": "Direcione os visitantes diretamente para o primeiro provedor de SSO em vez de exibir o formulário de login. A variável de ambiente OIDC_SILENT_LOGIN_DEFAULT substitui essa configuração."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "É necessário um nome de utilizador.",
"type": "Tipo de Provedor",
"enabled": "Ativado",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Guardar Provedor",
"saving": "A guardar...",
"saved": "Guardado",
@@ -61,5 +67,8 @@
"label": "Faça login automaticamente com o SSO.",
"description": "Dirija os visitantes diretamente para o primeiro fornecedor de SSO em vez de apresentar o formulário de início de sessão. A variável de ambiente OIDC_SILENT_LOGIN_DEFAULT substitui esta definição."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Este obligatoriu un nume afișat",
"type": "Tipul furnizorului",
"enabled": "Activat",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Salvați furnizorul",
"saving": "Economisire...",
"saved": "Salvat",
@@ -61,5 +67,8 @@
"label": "Conectați-vă automat cu SSO",
"description": "Trimiteți vizitatorii direct către primul furnizor SSO în loc să afișați formularul de conectare. Variabila de mediu OIDC_SILENT_LOGIN_DEFAULT are prioritate față de această opțiune."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Необходимо указать отображаемое имя.",
"type": "Тип провайдера",
"enabled": "Включено",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Сохранить провайдера",
"saving": "Сохранение…",
"saved": "Сохранено",
@@ -61,5 +67,8 @@
"label": "Автоматический вход через SSO.",
"description": "Вместо отображения формы входа, посетители будут направляться непосредственно к первому поставщику единого входа (SSO). Это определяется переменной среды OIDC_SILENT_LOGIN_DEFAULT."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Потребно је приказано име",
"type": "Тип добављача",
"enabled": "Омогућено",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Сачувај добављача",
"saving": "Чување...",
"saved": "Сачувано",
@@ -61,5 +67,8 @@
"label": "Аутоматски се пријавите помоћу SSO-а",
"description": "Шаљите посетиоце директно првом SSO провајдеру уместо приказивања формулара за пријаву. Променљива окружења OIDC_SILENT_LOGIN_DEFAULT ово поништава."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Ett visningsnamn krävs",
"type": "Leverantörstyp",
"enabled": "Aktiverad",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Spara leverantör",
"saving": "Sparande...",
"saved": "Sparad",
@@ -61,5 +67,8 @@
"label": "Logga in med SSO automatiskt",
"description": "Skicka besökare direkt till den första SSO-leverantören istället för att visa inloggningsformuläret. Miljövariabeln OIDC_SILENT_LOGIN_DEFAULT åsidosätter detta."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "จำเป็นต้องระบุชื่อที่แสดง",
"type": "ประเภทผู้ให้บริการ",
"enabled": "เปิดใช้งาน",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "บันทึกผู้ให้บริการ",
"saving": "ประหยัด...",
"saved": "บันทึกแล้ว",
@@ -61,5 +67,8 @@
"label": "ลงชื่อเข้าใช้ด้วย SSO โดยอัตโนมัติ",
"description": "ส่งผู้เข้าชมไปยังผู้ให้บริการ SSO ตัวแรกโดยตรงแทนที่จะแสดงแบบฟอร์มล็อกอิน ตัวแปรสภาพแวดล้อม OIDC_SILENT_LOGIN_DEFAULT จะแทนที่การตั้งค่านี้"
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Görünen ad gereklidir.",
"type": "Sağlayıcı Türü",
"enabled": "Etkin",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Sağlayıcıyı Kaydet",
"saving": "Kaydediliyor...",
"saved": "Kaydedildi",
@@ -61,5 +67,8 @@
"label": "SSO ile otomatik olarak giriş yap",
"description": "Ziyaretçileri giriş formunu göstermek yerine doğrudan ilk SSO sağlayıcısına yönlendirin. OIDC_SILENT_LOGIN_DEFAULT ortam değişkeni bunu geçersiz kılar."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Потрібне відображуване ім'я",
"type": "Тип постачальника",
"enabled": "Увімкнено",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Зберегти постачальника",
"saving": "Збереження...",
"saved": "Збережено",
@@ -61,5 +67,8 @@
"label": "Автоматичний вхід за допомогою єдиного входу",
"description": "Надсилати відвідувачів безпосередньо до першого постачальника SSO замість відображення форми входу. Змінна середовища OIDC_SILENT_LOGIN_DEFAULT замінює це."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "Tên hiển thị là bắt buộc",
"type": "Loại nhà cung cấp",
"enabled": "Đã bật",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "Lưu nhà cung cấp",
"saving": "Đang lưu...",
"saved": "Đã lưu",
@@ -61,5 +67,8 @@
"label": "Đăng nhập tự động bằng SSO",
"description": "Hãy chuyển hướng khách truy cập trực tiếp đến nhà cung cấp SSO đầu tiên thay vì hiển thị biểu mẫu đăng nhập. Biến môi trường OIDC_SILENT_LOGIN_DEFAULT sẽ ghi đè lên cài đặt này."
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "必须填写显示名称。",
"type": "提供商类型",
"enabled": "已启用",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "保存提供商",
"saving": "保存中...",
"saved": "已保存",
@@ -61,5 +67,8 @@
"label": "使用 SSO 自动登录",
"description": "将访客直接引导至第一个单点登录 (SSO) 提供商,而不是显示登录表单。环境变量 OIDC_SILENT_LOGIN_DEFAULT 会覆盖此设置。"
}
},
"plugin": {
"name": "Single sign-on"
}
}
@@ -20,6 +20,12 @@
"nameRequired": "必須填寫顯示名稱。",
"type": "提供者類型",
"enabled": "已啟用",
"enabledDesc": "Show a sign in button for this provider on the login screen.",
"dialogDesc": "Connect an identity provider so users can sign in with it.",
"sectionGeneral": "General",
"sectionCredentials": "Client",
"sectionClaims": "Claims",
"sectionAccess": "Access",
"save": "儲存提供者",
"saving": "儲存中…",
"saved": "已儲存",
@@ -61,5 +67,8 @@
"label": "使用 SSO 自動登入",
"description": "將訪客直接引導至第一個單一登入 (SSO) 供應商,而不是顯示登入表單。環境變數 OIDC_SILENT_LOGIN_DEFAULT 會覆寫此設定。"
}
},
"plugin": {
"name": "Single sign-on"
}
}
+3 -4
View File
@@ -194,7 +194,6 @@ export function createSsoLogin(ctx: PluginContext, store: ProviderStore) {
config: OidcConfig,
code: string,
pending: PendingState,
withVerifier: boolean,
): Promise<Record<string, unknown>> {
const response = await fetch(config.token_url, {
method: "POST",
@@ -208,7 +207,7 @@ export function createSsoLogin(ctx: PluginContext, store: ProviderStore) {
client_secret: config.client_secret,
code,
redirect_uri: pending.backendCallback,
...(withVerifier ? { code_verifier: pending.codeVerifier } : {}),
code_verifier: pending.codeVerifier,
}),
...buildFetchOptions(config.ca_cert),
} as RequestInit);
@@ -231,7 +230,7 @@ export function createSsoLogin(ctx: PluginContext, store: ProviderStore) {
code: string,
pending: PendingState,
): Promise<PluginVerifiedIdentity> {
const tokenData = await exchangeCode(provider.config, code, pending, false);
const tokenData = await exchangeCode(provider.config, code, pending);
const fetchOptions = buildFetchOptions(provider.config.ca_cert);
const headers = {
Authorization: `Bearer ${tokenData.access_token}`,
@@ -341,7 +340,7 @@ export function createSsoLogin(ctx: PluginContext, store: ProviderStore) {
pending: PendingState,
): Promise<PluginVerifiedIdentity> {
const { config } = provider;
const tokenData = await exchangeCode(config, code, pending, true);
const tokenData = await exchangeCode(config, code, pending);
let userInfo: Record<string, unknown> | null = null;
const claimSources: Record<string, unknown>[] = [];
+198 -210
View File
@@ -5,11 +5,15 @@ import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
FakeSwitch,
Input,
PasswordInput,
Select2,
Switch,
SettingRow,
Textarea,
copyToClipboard,
} from "@termix/plugin-sdk/ui";
import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend";
@@ -71,11 +75,6 @@ function errorMessage(error: unknown, fallback: string): string {
return err.response?.data?.error || err.message || fallback;
}
const labelClass =
"text-[10px] font-semibold text-muted-foreground uppercase tracking-widest";
const areaClass =
"w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono";
function Field({
label,
required,
@@ -88,14 +87,25 @@ function Field({
children: ReactNode;
}) {
return (
<div className="flex flex-col gap-1">
<label className={labelClass}>
<div className="flex flex-col gap-1.5 min-w-0">
<label className="text-xs font-semibold">
{label}
{required && <span className="text-accent-brand ml-1">*</span>}
</label>
{children}
{hint && (
<span className="text-[10px] text-muted-foreground">{hint}</span>
<p className="text-[10px] text-muted-foreground leading-snug">{hint}</p>
)}
</div>
);
}
function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="flex flex-col gap-3">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
{title}
</span>
{children}
</div>
);
@@ -104,14 +114,14 @@ function Field({
function RedirectUri({ uri }: { uri: string }) {
const { t } = useTranslation();
return (
<div className="flex items-center gap-1">
<code className="flex-1 min-w-0 truncate text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-1">
<div className="flex items-center gap-1 min-w-0 border border-border bg-muted/30">
<code className="flex-1 min-w-0 truncate text-[11px] font-mono text-muted-foreground px-2 py-1.5">
{uri}
</code>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
className="h-7 w-7 p-0 shrink-0 text-muted-foreground hover:text-foreground"
title={t("providers.copy")}
onClick={async () => {
await copyToClipboard(uri);
@@ -204,237 +214,216 @@ function ProviderDialog({
? (provider?.redirectUri ?? newRedirectUri)
: newRedirectUri;
const text = (
key: keyof Fields,
label: string,
placeholder: string,
options: { required?: boolean; hint?: string } = {},
) => (
<Field label={label} required={options.required} hint={options.hint}>
<Input
value={fields[key]}
onChange={(e) => set(key)(e.target.value)}
placeholder={placeholder}
/>
</Field>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<DialogContent className="flex flex-col gap-0 p-0 overflow-hidden w-[calc(100vw-2rem)] sm:max-w-xl max-h-[calc(100dvh-2rem)]">
<DialogHeader className="px-4 pt-4 pb-3 pr-10 border-b border-border shrink-0">
<DialogTitle className="text-lg font-bold">
{isEdit ? t("providers.edit") : t("providers.add")}
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground">
{t("providers.dialogDesc")}{" "}
<a
href={
simplified
? "https://docs.termix.site/features/authentication/github-google"
: "https://docs.termix.site/features/authentication/oidc"
}
target="_blank"
rel="noreferrer"
className="text-accent-brand hover:underline"
>
{t("providers.docsLink")}
</a>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 pt-2">
<Field label={t("providers.name")} required>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("providers.namePlaceholder")}
className="text-xs"
/>
</Field>
{!isEdit && (
<Field label={t("providers.type")}>
<Select2
value={type}
onChange={(e) => setType(e.target.value as SsoProviderType)}
className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring"
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 flex flex-col gap-6">
<Section title={t("providers.sectionGeneral")}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Field label={t("providers.name")} required>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("providers.namePlaceholder")}
/>
</Field>
<Field label={t("providers.type")}>
<Select2
value={type}
disabled={isEdit}
onChange={(e) => setType(e.target.value as SsoProviderType)}
className="w-full"
>
{(Object.keys(TYPE_LABELS) as SsoProviderType[]).map(
(value) => (
<option key={value} value={value}>
{TYPE_LABELS[value]}
</option>
),
)}
</Select2>
</Field>
</div>
<div className="border border-border px-3">
<SettingRow
label={t("providers.enabled")}
description={t("providers.enabledDesc")}
>
{(Object.keys(TYPE_LABELS) as SsoProviderType[]).map(
(value) => (
<option key={value} value={value}>
{TYPE_LABELS[value]}
</option>
),
)}
</Select2>
</Field>
)}
<FakeSwitch checked={enabled} onChange={setEnabled} />
</SettingRow>
</div>
</Section>
<div className="flex items-center justify-between">
<label className={labelClass}>{t("providers.enabled")}</label>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</div>
<div className="flex flex-col gap-1 border-t border-border pt-3">
<label className={labelClass}>{t("providers.redirectUri")}</label>
<span className="text-[10px] text-muted-foreground">
{t("providers.redirectUriDesc")}
</span>
<RedirectUri uri={redirectUri} />
<Section title={t("providers.redirectUri")}>
<div className="flex flex-col gap-1.5">
<RedirectUri uri={redirectUri} />
<p className="text-[10px] text-muted-foreground leading-snug">
{t("providers.redirectUriDesc")}
</p>
</div>
{isEdit && provider.legacyCallback && (
<div className="flex items-start justify-between gap-3 pt-1">
<span className="text-[10px] text-muted-foreground">
{t("providers.legacyCallbackDesc", {
<div className="border border-border px-3">
<SettingRow
label={t("providers.legacyCallback")}
description={t("providers.legacyCallbackDesc", {
uri: newRedirectUri,
})}
</span>
<Switch
checked={legacyCallback}
onCheckedChange={setLegacyCallback}
aria-label={t("providers.legacyCallback")}
/>
>
<FakeSwitch
checked={legacyCallback}
onChange={setLegacyCallback}
/>
</SettingRow>
</div>
)}
</div>
</Section>
<div className="flex flex-col gap-3 border-t border-border pt-3">
<span className="text-[10px] text-muted-foreground">
<a
href={
simplified
? "https://docs.termix.site/features/authentication/github-google"
: "https://docs.termix.site/features/authentication/oidc"
}
target="_blank"
rel="noreferrer"
className="text-accent-brand hover:underline"
<Section title={t("providers.sectionCredentials")}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{text("client_id", t("fields.clientId"), "your-client-id", {
required: true,
})}
<Field
label={t("fields.clientSecret")}
required={!provider?.hasClientSecret}
hint={secretHint}
>
{t("providers.docsLink")}
</a>
</span>
<Field label={t("fields.clientId")} required>
<Input
value={fields.client_id}
onChange={(e) => set("client_id")(e.target.value)}
placeholder="your-client-id"
className="text-xs"
/>
</Field>
<Field
label={t("fields.clientSecret")}
required={!provider?.hasClientSecret}
hint={secretHint}
>
<Input
type="password"
value={fields.client_secret}
onChange={(e) => set("client_secret")(e.target.value)}
placeholder="your-client-secret"
className="text-xs"
/>
</Field>
<PasswordInput
value={fields.client_secret}
onChange={(e) => set("client_secret")(e.target.value)}
placeholder="your-client-secret"
/>
</Field>
</div>
{simplified ? (
<span className="text-[10px] text-muted-foreground">
<p className="text-[10px] text-muted-foreground break-all">
{t("providers.authorizationUrl", {
url: AUTHORIZATION_URLS[type as "github" | "google"],
})}
</span>
</p>
) : (
<>
<Field label={t("fields.issuerUrl")} required>
<Input
value={fields.issuer_url}
onChange={(e) => set("issuer_url")(e.target.value)}
placeholder="https://provider"
className="text-xs"
/>
</Field>
<Field label={t("fields.authUrl")} required>
<Input
value={fields.authorization_url}
onChange={(e) => set("authorization_url")(e.target.value)}
placeholder="https://provider/oauth2/auth"
className="text-xs"
/>
</Field>
<Field label={t("fields.tokenUrl")} required>
<Input
value={fields.token_url}
onChange={(e) => set("token_url")(e.target.value)}
placeholder="https://provider/oauth2/token"
className="text-xs"
/>
</Field>
<Field label={t("fields.userIdentifier")} required>
<Input
value={fields.identifier_path}
onChange={(e) => set("identifier_path")(e.target.value)}
placeholder="sub"
className="text-xs"
/>
</Field>
<Field label={t("fields.displayName")} required>
<Input
value={fields.name_path}
onChange={(e) => set("name_path")(e.target.value)}
placeholder="name"
className="text-xs"
/>
</Field>
<Field label={t("fields.scopes")} required>
<Input
value={fields.scopes}
onChange={(e) => set("scopes")(e.target.value)}
placeholder="openid email profile"
className="text-xs"
/>
</Field>
<Field label={t("fields.userinfoUrl")}>
<Input
value={fields.userinfo_url}
onChange={(e) => set("userinfo_url")(e.target.value)}
placeholder="https://provider/oauth2/userinfo"
className="text-xs"
/>
</Field>
<Field
label={t("fields.groupClaim")}
hint={t("fields.groupClaimDesc")}
>
<Input
value={fields.group_claim}
onChange={(e) => set("group_claim")(e.target.value)}
placeholder="groups"
className="text-xs"
/>
</Field>
{text("issuer_url", t("fields.issuerUrl"), "https://provider", {
required: true,
})}
{text(
"authorization_url",
t("fields.authUrl"),
"https://provider/oauth2/auth",
{ required: true },
)}
{text(
"token_url",
t("fields.tokenUrl"),
"https://provider/oauth2/token",
{ required: true },
)}
{text(
"userinfo_url",
t("fields.userinfoUrl"),
"https://provider/oauth2/userinfo",
)}
</>
)}
</Section>
{!simplified && (
<Section title={t("providers.sectionClaims")}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{text("identifier_path", t("fields.userIdentifier"), "sub", {
required: true,
})}
{text("name_path", t("fields.displayName"), "name", {
required: true,
})}
</div>
{text("scopes", t("fields.scopes"), "openid email profile", {
required: true,
})}
{text("group_claim", t("fields.groupClaim"), "groups", {
hint: t("fields.groupClaimDesc"),
})}
</Section>
)}
<Section title={t("providers.sectionAccess")}>
<Field
label={t("fields.allowedUsers")}
hint={t("fields.allowedUsersDesc")}
>
<textarea
<Textarea
value={fields.allowed_users}
onChange={(e) => set("allowed_users")(e.target.value)}
placeholder={"user@example.com\nanother@example.com"}
rows={3}
className={areaClass}
/>
</Field>
<Field
label={t("fields.adminGroup")}
hint={t("fields.adminGroupDesc")}
>
<Input
value={fields.admin_group}
onChange={(e) => set("admin_group")(e.target.value)}
placeholder="admin"
className="text-xs"
className="font-mono resize-y"
/>
</Field>
{text("admin_group", t("fields.adminGroup"), "admin", {
hint: t("fields.adminGroupDesc"),
})}
<Field label={t("fields.caCert")} hint={t("fields.caCertDesc")}>
<textarea
<Textarea
value={fields.ca_cert}
onChange={(e) => set("ca_cert")(e.target.value)}
placeholder={
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}
rows={4}
className={areaClass}
className="font-mono resize-y"
/>
</Field>
</div>
</Section>
</div>
<div className="flex gap-2 justify-end pt-2">
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={() => onOpenChange(false)}
>
{t("providers.cancel")}
</Button>
<Button
size="sm"
variant="outline"
className="text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
onClick={save}
disabled={saving}
>
{saving ? t("providers.saving") : t("providers.save")}
</Button>
</div>
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t border-border shrink-0">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
{t("providers.cancel")}
</Button>
<Button
variant="outline"
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
onClick={save}
disabled={saving}
>
{saving ? t("providers.saving") : t("providers.save")}
</Button>
</div>
</DialogContent>
</Dialog>
@@ -493,7 +482,7 @@ export function ProvidersSetting() {
if (providers === null) return null;
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3 py-3 border-b border-border last:border-0">
<span className="text-[10px] text-muted-foreground">
<a
href="https://docs.termix.site/features/authentication/sso-providers"
@@ -505,7 +494,7 @@ export function ProvidersSetting() {
</a>
</span>
{providers.length === 0 ? (
<span className="text-[10px] text-muted-foreground">
<span className="text-xs text-muted-foreground">
{t("providers.none")}
</span>
) : (
@@ -513,26 +502,25 @@ export function ProvidersSetting() {
{providers.map((provider) => (
<div
key={provider.id}
className="flex flex-col gap-1.5 p-2 border border-border bg-background"
className="flex flex-col gap-2 p-3 border border-border bg-background"
>
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 flex items-center gap-1.5">
<span className="text-xs font-medium truncate">
<span className="text-sm font-medium truncate">
{provider.name}
</span>
<span className="text-[9px] px-1 py-0.5 bg-muted text-muted-foreground font-mono uppercase">
<span className="text-[9px] px-1 py-0.5 bg-muted text-muted-foreground font-mono uppercase shrink-0">
{TYPE_LABELS[provider.type] ?? provider.type}
</span>
</div>
<Switch
<FakeSwitch
checked={provider.enabled}
onCheckedChange={() => void toggleEnabled(provider)}
aria-label={t("providers.enabled")}
onChange={() => void toggleEnabled(provider)}
/>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
onClick={() => {
setEditing(provider);
setDialogOpen(true);
@@ -544,7 +532,7 @@ export function ProvidersSetting() {
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
onClick={() => void remove(provider)}
title={t("providers.delete")}
>
+14
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
LEGACY_DDL,
@@ -356,6 +357,19 @@ describe("the callback", () => {
"GET",
`/callback?code=abc&state=${url.searchParams.get("state")}`,
);
const exchange = vi
.mocked(fetch)
.mock.calls.find(
([url]) =>
String(url) === "https://github.com/login/oauth/access_token",
);
const token = new URLSearchParams(String(exchange?.[1]?.body));
const verifier = token.get("code_verifier");
expect(verifier).toBeTruthy();
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
expect(createHash("sha256").update(verifier!).digest("base64url")).toBe(
url.searchParams.get("code_challenge"),
);
expect(callback.body.identity).toMatchObject({
provider: String(created.body.id),
subject: "42",