1
0
forked from baron/baron-sso

Refactor password reset flow

This commit is contained in:
Lectom C Han
2026-01-27 11:16:44 +09:00
committed by kyy
parent 739da39a61
commit 72a36701da
9 changed files with 606 additions and 149 deletions

View File

@@ -17,8 +17,11 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
String? _loginId;
String? _token;
bool _isPasswordObscured = true;
bool _isConfirmPasswordObscured = true;
Map<String, dynamic>? _policy;
bool _isPolicyLoading = false;
@override
void initState() {
@@ -28,15 +31,43 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
// 2. Fallback to URI query parameter if not available via router
if (_loginId == null || _loginId!.isEmpty) {
final uri = Uri.base;
_loginId = uri.queryParameters['loginId'];
final uri = Uri.base;
_loginId = uri.queryParameters['loginId'];
}
// 토큰도 함께 읽어놓는다.
final uri = Uri.base;
_token = uri.queryParameters['token'];
_loadPolicy();
}
Future<void> _loadPolicy() async {
setState(() {
_isPolicyLoading = true;
});
try {
final policy = await AuthProxyService.fetchPasswordPolicy();
if (mounted) {
setState(() {
_policy = policy;
});
}
} catch (_) {
// 실패해도 기본 검증 로직 사용
} finally {
if (mounted) {
setState(() {
_isPolicyLoading = false;
});
}
}
}
Future<void> _handlePasswordReset() async {
if (_formKey.currentState?.validate() != true) return;
if (_loginId == null || _loginId!.isEmpty) {
_showError("유효하지 않은 재설정 링크입니다. (loginId 누락)");
if ((_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)) {
_showError("유효하지 않은 재설정 링크입니다. (loginId/token 누락)");
return;
}
@@ -44,8 +75,9 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
try {
await AuthProxyService.completePasswordReset(
_loginId!,
_passwordController.text,
loginId: _loginId,
token: _token,
newPassword: _passwordController.text,
);
if (mounted) {
@@ -74,6 +106,25 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
);
}
String _buildPolicyDescription() {
if (_isPolicyLoading) {
return "비밀번호 정책을 불러오는 중입니다...";
}
final minLength = (_policy?['minLength'] as int?) ?? 8;
final requiresLower = _policy?['lowercase'] ?? true;
final requiresUpper = _policy?['uppercase'] ?? true;
final requiresNumber = _policy?['number'] ?? true;
final requiresSymbol = _policy?['nonAlphanumeric'] ?? true;
final parts = <String>["최소 ${minLength}자 이상"];
if (requiresLower) parts.add("소문자 1개 이상");
if (requiresUpper) parts.add("대문자 1개 이상");
if (requiresNumber) parts.add("숫자 1개 이상");
if (requiresSymbol) parts.add("특수문자 1개 이상");
return parts.join(", ");
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -85,7 +136,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: _loginId == null || _loginId!.isEmpty
child: (_loginId == null || _loginId!.isEmpty) && (_token == null || _token!.isEmpty)
? _buildInvalidTokenView()
: Form(
key: _formKey,
@@ -102,10 +153,10 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Text(
"비밀번호는 최소 8자 이상이어야 하며,\n대소문자, 숫자, 특수문자를 모두 포함해야 합니다.",
Text(
_buildPolicyDescription(),
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 40),
TextFormField(
@@ -127,22 +178,24 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
),
),
validator: (value) {
if (value == null || value.isEmpty) {
final val = value ?? "";
if (val.isEmpty) {
return '비밀번호를 입력해주세요.';
}
if (value.length < 8) {
return '비밀번호는 8자 이상이어야 합니다.';
final minLength = (_policy?['minLength'] as int?) ?? 8;
if (val.length < minLength) {
return '비밀번호는 최소 $minLength자 이상이어야 합니다.';
}
if (!RegExp(r'(?=.*[a-z])').hasMatch(value)) {
if ((_policy?['lowercase'] ?? true) && !RegExp(r'(?=.*[a-z])').hasMatch(val)) {
return '최소 1개 이상의 소문자를 포함해야 합니다.';
}
if (!RegExp(r'(?=.*[A-Z])').hasMatch(value)) {
if ((_policy?['uppercase'] ?? true) && !RegExp(r'(?=.*[A-Z])').hasMatch(val)) {
return '최소 1개 이상의 대문자를 포함해야 합니다.';
}
if (!RegExp(r'(?=.*\d)').hasMatch(value)) {
if ((_policy?['number'] ?? true) && !RegExp(r'(?=.*\d)').hasMatch(val)) {
return '최소 1개 이상의 숫자를 포함해야 합니다.';
}
if (!RegExp(r'(?=.*[\W_])').hasMatch(value)) {
if ((_policy?['nonAlphanumeric'] ?? true) && !RegExp(r'(?=.*[\W_])').hasMatch(val)) {
return '최소 1개 이상의 특수문자를 포함해야 합니다.';
}
return null;