移除注册功能,管理员创建用户并新增创建用户弹窗

This commit is contained in:
2026-07-04 10:00:53 +08:00
parent 5a50af66bf
commit acd335c435
8 changed files with 218 additions and 212 deletions
-1
View File
@@ -54,7 +54,6 @@ open http://localhost:3018
| 方法 | 路径 | 说明 | 权限 | | 方法 | 路径 | 说明 | 权限 |
|---|---|---|---| |---|---|---|---|
| POST | /api/auth/register | 注册(默认 user | 公开 |
| POST | /api/auth/login | 登录 | 公开 | | POST | /api/auth/login | 登录 | 公开 |
| GET | /api/auth/me | 当前用户 | 需登录 | | GET | /api/auth/me | 当前用户 | 需登录 |
| POST | /api/auth/logout | 登出 | 需登录 | | POST | /api/auth/logout | 登出 | 需登录 |
-74
View File
@@ -19,80 +19,6 @@ type AuthHandler struct {
CFG *config.Config 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) { func (h *AuthHandler) Login(c *gin.Context) {
var req struct { var req struct {
Username string `json:"username" binding:"required"` Username string `json:"username" binding:"required"`
-1
View File
@@ -20,7 +20,6 @@ type CurrentUser struct {
} }
var publicPaths = map[string]struct{}{ var publicPaths = map[string]struct{}{
"/api/auth/register": {},
"/api/auth/login": {}, "/api/auth/login": {},
} }
+1 -2
View File
@@ -30,8 +30,7 @@ func Setup(cfg *config.Config, db *gorm.DB) *gin.Engine {
r.Use(middleware.AuthMiddleware(cfg, db)) r.Use(middleware.AuthMiddleware(cfg, db))
// 公开接口(仅登录/注册 // 公开接口(仅登录)
r.POST("/api/auth/register", authHandler.Register)
r.POST("/api/auth/login", authHandler.Login) 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>
)
}
+9 -53
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' 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 { api, setToken } from '@/lib/api'
import { setStoredUser } from '@/lib/auth' import { setStoredUser } from '@/lib/auth'
@@ -8,9 +8,7 @@ interface LoginFormProps {
} }
export function LoginForm({ onSuccess }: LoginFormProps) { export function LoginForm({ onSuccess }: LoginFormProps) {
const [mode, setMode] = useState<'login' | 'register'>('login')
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -22,25 +20,14 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
setError('请输入用户名和密码') setError('请输入用户名和密码')
return return
} }
if (mode === 'register' && password.length < 6) {
setError('密码长度至少 6 位')
return
}
setLoading(true) setLoading(true)
try { try {
const res = const res = await api.login({ username: username.trim(), password })
mode === 'login'
? await api.login({ username: username.trim(), password })
: await api.register({
username: username.trim(),
email: email.trim() || undefined,
password,
})
setToken(res.token) setToken(res.token)
setStoredUser(res.user) setStoredUser(res.user)
onSuccess() onSuccess()
} catch (err: any) { } catch (err: any) {
setError(err?.message || '操作失败,请稍后重试') setError(err?.message || '登录失败,请稍后重试')
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -61,13 +48,9 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
> >
<Shield className="h-8 w-8" style={{ color: '#8B5CF6' }} /> <Shield className="h-8 w-8" style={{ color: '#8B5CF6' }} />
</div> </div>
<h1 className="mt-5 text-2xl font-bold text-foreground tracking-tight"> <h1 className="mt-5 text-2xl font-bold text-foreground tracking-tight"></h1>
{mode === 'login' ? '登录账号' : '注册账号'}
</h1>
<p className="mt-2 text-sm text-secondary leading-relaxed"> <p className="mt-2 text-sm text-secondary leading-relaxed">
{mode === 'login'
? '请输入用户名和密码进入系统。'
: '注册后默认获得用户权限。'}
</p> </p>
</div> </div>
@@ -86,29 +69,13 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
/> />
</div> </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="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted"> <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted">
<Lock className="h-4 w-4" /> <Lock className="h-4 w-4" />
</div> </div>
<input <input
type="password" type="password"
autoComplete={mode === 'login' ? 'current-password' : 'new-password'} autoComplete="current-password"
placeholder="密码" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
@@ -130,26 +97,15 @@ export function LoginForm({ onSuccess }: LoginFormProps) {
> >
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <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> </button>
</form> </form>
<div className="mt-5 text-center"> <div className="mt-5 text-center">
<button <span className="text-xs text-secondary"></span>
type="button"
onClick={() => {
setMode(mode === 'login' ? 'register' : 'login')
setError('')
}}
className="text-xs text-secondary hover:text-accent transition-colors"
>
{mode === 'login' ? '没有账号?立即注册' : '已有账号?直接登录'}
</button>
</div> </div>
</div> </div>
</motion.div> </motion.div>
-6
View File
@@ -79,12 +79,6 @@ async function request<T>(
} }
export const api = { 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 }) => login: (body: { username: string; password: string }) =>
request<AuthResponse>('/api/auth/login', { request<AuthResponse>('/api/auth/login', {
method: 'POST', method: 'POST',
+29 -74
View File
@@ -1,23 +1,30 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Loader2, Plus, Trash2, Users as UsersIcon, AlertCircle } from 'lucide-react' import { Plus, Trash2, Users as UsersIcon, AlertCircle, Loader2 } from 'lucide-react'
import { api, Role, UserInfo } from '@/lib/api' import { api, UserInfo } from '@/lib/api'
import { canDeleteUsers, roleLabel } from '@/lib/auth' import { canDeleteUsers, getStoredUser, roleLabel, RoleName } from '@/lib/auth'
import { CreateUserDialog } from '@/components/CreateUserDialog'
import { cn } from '@/lib/cn' import { cn } from '@/lib/cn'
const creatableRoles: Record<RoleName, RoleName[]> = {
system_admin: ['admin', 'user'],
admin: ['user'],
user: [],
}
export function Users() { export function Users() {
const [users, setUsers] = useState<UserInfo[]>([]) const [users, setUsers] = useState<UserInfo[]>([])
const [roles, setRoles] = useState<Role[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const [formOpen, setFormOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [form, setForm] = useState({ username: '', email: '', password: '', role: 'user' })
const currentUser = getStoredUser()
const allowedRoles = currentUser ? creatableRoles[currentUser.role] : []
const fetchData = async () => { const fetchData = async () => {
setLoading(true) setLoading(true)
try { try {
const [u, r] = await Promise.all([api.listUsers(), api.listRoles()]) const u = await api.listUsers()
setUsers(u) setUsers(u)
setRoles(r)
setError('') setError('')
} catch (err: any) { } catch (err: any) {
setError(err?.message || '加载失败') setError(err?.message || '加载失败')
@@ -30,23 +37,6 @@ export function Users() {
fetchData() 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) => { const handleDelete = async (id: string) => {
if (!confirm('确定删除该用户?')) return if (!confirm('确定删除该用户?')) return
try { 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -78,7 +68,7 @@ export function Users() {
<h1 className="text-xl font-bold text-foreground"></h1> <h1 className="text-xl font-bold text-foreground"></h1>
</div> </div>
<button <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" 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" /> <Plus className="h-4 w-4" />
@@ -93,51 +83,6 @@ export function Users() {
</div> </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"> <div className="rounded-card border border-border bg-surface overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-elevated/50 text-secondary"> <thead className="bg-elevated/50 text-secondary">
@@ -173,7 +118,7 @@ export function Users() {
<select <select
value={u.status} value={u.status}
onChange={(e) => handleStatusChange(u, e.target.value)} 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" className="bg-base border border-border rounded-input px-2 py-1 text-xs disabled:opacity-50"
> >
<option value="active"></option> <option value="active"></option>
@@ -184,7 +129,7 @@ export function Users() {
{new Date(u.created_at).toLocaleString()} {new Date(u.created_at).toLocaleString()}
</td> </td>
<td className="px-4 py-3 text-right"> <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 <button
onClick={() => handleDelete(u.id)} onClick={() => handleDelete(u.id)}
className="p-1.5 rounded-btn text-danger hover:bg-danger/10 transition-colors" className="p-1.5 rounded-btn text-danger hover:bg-danger/10 transition-colors"
@@ -199,6 +144,16 @@ export function Users() {
</tbody> </tbody>
</table> </table>
</div> </div>
<CreateUserDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onSuccess={() => {
setDialogOpen(false)
fetchData()
}}
allowedRoles={allowedRoles}
/>
</div> </div>
) )
} }