1
0
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:
2026-04-02 16:07:33 +09:00
parent 71a006cd7b
commit b582c82c6f
25 changed files with 1154 additions and 1160 deletions

View File

@@ -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