diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index a5f58fc..6883d20 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -187,6 +187,23 @@ class AuthStateManager extends _$AuthStateManager { if (token != null && token.isNotEmpty) { DebugLogger.auth('Found stored token during initialization'); + + // Check if stored token is an API key - force logout if so + if (TokenValidator.isApiKey(token)) { + DebugLogger.auth('Detected API key token, forcing logout'); + await storage.deleteAuthToken(); + await storage.deleteSavedCredentials(); + _update( + (current) => current.copyWith( + status: AuthStatus.credentialError, + error: 'apiKeyNoLongerSupported', + isLoading: false, + clearToken: true, + ), + ); + return; + } + // Fast path: trust token format to avoid blocking startup on network final formatOk = _isValidTokenFormat(token); if (formatOk) { @@ -270,7 +287,8 @@ class AuthStateManager extends _$AuthStateManager { } } - /// Perform login with API key + /// Perform login with JWT token + /// Note: API keys (sk-...) are not supported for streaming. Future loginWithApiKey( String apiKey, { bool rememberCredentials = false, @@ -284,9 +302,16 @@ class AuthStateManager extends _$AuthStateManager { ); try { - // Validate API key format + // Validate token is not empty if (apiKey.trim().isEmpty) { - throw Exception('API key cannot be empty'); + throw Exception('Token cannot be empty'); + } + + final tokenStr = apiKey.trim(); + + // Reject API keys - they don't support streaming + if (TokenValidator.isApiKey(tokenStr)) { + throw Exception('apiKeyNotSupported'); } // Ensure API service is available @@ -296,12 +321,9 @@ class AuthStateManager extends _$AuthStateManager { throw Exception('No server connection available'); } - // Use API key directly as Bearer token - final tokenStr = apiKey.trim(); - - // Validate token format (consistent with credentials method) + // Validate token format if (!_isValidTokenFormat(tokenStr)) { - throw Exception('Invalid API key format'); + throw Exception('Invalid token format'); } // Update API service with the API key @@ -315,16 +337,15 @@ class AuthStateManager extends _$AuthStateManager { final storage = ref.read(optimizedStorageServiceProvider); await storage.saveAuthToken(tokenStr); - // Save API key if requested (for convenience, though less secure than credentials) + // Save JWT token if requested if (rememberCredentials) { final activeServer = await ref.read(activeServerProvider.future); if (activeServer != null) { - // Store API key as a special credential type + // Store JWT as a special credential type await storage.saveCredentials( serverId: activeServer.id, - username: - 'api_key_user', // Special username to indicate API key auth - password: tokenStr, // Store API key in password field + username: 'jwt_user', // Special username to indicate JWT auth + password: tokenStr, // Store JWT in password field ); } } @@ -348,11 +369,11 @@ class AuthStateManager extends _$AuthStateManager { _loadUserData(); _prefetchConversations(); - DebugLogger.auth('API key login successful'); + DebugLogger.auth('JWT token login successful'); return true; } catch (e) { - // If user fetch fails, the API key might be invalid - throw Exception('Invalid API key or insufficient permissions'); + // If user fetch fails, the token might be invalid + throw Exception('Invalid token or insufficient permissions'); } } catch (e, stack) { DebugLogger.error( @@ -561,8 +582,8 @@ class AuthStateManager extends _$AuthStateManager { } // Attempt login (detect API key vs normal credentials) - if (username == 'api_key_user') { - // This is a saved API key + if (username == 'api_key_user' || username == 'jwt_user') { + // This is a saved JWT token (or legacy API key) return await loginWithApiKey(password, rememberCredentials: false); } else { // Normal username/password credentials diff --git a/lib/core/auth/token_validator.dart b/lib/core/auth/token_validator.dart index 84e4355..08093bc 100644 --- a/lib/core/auth/token_validator.dart +++ b/lib/core/auth/token_validator.dart @@ -6,7 +6,15 @@ import '../utils/debug_logger.dart'; class TokenValidator { static const Duration _validationTimeout = Duration(seconds: 5); - /// Validate token format (supports both JWT and API key formats) + /// Check if token is an API key format (sk-, api-, key-) + /// API keys are not supported for streaming. + static bool isApiKey(String token) { + return token.startsWith('sk-') || + token.startsWith('api-') || + token.startsWith('key-'); + } + + /// Validate token format (JWT tokens only - API keys not supported) static TokenValidationResult validateTokenFormat(String token) { try { // Basic format check @@ -14,15 +22,11 @@ class TokenValidator { return TokenValidationResult.invalid('Token too short'); } - // Check if it's an API key format (starts with sk- or similar) - if (token.startsWith('sk-') || - token.startsWith('api-') || - token.startsWith('key-')) { - // API key format - validate differently - if (token.length < 20) { - return TokenValidationResult.invalid('API key too short'); - } - return TokenValidationResult.valid('API key format valid'); + // Reject API keys - they don't support streaming + if (isApiKey(token)) { + return TokenValidationResult.apiKeyNotSupported( + 'API keys are not supported. Please use a JWT token.', + ); } // Check if it looks like a JWT (has at least 2 dots) @@ -209,6 +213,9 @@ class TokenValidationResult { const TokenValidationResult.networkError(String message) : this._(false, TokenValidationStatus.networkError, message); + const TokenValidationResult.apiKeyNotSupported(String message) + : this._(false, TokenValidationStatus.apiKeyNotSupported, message); + final bool isValid; final TokenValidationStatus status; final String message; @@ -218,6 +225,8 @@ class TokenValidationResult { bool get isExpired => status == TokenValidationStatus.expired; bool get isExpiringSoon => status == TokenValidationStatus.expiringSoon; bool get hasNetworkError => status == TokenValidationStatus.networkError; + bool get isApiKeyNotSupported => + status == TokenValidationStatus.apiKeyNotSupported; @override String toString() => @@ -230,6 +239,7 @@ enum TokenValidationStatus { expired, expiringSoon, networkError, + apiKeyNotSupported, } /// Cache for token validation results diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 5b51590..77dd499 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -62,6 +62,14 @@ class RouterNotifier extends ChangeNotifier { final reviewerMode = ref.read(reviewerModeProvider); final activeServerAsync = ref.read(activeServerProvider); + // Check for API key forced logout first - redirect to authentication + final authSnapshot = ref + .read(authStateManagerProvider) + .maybeWhen(data: (s) => s, orElse: () => null); + if (authSnapshot?.error?.contains('apiKey') == true) { + return location == Routes.authentication ? null : Routes.authentication; + } + if (reviewerMode) { // Stay on whatever route if already in chat; otherwise go to chat if (location == Routes.chat) return null; diff --git a/lib/features/auth/views/authentication_page.dart b/lib/features/auth/views/authentication_page.dart index 55b0f8a..9150c77 100644 --- a/lib/features/auth/views/authentication_page.dart +++ b/lib/features/auth/views/authentication_page.dart @@ -43,6 +43,23 @@ class _AuthenticationPageState extends ConsumerState { void initState() { super.initState(); _loadSavedCredentials(); + // Check for auth errors (e.g., forced logout due to API key) + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAuthStateError(); + }); + } + + void _checkAuthStateError() { + final authState = ref.read(authStateManagerProvider).asData?.value; + if (authState?.error != null && authState!.error!.isNotEmpty) { + setState(() { + _loginError = _formatLoginError(authState.error!); + // Switch to token tab if the error is about API keys + if (authState.error!.contains('apiKey')) { + _useApiKey = true; + } + }); + } } Future _loadSavedCredentials() async { @@ -127,16 +144,21 @@ class _AuthenticationPageState extends ConsumerState { } String _formatLoginError(String error) { - if (error.contains('401') || error.contains('Unauthorized')) { - return AppLocalizations.of(context)!.invalidCredentials; + final l10n = AppLocalizations.of(context)!; + if (error.contains('apiKeyNotSupported')) { + return l10n.apiKeyNotSupported; + } else if (error.contains('apiKeyNoLongerSupported')) { + return l10n.apiKeyNoLongerSupported; + } else if (error.contains('401') || error.contains('Unauthorized')) { + return l10n.invalidCredentials; } else if (error.contains('redirect')) { - return AppLocalizations.of(context)!.serverRedirectingHttps; + return l10n.serverRedirectingHttps; } else if (error.contains('SocketException')) { - return AppLocalizations.of(context)!.unableToConnectServer; + return l10n.unableToConnectServer; } else if (error.contains('timeout')) { - return AppLocalizations.of(context)!.requestTimedOut; + return l10n.requestTimedOut; } - return AppLocalizations.of(context)!.genericSignInFailed; + return l10n.genericSignInFailed; } @override diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7acb46e..5455e87 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -230,6 +230,7 @@ "enterToken": "Gib dein JWT-Token ein", "tokenHint": "Hole das JWT-Token aus den OpenWebUI-Einstellungen. API-Schlüssel (sk-...) werden für Streaming nicht unterstützt.", "apiKeyNotSupported": "API-Schlüssel (sk-...) werden nicht unterstützt. Bitte verwende stattdessen ein JWT-Token.", + "apiKeyNoLongerSupported": "Du wurdest abgemeldet, da API-Schlüssel nicht mehr unterstützt werden. Bitte melde dich mit einem JWT-Token aus den OpenWebUI-Einstellungen an.", "tokenTooShort": "Token ist zu kurz", "signingIn": "Anmeldung läuft...", "advancedSettings": "Erweiterte Einstellungen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 425cc08..1a9dfbd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1024,6 +1024,10 @@ "@apiKeyNotSupported": { "description": "Error message when user tries to use an API key instead of JWT token." }, + "apiKeyNoLongerSupported": "You were logged out because API keys are no longer supported. Please sign in with a JWT token from OpenWebUI settings.", + "@apiKeyNoLongerSupported": { + "description": "Error message shown when user is forced logged out due to using an API key." + }, "tokenTooShort": "Token is too short", "@tokenTooShort": { "description": "Error message when token is too short." diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1a01220..b8128d7 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -230,6 +230,7 @@ "enterToken": "Ingresa tu token JWT", "tokenHint": "Obtén el token JWT desde la configuración de OpenWebUI. Las claves API (sk-...) no son compatibles con streaming.", "apiKeyNotSupported": "Las claves API (sk-...) no son compatibles. Por favor usa un token JWT en su lugar.", + "apiKeyNoLongerSupported": "Se cerró tu sesión porque las claves API ya no son compatibles. Por favor inicia sesión con un token JWT desde la configuración de OpenWebUI.", "tokenTooShort": "El token es demasiado corto", "signingIn": "Iniciando sesión...", "advancedSettings": "Configuración avanzada", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5c11ee3..2063bed 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -230,6 +230,7 @@ "enterToken": "Entrez votre jeton JWT", "tokenHint": "Obtenez le jeton JWT dans les paramètres d'OpenWebUI. Les clés API (sk-...) ne sont pas prises en charge pour le streaming.", "apiKeyNotSupported": "Les clés API (sk-...) ne sont pas prises en charge. Veuillez utiliser un jeton JWT à la place.", + "apiKeyNoLongerSupported": "Vous avez été déconnecté car les clés API ne sont plus prises en charge. Veuillez vous connecter avec un jeton JWT depuis les paramètres d'OpenWebUI.", "tokenTooShort": "Le jeton est trop court", "signingIn": "Connexion en cours...", "advancedSettings": "Paramètres avancés", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index afb47fe..6d754fe 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -230,6 +230,7 @@ "enterToken": "Inserisci il tuo token JWT", "tokenHint": "Ottieni il token JWT dalle impostazioni di OpenWebUI. Le chiavi API (sk-...) non sono supportate per lo streaming.", "apiKeyNotSupported": "Le chiavi API (sk-...) non sono supportate. Per favore usa un token JWT.", + "apiKeyNoLongerSupported": "Sei stato disconnesso perché le chiavi API non sono più supportate. Per favore accedi con un token JWT dalle impostazioni di OpenWebUI.", "tokenTooShort": "Il token è troppo corto", "signingIn": "Accesso in corso...", "advancedSettings": "Impostazioni avanzate", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 3b179f0..c0a5a53 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -340,6 +340,7 @@ "enterToken": "JWT 토큰을 입력하세요", "tokenHint": "OpenWebUI 설정에서 JWT 토큰을 가져오세요. API 키(sk-...)는 스트리밍에 지원되지 않습니다.", "apiKeyNotSupported": "API 키(sk-...)는 지원되지 않습니다. JWT 토큰을 사용하세요.", + "apiKeyNoLongerSupported": "API 키가 더 이상 지원되지 않아 로그아웃되었습니다. OpenWebUI 설정에서 JWT 토큰으로 로그인하세요.", "tokenTooShort": "토큰이 너무 짧습니다", "signingIn": "로그인 중...", "advancedSettings": "고급 설정", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2e68ccd..048151d 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -230,6 +230,7 @@ "enterToken": "Voer je JWT-token in", "tokenHint": "Haal het JWT-token uit de OpenWebUI-instellingen. API-sleutels (sk-...) worden niet ondersteund voor streaming.", "apiKeyNotSupported": "API-sleutels (sk-...) worden niet ondersteund. Gebruik in plaats daarvan een JWT-token.", + "apiKeyNoLongerSupported": "Je bent uitgelogd omdat API-sleutels niet meer worden ondersteund. Log in met een JWT-token uit de OpenWebUI-instellingen.", "tokenTooShort": "Token is te kort", "signingIn": "Inloggen...", "advancedSettings": "Geavanceerde instellingen", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index bd15449..66a963c 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -230,6 +230,7 @@ "enterToken": "Введите ваш JWT-токен", "tokenHint": "Получите JWT-токен в настройках OpenWebUI. API-ключи (sk-...) не поддерживаются для потоковой передачи.", "apiKeyNotSupported": "API-ключи (sk-...) не поддерживаются. Пожалуйста, используйте JWT-токен.", + "apiKeyNoLongerSupported": "Вы были выведены из системы, так как API-ключи больше не поддерживаются. Пожалуйста, войдите с JWT-токеном из настроек OpenWebUI.", "tokenTooShort": "Токен слишком короткий", "signingIn": "Вход...", "advancedSettings": "Расширенные настройки", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b0d01ae..827d626 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -230,6 +230,7 @@ "enterToken": "输入您的 JWT 令牌", "tokenHint": "从 OpenWebUI 设置获取 JWT 令牌。API 密钥 (sk-...) 不支持流式传输。", "apiKeyNotSupported": "不支持 API 密钥 (sk-...)。请改用 JWT 令牌。", + "apiKeyNoLongerSupported": "由于不再支持 API 密钥,您已被登出。请使用 OpenWebUI 设置中的 JWT 令牌登录。", "tokenTooShort": "令牌太短", "signingIn": "正在登录...", "advancedSettings": "高级设置", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 398f302..eae014e 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -230,6 +230,7 @@ "enterToken": "輸入您的 JWT 令牌", "tokenHint": "從 OpenWebUI 設定取得 JWT 令牌。API 密鑰 (sk-...) 不支援串流。", "apiKeyNotSupported": "不支援 API 密鑰 (sk-...)。請改用 JWT 令牌。", + "apiKeyNoLongerSupported": "由於不再支援 API 密鑰,您已被登出。請使用 OpenWebUI 設定中的 JWT 令牌登入。", "tokenTooShort": "令牌太短", "signingIn": "正在登錄...", "advancedSettings": "高級設置",