summaryrefslogtreecommitdiff
path: root/player-android/lib
diff options
context:
space:
mode:
Diffstat (limited to 'player-android/lib')
-rw-r--r--player-android/lib/api/dio_client.dart26
-rw-r--r--player-android/lib/providers/auth_state_provider.dart41
2 files changed, 48 insertions, 19 deletions
diff --git a/player-android/lib/api/dio_client.dart b/player-android/lib/api/dio_client.dart
index 35db589..c988cf0 100644
--- a/player-android/lib/api/dio_client.dart
+++ b/player-android/lib/api/dio_client.dart
@@ -1,6 +1,9 @@
+import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
+import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:go_router/go_router.dart';
// Storage key under which the bearer token is persisted across app restarts.
const _kTokenKey = 'bearer_token';
@@ -89,9 +92,15 @@ class _UnauthorizedInterceptor extends Interceptor {
// Purge the stale token so subsequent requests start unauthenticated.
await _storage.deleteToken();
- // Use the navigator key to redirect without needing a BuildContext.
- _navigatorKey.currentState
- ?.pushNamedAndRemoveUntil(_loginRoute, (_) => false);
+ // Redirect via go_router (the app's router) rather than the classic
+ // Navigator. pushNamedAndRemoveUntil would throw "Navigator.onGenerateRoute
+ // was null" because go_router does not register named routes on the
+ // underlying Navigator. Using the navigatorKey's currentContext lets us
+ // resolve the active GoRouter instance without a widget-tree BuildContext.
+ final ctx = _navigatorKey.currentContext;
+ if (ctx != null && ctx.mounted) {
+ GoRouter.of(ctx).go(_loginRoute);
+ }
}
handler.next(err);
}
@@ -138,9 +147,18 @@ class DioClient {
responseType: ResponseType.json,
);
+ // The server's /api/v1/auth/login sets an HttpOnly Set-Cookie (session=...).
+ // Browsers persist this automatically; on mobile we attach a CookieJar so
+ // Dio replays the cookie on subsequent requests. Without this, every call
+ // after login returns 401 because Dio discards cookies by default.
+ // In-memory is sufficient: logout clears it, and we persist the bearer
+ // token (for API-token auth) separately via flutter_secure_storage.
+ final cookieJar = CookieJar();
return Dio(options)
..interceptors.addAll([
- // Auth must run before the 401 handler so the token is attached first.
+ // Cookie manager runs first so the session cookie is replayed before
+ // _AuthInterceptor decides whether to add a Bearer fallback.
+ CookieManager(cookieJar),
_AuthInterceptor(storage),
_UnauthorizedInterceptor(
storage: storage,
diff --git a/player-android/lib/providers/auth_state_provider.dart b/player-android/lib/providers/auth_state_provider.dart
index 0e0c6fb..96c89d8 100644
--- a/player-android/lib/providers/auth_state_provider.dart
+++ b/player-android/lib/providers/auth_state_provider.dart
@@ -1,6 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-
-import 'api_client_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
/// All possible authentication states for the app.
///
@@ -56,31 +55,43 @@ class AuthState {
/// [AsyncNotifier] is used because the initial state check is async (it reads
/// the secure token store). Downstream consumers can call [login] and
/// [logout] to drive route redirects via the router's [refreshListenable].
+// SharedPreferences key for the session-presence marker. Used in place of the
+// previous bearer-token-in-secure-storage hack: the server authenticates the
+// session via an HttpOnly cookie, so the client has no token to persist. We
+// only need a tiny boolean to drive the router redirect on cold start.
+const _kAuthSessionPresentKey = 'auth_session_present';
+
class AuthStateNotifier extends AsyncNotifier<AuthState> {
@override
Future<AuthState> build() async {
- // Determine whether a token already exists on app startup. This drives
- // the initial route decision inside the go_router redirect callback.
- final storage = ref.read(tokenStorageProvider);
- final token = await storage.readToken();
-
- return token != null && token.isNotEmpty
+ // The auth state on cold start is derived from a SharedPreferences marker
+ // rather than from any stored bearer token. Writing the username into
+ // SecureTokenStorage (the previous behaviour) caused _AuthInterceptor to
+ // attach `Authorization: Bearer <username>` to every request, which the
+ // server checks before falling back to the session cookie — yielding 401
+ // on every API call after login despite a valid cookie being sent.
+ final prefs = await SharedPreferences.getInstance();
+ final marked = prefs.getBool(_kAuthSessionPresentKey) ?? false;
+ return marked
? const AuthState.authenticated()
: const AuthState.unauthenticated();
}
- /// Called after a successful login; persists [token] and updates state.
+ /// Called after a successful login. The [token] parameter is accepted for
+ /// backwards compatibility with the call site but is intentionally unused;
+ /// the real authentication artefact is the session cookie set by the server
+ /// and stored by the Dio CookieManager. See [build] for why.
Future<void> login(String token) async {
- final storage = ref.read(tokenStorageProvider);
- await storage.writeToken(token);
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_kAuthSessionPresentKey, true);
state = const AsyncData(AuthState.authenticated());
}
- /// Called on explicit logout or after [_UnauthorizedInterceptor] purges the
- /// token. Clears the stored token and moves to the unauthenticated state.
+ /// Called on explicit logout or after the API returns 401. Clears the
+ /// session marker so the next cold start redirects to /login.
Future<void> logout() async {
- final storage = ref.read(tokenStorageProvider);
- await storage.deleteToken();
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove(_kAuthSessionPresentKey);
state = const AsyncData(AuthState.unauthenticated());
}
}