forked from baron/baron-sso
admin page add
This commit is contained in:
@@ -99,6 +99,50 @@ class AuthProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> checkAdminAuth(String adminPassword) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/check');
|
||||
try {
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Password': adminPassword,
|
||||
},
|
||||
);
|
||||
return response.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> createUser({
|
||||
required String loginId,
|
||||
required String adminPassword,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? displayName,
|
||||
}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/admin/users');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Password': adminPassword,
|
||||
},
|
||||
body: jsonEncode({
|
||||
'loginId': loginId,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'displayName': displayName,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to create user: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
|
||||
final url = Uri.parse('$_baseUrl/api/v1/client-log');
|
||||
try {
|
||||
|
||||
232
frontend/lib/features/admin/presentation/create_user_screen.dart
Normal file
232
frontend/lib/features/admin/presentation/create_user_screen.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/services/auth_proxy_service.dart';
|
||||
|
||||
class CreateUserScreen extends StatefulWidget {
|
||||
const CreateUserScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CreateUserScreen> createState() => _CreateUserScreenState();
|
||||
}
|
||||
|
||||
class _CreateUserScreenState extends State<CreateUserScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final TextEditingController _loginIdController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isAuthorized = false;
|
||||
String? _verifiedAdminPassword;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _verifyAccess());
|
||||
}
|
||||
|
||||
Future<void> _verifyAccess() async {
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
// Show blocking dialog
|
||||
final String? inputPassword = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false, // User must enter password or leave
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Admin Authentication Required"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("Please enter the admin password to access this page."),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Password",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofocus: true,
|
||||
onSubmitted: (value) => Navigator.pop(context, value),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null), // Cancel
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, passwordController.text),
|
||||
child: const Text("Enter"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// If cancelled or empty
|
||||
if (inputPassword == null || inputPassword.isEmpty) {
|
||||
if (mounted) context.go('/dashboard'); // Kick out
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify against Backend
|
||||
setState(() => _isLoading = true);
|
||||
final isValid = await AuthProxyService.checkAdminAuth(inputPassword);
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
if (isValid) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isAuthorized = true;
|
||||
_verifiedAdminPassword = inputPassword;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid Password. Access Denied.'), backgroundColor: Colors.red),
|
||||
);
|
||||
context.go('/dashboard'); // Kick out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_loginIdController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (_verifiedAdminPassword == null) return; // Should not happen
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
await AuthProxyService.createUser(
|
||||
loginId: _loginIdController.text.trim(),
|
||||
adminPassword: _verifiedAdminPassword!,
|
||||
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
phone: _phoneController.text.trim().isEmpty ? null : _phoneController.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),
|
||||
);
|
||||
_formKey.currentState!.reset();
|
||||
_loginIdController.clear();
|
||||
_emailController.clear();
|
||||
_phoneController.clear();
|
||||
_nameController.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Hide content until authorized
|
||||
if (!_isAuthorized) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Create User'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/dashboard'),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
"Create New User",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
TextFormField(
|
||||
controller: _loginIdController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Login ID (Required)",
|
||||
border: OutlineInputBorder(),
|
||||
helperText: "Unique identifier (Email or Phone)",
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty ? 'Please enter Login ID' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Display Name",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Phone Number",
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
helperText: "Start with 010 (e.g., 010-1234-5678)",
|
||||
),
|
||||
),
|
||||
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)
|
||||
: const Text("Create User"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
import 'features/auth/presentation/login_screen.dart';
|
||||
import 'features/dashboard/presentation/dashboard_screen.dart';
|
||||
import 'features/admin/presentation/create_user_screen.dart';
|
||||
import 'core/services/auth_proxy_service.dart';
|
||||
import 'core/services/logger_service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -85,12 +86,19 @@ final _router = GoRouter(
|
||||
return const DashboardScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/admin/users',
|
||||
builder: (context, state) {
|
||||
_routerLogger.info("Navigating to /admin/users");
|
||||
return const CreateUserScreen();
|
||||
},
|
||||
),
|
||||
],
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn =
|
||||
Descope.sessionManager.session?.refreshToken.isExpired == false;
|
||||
final path = state.uri.path;
|
||||
final isLoggingIn = path == '/' || path.startsWith('/verify/');
|
||||
final isLoggingIn = path == '/' || path.startsWith('/verify/') || path.startsWith('/admin/');
|
||||
|
||||
_routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user