mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
updateauth
This commit is contained in:
116
AUTH_SYSTEM.md
Normal file
116
AUTH_SYSTEM.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 用户认证系统
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已成功集成了完整的用户认证系统,包括用户注册、登录、权限管理等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 用户管理
|
||||
- **用户注册**: 支持新用户注册
|
||||
- **用户登录**: JWT令牌认证
|
||||
- **用户管理**: 管理员可以创建、编辑、删除用户
|
||||
- **角色管理**: 支持用户(user)和管理员(admin)角色
|
||||
- **状态管理**: 支持用户激活/禁用状态
|
||||
|
||||
### 2. 认证机制
|
||||
- **JWT令牌**: 使用JWT进行身份验证
|
||||
- **密码加密**: 使用bcrypt进行密码哈希
|
||||
- **中间件保护**: 路由级别的权限控制
|
||||
|
||||
### 3. 权限控制
|
||||
- **公开接口**: 资源查看、搜索等
|
||||
- **用户接口**: 个人资料查看
|
||||
- **管理员接口**: 所有管理功能需要管理员权限
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### users表
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
role VARCHAR(20) DEFAULT 'user',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
last_login TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### 认证接口
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `POST /api/auth/register` - 用户注册
|
||||
- `GET /api/auth/profile` - 获取用户信息
|
||||
|
||||
### 用户管理接口(管理员)
|
||||
- `GET /api/users` - 获取用户列表
|
||||
- `POST /api/users` - 创建用户
|
||||
- `PUT /api/users/:id` - 更新用户
|
||||
- `DELETE /api/users/:id` - 删除用户
|
||||
|
||||
## 前端页面
|
||||
|
||||
### 登录页面 (`/login`)
|
||||
- 用户名/密码登录
|
||||
- 默认管理员账户: admin / password
|
||||
- 登录成功后跳转到管理页面
|
||||
|
||||
### 注册页面 (`/register`)
|
||||
- 新用户注册
|
||||
- 注册成功后跳转到登录页面
|
||||
|
||||
### 管理页面 (`/admin`)
|
||||
- 需要登录才能访问
|
||||
- 显示用户信息和退出登录按钮
|
||||
- 各种管理功能的入口
|
||||
|
||||
### 用户管理页面 (`/users`)
|
||||
- 仅管理员可访问
|
||||
- 用户列表展示
|
||||
- 创建、编辑、删除用户功能
|
||||
|
||||
## 中间件
|
||||
|
||||
### AuthMiddleware
|
||||
- 验证JWT令牌
|
||||
- 将用户信息存储到上下文中
|
||||
|
||||
### AdminMiddleware
|
||||
- 检查用户角色是否为管理员
|
||||
- 保护管理员专用接口
|
||||
|
||||
## 默认数据
|
||||
|
||||
系统启动时会自动创建默认管理员账户:
|
||||
- 用户名: admin
|
||||
- 密码: password
|
||||
- 角色: admin
|
||||
- 邮箱: admin@example.com
|
||||
|
||||
## 安全特性
|
||||
|
||||
1. **密码加密**: 使用bcrypt进行密码哈希
|
||||
2. **JWT令牌**: 24小时有效期的JWT令牌
|
||||
3. **角色权限**: 基于角色的访问控制
|
||||
4. **输入验证**: 服务器端数据验证
|
||||
5. **SQL注入防护**: 使用GORM进行参数化查询
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 启动服务器后,访问 `/login` 页面
|
||||
2. 使用默认管理员账户登录: admin / password
|
||||
3. 登录成功后可以访问管理功能
|
||||
4. 在用户管理页面可以创建新用户或修改现有用户
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Go + Gin + GORM + JWT
|
||||
- **前端**: Nuxt.js + Tailwind CSS
|
||||
- **数据库**: PostgreSQL
|
||||
- **认证**: JWT + bcrypt
|
||||
@@ -76,6 +76,7 @@ func autoMigrate() error {
|
||||
&entity.Resource{},
|
||||
&entity.ResourceTag{},
|
||||
&entity.ReadyResource{},
|
||||
&entity.User{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,5 +98,18 @@ func insertDefaultData() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 插入默认管理员用户
|
||||
defaultAdmin := entity.User{
|
||||
Username: "admin",
|
||||
Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // password
|
||||
Email: "admin@example.com",
|
||||
Role: "admin",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := DB.Where("username = ?", defaultAdmin.Username).FirstOrCreate(&defaultAdmin).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
29
db/converter/user_converter.go
Normal file
29
db/converter/user_converter.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"res_db/db/dto"
|
||||
"res_db/db/entity"
|
||||
)
|
||||
|
||||
// ToUserResponse 将User实体转换为UserResponse
|
||||
func ToUserResponse(user *entity.User) dto.UserResponse {
|
||||
return dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
LastLogin: user.LastLogin,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToUserResponseList 将User实体列表转换为UserResponse列表
|
||||
func ToUserResponseList(users []entity.User) []dto.UserResponse {
|
||||
responses := make([]dto.UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
responses[i] = ToUserResponse(&user)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
51
db/dto/user.go
Normal file
51
db/dto/user.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// RegisterRequest 注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
// CreateUserRequest 创建用户请求
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Role string `json:"role"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UserResponse 用户响应
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
IsActive bool `json:"is_active"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
26
db/entity/user.go
Normal file
26
db/entity/user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username" gorm:"size:50;not null;unique;comment:用户名"`
|
||||
Password string `json:"-" gorm:"size:255;not null;comment:密码"`
|
||||
Email string `json:"email" gorm:"size:100;comment:邮箱"`
|
||||
Role string `json:"role" gorm:"size:20;default:'user';comment:角色"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true;comment:是否激活"`
|
||||
LastLogin *time.Time `json:"last_login" gorm:"comment:最后登录时间"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type RepositoryManager struct {
|
||||
CategoryRepository CategoryRepository
|
||||
TagRepository TagRepository
|
||||
ReadyResourceRepository ReadyResourceRepository
|
||||
UserRepository UserRepository
|
||||
}
|
||||
|
||||
// NewRepositoryManager 创建Repository管理器
|
||||
@@ -23,5 +24,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
||||
CategoryRepository: NewCategoryRepository(db),
|
||||
TagRepository: NewTagRepository(db),
|
||||
ReadyResourceRepository: NewReadyResourceRepository(db),
|
||||
UserRepository: NewUserRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
61
db/repo/user_repository.go
Normal file
61
db/repo/user_repository.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"res_db/db/entity"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRepository 用户Repository接口
|
||||
type UserRepository interface {
|
||||
BaseRepository[entity.User]
|
||||
FindByUsername(username string) (*entity.User, error)
|
||||
FindByEmail(email string) (*entity.User, error)
|
||||
UpdateLastLogin(id uint) error
|
||||
FindByRole(role string) ([]entity.User, error)
|
||||
}
|
||||
|
||||
// UserRepositoryImpl 用户Repository实现
|
||||
type UserRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.User]
|
||||
}
|
||||
|
||||
// NewUserRepository 创建用户Repository
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &UserRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.User]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// FindByUsername 根据用户名查找
|
||||
func (r *UserRepositoryImpl) FindByUsername(username string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByEmail 根据邮箱查找
|
||||
func (r *UserRepositoryImpl) FindByEmail(email string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateLastLogin 更新最后登录时间
|
||||
func (r *UserRepositoryImpl) UpdateLastLogin(id uint) error {
|
||||
return r.db.Model(&entity.User{}).Where("id = ?", id).
|
||||
UpdateColumn("last_login", gorm.Expr("CURRENT_TIMESTAMP")).Error
|
||||
}
|
||||
|
||||
// FindByRole 根据角色查找用户
|
||||
func (r *UserRepositoryImpl) FindByRole(role string) ([]entity.User, error) {
|
||||
var users []entity.User
|
||||
err := r.db.Where("role = ?", role).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
3
go.mod
3
go.mod
@@ -7,8 +7,10 @@ toolchain go1.23.3
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
golang.org/x/crypto v0.39.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
@@ -38,7 +40,6 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -32,6 +32,8 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
||||
242
handlers/user_handler.go
Normal file
242
handlers/user_handler.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"res_db/db/converter"
|
||||
"res_db/db/dto"
|
||||
"res_db/db/entity"
|
||||
"res_db/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Login 用户登录
|
||||
func Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "账户已被禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
if !middleware.CheckPassword(req.Password, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
repoManager.UserRepository.UpdateLastLogin(user.ID)
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := middleware.GenerateToken(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.LoginResponse{
|
||||
Token: token,
|
||||
User: converter.ToUserResponse(user),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func Register(c *gin.Context) {
|
||||
var req dto.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Password: hashedPassword,
|
||||
Email: req.Email,
|
||||
Role: "user",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "注册成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUsers 获取用户列表(管理员)
|
||||
func GetUsers(c *gin.Context) {
|
||||
users, err := repoManager.UserRepository.FindAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
responses := converter.ToUserResponseList(users)
|
||||
c.JSON(http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户(管理员)
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Password: hashedPassword,
|
||||
Email: req.Email,
|
||||
Role: req.Role,
|
||||
IsActive: req.IsActive,
|
||||
}
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "用户创建成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户(管理员)
|
||||
func UpdateUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username != "" {
|
||||
user.Username = req.Username
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Role != "" {
|
||||
user.Role = req.Role
|
||||
}
|
||||
user.IsActive = req.IsActive
|
||||
|
||||
err = repoManager.UserRepository.Update(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户更新成功"})
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户(管理员)
|
||||
func DeleteUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.UserRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"})
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
func GetProfile(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(userID.(uint))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToUserResponse(user)
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
62
main.go
62
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"res_db/db"
|
||||
"res_db/db/repo"
|
||||
"res_db/handlers"
|
||||
"res_db/middleware"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -43,18 +44,23 @@ func main() {
|
||||
// API路由
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 认证路由
|
||||
api.POST("/auth/login", handlers.Login)
|
||||
api.POST("/auth/register", handlers.Register)
|
||||
api.GET("/auth/profile", middleware.AuthMiddleware(), handlers.GetProfile)
|
||||
|
||||
// 资源管理
|
||||
api.GET("/resources", handlers.GetResources)
|
||||
api.POST("/resources", handlers.CreateResource)
|
||||
api.PUT("/resources/:id", handlers.UpdateResource)
|
||||
api.DELETE("/resources/:id", handlers.DeleteResource)
|
||||
api.POST("/resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateResource)
|
||||
api.PUT("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateResource)
|
||||
api.DELETE("/resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteResource)
|
||||
api.GET("/resources/:id", handlers.GetResourceByID)
|
||||
|
||||
// 分类管理
|
||||
api.GET("/categories", handlers.GetCategories)
|
||||
api.POST("/categories", handlers.CreateCategory)
|
||||
api.PUT("/categories/:id", handlers.UpdateCategory)
|
||||
api.DELETE("/categories/:id", handlers.DeleteCategory)
|
||||
api.POST("/categories", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateCategory)
|
||||
api.PUT("/categories/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateCategory)
|
||||
api.DELETE("/categories/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCategory)
|
||||
|
||||
// 搜索
|
||||
api.GET("/search", handlers.SearchResources)
|
||||
@@ -63,34 +69,40 @@ func main() {
|
||||
api.GET("/stats", handlers.GetStats)
|
||||
|
||||
// 平台管理
|
||||
api.GET("/pans", handlers.GetPans)
|
||||
api.POST("/pans", handlers.CreatePan)
|
||||
api.PUT("/pans/:id", handlers.UpdatePan)
|
||||
api.DELETE("/pans/:id", handlers.DeletePan)
|
||||
api.GET("/pans/:id", handlers.GetPan)
|
||||
api.GET("/pans", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetPans)
|
||||
api.POST("/pans", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreatePan)
|
||||
api.PUT("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdatePan)
|
||||
api.DELETE("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeletePan)
|
||||
api.GET("/pans/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetPan)
|
||||
|
||||
// Cookie管理
|
||||
api.GET("/cks", handlers.GetCks)
|
||||
api.POST("/cks", handlers.CreateCks)
|
||||
api.PUT("/cks/:id", handlers.UpdateCks)
|
||||
api.DELETE("/cks/:id", handlers.DeleteCks)
|
||||
api.GET("/cks/:id", handlers.GetCksByID)
|
||||
api.GET("/cks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCks)
|
||||
api.POST("/cks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateCks)
|
||||
api.PUT("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateCks)
|
||||
api.DELETE("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCks)
|
||||
api.GET("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCksByID)
|
||||
|
||||
// 标签管理
|
||||
api.GET("/tags", handlers.GetTags)
|
||||
api.POST("/tags", handlers.CreateTag)
|
||||
api.PUT("/tags/:id", handlers.UpdateTag)
|
||||
api.DELETE("/tags/:id", handlers.DeleteTag)
|
||||
api.POST("/tags", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateTag)
|
||||
api.PUT("/tags/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateTag)
|
||||
api.DELETE("/tags/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteTag)
|
||||
api.GET("/tags/:id", handlers.GetTagByID)
|
||||
api.GET("/resources/:id/tags", handlers.GetResourceTags)
|
||||
|
||||
// 待处理资源管理
|
||||
api.GET("/ready-resources", handlers.GetReadyResources)
|
||||
api.POST("/ready-resources", handlers.CreateReadyResource)
|
||||
api.POST("/ready-resources/batch", handlers.BatchCreateReadyResources)
|
||||
api.POST("/ready-resources/text", handlers.CreateReadyResourcesFromText)
|
||||
api.DELETE("/ready-resources/:id", handlers.DeleteReadyResource)
|
||||
api.DELETE("/ready-resources", handlers.ClearReadyResources)
|
||||
api.GET("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetReadyResources)
|
||||
api.POST("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResource)
|
||||
api.POST("/ready-resources/batch", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.BatchCreateReadyResources)
|
||||
api.POST("/ready-resources/text", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateReadyResourcesFromText)
|
||||
api.DELETE("/ready-resources/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteReadyResource)
|
||||
api.DELETE("/ready-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearReadyResources)
|
||||
|
||||
// 用户管理(仅管理员)
|
||||
api.GET("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetUsers)
|
||||
api.POST("/users", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.CreateUser)
|
||||
api.PUT("/users/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateUser)
|
||||
api.DELETE("/users/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteUser)
|
||||
}
|
||||
|
||||
// 静态文件服务
|
||||
|
||||
123
middleware/auth.go
Normal file
123
middleware/auth.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"res_db/db/entity"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte("your-secret-key")
|
||||
|
||||
// Claims JWT声明
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// AuthMiddleware 认证中间件
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := parseToken(tokenString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的令牌"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminMiddleware 管理员中间件
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT令牌
|
||||
func GenerateToken(user *entity.User) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// parseToken 解析JWT令牌
|
||||
func parseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
// HashPassword 哈希密码
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPassword 检查密码
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -2,16 +2,23 @@
|
||||
export const useResourceApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getResources = async (params?: any) => {
|
||||
return await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getResource = async (id: number) => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +26,8 @@ export const useResourceApi = () => {
|
||||
return await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,28 +35,32 @@ export const useResourceApi = () => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteResource = async (id: number) => {
|
||||
return await $fetch(`/resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const searchResources = async (params: any) => {
|
||||
return await $fetch('/search', {
|
||||
baseURL: config.public.apiBase,
|
||||
params
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getResourcesByPan = async (panId: number, params?: any) => {
|
||||
return await $fetch('/resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
params: { ...params, pan_id: panId }
|
||||
params: { ...params, pan_id: panId },
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,13 +75,54 @@ export const useResourceApi = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
export const useAuthApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const login = async (data: any) => {
|
||||
return await $fetch('/auth/login', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
}
|
||||
|
||||
const register = async (data: any) => {
|
||||
return await $fetch('/auth/register', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
}
|
||||
|
||||
const getProfile = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return await $fetch('/auth/profile', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
register,
|
||||
getProfile,
|
||||
}
|
||||
}
|
||||
|
||||
// 分类相关API
|
||||
export const useCategoryApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getCategories = async () => {
|
||||
return await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,7 +130,8 @@ export const useCategoryApi = () => {
|
||||
return await $fetch('/categories', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,14 +139,16 @@ export const useCategoryApi = () => {
|
||||
return await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteCategory = async (id: number) => {
|
||||
return await $fetch(`/categories/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,15 +164,22 @@ export const useCategoryApi = () => {
|
||||
export const usePanApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getPans = async () => {
|
||||
return await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getPan = async (id: number) => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,7 +187,8 @@ export const usePanApi = () => {
|
||||
return await $fetch('/pans', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,14 +196,16 @@ export const usePanApi = () => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deletePan = async (id: number) => {
|
||||
return await $fetch(`/pans/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,16 +222,23 @@ export const usePanApi = () => {
|
||||
export const useCksApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getCks = async (params?: any) => {
|
||||
return await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
params
|
||||
params,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getCksByID = async (id: number) => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,7 +246,8 @@ export const useCksApi = () => {
|
||||
return await $fetch('/cks', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,14 +255,16 @@ export const useCksApi = () => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteCks = async (id: number) => {
|
||||
return await $fetch(`/cks/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,15 +281,22 @@ export const useCksApi = () => {
|
||||
export const useTagApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getTags = async () => {
|
||||
return await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getTag = async (id: number) => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -221,7 +304,8 @@ export const useTagApi = () => {
|
||||
return await $fetch('/tags', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,20 +313,23 @@ export const useTagApi = () => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'PUT',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteTag = async (id: number) => {
|
||||
return await $fetch(`/tags/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const getResourceTags = async (resourceId: number) => {
|
||||
return await $fetch(`/resources/${resourceId}/tags`, {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -256,14 +343,19 @@ export const useTagApi = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 统计相关API
|
||||
// 待处理资源相关API
|
||||
export const useReadyResourceApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
const getReadyResources = async () => {
|
||||
return await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -271,7 +363,8 @@ export const useReadyResourceApi = () => {
|
||||
return await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -279,29 +372,36 @@ export const useReadyResourceApi = () => {
|
||||
return await $fetch('/ready-resources/batch', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: data
|
||||
body: data,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const createReadyResourcesFromText = async (text: string) => {
|
||||
const formData = new FormData()
|
||||
formData.append('text', text)
|
||||
|
||||
return await $fetch('/ready-resources/text', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'POST',
|
||||
body: { text }
|
||||
body: formData,
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteReadyResource = async (id: number) => {
|
||||
return await $fetch(`/ready-resources/${id}`, {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
const clearReadyResources = async () => {
|
||||
return await $fetch('/ready-resources', {
|
||||
baseURL: config.public.apiBase,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders() as Record<string, string>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -315,6 +415,7 @@ export const useReadyResourceApi = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 统计相关API
|
||||
export const useStatsApi = () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
|
||||
@@ -3,9 +3,23 @@
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-slate-800 text-white rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-4">
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
|
||||
</h1>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold">
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 no-underline">网盘资源管理系统</NuxtLink>
|
||||
</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-sm">
|
||||
<span>欢迎,{{ user?.username }}</span>
|
||||
<span class="ml-2 px-2 py-1 bg-blue-600 rounded text-xs">{{ user?.role }}</span>
|
||||
</div>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="px-3 py-1 bg-red-600 hover:bg-red-700 rounded text-sm transition-colors"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-4">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
@@ -204,103 +218,98 @@
|
||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="goToBackupRestore" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<button @click="goToUserManagement" class="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-700">备份恢复</span>
|
||||
<i class="fas fa-database text-gray-400"></i>
|
||||
<span class="text-sm font-medium text-gray-700">用户管理</span>
|
||||
<i class="fas fa-users text-gray-400"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加资源模态框 -->
|
||||
<ResourceModal
|
||||
v-if="showAddResourceModal"
|
||||
:resource="editingResource"
|
||||
@close="closeModal"
|
||||
@save="handleSaveResource"
|
||||
/>
|
||||
<!-- 模态框组件 -->
|
||||
<ResourceModal v-if="showAddResourceModal" @close="showAddResourceModal = false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const store = useResourceStore()
|
||||
const { stats } = storeToRefs(store)
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const user = ref(null)
|
||||
const stats = ref(null)
|
||||
const showAddResourceModal = ref(false)
|
||||
const showAddPlatformModal = ref(false)
|
||||
const showAddCategoryModal = ref(false)
|
||||
const showAddTagModal = ref(false)
|
||||
const editingResource = ref(null)
|
||||
|
||||
// 获取统计数据
|
||||
onMounted(async () => {
|
||||
await store.fetchStats()
|
||||
})
|
||||
|
||||
// 导航到资源管理
|
||||
const goToResourceManagement = () => {
|
||||
// 这里可以导航到资源管理页面
|
||||
console.log('导航到资源管理')
|
||||
}
|
||||
|
||||
// 导航到平台管理
|
||||
const goToPlatformManagement = () => {
|
||||
// 这里可以导航到平台管理页面
|
||||
console.log('导航到平台管理')
|
||||
}
|
||||
|
||||
// 导航到分类管理
|
||||
const goToCategoryManagement = () => {
|
||||
// 这里可以导航到分类管理页面
|
||||
console.log('导航到分类管理')
|
||||
}
|
||||
|
||||
// 导航到标签管理
|
||||
const goToTagManagement = () => {
|
||||
// 这里可以导航到标签管理页面
|
||||
console.log('导航到标签管理')
|
||||
}
|
||||
|
||||
// 导航到系统设置
|
||||
const goToSystemSettings = () => {
|
||||
// 这里可以导航到系统设置页面
|
||||
console.log('导航到系统设置')
|
||||
}
|
||||
|
||||
// 导航到备份恢复
|
||||
const goToBackupRestore = () => {
|
||||
// 这里可以导航到备份恢复页面
|
||||
console.log('导航到备份恢复')
|
||||
}
|
||||
|
||||
// 导航到批量添加
|
||||
const goToBatchAdd = () => {
|
||||
// 导航到待处理资源页面
|
||||
navigateTo('/ready-resources')
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
showAddResourceModal.value = false
|
||||
editingResource.value = null
|
||||
}
|
||||
|
||||
// 保存资源
|
||||
const handleSaveResource = async (resourceData: any) => {
|
||||
// 检查认证状态
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
|
||||
if (!token || !userStr) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingResource.value) {
|
||||
await store.updateResource(editingResource.value.id, resourceData)
|
||||
} else {
|
||||
await store.createResource(resourceData)
|
||||
}
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
user.value = JSON.parse(userStr)
|
||||
} catch (e) {
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await $api.get('/stats')
|
||||
stats.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 页面跳转方法
|
||||
const goToResourceManagement = () => {
|
||||
// 实现资源管理页面跳转
|
||||
}
|
||||
|
||||
const goToPlatformManagement = () => {
|
||||
// 实现平台管理页面跳转
|
||||
}
|
||||
|
||||
const goToCategoryManagement = () => {
|
||||
// 实现分类管理页面跳转
|
||||
}
|
||||
|
||||
const goToTagManagement = () => {
|
||||
// 实现标签管理页面跳转
|
||||
}
|
||||
|
||||
const goToBatchAdd = () => {
|
||||
router.push('/ready-resources')
|
||||
}
|
||||
|
||||
const goToSystemSettings = () => {
|
||||
// 实现系统设置页面跳转
|
||||
}
|
||||
|
||||
const goToUserManagement = () => {
|
||||
router.push('/users')
|
||||
}
|
||||
|
||||
// 页面加载时检查认证
|
||||
onMounted(() => {
|
||||
checkAuth()
|
||||
fetchStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
112
web/pages/login.vue
Normal file
112
web/pages/login.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
登录账户
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
或
|
||||
<NuxtLink to="/register" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||
注册新账户
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
<form class="mt-8 space-y-6" @submit.prevent="handleLogin">
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="密码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="text-red-600 text-sm text-center">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
<span v-if="loading" class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
默认管理员账户:admin / password
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await $api.post('/auth/login', form.value)
|
||||
|
||||
// 保存token和用户信息
|
||||
localStorage.setItem('token', response.data.token)
|
||||
localStorage.setItem('user', JSON.stringify(response.data.user))
|
||||
|
||||
// 跳转到admin页面
|
||||
await router.push('/admin')
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || '登录失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经登录,直接跳转到admin页面
|
||||
onMounted(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
router.push('/admin')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
124
web/pages/register.vue
Normal file
124
web/pages/register.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
注册新账户
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
或
|
||||
<NuxtLink to="/login" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||
登录现有账户
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
<form class="mt-8 space-y-6" @submit.prevent="handleRegister">
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="sr-only">邮箱</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="密码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="text-red-600 text-sm text-center">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="text-green-600 text-sm text-center">
|
||||
{{ success }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
<span v-if="loading" class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
|
||||
const handleRegister = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
|
||||
try {
|
||||
await $api.post('/auth/register', form.value)
|
||||
success.value = '注册成功!正在跳转到登录页面...'
|
||||
|
||||
// 延迟跳转到登录页面
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || '注册失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经登录,直接跳转到admin页面
|
||||
onMounted(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
router.push('/admin')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
271
web/pages/users.vue
Normal file
271
web/pages/users.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 p-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 头部 -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">用户管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink
|
||||
to="/admin"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||
>
|
||||
返回管理
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
添加用户
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">用户列表</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户名</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">邮箱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最后登录</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.username }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.email }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="getRoleClass(user.role)" class="px-2 py-1 text-xs font-medium rounded-full">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span :class="user.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
|
||||
class="px-2 py-1 text-xs font-medium rounded-full">
|
||||
{{ user.is_active ? '激活' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ user.last_login ? formatDate(user.last_login) : '从未登录' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button @click="editUser(user)" class="text-indigo-600 hover:text-indigo-900 mr-3">编辑</button>
|
||||
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-900">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑用户模态框 -->
|
||||
<div v-if="showCreateModal || showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{{ showEditModal ? '编辑用户' : '创建用户' }}
|
||||
</h3>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">用户名</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">邮箱</label>
|
||||
<input
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showCreateModal">
|
||||
<label class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">角色</label>
|
||||
<select
|
||||
v-model="form.role"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
<option value="user">用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="form.is_active"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-gray-700">激活状态</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 border border-transparent rounded-md text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
{{ showEditModal ? '更新' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const users = ref([])
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
// 检查认证
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await $api.get('/users')
|
||||
users.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
const createUser = async () => {
|
||||
try {
|
||||
await $api.post('/users', form.value)
|
||||
await fetchUsers()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('创建用户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
const updateUser = async () => {
|
||||
try {
|
||||
await $api.put(`/users/${editingUser.value.id}`, form.value)
|
||||
await fetchUsers()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('更新用户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (id) => {
|
||||
if (!confirm('确定要删除这个用户吗?')) return
|
||||
|
||||
try {
|
||||
await $api.delete(`/users/${id}`)
|
||||
await fetchUsers()
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const editUser = (user) => {
|
||||
editingUser.value = user
|
||||
form.value = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
role: user.role,
|
||||
is_active: user.is_active
|
||||
}
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
showCreateModal.value = false
|
||||
showEditModal.value = false
|
||||
editingUser.value = null
|
||||
form.value = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user',
|
||||
is_active: true
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = () => {
|
||||
if (showEditModal.value) {
|
||||
updateUser()
|
||||
} else {
|
||||
createUser()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色样式
|
||||
const getRoleClass = (role) => {
|
||||
return role === 'admin' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkAuth()
|
||||
fetchUsers()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user