forked from baron/baron-sso
Merge branch 'dev' into feature/i18n
This commit is contained in:
@@ -15,7 +15,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isAuthorized = false;
|
||||
String? _verifiedAdminPassword;
|
||||
@@ -28,7 +28,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
|
||||
Future<void> _verifyAccess() async {
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
|
||||
// Show blocking dialog
|
||||
final String? inputPassword = await showDialog<String>(
|
||||
context: context,
|
||||
@@ -86,7 +86,10 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid Password. Access Denied.'), backgroundColor: Colors.red),
|
||||
const SnackBar(
|
||||
content: Text('Invalid Password. Access Denied.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
context.go('/'); // Kick out
|
||||
}
|
||||
@@ -116,7 +119,9 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
String? phone = _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim();
|
||||
String? phone = _phoneController.text.trim().isEmpty
|
||||
? null
|
||||
: _phoneController.text.trim();
|
||||
if (phone != null && !phone.contains('@')) {
|
||||
phone = phone.replaceAll(RegExp(r'[-\s]'), '');
|
||||
if (phone.startsWith('010')) {
|
||||
@@ -128,14 +133,21 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
await AuthProxyService.createUser(
|
||||
loginId: loginId,
|
||||
adminPassword: _verifiedAdminPassword!,
|
||||
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
email: _emailController.text.trim().isEmpty
|
||||
? null
|
||||
: _emailController.text.trim(),
|
||||
phone: phone,
|
||||
displayName: _nameController.text.trim().isEmpty ? null : _nameController.text.trim(),
|
||||
displayName: _nameController.text.trim().isEmpty
|
||||
? null
|
||||
: _nameController.text.trim(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('User created successfully!'), backgroundColor: Colors.green),
|
||||
const SnackBar(
|
||||
content: Text('User created successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
_formKey.currentState!.reset();
|
||||
_loginIdController.clear();
|
||||
@@ -158,9 +170,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
// Hide content until authorized
|
||||
if (!_isAuthorized) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
@@ -186,7 +196,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
|
||||
TextFormField(
|
||||
controller: _loginIdController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -194,10 +204,12 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
border: OutlineInputBorder(),
|
||||
helperText: "Unique identifier (Email or Phone)",
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty ? 'Please enter Login ID' : null,
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? 'Please enter Login ID'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -207,7 +219,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -217,7 +229,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -228,14 +240,14 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text("Create User"),
|
||||
),
|
||||
],
|
||||
@@ -245,4 +257,4 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ class UserManagementScreen extends StatefulWidget {
|
||||
State<UserManagementScreen> createState() => _UserManagementScreenState();
|
||||
}
|
||||
|
||||
class _UserManagementScreenState extends State<UserManagementScreen> with SingleTickerProviderStateMixin {
|
||||
class _UserManagementScreenState extends State<UserManagementScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
bool _isAuthorized = false;
|
||||
String? _verifiedAdminPassword;
|
||||
@@ -23,7 +24,8 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
|
||||
// --- Create Tab Controllers ---
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _createLoginIdController = TextEditingController();
|
||||
final TextEditingController _createLoginIdController =
|
||||
TextEditingController();
|
||||
final TextEditingController _createEmailController = TextEditingController();
|
||||
final TextEditingController _createPhoneController = TextEditingController();
|
||||
final TextEditingController _createNameController = TextEditingController();
|
||||
@@ -50,7 +52,7 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
// --- Authentication ---
|
||||
Future<void> _verifyAccess() async {
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
|
||||
final String? inputPassword = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@@ -64,15 +66,24 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: "Password", border: OutlineInputBorder()),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Password",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (value) => Navigator.pop(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, null), child: const Text("Cancel")),
|
||||
FilledButton(onPressed: () => Navigator.pop(context, passwordController.text), child: const Text("Enter")),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, passwordController.text),
|
||||
child: const Text("Enter"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -96,7 +107,12 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid Password'), backgroundColor: Colors.red));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Invalid Password'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
context.go('/');
|
||||
}
|
||||
}
|
||||
@@ -107,7 +123,10 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
if (_verifiedAdminPassword == null) return;
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final users = await AuthProxyService.listUsers(_verifiedAdminPassword!, query: query);
|
||||
final users = await AuthProxyService.listUsers(
|
||||
_verifiedAdminPassword!,
|
||||
query: query,
|
||||
);
|
||||
setState(() => _users = users);
|
||||
} catch (e) {
|
||||
_showError("Failed to load users: $e");
|
||||
@@ -125,18 +144,23 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
|
||||
Future<void> _deleteUser(String loginId) async {
|
||||
if (_verifiedAdminPassword == null) return;
|
||||
|
||||
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Delete User"),
|
||||
content: Text("Are you sure you want to delete $loginId? This cannot be undone."),
|
||||
content: Text(
|
||||
"Are you sure you want to delete $loginId? This cannot be undone.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("Delete")
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("Delete"),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -158,11 +182,17 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
|
||||
Future<void> _toggleStatus(String loginId, String currentStatus) async {
|
||||
if (_verifiedAdminPassword == null) return;
|
||||
final newStatus = (currentStatus == "enabled" || currentStatus == "active") ? "disabled" : "enabled";
|
||||
|
||||
final newStatus = (currentStatus == "enabled" || currentStatus == "active")
|
||||
? "disabled"
|
||||
: "enabled";
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await AuthProxyService.updateUserStatus(_verifiedAdminPassword!, loginId, newStatus);
|
||||
await AuthProxyService.updateUserStatus(
|
||||
_verifiedAdminPassword!,
|
||||
loginId,
|
||||
newStatus,
|
||||
);
|
||||
_showSuccess("User status updated to $newStatus");
|
||||
_loadUsers(query: _searchController.text);
|
||||
} catch (e) {
|
||||
@@ -174,14 +204,20 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
|
||||
Future<void> _editUser(Map user) async {
|
||||
if (_verifiedAdminPassword == null) return;
|
||||
|
||||
|
||||
final loginIDs = (user['loginIds'] as List?) ?? [];
|
||||
final loginId = loginIDs.isNotEmpty ? loginIDs.first.toString() : "";
|
||||
if (loginId.isEmpty) return;
|
||||
|
||||
final nameController = TextEditingController(text: user['name'] ?? user['user']?['name'] ?? "");
|
||||
final emailController = TextEditingController(text: user['user']?['email'] ?? "");
|
||||
final phoneController = TextEditingController(text: user['user']?['phone'] ?? "");
|
||||
final nameController = TextEditingController(
|
||||
text: user['name'] ?? user['user']?['name'] ?? "",
|
||||
);
|
||||
final emailController = TextEditingController(
|
||||
text: user['user']?['email'] ?? "",
|
||||
);
|
||||
final phoneController = TextEditingController(
|
||||
text: user['user']?['phone'] ?? "",
|
||||
);
|
||||
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -190,14 +226,29 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: nameController, decoration: const InputDecoration(labelText: "Name")),
|
||||
TextField(controller: emailController, decoration: const InputDecoration(labelText: "Email")),
|
||||
TextField(controller: phoneController, decoration: const InputDecoration(labelText: "Phone")),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: "Name"),
|
||||
),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration: const InputDecoration(labelText: "Email"),
|
||||
),
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
decoration: const InputDecoration(labelText: "Phone"),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("Cancel")),
|
||||
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text("Save")),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text("Save"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -206,7 +257,9 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
String? phone = phoneController.text.trim().isEmpty ? null : phoneController.text.trim();
|
||||
String? phone = phoneController.text.trim().isEmpty
|
||||
? null
|
||||
: phoneController.text.trim();
|
||||
if (phone != null && !phone.contains('@')) {
|
||||
phone = phone.replaceAll(RegExp(r'[-\s]'), '');
|
||||
if (phone.startsWith('010')) {
|
||||
@@ -246,7 +299,9 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
}
|
||||
}
|
||||
|
||||
String? phone = _createPhoneController.text.trim().isEmpty ? null : _createPhoneController.text.trim();
|
||||
String? phone = _createPhoneController.text.trim().isEmpty
|
||||
? null
|
||||
: _createPhoneController.text.trim();
|
||||
if (phone != null && !phone.contains('@')) {
|
||||
phone = phone.replaceAll(RegExp(r'[-\s]'), '');
|
||||
if (phone.startsWith('010')) {
|
||||
@@ -258,9 +313,13 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
await AuthProxyService.createUser(
|
||||
loginId: loginId,
|
||||
adminPassword: _verifiedAdminPassword!,
|
||||
email: _createEmailController.text.trim().isEmpty ? null : _createEmailController.text.trim(),
|
||||
email: _createEmailController.text.trim().isEmpty
|
||||
? null
|
||||
: _createEmailController.text.trim(),
|
||||
phone: phone,
|
||||
displayName: _createNameController.text.trim().isEmpty ? null : _createNameController.text.trim(),
|
||||
displayName: _createNameController.text.trim().isEmpty
|
||||
? null
|
||||
: _createNameController.text.trim(),
|
||||
);
|
||||
|
||||
_showSuccess("User created successfully");
|
||||
@@ -269,11 +328,10 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
_createEmailController.clear();
|
||||
_createPhoneController.clear();
|
||||
_createNameController.clear();
|
||||
|
||||
|
||||
// Switch to list tab and reload
|
||||
_tabController.animateTo(0);
|
||||
_loadUsers();
|
||||
|
||||
} catch (e) {
|
||||
_showError("Error: $e");
|
||||
} finally {
|
||||
@@ -284,12 +342,16 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
// --- UI Helpers ---
|
||||
void _showError(String msg) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.red));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.red));
|
||||
}
|
||||
|
||||
void _showSuccess(String msg) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.green));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(msg), backgroundColor: Colors.green));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -315,10 +377,7 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildUserListTab(),
|
||||
_buildCreateUserTab(),
|
||||
],
|
||||
children: [_buildUserListTab(), _buildCreateUserTab()],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -341,32 +400,49 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _users.isEmpty
|
||||
? const Center(child: Text("No users found."))
|
||||
child: _users.isEmpty
|
||||
? const Center(child: Text("No users found."))
|
||||
: ListView.separated(
|
||||
itemCount: _users.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final user = _users[index];
|
||||
// 일부 응답은 최상위 또는 user 하위에 필드를 포함합니다.
|
||||
|
||||
|
||||
final loginIDs = (user['loginIds'] as List?) ?? [];
|
||||
final loginId = loginIDs.isNotEmpty ? loginIDs.first.toString() : "Unknown ID";
|
||||
final name = user['name'] ?? user['user']?['name'] ?? "No Name";
|
||||
final loginId = loginIDs.isNotEmpty
|
||||
? loginIDs.first.toString()
|
||||
: "Unknown ID";
|
||||
final name =
|
||||
user['name'] ?? user['user']?['name'] ?? "No Name";
|
||||
final status = user['status'] ?? "unknown";
|
||||
final isEnabled = status == "enabled" || status == "active";
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isEnabled ? Colors.green.shade100 : Colors.grey.shade300,
|
||||
child: Icon(Icons.person, color: isEnabled ? Colors.green : Colors.grey),
|
||||
backgroundColor: isEnabled
|
||||
? Colors.green.shade100
|
||||
: Colors.grey.shade300,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: isEnabled ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
title: Text(name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(loginId, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text("Status: $status", style: TextStyle(color: isEnabled ? Colors.green : Colors.red, fontSize: 12)),
|
||||
Text(
|
||||
loginId,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"Status: $status",
|
||||
style: TextStyle(
|
||||
color: isEnabled ? Colors.green : Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
@@ -378,7 +454,10 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
onPressed: () => _editUser(user),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(isEnabled ? Icons.block : Icons.check_circle, color: isEnabled ? Colors.orange : Colors.green),
|
||||
icon: Icon(
|
||||
isEnabled ? Icons.block : Icons.check_circle,
|
||||
color: isEnabled ? Colors.orange : Colors.green,
|
||||
),
|
||||
tooltip: isEnabled ? "Disable User" : "Enable User",
|
||||
onPressed: () => _toggleStatus(loginId, status),
|
||||
),
|
||||
@@ -417,27 +496,44 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
||||
border: OutlineInputBorder(),
|
||||
helperText: "Unique identifier (Email or Phone)",
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty ? 'Please enter Login ID' : null,
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? 'Please enter Login ID'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _createNameController,
|
||||
decoration: const InputDecoration(labelText: "Display Name", border: OutlineInputBorder(), prefixIcon: Icon(Icons.person)),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Display Name",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _createEmailController,
|
||||
decoration: const InputDecoration(labelText: "Email", border: OutlineInputBorder(), prefixIcon: Icon(Icons.email)),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _createPhoneController,
|
||||
decoration: const InputDecoration(labelText: "Phone Number", border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone), helperText: "010-xxxx-xxxx"),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Phone Number",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
helperText: "010-xxxx-xxxx",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _createUserSubmit,
|
||||
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text("Create User"),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -134,13 +134,16 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
|
||||
if (_message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Text(
|
||||
_message!,
|
||||
style: TextStyle(color: _success ? Colors.green : Colors.red, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
color: _success ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@@ -155,7 +158,7 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
if (!isLoggedIn && !_success)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
|
||||
@@ -17,7 +17,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
bool _isLoading = true;
|
||||
bool _isSubmitting = false;
|
||||
String? _error;
|
||||
|
||||
|
||||
// 사용자가 선택한 스코프 목록
|
||||
final Set<String> _selectedScopes = {};
|
||||
|
||||
@@ -41,8 +41,10 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
|
||||
Future<void> _fetchConsentInfo() async {
|
||||
try {
|
||||
final info = await AuthProxyService.getConsentInfo(widget.consentChallenge);
|
||||
|
||||
final info = await AuthProxyService.getConsentInfo(
|
||||
widget.consentChallenge,
|
||||
);
|
||||
|
||||
// [Skip Logic] 백엔드에서 자동 승인되어 리다이렉트 URL이 온 경우 즉시 이동
|
||||
if (info['redirectTo'] != null) {
|
||||
webWindow.redirectTo(info['redirectTo']);
|
||||
@@ -52,19 +54,20 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
// 백엔드에서 전달받은 커스텀 스코프 정보(scope_details) 적용
|
||||
if (info['scope_details'] != null) {
|
||||
final details = info['scope_details'] as Map<String, dynamic>;
|
||||
|
||||
|
||||
details.forEach((scope, detail) {
|
||||
if (detail is Map<String, dynamic>) {
|
||||
// 설명 업데이트
|
||||
if (detail['description'] != null && detail['description'].toString().isNotEmpty) {
|
||||
if (detail['description'] != null &&
|
||||
detail['description'].toString().isNotEmpty) {
|
||||
_scopeDescriptions[scope] = detail['description'].toString();
|
||||
}
|
||||
// 필수 여부 업데이트
|
||||
if (detail['mandatory'] == true) {
|
||||
_mandatoryScopes.add(scope);
|
||||
} else {
|
||||
// openid는 기본적으로 필수지만 설정에서 굳이 껐다면?
|
||||
// 안전을 위해 openid는 항상 필수로 유지하는 것이 좋지만,
|
||||
// openid는 기본적으로 필수지만 설정에서 굳이 껐다면?
|
||||
// 안전을 위해 openid는 항상 필수로 유지하는 것이 좋지만,
|
||||
// 여기서는 서버 설정을 존중하되 openid는 예외처리 할 수도 있음.
|
||||
// 우선 서버 설정이 있으면 반영 (단, openid는 제거하지 않음)
|
||||
if (scope != 'openid') {
|
||||
@@ -76,7 +79,8 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
}
|
||||
|
||||
// 초기 선택 상태 설정: 모든 요청된 스코프를 기본 선택
|
||||
final requestedScopes = (info['requested_scope'] as List<dynamic>?)?.cast<String>() ?? [];
|
||||
final requestedScopes =
|
||||
(info['requested_scope'] as List<dynamic>?)?.cast<String>() ?? [];
|
||||
_selectedScopes.addAll(requestedScopes);
|
||||
|
||||
setState(() {
|
||||
@@ -102,7 +106,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
widget.consentChallenge,
|
||||
grantScope: _selectedScopes.toList(),
|
||||
);
|
||||
|
||||
|
||||
if (result['redirectTo'] != null) {
|
||||
webWindow.redirectTo(result['redirectTo']);
|
||||
} else {
|
||||
@@ -142,7 +146,9 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
if (confirmed == true) {
|
||||
setState(() => _isSubmitting = true);
|
||||
try {
|
||||
final resp = await AuthProxyService.rejectConsent(widget.consentChallenge);
|
||||
final resp = await AuthProxyService.rejectConsent(
|
||||
widget.consentChallenge,
|
||||
);
|
||||
final redirectTo = resp['redirectTo'];
|
||||
if (redirectTo != null) {
|
||||
webWindow.redirectTo(redirectTo);
|
||||
@@ -152,9 +158,9 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
} catch (e) {
|
||||
setState(() => _isSubmitting = false);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('취소 처리 중 오류가 발생했습니다: $e')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('취소 처리 중 오류가 발생했습니다: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,13 +170,13 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
// 배경색을 약간 어둡게 처리하거나, 전체적인 테마 색상을 사용
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: Center(
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: _error != null
|
||||
? _buildErrorCard()
|
||||
: _buildConsentCard(context),
|
||||
? _buildErrorCard()
|
||||
: _buildConsentCard(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -196,7 +202,9 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
final clientName = _consentInfo?['client']?['client_name'] ?? '알 수 없는 앱';
|
||||
final clientId = _consentInfo?['client']?['client_id'] ?? '-';
|
||||
final clientLogo = _consentInfo?['client']?['logo_uri'];
|
||||
final requestedScopes = (_consentInfo?['requested_scope'] as List<dynamic>?)?.cast<String>() ?? [];
|
||||
final requestedScopes =
|
||||
(_consentInfo?['requested_scope'] as List<dynamic>?)?.cast<String>() ??
|
||||
[];
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
@@ -204,7 +212,9 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
@@ -235,7 +245,8 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (clientLogo != null && clientLogo.toString().isNotEmpty)
|
||||
if (clientLogo != null &&
|
||||
clientLogo.toString().isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: CircleAvatar(
|
||||
@@ -286,7 +297,10 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
children: [
|
||||
const Text(
|
||||
'요청된 권한',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'총 ${requestedScopes.length}개',
|
||||
@@ -354,7 +368,10 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
||||
)
|
||||
: const Text(
|
||||
'동의하고 계속하기',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -18,22 +18,19 @@ String _envOrDefault(String key, String fallback) {
|
||||
String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
|
||||
Future<AuditPage> _fetchAuthTimelinePage({String? cursor}) async {
|
||||
final queryParameters = <String, String>{
|
||||
'limit': '20',
|
||||
};
|
||||
final queryParameters = <String, String>{'limit': '20'};
|
||||
if (cursor != null && cursor.isNotEmpty) {
|
||||
queryParameters['cursor'] = cursor;
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/audit/auth/timeline')
|
||||
.replace(queryParameters: queryParameters);
|
||||
final url = Uri.parse(
|
||||
'$_baseUrl/api/v1/audit/auth/timeline',
|
||||
).replace(queryParameters: queryParameters);
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -60,10 +57,6 @@ Future<AuditPage> _fetchAuthTimelinePage({String? cursor}) async {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
typedef AuthTimelineFetcher = Future<AuditPage> Function({String? cursor});
|
||||
|
||||
final authTimelineFetcherProvider = Provider<AuthTimelineFetcher>((ref) {
|
||||
@@ -86,11 +79,11 @@ class AuthTimelineState {
|
||||
});
|
||||
|
||||
const AuthTimelineState.initial()
|
||||
: items = const [],
|
||||
nextCursor = null,
|
||||
isLoading = false,
|
||||
isLoadingMore = false,
|
||||
error = null;
|
||||
: items = const [],
|
||||
nextCursor = null,
|
||||
isLoading = false,
|
||||
isLoadingMore = false,
|
||||
error = null;
|
||||
|
||||
AuthTimelineState copyWith({
|
||||
List<AuditLogEntry>? items,
|
||||
@@ -187,6 +180,7 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
||||
}
|
||||
}
|
||||
|
||||
final authTimelineProvider = NotifierProvider<AuthTimelineNotifier, AuthTimelineState>(
|
||||
AuthTimelineNotifier.new,
|
||||
);
|
||||
final authTimelineProvider =
|
||||
NotifierProvider<AuthTimelineNotifier, AuthTimelineState>(
|
||||
AuthTimelineNotifier.new,
|
||||
);
|
||||
|
||||
@@ -64,14 +64,12 @@ class LinkedRpsNotifier extends AsyncNotifier<List<LinkedRp>> {
|
||||
try {
|
||||
final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
final url = Uri.parse('$baseUrl/api/v1/user/rp/linked');
|
||||
|
||||
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
final token = AuthTokenStore.getToken();
|
||||
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -85,7 +83,7 @@ class LinkedRpsNotifier extends AsyncNotifier<List<LinkedRp>> {
|
||||
|
||||
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final items = (body['items'] as List?) ?? [];
|
||||
|
||||
|
||||
return items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LinkedRp.fromJson)
|
||||
@@ -106,6 +104,7 @@ class LinkedRpsNotifier extends AsyncNotifier<List<LinkedRp>> {
|
||||
}
|
||||
}
|
||||
|
||||
final linkedRpsProvider = AsyncNotifierProvider<LinkedRpsNotifier, List<LinkedRp>>(() {
|
||||
return LinkedRpsNotifier();
|
||||
});
|
||||
final linkedRpsProvider =
|
||||
AsyncNotifierProvider<LinkedRpsNotifier, List<LinkedRp>>(() {
|
||||
return LinkedRpsNotifier();
|
||||
});
|
||||
|
||||
@@ -21,12 +21,7 @@ class Tenant {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'slug': slug,
|
||||
'description': description,
|
||||
};
|
||||
return {'id': id, 'name': name, 'slug': slug, 'description': description};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +57,9 @@ class UserProfile {
|
||||
department: json['department'] ?? '',
|
||||
affiliationType: json['affiliationType'] ?? '',
|
||||
companyCode: json['companyCode'] ?? '',
|
||||
metadata: json['metadata'] != null ? Map<String, dynamic>.from(json['metadata']) : null,
|
||||
metadata: json['metadata'] != null
|
||||
? Map<String, dynamic>.from(json['metadata'])
|
||||
: null,
|
||||
tenant: json['tenant'] != null ? Tenant.fromJson(json['tenant']) : null,
|
||||
);
|
||||
}
|
||||
@@ -81,11 +78,7 @@ class UserProfile {
|
||||
};
|
||||
}
|
||||
|
||||
UserProfile copyWith({
|
||||
String? name,
|
||||
String? phone,
|
||||
String? department,
|
||||
}) {
|
||||
UserProfile copyWith({String? name, String? phone, String? department}) {
|
||||
return UserProfile(
|
||||
id: id,
|
||||
email: email,
|
||||
|
||||
@@ -13,7 +13,8 @@ class ProfileRepository {
|
||||
return dotenv.env[key] ?? fallback;
|
||||
}
|
||||
|
||||
static String get _baseUrl => _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
static String get _baseUrl =>
|
||||
_envOrDefault('BACKEND_URL', 'https://sso.hmac.kr');
|
||||
|
||||
// Helper to get session token
|
||||
static Future<String?> _getToken() async {
|
||||
@@ -31,9 +32,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -67,9 +66,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -105,9 +102,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -142,9 +137,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
@@ -179,9 +172,7 @@ class ProfileRepository {
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
||||
final client = createHttpClient(withCredentials: useCookie);
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final headers = <String, String>{'Content-Type': 'application/json'};
|
||||
if (!useCookie && token != null) {
|
||||
headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
@@ -32,20 +32,20 @@ class ProfileNotifier extends AsyncNotifier<UserProfile?> {
|
||||
}) async {
|
||||
// Show loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
|
||||
// Perform update and then re-fetch profile
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(profileRepositoryProvider).updateMyProfile(
|
||||
name: name,
|
||||
phone: phone,
|
||||
department: department,
|
||||
);
|
||||
await ref
|
||||
.read(profileRepositoryProvider)
|
||||
.updateMyProfile(name: name, phone: phone, department: department);
|
||||
return _fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Provider definition
|
||||
final profileProvider = AsyncNotifierProvider<ProfileNotifier, UserProfile?>(() {
|
||||
return ProfileNotifier();
|
||||
});
|
||||
final profileProvider = AsyncNotifierProvider<ProfileNotifier, UserProfile?>(
|
||||
() {
|
||||
return ProfileNotifier();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,11 +4,7 @@ class ProfileInfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const ProfileInfoRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
const ProfileInfoRow({super.key, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user