forked from baron/baron-sso
admin page add
This commit is contained in:
@@ -61,6 +61,7 @@ func main() {
|
|||||||
// 2. Initialize Handlers
|
// 2. Initialize Handlers
|
||||||
auditHandler := handler.NewAuditHandler(auditRepo)
|
auditHandler := handler.NewAuditHandler(auditRepo)
|
||||||
authHandler := handler.NewAuthHandler()
|
authHandler := handler.NewAuthHandler()
|
||||||
|
adminHandler := handler.NewAdminHandler()
|
||||||
|
|
||||||
// 3. Initialize Fiber
|
// 3. Initialize Fiber
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
@@ -132,6 +133,11 @@ func main() {
|
|||||||
auth.Post("/sms", authHandler.SendSms)
|
auth.Post("/sms", authHandler.SendSms)
|
||||||
auth.Post("/verify-sms", authHandler.VerifySms)
|
auth.Post("/verify-sms", authHandler.VerifySms)
|
||||||
|
|
||||||
|
// Admin Routes
|
||||||
|
admin := api.Group("/admin")
|
||||||
|
admin.Post("/users", adminHandler.CreateUser)
|
||||||
|
admin.Get("/check", adminHandler.CheckAuth)
|
||||||
|
|
||||||
// Webhook for Descope Generic SMS Gateway
|
// Webhook for Descope Generic SMS Gateway
|
||||||
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
|
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
|
||||||
|
|
||||||
|
|||||||
144
backend/internal/handler/admin_handler.go
Normal file
144
backend/internal/handler/admin_handler.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/descope/go-sdk/descope"
|
||||||
|
"github.com/descope/go-sdk/descope/client"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminHandler struct {
|
||||||
|
DescopeClient *client.DescopeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminHandler() *AdminHandler {
|
||||||
|
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
||||||
|
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
||||||
|
|
||||||
|
var descopeClient *client.DescopeClient
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if projectID != "" && managementKey != "" {
|
||||||
|
descopeClient, err = client.NewWithConfig(&client.Config{
|
||||||
|
ProjectID: projectID,
|
||||||
|
ManagementKey: managementKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to initialize Descope Client for Admin: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println("Warning: DESCOPE_PROJECT_ID or DESCOPE_MANAGEMENT_KEY missing. Admin functions will fail.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AdminHandler{
|
||||||
|
DescopeClient: descopeClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserRequest struct {
|
||||||
|
LoginID string `json:"loginId"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
Tenants map[string][]string `json:"tenants"` // tenantId -> roles
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) CreateUser(c *fiber.Ctx) error {
|
||||||
|
// 1. Simple Password Check
|
||||||
|
adminPass := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
if adminPass == "" {
|
||||||
|
adminPass = "admin" // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPass := c.Get("X-Admin-Password")
|
||||||
|
if reqPass != adminPass {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.DescopeClient == nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateUserRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.LoginID == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "LoginID is required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Phone
|
||||||
|
normalizedPhone := req.Phone
|
||||||
|
if normalizedPhone != "" {
|
||||||
|
if strings.HasPrefix(normalizedPhone, "010") {
|
||||||
|
normalizedPhone = "+82" + normalizedPhone[1:]
|
||||||
|
} else if strings.HasPrefix(normalizedPhone, "82") {
|
||||||
|
normalizedPhone = "+" + normalizedPhone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userObj := &descope.UserRequest{
|
||||||
|
User: descope.User{
|
||||||
|
Email: req.Email,
|
||||||
|
Phone: normalizedPhone,
|
||||||
|
Name: req.DisplayName,
|
||||||
|
},
|
||||||
|
VerifiedEmail: boolPtr(req.Email != ""),
|
||||||
|
VerifiedPhone: boolPtr(normalizedPhone != ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Roles if provided
|
||||||
|
if len(req.Roles) > 0 {
|
||||||
|
userObj.Roles = req.Roles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Tenants if provided
|
||||||
|
if len(req.Tenants) > 0 {
|
||||||
|
// Convert map[string][]string to []*descope.AssociatedTenant
|
||||||
|
userTenants := []*descope.AssociatedTenant{}
|
||||||
|
for tenantID, roles := range req.Tenants {
|
||||||
|
userTenants = append(userTenants, &descope.AssociatedTenant{
|
||||||
|
TenantID: tenantID,
|
||||||
|
Roles: roles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
userObj.Tenants = userTenants
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Admin] Creating user: %s (Email: %s, Phone: %s)", req.LoginID, req.Email, normalizedPhone)
|
||||||
|
|
||||||
|
res, err := h.DescopeClient.Management.User().Create(context.Background(), req.LoginID, userObj)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Admin] Failed to create user: %v", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"message": "User created successfully",
|
||||||
|
"user": res,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
||||||
|
adminPass := os.Getenv("ADMIN_PASSWORD")
|
||||||
|
if adminPass == "" {
|
||||||
|
adminPass = "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPass := c.Get("X-Admin-Password")
|
||||||
|
if reqPass != adminPass {
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
|
||||||
|
}
|
||||||
@@ -222,30 +222,64 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[Verify] Generating embedded link for %s", loginID)
|
// [Fix] Search for existing user by phone to prevent fragmentation
|
||||||
embeddedToken, err := h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), loginID, nil, 0)
|
// Normalize Phone Number for Search (E.164)
|
||||||
|
searchPhone := loginID
|
||||||
|
if !strings.Contains(searchPhone, "@") {
|
||||||
|
// If it looks like a KR mobile number (010...), format to +8210...
|
||||||
|
if strings.HasPrefix(searchPhone, "010") {
|
||||||
|
searchPhone = "+82" + searchPhone[1:]
|
||||||
|
} else if strings.HasPrefix(searchPhone, "82") {
|
||||||
|
searchPhone = "+" + searchPhone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Verify] Searching for user with phone: %s", searchPhone)
|
||||||
|
searchOptions := &descope.UserSearchOptions{
|
||||||
|
Phones: []string{searchPhone},
|
||||||
|
Limit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetLoginID string
|
||||||
|
users, _, errSearch := h.DescopeClient.Management.User().SearchAll(context.Background(), searchOptions)
|
||||||
|
|
||||||
|
if errSearch == nil && len(users) > 0 {
|
||||||
|
if len(users[0].LoginIDs) > 0 {
|
||||||
|
targetLoginID = users[0].LoginIDs[0]
|
||||||
|
log.Printf("[Verify] User found! Existing LoginID: %s", targetLoginID)
|
||||||
|
} else {
|
||||||
|
// Should not happen for a valid user, but fallback to UserID or searchPhone
|
||||||
|
log.Printf("[Verify] User found but no LoginIDs. Using UserID.")
|
||||||
|
targetLoginID = users[0].UserID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not found, or search error. Fallback to using the phone as LoginID.
|
||||||
|
// Use the normalized phone number to ensure consistency (+82...)
|
||||||
|
targetLoginID = searchPhone
|
||||||
|
log.Printf("[Verify] User not found by phone. Will use/create: %s", targetLoginID)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Verify] Generating embedded link for %s", targetLoginID)
|
||||||
|
embeddedToken, err := h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), targetLoginID, nil, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "User not found") || strings.Contains(err.Error(), "E062108") {
|
if strings.Contains(err.Error(), "User not found") || strings.Contains(err.Error(), "E062108") {
|
||||||
log.Printf("[Verify] User %s not found. Creating...", loginID)
|
log.Printf("[Verify] User %s not found. Creating...", targetLoginID)
|
||||||
|
|
||||||
descopeLoginID := loginID
|
// Create User with Explicit Phone Attribute
|
||||||
userObj := &descope.UserRequest{}
|
userObj := &descope.UserRequest{}
|
||||||
if strings.Contains(loginID, "@") {
|
if strings.Contains(targetLoginID, "@") {
|
||||||
userObj.Email = loginID
|
userObj.Email = targetLoginID
|
||||||
} else {
|
} else {
|
||||||
if strings.HasPrefix(loginID, "010") {
|
userObj.Phone = targetLoginID // Must be E.164
|
||||||
descopeLoginID = "+82" + loginID[1:]
|
|
||||||
}
|
|
||||||
userObj.Phone = descopeLoginID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, errCreate := h.DescopeClient.Management.User().Create(context.Background(), descopeLoginID, userObj)
|
_, errCreate := h.DescopeClient.Management.User().Create(context.Background(), targetLoginID, userObj)
|
||||||
if errCreate != nil {
|
if errCreate != nil {
|
||||||
log.Printf("[Verify] Failed to create user: %v", errCreate)
|
log.Printf("[Verify] Failed to create user: %v", errCreate)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create new user"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create new user"})
|
||||||
}
|
}
|
||||||
|
|
||||||
embeddedToken, err = h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), descopeLoginID, nil, 0)
|
embeddedToken, err = h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), targetLoginID, nil, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Verify] Failed to generate token after creation: %v", err)
|
log.Printf("[Verify] Failed to generate token after creation: %v", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate upstream token"})
|
||||||
|
|||||||
@@ -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 {
|
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/client-log');
|
final url = Uri.parse('$_baseUrl/api/v1/client-log');
|
||||||
try {
|
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 'package:flutter_web_plugins/url_strategy.dart';
|
||||||
import 'features/auth/presentation/login_screen.dart';
|
import 'features/auth/presentation/login_screen.dart';
|
||||||
import 'features/dashboard/presentation/dashboard_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/auth_proxy_service.dart';
|
||||||
import 'core/services/logger_service.dart';
|
import 'core/services/logger_service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -85,12 +86,19 @@ final _router = GoRouter(
|
|||||||
return const DashboardScreen();
|
return const DashboardScreen();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/admin/users',
|
||||||
|
builder: (context, state) {
|
||||||
|
_routerLogger.info("Navigating to /admin/users");
|
||||||
|
return const CreateUserScreen();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
final isLoggedIn =
|
final isLoggedIn =
|
||||||
Descope.sessionManager.session?.refreshToken.isExpired == false;
|
Descope.sessionManager.session?.refreshToken.isExpired == false;
|
||||||
final path = state.uri.path;
|
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");
|
_routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user