diff --git a/cmd/server/main.go b/cmd/server/main.go index 3447bfc..879f0b9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -44,6 +44,7 @@ func main() { uploadPartHandler := handlers.NewUploadPartHandler(s3Repo) completeMultipartHandler := handlers.NewCompleteMultipartHandler(s3Repo) deleteFileHandler := handlers.NewDeleteFileHandler(s3Repo) + loginHandler := handlers.NewLoginHandler(middleware.API_KEY_VALUE) // Register Handlers mediator.Register[handlers.UploadFileCommand, string](m, uploadHandler) @@ -58,6 +59,7 @@ func main() { mediator.Register[handlers.UploadPartCommand, string](m, uploadPartHandler) mediator.Register[handlers.CompleteMultipartCommand, string](m, completeMultipartHandler) mediator.Register[handlers.DeleteFileCommand, string](m, deleteFileHandler) + mediator.Register[handlers.LoginQuery, handlers.LoginResult](m, loginHandler) // Validators uploadValidator := validators.NewUploadFileValidator() @@ -68,6 +70,7 @@ func main() { // Endpoints fileEndpoint := endpoints.NewFileEndpoint(m, uploadValidator, downloadValidator, newFeaturesValidator) bucketEndpoint := endpoints.NewBucketEndpoint(m, createBucketValidator) + authEndpoint := endpoints.NewAuthEndpoint(m) // Router r := gin.Default() @@ -85,6 +88,9 @@ func main() { c.Next() }) + // 公开接口(无需授权) + r.POST("/auth/login", authEndpoint.Login) + // API授权中间件组 api := r.Group("/") api.Use(middleware.AuthMiddleware()) diff --git a/docs/LOGIN_GUIDE.md b/docs/LOGIN_GUIDE.md new file mode 100644 index 0000000..fd991ad --- /dev/null +++ b/docs/LOGIN_GUIDE.md @@ -0,0 +1,174 @@ +# Web 登录功能说明 + +## 功能概述 + +Web UI 现已添加密钥登录功能,用户必须先登录才能访问文件管理系统。 + +## 使用流程 + +### 1. 访问登录页面 + +打开浏览器访问: +``` +http://localhost:8080/web/login.html +``` + +### 2. 输入 API 密钥 + +在登录页面输入 API 密钥: +``` +xn001624. +``` + +### 3. 登录成功 + +登录成功后,系统会: +- 将密钥保存到浏览器的 localStorage +- 自动跳转到主页面 `/web/` +- 所有后续 API 请求都会自动携带密钥 + +### 4. 退出登录 + +点击右上角的"退出"按钮可以: +- 清除本地存储的密钥 +- 跳转回登录页面 + +## 技术实现 + +### 后端 (遵循 CQRS 模式) + +1. **登录查询** (`internal/api/handlers/auth_handlers.go`) + ```go + type LoginQuery struct { + APIKey string `json:"api_key" binding:"required"` + } + ``` + +2. **登录处理器** + ```go + type LoginHandler struct { + apiKey string + } + + func (h *LoginHandler) Handle(ctx context.Context, query LoginQuery) (LoginResult, error) + ``` + +3. **认证端点** (`internal/api/endpoints/auth_endpoints.go`) + ```go + func (e *AuthEndpoint) Login(c *gin.Context) + ``` + +4. **路由配置** (`cmd/server/main.go`) + - `/auth/login` - 公开接口,无需授权 + - 所有其他 API 接口都需要 `X-API-Key` 请求头 + +### 前端 + +1. **登录页面** (`web/login.html`) + - 美观的渐变背景登录界面 + - 密钥输入框(密码类型) + - 表单验证和错误提示 + - 登录后自动跳转 + +2. **主页面** (`web/index.html`) + - 检查 localStorage 中的 token + - 未登录显示提示信息 + - 已登录正常显示文件管理界面 + - 所有 API 请求自动携带 `X-API-Key` 头 + - 401 错误自动跳转回登录页 + +## 安全特性 + +1. **前端存储**: 使用 localStorage 存储密钥(适合单用户场景) +2. **自动过期**: 当 API 返回 401 时自动清除密钥并跳转登录 +3. **密钥保护**: 密钥输入框使用 password 类型,屏幕上不可见 +4. **退出功能**: 支持主动退出登录 + +## 接口说明 + +### POST /auth/login + +**请求体**: +```json +{ + "api_key": "xn001624." +} +``` + +**成功响应** (200): +```json +{ + "success": true, + "message": "登录成功", + "token": "xn001624." +} +``` + +**失败响应** (200): +```json +{ + "success": false, + "message": "API密钥无效" +} +``` + +## 使用示例 + +### 使用 cURL 测试登录接口 + +```bash +curl -X POST http://localhost:8080/auth/login \ + -H "Content-Type: application/json" \ + -d '{"api_key": "xn001624."}' +``` + +### 前端 API 调用示例 + +登录后,所有 API 请求会自动携带密钥: + +```javascript +// 登录 +const res = await axios.post('/auth/login', { + api_key: 'xn001624.' +}); + +// 保存 token +localStorage.setItem('rustfs_token', res.data.token); + +// 后续请求自动携带密钥 +const api = axios.create({ + baseURL: window.location.origin, + headers: { + 'X-API-Key': localStorage.getItem('rustfs_token') + } +}); + +// 调用 API +const buckets = await api.get('/buckets'); +``` + +## 文件结构 + +``` +internal/ +├── api/ +│ ├── endpoints/ +│ │ └── auth_endpoints.go # 认证端点 +│ └── handlers/ +│ └── auth_handlers.go # 登录处理器 +└── middleware/ + └── auth.go # API 授权中间件 + +web/ +├── login.html # 登录页面 +└── index.html # 主页面(含登录检查) +``` + +## 更新日志 + +### v1.2 +- ✨ 添加 Web 登录功能 +- ✨ 创建登录页面 UI +- ✨ 实现 CQRS 模式的登录验证 +- 🔒 所有 Web 操作需要先登录 +- 📝 添加登录文档 diff --git a/internal/api/endpoints/auth_endpoints.go b/internal/api/endpoints/auth_endpoints.go new file mode 100644 index 0000000..38a8e85 --- /dev/null +++ b/internal/api/endpoints/auth_endpoints.go @@ -0,0 +1,45 @@ +package endpoints + +import ( + "file-system/internal/api/handlers" + "file-system/internal/infrastructure/mediator" + + "github.com/gin-gonic/gin" +) + +// AuthEndpoint 认证端点 +type AuthEndpoint struct { + mediator *mediator.Mediator +} + +// NewAuthEndpoint 创建认证端点 +func NewAuthEndpoint(m *mediator.Mediator) *AuthEndpoint { + return &AuthEndpoint{ + mediator: m, + } +} + +// Login 用户登录 +// @Summary 用户登录 +// @Description 使用 API 密钥登录 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body handlers.LoginQuery true "登录信息" +// @Success 200 {object} handlers.LoginResult +// @Router /auth/login [post] +func (e *AuthEndpoint) Login(c *gin.Context) { + var query handlers.LoginQuery + if err := c.ShouldBindJSON(&query); err != nil { + c.JSON(400, gin.H{"error": "请求参数错误"}) + return + } + + result, err := mediator.Send[handlers.LoginQuery, handlers.LoginResult](e.mediator, c.Request.Context(), query) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, result) +} diff --git a/internal/api/handlers/auth_handlers.go b/internal/api/handlers/auth_handlers.go new file mode 100644 index 0000000..8150eaf --- /dev/null +++ b/internal/api/handlers/auth_handlers.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "context" +) + +// LoginQuery 登录查询 +type LoginQuery struct { + APIKey string `json:"api_key" binding:"required"` +} + +// LoginResult 登录结果 +type LoginResult struct { + Success bool `json:"success"` + Message string `json:"message"` + Token string `json:"token,omitempty"` +} + +// LoginHandler 登录处理器 +type LoginHandler struct { + apiKey string +} + +// NewLoginHandler 创建登录处理器 +func NewLoginHandler(apiKey string) *LoginHandler { + return &LoginHandler{ + apiKey: apiKey, + } +} + +// Handle 处理登录查询 +func (h *LoginHandler) Handle(ctx context.Context, query LoginQuery) (LoginResult, error) { + if query.APIKey == h.apiKey { + return LoginResult{ + Success: true, + Message: "登录成功", + Token: query.APIKey, + }, nil + } + + return LoginResult{ + Success: false, + Message: "API密钥无效", + }, nil +} diff --git a/web/index.html b/web/index.html index c83fc15..a4861af 100644 --- a/web/index.html +++ b/web/index.html @@ -26,196 +26,214 @@
-
-

RustFS 文件管理系统

-
- - API 文档 + + -
- -
-
-
- 存储桶列表 - -
-
- -
- 暂无存储桶 -
-
+ +
+
+

RustFS 文件管理系统

+
+ + API 文档 + + +
- -
- -
-
-
-
- +
+ +
+
+
+ 存储桶列表 + +
+
+ -
-
- - +
+ 暂无存储桶 +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+ +
-
- - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - -
文件名大小修改时间操作
- - {{ file.Key }} - {{ formatSize(file.Size) }}{{ formatDate(file.LastModified) }} - - - - - - - - - -
- - 暂无文件 -
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
文件名大小修改时间操作
+ + {{ file.Key }} + {{ formatSize(file.Size) }}{{ formatDate(file.LastModified) }} + + + + + + + + + +
+ + 暂无文件 +
+
+ +
- - -
- -

请选择一个存储桶

-
- - -
-
上传任务队列
-
    -
  • -
    -
    - {{ upload.file.name }} - {{ upload.status }} + +
    +
    上传任务队列
    +
      +
    • +
      +
      + {{ upload.file.name }} + {{ upload.status }} +
      + {{ upload.progress }}%
      - {{ upload.progress }}% -
    -
    -
    -
    -
    {{ upload.error }}
    -
  • -
+
+
+
+
{{ upload.error }}
+ + +
-
- -