mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
feat: implement RdClient login functionality and enhance authentication flow
- Added a dedicated login page for the Remote Desktop client, allowing operators to sign in when their session expires. - Implemented safe return URL handling for the login process. - Updated authentication middleware to support RdClient-specific routes and permissions. - Enhanced localization support for the new login feature across multiple languages. - Improved user experience by redirecting to the appropriate pages based on authentication status.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
### Added
|
||||
|
||||
- **RdClient dashboard (web):** **Tools → Remote Desktop** opens `/remote` in a new browser tab — standalone operator device list with branding, search/filters, and **Connect** (same `/remote/:id` viewer as Devices). Devices → Web Remote unchanged.
|
||||
- **RdClient login (web):** `/remote/login` — dedicated operator sign-in when the panel session expires (redirects to `/remote` or viewer instead of admin `/login`); optional **Remember me** stores encrypted credentials in IndexedDB (Web Crypto AES-GCM), separate from the peer password vault planned for Phase B.
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
@@ -3819,5 +3819,24 @@
|
||||
"open_panel": "فتح لوحة التحكم",
|
||||
"devices_count": "{count} أجهزة",
|
||||
"retry": "إعادة المحاولة"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "عميل سطح المكتب البعيد",
|
||||
"subtitle": "سجّل الدخول للاتصال بأجهزتك",
|
||||
"username_placeholder": "اسم المستخدم",
|
||||
"password_placeholder": "كلمة المرور",
|
||||
"remember_me": "تذكرني على هذا الجهاز",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"totp_title": "المصادقة الثنائية",
|
||||
"totp_subtitle": "أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة",
|
||||
"verify": "تحقق",
|
||||
"back_to_login": "العودة إلى تسجيل الدخول",
|
||||
"open_console_login": "فتح تسجيل دخول وحدة التحكم",
|
||||
"session_expired": "انتهت الجلسة. يرجى تسجيل الدخول مرة أخرى.",
|
||||
"fill_all_fields": "يرجى ملء جميع الحقول",
|
||||
"invalid_credentials": "بيانات اعتماد غير صالحة",
|
||||
"network_error": "خطأ في الشبكة",
|
||||
"enter_6_digits": "أدخل جميع الأرقام الستة",
|
||||
"invalid_code": "رمز غير صالح"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Otevřít konzoli",
|
||||
"devices_count": "{count} zařízení",
|
||||
"retry": "Zkusit znovu"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Klient vzdálené plochy",
|
||||
"subtitle": "Přihlaste se pro připojení k vašim zařízením",
|
||||
"username_placeholder": "Uživatelské jméno",
|
||||
"password_placeholder": "Heslo",
|
||||
"remember_me": "Zapamatovat si mě na tomto zařízení",
|
||||
"sign_in": "Přihlásit se",
|
||||
"totp_title": "Dvoufaktorové ověření",
|
||||
"totp_subtitle": "Zadejte 6místný kód z vaší autentizační aplikace",
|
||||
"verify": "Ověřit",
|
||||
"back_to_login": "Zpět k přihlášení",
|
||||
"open_console_login": "Otevřít přihlášení konzole",
|
||||
"session_expired": "Relace vypršela. Přihlaste se znovu.",
|
||||
"fill_all_fields": "Vyplňte všechna pole",
|
||||
"invalid_credentials": "Neplatné přihlašovací údaje",
|
||||
"network_error": "Chyba sítě",
|
||||
"enter_6_digits": "Zadejte všech 6 číslic",
|
||||
"invalid_code": "Neplatný kód"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Åbn konsol",
|
||||
"devices_count": "{count} enheder",
|
||||
"retry": "Prøv igen"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote Desktop-klient",
|
||||
"subtitle": "Log ind for at oprette forbindelse til dine enheder",
|
||||
"username_placeholder": "Brugernavn",
|
||||
"password_placeholder": "Adgangskode",
|
||||
"remember_me": "Husk mig på denne enhed",
|
||||
"sign_in": "Log ind",
|
||||
"totp_title": "To-faktor-godkendelse",
|
||||
"totp_subtitle": "Indtast den 6-cifrede kode fra din godkendelsesapp",
|
||||
"verify": "Bekræft",
|
||||
"back_to_login": "Tilbage til login",
|
||||
"open_console_login": "Åbn konsol-login",
|
||||
"session_expired": "Session udløbet. Log ind igen.",
|
||||
"fill_all_fields": "Udfyld alle felter",
|
||||
"invalid_credentials": "Ugyldige loginoplysninger",
|
||||
"network_error": "Netværksfejl",
|
||||
"enter_6_digits": "Indtast alle 6 cifre",
|
||||
"invalid_code": "Ugyldig kode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Konsole öffnen",
|
||||
"devices_count": "{count} Geräte",
|
||||
"retry": "Erneut versuchen"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote-Desktop-Client",
|
||||
"subtitle": "Melden Sie sich an, um eine Verbindung zu Ihren Geräten herzustellen",
|
||||
"username_placeholder": "Benutzername",
|
||||
"password_placeholder": "Passwort",
|
||||
"remember_me": "Auf diesem Gerät merken",
|
||||
"sign_in": "Anmelden",
|
||||
"totp_title": "Zwei-Faktor-Authentifizierung",
|
||||
"totp_subtitle": "Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein",
|
||||
"verify": "Bestätigen",
|
||||
"back_to_login": "Zurück zur Anmeldung",
|
||||
"open_console_login": "Konsolen-Anmeldung öffnen",
|
||||
"session_expired": "Sitzung abgelaufen. Bitte erneut anmelden.",
|
||||
"fill_all_fields": "Bitte alle Felder ausfüllen",
|
||||
"invalid_credentials": "Ungültige Anmeldedaten",
|
||||
"network_error": "Netzwerkfehler",
|
||||
"enter_6_digits": "Alle 6 Ziffern eingeben",
|
||||
"invalid_code": "Ungültiger Code"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"save": "Save notification settings",
|
||||
"saved": "Notification settings saved"
|
||||
}
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote Desktop Client",
|
||||
"subtitle": "Sign in to connect to your devices",
|
||||
"username_placeholder": "Username",
|
||||
"password_placeholder": "Password",
|
||||
"remember_me": "Remember me on this device",
|
||||
"sign_in": "Sign in",
|
||||
"totp_title": "Two-factor authentication",
|
||||
"totp_subtitle": "Enter the 6-digit code from your authenticator app",
|
||||
"verify": "Verify",
|
||||
"back_to_login": "Back to sign in",
|
||||
"open_console_login": "Open console login",
|
||||
"session_expired": "Session expired. Please sign in again.",
|
||||
"fill_all_fields": "Please fill in all fields",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"network_error": "Network error",
|
||||
"enter_6_digits": "Enter all 6 digits",
|
||||
"invalid_code": "Invalid code"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Abrir consola",
|
||||
"devices_count": "{count} dispositivos",
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Cliente de escritorio remoto",
|
||||
"subtitle": "Inicie sesión para conectarse a sus dispositivos",
|
||||
"username_placeholder": "Usuario",
|
||||
"password_placeholder": "Contraseña",
|
||||
"remember_me": "Recordarme en este dispositivo",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"totp_title": "Autenticación de dos factores",
|
||||
"totp_subtitle": "Introduzca el código de 6 dígitos de su aplicación de autenticación",
|
||||
"verify": "Verificar",
|
||||
"back_to_login": "Volver al inicio de sesión",
|
||||
"open_console_login": "Abrir inicio de sesión de consola",
|
||||
"session_expired": "Sesión expirada. Inicie sesión de nuevo.",
|
||||
"fill_all_fields": "Complete todos los campos",
|
||||
"invalid_credentials": "Credenciales inválidas",
|
||||
"network_error": "Error de red",
|
||||
"enter_6_digits": "Introduzca los 6 dígitos",
|
||||
"invalid_code": "Código inválido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Avaa konsoli",
|
||||
"devices_count": "{count} laitetta",
|
||||
"retry": "Yritä uudelleen"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Etätyöpöytäasiakas",
|
||||
"subtitle": "Kirjaudu sisään yhdistääksesi laitteisiisi",
|
||||
"username_placeholder": "Käyttäjätunnus",
|
||||
"password_placeholder": "Salasana",
|
||||
"remember_me": "Muista minut tällä laitteella",
|
||||
"sign_in": "Kirjaudu sisään",
|
||||
"totp_title": "Kaksivaiheinen tunnistautuminen",
|
||||
"totp_subtitle": "Syötä 6-numeroinen koodi tunnistussovelluksestasi",
|
||||
"verify": "Vahvista",
|
||||
"back_to_login": "Takaisin kirjautumiseen",
|
||||
"open_console_login": "Avaa konsolin kirjautuminen",
|
||||
"session_expired": "Istunto vanhentui. Kirjaudu uudelleen.",
|
||||
"fill_all_fields": "Täytä kaikki kentät",
|
||||
"invalid_credentials": "Virheelliset tunnistetiedot",
|
||||
"network_error": "Verkkovirhe",
|
||||
"enter_6_digits": "Syötä kaikki 6 numeroa",
|
||||
"invalid_code": "Virheellinen koodi"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3816,5 +3816,24 @@
|
||||
"open_panel": "Ouvrir la console",
|
||||
"devices_count": "{count} appareils",
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Client Bureau à distance",
|
||||
"subtitle": "Connectez-vous pour accéder à vos appareils",
|
||||
"username_placeholder": "Nom d'utilisateur",
|
||||
"password_placeholder": "Mot de passe",
|
||||
"remember_me": "Se souvenir de moi sur cet appareil",
|
||||
"sign_in": "Se connecter",
|
||||
"totp_title": "Authentification à deux facteurs",
|
||||
"totp_subtitle": "Entrez le code à 6 chiffres de votre application d'authentification",
|
||||
"verify": "Vérifier",
|
||||
"back_to_login": "Retour à la connexion",
|
||||
"open_console_login": "Ouvrir la connexion console",
|
||||
"session_expired": "Session expirée. Veuillez vous reconnecter.",
|
||||
"fill_all_fields": "Veuillez remplir tous les champs",
|
||||
"invalid_credentials": "Identifiants invalides",
|
||||
"network_error": "Erreur réseau",
|
||||
"enter_6_digits": "Entrez les 6 chiffres",
|
||||
"invalid_code": "Code invalide"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "कंसोल खोलें",
|
||||
"devices_count": "{count} डिवाइस",
|
||||
"retry": "पुनः प्रयास"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "रिमोट डेस्कटॉप क्लाइंट",
|
||||
"subtitle": "अपने उपकरणों से कनेक्ट करने के लिए साइन इन करें",
|
||||
"username_placeholder": "उपयोगकर्ता नाम",
|
||||
"password_placeholder": "पासवर्ड",
|
||||
"remember_me": "इस उपकरण पर मुझे याद रखें",
|
||||
"sign_in": "साइन इन",
|
||||
"totp_title": "दो-कारक प्रमाणीकरण",
|
||||
"totp_subtitle": "अपने प्रमाणीकरण ऐप से 6-अंकीय कोड दर्ज करें",
|
||||
"verify": "सत्यापित करें",
|
||||
"back_to_login": "साइन इन पर वापस",
|
||||
"open_console_login": "कंसोल लॉगिन खोलें",
|
||||
"session_expired": "सत्र समाप्त हो गया। कृपया फिर से साइन इन करें।",
|
||||
"fill_all_fields": "कृपया सभी फ़ील्ड भरें",
|
||||
"invalid_credentials": "अमान्य क्रेडेंशियल",
|
||||
"network_error": "नेटवर्क त्रुटि",
|
||||
"enter_6_digits": "सभी 6 अंक दर्ज करें",
|
||||
"invalid_code": "अमान्य कोड"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Konzol megnyitása",
|
||||
"devices_count": "{count} eszköz",
|
||||
"retry": "Újra"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Távoli asztal kliens",
|
||||
"subtitle": "Jelentkezzen be az eszközeihez való csatlakozáshoz",
|
||||
"username_placeholder": "Felhasználónév",
|
||||
"password_placeholder": "Jelszó",
|
||||
"remember_me": "Emlékezzen rám ezen az eszközön",
|
||||
"sign_in": "Bejelentkezés",
|
||||
"totp_title": "Kétlépcsős hitelesítés",
|
||||
"totp_subtitle": "Adja meg a 6 számjegyű kódot a hitelesítő alkalmazásból",
|
||||
"verify": "Ellenőrzés",
|
||||
"back_to_login": "Vissza a bejelentkezéshez",
|
||||
"open_console_login": "Konzol bejelentkezés megnyitása",
|
||||
"session_expired": "A munkamenet lejárt. Jelentkezzen be újra.",
|
||||
"fill_all_fields": "Töltse ki az összes mezőt",
|
||||
"invalid_credentials": "Érvénytelen hitelesítő adatok",
|
||||
"network_error": "Hálózati hiba",
|
||||
"enter_6_digits": "Adja meg mind a 6 számjegyet",
|
||||
"invalid_code": "Érvénytelen kód"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Buka konsol",
|
||||
"devices_count": "{count} perangkat",
|
||||
"retry": "Coba lagi"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Klien Desktop Jarak Jauh",
|
||||
"subtitle": "Masuk untuk terhubung ke perangkat Anda",
|
||||
"username_placeholder": "Nama pengguna",
|
||||
"password_placeholder": "Kata sandi",
|
||||
"remember_me": "Ingat saya di perangkat ini",
|
||||
"sign_in": "Masuk",
|
||||
"totp_title": "Autentikasi dua faktor",
|
||||
"totp_subtitle": "Masukkan kode 6 digit dari aplikasi autentikator Anda",
|
||||
"verify": "Verifikasi",
|
||||
"back_to_login": "Kembali ke masuk",
|
||||
"open_console_login": "Buka login konsol",
|
||||
"session_expired": "Sesi berakhir. Silakan masuk lagi.",
|
||||
"fill_all_fields": "Harap isi semua bidang",
|
||||
"invalid_credentials": "Kredensial tidak valid",
|
||||
"network_error": "Kesalahan jaringan",
|
||||
"enter_6_digits": "Masukkan semua 6 digit",
|
||||
"invalid_code": "Kode tidak valid"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Apri console",
|
||||
"devices_count": "{count} dispositivi",
|
||||
"retry": "Riprova"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Client desktop remoto",
|
||||
"subtitle": "Accedi per connetterti ai tuoi dispositivi",
|
||||
"username_placeholder": "Nome utente",
|
||||
"password_placeholder": "Password",
|
||||
"remember_me": "Ricordami su questo dispositivo",
|
||||
"sign_in": "Accedi",
|
||||
"totp_title": "Autenticazione a due fattori",
|
||||
"totp_subtitle": "Inserisci il codice a 6 cifre dalla tua app di autenticazione",
|
||||
"verify": "Verifica",
|
||||
"back_to_login": "Torna all'accesso",
|
||||
"open_console_login": "Apri accesso console",
|
||||
"session_expired": "Sessione scaduta. Accedi di nuovo.",
|
||||
"fill_all_fields": "Compila tutti i campi",
|
||||
"invalid_credentials": "Credenziali non valide",
|
||||
"network_error": "Errore di rete",
|
||||
"enter_6_digits": "Inserisci tutte le 6 cifre",
|
||||
"invalid_code": "Codice non valido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "コンソールを開く",
|
||||
"devices_count": "{count} 台",
|
||||
"retry": "再試行"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "リモートデスクトップクライアント",
|
||||
"subtitle": "デバイスに接続するにはサインインしてください",
|
||||
"username_placeholder": "ユーザー名",
|
||||
"password_placeholder": "パスワード",
|
||||
"remember_me": "このデバイスでログイン情報を保存",
|
||||
"sign_in": "サインイン",
|
||||
"totp_title": "二要素認証",
|
||||
"totp_subtitle": "認証アプリの6桁のコードを入力してください",
|
||||
"verify": "確認",
|
||||
"back_to_login": "サインインに戻る",
|
||||
"open_console_login": "コンソールログインを開く",
|
||||
"session_expired": "セッションの有効期限が切れました。再度サインインしてください。",
|
||||
"fill_all_fields": "すべての項目を入力してください",
|
||||
"invalid_credentials": "認証情報が無効です",
|
||||
"network_error": "ネットワークエラー",
|
||||
"enter_6_digits": "6桁すべてを入力してください",
|
||||
"invalid_code": "無効なコード"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "콘솔 열기",
|
||||
"devices_count": "{count}개 장치",
|
||||
"retry": "다시 시도"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "원격 데스크톱 클라이언트",
|
||||
"subtitle": "기기에 연결하려면 로그인하세요",
|
||||
"username_placeholder": "사용자 이름",
|
||||
"password_placeholder": "비밀번호",
|
||||
"remember_me": "이 기기에서 로그인 정보 저장",
|
||||
"sign_in": "로그인",
|
||||
"totp_title": "2단계 인증",
|
||||
"totp_subtitle": "인증 앱의 6자리 코드를 입력하세요",
|
||||
"verify": "확인",
|
||||
"back_to_login": "로그인으로 돌아가기",
|
||||
"open_console_login": "콘솔 로그인 열기",
|
||||
"session_expired": "세션이 만료되었습니다. 다시 로그인하세요.",
|
||||
"fill_all_fields": "모든 항목을 입력하세요",
|
||||
"invalid_credentials": "잘못된 자격 증명",
|
||||
"network_error": "네트워크 오류",
|
||||
"enter_6_digits": "6자리 모두 입력하세요",
|
||||
"invalid_code": "잘못된 코드"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Åpne konsoll",
|
||||
"devices_count": "{count} enheter",
|
||||
"retry": "Prøv igjen"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote Desktop-klient",
|
||||
"subtitle": "Logg inn for å koble til enhetene dine",
|
||||
"username_placeholder": "Brukernavn",
|
||||
"password_placeholder": "Passord",
|
||||
"remember_me": "Husk meg på denne enheten",
|
||||
"sign_in": "Logg inn",
|
||||
"totp_title": "Tofaktorautentisering",
|
||||
"totp_subtitle": "Skriv inn 6-sifret kode fra autentiseringsappen",
|
||||
"verify": "Bekreft",
|
||||
"back_to_login": "Tilbake til innlogging",
|
||||
"open_console_login": "Åpne konsollinnlogging",
|
||||
"session_expired": "Økten har utløpt. Logg inn på nytt.",
|
||||
"fill_all_fields": "Fyll ut alle felt",
|
||||
"invalid_credentials": "Ugyldige påloggingsdetaljer",
|
||||
"network_error": "Nettverksfeil",
|
||||
"enter_6_digits": "Skriv inn alle 6 sifre",
|
||||
"invalid_code": "Ugyldig kode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Console openen",
|
||||
"devices_count": "{count} apparaten",
|
||||
"retry": "Opnieuw"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote Desktop-client",
|
||||
"subtitle": "Meld u aan om verbinding te maken met uw apparaten",
|
||||
"username_placeholder": "Gebruikersnaam",
|
||||
"password_placeholder": "Wachtwoord",
|
||||
"remember_me": "Onthoud mij op dit apparaat",
|
||||
"sign_in": "Aanmelden",
|
||||
"totp_title": "Tweefactorauthenticatie",
|
||||
"totp_subtitle": "Voer de 6-cijferige code uit uw authenticator-app in",
|
||||
"verify": "Verifiëren",
|
||||
"back_to_login": "Terug naar aanmelden",
|
||||
"open_console_login": "Console-aanmelding openen",
|
||||
"session_expired": "Sessie verlopen. Meld u opnieuw aan.",
|
||||
"fill_all_fields": "Vul alle velden in",
|
||||
"invalid_credentials": "Ongeldige inloggegevens",
|
||||
"network_error": "Netwerkfout",
|
||||
"enter_6_digits": "Voer alle 6 cijfers in",
|
||||
"invalid_code": "Ongeldige code"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Otwórz konsolę",
|
||||
"devices_count": "{count} urządzeń",
|
||||
"retry": "Ponów"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Klient pulpitu zdalnego",
|
||||
"subtitle": "Zaloguj się, aby połączyć się ze swoimi urządzeniami",
|
||||
"username_placeholder": "Nazwa użytkownika",
|
||||
"password_placeholder": "Hasło",
|
||||
"remember_me": "Zapamiętaj mnie na tym urządzeniu",
|
||||
"sign_in": "Zaloguj się",
|
||||
"totp_title": "Uwierzytelnianie dwuskładnikowe",
|
||||
"totp_subtitle": "Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej",
|
||||
"verify": "Weryfikuj",
|
||||
"back_to_login": "Wróć do logowania",
|
||||
"open_console_login": "Otwórz logowanie do konsoli",
|
||||
"session_expired": "Sesja wygasła. Zaloguj się ponownie.",
|
||||
"fill_all_fields": "Wypełnij wszystkie pola",
|
||||
"invalid_credentials": "Nieprawidłowe dane logowania",
|
||||
"network_error": "Błąd sieci",
|
||||
"enter_6_digits": "Wprowadź wszystkie 6 cyfr",
|
||||
"invalid_code": "Nieprawidłowy kod"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "Abrir consola",
|
||||
"devices_count": "{count} dispositivos",
|
||||
"retry": "Tentar novamente"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Cliente de ambiente de trabalho remoto",
|
||||
"subtitle": "Inicie sessão para ligar aos seus dispositivos",
|
||||
"username_placeholder": "Nome de utilizador",
|
||||
"password_placeholder": "Palavra-passe",
|
||||
"remember_me": "Lembrar-me neste dispositivo",
|
||||
"sign_in": "Iniciar sessão",
|
||||
"totp_title": "Autenticação de dois fatores",
|
||||
"totp_subtitle": "Introduza o código de 6 dígitos da sua aplicação de autenticação",
|
||||
"verify": "Verificar",
|
||||
"back_to_login": "Voltar ao início de sessão",
|
||||
"open_console_login": "Abrir início de sessão da consola",
|
||||
"session_expired": "Sessão expirada. Inicie sessão novamente.",
|
||||
"fill_all_fields": "Preencha todos os campos",
|
||||
"invalid_credentials": "Credenciais inválidas",
|
||||
"network_error": "Erro de rede",
|
||||
"enter_6_digits": "Introduza os 6 dígitos",
|
||||
"invalid_code": "Código inválido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Deschide consola",
|
||||
"devices_count": "{count} dispozitive",
|
||||
"retry": "Reîncearcă"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Client desktop la distanță",
|
||||
"subtitle": "Autentificați-vă pentru a vă conecta la dispozitive",
|
||||
"username_placeholder": "Nume utilizator",
|
||||
"password_placeholder": "Parolă",
|
||||
"remember_me": "Ține-mă minte pe acest dispozitiv",
|
||||
"sign_in": "Autentificare",
|
||||
"totp_title": "Autentificare cu doi factori",
|
||||
"totp_subtitle": "Introduceți codul de 6 cifre din aplicația de autentificare",
|
||||
"verify": "Verifică",
|
||||
"back_to_login": "Înapoi la autentificare",
|
||||
"open_console_login": "Deschide autentificarea consolă",
|
||||
"session_expired": "Sesiunea a expirat. Autentificați-vă din nou.",
|
||||
"fill_all_fields": "Completați toate câmpurile",
|
||||
"invalid_credentials": "Date de autentificare invalide",
|
||||
"network_error": "Eroare de rețea",
|
||||
"enter_6_digits": "Introduceți toate cele 6 cifre",
|
||||
"invalid_code": "Cod invalid"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Öppna konsol",
|
||||
"devices_count": "{count} enheter",
|
||||
"retry": "Försök igen"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Remote Desktop-klient",
|
||||
"subtitle": "Logga in för att ansluta till dina enheter",
|
||||
"username_placeholder": "Användarnamn",
|
||||
"password_placeholder": "Lösenord",
|
||||
"remember_me": "Kom ihåg mig på den här enheten",
|
||||
"sign_in": "Logga in",
|
||||
"totp_title": "Tvåfaktorsautentisering",
|
||||
"totp_subtitle": "Ange den 6-siffriga koden från din autentiseringsapp",
|
||||
"verify": "Verifiera",
|
||||
"back_to_login": "Tillbaka till inloggning",
|
||||
"open_console_login": "Öppna konsolinloggning",
|
||||
"session_expired": "Sessionen har gått ut. Logga in igen.",
|
||||
"fill_all_fields": "Fyll i alla fält",
|
||||
"invalid_credentials": "Ogiltiga inloggningsuppgifter",
|
||||
"network_error": "Nätverksfel",
|
||||
"enter_6_digits": "Ange alla 6 siffror",
|
||||
"invalid_code": "Ogiltig kod"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "เปิดคอนโซล",
|
||||
"devices_count": "{count} อุปกรณ์",
|
||||
"retry": "ลองอีกครั้ง"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "ไคลเอนต์เดสก์ท็อประยะไกล",
|
||||
"subtitle": "ลงชื่อเข้าใช้เพื่อเชื่อมต่อกับอุปกรณ์ของคุณ",
|
||||
"username_placeholder": "ชื่อผู้ใช้",
|
||||
"password_placeholder": "รหัสผ่าน",
|
||||
"remember_me": "จดจำฉันบนอุปกรณ์นี้",
|
||||
"sign_in": "ลงชื่อเข้าใช้",
|
||||
"totp_title": "การยืนยันตัวตนสองขั้นตอน",
|
||||
"totp_subtitle": "ป้อนรหัส 6 หลักจากแอปยืนยันตัวตนของคุณ",
|
||||
"verify": "ยืนยัน",
|
||||
"back_to_login": "กลับไปลงชื่อเข้าใช้",
|
||||
"open_console_login": "เปิดการเข้าสู่ระบบคอนโซล",
|
||||
"session_expired": "เซสชันหมดอายุ กรุณาลงชื่อเข้าใช้อีกครั้ง",
|
||||
"fill_all_fields": "กรุณากรอกข้อมูลให้ครบทุกช่อง",
|
||||
"invalid_credentials": "ข้อมูลรับรองไม่ถูกต้อง",
|
||||
"network_error": "ข้อผิดพลาดเครือข่าย",
|
||||
"enter_6_digits": "ป้อนตัวเลขครบ 6 หลัก",
|
||||
"invalid_code": "รหัสไม่ถูกต้อง"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Konsolu aç",
|
||||
"devices_count": "{count} cihaz",
|
||||
"retry": "Yeniden dene"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Uzak Masaüstü İstemcisi",
|
||||
"subtitle": "Cihazlarınıza bağlanmak için oturum açın",
|
||||
"username_placeholder": "Kullanıcı adı",
|
||||
"password_placeholder": "Parola",
|
||||
"remember_me": "Bu cihazda beni hatırla",
|
||||
"sign_in": "Oturum aç",
|
||||
"totp_title": "İki faktörlü kimlik doğrulama",
|
||||
"totp_subtitle": "Kimlik doğrulama uygulamanızdaki 6 haneli kodu girin",
|
||||
"verify": "Doğrula",
|
||||
"back_to_login": "Oturum açmaya dön",
|
||||
"open_console_login": "Konsol oturum açmayı aç",
|
||||
"session_expired": "Oturum süresi doldu. Lütfen tekrar oturum açın.",
|
||||
"fill_all_fields": "Lütfen tüm alanları doldurun",
|
||||
"invalid_credentials": "Geçersiz kimlik bilgileri",
|
||||
"network_error": "Ağ hatası",
|
||||
"enter_6_digits": "6 hanenin tamamını girin",
|
||||
"invalid_code": "Geçersiz kod"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Відкрити консоль",
|
||||
"devices_count": "{count} пристроїв",
|
||||
"retry": "Повторити"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Клієнт віддаленого робочого столу",
|
||||
"subtitle": "Увійдіть, щоб підключитися до ваших пристроїв",
|
||||
"username_placeholder": "Ім'я користувача",
|
||||
"password_placeholder": "Пароль",
|
||||
"remember_me": "Запам'ятати мене на цьому пристрої",
|
||||
"sign_in": "Увійти",
|
||||
"totp_title": "Двофакторна автентифікація",
|
||||
"totp_subtitle": "Введіть 6-значний код з вашого додатку автентифікації",
|
||||
"verify": "Підтвердити",
|
||||
"back_to_login": "Назад до входу",
|
||||
"open_console_login": "Відкрити вхід у консоль",
|
||||
"session_expired": "Сесію закінчено. Увійдіть знову.",
|
||||
"fill_all_fields": "Заповніть усі поля",
|
||||
"invalid_credentials": "Невірні облікові дані",
|
||||
"network_error": "Помилка мережі",
|
||||
"enter_6_digits": "Введіть усі 6 цифр",
|
||||
"invalid_code": "Невірний код"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,5 +3818,24 @@
|
||||
"open_panel": "Mở bảng điều khiển",
|
||||
"devices_count": "{count} thiết bị",
|
||||
"retry": "Thử lại"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "Máy khách Desktop từ xa",
|
||||
"subtitle": "Đăng nhập để kết nối với thiết bị của bạn",
|
||||
"username_placeholder": "Tên người dùng",
|
||||
"password_placeholder": "Mật khẩu",
|
||||
"remember_me": "Ghi nhớ tôi trên thiết bị này",
|
||||
"sign_in": "Đăng nhập",
|
||||
"totp_title": "Xác thực hai yếu tố",
|
||||
"totp_subtitle": "Nhập mã 6 chữ số từ ứng dụng xác thực của bạn",
|
||||
"verify": "Xác minh",
|
||||
"back_to_login": "Quay lại đăng nhập",
|
||||
"open_console_login": "Mở đăng nhập bảng điều khiển",
|
||||
"session_expired": "Phiên đã hết hạn. Vui lòng đăng nhập lại.",
|
||||
"fill_all_fields": "Vui lòng điền đầy đủ các trường",
|
||||
"invalid_credentials": "Thông tin đăng nhập không hợp lệ",
|
||||
"network_error": "Lỗi mạng",
|
||||
"enter_6_digits": "Nhập đủ 6 chữ số",
|
||||
"invalid_code": "Mã không hợp lệ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3816,5 +3816,24 @@
|
||||
"open_panel": "開啟主控台",
|
||||
"devices_count": "{count} 台裝置",
|
||||
"retry": "重試"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "遠端桌面用戶端",
|
||||
"subtitle": "登入以連線至您的裝置",
|
||||
"username_placeholder": "使用者名稱",
|
||||
"password_placeholder": "密碼",
|
||||
"remember_me": "在此裝置上記住我",
|
||||
"sign_in": "登入",
|
||||
"totp_title": "雙因素驗證",
|
||||
"totp_subtitle": "輸入驗證器應用程式中的6位數代碼",
|
||||
"verify": "驗證",
|
||||
"back_to_login": "返回登入",
|
||||
"open_console_login": "開啟主控台登入",
|
||||
"session_expired": "工作階段已過期,請重新登入。",
|
||||
"fill_all_fields": "請填寫所有欄位",
|
||||
"invalid_credentials": "憑證無效",
|
||||
"network_error": "網路錯誤",
|
||||
"enter_6_digits": "請輸入全部6位數字",
|
||||
"invalid_code": "代碼無效"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,5 +3812,24 @@
|
||||
"open_panel": "打开控制台",
|
||||
"devices_count": "{count} 台设备",
|
||||
"retry": "重试"
|
||||
},
|
||||
"rdclient_login": {
|
||||
"title": "远程桌面客户端",
|
||||
"subtitle": "登录以连接您的设备",
|
||||
"username_placeholder": "用户名",
|
||||
"password_placeholder": "密码",
|
||||
"remember_me": "在此设备上记住我",
|
||||
"sign_in": "登录",
|
||||
"totp_title": "双因素认证",
|
||||
"totp_subtitle": "输入身份验证应用中的6位代码",
|
||||
"verify": "验证",
|
||||
"back_to_login": "返回登录",
|
||||
"open_console_login": "打开控制台登录",
|
||||
"session_expired": "会话已过期,请重新登录。",
|
||||
"fill_all_fields": "请填写所有字段",
|
||||
"invalid_credentials": "凭据无效",
|
||||
"network_error": "网络错误",
|
||||
"enter_6_digits": "请输入全部6位数字",
|
||||
"invalid_code": "代码无效"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,81 @@ function roleHasPermission(role, permission) {
|
||||
return perms.has(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe relative return URL for RdClient pages only (/remote…).
|
||||
*/
|
||||
function isSafeRdClientReturnUrl(u) {
|
||||
if (typeof u !== 'string' || u.length === 0) return false;
|
||||
if (/[\r\n\x00]/.test(u)) return false;
|
||||
if (!u.startsWith('/remote')) return false;
|
||||
if (u.startsWith('//') || u.startsWith('/\\')) return false;
|
||||
if (u.startsWith('/remote/login')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function rdClientLoginRedirect(req) {
|
||||
const requested = req.originalUrl || req.url || '/remote';
|
||||
const returnParam = isSafeRdClientReturnUrl(requested)
|
||||
? requested
|
||||
: (isSafeRdClientReturnUrl(req.query.return) ? req.query.return : '/remote');
|
||||
return `/remote/login?return=${encodeURIComponent(returnParam)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* RdClient auth — redirects to /remote/login instead of panel /login.
|
||||
* @param {string} [permission] - optional permission (e.g. device.connect)
|
||||
*/
|
||||
function requireRdClientAuth(permission) {
|
||||
return function (req, res, next) {
|
||||
if (!req.session || !req.session.userId) {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized. Please log in.' });
|
||||
}
|
||||
return res.redirect(rdClientLoginRedirect(req));
|
||||
}
|
||||
|
||||
if (req.session.user && req.session.user.role === 'pro') {
|
||||
req.session.destroy();
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Pro accounts can only access the RustDesk desktop client API'
|
||||
});
|
||||
}
|
||||
return res.redirect(rdClientLoginRedirect(req));
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
const role = req.session.user && req.session.user.role;
|
||||
if (!roleHasPermission(role, permission)) {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(403).json({ success: false, error: `Permission denied: ${permission}` });
|
||||
}
|
||||
return res.status(403).render('errors/403', {
|
||||
title: 'Forbidden',
|
||||
message: 'You do not have permission to access this resource'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.locals.user = req.session.user;
|
||||
res.locals.emergencyMode = !!req.session.emergencyMode;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* RdClient login page — redirect away if already authenticated with device.connect.
|
||||
*/
|
||||
function rdClientGuestOnly(req, res, next) {
|
||||
const role = req.session && req.session.user && req.session.user.role;
|
||||
if (req.session && req.session.userId && role !== 'pro' && roleHasPermission(role, 'device.connect')) {
|
||||
const dest = isSafeRdClientReturnUrl(req.query.return) ? req.query.return : '/remote';
|
||||
return res.redirect(dest);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication middleware
|
||||
* Redirects to login page for HTML requests, returns 401 for API requests
|
||||
@@ -247,6 +322,9 @@ module.exports = {
|
||||
requireRole,
|
||||
requireAdmin,
|
||||
requirePermission,
|
||||
requireRdClientAuth,
|
||||
rdClientGuestOnly,
|
||||
isSafeRdClientReturnUrl,
|
||||
optionalAuth,
|
||||
guestOnly,
|
||||
roleHasPermission,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* RdClient operator login — extends desktop-login styles
|
||||
*/
|
||||
|
||||
.rdclient-login-page .dl-login-layer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rdclient-login-page .dl-lock-screen {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rdclient-login-shell {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px 32px;
|
||||
}
|
||||
|
||||
.rdclient-login-wallpaper {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a1a2e 70%, #0f3460 100%);
|
||||
}
|
||||
|
||||
.rdclient-login-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--text-primary, #e6edf3);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rdclient-login-brand .material-icons {
|
||||
font-size: 28px;
|
||||
color: var(--accent-blue, #388bfd);
|
||||
}
|
||||
|
||||
.rdclient-login-card-wrap {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.rdclient-login-heading {
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rdclient-login-heading h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #e6edf3);
|
||||
}
|
||||
|
||||
.rdclient-login-heading p {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
}
|
||||
|
||||
.rdclient-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0 12px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rdclient-remember input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent-blue, #388bfd);
|
||||
}
|
||||
|
||||
.rdclient-login-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
}
|
||||
|
||||
.rdclient-login-console-link {
|
||||
color: var(--accent-blue, #388bfd);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rdclient-login-console-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* BetterDesk RdClient — operator login (/remote/login)
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_GRADIENT = 'linear-gradient(135deg, #0d1117 0%, #161b22 40%, #1a1a2e 70%, #0f3460 100%)';
|
||||
|
||||
function t(key, fallback) {
|
||||
if (typeof _ === 'function') {
|
||||
var v = _(key);
|
||||
if (v && v !== key) return v;
|
||||
}
|
||||
return fallback || key;
|
||||
}
|
||||
|
||||
function returnUrl() {
|
||||
var u = (window.BetterDesk && window.BetterDesk.returnUrl) || '/remote';
|
||||
if (typeof u !== 'string' || !u.startsWith('/remote')) return '/remote';
|
||||
return u;
|
||||
}
|
||||
|
||||
function redirectAfterLogin() {
|
||||
window.location.href = returnUrl();
|
||||
}
|
||||
|
||||
function showError(el, textEl, msg) {
|
||||
if (el) el.classList.remove('hidden');
|
||||
if (textEl) textEl.textContent = msg;
|
||||
}
|
||||
|
||||
function hideError(el) {
|
||||
if (el) el.classList.add('hidden');
|
||||
}
|
||||
|
||||
function preloadWallpaper() {
|
||||
var el = document.getElementById('rdclient-wallpaper');
|
||||
if (!el) return;
|
||||
el.style.background = DEFAULT_GRADIENT;
|
||||
}
|
||||
|
||||
async function prefillCredentials() {
|
||||
var store = window.RdClientSecureStore;
|
||||
if (!store) return;
|
||||
var usernameInput = document.getElementById('rdclient-username');
|
||||
var passwordInput = document.getElementById('rdclient-password');
|
||||
var rememberInput = document.getElementById('rdclient-remember');
|
||||
if (!usernameInput || !passwordInput) return;
|
||||
|
||||
try {
|
||||
var creds = await store.loadCredentials();
|
||||
if (creds.username) usernameInput.value = creds.username;
|
||||
if (creds.password) {
|
||||
passwordInput.value = creds.password;
|
||||
if (rememberInput) rememberInput.checked = true;
|
||||
} else if (rememberInput) {
|
||||
rememberInput.checked = await store.hasStoredPassword();
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
function checkSessionExpired() {
|
||||
if (!(window.BetterDesk && window.BetterDesk.sessionExpired)) return;
|
||||
showError(
|
||||
document.getElementById('rdclient-error'),
|
||||
document.getElementById('rdclient-error-text'),
|
||||
t('rdclient_login.session_expired', 'Session expired. Please sign in again.')
|
||||
);
|
||||
}
|
||||
|
||||
function bindLoginForm() {
|
||||
var form = document.getElementById('rdclient-login-form');
|
||||
var submitBtn = document.getElementById('rdclient-submit-btn');
|
||||
var errorEl = document.getElementById('rdclient-error');
|
||||
var errorText = document.getElementById('rdclient-error-text');
|
||||
var passwordToggle = document.getElementById('rdclient-password-toggle');
|
||||
var passwordInput = document.getElementById('rdclient-password');
|
||||
var csrfToken = (window.BetterDesk && window.BetterDesk.csrfToken) || '';
|
||||
|
||||
if (passwordToggle && passwordInput) {
|
||||
passwordToggle.addEventListener('click', function () {
|
||||
var isPassword = passwordInput.type === 'password';
|
||||
passwordInput.type = isPassword ? 'text' : 'password';
|
||||
passwordToggle.querySelector('.material-icons').textContent =
|
||||
isPassword ? 'visibility_off' : 'visibility';
|
||||
});
|
||||
}
|
||||
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var username = (document.getElementById('rdclient-username').value || '').trim();
|
||||
var password = (document.getElementById('rdclient-password').value || '');
|
||||
var remember = document.getElementById('rdclient-remember');
|
||||
var rememberChecked = remember ? remember.checked : false;
|
||||
|
||||
if (!username || !password) {
|
||||
showError(errorEl, errorText, t('rdclient_login.fill_all_fields', 'Please fill in all fields'));
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
hideError(errorEl);
|
||||
|
||||
fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ username: username, password: password })
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (data) { return { data: data }; }); })
|
||||
.then(function (result) {
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
|
||||
if (result.data.success && result.data.totpRequired) {
|
||||
showTotpForm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data.success) {
|
||||
var store = window.RdClientSecureStore;
|
||||
if (store) {
|
||||
store.saveCredentials(username, password, rememberChecked)
|
||||
.finally(redirectAfterLogin);
|
||||
} else {
|
||||
redirectAfterLogin();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showError(errorEl, errorText, result.data.error || t('rdclient_login.invalid_credentials', 'Invalid credentials'));
|
||||
})
|
||||
.catch(function () {
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
showError(errorEl, errorText, t('rdclient_login.network_error', 'Network error'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showTotpForm() {
|
||||
document.getElementById('rdclient-login-form').classList.add('hidden');
|
||||
var totpForm = document.getElementById('rdclient-totp-form');
|
||||
if (totpForm) {
|
||||
totpForm.classList.remove('hidden');
|
||||
var first = totpForm.querySelector('.dl-totp-digit[data-idx="0"]');
|
||||
if (first) setTimeout(function () { first.focus(); }, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function hideTotpForm() {
|
||||
document.getElementById('rdclient-login-form').classList.remove('hidden');
|
||||
document.getElementById('rdclient-totp-form').classList.add('hidden');
|
||||
document.querySelectorAll('#rdclient-totp-form .dl-totp-digit').forEach(function (d) { d.value = ''; });
|
||||
}
|
||||
|
||||
function getTotpCode() {
|
||||
var code = '';
|
||||
document.querySelectorAll('#rdclient-totp-form .dl-totp-digit').forEach(function (d) { code += d.value; });
|
||||
return code;
|
||||
}
|
||||
|
||||
function bindTotpForm() {
|
||||
var form = document.getElementById('rdclient-totp-form');
|
||||
var submitBtn = document.getElementById('rdclient-totp-submit');
|
||||
var errorEl = document.getElementById('rdclient-totp-error');
|
||||
var errorText = document.getElementById('rdclient-totp-error-text');
|
||||
var backLink = document.getElementById('rdclient-totp-back-link');
|
||||
var csrfToken = (window.BetterDesk && window.BetterDesk.csrfToken) || '';
|
||||
|
||||
if (!form) return;
|
||||
|
||||
var digits = form.querySelectorAll('.dl-totp-digit');
|
||||
digits.forEach(function (digit, idx) {
|
||||
digit.addEventListener('input', function () {
|
||||
var val = digit.value.replace(/\D/g, '');
|
||||
digit.value = val.substring(0, 1);
|
||||
if (val && idx < 5) digits[idx + 1].focus();
|
||||
if (getTotpCode().length === 6) submitTotp();
|
||||
});
|
||||
digit.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Backspace' && !digit.value && idx > 0) {
|
||||
digits[idx - 1].focus();
|
||||
digits[idx - 1].value = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
submitTotp();
|
||||
});
|
||||
|
||||
function submitTotp() {
|
||||
var code = getTotpCode();
|
||||
if (code.length !== 6) {
|
||||
showError(errorEl, errorText, t('rdclient_login.enter_6_digits', 'Enter all 6 digits'));
|
||||
return;
|
||||
}
|
||||
submitBtn.classList.add('loading');
|
||||
submitBtn.disabled = true;
|
||||
hideError(errorEl);
|
||||
|
||||
fetch('/api/auth/totp/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ code: code })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
if (data.success) {
|
||||
redirectAfterLogin();
|
||||
return;
|
||||
}
|
||||
showError(errorEl, errorText, data.error || t('rdclient_login.invalid_code', 'Invalid code'));
|
||||
document.querySelectorAll('#rdclient-totp-form .dl-totp-digit').forEach(function (d) { d.value = ''; });
|
||||
if (digits[0]) digits[0].focus();
|
||||
})
|
||||
.catch(function () {
|
||||
submitBtn.classList.remove('loading');
|
||||
submitBtn.disabled = false;
|
||||
showError(errorEl, errorText, t('rdclient_login.network_error', 'Network error'));
|
||||
});
|
||||
}
|
||||
|
||||
if (backLink) {
|
||||
backLink.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
hideTotpForm();
|
||||
hideError(errorEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
preloadWallpaper();
|
||||
prefillCredentials();
|
||||
checkSessionExpired();
|
||||
bindLoginForm();
|
||||
bindTotpForm();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* BetterDesk RdClient — encrypted credential storage (browser).
|
||||
* Passwords are stored in IndexedDB with AES-GCM (Web Crypto); never plaintext in localStorage.
|
||||
*/
|
||||
var RdClientSecureStore = (function () {
|
||||
'use strict';
|
||||
|
||||
var DB_NAME = 'betterdesk-rdclient';
|
||||
var DB_VERSION = 1;
|
||||
var STORE = 'vault';
|
||||
var KEY_ID = 'device-key';
|
||||
var CRED_ID = 'credentials';
|
||||
var USERNAME_KEY = 'bd_rdclient_last_user';
|
||||
|
||||
function openDb() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!window.indexedDB) {
|
||||
reject(new Error('IndexedDB unavailable'));
|
||||
return;
|
||||
}
|
||||
var req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onerror = function () { reject(req.error || new Error('IndexedDB open failed')); };
|
||||
req.onupgradeneeded = function (ev) {
|
||||
var db = ev.target.result;
|
||||
if (!db.objectStoreNames.contains(STORE)) {
|
||||
db.createObjectStore(STORE);
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
});
|
||||
}
|
||||
|
||||
function idbGet(db, key) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tx = db.transaction(STORE, 'readonly');
|
||||
var req = tx.objectStore(STORE).get(key);
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () { reject(req.error); };
|
||||
});
|
||||
}
|
||||
|
||||
function idbPut(db, key, value) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tx = db.transaction(STORE, 'readwrite');
|
||||
var req = tx.objectStore(STORE).put(value, key);
|
||||
req.onsuccess = function () { resolve(); };
|
||||
req.onerror = function () { reject(req.error); };
|
||||
});
|
||||
}
|
||||
|
||||
function idbDelete(db, key) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tx = db.transaction(STORE, 'readwrite');
|
||||
var req = tx.objectStore(STORE).delete(key);
|
||||
req.onsuccess = function () { resolve(); };
|
||||
req.onerror = function () { reject(req.error); };
|
||||
});
|
||||
}
|
||||
|
||||
function bufToB64(buf) {
|
||||
var bytes = new Uint8Array(buf);
|
||||
var bin = '';
|
||||
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
function b64ToBuf(b64) {
|
||||
var bin = atob(b64);
|
||||
var bytes = new Uint8Array(bin.length);
|
||||
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
async function getOrCreateDeviceKey(db) {
|
||||
var existing = await idbGet(db, KEY_ID);
|
||||
if (existing && existing.key) {
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
b64ToBuf(existing.key),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
var raw = crypto.getRandomValues(new Uint8Array(32));
|
||||
await idbPut(db, KEY_ID, { key: bufToB64(raw.buffer) });
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
raw,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
async function encryptPassword(key, password) {
|
||||
var iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
var enc = new TextEncoder().encode(password);
|
||||
var cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, enc);
|
||||
return { iv: bufToB64(iv.buffer), data: bufToB64(cipher) };
|
||||
}
|
||||
|
||||
async function decryptPassword(key, payload) {
|
||||
if (!payload || !payload.iv || !payload.data) return '';
|
||||
var iv = new Uint8Array(b64ToBuf(payload.iv));
|
||||
var data = b64ToBuf(payload.data);
|
||||
var plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, data);
|
||||
return new TextDecoder().decode(plain);
|
||||
}
|
||||
|
||||
function saveLastUsername(username) {
|
||||
try {
|
||||
if (username) localStorage.setItem(USERNAME_KEY, username);
|
||||
else localStorage.removeItem(USERNAME_KEY);
|
||||
} catch (_) { /* quota / private mode */ }
|
||||
}
|
||||
|
||||
function loadLastUsername() {
|
||||
try {
|
||||
return localStorage.getItem(USERNAME_KEY) || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCredentials(username, password, rememberPassword) {
|
||||
saveLastUsername(username);
|
||||
if (!rememberPassword || !password) {
|
||||
await clearStoredPassword();
|
||||
return;
|
||||
}
|
||||
if (!window.crypto || !window.crypto.subtle) return;
|
||||
var db = await openDb();
|
||||
try {
|
||||
var key = await getOrCreateDeviceKey(db);
|
||||
var encrypted = await encryptPassword(key, password);
|
||||
await idbPut(db, CRED_ID, {
|
||||
username: username,
|
||||
password: encrypted,
|
||||
rememberPassword: true,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function clearStoredPassword() {
|
||||
if (!window.indexedDB) return;
|
||||
var db = await openDb();
|
||||
try {
|
||||
await idbDelete(db, CRED_ID);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCredentials() {
|
||||
saveLastUsername('');
|
||||
await clearStoredPassword();
|
||||
}
|
||||
|
||||
async function loadCredentials() {
|
||||
var username = loadLastUsername();
|
||||
var result = { username: username, password: '', rememberPassword: false };
|
||||
if (!window.crypto || !window.crypto.subtle || !window.indexedDB) {
|
||||
return result;
|
||||
}
|
||||
var db = await openDb();
|
||||
try {
|
||||
var record = await idbGet(db, CRED_ID);
|
||||
if (!record || !record.rememberPassword || !record.password) {
|
||||
return result;
|
||||
}
|
||||
var key = await getOrCreateDeviceKey(db);
|
||||
var password = await decryptPassword(key, record.password);
|
||||
result.username = record.username || username;
|
||||
result.password = password;
|
||||
result.rememberPassword = true;
|
||||
return result;
|
||||
} catch (_) {
|
||||
return result;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function hasStoredPassword() {
|
||||
if (!window.indexedDB) return false;
|
||||
var db = await openDb();
|
||||
try {
|
||||
var record = await idbGet(db, CRED_ID);
|
||||
return !!(record && record.rememberPassword && record.password);
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
saveCredentials: saveCredentials,
|
||||
loadCredentials: loadCredentials,
|
||||
clearCredentials: clearCredentials,
|
||||
clearStoredPassword: clearStoredPassword,
|
||||
hasStoredPassword: hasStoredPassword,
|
||||
loadLastUsername: loadLastUsername
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.RdClientSecureStore = RdClientSecureStore;
|
||||
}
|
||||
@@ -252,8 +252,13 @@ const Utils = {
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
// Redirect to login on auth error
|
||||
window.location.href = '/login';
|
||||
var path = window.location.pathname || '';
|
||||
if (path.startsWith('/remote') && !path.startsWith('/remote/login')) {
|
||||
window.location.href = '/remote/login?return=' +
|
||||
encodeURIComponent(path + (window.location.search || '')) + '&expired=1';
|
||||
} else {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const db = require('../services/database');
|
||||
const config = require('../config/config');
|
||||
const { requireAuth, requirePermission } = require('../middleware/auth');
|
||||
const { requireRdClientAuth, rdClientGuestOnly, isSafeRdClientReturnUrl } = require('../middleware/auth');
|
||||
|
||||
// Lazy-loaded relay helper — avoid circular require at module load time
|
||||
function getRemoteRelay() {
|
||||
@@ -25,10 +25,24 @@ try {
|
||||
console.warn('Warning: Could not read server public key:', err.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /remote/login - RdClient operator login (when panel session expired)
|
||||
*/
|
||||
router.get('/remote/login', rdClientGuestOnly, (req, res) => {
|
||||
const returnUrl = isSafeRdClientReturnUrl(req.query.return) ? req.query.return : '/remote';
|
||||
const sessionExpired = req.query.expired === '1' || req.query.expired === 'true';
|
||||
res.render('rdclient-login', {
|
||||
title: req.t('rdclient_login.title'),
|
||||
activePage: 'remote',
|
||||
returnUrl,
|
||||
sessionExpired,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /remote - RdClient operator dashboard (device list + connect)
|
||||
*/
|
||||
router.get('/remote', requireAuth, requirePermission('device.connect'), (req, res) => {
|
||||
router.get('/remote', requireRdClientAuth('device.connect'), (req, res) => {
|
||||
res.render('remote-dashboard', {
|
||||
title: req.t('remote_dashboard.title'),
|
||||
activePage: 'remote',
|
||||
@@ -37,25 +51,11 @@ router.get('/remote', requireAuth, requirePermission('device.connect'), (req, re
|
||||
|
||||
/**
|
||||
* GET /remote/:deviceId - Unified remote desktop viewer (single entry point).
|
||||
*
|
||||
* Phase 2.1 of the unification plan: this route is now the only canonical
|
||||
* URL for browser-based remote desktop. The transport (RustDesk relay vs.
|
||||
* CDAP WebSocket) is auto-detected on the server by probing the Go server
|
||||
* for `device_type` and `cdap_connected`. The decision is then passed to
|
||||
* the appropriate template.
|
||||
*
|
||||
* Query overrides:
|
||||
* ?transport=cdap → force CDAP transport (skip auto-probe)
|
||||
* ?transport=rd → force RustDesk transport
|
||||
*
|
||||
* Until the unified `remote.ejs` shell lands (PR 2.2 / 2.3) we still render
|
||||
* the existing two templates underneath. Operators get a single URL and
|
||||
* shareable links work regardless of which transport is active.
|
||||
*/
|
||||
router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
router.get('/remote/:deviceId', requireRdClientAuth('device.connect'), async (req, res) => {
|
||||
const deviceId = req.params.deviceId;
|
||||
|
||||
if (!deviceId || !/^[A-Za-z0-9_-]{3,64}$/.test(deviceId)) {
|
||||
if (!deviceId || deviceId === 'login' || !/^[A-Za-z0-9_-]{3,64}$/.test(deviceId)) {
|
||||
return res.redirect('/devices');
|
||||
}
|
||||
|
||||
@@ -66,8 +66,6 @@ router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
// Database lookup failure is non-blocking - viewer can still work
|
||||
}
|
||||
|
||||
// Probe Go server for authoritative transport hint. Local panel DB
|
||||
// does not carry `device_type` or `cdap_connected`.
|
||||
let isOsAgent = false;
|
||||
let isCdapConnected = false;
|
||||
let goPeer = null;
|
||||
@@ -80,7 +78,6 @@ router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
}
|
||||
} catch { /* non-fatal: degrade to standard viewer */ }
|
||||
|
||||
// Resolve transport: explicit query param wins, then auto-detect.
|
||||
const forced = String(req.query.transport || '').toLowerCase();
|
||||
let transport;
|
||||
if (forced === 'cdap' || forced === 'rd') {
|
||||
@@ -91,8 +88,6 @@ router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
transport = 'rd';
|
||||
}
|
||||
|
||||
// Capability hints exposed to the browser so the unified UI can light
|
||||
// up the right toolbar buttons.
|
||||
const capabilities = {
|
||||
transport,
|
||||
os_agent: isOsAgent,
|
||||
@@ -100,11 +95,6 @@ router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
device_type: goPeer && goPeer.device_type ? String(goPeer.device_type) : '',
|
||||
};
|
||||
|
||||
// PR 2.2/2.3 unification: a single canonical web client (`remote.ejs`)
|
||||
// serves both transports. The browser branches on
|
||||
// `window.__capabilities.transport`. The legacy `remote-cdap` template
|
||||
// is no longer rendered; its inline widget remains usable from
|
||||
// device-detail panels via `cdap-desktop.js` directly.
|
||||
res.render('remote', {
|
||||
title: `${req.t('remote.title')} - ${deviceId}`,
|
||||
activePage: 'remote',
|
||||
@@ -118,12 +108,8 @@ router.get('/remote/:deviceId', requireAuth, async (req, res) => {
|
||||
|
||||
/**
|
||||
* GET /remote-cdap/:deviceId - Legacy alias, redirects to unified entry.
|
||||
*
|
||||
* Kept for backwards compatibility with existing bookmarks, deep links, and
|
||||
* the `devices.js` "Connect" button. New code should link to
|
||||
* `/remote/:deviceId` directly.
|
||||
*/
|
||||
router.get('/remote-cdap/:deviceId', requireAuth, (req, res) => {
|
||||
router.get('/remote-cdap/:deviceId', requireRdClientAuth('device.connect'), (req, res) => {
|
||||
const deviceId = req.params.deviceId;
|
||||
if (deviceId && /^[A-Za-z0-9_-]{3,64}$/.test(deviceId)) {
|
||||
return res.redirect(`/remote/${encodeURIComponent(deviceId)}?transport=cdap`);
|
||||
@@ -133,12 +119,8 @@ router.get('/remote-cdap/:deviceId', requireAuth, (req, res) => {
|
||||
|
||||
/**
|
||||
* GET /remote-desktop/:deviceId - Legacy route, redirects to unified /remote/:deviceId
|
||||
*
|
||||
* Previously served a separate JPEG stream viewer. The web remote client has
|
||||
* been unified: `/remote/:deviceId` is now the only canonical entry point for
|
||||
* browser-based remote desktop.
|
||||
*/
|
||||
router.get('/remote-desktop/:deviceId', requireAuth, (req, res) => {
|
||||
router.get('/remote-desktop/:deviceId', requireRdClientAuth('device.connect'), (req, res) => {
|
||||
const deviceId = req.params.deviceId;
|
||||
if (deviceId && /^[A-Za-z0-9_-]{3,64}$/.test(deviceId)) {
|
||||
return res.redirect(`/remote/${encodeURIComponent(deviceId)}`);
|
||||
@@ -149,7 +131,7 @@ router.get('/remote-desktop/:deviceId', requireAuth, (req, res) => {
|
||||
/**
|
||||
* GET /api/remote/sessions - List active native remote sessions
|
||||
*/
|
||||
router.get('/api/remote/sessions', requireAuth, (req, res) => {
|
||||
router.get('/api/remote/sessions', requireRdClientAuth(), (req, res) => {
|
||||
const relay = getRemoteRelay();
|
||||
if (!relay) return res.json({ sessions: [] });
|
||||
const sessions = relay.getActiveSessions();
|
||||
@@ -159,7 +141,7 @@ router.get('/api/remote/sessions', requireAuth, (req, res) => {
|
||||
/**
|
||||
* GET /api/remote/session/:deviceId - Get state of a single native remote session
|
||||
*/
|
||||
router.get('/api/remote/session/:deviceId', requireAuth, (req, res) => {
|
||||
router.get('/api/remote/session/:deviceId', requireRdClientAuth(), (req, res) => {
|
||||
const relay = getRemoteRelay();
|
||||
if (!relay) return res.status(404).json({ error: 'Remote relay not available' });
|
||||
const state = relay.getSessionState(req.params.deviceId);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
const request = require('supertest');
|
||||
const { createTestApp } = require('./helpers');
|
||||
|
||||
const { requireAuth, requireRole, guestOnly } = require('../middleware/auth');
|
||||
const { requireAuth, requireRole, guestOnly, requireRdClientAuth, rdClientGuestOnly, isSafeRdClientReturnUrl } = require('../middleware/auth');
|
||||
|
||||
describe('Auth Middleware', () => {
|
||||
describe('requireAuth', () => {
|
||||
@@ -141,4 +141,74 @@ describe('Auth Middleware', () => {
|
||||
expect(res.headers.location).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireRdClientAuth', () => {
|
||||
it('should redirect unauthenticated HTML requests to /remote/login', async () => {
|
||||
const app = createTestApp();
|
||||
app.get('/remote', requireRdClientAuth('device.connect'), (_req, res) => {
|
||||
res.send('OK');
|
||||
});
|
||||
|
||||
const res = await request(app).get('/remote');
|
||||
|
||||
expect(res.status).toBe(302);
|
||||
expect(res.headers.location).toBe('/remote/login?return=%2Fremote');
|
||||
});
|
||||
|
||||
it('should return 401 for unauthenticated API requests', async () => {
|
||||
const app = createTestApp();
|
||||
app.get('/api/remote/sessions', requireRdClientAuth(), (_req, res) => {
|
||||
res.json({ sessions: [] });
|
||||
});
|
||||
|
||||
const res = await request(app).get('/api/remote/sessions');
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should pass through for authenticated operator with permission', async () => {
|
||||
const app = createTestApp();
|
||||
app.use((req, _res, next) => {
|
||||
req.session.userId = 2;
|
||||
req.session.user = { id: 2, username: 'operator1', role: 'operator' };
|
||||
next();
|
||||
});
|
||||
app.get('/remote', requireRdClientAuth('device.connect'), (_req, res) => {
|
||||
res.send('OK');
|
||||
});
|
||||
|
||||
const res = await request(app).get('/remote');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSafeRdClientReturnUrl', () => {
|
||||
it('should accept /remote paths only', () => {
|
||||
expect(isSafeRdClientReturnUrl('/remote')).toBe(true);
|
||||
expect(isSafeRdClientReturnUrl('/remote/abc123')).toBe(true);
|
||||
expect(isSafeRdClientReturnUrl('/login')).toBe(false);
|
||||
expect(isSafeRdClientReturnUrl('/remote/login')).toBe(false);
|
||||
expect(isSafeRdClientReturnUrl('//evil.com/remote')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rdClientGuestOnly', () => {
|
||||
it('should redirect authenticated operator to return URL', async () => {
|
||||
const app = createTestApp();
|
||||
app.use((req, _res, next) => {
|
||||
req.session.userId = 2;
|
||||
req.session.user = { id: 2, username: 'operator1', role: 'operator' };
|
||||
next();
|
||||
});
|
||||
app.get('/remote/login', rdClientGuestOnly, (_req, res) => {
|
||||
res.send('login');
|
||||
});
|
||||
|
||||
const res = await request(app).get('/remote/login?return=%2Fremote%2Fdev1');
|
||||
|
||||
expect(res.status).toBe(302);
|
||||
expect(res.headers.location).toBe('/remote/dev1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="<%= lang || 'en' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><%= _('rdclient_login.title') %> - <%= appName %></title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/branding/favicon.svg">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="/css/theme.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/branding.css?v=<%= typeof brandingRevision !== 'undefined' ? brandingRevision : cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/desktop-login.css?v=<%= cacheVersion %>">
|
||||
<link rel="stylesheet" href="/css/rdclient-login.css?v=<%= cacheVersion %>">
|
||||
</head>
|
||||
<body class="desktop-login-page rdclient-login-page">
|
||||
<div class="dl-wallpaper rdclient-login-wallpaper" id="rdclient-wallpaper">
|
||||
<div class="dl-wallpaper-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="rdclient-login-shell">
|
||||
<div class="rdclient-login-brand" id="rdclient-login-brand">
|
||||
<span class="material-icons">connected_tv</span>
|
||||
<span><%= appName %></span>
|
||||
</div>
|
||||
|
||||
<div class="dl-card-wrapper rdclient-login-card-wrap">
|
||||
<div class="dl-card" id="rdclient-login-card">
|
||||
<div class="rdclient-login-heading">
|
||||
<h1><%= _('rdclient_login.title') %></h1>
|
||||
<p><%= _('rdclient_login.subtitle') %></p>
|
||||
</div>
|
||||
|
||||
<div class="dl-avatar" id="rdclient-avatar">
|
||||
<span class="material-icons">person</span>
|
||||
</div>
|
||||
|
||||
<form class="dl-form" id="rdclient-login-form">
|
||||
<div class="dl-field">
|
||||
<input type="text" id="rdclient-username" class="dl-input"
|
||||
placeholder="<%= _('rdclient_login.username_placeholder') %>"
|
||||
autocomplete="username" required autofocus>
|
||||
</div>
|
||||
<div class="dl-field">
|
||||
<div class="dl-password-wrapper">
|
||||
<input type="password" id="rdclient-password" class="dl-input"
|
||||
placeholder="<%= _('rdclient_login.password_placeholder') %>"
|
||||
autocomplete="current-password" required>
|
||||
<button type="button" class="dl-password-toggle" id="rdclient-password-toggle">
|
||||
<span class="material-icons">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="rdclient-remember">
|
||||
<input type="checkbox" id="rdclient-remember" checked>
|
||||
<span><%= _('rdclient_login.remember_me') %></span>
|
||||
</label>
|
||||
|
||||
<div class="dl-error hidden" id="rdclient-error">
|
||||
<span class="material-icons">error</span>
|
||||
<span id="rdclient-error-text"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="dl-submit" id="rdclient-submit-btn">
|
||||
<span class="dl-submit-text"><%= _('rdclient_login.sign_in') %></span>
|
||||
<span class="dl-submit-arrow"><span class="material-icons">arrow_forward</span></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form class="dl-form hidden" id="rdclient-totp-form">
|
||||
<div class="dl-totp-header">
|
||||
<span class="material-icons dl-totp-icon">security</span>
|
||||
<div class="dl-totp-title"><%= _('rdclient_login.totp_title') %></div>
|
||||
<div class="dl-totp-subtitle"><%= _('rdclient_login.totp_subtitle') %></div>
|
||||
</div>
|
||||
<div class="dl-totp-inputs" id="rdclient-totp-inputs">
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="0">
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="1">
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="2">
|
||||
<span class="dl-totp-separator">–</span>
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="3">
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="4">
|
||||
<input type="text" class="dl-totp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]" data-idx="5">
|
||||
</div>
|
||||
<div class="dl-error hidden" id="rdclient-totp-error">
|
||||
<span class="material-icons">error</span>
|
||||
<span id="rdclient-totp-error-text"></span>
|
||||
</div>
|
||||
<button type="submit" class="dl-submit" id="rdclient-totp-submit">
|
||||
<span class="dl-submit-text"><%= _('rdclient_login.verify') %></span>
|
||||
<span class="dl-submit-arrow"><span class="material-icons">verified_user</span></span>
|
||||
</button>
|
||||
<div class="dl-totp-back">
|
||||
<a href="#" id="rdclient-totp-back-link">
|
||||
<span class="material-icons" style="font-size:16px">arrow_back</span>
|
||||
<%= _('rdclient_login.back_to_login') %>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rdclient-login-footer">
|
||||
<a href="/login" class="rdclient-login-console-link"><%= _('rdclient_login.open_console_login') %></a>
|
||||
<span><%= appName %> v<%= appVersion %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="<%= cspNonce %>">
|
||||
window.BetterDesk = {
|
||||
csrfToken: '<%= csrfToken || "" %>',
|
||||
lang: '<%= lang || "en" %>',
|
||||
translations: <%- JSON.stringify(translations || {}) %>,
|
||||
branding: <%- JSON.stringify(branding || {}) %>,
|
||||
returnUrl: <%- JSON.stringify(typeof returnUrl !== 'undefined' ? returnUrl : '/remote') %>,
|
||||
sessionExpired: <%= sessionExpired ? 'true' : 'false' %>
|
||||
};
|
||||
</script>
|
||||
<script src="/js/i18n-client.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclientSecureStore.js?v=<%= cacheVersion %>"></script>
|
||||
<script src="/js/rdclient-login.js?v=<%= cacheVersion %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user