Merge pull request 'Enhance authentication flow and user role management' (#218) from fixed_role_bug into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/218
This commit is contained in:
a.ghabeli
2025-10-14 12:26:41 +03:30
9 changed files with 80 additions and 8 deletions
+7 -2
View File
@@ -157,11 +157,16 @@ List<SingleChildWidget> get services {
List<SingleChildWidget> get authProvider {
return [
ChangeNotifierProvider(
create: (context) => AuthViewModel(authRepository: context.read()),
create: (context) {
final vm = AuthViewModel(authRepository: context.read());
// Hydrate stored user/customer so userRole is available on hot reloads
vm.hydrateFromStorage();
return vm;
},
lazy: false, // Load immediately for auth state checks
),
ChangeNotifierProvider(
ChangeNotifierProvider(
create: (context) => FinalCompletionOfDocumentsViewModel(),
lazy: false, // Load immediately for auth state checks
),
@@ -1,3 +1,4 @@
import 'package:tm_app/data/services/model/customer/customer.dart';
import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/login_response/login_response_model.dart';
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart';
@@ -51,4 +52,8 @@ class AuthRepository {
Future<Result<bool>> checkIsLoggedIn() async {
return _authService.checkIsLoggedIn();
}
Future<Customer?> getStoredCustomer() async {
return _authService.getStoredCustomer();
}
}
+32 -1
View File
@@ -6,6 +6,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/data/services/api/auth_api.dart';
import 'package:tm_app/data/services/model/customer/customer.dart';
import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/login_response/login_response_model.dart';
import 'package:tm_app/data/services/model/logout_response/logout_response.dart';
@@ -55,9 +56,17 @@ class AuthService {
final token = result.value.data.accessToken;
AppLogger().info('🔑 Bearer token: $token');
final refreshToken = result.value.data.refreshToken;
final customer = result.value.data.customer;
await prefs.setString('bearer', token!);
await prefs.setString('refresh_token', refreshToken!);
// Save customer data as JSON string
if (customer != null) {
final customerJson = jsonEncode(customer.toJson());
await prefs.setString('customer_data', customerJson);
AppLogger().info('👤 Customer data saved to preferences');
}
}
return result;
}
@@ -68,6 +77,12 @@ class AuthService {
method: 'DELETE',
(json) => LogoutResponse.fromJson(json),
);
// Clear local data after successful logout
if (result is Ok<LogoutResponse>) {
await localLogout();
}
return result;
}
@@ -89,7 +104,8 @@ class AuthService {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('bearer');
await prefs.remove('refresh_token');
appLogger.info('tokens cleared!');
await prefs.remove('customer_data');
appLogger.info('tokens and customer data cleared!');
}
Future<Result<ForgotPasswordResponseModel>> forgotPassword({
@@ -131,4 +147,19 @@ class AuthService {
}
return const Result.ok(true);
}
Future<Customer?> getStoredCustomer() async {
final prefs = await SharedPreferences.getInstance();
final customerJson = prefs.getString('customer_data');
if (customerJson == null) {
return null;
}
try {
final customerMap = jsonDecode(customerJson) as Map<String, dynamic>;
return Customer.fromJson(customerMap);
} catch (e) {
AppLogger().error('❌ Failed to parse stored customer data: $e');
return null;
}
}
}
+25
View File
@@ -46,6 +46,19 @@ class AuthViewModel with ChangeNotifier {
notifyListeners();
}
Future<void> hydrateFromStorage() async {
try {
final customer = await _authRepository.getStoredCustomer();
if (customer != null) {
_loggedInUser = customer;
_userRole = customer.role == 'admin' ? Role.admin : Role.analyst;
notifyListeners();
}
} catch (e) {
AppLogger().error('❌ Failed to hydrate auth state: $e');
}
}
void _onTextChanged() {
notifyListeners();
}
@@ -97,11 +110,22 @@ class AuthViewModel with ChangeNotifier {
if (result is Ok<bool>) {
if (result.value) {
_isLoggedIn = result.value;
// Restore customer data from SharedPreferences
final customer = await _authRepository.getStoredCustomer();
if (customer != null) {
_loggedInUser = customer;
_userRole = customer.role == 'admin' ? Role.admin : Role.analyst;
}
} else {
_isLoggedIn = false;
_loggedInUser = null;
_userRole = null;
}
} else {
_isLoggedIn = false;
_loggedInUser = null;
_userRole = null;
}
notifyListeners();
}
@@ -113,6 +137,7 @@ class AuthViewModel with ChangeNotifier {
passwordController.clear();
_isLoggedIn = false;
_loggedInUser = null;
_userRole = null;
notifyListeners();
}
@@ -29,7 +29,7 @@ class _MeetingTimeDialogState extends State<MeetingTimeDialog> {
@override
Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole;
final userRole = context.watch<AuthViewModel>().userRole;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@@ -35,7 +35,7 @@ class TenderDetailActions extends StatefulWidget {
class _TenderDetailActionsState extends State<TenderDetailActions> {
@override
Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole;
final userRole = context.watch<AuthViewModel>().userRole;
return widget.isScreenBig
? _webActions(userRole!)
: Column(
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
@@ -50,7 +51,9 @@ class DesktopFinalCompletionOfDocumentsPage extends StatelessWidget {
children: [
BaseButton(
isEnabled: true,
onPressed: () {},
onPressed: () {
context.pop();
},
width: 176,
backgroundColor: AppColors.primary10,
textColor: AppColors.mainBlue,
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
@@ -61,7 +62,9 @@ class TabletFinalCompletionOfDocumentsPage extends StatelessWidget {
children: [
BaseButton(
isEnabled: true,
onPressed: () {},
onPressed: () {
context.pop();
},
width: 176.0.w(),
backgroundColor: AppColors.primary10,
textColor: AppColors.mainBlue,
+1 -1
View File
@@ -50,7 +50,7 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
@override
Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole;
final userRole = context.watch<AuthViewModel>().userRole;
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: SafeArea(