From b289ed3b02a0e6caf2dfa479e604018420964e77 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 17 Mar 2026 10:21:57 -0400 Subject: [PATCH] feat(auth): add staff-specific sign-out endpoint and enhance session management --- .../core_api_services/v2_api_endpoints.dart | 3 ++ .../inspectors/auth_interceptor.dart | 34 ++++++++++++++----- .../services/session/v2_session_service.dart | 31 +++++++++++++++++ .../auth_repository_impl.dart | 10 +++++- .../pages/phone_verification_page.dart | 4 ++- 5 files changed, 72 insertions(+), 10 deletions(-) diff --git a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart index a902410f..dcc9c619 100644 --- a/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart +++ b/apps/mobile/packages/core/lib/src/services/api_service/core_api_services/v2_api_endpoints.dart @@ -30,6 +30,9 @@ class V2ApiEndpoints { /// Generic sign-out. static const String signOut = '$baseUrl/auth/sign-out'; + /// Staff-specific sign-out. + static const String staffSignOut = '$baseUrl/auth/staff/sign-out'; + /// Get current session data. static const String session = '$baseUrl/auth/session'; diff --git a/apps/mobile/packages/core/lib/src/services/api_service/inspectors/auth_interceptor.dart b/apps/mobile/packages/core/lib/src/services/api_service/inspectors/auth_interceptor.dart index d6974e57..b52849b7 100644 --- a/apps/mobile/packages/core/lib/src/services/api_service/inspectors/auth_interceptor.dart +++ b/apps/mobile/packages/core/lib/src/services/api_service/inspectors/auth_interceptor.dart @@ -2,21 +2,39 @@ import 'package:dio/dio.dart'; import 'package:firebase_auth/firebase_auth.dart'; /// An interceptor that adds the Firebase Auth ID token to the Authorization header. +/// +/// Skips unauthenticated auth endpoints (sign-in, sign-up, phone/start) since +/// the user has no Firebase session yet. Sign-out, session, and phone/verify +/// endpoints DO require the token. class AuthInterceptor extends Interceptor { + /// Auth paths that must NOT receive a Bearer token (no session exists yet). + static const List _unauthenticatedPaths = [ + '/auth/client/sign-in', + '/auth/client/sign-up', + '/auth/staff/phone/start', + ]; + @override Future onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { - final User? user = FirebaseAuth.instance.currentUser; - if (user != null) { - try { - final String? token = await user.getIdToken(); - if (token != null) { - options.headers['Authorization'] = 'Bearer $token'; + // Skip token injection for endpoints that don't require authentication. + final bool skipAuth = _unauthenticatedPaths.any( + (String path) => options.path.contains(path), + ); + + if (!skipAuth) { + final User? user = FirebaseAuth.instance.currentUser; + if (user != null) { + try { + final String? token = await user.getIdToken(); + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } + } catch (e) { + rethrow; } - } catch (e) { - rethrow; } } return handler.next(options); diff --git a/apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart b/apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart index b126d74b..126ada3b 100644 --- a/apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart +++ b/apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart @@ -5,6 +5,8 @@ import 'package:krow_domain/krow_domain.dart'; import '../api_service/api_service.dart'; import '../api_service/core_api_services/v2_api_endpoints.dart'; import '../api_service/mixins/session_handler_mixin.dart'; +import 'client_session_store.dart'; +import 'staff_session_store.dart'; /// A singleton service that manages user session state via the V2 REST API. /// @@ -67,6 +69,11 @@ class V2SessionService with SessionHandlerMixin { if (response.data is Map) { final Map data = response.data as Map; + + // Hydrate session stores from the session endpoint response. + // Per V2 auth doc, GET /auth/session is used for app startup hydration. + _hydrateSessionStores(data); + final String? role = data['role'] as String?; return role; } @@ -77,6 +84,28 @@ class V2SessionService with SessionHandlerMixin { } } + /// Hydrates session stores from a `GET /auth/session` response. + /// + /// The session endpoint returns `{ user, tenant, business, vendor, staff }` + /// which maps to both [ClientSession] and [StaffSession] entities. + void _hydrateSessionStores(Map data) { + try { + // Hydrate staff session if staff context is present. + if (data['staff'] is Map) { + final StaffSession staffSession = StaffSession.fromJson(data); + StaffSessionStore.instance.setSession(staffSession); + } + + // Hydrate client session if business context is present. + if (data['business'] is Map) { + final ClientSession clientSession = ClientSession.fromJson(data); + ClientSessionStore.instance.setSession(clientSession); + } + } catch (e) { + debugPrint('[V2SessionService] Error hydrating session stores: $e'); + } + } + /// Signs out the current user from Firebase Auth and clears local state. Future signOut() async { try { @@ -95,6 +124,8 @@ class V2SessionService with SessionHandlerMixin { debugPrint('[V2SessionService] Error signing out: $e'); rethrow; } finally { + StaffSessionStore.instance.clear(); + ClientSessionStore.instance.clear(); handleSignOut(); } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index df41895d..69ba37ea 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -210,6 +210,13 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } } + // Step 5: Populate StaffSessionStore from the V2 auth envelope. + if (staffData != null) { + final domain.StaffSession staffSession = + domain.StaffSession.fromJson(data); + StaffSessionStore.instance.setSession(staffSession); + } + // Build the domain user from the V2 response. final domain.User domainUser = domain.User( id: userData?['id'] as String? ?? firebaseUser.uid, @@ -226,11 +233,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { @override Future signOut() async { try { - await _apiService.post(V2ApiEndpoints.signOut); + await _apiService.post(V2ApiEndpoints.staffSignOut); } catch (_) { // Sign-out should not fail even if the API call fails. // The local sign-out below will clear the session regardless. } await _auth.signOut(); + StaffSessionStore.instance.clear(); } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart index 93bf4e9f..17a0a531 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart @@ -105,6 +105,8 @@ class _PhoneVerificationPageState extends State { if (state.status == AuthStatus.authenticated) { if (state.mode == AuthMode.signup) { Modular.to.toProfileSetup(); + } else { + Modular.to.toStaffHome(); } } else if (state.status == AuthStatus.error && state.mode == AuthMode.signup) { @@ -155,7 +157,7 @@ class _PhoneVerificationPageState extends State { BlocProvider.of( context, ).add(AuthResetRequested(mode: widget.mode)); - Modular.to.popSafe();; + Modular.to.popSafe(); }, ), body: SafeArea(