forked from eatvio/eatvio_app
feat(session): centralize session teardown and refresh
Add SessionLifecycleService to reset user-scoped cubits, analytics, socket id and account-scoped preferences on logout, 401 or account deletion. Run the same refresh after every successful auth path, including registration v1/v2, and ignore 401 responses from stale tokens so an old request cannot terminate the new user's session. Refs: #1113
This commit is contained in:
@@ -97,6 +97,7 @@ class CubitRegistrations {
|
||||
systemPipCoordinator: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
}
|
||||
|
||||
/// Регистрация Cubit'ов как фабрик
|
||||
@@ -108,10 +109,15 @@ class CubitRegistrations {
|
||||
firebaseApi: sl(),
|
||||
prefs: sl(),
|
||||
profileRepository: sl(),
|
||||
sessionLifecycleService: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<RegistrationCubit>(
|
||||
() => RegistrationCubit(registrationRepository: sl(), prefs: sl()),
|
||||
() => RegistrationCubit(
|
||||
registrationRepository: sl(),
|
||||
prefs: sl(),
|
||||
sessionLifecycleService: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<ResetPasswordCubit>(
|
||||
() => ResetPasswordCubit(resetPasswordRepository: sl()),
|
||||
@@ -120,14 +126,21 @@ class CubitRegistrations {
|
||||
() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()),
|
||||
);
|
||||
sl.registerFactory<OAuthCubit>(
|
||||
() => OAuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl()),
|
||||
() => OAuthCubit(
|
||||
authRepository: sl(),
|
||||
firebaseApi: sl(),
|
||||
prefs: sl(),
|
||||
sessionLifecycleService: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<RegistrationCubitV2>(
|
||||
() =>
|
||||
RegistrationCubitV2(registrationRepository: sl(), firebaseApi: sl()),
|
||||
() => RegistrationCubitV2(
|
||||
registrationRepository: sl(),
|
||||
firebaseApi: sl(),
|
||||
sessionLifecycleService: sl(),
|
||||
),
|
||||
);
|
||||
// Feed related cubits
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RecipesCubit>(
|
||||
() => RecipesCubit(
|
||||
feedRepository: sl(),
|
||||
@@ -242,6 +255,7 @@ class CubitRegistrations {
|
||||
settingsRepository: sl(),
|
||||
consultantRepository: sl(),
|
||||
consultationsRepository: sl(),
|
||||
sessionLifecycleService: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<RegisterConsultantCubit>(
|
||||
@@ -297,7 +311,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<UserDetailsCubit>(
|
||||
() => UserDetailsCubit(profileRepository: sl()),
|
||||
);
|
||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl(), sl()));
|
||||
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
||||
sl.registerFactory<PersonalDietSuggestionCubit>(
|
||||
() => PersonalDietSuggestionCubit(
|
||||
|
||||
15
lib/core/di/session_registrations.dart
Normal file
15
lib/core/di/session_registrations.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
|
||||
class SessionRegistrations {
|
||||
static void registerAll() {
|
||||
sl.registerLazySingleton<SessionLifecycleService>(
|
||||
() => SessionLifecycleService(
|
||||
wsManager: sl<WsManager>(),
|
||||
errorReporter: sl<ErrorReporter>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/session/session_lifecycle_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class TokenInterceptor extends Interceptor {
|
||||
@override
|
||||
@@ -14,6 +12,7 @@ class TokenInterceptor extends Interceptor {
|
||||
final token = await TokenStorage.getTokenFromStorage();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
options.extra['authToken'] = token;
|
||||
}
|
||||
|
||||
super.onRequest(options, handler);
|
||||
@@ -25,9 +24,18 @@ class TokenInterceptor extends Interceptor {
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
if (err.response?.statusCode == 401) {
|
||||
await sl<WsManager>().clear();
|
||||
await TokenStorage.removeTokenInStorage();
|
||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||
final requestCarriedAuth = err.requestOptions.headers.containsKey(
|
||||
'Authorization',
|
||||
);
|
||||
final requestToken = err.requestOptions.extra['authToken'];
|
||||
final currentToken = await TokenStorage.getTokenFromStorage();
|
||||
if (requestCarriedAuth &&
|
||||
requestToken != null &&
|
||||
requestToken == currentToken) {
|
||||
await sl<SessionLifecycleService>().endSession(
|
||||
reason: SessionEndReason.unauthorized,
|
||||
);
|
||||
}
|
||||
}
|
||||
super.onError(err, handler);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/service_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/session_registrations.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
@@ -38,4 +39,6 @@ Future<void> init({required AppBuildType buildType}) async {
|
||||
sl.registerLazySingleton<WsManager>(
|
||||
() => WsManager(wsService: sl(), errorReporter: sl()),
|
||||
);
|
||||
|
||||
SessionRegistrations.registerAll();
|
||||
}
|
||||
|
||||
122
lib/core/session/session_lifecycle_service.dart
Normal file
122
lib/core/session/session_lifecycle_service.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
enum SessionEndReason { logout, unauthorized, accountDeleted }
|
||||
|
||||
class SessionLifecycleService {
|
||||
SessionLifecycleService({
|
||||
required WsManager wsManager,
|
||||
required ErrorReporter errorReporter,
|
||||
}) : _wsManager = wsManager,
|
||||
_errorReporter = errorReporter;
|
||||
|
||||
final WsManager _wsManager;
|
||||
final ErrorReporter _errorReporter;
|
||||
|
||||
int? _authenticatedFingerprint;
|
||||
bool _endingSession = false;
|
||||
|
||||
Future<void> endSession({required SessionEndReason reason}) async {
|
||||
if (_endingSession) return;
|
||||
_endingSession = true;
|
||||
try {
|
||||
await _run(() => _wsManager.clear());
|
||||
await _run(TokenStorage.removeTokenInStorage);
|
||||
await _run(SocketIdStorage.removeSocketIdInStorage);
|
||||
await _run(() => sl<ProfileAvatarCubit>().reset());
|
||||
await _run(() => sl<ChatsCubit>().reset());
|
||||
await _run(() => sl<NotificationsCubit>().reset());
|
||||
await _run(() => sl<NewMessagesCubit>().reset());
|
||||
await _run(() => sl<FeedCubit>().reset());
|
||||
await _run(() async => ProfileCubit.clearMyProfileReference());
|
||||
await _run(() => sl<VideoCallSessionCubit>().endCall());
|
||||
await _run(() async => AppMetrica.setUserProfileID(null));
|
||||
await _run(
|
||||
() => sl<Prefs>().remove(PrefsKeys.isEmailVerificationSkipped),
|
||||
);
|
||||
await _run(() => sl<Prefs>().remove(PrefsKeys.registrationDate));
|
||||
_authenticatedFingerprint = null;
|
||||
await _run(() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
final path =
|
||||
AppRouter.appRouter.routeInformationProvider.value.uri.path;
|
||||
if (path != RouteConstants.authScreen) {
|
||||
context.go(RouteConstants.authScreen);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
_endingSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onAuthenticated() async {
|
||||
String? token;
|
||||
try {
|
||||
token = await TokenStorage.getTokenFromStorage();
|
||||
} catch (e, st) {
|
||||
await _report(e, st);
|
||||
return;
|
||||
}
|
||||
if (token == null || token.isEmpty) return;
|
||||
final fingerprint = token.hashCode;
|
||||
if (_authenticatedFingerprint == fingerprint) return;
|
||||
_authenticatedFingerprint = fingerprint;
|
||||
|
||||
await _run(() async {
|
||||
await _wsManager.clear();
|
||||
await _wsManager.initialize();
|
||||
});
|
||||
await _run(() => sl<ProfileAvatarCubit>().getMyProfile());
|
||||
await _run(() => sl<ChatsCubit>().getChatRoomsWithLoading());
|
||||
await _run(() => sl<NotificationsCubit>().getNotifications());
|
||||
await _run(() => sl<FirebaseApi>().registerPushToken());
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _run(() => sl<FirebaseApi>().unregisterPushTokens());
|
||||
await _run(
|
||||
() => sl<Dio>().post('${sl<AppConfig>().baseUrl}/api/profile/logout'),
|
||||
);
|
||||
} finally {
|
||||
await endSession(reason: SessionEndReason.logout);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _run(FutureOr<void> Function() action) async {
|
||||
try {
|
||||
await action();
|
||||
} catch (e, st) {
|
||||
await _report(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _report(Object error, StackTrace stackTrace) {
|
||||
return _errorReporter.report(
|
||||
error,
|
||||
stackTrace,
|
||||
origin: 'SessionLifecycleService',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user