deda384aaa
Addresses PR review on the auth interceptor: - Guard against unbounded retry recursion: tag the replayed request with extra['auth_retried'] and skip the refresh/replay path on a second 401 so it can't recurse refresh -> replay indefinitely. Drop the redundant manual Authorization header on the replay (onRequest re-injects it). - Fix the concurrent-refresh race: coalesce concurrent 401s onto a single in-flight refresh future so the rotating refresh token is only consumed once, instead of later refreshes posting an already-consumed token and spuriously logging out. (Chose the shared-future approach over QueuedInterceptor, which deadlocks with the replay-via-same-Dio pattern.) - Centralise the 'bearer' / 'refresh_token' / 'customer_data' pref keys in a PrefKeys constants class used by the interceptor, network manager, auth service and router so they can't silently diverge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
5.2 KiB
Dart
167 lines
5.2 KiB
Dart
// ignore_for_file: prefer_single_quotes
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:tm_app/core/constants/pref_keys.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';
|
|
import 'package:tm_app/data/services/model/reset_password_response/reset_password_response.dart';
|
|
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart';
|
|
|
|
import '../../core/network/network_manager.dart';
|
|
import '../../core/utils/result.dart';
|
|
|
|
class AuthService {
|
|
AuthService({required NetworkManager networkManager})
|
|
: _networkManager = networkManager;
|
|
|
|
final NetworkManager _networkManager;
|
|
|
|
Future<Result<LoginResponseModel>> login({
|
|
required String username,
|
|
required String password,
|
|
}) async {
|
|
//get device fcm token with error handling
|
|
String fcmToken = '';
|
|
try {
|
|
final FirebaseMessaging firebaseMessaging = FirebaseMessaging.instance;
|
|
await firebaseMessaging.requestPermission();
|
|
fcmToken = await firebaseMessaging.getToken() ?? '';
|
|
AppLogger().info('📱 FCM Token retrieved successfully');
|
|
} catch (e) {
|
|
AppLogger().error('❌ Failed to get FCM token: $e');
|
|
fcmToken = '';
|
|
}
|
|
|
|
var data = jsonEncode({
|
|
"password": password,
|
|
"username": username,
|
|
"device_token": fcmToken,
|
|
});
|
|
|
|
final result = await _networkManager.makeRequest(
|
|
AuthApi.login,
|
|
method: 'POST',
|
|
(json) => LoginResponseModel.fromJson(json),
|
|
data: data,
|
|
);
|
|
|
|
if (result is Ok<LoginResponseModel>) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
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(PrefKeys.bearer, token!);
|
|
await prefs.setString(PrefKeys.refreshToken, refreshToken!);
|
|
|
|
// Save customer data as JSON string
|
|
if (customer != null) {
|
|
final customerJson = jsonEncode(customer.toJson());
|
|
await prefs.setString(PrefKeys.customerData, customerJson);
|
|
AppLogger().info('👤 Customer data saved to preferences');
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Future<Result<LogoutResponse>> logout() async {
|
|
final result = await _networkManager.makeRequest(
|
|
AuthApi.logout,
|
|
method: 'DELETE',
|
|
(json) => LogoutResponse.fromJson(json),
|
|
);
|
|
|
|
// Clear local data after successful logout
|
|
if (result is Ok<LogoutResponse>) {
|
|
await localLogout();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Future<Result<ResetPasswordResponse>> resetPassword({
|
|
required String newPassword,
|
|
required String token,
|
|
}) async {
|
|
var data = jsonEncode({"new_password": newPassword, "token": token});
|
|
final result = await _networkManager.makeRequest(
|
|
AuthApi.resetPassword,
|
|
method: 'POST',
|
|
(json) => ResetPasswordResponse.fromJson(json),
|
|
data: data,
|
|
);
|
|
return result;
|
|
}
|
|
|
|
Future<void> localLogout() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(PrefKeys.bearer);
|
|
await prefs.remove(PrefKeys.refreshToken);
|
|
await prefs.remove(PrefKeys.customerData);
|
|
appLogger.info('tokens and customer data cleared!');
|
|
}
|
|
|
|
Future<Result<ForgotPasswordResponseModel>> forgotPassword({
|
|
required String email,
|
|
}) async {
|
|
final data = jsonEncode({"email": email});
|
|
|
|
final result = await _networkManager.makeRequest(
|
|
AuthApi.forgotPassword,
|
|
method: 'POST',
|
|
(json) => ForgotPasswordResponseModel.fromJson(json),
|
|
data: data,
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
Future<Result<VerifyOtpResponseModel>> verifyOtp({
|
|
required String code,
|
|
required String email,
|
|
}) async {
|
|
final data = jsonEncode({"code": code, "email": email});
|
|
|
|
final result = await _networkManager.makeRequest(
|
|
AuthApi.forgotPasswordOtp,
|
|
method: 'POST',
|
|
(json) => VerifyOtpResponseModel.fromJson(json),
|
|
data: data,
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
Future<Result<bool>> checkIsLoggedIn() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final token = prefs.getString(PrefKeys.bearer);
|
|
if (token == null) {
|
|
return const Result.ok(false);
|
|
}
|
|
return const Result.ok(true);
|
|
}
|
|
|
|
Future<Customer?> getStoredCustomer() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final customerJson = prefs.getString(PrefKeys.customerData);
|
|
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;
|
|
}
|
|
}
|
|
}
|