mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
parent
08b23a9732
commit
ab376d9185
29 changed files with 684 additions and 4 deletions
|
|
@ -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": "خطأ داخلي في الخادم",
|
||||
|
|
|
|||
|
|
@ -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": "Вътрешна грешка на сървъра",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "خطای داخلی سرور",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "サーバーエラー",
|
||||
|
|
|
|||
|
|
@ -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": "서버 오류",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "Внутренняя ошибка сервера",
|
||||
|
|
|
|||
|
|
@ -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ı",
|
||||
|
|
|
|||
|
|
@ -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ộ",
|
||||
|
|
|
|||
|
|
@ -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": "服务端错误",
|
||||
|
|
|
|||
|
|
@ -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": "服務端錯誤",
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
53
src/app/[variants]/(auth)/oauth/device/DeviceCodeInput.tsx
Normal file
53
src/app/[variants]/(auth)/oauth/device/DeviceCodeInput.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
30
src/app/[variants]/(auth)/oauth/device/confirm/page.tsx
Normal file
30
src/app/[variants]/(auth)/oauth/device/confirm/page.tsx
Normal 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;
|
||||
41
src/app/[variants]/(auth)/oauth/device/page.tsx
Normal file
41
src/app/[variants]/(auth)/oauth/device/page.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
13
src/app/[variants]/(auth)/oauth/device/success/page.tsx
Normal file
13
src/app/[variants]/(auth)/oauth/device/success/page.tsx
Normal 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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue