forked from baron/baron-sso
feat: implement multi-identifier architecture (Issue #496)
- Database: Add user_login_ids table for 1:N identifier mapping and remove legacy login_id column - Kratos: Update identity schema to use custom_login_ids array instead of a single id trait - Backend: Implement syncCustomLoginIDs to collect isLoginId fields across tenant schemas - Backend: Add backtracking logic to auto-assign session tenant based on used login identifier - Backend: Add 409 Conflict exception handling for Create/Update operations - AdminFront: Refactor UserDetailPage to a tabbed grid layout (Info, Tenants, Security) - AdminFront: Show '로그인 ID' badge on tenant schema fields used for authentication - UserFront: Remove legacy optional 'Login ID' input from signup flow - Tests: Add multi-identifier repository tests and update handler tests
This commit is contained in:
@@ -32,13 +32,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
|
||||
// Controllers
|
||||
final _emailController = TextEditingController();
|
||||
final _loginIdController = TextEditingController();
|
||||
final _emailCodeController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _phoneCodeController = TextEditingController();
|
||||
final _emailCodeController = TextEditingController(); // [Restore]
|
||||
final _nameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _phoneCodeController = TextEditingController(); // [Restore]
|
||||
final _deptController = TextEditingController();
|
||||
|
||||
// State
|
||||
@@ -60,8 +59,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
String? _phoneError;
|
||||
String? _passwordError;
|
||||
String? _confirmPasswordError;
|
||||
String? _loginIdError;
|
||||
String? _loginIdSuccess;
|
||||
|
||||
// Timers
|
||||
Timer? _emailTimer;
|
||||
@@ -102,7 +99,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
_emailTimer?.cancel();
|
||||
_phoneTimer?.cancel();
|
||||
_emailController.dispose();
|
||||
_loginIdController.dispose();
|
||||
_emailCodeController.dispose();
|
||||
_phoneController.dispose();
|
||||
_phoneCodeController.dispose();
|
||||
@@ -316,7 +312,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
try {
|
||||
await AuthProxyService.signup(
|
||||
email: _emailController.text.trim(),
|
||||
loginId: _loginIdController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
name: _nameController.text.trim(),
|
||||
phone: _phoneController.text.trim(),
|
||||
@@ -1437,95 +1432,6 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_buildProfileFieldGroup(
|
||||
title: '로그인 ID (선택)',
|
||||
description: '이메일/전화번호 외에 별도의 식별자로 로그인할 때 사용합니다.',
|
||||
isDesktop: isDesktop,
|
||||
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),
|
||||
_buildProfileFieldGroup(
|
||||
title: tr('ui.userfront.signup.profile.affiliation_type'),
|
||||
description: _isAffiliateEmail
|
||||
|
||||
Reference in New Issue
Block a user