// lib/views/authentication/authentication_state.dart // // State مربوط به جریان احراز هویت. ساختار بیرونی این کلاس و امضای متدها // عمدا بدون تغییر باقی مانده تا UI و screen ها به همان شکل قبل کار کنند. // اما درون متدها به‌جای فراخوانی endpoint های قدیمی، از سرویس‌های جدید // (KeycloakAuthService و AuthApiService) استفاده می‌کنیم. import 'package:didvan/main.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/services/auth/auth_api_service.dart'; import 'package:didvan/services/auth/keycloak_auth_service.dart'; import 'package:didvan/services/auth/token_storage.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:flutter/foundation.dart'; import 'package:home_widget/home_widget.dart'; class AuthenticationState extends CoreProvier { int _currentPageIndex = 0; String username = ''; String password = ''; /// OTPای که در مرحله‌ی verify کاربر وارد می‌کند تا بعدا در reset-password /// همراه newPassword به سرور فرستاده شود. String _verificationCode = ''; bool isShowCustomDialog = true; /// آیا کاربر روی Keycloak رمز فعال دارد؟ /// در مدل قدیمی: response /user/checkHasPassword /// در مدل جدید: اگر `UPDATE_PASSWORD` در requiredActions باشد => false. bool hasPassword = true; set currentPageIndex(int value) { _currentPageIndex = value; notifyListeners(); } int get currentPageIndex => _currentPageIndex; /// مرحله‌ی اول: بررسی وجود کاربر و این که آیا رمز دارد یا نه. Future confirmUsername() async { appState = AppState.isolatedBusy; // Users can enter either their Keycloak username OR their Iran phone // number. Phone gets swapped for the corresponding username here so // every downstream call (getRequiredActions, OTP, Keycloak grant) // sees a real username. final resolved = await KeycloakAuthService.instance.resolveIdentifier(username); if (resolved == null) { appState = AppState.failed; ActionSheetUtils(navigatorKey.currentContext!) .showAlert(AlertData(message: 'کاربر موجود نمی‌باشد.')); return; } username = resolved; final result = await AuthApiService.instance.getRequiredActions(username); if (result.isSuccess) { final data = result.data ?? const {}; final actions = (data['requiredActions'] as List?)?.cast() ?? const []; hasPassword = !actions.contains('UPDATE_PASSWORD'); appState = AppState.idle; if (hasPassword) { currentPageIndex = 1; // صفحه‌ی ورود رمز } else { // کاربر هنوز رمز ندارد => مستقیما به OTP می‌رویم تا تنظیم رمز انجام شود. await sendOtpToken(); currentPageIndex = 2; } } else { appState = AppState.failed; String message = result.errorMessage ?? 'خطایی رخ داد.'; if (result.statusCode == 404) { message = 'کاربر موجود نمی‌باشد.'; } ActionSheetUtils(navigatorKey.currentContext!) .showAlert(AlertData(message: message)); } } /// لاگین با username/password از طریق Keycloak. /// در صورت موفقیت، توکن ذخیره شده و `getUserInfo` صدا زده می‌شود. Future login(UserProvider userProvider) async { appState = AppState.isolatedBusy; final result = await KeycloakAuthService.instance.login( username: username, password: password, ); if (result.success) { final token = TokenStorage.accessToken; // برای سازگاری با کد قدیمی که مستقیم با Storage کلید `token` کار می‌کند // و home widget که توکن را برای rendering ویجت لازم دارد: if (!kIsWeb && token != null) { HomeWidget.saveWidgetData('token', token); } await userProvider.setAndGetToken(newToken: token); await userProvider.getUserInfo(); appState = AppState.idle; return token; } else { appState = AppState.failed; ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData(message: result.errorMessage ?? 'خطا در ورود.'), ); } return null; } /// درخواست ارسال OTP برای reset password (و یا تنظیم رمز اولیه). /// در صورت خطا یک alert به کاربر نشان می‌دهیم تا مشکل پنهان نماند. Future sendOtpToken() async { if (kDebugMode) { print('AuthenticationState: sendOtpToken for username="$username"'); } final result = await AuthApiService.instance.generateOtp(username); if (kDebugMode) { print('AuthenticationState: generateOtp result -> ' 'success=${result.isSuccess}, status=${result.statusCode}, ' 'error=${result.errorMessage}'); } if (!result.isSuccess) { ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData( message: result.errorMessage ?? 'خطا در ارسال کد یک‌بار مصرف. لطفا دوباره تلاش کنید.', ), ); } } /// تایید OTP. اگر معتبر بود، صفحه را به مرحله‌ی بعد (تنظیم رمز) می‌بریم. /// `_verificationCode` نگه داشته می‌شود تا در `resetPassword` /// همراه `newPassword` به سرور فرستاده شود (مدل جدید این دو را در یک /// درخواست `reset-password-with-otp` ترکیب می‌کند). Future verifyOtpToken(String token) async { appState = AppState.isolatedBusy; final result = await AuthApiService.instance.validateOtp( username: username, otp: token, ); appState = AppState.idle; if (result.isSuccess) { _verificationCode = token; currentPageIndex++; return true; } ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData( message: result.errorMessage ?? 'کد وارد شده صحیح نمی‌باشد', ), ); return false; } /// تنظیم/تغییر رمز با استفاده از OTP تاییدشده. Future resetPassword(String password) async { if (_verificationCode.isEmpty) { ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData(message: 'ابتدا کد یک‌بار مصرف را تایید کنید.'), ); return false; } appState = AppState.isolatedBusy; final result = await AuthApiService.instance.resetPasswordWithOtp( username: username, otp: _verificationCode, newPassword: password, ); appState = AppState.idle; if (result.isSuccess) { ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData( message: 'کلمه عبور با موفقیت تغییر کرد', aLertType: ALertType.success, ), ); return true; } ActionSheetUtils(navigatorKey.currentContext!).showAlert( AlertData(message: result.errorMessage ?? 'خطا در تغییر رمز.'), ); return false; } }