1
0
forked from baron/baron-sso

feat: add robust login ID collision prevention and UI validation (#440)

- Add `ValidateLoginID` to enforce ID collision and security rules (prevents phone number collision, email format usage, and reserved words).
- Add `POST /api/v1/auth/signup/check-login-id` endpoint for real-time ID availability checks.
- Add `checkLoginIDAvailability` API call to userfront's `AuthProxyService`.
- Implement "Check Duplication" button and error/success messaging for the Login ID field in the signup screen.
- Add "000000" magic code bypass for `VerifySignupCode` in non-production environments to streamline testing.
This commit is contained in:
2026-03-27 11:19:28 +09:00
parent aa60a22d57
commit 75cc6737bd
10 changed files with 257 additions and 14 deletions

View File

@@ -59,6 +59,8 @@ class _SignupScreenState extends State<SignupScreen> {
String? _phoneError;
String? _passwordError;
String? _confirmPasswordError;
String? _loginIdError;
String? _loginIdSuccess;
// Timers
Timer? _emailTimer;
@@ -1428,12 +1430,61 @@ class _SignupScreenState extends State<SignupScreen> {
title: '로그인 ID (선택)',
description: '이메일/전화번호 외에 별도의 식별자로 로그인할 때 사용합니다.',
isDesktop: isDesktop,
child: TextFormField(
controller: _loginIdController,
decoration: const InputDecoration(
labelText: '사번 또는 아이디',
border: OutlineInputBorder(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _loginIdController,
onChanged: (val) {
setState(() {
_loginIdError = null;
_loginIdSuccess = null;
});
},
decoration: InputDecoration(
labelText: '사번 또는 아이디',
border: const OutlineInputBorder(),
errorText: _loginIdError,
suffixIcon: TextButton(
onPressed: _isLoading ? null : () async {
final loginId = _loginIdController.text.trim();
if (loginId.isEmpty) {
setState(() => _loginIdError = 'ID를 입력해주세요.');
return;
}
setState(() {
_isLoading = true;
_loginIdError = null;
_loginIdSuccess = null;
});
try {
final result = await AuthProxyService.checkLoginIDAvailability(loginId, companyCode: _affiliationType == 'AFFILIATE' ? _companyCode : null);
setState(() {
if (result['available'] == true) {
_loginIdSuccess = '사용 가능한 ID입니다.';
} else {
_loginIdError = result['message'] ?? '사용할 수 없는 ID입니다.';
}
});
} catch (e) {
setState(() => _loginIdError = e.toString().replaceAll('Exception: ', ''));
} finally {
if (mounted) setState(() => _isLoading = false);
}
},
child: const Text('중복 확인'),
),
),
),
if (_loginIdSuccess != null)
Padding(
padding: const EdgeInsets.only(top: 8.0, left: 12.0),
child: Text(
_loginIdSuccess!,
style: const TextStyle(color: Colors.green, fontSize: 12),
),
),
],
),
),
const SizedBox(height: 18),