feat: add device code auth flow (#12697)

* add i18n

* fix types
This commit is contained in:
Arvin Xu 2026-03-04 23:58:41 +08:00 committed by GitHub
parent 08b23a9732
commit ab376d9185
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 684 additions and 4 deletions

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "قراءة بياناتك المتزامنة",
"consent.scope.sync-write": "كتابة وتحديث بياناتك المتزامنة",
"consent.title": "تفويض {{clientName}}",
"device.confirm.authorize": "السماح",
"device.confirm.codeHint": "تأكد من أن هذا الرمز يطابق الرمز المعروض في جهازك الطرفي.",
"device.confirm.deny": "رفض",
"device.confirm.description": "{{clientName}} يطلب الوصول",
"device.confirm.title": "السماح للجهاز",
"device.error.aborted": "تم رفض التفويض.",
"device.error.alreadyUsed": "تم استخدام هذا الرمز بالفعل. يرجى طلب رمز جديد.",
"device.error.expired": "انتهت صلاحية هذا الرمز. يرجى طلب رمز جديد.",
"device.error.noCode": "لم يتم تقديم رمز الجهاز. يرجى إدخال رمز صالح.",
"device.error.notFound": "رمز غير صالح. يرجى التحقق والمحاولة مرة أخرى.",
"device.error.unknown": "حدث خطأ. يرجى المحاولة مرة أخرى.",
"device.input.description": "أدخل الرمز المعروض على جهازك للسماح بالوصول.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "إرسال",
"device.input.title": "أدخل رمز الجهاز",
"device.success.description": "لقد قمت بالسماح للجهاز بنجاح. يمكنك إغلاق علامة التبويب هذه والعودة إلى جهازك الطرفي.",
"device.success.title": "تم السماح بنجاح",
"error.backToHome": "العودة إلى الصفحة الرئيسية",
"error.desc": "فشل تفويض OAuth، السبب: {{reason}}",
"error.reason.internal_error": "خطأ داخلي في الخادم",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Прочитане на синхронизираните ви данни",
"consent.scope.sync-write": "Запис и актуализация на синхронизираните ви данни",
"consent.title": "Разрешаване на {{clientName}}",
"device.confirm.authorize": "Разреши",
"device.confirm.codeHint": "Потвърдете, че този код съвпада с показания на вашия терминал.",
"device.confirm.deny": "Откажи",
"device.confirm.description": "{{clientName}} иска достъп",
"device.confirm.title": "Разрешаване на устройство",
"device.error.aborted": "Разрешението беше отказано.",
"device.error.alreadyUsed": "Този код вече е използван. Моля, поискайте нов код.",
"device.error.expired": "Този код е изтекъл. Моля, поискайте нов код.",
"device.error.noCode": "Не е предоставен код за устройство. Моля, въведете валиден код.",
"device.error.notFound": "Невалиден код. Моля, проверете и опитайте отново.",
"device.error.unknown": "Възникна грешка. Моля, опитайте отново.",
"device.input.description": "Въведете кода, показан на вашето устройство, за да разрешите достъп.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Изпрати",
"device.input.title": "Въведете код за устройство",
"device.success.description": "Успешно разрешихте устройството. Можете да затворите този раздел на браузъра и да се върнете към вашия терминал.",
"device.success.title": "Успешно разрешение",
"error.backToHome": "Обратно към началната страница",
"error.desc": "OAuth оторизацията не бе успешна, причина: {{reason}}",
"error.reason.internal_error": "Вътрешна грешка на сървъра",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Lesen Ihrer synchronisierten Daten",
"consent.scope.sync-write": "Schreiben und Aktualisieren Ihrer synchronisierten Daten",
"consent.title": "{{clientName}} autorisieren",
"device.confirm.authorize": "Autorisieren",
"device.confirm.codeHint": "Bestätigen Sie, dass dieser Code mit dem auf Ihrem Terminal angezeigten übereinstimmt.",
"device.confirm.deny": "Ablehnen",
"device.confirm.description": "{{clientName}} fordert Zugriff an",
"device.confirm.title": "Gerät autorisieren",
"device.error.aborted": "Die Autorisierung wurde abgelehnt.",
"device.error.alreadyUsed": "Dieser Code wurde bereits verwendet. Bitte fordern Sie einen neuen Code an.",
"device.error.expired": "Dieser Code ist abgelaufen. Bitte fordern Sie einen neuen Code an.",
"device.error.noCode": "Es wurde kein Gerätecode bereitgestellt. Bitte geben Sie einen gültigen Code ein.",
"device.error.notFound": "Ungültiger Code. Bitte überprüfen Sie den Code und versuchen Sie es erneut.",
"device.error.unknown": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
"device.input.description": "Geben Sie den auf Ihrem Gerät angezeigten Code ein, um den Zugriff zu autorisieren.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Absenden",
"device.input.title": "Gerätecode eingeben",
"device.success.description": "Sie haben das Gerät erfolgreich autorisiert. Sie können diesen Browser-Tab schließen und zu Ihrem Terminal zurückkehren.",
"device.success.title": "Autorisierung erfolgreich",
"error.backToHome": "Zurück zur Startseite",
"error.desc": "OAuth-Autorisierung fehlgeschlagen, Grund: {{reason}}",
"error.reason.internal_error": "Interner Serverfehler",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Read your synchronized data",
"consent.scope.sync-write": "Write and update your synchronized data",
"consent.title": "Authorize {{clientName}}",
"device.confirm.authorize": "Authorize",
"device.confirm.codeHint": "Confirm that this code matches the one shown in your terminal.",
"device.confirm.deny": "Deny",
"device.confirm.description": "{{clientName}} is requesting access",
"device.confirm.title": "Authorize Device",
"device.error.aborted": "Authorization was denied.",
"device.error.alreadyUsed": "This code has already been used. Please request a new code.",
"device.error.expired": "This code has expired. Please request a new code.",
"device.error.noCode": "No device code was provided. Please enter a valid code.",
"device.error.notFound": "Invalid code. Please check and try again.",
"device.error.unknown": "An error occurred. Please try again.",
"device.input.description": "Enter the code displayed on your device to authorize access.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Submit",
"device.input.title": "Enter Device Code",
"device.success.description": "You have successfully authorized the device. You can close this browser tab and return to your terminal.",
"device.success.title": "Authorization Successful",
"error.backToHome": "Back to Home",
"error.desc": "OAuth authorization failed, reason: {{reason}}",
"error.reason.internal_error": "Internal Server Error",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Leer tus datos sincronizados",
"consent.scope.sync-write": "Escribir y actualizar tus datos sincronizados",
"consent.title": "Autorizar a {{clientName}}",
"device.confirm.authorize": "Autorizar",
"device.confirm.codeHint": "Confirma que este código coincide con el que se muestra en tu terminal.",
"device.confirm.deny": "Denegar",
"device.confirm.description": "{{clientName}} está solicitando acceso",
"device.confirm.title": "Autorizar Dispositivo",
"device.error.aborted": "La autorización fue denegada.",
"device.error.alreadyUsed": "Este código ya ha sido utilizado. Por favor, solicita un nuevo código.",
"device.error.expired": "Este código ha expirado. Por favor, solicita un nuevo código.",
"device.error.noCode": "No se proporcionó un código de dispositivo. Por favor, ingresa un código válido.",
"device.error.notFound": "Código inválido. Por favor, verifica e inténtalo de nuevo.",
"device.error.unknown": "Ocurrió un error. Por favor, inténtalo de nuevo.",
"device.input.description": "Ingresa el código mostrado en tu dispositivo para autorizar el acceso.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Enviar",
"device.input.title": "Ingresar Código de Dispositivo",
"device.success.description": "Has autorizado exitosamente el dispositivo. Puedes cerrar esta pestaña del navegador y regresar a tu terminal.",
"device.success.title": "Autorización Exitosa",
"error.backToHome": "Volver al inicio",
"error.desc": "La autorización OAuth ha fallado, motivo: {{reason}}",
"error.reason.internal_error": "Error interno del servidor",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "خواندن داده‌های همگام‌سازی‌شده شما",
"consent.scope.sync-write": "نوشتن و به‌روزرسانی داده‌های همگام‌سازی‌شده شما",
"consent.title": "اجازه دادن به {{clientName}}",
"device.confirm.authorize": "مجوز دادن",
"device.confirm.codeHint": "تأیید کنید که این کد با کدی که در ترمینال شما نمایش داده شده است مطابقت دارد.",
"device.confirm.deny": "رد کردن",
"device.confirm.description": "{{clientName}} درخواست دسترسی دارد",
"device.confirm.title": "مجوز دادن به دستگاه",
"device.error.aborted": "مجوز رد شد.",
"device.error.alreadyUsed": "این کد قبلاً استفاده شده است. لطفاً یک کد جدید درخواست کنید.",
"device.error.expired": "این کد منقضی شده است. لطفاً یک کد جدید درخواست کنید.",
"device.error.noCode": "هیچ کد دستگاهی ارائه نشده است. لطفاً یک کد معتبر وارد کنید.",
"device.error.notFound": "کد نامعتبر است. لطفاً بررسی کرده و دوباره تلاش کنید.",
"device.error.unknown": "خطایی رخ داده است. لطفاً دوباره تلاش کنید.",
"device.input.description": "برای مجوز دادن به دسترسی، کدی که در دستگاه شما نمایش داده شده است را وارد کنید.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "ارسال",
"device.input.title": "کد دستگاه را وارد کنید",
"device.success.description": "شما با موفقیت دستگاه را مجوز دادید. می‌توانید این تب مرورگر را ببندید و به ترمینال خود بازگردید.",
"device.success.title": "مجوز با موفقیت انجام شد",
"error.backToHome": "بازگشت به صفحه اصلی",
"error.desc": "احراز هویت OAuth با شکست مواجه شد، دلیل: {{reason}}",
"error.reason.internal_error": "خطای داخلی سرور",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Lire vos données synchronisées",
"consent.scope.sync-write": "Écrire et mettre à jour vos données synchronisées",
"consent.title": "Autoriser {{clientName}}",
"device.confirm.authorize": "Autoriser",
"device.confirm.codeHint": "Confirmez que ce code correspond à celui affiché sur votre terminal.",
"device.confirm.deny": "Refuser",
"device.confirm.description": "{{clientName}} demande l'accès",
"device.confirm.title": "Autoriser l'appareil",
"device.error.aborted": "L'autorisation a été refusée.",
"device.error.alreadyUsed": "Ce code a déjà été utilisé. Veuillez demander un nouveau code.",
"device.error.expired": "Ce code a expiré. Veuillez demander un nouveau code.",
"device.error.noCode": "Aucun code d'appareil n'a été fourni. Veuillez entrer un code valide.",
"device.error.notFound": "Code invalide. Veuillez vérifier et réessayer.",
"device.error.unknown": "Une erreur s'est produite. Veuillez réessayer.",
"device.input.description": "Entrez le code affiché sur votre appareil pour autoriser l'accès.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Soumettre",
"device.input.title": "Entrer le code de l'appareil",
"device.success.description": "Vous avez autorisé l'appareil avec succès. Vous pouvez fermer cet onglet de navigateur et retourner à votre terminal.",
"device.success.title": "Autorisation réussie",
"error.backToHome": "Retour à l'accueil",
"error.desc": "L'autorisation OAuth a échoué, raison : {{reason}}",
"error.reason.internal_error": "Erreur interne du serveur",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Leggi i tuoi dati sincronizzati",
"consent.scope.sync-write": "Scrivi e aggiorna i tuoi dati sincronizzati",
"consent.title": "Autorizza {{clientName}}",
"device.confirm.authorize": "Autorizza",
"device.confirm.codeHint": "Conferma che questo codice corrisponda a quello mostrato nel tuo terminale.",
"device.confirm.deny": "Nega",
"device.confirm.description": "{{clientName}} sta richiedendo l'accesso",
"device.confirm.title": "Autorizza Dispositivo",
"device.error.aborted": "L'autorizzazione è stata negata.",
"device.error.alreadyUsed": "Questo codice è già stato utilizzato. Si prega di richiedere un nuovo codice.",
"device.error.expired": "Questo codice è scaduto. Si prega di richiedere un nuovo codice.",
"device.error.noCode": "Non è stato fornito alcun codice dispositivo. Si prega di inserire un codice valido.",
"device.error.notFound": "Codice non valido. Si prega di controllare e riprovare.",
"device.error.unknown": "Si è verificato un errore. Si prega di riprovare.",
"device.input.description": "Inserisci il codice visualizzato sul tuo dispositivo per autorizzare l'accesso.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Invia",
"device.input.title": "Inserisci Codice Dispositivo",
"device.success.description": "Hai autorizzato con successo il dispositivo. Puoi chiudere questa scheda del browser e tornare al tuo terminale.",
"device.success.title": "Autorizzazione Riuscita",
"error.backToHome": "Torna alla Home",
"error.desc": "Autorizzazione OAuth non riuscita, motivo: {{reason}}",
"error.reason.internal_error": "Errore interno del server",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "あなたの同期データを読み取る",
"consent.scope.sync-write": "あなたの同期データを書き込み、更新する",
"consent.title": "{{clientName}} の承認",
"device.confirm.authorize": "承認する",
"device.confirm.codeHint": "このコードがターミナルに表示されているものと一致することを確認してください。",
"device.confirm.deny": "拒否する",
"device.confirm.description": "{{clientName}} がアクセスを要求しています",
"device.confirm.title": "デバイスを承認",
"device.error.aborted": "承認が拒否されました。",
"device.error.alreadyUsed": "このコードはすでに使用されています。新しいコードをリクエストしてください。",
"device.error.expired": "このコードの有効期限が切れています。新しいコードをリクエストしてください。",
"device.error.noCode": "デバイスコードが提供されていません。有効なコードを入力してください。",
"device.error.notFound": "無効なコードです。確認してもう一度お試しください。",
"device.error.unknown": "エラーが発生しました。もう一度お試しください。",
"device.input.description": "デバイスに表示されているコードを入力してアクセスを承認してください。",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "送信",
"device.input.title": "デバイスコードを入力",
"device.success.description": "デバイスの承認に成功しました。このブラウザタブを閉じてターミナルに戻ってください。",
"device.success.title": "承認成功",
"error.backToHome": "ホームに戻る",
"error.desc": "OAuth認証に失敗しました。失敗理由{{reason}}",
"error.reason.internal_error": "サーバーエラー",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "동기화된 데이터를 읽기",
"consent.scope.sync-write": "동기화된 데이터를 쓰고 업데이트",
"consent.title": "{{clientName}} 권한 요청",
"device.confirm.authorize": "승인",
"device.confirm.codeHint": "이 코드가 터미널에 표시된 코드와 일치하는지 확인하세요.",
"device.confirm.deny": "거부",
"device.confirm.description": "{{clientName}}이(가) 액세스를 요청하고 있습니다",
"device.confirm.title": "기기 승인",
"device.error.aborted": "승인이 거부되었습니다.",
"device.error.alreadyUsed": "이 코드는 이미 사용되었습니다. 새 코드를 요청하세요.",
"device.error.expired": "이 코드는 만료되었습니다. 새 코드를 요청하세요.",
"device.error.noCode": "기기 코드가 제공되지 않았습니다. 유효한 코드를 입력하세요.",
"device.error.notFound": "유효하지 않은 코드입니다. 확인 후 다시 시도하세요.",
"device.error.unknown": "오류가 발생했습니다. 다시 시도하세요.",
"device.input.description": "기기에 표시된 코드를 입력하여 액세스를 승인하세요.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "제출",
"device.input.title": "기기 코드 입력",
"device.success.description": "기기를 성공적으로 승인했습니다. 이 브라우저 탭을 닫고 터미널로 돌아가세요.",
"device.success.title": "승인 성공",
"error.backToHome": "홈으로 돌아가기",
"error.desc": "OAuth 인증에 실패했습니다. 실패 사유: {{reason}}",
"error.reason.internal_error": "서버 오류",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Lees je gesynchroniseerde gegevens",
"consent.scope.sync-write": "Schrijf en werk je gesynchroniseerde gegevens bij",
"consent.title": "Geef toestemming aan {{clientName}}",
"device.confirm.authorize": "Autoriseren",
"device.confirm.codeHint": "Bevestig dat deze code overeenkomt met de code die op uw terminal wordt weergegeven.",
"device.confirm.deny": "Weigeren",
"device.confirm.description": "{{clientName}} vraagt om toegang",
"device.confirm.title": "Apparaat Autoriseren",
"device.error.aborted": "De autorisatie is geweigerd.",
"device.error.alreadyUsed": "Deze code is al gebruikt. Vraag een nieuwe code aan.",
"device.error.expired": "Deze code is verlopen. Vraag een nieuwe code aan.",
"device.error.noCode": "Er is geen apparaatcode opgegeven. Voer een geldige code in.",
"device.error.notFound": "Ongeldige code. Controleer en probeer opnieuw.",
"device.error.unknown": "Er is een fout opgetreden. Probeer het opnieuw.",
"device.input.description": "Voer de code in die op uw apparaat wordt weergegeven om toegang te autoriseren.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Verzenden",
"device.input.title": "Voer Apparaatcode In",
"device.success.description": "U heeft het apparaat succesvol geautoriseerd. U kunt dit browsertabblad sluiten en terugkeren naar uw terminal.",
"device.success.title": "Autorisatie Geslaagd",
"error.backToHome": "Terug naar startpagina",
"error.desc": "OAuth-autorisatie mislukt, reden: {{reason}}",
"error.reason.internal_error": "Interne serverfout",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Odczyt zsynchronizowanych danych",
"consent.scope.sync-write": "Zapisywanie i aktualizacja zsynchronizowanych danych",
"consent.title": "Autoryzuj {{clientName}}",
"device.confirm.authorize": "Autoryzuj",
"device.confirm.codeHint": "Potwierdź, że ten kod zgadza się z kodem wyświetlonym na Twoim terminalu.",
"device.confirm.deny": "Odmów",
"device.confirm.description": "{{clientName}} prosi o dostęp",
"device.confirm.title": "Autoryzuj urządzenie",
"device.error.aborted": "Autoryzacja została odrzucona.",
"device.error.alreadyUsed": "Ten kod został już użyty. Proszę poprosić o nowy kod.",
"device.error.expired": "Ten kod wygasł. Proszę poprosić o nowy kod.",
"device.error.noCode": "Nie podano kodu urządzenia. Proszę wprowadzić prawidłowy kod.",
"device.error.notFound": "Nieprawidłowy kod. Proszę sprawdzić i spróbować ponownie.",
"device.error.unknown": "Wystąpił błąd. Proszę spróbować ponownie.",
"device.input.description": "Wprowadź kod wyświetlony na Twoim urządzeniu, aby autoryzować dostęp.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Zatwierdź",
"device.input.title": "Wprowadź kod urządzenia",
"device.success.description": "Pomyślnie autoryzowano urządzenie. Możesz zamknąć tę kartę przeglądarki i wrócić do swojego terminala.",
"device.success.title": "Autoryzacja zakończona sukcesem",
"error.backToHome": "Powrót do strony głównej",
"error.desc": "Autoryzacja OAuth nie powiodła się, powód: {{reason}}",
"error.reason.internal_error": "Wewnętrzny błąd serwera",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Ler seus dados sincronizados",
"consent.scope.sync-write": "Escrever e atualizar seus dados sincronizados",
"consent.title": "Autorizar {{clientName}}",
"device.confirm.authorize": "Autorizar",
"device.confirm.codeHint": "Confirme se este código corresponde ao exibido no seu terminal.",
"device.confirm.deny": "Negar",
"device.confirm.description": "{{clientName}} está solicitando acesso",
"device.confirm.title": "Autorizar Dispositivo",
"device.error.aborted": "A autorização foi negada.",
"device.error.alreadyUsed": "Este código já foi utilizado. Por favor, solicite um novo código.",
"device.error.expired": "Este código expirou. Por favor, solicite um novo código.",
"device.error.noCode": "Nenhum código de dispositivo foi fornecido. Por favor, insira um código válido.",
"device.error.notFound": "Código inválido. Por favor, verifique e tente novamente.",
"device.error.unknown": "Ocorreu um erro. Por favor, tente novamente.",
"device.input.description": "Insira o código exibido no seu dispositivo para autorizar o acesso.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Enviar",
"device.input.title": "Insira o Código do Dispositivo",
"device.success.description": "Você autorizou o dispositivo com sucesso. Você pode fechar esta aba do navegador e retornar ao seu terminal.",
"device.success.title": "Autorização Bem-Sucedida",
"error.backToHome": "Voltar para a Página Inicial",
"error.desc": "A autorização OAuth falhou, motivo: {{reason}}",
"error.reason.internal_error": "Erro Interno do Servidor",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Чтение ваших синхронизированных данных",
"consent.scope.sync-write": "Запись и обновление ваших синхронизированных данных",
"consent.title": "Разрешить {{clientName}}",
"device.confirm.authorize": "Авторизовать",
"device.confirm.codeHint": "Убедитесь, что этот код совпадает с кодом, отображаемым в вашем терминале.",
"device.confirm.deny": "Отклонить",
"device.confirm.description": "{{clientName}} запрашивает доступ",
"device.confirm.title": "Авторизация устройства",
"device.error.aborted": "Авторизация была отклонена.",
"device.error.alreadyUsed": "Этот код уже был использован. Пожалуйста, запросите новый код.",
"device.error.expired": "Срок действия этого кода истёк. Пожалуйста, запросите новый код.",
"device.error.noCode": "Код устройства не был предоставлен. Пожалуйста, введите действительный код.",
"device.error.notFound": "Неверный код. Пожалуйста, проверьте и попробуйте снова.",
"device.error.unknown": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"device.input.description": "Введите код, отображаемый на вашем устройстве, чтобы авторизовать доступ.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Отправить",
"device.input.title": "Введите код устройства",
"device.success.description": "Вы успешно авторизовали устройство. Вы можете закрыть эту вкладку браузера и вернуться к своему терминалу.",
"device.success.title": "Авторизация успешна",
"error.backToHome": "На главную",
"error.desc": "Ошибка авторизации OAuth, причина: {{reason}}",
"error.reason.internal_error": "Внутренняя ошибка сервера",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Senkronize verilerinizi okuma",
"consent.scope.sync-write": "Senkronize verilerinizi yazma ve güncelleme",
"consent.title": "{{clientName}} uygulamasını yetkilendir",
"device.confirm.authorize": "Yetkilendir",
"device.confirm.codeHint": "Bu kodun terminalinizde gösterilenle eşleştiğini doğrulayın.",
"device.confirm.deny": "Reddet",
"device.confirm.description": "{{clientName}} erişim talep ediyor",
"device.confirm.title": "Cihazı Yetkilendir",
"device.error.aborted": "Yetkilendirme reddedildi.",
"device.error.alreadyUsed": "Bu kod zaten kullanıldı. Lütfen yeni bir kod isteyin.",
"device.error.expired": "Bu kodun süresi doldu. Lütfen yeni bir kod isteyin.",
"device.error.noCode": "Herhangi bir cihaz kodu sağlanmadı. Lütfen geçerli bir kod girin.",
"device.error.notFound": "Geçersiz kod. Lütfen kontrol edip tekrar deneyin.",
"device.error.unknown": "Bir hata oluştu. Lütfen tekrar deneyin.",
"device.input.description": "Erişimi yetkilendirmek için cihazınızda gösterilen kodu girin.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Gönder",
"device.input.title": "Cihaz Kodunu Girin",
"device.success.description": "Cihazı başarıyla yetkilendirdiniz. Bu tarayıcı sekmesini kapatabilir ve terminalinize dönebilirsiniz.",
"device.success.title": "Yetkilendirme Başarılı",
"error.backToHome": "Ana Sayfaya Dön",
"error.desc": "OAuth yetkilendirmesi başarısız oldu, sebep: {{reason}}",
"error.reason.internal_error": "Sunucu Hatası",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "Đọc dữ liệu đã đồng bộ của bạn",
"consent.scope.sync-write": "Ghi và cập nhật dữ liệu đã đồng bộ của bạn",
"consent.title": "Ủy quyền cho {{clientName}}",
"device.confirm.authorize": "Ủy quyền",
"device.confirm.codeHint": "Xác nhận rằng mã này khớp với mã hiển thị trên thiết bị đầu cuối của bạn.",
"device.confirm.deny": "Từ chối",
"device.confirm.description": "{{clientName}} đang yêu cầu quyền truy cập",
"device.confirm.title": "Ủy quyền Thiết bị",
"device.error.aborted": "Quyền ủy quyền đã bị từ chối.",
"device.error.alreadyUsed": "Mã này đã được sử dụng. Vui lòng yêu cầu mã mới.",
"device.error.expired": "Mã này đã hết hạn. Vui lòng yêu cầu mã mới.",
"device.error.noCode": "Không có mã thiết bị nào được cung cấp. Vui lòng nhập mã hợp lệ.",
"device.error.notFound": "Mã không hợp lệ. Vui lòng kiểm tra và thử lại.",
"device.error.unknown": "Đã xảy ra lỗi. Vui lòng thử lại.",
"device.input.description": "Nhập mã hiển thị trên thiết bị của bạn để ủy quyền truy cập.",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "Gửi",
"device.input.title": "Nhập Mã Thiết Bị",
"device.success.description": "Bạn đã ủy quyền thiết bị thành công. Bạn có thể đóng tab trình duyệt này và quay lại thiết bị đầu cuối của mình.",
"device.success.title": "Ủy quyền Thành công",
"error.backToHome": "Quay về trang chủ",
"error.desc": "Ủy quyền OAuth thất bại, lý do: {{reason}}",
"error.reason.internal_error": "Lỗi máy chủ nội bộ",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "读取你的同步数据",
"consent.scope.sync-write": "写入并更新你的同步数据",
"consent.title": "授权 {{clientName}}",
"device.confirm.authorize": "授权",
"device.confirm.codeHint": "确认此代码与您的终端上显示的代码一致。",
"device.confirm.deny": "拒绝",
"device.confirm.description": "{{clientName}} 正在请求访问权限",
"device.confirm.title": "授权设备",
"device.error.aborted": "授权已被拒绝。",
"device.error.alreadyUsed": "此代码已被使用。请请求一个新代码。",
"device.error.expired": "此代码已过期。请请求一个新代码。",
"device.error.noCode": "未提供设备代码。请输入有效的代码。",
"device.error.notFound": "无效的代码。请检查后重试。",
"device.error.unknown": "发生错误。请重试。",
"device.input.description": "输入您设备上显示的代码以授权访问。",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "提交",
"device.input.title": "输入设备代码",
"device.success.description": "您已成功授权该设备。您可以关闭此浏览器标签页并返回到您的终端。",
"device.success.title": "授权成功",
"error.backToHome": "返回首页",
"error.desc": "OAuth 授权未完成:{{reason}}。你可以返回首页后重试",
"error.reason.internal_error": "服务端错误",

View file

@ -18,6 +18,23 @@
"consent.scope.sync-read": "讀取您的同步資料",
"consent.scope.sync-write": "寫入並更新您的同步資料",
"consent.title": "授權 {{clientName}}",
"device.confirm.authorize": "授權",
"device.confirm.codeHint": "確認此代碼與您的終端機上顯示的代碼一致。",
"device.confirm.deny": "拒絕",
"device.confirm.description": "{{clientName}} 正在請求存取權限",
"device.confirm.title": "授權裝置",
"device.error.aborted": "授權已被拒絕。",
"device.error.alreadyUsed": "此代碼已被使用。請重新申請新的代碼。",
"device.error.expired": "此代碼已過期。請重新申請新的代碼。",
"device.error.noCode": "未提供裝置代碼。請輸入有效的代碼。",
"device.error.notFound": "代碼無效。請檢查後再試一次。",
"device.error.unknown": "發生錯誤。請再試一次。",
"device.input.description": "輸入您裝置上顯示的代碼以授權存取。",
"device.input.placeholder": "XXXX-XXXX",
"device.input.submit": "提交",
"device.input.title": "輸入裝置代碼",
"device.success.description": "您已成功授權此裝置。您可以關閉此瀏覽器分頁並返回您的終端機。",
"device.success.title": "授權成功",
"error.backToHome": "返回首頁",
"error.desc": "OAuth 授權失敗,失敗原因:{{reason}}",
"error.reason.internal_error": "服務端錯誤",

View file

@ -1,5 +1,5 @@
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DrizzleAdapter } from '@/libs/oidc-provider/adapter';
@ -14,6 +14,10 @@ import {
oidcSessions,
} from '../../../schemas/oidc';
vi.mock('@lobechat/utils/server', () => ({
getUserAuth: vi.fn(),
}));
const serverDB = await getTestDB();
// Test data
@ -200,6 +204,80 @@ describe('DrizzleAdapter', () => {
expect(result?.scopes).toEqual(['openid', 'profile']);
expect(result?.redirectUris).toEqual(['https://updated.com/callback']);
});
describe('DeviceCode payload accountId protection', () => {
afterEach(async () => {
const { getUserAuth } = await import('@lobechat/utils/server');
vi.mocked(getUserAuth).mockReset();
});
it('should NOT inject accountId into DeviceCode payload from auth context', async () => {
const { getUserAuth } = await import('@lobechat/utils/server');
vi.mocked(getUserAuth).mockResolvedValueOnce({ userId: testUserId } as any);
const adapter = new DrizzleAdapter('DeviceCode', serverDB);
const payload = {
clientId: testClientId,
userCode: testUserCode,
inFlight: true,
// No accountId — simulates oidc-provider's inFlight stage
};
await adapter.upsert(testId, payload, 3600);
const result = await serverDB.query.oidcDeviceCodes.findFirst({
where: eq(oidcDeviceCodes.id, testId),
});
expect(result).toBeDefined();
// JSONB data must NOT contain accountId — oidc-provider uses its absence
// to signal that authorization is still pending (inFlight).
// Injecting it would cause the token endpoint to consume the code prematurely.
expect(result?.data).not.toHaveProperty('accountId');
// DB column userId should still be set for indexing
expect(result?.userId).toBe(testUserId);
});
it('should preserve accountId in DeviceCode payload when oidc-provider explicitly sets it', async () => {
const adapter = new DrizzleAdapter('DeviceCode', serverDB);
const payload = {
clientId: testClientId,
userCode: testUserCode,
accountId: testUserId, // Explicitly set by oidc-provider after consent
};
await adapter.upsert(testId, payload, 3600);
const result = await serverDB.query.oidcDeviceCodes.findFirst({
where: eq(oidcDeviceCodes.id, testId),
});
expect(result).toBeDefined();
expect((result?.data as any).accountId).toBe(testUserId);
expect(result?.userId).toBe(testUserId);
});
it('should still inject accountId into non-DeviceCode payload from auth context', async () => {
const { getUserAuth } = await import('@lobechat/utils/server');
vi.mocked(getUserAuth).mockResolvedValueOnce({ userId: testUserId } as any);
const adapter = new DrizzleAdapter('Interaction', serverDB);
const payload = {
prompt: { name: 'consent' },
// No accountId — auth context should inject it for non-DeviceCode models
};
await adapter.upsert(testId, payload, 3600);
const result = await serverDB.query.oidcInteractions.findFirst({
where: eq(oidcInteractions.id, testId),
});
expect(result).toBeDefined();
// For non-DeviceCode models, accountId should be injected into payload
expect((result?.data as any).accountId).toBe(testUserId);
});
});
});
describe('find', () => {

View file

@ -0,0 +1,53 @@
'use client';
import { Block, Button, Flexbox, Input, Text } from '@lobehub/ui';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import AuthCard from '@/features/AuthCard';
interface DeviceCodeInputProps {
errorKey?: string;
userCode?: string;
xsrf?: string;
}
const DeviceCodeInput = memo<DeviceCodeInputProps>(({ xsrf, errorKey, userCode }) => {
const { t } = useTranslation('oauth');
return (
<AuthCard
subtitle={t('device.input.description')}
title={t('device.input.title')}
footer={
<form action="/oidc/device" method="post" style={{ width: '100%' }}>
{xsrf && <input name="xsrf" type="hidden" value={xsrf} />}
<Flexbox gap={16}>
<Input
autoFocus
autoComplete="off"
defaultValue={userCode}
name="user_code"
placeholder={t('device.input.placeholder')}
size="large"
style={{ fontFamily: 'monospace', letterSpacing: '0.15em', textAlign: 'center' }}
/>
<Button block htmlType="submit" size="large" type="primary">
{t('device.input.submit')}
</Button>
</Flexbox>
</form>
}
>
{errorKey && (
<Block padding={16} variant="filled">
<Text style={{ color: 'red' }}>{t(errorKey as any)}</Text>
</Block>
)}
</AuthCard>
);
});
DeviceCodeInput.displayName = 'DeviceCodeInput';
export default DeviceCodeInput;

View file

@ -0,0 +1,68 @@
'use client';
import { Block, Button, Flexbox, Text } from '@lobehub/ui';
import React, { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import AuthCard from '@/features/AuthCard';
interface DeviceCodeConfirmProps {
clientName: string;
userCode: string;
xsrf?: string;
}
const DeviceCodeConfirm = memo<DeviceCodeConfirmProps>(({ xsrf, userCode, clientName }) => {
const { t } = useTranslation('oauth');
const [isLoading, setIsLoading] = useState(false);
return (
<AuthCard
subtitle={t('device.confirm.description', { clientName })}
title={t('device.confirm.title')}
footer={
<form action="/oidc/device" method="post" style={{ width: '100%' }}>
{xsrf && <input name="xsrf" type="hidden" value={xsrf} />}
<input name="user_code" type="hidden" value={userCode} />
<input name="confirm" type="hidden" value="yes" />
<Flexbox gap={12}>
<Button
block
htmlType="submit"
loading={isLoading}
size="large"
type="primary"
onClick={() => setIsLoading(true)}
>
{t('device.confirm.authorize')}
</Button>
<Button block htmlType="submit" name="abort" size="large" value="yes">
{t('device.confirm.deny')}
</Button>
</Flexbox>
</form>
}
>
<Block padding={16} variant="filled">
<Text
style={{
fontFamily: 'monospace',
fontSize: 24,
fontWeight: 'bold',
letterSpacing: '0.15em',
textAlign: 'center',
}}
>
{userCode}
</Text>
</Block>
<Text style={{ marginTop: 8 }} type="secondary">
{t('device.confirm.codeHint')}
</Text>
</AuthCard>
);
});
DeviceCodeConfirm.displayName = 'DeviceCodeConfirm';
export default DeviceCodeConfirm;

View file

@ -0,0 +1,30 @@
import { notFound } from 'next/navigation';
import { authEnv } from '@/envs/auth';
import DeviceCodeConfirm from './DeviceCodeConfirm';
const DeviceConfirmPage = async (props: {
searchParams: Promise<{
client_id?: string;
client_name?: string;
user_code?: string;
xsrf?: string;
}>;
}) => {
if (!authEnv.ENABLE_OIDC) return notFound();
const searchParams = await props.searchParams;
if (!searchParams.user_code) return notFound();
return (
<DeviceCodeConfirm
clientName={searchParams.client_name || searchParams.client_id || 'Unknown Application'}
userCode={searchParams.user_code}
xsrf={searchParams.xsrf}
/>
);
};
export default DeviceConfirmPage;

View file

@ -0,0 +1,41 @@
import { notFound } from 'next/navigation';
import { authEnv } from '@/envs/auth';
import DeviceCodeInput from './DeviceCodeInput';
const getErrorMessage = (error?: string): string | undefined => {
if (!error) return undefined;
const errorMap: Record<string, string> = {
'already been used': 'device.error.alreadyUsed',
'interaction was aborted': 'device.error.aborted',
'code has expired': 'device.error.expired',
'code was not found': 'device.error.notFound',
'no code': 'device.error.noCode',
};
for (const [key, i18nKey] of Object.entries(errorMap)) {
if (error.toLowerCase().includes(key)) return i18nKey;
}
return 'device.error.unknown';
};
const DeviceInputPage = async (props: {
searchParams: Promise<{ error?: string; user_code?: string; xsrf?: string }>;
}) => {
if (!authEnv.ENABLE_OIDC) return notFound();
const searchParams = await props.searchParams;
return (
<DeviceCodeInput
errorKey={getErrorMessage(searchParams.error)}
userCode={searchParams.user_code}
xsrf={searchParams.xsrf}
/>
);
};
export default DeviceInputPage;

View file

@ -0,0 +1,31 @@
'use client';
import { FluentEmoji, Text } from '@lobehub/ui';
import { Result } from 'antd';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
const DeviceSuccess = memo(() => {
const { t } = useTranslation('oauth');
return (
<Result
icon={<FluentEmoji emoji={'✅'} size={96} type={'anim'} />}
status="success"
subTitle={
<Text fontSize={16} type="secondary">
{t('device.success.description')}
</Text>
}
title={
<Text fontSize={32} weight={'bold'}>
{t('device.success.title')}
</Text>
}
/>
);
});
DeviceSuccess.displayName = 'DeviceSuccess';
export default DeviceSuccess;

View file

@ -0,0 +1,13 @@
import { notFound } from 'next/navigation';
import { authEnv } from '@/envs/auth';
import DeviceSuccess from './DeviceSuccess';
const DeviceSuccessPage = async () => {
if (!authEnv.ENABLE_OIDC) return notFound();
return <DeviceSuccess />;
};
export default DeviceSuccessPage;

View file

@ -181,7 +181,14 @@ class OIDCAdapter {
try {
const { userId } = await getUserAuth();
if (userId) {
payload.accountId = userId;
// For DeviceCode, only set record.userId (DB column) without modifying payload.
// oidc-provider uses payload.accountId to track authorization state:
// it's unset during inFlight stage and set only after consent completes.
// Injecting accountId into payload would cause the token endpoint to
// mistake an in-flight code as fully authorized.
if (this.name !== 'DeviceCode') {
payload.accountId = userId;
}
record.userId = userId;
log('[%s] Setting userId from auth context: %s', this.name, userId);
}

View file

@ -53,7 +53,14 @@ export const defaultClients: ClientMetadata[] = [
// Public client with no secret
token_endpoint_auth_method: 'none',
},
{
application_type: 'native',
client_id: 'lobehub-cli',
client_name: 'LobeHub CLI',
grant_types: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
response_types: [],
token_endpoint_auth_method: 'none',
},
{
application_type: 'web',
client_id: 'lobehub-market',

View file

@ -93,7 +93,33 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
backchannelLogout: { enabled: true },
clientCredentials: { enabled: false },
devInteractions: { enabled: false },
deviceFlow: { enabled: false },
deviceFlow: {
charset: 'base-20',
enabled: true,
mask: '****-****',
successSource: async (ctx) => {
ctx.redirect('/oauth/device/success');
},
userCodeConfirmSource: async (ctx, form, client, deviceInfo, userCode) => {
const xsrf = (ctx.oidc.session as any)?.state?.secret;
const params = new URLSearchParams();
if (xsrf) params.set('xsrf', xsrf);
params.set('user_code', userCode);
params.set('client_name', client.clientName || client.clientId);
params.set('client_id', client.clientId);
ctx.redirect(`/oauth/device/confirm?${params.toString()}`);
},
userCodeInputSource: async (ctx, form, out, err) => {
const xsrf = (ctx.oidc.session as any)?.state?.secret;
const params = new URLSearchParams();
if (xsrf) params.set('xsrf', xsrf);
if (err) {
params.set('error', err.message || 'Unknown error');
if ((err as any).userCode) params.set('user_code', (err as any).userCode);
}
ctx.redirect(`/oauth/device?${params.toString()}`);
},
},
introspection: { enabled: true },
resourceIndicators: {
defaultResource: () => API_AUDIENCE,
@ -262,6 +288,8 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
routes: {
authorization: '/oidc/auth',
code_verification: '/oidc/device',
device_authorization: '/oidc/device/auth',
end_session: '/oidc/session/end',
token: '/oidc/token',
},

View file

@ -20,6 +20,24 @@ export default {
'consent.scope.sync-read': 'Read your synchronized data',
'consent.scope.sync-write': 'Write and update your synchronized data',
'consent.title': 'Authorize {{clientName}}',
'device.confirm.authorize': 'Authorize',
'device.confirm.codeHint': 'Confirm that this code matches the one shown in your terminal.',
'device.confirm.deny': 'Deny',
'device.confirm.description': '{{clientName}} is requesting access',
'device.confirm.title': 'Authorize Device',
'device.error.aborted': 'Authorization was denied.',
'device.error.alreadyUsed': 'This code has already been used. Please request a new code.',
'device.error.expired': 'This code has expired. Please request a new code.',
'device.error.noCode': 'No device code was provided. Please enter a valid code.',
'device.error.notFound': 'Invalid code. Please check and try again.',
'device.error.unknown': 'An error occurred. Please try again.',
'device.input.description': 'Enter the code displayed on your device to authorize access.',
'device.input.placeholder': 'XXXX-XXXX',
'device.input.submit': 'Submit',
'device.input.title': 'Enter Device Code',
'device.success.description':
'You have successfully authorized the device. You can close this browser tab and return to your terminal.',
'device.success.title': 'Authorization Successful',
'error.backToHome': 'Back to Home',
'error.desc': 'OAuth authorization failed, reason: {{reason}}',
'error.reason.internal_error': 'Internal Server Error',