feat(auth): add staff-specific sign-out endpoint and enhance session management

This commit is contained in:
Achintha Isuru
2026-03-17 10:21:57 -04:00
parent bad2a3d976
commit b289ed3b02
5 changed files with 72 additions and 10 deletions

View File

@@ -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';

View File

@@ -2,12 +2,29 @@ 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<String> _unauthenticatedPaths = <String>[
'/auth/client/sign-in',
'/auth/client/sign-up',
'/auth/staff/phone/start',
];
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// 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 {
@@ -19,6 +36,7 @@ class AuthInterceptor extends Interceptor {
rethrow;
}
}
}
return handler.next(options);
}
}

View File

@@ -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<String, dynamic>) {
final Map<String, dynamic> data =
response.data as Map<String, dynamic>;
// 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<String, dynamic> data) {
try {
// Hydrate staff session if staff context is present.
if (data['staff'] is Map<String, dynamic>) {
final StaffSession staffSession = StaffSession.fromJson(data);
StaffSessionStore.instance.setSession(staffSession);
}
// Hydrate client session if business context is present.
if (data['business'] is Map<String, dynamic>) {
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<void> 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();
}
}

View File

@@ -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<void> 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();
}
}

View File

@@ -105,6 +105,8 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
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<PhoneVerificationPage> {
BlocProvider.of<AuthBloc>(
context,
).add(AuthResetRequested(mode: widget.mode));
Modular.to.popSafe();;
Modular.to.popSafe();
},
),
body: SafeArea(