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.
|
/// Generic sign-out.
|
||||||
static const String signOut = '$baseUrl/auth/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.
|
/// Get current session data.
|
||||||
static const String session = '$baseUrl/auth/session';
|
static const String session = '$baseUrl/auth/session';
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,39 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
/// An interceptor that adds the Firebase Auth ID token to the Authorization header.
|
/// 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 {
|
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
|
@override
|
||||||
Future<void> onRequest(
|
Future<void> onRequest(
|
||||||
RequestOptions options,
|
RequestOptions options,
|
||||||
RequestInterceptorHandler handler,
|
RequestInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
final User? user = FirebaseAuth.instance.currentUser;
|
// Skip token injection for endpoints that don't require authentication.
|
||||||
if (user != null) {
|
final bool skipAuth = _unauthenticatedPaths.any(
|
||||||
try {
|
(String path) => options.path.contains(path),
|
||||||
final String? token = await user.getIdToken();
|
);
|
||||||
if (token != null) {
|
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
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);
|
return handler.next(options);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../api_service/api_service.dart';
|
import '../api_service/api_service.dart';
|
||||||
import '../api_service/core_api_services/v2_api_endpoints.dart';
|
import '../api_service/core_api_services/v2_api_endpoints.dart';
|
||||||
import '../api_service/mixins/session_handler_mixin.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.
|
/// 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>) {
|
if (response.data is Map<String, dynamic>) {
|
||||||
final Map<String, dynamic> data =
|
final Map<String, dynamic> data =
|
||||||
response.data as Map<String, dynamic>;
|
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?;
|
final String? role = data['role'] as String?;
|
||||||
return role;
|
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.
|
/// Signs out the current user from Firebase Auth and clears local state.
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
@@ -95,6 +124,8 @@ class V2SessionService with SessionHandlerMixin {
|
|||||||
debugPrint('[V2SessionService] Error signing out: $e');
|
debugPrint('[V2SessionService] Error signing out: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
} finally {
|
} finally {
|
||||||
|
StaffSessionStore.instance.clear();
|
||||||
|
ClientSessionStore.instance.clear();
|
||||||
handleSignOut();
|
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.
|
// Build the domain user from the V2 response.
|
||||||
final domain.User domainUser = domain.User(
|
final domain.User domainUser = domain.User(
|
||||||
id: userData?['id'] as String? ?? firebaseUser.uid,
|
id: userData?['id'] as String? ?? firebaseUser.uid,
|
||||||
@@ -226,11 +233,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
try {
|
try {
|
||||||
await _apiService.post(V2ApiEndpoints.signOut);
|
await _apiService.post(V2ApiEndpoints.staffSignOut);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Sign-out should not fail even if the API call fails.
|
// Sign-out should not fail even if the API call fails.
|
||||||
// The local sign-out below will clear the session regardless.
|
// The local sign-out below will clear the session regardless.
|
||||||
}
|
}
|
||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
|
StaffSessionStore.instance.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
|||||||
if (state.status == AuthStatus.authenticated) {
|
if (state.status == AuthStatus.authenticated) {
|
||||||
if (state.mode == AuthMode.signup) {
|
if (state.mode == AuthMode.signup) {
|
||||||
Modular.to.toProfileSetup();
|
Modular.to.toProfileSetup();
|
||||||
|
} else {
|
||||||
|
Modular.to.toStaffHome();
|
||||||
}
|
}
|
||||||
} else if (state.status == AuthStatus.error &&
|
} else if (state.status == AuthStatus.error &&
|
||||||
state.mode == AuthMode.signup) {
|
state.mode == AuthMode.signup) {
|
||||||
@@ -155,7 +157,7 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
|||||||
BlocProvider.of<AuthBloc>(
|
BlocProvider.of<AuthBloc>(
|
||||||
context,
|
context,
|
||||||
).add(AuthResetRequested(mode: widget.mode));
|
).add(AuthResetRequested(mode: widget.mode));
|
||||||
Modular.to.popSafe();;
|
Modular.to.popSafe();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|||||||
Reference in New Issue
Block a user