fix: added reauth popup when using soo (#174)

This commit is contained in:
Daniel
2026-07-18 13:11:52 +02:00
parent 3bfc9b91d1
commit ad021bc9e8
7 changed files with 84 additions and 5 deletions
+7
View File
@@ -12,6 +12,7 @@ import 'package:http/io_client.dart';
import 'package:calibre_web_companion/core/exceptions/redirect_exception.dart';
import 'package:calibre_web_companion/core/services/connection_diagnostics.dart';
import 'package:calibre_web_companion/core/services/digest_auth.dart';
import 'package:calibre_web_companion/core/services/session_reauth_service.dart';
import 'package:calibre_web_companion/features/book_view/data/datasources/book_view_remote_datasource.dart';
enum AuthMethod { none, cookie, basic, auto }
@@ -32,6 +33,7 @@ class ApiService {
String? _userAgent;
bool _allowSelfSigned = false;
bool _isSsoSession = false;
Future<bool>? _reauthFuture;
final DigestAuth _digest = DigestAuth();
@@ -104,6 +106,7 @@ class ApiService {
_basePath = prefs.getString('base_path') ?? '';
_userAgent = prefs.getString('user_agent'); // User Agent laden
_allowSelfSigned = prefs.getBool('allow_self_signed') ?? false;
_isSsoSession = prefs.getBool('is_sso_session') ?? false;
_digest.reset();
@@ -292,6 +295,10 @@ class ApiService {
_logger.e('Invalid JSON from "$endpoint" (status ${response.statusCode})');
if (_looksLikeHtml(response.body)) {
if (_isSsoSession) {
_logger.w('SSO session lost; requesting re-auth web view.');
unawaited(SessionReauthService().requestReauth());
}
throw Exception(
'Session not accepted by the server (received a login page instead of data).',
);
@@ -0,0 +1,27 @@
import 'dart:async';
class SessionReauthService {
static final SessionReauthService _instance =
SessionReauthService._internal();
factory SessionReauthService() => _instance;
SessionReauthService._internal();
Future<bool> Function()? _handler;
Future<bool>? _inFlight;
void registerHandler(Future<bool> Function()? handler) {
_handler = handler;
}
Future<bool> requestReauth() {
final existing = _inFlight;
if (existing != null) return existing;
final handler = _handler;
if (handler == null) return Future.value(false);
final future = handler();
_inFlight = future;
return future.whenComplete(() => _inFlight = null);
}
}
@@ -168,6 +168,7 @@ class LoginRemoteDataSource {
final isSuccess = !response.body.contains('flash_danger');
if (isSuccess) {
await prefs.setBool('is_sso_session', false);
if (response.headers.containsKey('set-cookie')) {
final cookie = response.headers['set-cookie']!;
await prefs.setString('calibre_web_session', cookie);
@@ -341,6 +342,7 @@ class LoginRemoteDataSource {
await prefs.remove('user_agent');
await prefs.remove('calibre_library_id');
await prefs.remove('calibre_library_map');
await prefs.remove('is_sso_session');
await apiService.reset();
}
@@ -545,6 +547,7 @@ class LoginRemoteDataSource {
await prefs.setString('user_agent', userAgent);
await prefs.setString('calibre_web_cookie', cookieHeader);
await prefs.remove('calibre_web_session');
await prefs.setBool('is_sso_session', true);
if (username != null && username.isNotEmpty) {
await prefs.setString('username', username);
@@ -19,6 +19,7 @@ class WebViewLoginPage extends StatefulWidget {
final String baseUrl;
final String? username;
final String? password;
final bool isReauth;
const WebViewLoginPage({
super.key,
@@ -26,6 +27,7 @@ class WebViewLoginPage extends StatefulWidget {
required this.baseUrl,
this.username,
this.password,
this.isReauth = false,
});
@override
@@ -164,12 +166,17 @@ class _WebViewLoginPageState extends State<WebViewLoginPage> {
return BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state.status == LoginStatus.success) {
_logger.i('SSO Login finalized via Bloc. Navigating home.');
context.showSnackBar(localizations.loginSuccessfull, isError: false);
if (widget.isReauth) {
_logger.i('SSO session renewed via Bloc. Returning to caller.');
Navigator.of(context).pop(true);
} else {
_logger.i('SSO Login finalized via Bloc. Navigating home.');
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const HomePage()),
(route) => false,
);
}
} else if (state.status == LoginStatus.failure) {
_logger.e('SSO Finalization failed: ${state.errorMessage}');
setState(() => _isExtracting = false);
+1
View File
@@ -343,6 +343,7 @@
"ok": "Ok",
"attentionSSLCertificate": "Achtung: Selbstsignierte Zertifikate können unsicher sein. Verwende sie nur, wenn du die Risiken verstehst.",
"ssoLogin": "SSO-Anmeldung",
"sessionExpiredReauth": "Sitzung abgelaufen bitte erneut anmelden",
"pleaseLoginWithYourSSOAccount": "Bitte melde dich mit deinem SSO-Konto an. Du wirst nach der Anmeldung automatisch zur App zurückgeleitet.",
"loginWithSSO": "Mit SSO anmelden",
"pleaseEnterSSOUrl": "Bitte gib die SSO-URL ein",
+1
View File
@@ -343,6 +343,7 @@
"ok": "Ok",
"attentionSSLCertificate": "Attention: Self-signed certificates can be insecure. Use them only if you understand the risks.",
"ssoLogin": "SSO-Login",
"sessionExpiredReauth": "Session expired — please sign in again",
"pleaseLoginWithYourSSOAccount": "Please login with your SSO account. You will be redirected back to the app after successful login.",
"loginWithSSO": "Login with SSO",
"pleaseEnterSSOUrl": "Please enter the SSO URL",
+33
View File
@@ -15,6 +15,9 @@ import 'package:calibre_web_companion/l10n/app_localizations.dart';
import 'package:calibre_web_companion/core/di/injection_container.dart' as di;
import 'package:calibre_web_companion/core/services/api_service.dart';
import 'package:calibre_web_companion/core/services/app_transition.dart';
import 'package:calibre_web_companion/core/services/session_reauth_service.dart';
import 'package:calibre_web_companion/core/services/snackbar.dart';
import 'package:calibre_web_companion/features/login/presentation/widgets/web_view_login_page.dart';
import 'package:calibre_web_companion/core/services/connectivity_service.dart';
import 'package:calibre_web_companion/core/services/widget_service.dart';
import 'package:calibre_web_companion/core/services/download_manager.dart';
@@ -174,14 +177,44 @@ class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
_setupWidgetLaunch();
SessionReauthService().registerHandler(_openSsoReauth);
}
@override
void dispose() {
_widgetClickSub?.cancel();
SessionReauthService().registerHandler(null);
super.dispose();
}
Future<bool> _openSsoReauth() async {
final navigator = navigatorKey.currentState;
if (navigator == null) return false;
final prefs = getIt<SharedPreferences>();
final baseUrl = prefs.getString('base_url');
if (baseUrl == null || baseUrl.isEmpty) return false;
final message = AppLocalizations.of(navigator.context)?.sessionExpiredReauth;
if (message != null) {
navigator.context.showSnackBar(message);
}
final result = await navigator.push<bool>(
MaterialPageRoute(
builder:
(_) => WebViewLoginPage(
redirectUrl: baseUrl,
baseUrl: baseUrl,
username: prefs.getString('username'),
password: prefs.getString('password'),
isReauth: true,
),
),
);
return result ?? false;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();