updateauth

This commit is contained in:
Kerwin
2025-07-10 15:07:29 +08:00
parent ab1074a2c6
commit 1133563182
17 changed files with 1431 additions and 135 deletions

116
AUTH_SYSTEM.md Normal file
View 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

View File

@@ -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
}

View 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
View 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
View 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"
}

View File

@@ -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),
}
}

View 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
View File

@@ -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
View File

@@ -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
View 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
View File

@@ -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
View 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
}

View File

@@ -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()

View File

@@ -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
View 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
View 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
View 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>