移除注册功能,管理员创建用户并新增创建用户弹窗
This commit is contained in:
@@ -54,7 +54,6 @@ open http://localhost:3018
|
||||
|
||||
| 方法 | 路径 | 说明 | 权限 |
|
||||
|---|---|---|---|
|
||||
| POST | /api/auth/register | 注册(默认 user) | 公开 |
|
||||
| POST | /api/auth/login | 登录 | 公开 |
|
||||
| GET | /api/auth/me | 当前用户 | 需登录 |
|
||||
| POST | /api/auth/logout | 登出 | 需登录 |
|
||||
|
||||
@@ -19,80 +19,6 @@ type AuthHandler struct {
|
||||
CFG *config.Config
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email *string `json:"email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateUsername(req.Username); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "密码长度至少 6 位"})
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.ToLower(strings.TrimSpace(req.Username))
|
||||
|
||||
var role models.Role
|
||||
if err := h.DB.Where("name = ?", models.RoleUser).First(&role).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := hashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
var email *string
|
||||
if req.Email != nil && *req.Email != "" {
|
||||
e := strings.ToLower(strings.TrimSpace(*req.Email))
|
||||
email = &e
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
PasswordHash: hash,
|
||||
RoleID: role.ID,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") {
|
||||
c.JSON(http.StatusConflict, gin.H{"success": false, "error": "用户名或邮箱已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.DB.Preload("Role").First(&user, "id = ?", user.ID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateToken(user.ID, models.RoleUser, h.CFG)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"user": user.ToPublicInfo(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
@@ -20,7 +20,6 @@ type CurrentUser struct {
|
||||
}
|
||||
|
||||
var publicPaths = map[string]struct{}{
|
||||
"/api/auth/register": {},
|
||||
"/api/auth/login": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
|
||||
|
||||
r.Use(middleware.AuthMiddleware(cfg, db))
|
||||
|
||||
// 公开接口(仅登录/注册)
|
||||
r.POST("/api/auth/register", authHandler.Register)
|
||||
// 公开接口(仅登录)
|
||||
r.POST("/api/auth/login", authHandler.Login)
|
||||
|
||||
// 受保护接口
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useState } from 'react'
|
||||
import { X, UserPlus, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { RoleName, roleLabel } from '@/lib/auth'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
interface CreateUserDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
allowedRoles: RoleName[]
|
||||
}
|
||||
|
||||
export function CreateUserDialog({ open, onClose, onSuccess, allowedRoles }: CreateUserDialogProps) {
|
||||
const [form, setForm] = useState({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
role: (allowedRoles[0] || 'user') as RoleName,
|
||||
})
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!form.username.trim()) {
|
||||
setError('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (form.password.length < 6) {
|
||||
setError('密码长度至少 6 位')
|
||||
return
|
||||
}
|
||||
if (form.password !== form.confirmPassword) {
|
||||
setError('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
if (!allowedRoles.includes(form.role as RoleName)) {
|
||||
setError('没有权限分配该角色')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await api.createUser({
|
||||
username: form.username.trim(),
|
||||
email: form.email.trim() || undefined,
|
||||
password: form.password,
|
||||
role: form.role,
|
||||
})
|
||||
setForm({ username: '', email: '', password: '', confirmPassword: '', role: (allowedRoles[0] || 'user') as RoleName })
|
||||
onSuccess()
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '创建失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative w-full max-w-md rounded-card border border-border bg-surface p-6 shadow-xl">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-btn bg-accent/10 text-accent">
|
||||
<UserPlus className="h-4 w-4" />
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-foreground">创建用户</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-btn text-secondary hover:bg-elevated hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-btn border border-danger/30 bg-danger/10 px-3 py-2.5 text-xs text-danger mb-4">
|
||||
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-secondary mb-1">用户名</label>
|
||||
<input
|
||||
required
|
||||
placeholder="用户名"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
className="w-full rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-secondary mb-1">邮箱(可选)</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="邮箱"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="w-full rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-secondary mb-1">密码</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="w-full rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-secondary mb-1">确认密码</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
placeholder="确认密码"
|
||||
value={form.confirmPassword}
|
||||
onChange={(e) => setForm({ ...form, confirmPassword: e.target.value })}
|
||||
className="w-full rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs text-secondary mb-1">角色</label>
|
||||
<select
|
||||
value={form.role}
|
||||
onChange={(e) => setForm({ ...form, role: e.target.value as RoleName })}
|
||||
className="w-full rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
>
|
||||
{allowedRoles.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{roleLabel(r)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-btn text-sm text-secondary hover:bg-elevated hover:text-foreground transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 px-4 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 transition-colors',
|
||||
loading && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Shield, Lock, AlertCircle, Loader2, UserPlus, LogIn } from 'lucide-react'
|
||||
import { Shield, Lock, AlertCircle, Loader2, LogIn } from 'lucide-react'
|
||||
import { api, setToken } from '@/lib/api'
|
||||
import { setStoredUser } from '@/lib/auth'
|
||||
|
||||
@@ -8,9 +8,7 @@ interface LoginFormProps {
|
||||
}
|
||||
|
||||
export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login')
|
||||
const [username, setUsername] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -22,25 +20,14 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
setError('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
if (mode === 'register' && password.length < 6) {
|
||||
setError('密码长度至少 6 位')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const res =
|
||||
mode === 'login'
|
||||
? await api.login({ username: username.trim(), password })
|
||||
: await api.register({
|
||||
username: username.trim(),
|
||||
email: email.trim() || undefined,
|
||||
password,
|
||||
})
|
||||
const res = await api.login({ username: username.trim(), password })
|
||||
setToken(res.token)
|
||||
setStoredUser(res.user)
|
||||
onSuccess()
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '操作失败,请稍后重试')
|
||||
setError(err?.message || '登录失败,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -61,13 +48,9 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
>
|
||||
<Shield className="h-8 w-8" style={{ color: '#8B5CF6' }} />
|
||||
</div>
|
||||
<h1 className="mt-5 text-2xl font-bold text-foreground tracking-tight">
|
||||
{mode === 'login' ? '登录账号' : '注册账号'}
|
||||
</h1>
|
||||
<h1 className="mt-5 text-2xl font-bold text-foreground tracking-tight">登录账号</h1>
|
||||
<p className="mt-2 text-sm text-secondary leading-relaxed">
|
||||
{mode === 'login'
|
||||
? '请输入用户名和密码进入系统。'
|
||||
: '注册后默认获得用户权限。'}
|
||||
请输入用户名和密码进入系统。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -86,29 +69,13 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mode === 'register' && (
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted">
|
||||
<Shield className="h-4 w-4" />
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="邮箱(可选)"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2.5 rounded-input bg-base border border-border text-sm focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-all"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted">
|
||||
<Lock className="h-4 w-4" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
|
||||
autoComplete="current-password"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
@@ -130,26 +97,15 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : mode === 'login' ? (
|
||||
<LogIn className="h-4 w-4" />
|
||||
) : (
|
||||
<UserPlus className="h-4 w-4" />
|
||||
<LogIn className="h-4 w-4" />
|
||||
)}
|
||||
{loading ? '处理中…' : mode === 'login' ? '登录' : '注册'}
|
||||
{loading ? '处理中…' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-5 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'login' ? 'register' : 'login')
|
||||
setError('')
|
||||
}}
|
||||
className="text-xs text-secondary hover:text-accent transition-colors"
|
||||
>
|
||||
{mode === 'login' ? '没有账号?立即注册' : '已有账号?直接登录'}
|
||||
</button>
|
||||
<span className="text-xs text-secondary">没有账号?请联系管理员创建</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -79,12 +79,6 @@ async function request<T>(
|
||||
}
|
||||
|
||||
export const api = {
|
||||
register: (body: { username: string; email?: string; password: string }) =>
|
||||
request<AuthResponse>('/api/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
login: (body: { username: string; password: string }) =>
|
||||
request<AuthResponse>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Loader2, Plus, Trash2, Users as UsersIcon, AlertCircle } from 'lucide-react'
|
||||
import { api, Role, UserInfo } from '@/lib/api'
|
||||
import { canDeleteUsers, roleLabel } from '@/lib/auth'
|
||||
import { Plus, Trash2, Users as UsersIcon, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { api, UserInfo } from '@/lib/api'
|
||||
import { canDeleteUsers, getStoredUser, roleLabel, RoleName } from '@/lib/auth'
|
||||
import { CreateUserDialog } from '@/components/CreateUserDialog'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
const creatableRoles: Record<RoleName, RoleName[]> = {
|
||||
system_admin: ['admin', 'user'],
|
||||
admin: ['user'],
|
||||
user: [],
|
||||
}
|
||||
|
||||
export function Users() {
|
||||
const [users, setUsers] = useState<UserInfo[]>([])
|
||||
const [roles, setRoles] = useState<Role[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [formOpen, setFormOpen] = useState(false)
|
||||
const [form, setForm] = useState({ username: '', email: '', password: '', role: 'user' })
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const currentUser = getStoredUser()
|
||||
const allowedRoles = currentUser ? creatableRoles[currentUser.role] : []
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [u, r] = await Promise.all([api.listUsers(), api.listRoles()])
|
||||
const u = await api.listUsers()
|
||||
setUsers(u)
|
||||
setRoles(r)
|
||||
setError('')
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '加载失败')
|
||||
@@ -30,23 +37,6 @@ export function Users() {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
await api.createUser({
|
||||
username: form.username,
|
||||
email: form.email || undefined,
|
||||
password: form.password,
|
||||
role: form.role,
|
||||
})
|
||||
setForm({ username: '', email: '', password: '', role: 'user' })
|
||||
setFormOpen(false)
|
||||
fetchData()
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('确定删除该用户?')) return
|
||||
try {
|
||||
@@ -66,7 +56,7 @@ export function Users() {
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = users.find((u) => u.username === JSON.parse(localStorage.getItem('user') || '{}')?.username)
|
||||
const current = users.find((u) => u.username === currentUser?.username)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -78,7 +68,7 @@ export function Users() {
|
||||
<h1 className="text-xl font-bold text-foreground">用户管理</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFormOpen(!formOpen)}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -93,51 +83,6 @@ export function Users() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formOpen && (
|
||||
<form
|
||||
onSubmit={handleCreate}
|
||||
className="rounded-card border border-border bg-surface p-4 grid grid-cols-1 md:grid-cols-5 gap-3"
|
||||
>
|
||||
<input
|
||||
required
|
||||
placeholder="用户名"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="邮箱"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<select
|
||||
value={form.role}
|
||||
onChange={(e) => setForm({ ...form, role: e.target.value })}
|
||||
className="rounded-input bg-base border border-border px-3 py-2 text-sm focus:outline-none focus:border-accent"
|
||||
>
|
||||
{roles.map((r) => (
|
||||
<option key={r.id} value={r.name}>{roleLabel(r.name)}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="md:col-span-5 inline-flex items-center justify-center gap-2 px-4 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="rounded-card border border-border bg-surface overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-elevated/50 text-secondary">
|
||||
@@ -173,7 +118,7 @@ export function Users() {
|
||||
<select
|
||||
value={u.status}
|
||||
onChange={(e) => handleStatusChange(u, e.target.value)}
|
||||
disabled={u.id === currentUser?.id}
|
||||
disabled={u.id === current?.id}
|
||||
className="bg-base border border-border rounded-input px-2 py-1 text-xs disabled:opacity-50"
|
||||
>
|
||||
<option value="active">启用</option>
|
||||
@@ -184,7 +129,7 @@ export function Users() {
|
||||
{new Date(u.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{canDeleteUsers(currentUser?.role || 'user') && u.id !== currentUser?.id && (
|
||||
{canDeleteUsers(current?.role || 'user') && u.id !== current?.id && (
|
||||
<button
|
||||
onClick={() => handleDelete(u.id)}
|
||||
className="p-1.5 rounded-btn text-danger hover:bg-danger/10 transition-colors"
|
||||
@@ -199,6 +144,16 @@ export function Users() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<CreateUserDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onSuccess={() => {
|
||||
setDialogOpen(false)
|
||||
fetchData()
|
||||
}}
|
||||
allowedRoles={allowedRoles}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user