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,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<String> _unauthenticatedPaths = <String>[
'/auth/client/sign-in',
'/auth/client/sign-up',
'/auth/staff/phone/start',
];
@override
Future<void> 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);

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