Add session persistence with shared_preferences - maintain login across page refreshes
This commit is contained in:
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png
Normal file
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 574 KiB |
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon.png
Normal file
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
@@ -252,7 +252,21 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
CheckAuthRequested event,
|
CheckAuthRequested event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
// Check if token is valid in SessionBloc
|
// Try to restore session from persistent storage
|
||||||
emit(AuthUnauthenticated());
|
final sessionState = sessionBloc.state;
|
||||||
|
|
||||||
|
if (sessionState is SessionActive) {
|
||||||
|
// Session already active
|
||||||
|
emit(AuthAuthenticated(
|
||||||
|
token: sessionState.token,
|
||||||
|
userId: '',
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Try to restore from SharedPreferences
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100)); // Give time for restoration
|
||||||
|
emit(AuthUnauthenticated());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,98 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'session_event.dart';
|
import 'session_event.dart';
|
||||||
import 'session_state.dart';
|
import 'session_state.dart';
|
||||||
|
|
||||||
class SessionBloc extends Bloc<SessionEvent, SessionState> {
|
class SessionBloc extends Bloc<SessionEvent, SessionState> {
|
||||||
Timer? _expiryTimer;
|
Timer? _expiryTimer;
|
||||||
|
static const String _tokenKey = 'auth_token';
|
||||||
|
static const String _expiryKey = 'auth_expiry';
|
||||||
|
|
||||||
SessionBloc() : super(SessionInitial()) {
|
SessionBloc() : super(SessionInitial()) {
|
||||||
on<SessionStarted>(_onSessionStarted);
|
on<SessionStarted>(_onSessionStarted);
|
||||||
on<SessionExpired>(_onSessionExpired);
|
on<SessionExpired>(_onSessionExpired);
|
||||||
on<SessionRefreshed>(_onSessionRefreshed);
|
on<SessionRefreshed>(_onSessionRefreshed);
|
||||||
on<SessionEnded>(_onSessionEnded);
|
on<SessionEnded>(_onSessionEnded);
|
||||||
|
on<SessionRestored>(_onSessionRestored);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSessionStarted(SessionStarted event, Emitter<SessionState> emit) {
|
void _onSessionStarted(SessionStarted event, Emitter<SessionState> emit) async {
|
||||||
final expiresAt = DateTime.now().add(
|
final expiresAt = DateTime.now().add(
|
||||||
const Duration(minutes: 15),
|
const Duration(minutes: 15),
|
||||||
); // Match Go
|
); // Match Go
|
||||||
|
|
||||||
|
// Save token to persistent storage
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_tokenKey, event.token);
|
||||||
|
await prefs.setString(_expiryKey, expiresAt.toIso8601String());
|
||||||
|
|
||||||
emit(SessionActive(token: event.token, expiresAt: expiresAt));
|
emit(SessionActive(token: event.token, expiresAt: expiresAt));
|
||||||
_startExpiryTimer(expiresAt);
|
_startExpiryTimer(expiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSessionExpired(SessionExpired event, Emitter<SessionState> emit) {
|
void _onSessionExpired(SessionExpired event, Emitter<SessionState> emit) {
|
||||||
_expiryTimer?.cancel();
|
_expiryTimer?.cancel();
|
||||||
|
_clearStoredSession();
|
||||||
emit(SessionExpiredState());
|
emit(SessionExpiredState());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> emit) {
|
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> emit) async {
|
||||||
final expiresAt = DateTime.now().add(const Duration(minutes: 15));
|
final expiresAt = DateTime.now().add(const Duration(minutes: 15));
|
||||||
|
|
||||||
|
// Update stored token
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_tokenKey, event.newToken);
|
||||||
|
await prefs.setString(_expiryKey, expiresAt.toIso8601String());
|
||||||
|
|
||||||
emit(SessionActive(token: event.newToken, expiresAt: expiresAt));
|
emit(SessionActive(token: event.newToken, expiresAt: expiresAt));
|
||||||
_startExpiryTimer(expiresAt);
|
_startExpiryTimer(expiresAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSessionEnded(SessionEnded event, Emitter<SessionState> emit) {
|
void _onSessionEnded(SessionEnded event, Emitter<SessionState> emit) {
|
||||||
_expiryTimer?.cancel();
|
_expiryTimer?.cancel();
|
||||||
|
_clearStoredSession();
|
||||||
emit(SessionInitial());
|
emit(SessionInitial());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSessionRestored(SessionRestored event, Emitter<SessionState> emit) {
|
||||||
|
final expiresAt = event.expiresAt;
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// Check if token is still valid
|
||||||
|
if (expiresAt.isAfter(now)) {
|
||||||
|
emit(SessionActive(token: event.token, expiresAt: expiresAt));
|
||||||
|
_startExpiryTimer(expiresAt);
|
||||||
|
} else {
|
||||||
|
// Token expired, clear it
|
||||||
|
_clearStoredSession();
|
||||||
|
emit(SessionInitial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _clearStoredSession() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_tokenKey);
|
||||||
|
await prefs.remove(_expiryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> restoreSession(SessionBloc bloc) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString(_tokenKey);
|
||||||
|
final expiryStr = prefs.getString(_expiryKey);
|
||||||
|
|
||||||
|
if (token != null && expiryStr != null) {
|
||||||
|
try {
|
||||||
|
final expiresAt = DateTime.parse(expiryStr);
|
||||||
|
bloc.add(SessionRestored(token: token, expiresAt: expiresAt));
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid stored data, clear it
|
||||||
|
await prefs.remove(_tokenKey);
|
||||||
|
await prefs.remove(_expiryKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _startExpiryTimer(DateTime expiresAt) {
|
void _startExpiryTimer(DateTime expiresAt) {
|
||||||
_expiryTimer?.cancel();
|
_expiryTimer?.cancel();
|
||||||
final duration = expiresAt.difference(DateTime.now());
|
final duration = expiresAt.difference(DateTime.now());
|
||||||
|
|||||||
@@ -28,3 +28,13 @@ class SessionRefreshed extends SessionEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SessionEnded extends SessionEvent {}
|
class SessionEnded extends SessionEvent {}
|
||||||
|
|
||||||
|
class SessionRestored extends SessionEvent {
|
||||||
|
final String token;
|
||||||
|
final DateTime expiresAt;
|
||||||
|
|
||||||
|
const SessionRestored({required this.token, required this.expiresAt});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [token, expiresAt];
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,23 +41,37 @@ void main() {
|
|||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainApp extends StatelessWidget {
|
class MainApp extends StatefulWidget {
|
||||||
const MainApp({super.key});
|
const MainApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MainApp> createState() => _MainAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainAppState extends State<MainApp> {
|
||||||
|
final _sessionBloc = SessionBloc();
|
||||||
|
late final AuthBloc _authBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_authBloc = AuthBloc(
|
||||||
|
apiClient: ApiClient(_sessionBloc),
|
||||||
|
sessionBloc: _sessionBloc,
|
||||||
|
);
|
||||||
|
// Restore session from persistent storage
|
||||||
|
SessionBloc.restoreSession(_sessionBloc);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<SessionBloc>(create: (_) => SessionBloc()),
|
BlocProvider<SessionBloc>.value(value: _sessionBloc),
|
||||||
BlocProvider<AuthBloc>(
|
BlocProvider<AuthBloc>.value(value: _authBloc),
|
||||||
create: (context) => AuthBloc(
|
|
||||||
apiClient: ApiClient(context.read<SessionBloc>()),
|
|
||||||
sessionBloc: context.read<SessionBloc>(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider<ActivityBloc>(
|
BlocProvider<ActivityBloc>(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
ActivityBloc(ActivityApi(ApiClient(context.read<SessionBloc>()))),
|
ActivityBloc(ActivityApi(ApiClient(_sessionBloc))),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
@@ -66,4 +80,11 @@ class MainApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_authBloc.close();
|
||||||
|
_sessionBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user