// lib/services/auth/auth_api_service.dart // // سرویس برای endpoint های auth در API gateway جدید (Nest microservice). // شامل: OTP، required-actions (معادل checkHasPassword)، user info، update profile، ... import 'dart:async'; import 'dart:convert'; import 'dart:developer' as dev; import 'dart:io' show SocketException; // ignore: depend_on_referenced_packages import 'package:http/http.dart' as http; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/services/auth/token_storage.dart'; /// در صورت دریافت SocketException (مثلا DNS resolve fail در لحظه‌ی boot /// emulator)، تلاش را با تاخیر تکرار می‌کند. در غیر این صورت همان نتیجه‌ی /// اولیه را برمی‌گرداند. Future _withRetry(Future Function() body, {int retries = 2}) async { for (var attempt = 0; attempt <= retries; attempt++) { try { return await body(); } on SocketException { if (attempt == retries) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on http.ClientException { if (attempt == retries) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on TimeoutException { if (attempt == retries) rethrow; } } // unreachable throw StateError('retry loop exited without return'); } class AuthApiResult { final bool isSuccess; final int? statusCode; final T? data; final String? errorMessage; const AuthApiResult({ required this.isSuccess, this.statusCode, this.data, this.errorMessage, }); } class AuthApiService { AuthApiService._(); static final AuthApiService instance = AuthApiService._(); String get _base => AuthConfig.apiBaseUrl; Map _headers({bool withAuth = false}) { final headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; if (withAuth) { final t = TokenStorage.accessToken; if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t'; } return headers; } // --------------------------------------------------------------- // معادل checkHasPassword قدیمی. // GET /auth/users/required-actions?username=X // پاسخ: { id, requiredActions: [...] } // - اگر `UPDATE_PASSWORD` در لیست باشد => کاربر هنوز رمز ندارد // - اگر id خالی باشد => کاربر موجود نیست (404 منطقی) // --------------------------------------------------------------- Future>> getRequiredActions( String username, ) async { try { final url = Uri.parse( '$_base/auth/users/required-actions', ).replace(queryParameters: {'username': username}); final response = await _withRetry( () => http.get(url, headers: _headers()).timeout(_timeout), ); final body = _safeDecode(response.body); if (response.statusCode == 200) { if (body is Map && (body['id'] == null || (body['id'] as String).isEmpty)) { return const AuthApiResult( isSuccess: false, statusCode: 404, errorMessage: 'کاربر موجود نمی‌باشد.', ); } return AuthApiResult( isSuccess: true, statusCode: 200, data: body is Map ? body : null, ); } return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'خطا در دریافت اطلاعات کاربر.', ); } catch (e) { dev.log('getRequiredActions error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // POST /auth/users/generate-otp body: { username } // --------------------------------------------------------------- Future> generateOtp(String username) async { return _postJson( path: '/auth/users/generate-otp', body: {'username': username}, ); } // --------------------------------------------------------------- // POST /auth/users/validate-otp body: { username, otp } // پاسخ موفق: { isValid: true|false } // --------------------------------------------------------------- Future> validateOtp({ required String username, required String otp, }) async { try { final response = await http .post( Uri.parse('$_base/auth/users/validate-otp'), headers: _headers(), body: jsonEncode({'username': username, 'otp': otp}), ) .timeout(_timeout); final body = _safeDecode(response.body); if (response.statusCode == 200 && body is Map) { final valid = body['isValid'] == true; return AuthApiResult( isSuccess: valid, statusCode: 200, data: valid, errorMessage: valid ? null : 'کد وارد شده صحیح نمی‌باشد.', ); } return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'کد وارد شده صحیح نمی‌باشد.', ); } catch (e) { dev.log('validateOtp error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // POST /auth/users/reset-password-with-otp // body: { username, otp, newPassword } // --------------------------------------------------------------- Future> resetPasswordWithOtp({ required String username, required String otp, required String newPassword, }) async { return _postJson( path: '/auth/users/reset-password-with-otp', body: { 'username': username, 'otp': otp, 'newPassword': newPassword, }, ); } // --------------------------------------------------------------- // GET /auth/users/:id (نیاز به Bearer) // --------------------------------------------------------------- Future>> getUserInfo(String id) async { try { final response = await http .get( Uri.parse('$_base/auth/users/$id'), headers: _headers(withAuth: true), ) .timeout(_timeout); final body = _safeDecode(response.body); if (response.statusCode == 200 && body is Map) { return AuthApiResult( isSuccess: true, statusCode: 200, data: body, ); } return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'خطا در دریافت اطلاعات کاربر.', ); } catch (e) { dev.log('getUserInfo error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // PATCH /auth/users/:id (نیاز به Bearer) // --------------------------------------------------------------- Future>> updateUser({ required String id, required Map data, }) async { try { final response = await http .patch( Uri.parse('$_base/auth/users/$id'), headers: _headers(withAuth: true), body: jsonEncode(data), ) .timeout(_timeout); final body = _safeDecode(response.body); if (response.statusCode == 200 && body is Map) { return AuthApiResult( isSuccess: true, statusCode: 200, data: body, ); } return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'خطا در به‌روزرسانی اطلاعات.', ); } catch (e) { dev.log('updateUser error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // PUT /auth/users/:id/reset-password (نیاز به Bearer) // برای تغییر رمز کاربر لاگین‌شده (بدون OTP). // body: { password, temporary? } // --------------------------------------------------------------- Future> setOwnPassword({ required String id, required String newPassword, }) async { try { final response = await http .put( Uri.parse('$_base/auth/users/$id/reset-password'), headers: _headers(withAuth: true), body: jsonEncode({ 'password': newPassword, 'temporary': false, }), ) .timeout(_timeout); if (response.statusCode == 200 || response.statusCode == 204) { return const AuthApiResult(isSuccess: true, statusCode: 200); } final body = _safeDecode(response.body); return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'خطا در تغییر رمز عبور.', ); } catch (e) { dev.log('setOwnPassword error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // PATCH /auth/users/:id/profile-picture body: { key } // کلید S3 (آپلود‌شده توسط کلاینت) را به اواتار کاربر سینک می‌کند. // --------------------------------------------------------------- Future>> setProfilePicture({ required String id, required String s3Key, }) async { try { final response = await http .patch( Uri.parse('$_base/auth/users/$id/profile-picture'), headers: _headers(withAuth: true), body: jsonEncode({'key': s3Key}), ) .timeout(_timeout); final body = _safeDecode(response.body); if (response.statusCode == 200 && body is Map) { return AuthApiResult( isSuccess: true, statusCode: 200, data: body, ); } return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(body) ?? 'خطا در تنظیم عکس پروفایل.', ); } catch (e) { dev.log('setProfilePicture error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } // --------------------------------------------------------------- // Helpers // --------------------------------------------------------------- static const _timeout = Duration(seconds: 30); Future> _postJson({ required String path, required Map body, bool withAuth = false, }) async { try { final response = await http .post( Uri.parse('$_base$path'), headers: _headers(withAuth: withAuth), body: jsonEncode(body), ) .timeout(_timeout); if (response.statusCode == 200 || response.statusCode == 204) { return const AuthApiResult(isSuccess: true, statusCode: 200); } final decoded = _safeDecode(response.body); return AuthApiResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(decoded) ?? 'خطا در درخواست.', ); } catch (e) { dev.log('_postJson error: $e', name: 'AuthApiService'); return const AuthApiResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } dynamic _safeDecode(String body) { if (body.isEmpty) return null; try { return json.decode(body); } catch (_) { return null; } } String? _extractError(dynamic body) { if (body is Map) { final msg = body['message'] ?? body['error'] ?? body['msg']; if (msg is String && msg.isNotEmpty) return msg; if (msg is List && msg.isNotEmpty) return msg.first.toString(); } return null; } }