2026-02-23 19:23:30 +05:30
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
|
import 'package:real_estate_mobile/core/constants/api_constants.dart';
|
|
|
|
|
import 'package:real_estate_mobile/core/network/api_client.dart';
|
|
|
|
|
import 'package:real_estate_mobile/core/network/api_exceptions.dart';
|
|
|
|
|
import 'package:real_estate_mobile/core/storage/secure_storage.dart';
|
|
|
|
|
import 'package:real_estate_mobile/features/auth/data/models/auth_response.dart';
|
|
|
|
|
import 'package:real_estate_mobile/features/auth/data/models/register_request.dart';
|
|
|
|
|
import 'package:real_estate_mobile/features/auth/data/models/user_model.dart';
|
|
|
|
|
|
|
|
|
|
class AuthRepository {
|
|
|
|
|
final Dio _dio = ApiClient.instance.dio;
|
|
|
|
|
|
2026-04-10 17:35:59 +05:30
|
|
|
/// Extract a user-friendly message from a DioException.
|
|
|
|
|
/// NEVER returns Dio's raw technical text — always a short human sentence.
|
|
|
|
|
static String _friendlyError(DioException e, String fallback) {
|
|
|
|
|
// If our interceptor already wrapped it, use that message
|
|
|
|
|
if (e.error is ApiException) return (e.error as ApiException).message;
|
|
|
|
|
// Status-code based fallback
|
|
|
|
|
switch (e.response?.statusCode) {
|
|
|
|
|
case 400: return 'The request was invalid. Please check your input.';
|
|
|
|
|
case 401: return 'Invalid email or password.';
|
|
|
|
|
case 403: return 'You don\'t have permission to perform this action.';
|
|
|
|
|
case 404: return 'Not found.';
|
|
|
|
|
case 409: return 'This action conflicts with existing data.';
|
|
|
|
|
case 429: return 'Too many attempts. Please wait and try again.';
|
|
|
|
|
}
|
|
|
|
|
if (e.type == DioExceptionType.connectionTimeout ||
|
|
|
|
|
e.type == DioExceptionType.receiveTimeout ||
|
|
|
|
|
e.type == DioExceptionType.sendTimeout) {
|
|
|
|
|
return 'Request timed out. Please check your connection.';
|
|
|
|
|
}
|
|
|
|
|
if (e.type == DioExceptionType.connectionError) {
|
|
|
|
|
return 'Unable to reach the server. Check your internet connection.';
|
|
|
|
|
}
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 19:23:30 +05:30
|
|
|
Future<Map<String, dynamic>> login({
|
|
|
|
|
required String email,
|
|
|
|
|
required String password,
|
2026-04-13 20:26:10 +05:30
|
|
|
String? loginRole,
|
2026-02-23 19:23:30 +05:30
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
final response = await _dio.post(
|
|
|
|
|
ApiConstants.authLogin,
|
2026-04-13 20:26:10 +05:30
|
|
|
data: {
|
|
|
|
|
'email': email,
|
|
|
|
|
'password': password,
|
|
|
|
|
if (loginRole != null) 'loginRole': loginRole,
|
|
|
|
|
},
|
2026-02-23 19:23:30 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
|
|
|
|
|
|
|
|
|
// Check if 2FA is required
|
|
|
|
|
if (data['requiresTwoFactor'] == true) {
|
|
|
|
|
return {
|
|
|
|
|
'requiresTwoFactor': true,
|
|
|
|
|
'tempToken': data['tempToken'],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final authResponse = AuthResponse.fromJson(data);
|
|
|
|
|
|
|
|
|
|
await SecureStorage.saveTokens(
|
|
|
|
|
accessToken: authResponse.accessToken,
|
|
|
|
|
refreshToken: authResponse.refreshToken,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'requiresTwoFactor': false,
|
|
|
|
|
'user': authResponse.user,
|
|
|
|
|
};
|
|
|
|
|
} on DioException catch (e) {
|
2026-04-10 17:35:59 +05:30
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
2026-02-23 19:23:30 +05:30
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, 'Login failed'),
|
2026-02-23 19:23:30 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 13:26:16 +05:30
|
|
|
/// Verify 2FA TOTP token during login
|
|
|
|
|
Future<AuthResponse> verifyTwoFactor({
|
|
|
|
|
required String tempToken,
|
|
|
|
|
required String token,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
final response = await _dio.post(
|
|
|
|
|
ApiConstants.twoFactorVerify,
|
|
|
|
|
data: {'tempToken': tempToken, 'token': token},
|
|
|
|
|
);
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
|
|
|
|
final authResponse = AuthResponse.fromJson(data);
|
|
|
|
|
await SecureStorage.saveTokens(
|
|
|
|
|
accessToken: authResponse.accessToken,
|
|
|
|
|
refreshToken: authResponse.refreshToken,
|
|
|
|
|
);
|
|
|
|
|
return authResponse;
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
|
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, '2FA verification failed'),
|
2026-03-11 13:26:16 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verify backup code during login
|
|
|
|
|
Future<AuthResponse> verifyBackupCode({
|
|
|
|
|
required String tempToken,
|
|
|
|
|
required String backupCode,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
final response = await _dio.post(
|
|
|
|
|
ApiConstants.twoFactorVerifyBackup,
|
|
|
|
|
data: {'tempToken': tempToken, 'backupCode': backupCode},
|
|
|
|
|
);
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
|
|
|
|
final authResponse = AuthResponse.fromJson(data);
|
|
|
|
|
await SecureStorage.saveTokens(
|
|
|
|
|
accessToken: authResponse.accessToken,
|
|
|
|
|
refreshToken: authResponse.refreshToken,
|
|
|
|
|
);
|
|
|
|
|
return authResponse;
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
|
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, 'Backup code verification failed'),
|
2026-03-11 13:26:16 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 03:19:55 +05:30
|
|
|
Future<UserModel> getMe() async {
|
|
|
|
|
try {
|
|
|
|
|
final response = await _dio.get(ApiConstants.authMe);
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
|
|
|
|
return UserModel.fromJson(data);
|
|
|
|
|
} on DioException catch (e) {
|
2026-04-10 17:35:59 +05:30
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
2026-02-24 03:19:55 +05:30
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, 'Failed to fetch user'),
|
2026-02-24 03:19:55 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> logout() async {
|
|
|
|
|
try {
|
|
|
|
|
await _dio.post(ApiConstants.authLogout);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Ignore logout API errors — still clear local tokens
|
|
|
|
|
} finally {
|
|
|
|
|
await SecureStorage.clearTokens();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 00:07:31 +05:30
|
|
|
/// Social auth (Google, Facebook, etc.) — calls POST /auth/social
|
|
|
|
|
Future<UserModel> socialAuth({
|
|
|
|
|
required String provider,
|
|
|
|
|
required String providerId,
|
|
|
|
|
required String email,
|
|
|
|
|
String? name,
|
|
|
|
|
String? avatar,
|
2026-03-31 22:45:16 +05:30
|
|
|
String? role,
|
|
|
|
|
String? agentTypeId,
|
|
|
|
|
String mode = 'signup',
|
2026-03-26 00:07:31 +05:30
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
final response = await _dio.post(
|
|
|
|
|
ApiConstants.authSocial,
|
|
|
|
|
data: {
|
|
|
|
|
'provider': provider,
|
|
|
|
|
'providerId': providerId,
|
|
|
|
|
'email': email,
|
2026-03-31 22:45:16 +05:30
|
|
|
'mode': mode,
|
2026-03-26 00:07:31 +05:30
|
|
|
if (name != null) 'name': name,
|
|
|
|
|
if (avatar != null) 'avatar': avatar,
|
2026-03-31 22:45:16 +05:30
|
|
|
if (role != null) 'role': role,
|
|
|
|
|
if (agentTypeId != null) 'agentTypeId': agentTypeId,
|
2026-03-26 00:07:31 +05:30
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
|
|
|
|
final authResponse = AuthResponse.fromJson(data);
|
|
|
|
|
|
|
|
|
|
await SecureStorage.saveTokens(
|
|
|
|
|
accessToken: authResponse.accessToken,
|
|
|
|
|
refreshToken: authResponse.refreshToken,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return authResponse.user;
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
|
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, 'Social login failed'),
|
2026-03-26 00:07:31 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 15:32:17 +05:30
|
|
|
/// Register returns { message, user } — no tokens.
|
|
|
|
|
/// The user must verify their email before logging in.
|
|
|
|
|
Future<Map<String, dynamic>> register(RegisterRequest request) async {
|
2026-02-23 19:23:30 +05:30
|
|
|
try {
|
|
|
|
|
final response = await _dio.post(
|
|
|
|
|
ApiConstants.authRegister,
|
|
|
|
|
data: request.toJson(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final data = response.data['data'] as Map<String, dynamic>;
|
2026-03-16 15:32:17 +05:30
|
|
|
return data;
|
2026-02-23 19:23:30 +05:30
|
|
|
} on DioException catch (e) {
|
2026-04-10 17:35:59 +05:30
|
|
|
if (e.error is ApiException) throw e.error as ApiException;
|
2026-02-23 19:23:30 +05:30
|
|
|
throw ApiException(
|
2026-04-10 17:35:59 +05:30
|
|
|
message: _friendlyError(e, 'Registration failed'),
|
2026-02-23 19:23:30 +05:30
|
|
|
statusCode: e.response?.statusCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|