feat(auth): add staff-specific sign-out endpoint and enhance session management
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user