diff --git a/userfront/lib/features/auth/presentation/reset_password_screen.dart b/userfront/lib/features/auth/presentation/reset_password_screen.dart index 938831a2..f2951e1c 100644 --- a/userfront/lib/features/auth/presentation/reset_password_screen.dart +++ b/userfront/lib/features/auth/presentation/reset_password_screen.dart @@ -68,6 +68,7 @@ class _ResetPasswordScreenState extends State { } Future _handlePasswordReset() async { + if (_isLoading) return; if (_formKey.currentState?.validate() != true) return; if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) { @@ -76,6 +77,7 @@ class _ResetPasswordScreenState extends State { } setState(() => _isLoading = true); + bool isSuccess = false; try { await AuthProxyService.completePasswordReset( @@ -84,6 +86,7 @@ class _ResetPasswordScreenState extends State { newPassword: _passwordController.text, ); + isSuccess = true; if (mounted) { ToastService.success(tr('msg.userfront.reset.success')); context.go(buildLocalizedSigninPath(Uri.base)); @@ -98,7 +101,7 @@ class _ResetPasswordScreenState extends State { ); } } finally { - if (mounted) { + if (mounted && !isSuccess) { setState(() => _isLoading = false); } } diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index c489e2a8..fb29e7d7 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:userfront/i18n.dart'; import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/i18n/locale_utils.dart'; +import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; @@ -54,10 +55,80 @@ class _ProfilePageState extends ConsumerState { bool _showCurrentPassword = false; bool _showNewPassword = false; bool _showConfirmPassword = false; + Map? _passwordPolicy; + bool _isPasswordPolicyLoading = false; @override void initState() { super.initState(); + _loadPasswordPolicy(); + } + + Future _loadPasswordPolicy() async { + setState(() { + _isPasswordPolicyLoading = true; + }); + try { + final policy = await AuthProxyService.fetchPasswordPolicy(); + if (mounted) { + setState(() { + _passwordPolicy = policy; + }); + } + } catch (_) { + // 정책 조회 실패 시 기본 검증 규칙 사용 + } finally { + if (mounted) { + setState(() { + _isPasswordPolicyLoading = false; + }); + } + } + } + + String _buildPasswordPolicyDescription() { + if (_isPasswordPolicyLoading) { + return tr('msg.userfront.signup.policy.loading'); + } + + final minLength = (_passwordPolicy?['minLength'] as int?) ?? 12; + final minTypes = (_passwordPolicy?['minCharacterTypes'] as int?) ?? 0; + final requiresLower = _passwordPolicy?['lowercase'] ?? true; + final requiresUpper = _passwordPolicy?['uppercase'] ?? false; + final requiresNumber = _passwordPolicy?['number'] ?? true; + final requiresSymbol = _passwordPolicy?['nonAlphanumeric'] ?? true; + + final parts = [ + tr( + 'msg.userfront.signup.policy.min_length', + params: {'count': '$minLength'}, + ), + ]; + if (minTypes > 0) { + parts.add( + tr( + 'msg.userfront.signup.policy.min_types', + params: {'count': '$minTypes'}, + ), + ); + } + if (requiresLower) { + parts.add(tr('msg.userfront.signup.policy.lowercase')); + } + if (requiresUpper) { + parts.add(tr('msg.userfront.signup.policy.uppercase')); + } + if (requiresNumber) { + parts.add(tr('msg.userfront.signup.policy.number')); + } + if (requiresSymbol) { + parts.add(tr('msg.userfront.signup.policy.symbol')); + } + + return tr( + 'msg.userfront.signup.policy.summary', + params: {'rules': parts.join(", ")}, + ); } void _debugLog( @@ -267,6 +338,62 @@ class _ProfilePageState extends ConsumerState { ); return; } + + final minLength = (_passwordPolicy?['minLength'] as int?) ?? 12; + final minTypes = (_passwordPolicy?['minCharacterTypes'] as int?) ?? 0; + final hasLower = RegExp(r'[a-z]').hasMatch(newPassword); + final hasUpper = RegExp(r'[A-Z]').hasMatch(newPassword); + final hasNumber = RegExp(r'[0-9]').hasMatch(newPassword); + final hasSymbol = RegExp(r'[\W_]').hasMatch(newPassword); + int typeCount = 0; + if (hasLower) typeCount++; + if (hasUpper) typeCount++; + if (hasNumber) typeCount++; + if (hasSymbol) typeCount++; + + if (newPassword.length < minLength) { + setState( + () => _passwordError = tr( + 'msg.userfront.reset.error.min_length', + params: {'count': '$minLength'}, + ), + ); + return; + } + if (minTypes > 0 && typeCount < minTypes) { + setState( + () => _passwordError = tr( + 'msg.userfront.reset.error.min_types', + params: {'count': '$minTypes'}, + ), + ); + return; + } + if ((_passwordPolicy?['lowercase'] ?? true) && !hasLower) { + setState( + () => _passwordError = tr('msg.userfront.reset.error.lowercase'), + ); + return; + } + if ((_passwordPolicy?['uppercase'] ?? false) && !hasUpper) { + setState( + () => _passwordError = tr('msg.userfront.reset.error.uppercase'), + ); + return; + } + if ((_passwordPolicy?['number'] ?? true) && !hasNumber) { + setState( + () => _passwordError = tr('msg.userfront.reset.error.number'), + ); + return; + } + if ((_passwordPolicy?['nonAlphanumeric'] ?? true) && !hasSymbol) { + setState( + () => _passwordError = tr('msg.userfront.reset.error.symbol'), + ); + return; + } + if (newPassword != confirmPassword) { setState( () => _passwordError = tr('msg.userfront.profile.password.mismatch'), @@ -853,6 +980,11 @@ class _ProfilePageState extends ConsumerState { tr('msg.userfront.profile.password.subtitle'), style: const TextStyle(color: Color(0xFF6B7280)), ), + const SizedBox(height: 8), + Text( + _buildPasswordPolicyDescription(), + style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12), + ), const SizedBox(height: 16), TextField( controller: _currentPasswordController,