mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
refactor(util): move response helper to common/response and session logic to oauth
- Relocated generic HTTP response helpers (Response, OK, Err, etc.) from internal/util/ to a dedicated internal/common/response/ package. - Renamed ResponseAny to Any to resolve revive stuttering warnings. - Moved session building options and cookie headers logic from internal/util/ to internal/apps/oauth/. - Removed all direct imports of Gin/Sessions/HTTP frameworks from internal/util/ to keep general utilities 100% pure. - Regenerated Swagger API documentation via make swagger. - All tests and make code-check compile and pass with 0 issues.
This commit is contained in:
+327
-327
File diff suppressed because it is too large
Load Diff
+327
-327
File diff suppressed because it is too large
Load Diff
+318
-318
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,15 @@
|
||||
// Package auth_source 提供认证源管理功能
|
||||
package auth_source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
import ("errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// AuthSourceRequest 创建或更新认证源的请求参数
|
||||
type AuthSourceRequest struct {
|
||||
@@ -39,18 +38,18 @@ type ToggleAuthSourceRequest struct {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.AuthSource} "认证源列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]model.AuthSource} "认证源列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/auth-sources [get]
|
||||
func ListAuthSources(c *gin.Context) {
|
||||
sources, err := model.GetAuthSources(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(sources))
|
||||
c.JSON(http.StatusOK, response.OK(sources))
|
||||
}
|
||||
|
||||
// CreateAuthSource 创建认证源
|
||||
@@ -61,15 +60,15 @@ func ListAuthSources(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body auth_source.AuthSourceRequest true "创建认证源参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.AuthSource} "创建成功,返回认证源信息"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或验证失败"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=model.AuthSource} "创建成功,返回认证源信息"
|
||||
// @Failure 400 {object} response.Any "参数错误或验证失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/auth-sources [post]
|
||||
func CreateAuthSource(c *gin.Context) {
|
||||
var req AuthSourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,11 +84,11 @@ func CreateAuthSource(c *gin.Context) {
|
||||
IconURL: req.IconURL,
|
||||
}
|
||||
if err := model.CreateAuthSource(c.Request.Context(), &source); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
source.Sanitize()
|
||||
c.JSON(http.StatusOK, util.OK(source))
|
||||
c.JSON(http.StatusOK, response.OK(source))
|
||||
}
|
||||
|
||||
// UpdateAuthSource 更新认证源
|
||||
@@ -101,22 +100,22 @@ func CreateAuthSource(c *gin.Context) {
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "认证源 ID 或名称"
|
||||
// @Param request body auth_source.AuthSourceRequest true "更新认证源参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.AuthSource} "更新成功,返回更新后的认证源信息"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或验证失败"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=model.AuthSource} "更新成功,返回更新后的认证源信息"
|
||||
// @Failure 400 {object} response.Any "参数错误或验证失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/auth-sources/{id} [put]
|
||||
func UpdateAuthSource(c *gin.Context) {
|
||||
id, err := parseSourceID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var req AuthSourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,16 +133,16 @@ func UpdateAuthSource(c *gin.Context) {
|
||||
}
|
||||
keepSecret := source.ClientSecret == ""
|
||||
if err := model.UpdateAuthSource(c.Request.Context(), &source, keepSecret); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
updated, err := model.GetAuthSourceByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
updated.Sanitize()
|
||||
c.JSON(http.StatusOK, util.OK(updated))
|
||||
c.JSON(http.StatusOK, response.OK(updated))
|
||||
}
|
||||
|
||||
// ToggleAuthSource 切换认证源启用状态
|
||||
@@ -155,29 +154,29 @@ func UpdateAuthSource(c *gin.Context) {
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "认证源 ID 或名称"
|
||||
// @Param request body auth_source.ToggleAuthSourceRequest true "启用状态"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "切换成功"
|
||||
// @Failure 400 {object} util.ResponseAny "验证失败或认证源不存在"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=string} "切换成功"
|
||||
// @Failure 400 {object} response.Any "验证失败或认证源不存在"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/auth-sources/{id}/toggle [put]
|
||||
func ToggleAuthSource(c *gin.Context) {
|
||||
id, err := parseSourceID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var req ToggleAuthSourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := model.ToggleAuthSource(c.Request.Context(), id, req.IsActive); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// DeleteAuthSource 删除认证源
|
||||
@@ -187,22 +186,22 @@ func ToggleAuthSource(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "认证源 ID 或名称"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Failure 400 {object} util.ResponseAny "ID 无效或删除失败"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Failure 400 {object} response.Any "ID 无效或删除失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/auth-sources/{id} [delete]
|
||||
func DeleteAuthSource(c *gin.Context) {
|
||||
id, err := parseSourceID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := model.DeleteAuthSource(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
func parseSourceID(c *gin.Context) (uint64, error) {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package auth_source
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -16,7 +15,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -67,7 +67,7 @@ func TestListAuthSources(t *testing.T) {
|
||||
t.Errorf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
|
||||
Vendored
+24
-25
@@ -4,8 +4,7 @@
|
||||
// Package cache provides HTTP handlers for managing disk cache.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -13,10 +12,10 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/diskcache"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
type updateCacheConfigRequest struct {
|
||||
MaxSizeMB int64 `json:"max_size_mb" binding:"required,min=1"`
|
||||
@@ -30,14 +29,14 @@ type updateCacheConfigRequest struct {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=diskcache.Status} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=diskcache.Status} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/cache/status [get]
|
||||
func GetCacheStatus(c *gin.Context) {
|
||||
status := diskcache.GetGlobalCache().Status()
|
||||
c.JSON(http.StatusOK, util.OK(status))
|
||||
c.JSON(http.StatusOK, response.OK(status))
|
||||
}
|
||||
|
||||
// UpdateCacheConfig 更新磁盘缓存策略配置
|
||||
@@ -48,16 +47,16 @@ func GetCacheStatus(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param request body cache.updateCacheConfigRequest true "缓存配置请求体"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny "更新成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Success 200 {object} response.Any "更新成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/admin/cache/config [post]
|
||||
func UpdateCacheConfig(c *gin.Context) {
|
||||
var req updateCacheConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,26 +64,26 @@ func UpdateCacheConfig(c *gin.Context) {
|
||||
|
||||
// Update Max Size
|
||||
if err := saveOrUpdateConfig(ctx, model.ConfigKeyDiskCacheMaxSizeMB, strconv.FormatInt(req.MaxSizeMB, 10)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Update Default TTL
|
||||
if err := saveOrUpdateConfig(ctx, model.ConfigKeyDiskCacheTTLMinutes, strconv.FormatInt(req.TTLMinutes, 10)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Update LRU Enabled
|
||||
if err := saveOrUpdateConfig(ctx, model.ConfigKeyDiskCacheLRUEnabled, strconv.FormatBool(req.LRUEnabled)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger hot reloading in global cache
|
||||
diskcache.GetGlobalCache().ReloadConfig(ctx)
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// ClearCache 一键清空所有磁盘缓存数据
|
||||
@@ -93,17 +92,17 @@ func UpdateCacheConfig(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny "清理成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Success 200 {object} response.Any "清理成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/admin/cache/clear [post]
|
||||
func ClearCache(c *gin.Context) {
|
||||
if err := diskcache.GetGlobalCache().Clear(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
func saveOrUpdateConfig(ctx context.Context, key string, value string) error {
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
// overview information, and executing custom SQL queries.
|
||||
package db_manage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
import ("database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -17,10 +16,10 @@ import (
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
const (
|
||||
binaryKB = 0
|
||||
@@ -206,15 +205,15 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=db_manage.DBOverviewResponse} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=db_manage.DBOverviewResponse} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/db-manage/overview [get]
|
||||
func GetDBOverview(c *gin.Context) {
|
||||
gormDB := db.DB(c.Request.Context())
|
||||
if gormDB == nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("数据库未初始化"))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("数据库未初始化"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,11 +227,11 @@ func GetDBOverview(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(overview))
|
||||
c.JSON(http.StatusOK, response.OK(overview))
|
||||
}
|
||||
|
||||
// ListDBTables 获取数据库所有表名
|
||||
@@ -241,15 +240,15 @@ func GetDBOverview(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]string} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]string} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/db-manage/tables [get]
|
||||
func ListDBTables(c *gin.Context) {
|
||||
gormDB := db.DB(c.Request.Context())
|
||||
if gormDB == nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("数据库未初始化"))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("数据库未初始化"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -263,24 +262,24 @@ func ListDBTables(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tables))
|
||||
c.JSON(http.StatusOK, response.OK(tables))
|
||||
}
|
||||
|
||||
// GetDBTableData 获取数据表 data
|
||||
func GetDBTableData(c *gin.Context) {
|
||||
var req GetTableDataRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
gormDB := db.DB(c.Request.Context())
|
||||
if gormDB == nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("数据库未初始化"))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("数据库未初始化"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -289,7 +288,7 @@ func GetDBTableData(c *gin.Context) {
|
||||
|
||||
var total int64
|
||||
if err := gormDB.Raw("SELECT count(*) FROM " + quotedTable).Scan(&total).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -304,7 +303,7 @@ func GetDBTableData(c *gin.Context) {
|
||||
|
||||
rows, err := gormDB.Raw("SELECT * FROM "+quotedTable+" LIMIT ? OFFSET ?", limit, offset).Rows()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
@@ -313,17 +312,17 @@ func GetDBTableData(c *gin.Context) {
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
results, err := scanTableRows(rows, cols)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(TableDataResponse{
|
||||
c.JSON(http.StatusOK, response.OK(TableDataResponse{
|
||||
Columns: cols,
|
||||
Total: total,
|
||||
Results: results,
|
||||
@@ -441,28 +440,28 @@ func executeSQLMutation(gormDB *gorm.DB, sqlStr string, startTime time.Time) (Ex
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body db_manage.ExecuteSQLRequest true "SQL 请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=db_manage.ExecuteSQLResponse} "执行完毕"
|
||||
// @Failure 400 {object} util.ResponseAny "SQL 语句错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=db_manage.ExecuteSQLResponse} "执行完毕"
|
||||
// @Failure 400 {object} response.Any "SQL 语句错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/db-manage/query [post]
|
||||
func ExecuteSQL(c *gin.Context) {
|
||||
var req ExecuteSQLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
gormDB := db.DB(c.Request.Context())
|
||||
if gormDB == nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("数据库未初始化"))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("数据库未初始化"))
|
||||
return
|
||||
}
|
||||
|
||||
trimmedSQL := strings.TrimSpace(req.SQL)
|
||||
if trimmedSQL == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err("SQL 语句不能为空"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("SQL 语句不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -489,9 +488,9 @@ func ExecuteSQL(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(resp))
|
||||
c.JSON(http.StatusOK, response.OK(resp))
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
// Package logs 提供日志查询与分析功能
|
||||
package logs
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -19,10 +18,10 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
const (
|
||||
defaultLimit = 200
|
||||
@@ -48,9 +47,9 @@ type logsResponse struct {
|
||||
// @Security SessionCookie
|
||||
// @Param cursor query int false "日志游标,0=获取最新" default(0)
|
||||
// @Param limit query int false "每页条数" default(200)
|
||||
// @Success 200 {object} util.ResponseAny{data=logs.logsResponse} "日志列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=logs.logsResponse} "日志列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/logs [get]
|
||||
func GetLogs(c *gin.Context) {
|
||||
cursorStr := c.DefaultQuery("cursor", "0")
|
||||
@@ -58,7 +57,7 @@ func GetLogs(c *gin.Context) {
|
||||
|
||||
var cursor, limit int
|
||||
if _, err := parsePositiveInt(cursorStr, &cursor); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(admin.InvalidCursorParam))
|
||||
c.JSON(http.StatusBadRequest, response.Err(admin.InvalidCursorParam))
|
||||
return
|
||||
}
|
||||
if _, err := parsePositiveInt(limitStr, &limit); err != nil || limit <= 0 {
|
||||
@@ -78,7 +77,7 @@ func GetLogs(c *gin.Context) {
|
||||
resp.NextCursor = entries[0].Index
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(resp))
|
||||
c.JSON(http.StatusOK, response.OK(resp))
|
||||
}
|
||||
|
||||
// wsMessage WebSocket 消息格式
|
||||
@@ -281,15 +280,15 @@ func fetchAccessLogDetails(ctx context.Context, whereClause string, args []inter
|
||||
// @Param path query string false "接口路径模糊搜索"
|
||||
// @Param start_time query string false "起始时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)"
|
||||
// @Param end_time query string false "结束时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)"
|
||||
// @Success 200 {object} util.ResponseAny{data=logs.accessLogsResponse} "访问日志列表"
|
||||
// @Failure 400 {object} util.ResponseAny "ClickHouse 未启用或参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=logs.accessLogsResponse} "访问日志列表"
|
||||
// @Failure 400 {object} response.Any "ClickHouse 未启用或参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/logs/access [get]
|
||||
func GetAccessLogs(c *gin.Context) {
|
||||
// 1. 检查 ClickHouse 是否启用
|
||||
if !config.Config.ClickHouse.Enabled || db.ChConn == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("ClickHouse 存储服务未启用,无法检索访问日志"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("ClickHouse 存储服务未启用,无法检索访问日志"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,11 +309,11 @@ func GetAccessLogs(c *gin.Context) {
|
||||
// 3. 构建过滤条件
|
||||
conditions, args, userIDs, err := buildAccessLogFilters(c.Request.Context(), c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if userIDs != nil && len(userIDs) == 0 {
|
||||
c.JSON(http.StatusOK, util.OK(accessLogsResponse{Total: 0, List: []accessLogItem{}}))
|
||||
c.JSON(http.StatusOK, response.OK(accessLogsResponse{Total: 0, List: []accessLogItem{}}))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -327,22 +326,22 @@ func GetAccessLogs(c *gin.Context) {
|
||||
var total uint64
|
||||
countQuery := fmt.Sprintf("SELECT count() FROM w_user_access_logs %s", whereClause)
|
||||
if err := db.ChConn.QueryRow(c.Request.Context(), countQuery, args...).Scan(&total); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("查询 ClickHouse 日志统计失败: "+err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("查询 ClickHouse 日志统计失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
if total == 0 {
|
||||
c.JSON(http.StatusOK, util.OK(accessLogsResponse{Total: 0, List: []accessLogItem{}}))
|
||||
c.JSON(http.StatusOK, response.OK(accessLogsResponse{Total: 0, List: []accessLogItem{}}))
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 分页查询明细数据
|
||||
list, err := fetchAccessLogDetails(c.Request.Context(), whereClause, args, pageSize, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(accessLogsResponse{
|
||||
c.JSON(http.StatusOK, response.OK(accessLogsResponse{
|
||||
Total: total,
|
||||
List: list,
|
||||
}))
|
||||
@@ -381,15 +380,15 @@ type logsAnalyticsResponse struct {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=logs.logsAnalyticsResponse} "分析统计数据"
|
||||
// @Failure 400 {object} util.ResponseAny "ClickHouse 未启用"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=logs.logsAnalyticsResponse} "分析统计数据"
|
||||
// @Failure 400 {object} response.Any "ClickHouse 未启用"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/logs/analytics [get]
|
||||
func GetLogsAnalytics(c *gin.Context) {
|
||||
// 1. 检查 ClickHouse 是否启用
|
||||
if !config.Config.ClickHouse.Enabled || db.ChConn == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("ClickHouse 存储服务未启用,无法获取分析数据"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("ClickHouse 存储服务未启用,无法获取分析数据"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -401,7 +400,7 @@ func GetLogsAnalytics(c *gin.Context) {
|
||||
browserList := queryBrowserDistribution(ctx, startTime)
|
||||
topUsers := queryTopActiveUsers(ctx, startTime)
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(logsAnalyticsResponse{
|
||||
c.JSON(http.StatusOK, response.OK(logsAnalyticsResponse{
|
||||
Trend: trendList,
|
||||
Browsers: browserList,
|
||||
TopUsers: topUsers,
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package push
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
import ("encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -12,11 +11,11 @@ import (
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
pkgpush "github.com/Rain-kl/Wavelet/pkg/push"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// ListChannelDefinitions 获取各种消息通道的表单配置定义列表
|
||||
// @Summary 获取所有消息通道配置字段定义
|
||||
@@ -24,10 +23,10 @@ import (
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]Definition} "通道配置定义列表"
|
||||
// @Success 200 {object} response.Any{data=[]Definition} "通道配置定义列表"
|
||||
// @Router /api/v1/admin/push/channels/definitions [get]
|
||||
func ListChannelDefinitions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK(ListDefinitions()))
|
||||
c.JSON(http.StatusOK, response.OK(ListDefinitions()))
|
||||
}
|
||||
|
||||
// ListChannels 获取消息通道列表
|
||||
@@ -36,16 +35,16 @@ func ListChannelDefinitions(c *gin.Context) {
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.PushChannel} "消息通道列表"
|
||||
// @Success 200 {object} response.Any{data=[]model.PushChannel} "消息通道列表"
|
||||
// @Router /api/v1/admin/push/channels [get]
|
||||
func ListChannels(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var channels []model.PushChannel
|
||||
if err := db.DB(ctx).Order("created_at DESC").Find(&channels).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(channels))
|
||||
c.JSON(http.StatusOK, response.OK(channels))
|
||||
}
|
||||
|
||||
// CreateChannelRequest 创建通道参数
|
||||
@@ -67,12 +66,12 @@ type CreateChannelRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body CreateChannelRequest true "创建参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.PushChannel} "创建成功"
|
||||
// @Success 200 {object} response.Any{data=model.PushChannel} "创建成功"
|
||||
// @Router /api/v1/admin/push/channels [post]
|
||||
func CreateChannel(c *gin.Context) {
|
||||
var req CreateChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,11 +79,11 @@ func CreateChannel(c *gin.Context) {
|
||||
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.PushChannel{}).Where("name = ?", req.Name).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err("channel name already exists"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("channel name already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,16 +98,16 @@ func CreateChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := channel.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(channel))
|
||||
c.JSON(http.StatusOK, response.OK(channel))
|
||||
}
|
||||
|
||||
// UpdateChannelRequest 修改通道参数
|
||||
@@ -130,19 +129,19 @@ type UpdateChannelRequest struct {
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "通道ID"
|
||||
// @Param request body UpdateChannelRequest true "更新参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.PushChannel} "更新成功"
|
||||
// @Success 200 {object} response.Any{data=model.PushChannel} "更新成功"
|
||||
// @Router /api/v1/admin/push/channels/{id} [put]
|
||||
func UpdateChannel(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid channel id"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid channel id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,10 +150,10 @@ func UpdateChannel(c *gin.Context) {
|
||||
var channel model.PushChannel
|
||||
if err := db.DB(ctx).Where("id = ?", id).First(&channel).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err("channel not found"))
|
||||
c.JSON(http.StatusNotFound, response.Err("channel not found"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,16 +165,16 @@ func UpdateChannel(c *gin.Context) {
|
||||
channel.Enabled = req.Enabled
|
||||
|
||||
if err := channel.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Save(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(channel))
|
||||
c.JSON(http.StatusOK, response.OK(channel))
|
||||
}
|
||||
|
||||
// DeleteChannel 删除消息通道
|
||||
@@ -185,13 +184,13 @@ func UpdateChannel(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "通道ID"
|
||||
// @Success 200 {object} util.ResponseAny "删除成功"
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Router /api/v1/admin/push/channels/{id} [delete]
|
||||
func DeleteChannel(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid channel id"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid channel id"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,19 +198,19 @@ func DeleteChannel(c *gin.Context) {
|
||||
var channel model.PushChannel
|
||||
if err := db.DB(ctx).Where("id = ?", id).First(&channel).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err("channel not found"))
|
||||
c.JSON(http.StatusNotFound, response.Err("channel not found"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Delete(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// TestChannelRequest 测试通道连通性参数
|
||||
@@ -232,12 +231,12 @@ type TestChannelRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body TestChannelRequest true "测试参数"
|
||||
// @Success 200 {object} util.ResponseAny "测试触发成功"
|
||||
// @Success 200 {object} response.Any "测试触发成功"
|
||||
// @Router /api/v1/admin/push/channels/test [post]
|
||||
func TestChannel(c *gin.Context) {
|
||||
var req TestChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,7 +246,7 @@ func TestChannel(c *gin.Context) {
|
||||
if req.Name != "" {
|
||||
var channel model.PushChannel
|
||||
if err := db.DB(ctx).Where("name = ?", req.Name).First(&channel).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("channel not found"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("channel not found"))
|
||||
return
|
||||
}
|
||||
url = channel.URL
|
||||
@@ -276,7 +275,7 @@ func TestChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := tempChannel.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
url = tempChannel.URL
|
||||
@@ -334,11 +333,11 @@ func TestChannel(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := enqueuePushTask(ctx, payload); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// CustomPushRequest 外部公开推送请求参数
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package push
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -25,7 +24,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
var adminLoginEvent = EventMetadata{
|
||||
Key: "admin_login",
|
||||
@@ -344,7 +344,7 @@ func TestPushRouters(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
err = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -415,7 +415,7 @@ func TestPushRouters(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]any)
|
||||
@@ -449,7 +449,7 @@ func TestPushRouters(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -674,7 +674,7 @@ func TestPushChannelAPI(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataMap, ok := resp.Data.(map[string]any)
|
||||
@@ -689,7 +689,7 @@ func TestPushChannelAPI(t *testing.T) {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
list, ok := resp.Data.([]any)
|
||||
assert.True(t, ok)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// Package push defines push notification HTTP routes.
|
||||
package push
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -16,11 +15,11 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/push"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// UpdateEventRequest 更新事件请求参数
|
||||
type UpdateEventRequest struct {
|
||||
@@ -70,17 +69,17 @@ func SyncEvents(ctx context.Context) error {
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.PushEvent} "通知事件列表"
|
||||
// @Success 200 {object} response.Any{data=[]model.PushEvent} "通知事件列表"
|
||||
// @Router /api/v1/admin/push/events [get]
|
||||
func ListEvents(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var events []model.PushEvent
|
||||
if err := db.DB(ctx).Order("created_at DESC").Find(&events).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(events))
|
||||
c.JSON(http.StatusOK, response.OK(events))
|
||||
}
|
||||
|
||||
// CreateEventRequest 创建事件请求参数
|
||||
@@ -108,10 +107,10 @@ func findBuiltInEvent(key string) (EventMetadata, bool) {
|
||||
// @Tags admin-push
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]EventMetadata} "内置通知事件列表"
|
||||
// @Success 200 {object} response.Any{data=[]EventMetadata} "内置通知事件列表"
|
||||
// @Router /api/v1/admin/push/events/builtin [get]
|
||||
func ListBuiltInEvents(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK(BuiltInEvents))
|
||||
c.JSON(http.StatusOK, response.OK(BuiltInEvents))
|
||||
}
|
||||
|
||||
func getEventInfo(req CreateEventRequest) (string, string, []byte, error) {
|
||||
@@ -162,12 +161,12 @@ func getEventInfo(req CreateEventRequest) (string, string, []byte, error) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body CreateEventRequest true "创建参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.PushEvent} "创建成功"
|
||||
// @Success 200 {object} response.Any{data=model.PushEvent} "创建成功"
|
||||
// @Router /api/v1/admin/push/events [post]
|
||||
func CreateEvent(c *gin.Context) {
|
||||
var req CreateEventRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -175,18 +174,18 @@ func CreateEvent(c *gin.Context) {
|
||||
|
||||
eventKey, eventName, defaultTemplateBytes, err := getEventInfo(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查是否已经创建过该事件的配置
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.PushEvent{}).Where("event_key = ?", eventKey).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err("this notification event is already configured"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("this notification event is already configured"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ func CreateEvent(c *gin.Context) {
|
||||
} else {
|
||||
var tempMap map[string]any
|
||||
if err := json.Unmarshal([]byte(templateStr), &tempMap); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("custom template is not a valid JSON format"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("custom template is not a valid JSON format"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -223,16 +222,16 @@ func CreateEvent(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := event.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&event).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(event))
|
||||
c.JSON(http.StatusOK, response.OK(event))
|
||||
}
|
||||
|
||||
// DeleteEvent 删除通知事件配置
|
||||
@@ -242,13 +241,13 @@ func CreateEvent(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "事件 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Router /api/v1/admin/push/events/{id} [delete]
|
||||
func DeleteEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid event id"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid event id"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -256,19 +255,19 @@ func DeleteEvent(c *gin.Context) {
|
||||
var event model.PushEvent
|
||||
if err := db.DB(ctx).First(&event, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err("notification event not found"))
|
||||
c.JSON(http.StatusNotFound, response.Err("notification event not found"))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Delete(&event).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// UpdateEvent 更新通知事件
|
||||
@@ -280,28 +279,28 @@ func DeleteEvent(c *gin.Context) {
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "事件 ID"
|
||||
// @Param request body push.UpdateEventRequest true "更新参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "修改成功"
|
||||
// @Success 200 {object} response.Any{data=string} "修改成功"
|
||||
// @Router /api/v1/admin/push/events/{id} [put]
|
||||
func UpdateEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid event id"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid event id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateEventRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var event model.PushEvent
|
||||
if err := db.DB(c.Request.Context()).First(&event, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err("notification event not found"))
|
||||
c.JSON(http.StatusNotFound, response.Err("notification event not found"))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -312,16 +311,16 @@ func UpdateEvent(c *gin.Context) {
|
||||
event.Enabled = req.Enabled
|
||||
|
||||
if err := event.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Save(&event).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// ToggleEvent 快捷切换通知事件启用状态
|
||||
@@ -331,33 +330,33 @@ func UpdateEvent(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "事件 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "切换成功"
|
||||
// @Success 200 {object} response.Any{data=string} "切换成功"
|
||||
// @Router /api/v1/admin/push/events/{id}/toggle [post]
|
||||
func ToggleEvent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid event id"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid event id"))
|
||||
return
|
||||
}
|
||||
|
||||
var event model.PushEvent
|
||||
if err := db.DB(c.Request.Context()).First(&event, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err("notification event not found"))
|
||||
c.JSON(http.StatusNotFound, response.Err("notification event not found"))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
event.Enabled = !event.Enabled
|
||||
if err := db.DB(c.Request.Context()).Model(&event).Update("enabled", event.Enabled).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(event.Enabled))
|
||||
c.JSON(http.StatusOK, response.OK(event.Enabled))
|
||||
}
|
||||
|
||||
// pushHistoriesResponse 推送历史分页响应
|
||||
@@ -378,7 +377,7 @@ type pushHistoriesResponse struct {
|
||||
// @Param page_size query int false "分页大小"
|
||||
// @Param event_key query string false "过滤事件名称"
|
||||
// @Param status query string false "过滤发送状态"
|
||||
// @Success 200 {object} util.ResponseAny{data=pushHistoriesResponse} "推送历史列表"
|
||||
// @Success 200 {object} response.Any{data=pushHistoriesResponse} "推送历史列表"
|
||||
// @Router /api/v1/admin/push/histories [get]
|
||||
func ListHistories(c *gin.Context) {
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
@@ -405,18 +404,18 @@ func ListHistories(c *gin.Context) {
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var results []model.PushHistory
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(&results).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(map[string]any{
|
||||
c.JSON(http.StatusOK, response.OK(map[string]any{
|
||||
"total": total,
|
||||
"results": results,
|
||||
}))
|
||||
@@ -430,24 +429,24 @@ func ListHistories(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body push.TestPushRequest true "测试请求体"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "测试成功"
|
||||
// @Success 200 {object} response.Any{data=string} "测试成功"
|
||||
// @Router /api/v1/admin/push/test [post]
|
||||
func TestPush(c *gin.Context) {
|
||||
var req TestPushRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
pusher, err := push.GetPusher(req.Config.Channel)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验配置
|
||||
if err := pusher.ValidateConfig(req.Config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(fmt.Sprintf("validation failed: %v", err)))
|
||||
c.JSON(http.StatusBadRequest, response.Err(fmt.Sprintf("validation failed: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -479,9 +478,9 @@ func TestPush(c *gin.Context) {
|
||||
|
||||
err = pusher.Send(c.Request.Context(), req.Config, req.Target, testBody, "", nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
// Package status 提供系统状态查询接口
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
@@ -18,9 +17,9 @@ import (
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// startTime 记录服务启动时间
|
||||
var startTime = time.Now()
|
||||
@@ -135,9 +134,9 @@ func formatDuration(d time.Duration) string {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=status.SystemStatusResponse} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=status.SystemStatusResponse} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/status [get]
|
||||
func GetSystemStatus(c *gin.Context) {
|
||||
var m runtime.MemStats
|
||||
@@ -194,7 +193,7 @@ func GetSystemStatus(c *gin.Context) {
|
||||
NumGC: m.NumGC,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(res))
|
||||
c.JSON(http.StatusOK, response.OK(res))
|
||||
}
|
||||
|
||||
// DatabaseInfoResponse 数据库信息响应结构体
|
||||
@@ -249,9 +248,9 @@ func getPostgresInfo(ctx context.Context) DatabaseInfoResponse {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=status.DatabaseInfoResponse} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=status.DatabaseInfoResponse} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/db-info [get]
|
||||
func GetDatabaseInfo(c *gin.Context) {
|
||||
var info DatabaseInfoResponse
|
||||
@@ -260,7 +259,7 @@ func GetDatabaseInfo(c *gin.Context) {
|
||||
} else {
|
||||
info = getPostgresInfo(c.Request.Context())
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(info))
|
||||
c.JSON(http.StatusOK, response.OK(info))
|
||||
}
|
||||
|
||||
// ExportDatabase 导出数据库
|
||||
@@ -270,9 +269,9 @@ func GetDatabaseInfo(c *gin.Context) {
|
||||
// @Produce application/octet-stream
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {file} binary "数据库文件"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "导出失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "导出失败"
|
||||
// @Router /api/v1/admin/db-export [get]
|
||||
func ExportDatabase(c *gin.Context) {
|
||||
if !config.Config.Database.Enabled {
|
||||
@@ -291,7 +290,7 @@ func exportSQLite(c *gin.Context) {
|
||||
|
||||
f, err := os.Open(path) //nolint:gosec // path is loaded from server startup configuration, not user input
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("无法打开数据库文件: "+err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("无法打开数据库文件: "+err.Error()))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
@@ -302,7 +301,7 @@ func exportSQLite(c *gin.Context) {
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("无法读取数据库文件信息: "+err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("无法读取数据库文件信息: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,7 +319,7 @@ func exportPostgres(c *gin.Context) {
|
||||
// 检查 pg_dump 是否可用
|
||||
pgDumpPath, err := exec.LookPath("pg_dump")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("pg_dump 不可用,请确保服务器已安装 PostgreSQL 客户端工具"))
|
||||
c.JSON(http.StatusInternalServerError, response.Err("pg_dump 不可用,请确保服务器已安装 PostgreSQL 客户端工具"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package system_config
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -15,12 +14,12 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
mail "github.com/Rain-kl/Wavelet/pkg/mail"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
const maskedConfigValue = "******"
|
||||
|
||||
@@ -48,26 +47,26 @@ type UpdateSystemConfigRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body system_config.CreateSystemConfigRequest true "创建请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "创建成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或配置键已存在"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "创建成功"
|
||||
// @Failure 400 {object} response.Any "参数错误或配置键已存在"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/system-configs [post]
|
||||
func CreateSystemConfig(c *gin.Context) {
|
||||
var req CreateSystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查配置键是否已存在
|
||||
var existing model.SystemConfig
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", req.Key).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(ConfigKeyExists))
|
||||
c.JSON(http.StatusBadRequest, response.Err(ConfigKeyExists))
|
||||
return
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,11 +90,11 @@ func CreateSystemConfig(c *gin.Context) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// ListSystemConfigs 获取系统配置列表
|
||||
@@ -105,10 +104,10 @@ func CreateSystemConfig(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param type query string false "配置类型(system/business)"
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.SystemConfig} "系统配置列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]model.SystemConfig} "系统配置列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/system-configs [get]
|
||||
func ListSystemConfigs(c *gin.Context) {
|
||||
configType := c.Query("type")
|
||||
@@ -119,7 +118,7 @@ func ListSystemConfigs(c *gin.Context) {
|
||||
|
||||
var configs []model.SystemConfig
|
||||
if err := query.Find(&configs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +126,7 @@ func ListSystemConfigs(c *gin.Context) {
|
||||
configs[i].Value = maskSensitiveConfig(configs[i].Key, configs[i].Value)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(configs))
|
||||
c.JSON(http.StatusOK, response.OK(configs))
|
||||
}
|
||||
|
||||
// GetSystemConfig 获取单个系统配置
|
||||
@@ -137,26 +136,26 @@ func ListSystemConfigs(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param key path string true "配置键"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.SystemConfig} "系统配置详情"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "配置不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=model.SystemConfig} "系统配置详情"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "配置不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/system-configs/{key} [get]
|
||||
func GetSystemConfig(c *gin.Context) {
|
||||
var config model.SystemConfig
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", c.Param("key")).First(&config).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err(SystemConfigNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(SystemConfigNotFound))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
config.Value = maskSensitiveConfig(config.Key, config.Value)
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(config))
|
||||
c.JSON(http.StatusOK, response.OK(config))
|
||||
}
|
||||
|
||||
// UpdateSystemConfig 更新系统配置
|
||||
@@ -168,17 +167,17 @@ func GetSystemConfig(c *gin.Context) {
|
||||
// @Security SessionCookie
|
||||
// @Param key path string true "配置键"
|
||||
// @Param request body system_config.UpdateSystemConfigRequest true "更新请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "更新成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "配置不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "更新成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "配置不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/system-configs/{key} [put]
|
||||
func UpdateSystemConfig(c *gin.Context) {
|
||||
var req UpdateSystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,9 +187,9 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
var config model.SystemConfig
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", key).First(&config).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err(SystemConfigNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(SystemConfigNotFound))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -204,7 +203,7 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
|
||||
validatedVal, err := validateAndMergeStorageConfig(c.Request.Context(), req.Value, config.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
req.Value = validatedVal
|
||||
@@ -251,7 +250,7 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -260,7 +259,7 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
storage.PublishCacheInvalidation(c.Request.Context())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// TestSMTPRequest 测试 SMTP 配置请求
|
||||
@@ -287,13 +286,13 @@ type TestSMTPResponse struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body system_config.TestSMTPRequest true "测试请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=system_config.TestSMTPResponse} "测试执行完毕"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Success 200 {object} response.Any{data=system_config.TestSMTPResponse} "测试执行完毕"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/admin/system-configs/smtp/test [post]
|
||||
func TestSMTP(c *gin.Context) {
|
||||
var req TestSMTPRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -326,7 +325,7 @@ func TestSMTP(c *gin.Context) {
|
||||
resp.Error = err.Error()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(resp))
|
||||
c.JSON(http.StatusOK, response.OK(resp))
|
||||
}
|
||||
|
||||
func maskSensitiveConfig(key, value string) string {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package system_config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
import ("bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -22,7 +21,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
const expectedDefaultConfigsCount = 30
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestListSystemConfigs(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -151,7 +151,7 @@ func TestListSystemConfigs(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -180,7 +180,7 @@ func TestGetSystemConfig(t *testing.T) {
|
||||
t.Errorf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -352,7 +352,7 @@ func TestTestSMTP(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
import ("fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,11 +15,11 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
||||
"github.com/Rain-kl/Wavelet/internal/task/scheduler"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func init() {
|
||||
taskhandlers.Register()
|
||||
@@ -32,12 +31,12 @@ func init() {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]task.TaskMeta} "任务类型列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=[]task.TaskMeta} "任务类型列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/tasks/types [get]
|
||||
func ListTaskTypes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK(task.GetDispatchableTasks()))
|
||||
c.JSON(http.StatusOK, response.OK(task.GetDispatchableTasks()))
|
||||
}
|
||||
|
||||
// DispatchTaskRequest 下发任务请求
|
||||
@@ -57,22 +56,22 @@ type DispatchTaskRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body DispatchTaskRequest true "任务请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "任务已入队"
|
||||
// @Failure 400 {object} util.ResponseAny "任务类型不存在或参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "任务入队失败"
|
||||
// @Success 200 {object} response.Any{data=string} "任务已入队"
|
||||
// @Failure 400 {object} response.Any "任务类型不存在或参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "任务入队失败"
|
||||
// @Router /api/v1/admin/tasks/dispatch [post]
|
||||
func DispatchTask(c *gin.Context) {
|
||||
var req DispatchTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
meta := task.GetTaskMeta(req.TaskType)
|
||||
if meta == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(InvalidTaskType))
|
||||
c.JSON(http.StatusBadRequest, response.Err(InvalidTaskType))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,17 +82,17 @@ func DispatchTask(c *gin.Context) {
|
||||
|
||||
validated, err := task.ValidateAndNormalizePayload(meta.AsynqTask, payloadBytes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
taskID, err := task.DispatchTask(c.Request.Context(), req.TaskType, validated, "manual")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", TaskDispatchFailed, err)))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(fmt.Sprintf("%s: %v", TaskDispatchFailed, err)))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(taskID))
|
||||
c.JSON(http.StatusOK, response.OK(taskID))
|
||||
}
|
||||
|
||||
// ListTaskExecutions 查询任务执行记录列表
|
||||
@@ -106,14 +105,14 @@ func DispatchTask(c *gin.Context) {
|
||||
// @Param task_type query string false "任务类型筛选"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页条数" default(20)
|
||||
// @Success 200 {object} util.ResponseAny{data=object} "任务执行记录列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=object} "任务执行记录列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/tasks/executions [get]
|
||||
func ListTaskExecutions(c *gin.Context) {
|
||||
var req model.ListTaskExecutionsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,11 +124,11 @@ func ListTaskExecutions(c *gin.Context) {
|
||||
|
||||
executions, total, err := model.ListTaskExecutions(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(gin.H{
|
||||
c.JSON(http.StatusOK, response.OK(gin.H{
|
||||
"items": executions,
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
@@ -144,26 +143,26 @@ func ListTaskExecutions(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "任务执行记录 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.TaskExecution} "任务执行详情"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "记录不存在"
|
||||
// @Success 200 {object} response.Any{data=model.TaskExecution} "任务执行详情"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "记录不存在"
|
||||
// @Router /api/v1/admin/tasks/executions/{id} [get]
|
||||
func GetTaskExecution(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(admin.InvalidTaskExecutionID))
|
||||
c.JSON(http.StatusBadRequest, response.Err(admin.InvalidTaskExecutionID))
|
||||
return
|
||||
}
|
||||
|
||||
execution, err := model.GetTaskExecutionByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, util.Err(TaskNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(TaskNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(execution))
|
||||
c.JSON(http.StatusOK, response.OK(execution))
|
||||
}
|
||||
|
||||
// RetryTask 重试失败的任务
|
||||
@@ -173,17 +172,17 @@ func GetTaskExecution(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "任务执行记录 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "新任务的 TaskID"
|
||||
// @Failure 400 {object} util.ResponseAny "任务不支持重试或参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "记录不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "重试失败"
|
||||
// @Success 200 {object} response.Any{data=string} "新任务的 TaskID"
|
||||
// @Failure 400 {object} response.Any "任务不支持重试或参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "记录不存在"
|
||||
// @Failure 500 {object} response.Any "重试失败"
|
||||
// @Router /api/v1/admin/tasks/executions/{id}/retry [post]
|
||||
func RetryTask(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(admin.InvalidTaskExecutionID))
|
||||
c.JSON(http.StatusBadRequest, response.Err(admin.InvalidTaskExecutionID))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,16 +191,16 @@ func RetryTask(c *gin.Context) {
|
||||
errMsg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(errMsg, "不存在"):
|
||||
c.JSON(http.StatusNotFound, util.Err(errMsg))
|
||||
c.JSON(http.StatusNotFound, response.Err(errMsg))
|
||||
case strings.Contains(errMsg, "只有失败的任务") || strings.Contains(errMsg, "不支持重试") || strings.Contains(errMsg, "已达到最大重试"):
|
||||
c.JSON(http.StatusBadRequest, util.Err(errMsg))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errMsg))
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", TaskRetryFailed, err)))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(fmt.Sprintf("%s: %v", TaskRetryFailed, err)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(newTaskID))
|
||||
c.JSON(http.StatusOK, response.OK(newTaskID))
|
||||
}
|
||||
|
||||
// ListSchedules 获取定时任务列表
|
||||
@@ -210,17 +209,17 @@ func RetryTask(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.Schedule} "定时任务列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=[]model.Schedule} "定时任务列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/tasks/schedules [get]
|
||||
func ListSchedules(c *gin.Context) {
|
||||
schedules, err := model.ListSchedules(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(schedules))
|
||||
c.JSON(http.StatusOK, response.OK(schedules))
|
||||
}
|
||||
|
||||
// CreateScheduleRequest 创建定时任务请求
|
||||
@@ -240,29 +239,29 @@ type CreateScheduleRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body CreateScheduleRequest true "创建定时任务请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Schedule} "创建成功的定时任务信息"
|
||||
// @Failure 400 {object} util.ResponseAny "Cron 表达式无效、异步任务类型不存在或参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "保存定时任务失败"
|
||||
// @Success 200 {object} response.Any{data=model.Schedule} "创建成功的定时任务信息"
|
||||
// @Failure 400 {object} response.Any "Cron 表达式无效、异步任务类型不存在或参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "保存定时任务失败"
|
||||
// @Router /api/v1/admin/tasks/schedules [post]
|
||||
func CreateSchedule(c *gin.Context) {
|
||||
var req CreateScheduleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验 Cron 表达式
|
||||
if _, err := cron.ParseStandard(req.Cron); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(InvalidCronExpression))
|
||||
c.JSON(http.StatusBadRequest, response.Err(InvalidCronExpression))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验关联的异步任务类型
|
||||
meta := task.GetTaskMeta(req.TaskType)
|
||||
if meta == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(InvalidTaskType))
|
||||
c.JSON(http.StatusBadRequest, response.Err(InvalidTaskType))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -273,7 +272,7 @@ func CreateSchedule(c *gin.Context) {
|
||||
}
|
||||
validated, err := task.ValidateAndNormalizePayload(meta.AsynqTask, payloadBytes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -286,7 +285,7 @@ func CreateSchedule(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := model.CreateSchedule(c.Request.Context(), schedule); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", ScheduleSaveFailed, err)))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(fmt.Sprintf("%s: %v", ScheduleSaveFailed, err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -295,7 +294,7 @@ func CreateSchedule(c *gin.Context) {
|
||||
logger.ErrorF(c.Request.Context(), "[TaskAdmin] 重载调度器失败: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(schedule))
|
||||
c.JSON(http.StatusOK, response.OK(schedule))
|
||||
}
|
||||
|
||||
// UpdateScheduleRequest 修改定时任务请求
|
||||
@@ -316,43 +315,43 @@ type UpdateScheduleRequest struct {
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "定时任务 ID"
|
||||
// @Param request body UpdateScheduleRequest true "修改定时任务请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Schedule} "修改后的定时任务信息"
|
||||
// @Failure 400 {object} util.ResponseAny "Cron 表达式无效、参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "定时任务不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "修改定时任务失败"
|
||||
// @Success 200 {object} response.Any{data=model.Schedule} "修改后的定时任务信息"
|
||||
// @Failure 400 {object} response.Any "Cron 表达式无效、参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "定时任务不存在"
|
||||
// @Failure 500 {object} response.Any "修改定时任务失败"
|
||||
// @Router /api/v1/admin/tasks/schedules/{id} [put]
|
||||
func UpdateSchedule(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("无效的定时任务ID"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("无效的定时任务ID"))
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateScheduleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查定时任务是否存在
|
||||
schedule, err := model.GetScheduleByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, util.Err(ScheduleNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(ScheduleNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验 Cron 表达式
|
||||
if _, err := cron.ParseStandard(req.Cron); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(InvalidCronExpression))
|
||||
c.JSON(http.StatusBadRequest, response.Err(InvalidCronExpression))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验关联的异步任务类型
|
||||
meta := task.GetTaskMeta(req.TaskType)
|
||||
if meta == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(InvalidTaskType))
|
||||
c.JSON(http.StatusBadRequest, response.Err(InvalidTaskType))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -363,7 +362,7 @@ func UpdateSchedule(c *gin.Context) {
|
||||
}
|
||||
validated, err := task.ValidateAndNormalizePayload(meta.AsynqTask, payloadBytes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -374,7 +373,7 @@ func UpdateSchedule(c *gin.Context) {
|
||||
schedule.IsActive = *req.IsActive
|
||||
|
||||
if err := model.UpdateSchedule(c.Request.Context(), schedule); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", ScheduleSaveFailed, err)))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(fmt.Sprintf("%s: %v", ScheduleSaveFailed, err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -383,7 +382,7 @@ func UpdateSchedule(c *gin.Context) {
|
||||
logger.ErrorF(c.Request.Context(), "[TaskAdmin] 重载调度器失败: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(schedule))
|
||||
c.JSON(http.StatusOK, response.OK(schedule))
|
||||
}
|
||||
|
||||
// DeleteSchedule 删除定时任务
|
||||
@@ -393,21 +392,21 @@ func UpdateSchedule(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "定时任务 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除结果"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "删除定时任务失败"
|
||||
// @Success 200 {object} response.Any{data=string} "删除结果"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "删除定时任务失败"
|
||||
// @Router /api/v1/admin/tasks/schedules/{id} [delete]
|
||||
func DeleteSchedule(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err("无效的定时任务ID"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("无效的定时任务ID"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := model.DeleteSchedule(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(fmt.Sprintf("%s: %v", ScheduleDeleteFailed, err)))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(fmt.Sprintf("%s: %v", ScheduleDeleteFailed, err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -416,5 +415,5 @@ func DeleteSchedule(c *gin.Context) {
|
||||
logger.ErrorF(c.Request.Context(), "[TaskAdmin] 重载调度器失败: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -26,7 +25,8 @@ import (
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func setupTaskTestEnvironment(t *testing.T) func() {
|
||||
_, mr, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
@@ -78,7 +78,7 @@ func TestListTaskTypes(t *testing.T) {
|
||||
t.Errorf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -126,7 +126,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String())
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Empty(t, resp.ErrorMsg)
|
||||
assert.NotNil(t, resp.Data)
|
||||
@@ -150,7 +150,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Body: %s", w.Body.String())
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Empty(t, resp.ErrorMsg)
|
||||
assert.NotNil(t, resp.Data)
|
||||
@@ -168,7 +168,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Contains(t, resp.ErrorMsg, "无效的 JSON 格式")
|
||||
})
|
||||
@@ -185,7 +185,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Contains(t, resp.ErrorMsg, "不能为空")
|
||||
})
|
||||
@@ -202,7 +202,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, InvalidTaskType, resp.ErrorMsg)
|
||||
})
|
||||
@@ -244,7 +244,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -261,7 +261,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -278,7 +278,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -295,7 +295,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -312,7 +312,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -355,7 +355,7 @@ func TestGetTaskExecution(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -419,7 +419,7 @@ func TestRetryTask(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Empty(t, resp.ErrorMsg)
|
||||
assert.NotNil(t, resp.Data)
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"errors"
|
||||
import ("errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// CreateTemplateRequest 创建模板请求
|
||||
type CreateTemplateRequest struct {
|
||||
@@ -41,26 +40,26 @@ type UpdateTemplateRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body template.CreateTemplateRequest true "创建请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "创建成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或模板标识符已存在"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "创建成功"
|
||||
// @Failure 400 {object} response.Any "参数错误或模板标识符已存在"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/templates [post]
|
||||
func CreateTemplate(c *gin.Context) {
|
||||
var req CreateTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查模板 Key 是否已存在
|
||||
var existing model.Template
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", req.Key).First(&existing).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(TemplateKeyExists))
|
||||
c.JSON(http.StatusBadRequest, response.Err(TemplateKeyExists))
|
||||
return
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,16 +74,16 @@ func CreateTemplate(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := tmpl.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Create(&tmpl).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tmpl))
|
||||
c.JSON(http.StatusOK, response.OK(tmpl))
|
||||
}
|
||||
|
||||
// ListTemplates 获取模板列表
|
||||
@@ -93,19 +92,19 @@ func CreateTemplate(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.Template} "模板列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]model.Template} "模板列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/templates [get]
|
||||
func ListTemplates(c *gin.Context) {
|
||||
var templates []model.Template
|
||||
if err := db.DB(c.Request.Context()).Order("is_system DESC, created_at DESC").Find(&templates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(templates))
|
||||
c.JSON(http.StatusOK, response.OK(templates))
|
||||
}
|
||||
|
||||
// GetTemplate 获取单个模板
|
||||
@@ -115,24 +114,24 @@ func ListTemplates(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param key path string true "模板标识符"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Template} "模板详情"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "模板不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=model.Template} "模板详情"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "模板不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/templates/{key} [get]
|
||||
func GetTemplate(c *gin.Context) {
|
||||
var tmpl model.Template
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", c.Param("key")).First(&tmpl).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err(TemplateNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(TemplateNotFound))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tmpl))
|
||||
c.JSON(http.StatusOK, response.OK(tmpl))
|
||||
}
|
||||
|
||||
// UpdateTemplate 更新模板
|
||||
@@ -144,17 +143,17 @@ func GetTemplate(c *gin.Context) {
|
||||
// @Security SessionCookie
|
||||
// @Param key path string true "模板标识符"
|
||||
// @Param request body template.UpdateTemplateRequest true "更新请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Template} "更新成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "模板不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=model.Template} "更新成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "模板不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/templates/{key} [put]
|
||||
func UpdateTemplate(c *gin.Context) {
|
||||
var req UpdateTemplateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,9 +163,9 @@ func UpdateTemplate(c *gin.Context) {
|
||||
var tmpl model.Template
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", key).First(&tmpl).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err(TemplateNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(TemplateNotFound))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -178,16 +177,16 @@ func UpdateTemplate(c *gin.Context) {
|
||||
tmpl.Description = req.Description
|
||||
|
||||
if err := tmpl.Validate(); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Save(&tmpl).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tmpl))
|
||||
c.JSON(http.StatusOK, response.OK(tmpl))
|
||||
}
|
||||
|
||||
// DeleteTemplate 删除模板
|
||||
@@ -197,12 +196,12 @@ func UpdateTemplate(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param key path string true "模板标识符"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Failure 400 {object} util.ResponseAny "不可删除系统模板"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "模板不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Failure 400 {object} response.Any "不可删除系统模板"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "模板不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/templates/{key} [delete]
|
||||
func DeleteTemplate(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
@@ -211,23 +210,23 @@ func DeleteTemplate(c *gin.Context) {
|
||||
var tmpl model.Template
|
||||
if err := db.DB(c.Request.Context()).Where("key = ?", key).First(&tmpl).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, util.Err(TemplateNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(TemplateNotFound))
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 限制系统模板删除
|
||||
if tmpl.IsSystem {
|
||||
c.JSON(http.StatusBadRequest, util.Err(SystemTemplateCannotDelete))
|
||||
c.JSON(http.StatusBadRequest, response.Err(SystemTemplateCannotDelete))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(c.Request.Context()).Delete(&tmpl).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -15,7 +14,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -122,7 +122,7 @@ func TestListTemplates(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// GetUpdateStatus 获取应用更新状态
|
||||
// @Summary 获取应用更新状态
|
||||
@@ -19,19 +18,19 @@ import (
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=updater.Status} "更新状态"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "查询失败"
|
||||
// @Success 200 {object} response.Any{data=updater.Status} "更新状态"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "查询失败"
|
||||
// @Router /api/v1/admin/update [get]
|
||||
func GetUpdateStatus(c *gin.Context) {
|
||||
status, _, err := defaultManager.status(c.Request.Context())
|
||||
if err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "[Updater] check release failed: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(status))
|
||||
c.JSON(http.StatusOK, response.OK(status))
|
||||
}
|
||||
|
||||
// ApplyUpdate 下载并应用应用更新
|
||||
@@ -40,22 +39,22 @@ func GetUpdateStatus(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny "升级已准备并即将重启"
|
||||
// @Failure 400 {object} util.ResponseAny "当前版本不可升级"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "升级准备失败"
|
||||
// @Success 200 {object} response.Any "升级已准备并即将重启"
|
||||
// @Failure 400 {object} response.Any "当前版本不可升级"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "升级准备失败"
|
||||
// @Router /api/v1/admin/update/apply [post]
|
||||
func ApplyUpdate(c *gin.Context) {
|
||||
executable, stagedBinary, err := defaultManager.prepareUpgrade(c.Request.Context())
|
||||
if err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "[Updater] prepare upgrade failed: %v", err)
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.InfoF(c.Request.Context(), "[Updater] upgrade prepared; restarting with %s", stagedBinary)
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,7 +16,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// minPasswordLength 密码最小长度
|
||||
const minPasswordLength = 8
|
||||
@@ -57,7 +57,7 @@ type listUsersResponse struct {
|
||||
func parseUserID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(userNotFound))
|
||||
c.JSON(http.StatusBadRequest, response.Err(userNotFound))
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
@@ -90,17 +90,17 @@ func toUser(u model.User) user {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request query listUsersRequest true "查询参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=user.listUsersResponse} "用户列表"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=user.listUsersResponse} "用户列表"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/users [get]
|
||||
// ListUsers 获取用户列表
|
||||
func ListUsers(c *gin.Context) {
|
||||
var req listUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func ListUsers(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,11 +132,11 @@ func ListUsers(c *gin.Context) {
|
||||
Offset(offset).
|
||||
Limit(req.PageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(listUsersResponse{
|
||||
c.JSON(http.StatusOK, response.OK(listUsersResponse{
|
||||
Users: users,
|
||||
Total: total,
|
||||
}))
|
||||
@@ -149,12 +149,12 @@ func ListUsers(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=user.user} "用户详情"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=user.user} "用户详情"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 404 {object} response.Any "用户不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/users/{id} [get]
|
||||
func GetUser(c *gin.Context) {
|
||||
id, ok := parseUserID(c)
|
||||
@@ -169,14 +169,14 @@ func GetUser(c *gin.Context) {
|
||||
Where("id = ?", id).
|
||||
First(&targetUser).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(userNotFound))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(toUser(targetUser)))
|
||||
c.JSON(http.StatusOK, response.OK(toUser(targetUser)))
|
||||
}
|
||||
|
||||
// updateUserStatusRequest 更新用户状态请求
|
||||
@@ -193,17 +193,17 @@ type updateUserStatusRequest struct {
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Param request body updateUserStatusRequest true "状态参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "更新成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限或尝试禁用管理员"
|
||||
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "更新成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限或尝试禁用管理员"
|
||||
// @Failure 404 {object} response.Any "用户不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/users/{id}/status [put]
|
||||
func UpdateUserStatus(c *gin.Context) {
|
||||
var req updateUserStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,15 +222,15 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
Where("id = ?", id).
|
||||
First(&targetUser).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(userNotFound))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsActive && targetUser.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, util.Err(cannotDisable))
|
||||
c.JSON(http.StatusForbidden, response.Err(cannotDisable))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -238,11 +238,11 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_active", req.IsActive).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(updateUserFailed))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(updateUserFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
@@ -252,12 +252,12 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path int true "用户 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限、尝试删除管理员或当前用户"
|
||||
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限、尝试删除管理员或当前用户"
|
||||
// @Failure 404 {object} response.Any "用户不存在"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/users/{id} [delete]
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id, ok := parseUserID(c)
|
||||
@@ -267,7 +267,7 @@ func DeleteUser(c *gin.Context) {
|
||||
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if currUser != nil && currUser.ID == id {
|
||||
c.JSON(http.StatusForbidden, util.Err(cannotDeleteSelf))
|
||||
c.JSON(http.StatusForbidden, response.Err(cannotDeleteSelf))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -281,15 +281,15 @@ func DeleteUser(c *gin.Context) {
|
||||
Where("id = ?", id).
|
||||
First(&targetUser).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||
c.JSON(http.StatusNotFound, response.Err(userNotFound))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if targetUser.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, util.Err(cannotDelete))
|
||||
c.JSON(http.StatusForbidden, response.Err(cannotDelete))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -302,11 +302,11 @@ func DeleteUser(c *gin.Context) {
|
||||
}
|
||||
return tx.Where("id = ?", id).Delete(&model.User{}).Error
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(deleteUserFailed))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(deleteUserFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// createUserRequest 创建用户请求
|
||||
@@ -327,16 +327,16 @@ type createUserRequest struct {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body user.createUserRequest true "创建用户参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=user.user} "创建成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或用户名已存在"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=user.user} "创建成功"
|
||||
// @Failure 400 {object} response.Any "参数错误或用户名已存在"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/users [post]
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req createUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -346,36 +346,36 @@ func CreateUser(c *gin.Context) {
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
|
||||
if req.Username == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err(usernameRequired))
|
||||
c.JSON(http.StatusBadRequest, response.Err(usernameRequired))
|
||||
return
|
||||
}
|
||||
if req.Email == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err(emailRequired))
|
||||
c.JSON(http.StatusBadRequest, response.Err(emailRequired))
|
||||
return
|
||||
}
|
||||
if len(req.Password) < minPasswordLength {
|
||||
c.JSON(http.StatusBadRequest, util.Err(passwordTooShort))
|
||||
c.JSON(http.StatusBadRequest, response.Err(passwordTooShort))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(usernameExists))
|
||||
c.JSON(http.StatusBadRequest, response.Err(usernameExists))
|
||||
return
|
||||
}
|
||||
|
||||
var emailCount int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", req.Email).Count(&emailCount).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if emailCount > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(emailExists))
|
||||
c.JSON(http.StatusBadRequest, response.Err(emailExists))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -393,14 +393,14 @@ func CreateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&newUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(toUser(newUser)))
|
||||
c.JSON(http.StatusOK, response.OK(toUser(newUser)))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -17,7 +16,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -90,7 +90,7 @@ func TestListUsers(t *testing.T) {
|
||||
t.Errorf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func TestListUsers(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -136,7 +136,7 @@ func TestListUsers(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
@@ -194,7 +194,7 @@ func TestGetUser(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func TestUpdateUserStatus(t *testing.T) {
|
||||
t.Errorf("expected 403 Forbidden, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.ErrorMsg != cannotDisable {
|
||||
t.Errorf("expected error message '%s', got '%s'", cannotDisable, resp.ErrorMsg)
|
||||
@@ -327,7 +327,7 @@ func TestCreateUser(t *testing.T) {
|
||||
t.Errorf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
@@ -386,7 +386,7 @@ func TestCreateUser(t *testing.T) {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.ErrorMsg != usernameExists {
|
||||
t.Errorf("expected error '%s', got '%s'", usernameExists, resp.ErrorMsg)
|
||||
@@ -419,7 +419,7 @@ func TestCreateUser(t *testing.T) {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.ErrorMsg != emailExists {
|
||||
t.Errorf("expected error '%s', got '%s'", emailExists, resp.ErrorMsg)
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
caputil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// VerifyMiddleware returns a Gin middleware that checks and consumes the X-Cap-Token header.
|
||||
// enabledFunc is an optional callback allowing dynamic check of whether captcha protection is turned on.
|
||||
@@ -22,13 +21,13 @@ func VerifyMiddleware(mgr *caputil.Manager, scope string, enabledFunc func() boo
|
||||
|
||||
token := c.GetHeader("X-Cap-Token")
|
||||
if token == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(errCapTokenMissing))
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, response.Err(errCapTokenMissing))
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := mgr.VerifyToken(c.Request.Context(), token, scope)
|
||||
if err != nil || !valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(errCapTokenInvalidOrExpired))
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, response.Err(errCapTokenInvalidOrExpired))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ type redeemRequest struct {
|
||||
// @Produce json
|
||||
// @Param request body challengeRequest false "可选范围限制参数"
|
||||
// @Success 200 {object} cap.ChallengeResponse "成功返回 PoW 难题"
|
||||
// @Failure 500 {object} cap.RedeemResponse "内部服务错误"
|
||||
// @Failure 500 {object} capService.RedeemResponse "内部服务错误"
|
||||
// @Router /api/cap/challenge [post]
|
||||
func Challenge(c *gin.Context) {
|
||||
var req challengeRequest
|
||||
@@ -58,9 +58,9 @@ func Challenge(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body redeemRequest true "难题 Token 与解答 solutions 数组"
|
||||
// @Success 200 {object} cap.RedeemResponse "核销成功,返回 X-Cap-Token"
|
||||
// @Failure 400 {object} cap.RedeemResponse "参数错误或核销失败"
|
||||
// @Failure 500 {object} cap.RedeemResponse "内部服务错误"
|
||||
// @Success 200 {object} capService.RedeemResponse "核销成功,返回 X-Cap-Token"
|
||||
// @Failure 400 {object} capService.RedeemResponse "参数错误或核销失败"
|
||||
// @Failure 500 {object} capService.RedeemResponse "内部服务错误"
|
||||
// @Router /api/cap/redeem [post]
|
||||
func Redeem(c *gin.Context) {
|
||||
var req redeemRequest
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -14,11 +13,11 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
capUtil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func TestCapEndpointsAndMiddleware(t *testing.T) {
|
||||
sqliteDB, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
@@ -42,7 +41,7 @@ func TestCapEndpointsAndMiddleware(t *testing.T) {
|
||||
}
|
||||
return enabled
|
||||
}), func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK("login success"))
|
||||
c.JSON(http.StatusOK, response.OK("login success"))
|
||||
})
|
||||
|
||||
// 1. Test challenge generation
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
// Package config 提供公开配置查询接口
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// GetPublicConfig 获取公共配置
|
||||
// @Summary 获取公共配置
|
||||
@@ -19,22 +18,22 @@ import (
|
||||
// @Tags config
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny
|
||||
// @Success 200 {object} response.Any
|
||||
// @Router /api/v1/config/public [get]
|
||||
func GetPublicConfig(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
configs, err := model.ListVisibleSystemConfigs(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response := make(map[string]string, len(configs))
|
||||
resp := make(map[string]string, len(configs))
|
||||
for _, config := range configs {
|
||||
response[config.Key] = config.Value
|
||||
resp[config.Key] = config.Value
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
c.JSON(http.StatusOK, response.OK(resp))
|
||||
}
|
||||
|
||||
// GetRobotsTXT 动态生成 robots.txt
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
import ("encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func TestGetPublicConfigUsesVisibility(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
@@ -47,7 +46,7 @@ func TestGetPublicConfigUsesVisibility(t *testing.T) {
|
||||
t.Fatalf("GetPublicConfig() status = %d, want %d; body = %s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("json.Unmarshal(GetPublicConfig()) error = %v", err)
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@
|
||||
// Package custom provides custom business handlers
|
||||
package custom
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// Hello is a sample handler for custom business logic
|
||||
// @Summary Sample Hello API
|
||||
// @Description A sample business API for customization
|
||||
// @Tags custom
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "成功"
|
||||
// @Success 200 {object} response.Any{data=string} "成功"
|
||||
// @Router /api/v1/custom/hello [get]
|
||||
func Hello(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK("Hello from custom business module!"))
|
||||
c.JSON(http.StatusOK, response.OK("Hello from custom business module!"))
|
||||
}
|
||||
|
||||
@@ -5,20 +5,19 @@
|
||||
// Package health 提供健康检查端点
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// Health 健康检查
|
||||
// @Summary 健康检查
|
||||
// @Description 检查服务是否正常运行,可用于负载均衡存活探测
|
||||
// @Tags health
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "服务正常"
|
||||
// @Success 200 {object} response.Any{data=string} "服务正常"
|
||||
// @Router /api/health [get]
|
||||
func Health(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ func setupTestRouter(dbConn *gorm.DB, mockRedis *mockRedisClient, mockClient *ht
|
||||
r.Use(mockContextMiddleware(mockClient))
|
||||
|
||||
store := cookie.NewStore([]byte(config.Config.App.SessionSecret))
|
||||
store.Options(util.GetSessionOptions(3600))
|
||||
store.Options(GetSessionOptions(3600))
|
||||
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, store))
|
||||
|
||||
db.SetDB(dbConn)
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// BasicUserInfo 用户基本信息结构体
|
||||
type BasicUserInfo struct {
|
||||
@@ -54,8 +54,8 @@ func BuildBasicUserInfo(user *model.User, needChange bool) BasicUserInfo {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "用户信息"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "用户信息"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/oauth/user-info [get]
|
||||
// @Router /api/v1/user-info [get]
|
||||
// @Router /api/v1/user/self [get]
|
||||
@@ -66,7 +66,7 @@ func UserInfo(c *gin.Context) {
|
||||
|
||||
c.JSON(
|
||||
http.StatusOK,
|
||||
util.OK(BuildBasicUserInfo(user, needChange)),
|
||||
response.OK(BuildBasicUserInfo(user, needChange)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ func UserInfo(c *gin.Context) {
|
||||
// @Description 生成 OAuth 登录 URL,前端跳转至该地址完成授权。返回的 URL 中包含 state 参数用于 CSRF 防护。
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "OAuth 登录 URL"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常或内部错误"
|
||||
// @Success 200 {object} response.Any{data=string} "OAuth 登录 URL"
|
||||
// @Failure 500 {object} response.Any "Redis 异常或内部错误"
|
||||
// @Router /api/v1/oauth/login [get]
|
||||
|
||||
// Logout 退出登录
|
||||
@@ -85,8 +85,8 @@ func UserInfo(c *gin.Context) {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "退出成功"
|
||||
// @Failure 500 {object} util.ResponseAny "Session 清除失败"
|
||||
// @Success 200 {object} response.Any{data=string} "退出成功"
|
||||
// @Failure 500 {object} response.Any "Session 清除失败"
|
||||
// @Router /api/v1/oauth/logout [get]
|
||||
func Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
@@ -95,11 +95,11 @@ func Logout(c *gin.Context) {
|
||||
if userID != nil {
|
||||
logger.InfoF(c.Request.Context(), "[LoginAudit] user logged out: %v, ID: %v, IP: %s", username, userID, c.ClientIP())
|
||||
}
|
||||
session.Options(util.GetSessionOptions(-1))
|
||||
session.Options(GetSessionOptions(-1))
|
||||
session.Clear()
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
// Package oauth provides authentication and OAuth integration.
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -27,6 +25,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
// AuthSourceView 登录源展示信息
|
||||
@@ -215,14 +214,14 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
|
||||
isSessionCookie = true
|
||||
}
|
||||
}
|
||||
session.Options(util.GetSessionOptions(maxAge))
|
||||
session.Options(GetSessionOptions(maxAge))
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isSessionCookie {
|
||||
util.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -358,10 +357,10 @@ func buildCallbackResult(user *model.User, status string) OAuthCallbackResult {
|
||||
// @Description 返回当前系统已启用的所有 OAuth 登录源,前端展示登录按钮列表时调用
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Success 200 {object} util.ResponseAny{data=[]oauth.AuthSourceView} "登录源列表"
|
||||
// @Success 200 {object} response.Any{data=[]oauth.AuthSourceView} "登录源列表"
|
||||
// @Router /api/v1/oauth/sources [get]
|
||||
func GetLoginSources(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, util.OK(activeLoginSources(c.Request.Context())))
|
||||
c.JSON(http.StatusOK, response.OK(activeLoginSources(c.Request.Context())))
|
||||
}
|
||||
|
||||
// GetLoginURL 获取登录授权地址
|
||||
@@ -370,25 +369,25 @@ func GetLoginSources(c *gin.Context) {
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Param source query string false "认证源名称,为空使用第一个启用的认证源"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} util.ResponseAny "认证源不存在或未配置"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常 or 构造 URL 失败"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} response.Any "认证源不存在或未配置"
|
||||
// @Failure 500 {object} response.Any "Redis 异常 or 构造 URL 失败"
|
||||
// @Router /api/v1/oauth/login [get]
|
||||
func GetLoginURL(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, c.Query("source"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -396,7 +395,7 @@ func GetLoginURL(c *gin.Context) {
|
||||
token, isNew := ensureSessionToken(session)
|
||||
if isNew {
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -412,20 +411,20 @@ func GetLoginURL(c *gin.Context) {
|
||||
SessionHash: sessionHash,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authorizeURL, err := buildAuthorizeURL(c.Request.Context(), source, state)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
c.JSON(http.StatusOK, response.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
}
|
||||
|
||||
func buildAuthorizeURL(ctx context.Context, source *model.AuthSource, state string) (string, error) {
|
||||
@@ -450,25 +449,25 @@ func buildAuthorizeURL(ctx context.Context, source *model.AuthSource, state stri
|
||||
// @Produce json
|
||||
// @Param source path string true "认证源名称"
|
||||
// @Param purpose query string false "授权目的:login(登录)或 bind(绑定账号),默认 login"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} util.ResponseAny "认证源不存在或未启用"
|
||||
// @Failure 500 {object} util.ResponseAny "Redis 异常或构造 URL 失败"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthAuthorizeResponse} "授权 URL"
|
||||
// @Failure 400 {object} response.Any "认证源不存在或未启用"
|
||||
// @Failure 500 {object} response.Any "Redis 异常或构造 URL 失败"
|
||||
// @Router /api/v1/oauth/{source}/authorize [get]
|
||||
func Authorize(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, c.Param("source"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
purpose := strings.ToLower(strings.TrimSpace(c.Query("purpose")))
|
||||
@@ -479,14 +478,14 @@ func Authorize(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userID := GetUserIDFromSession(session)
|
||||
if purpose == OAuthPurposeBind && userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
token, isNew := ensureSessionToken(session)
|
||||
if isNew {
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -501,20 +500,20 @@ func Authorize(c *gin.Context) {
|
||||
SessionHash: sessionHash,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := db.Redis.Set(c.Request.Context(), db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, state)), payloadValue, OAuthStateCacheKeyExpiration).Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authorizeURL, err := buildAuthorizeURL(c.Request.Context(), source, state)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
c.JSON(http.StatusOK, response.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
|
||||
}
|
||||
|
||||
// Callback OAuth 回调处理
|
||||
@@ -524,15 +523,15 @@ func Authorize(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body oauth.CallbackRequest true "回调请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.OAuthCallbackResult} "登录或绑定成功"
|
||||
// @Failure 400 {object} util.ResponseAny "state 无效、参数错误或认证源错误"
|
||||
// @Failure 401 {object} util.ResponseAny "绑定场景未登录"
|
||||
// @Failure 500 {object} util.ResponseAny "OAuth 认证失败或内部错误"
|
||||
// @Success 200 {object} response.Any{data=oauth.OAuthCallbackResult} "登录或绑定成功"
|
||||
// @Failure 400 {object} response.Any "state 无效、参数错误或认证源错误"
|
||||
// @Failure 401 {object} response.Any "绑定场景未登录"
|
||||
// @Failure 500 {object} response.Any "OAuth 认证失败或内部错误"
|
||||
// @Router /api/v1/oauth/callback [post]
|
||||
func Callback(c *gin.Context) {
|
||||
var req CallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -540,14 +539,14 @@ func Callback(c *gin.Context) {
|
||||
stateKey := db.PrefixedKey(fmt.Sprintf(OAuthStateCacheKeyFormat, req.State))
|
||||
payloadRaw, err := db.Redis.Get(ctx, stateKey).Result()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errInvalidState))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errInvalidState))
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Del(ctx, stateKey)
|
||||
|
||||
payload, err := decodeOAuthStatePayload(payloadRaw)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -555,55 +554,55 @@ func Callback(c *gin.Context) {
|
||||
currentUserID := GetUserIDFromSession(session)
|
||||
|
||||
if payload.Purpose == OAuthPurposeBind && currentUserID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
|
||||
token, ok := session.Get(SessionTokenKey).(string)
|
||||
if !ok || token == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err("invalid session context"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("invalid session context"))
|
||||
return
|
||||
}
|
||||
|
||||
if hashSessionToken(token) != payload.SessionHash {
|
||||
c.JSON(http.StatusBadRequest, util.Err("session mismatch for oauth state"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("session mismatch for oauth state"))
|
||||
return
|
||||
}
|
||||
|
||||
if payload.Purpose == OAuthPurposeBind && currentUserID != payload.UserID {
|
||||
c.JSON(http.StatusBadRequest, util.Err("user context mismatch for oauth binding"))
|
||||
c.JSON(http.StatusBadRequest, response.Err("user context mismatch for oauth binding"))
|
||||
return
|
||||
}
|
||||
|
||||
if !isOIDCLoginEnabled(ctx) {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := resolveAuthSource(ctx, payload.SourceName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !source.IsActive {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errAuthSourceDisabled))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errAuthSourceDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := getFrontendLoginRedirectURL(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, err := buildOAuthUserInfo(ctx, source, req.Code, req.State, redirectURL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := normalizeOAuthUserInfo(userInfo); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if userInfo.Sub == "" {
|
||||
@@ -622,12 +621,12 @@ func Callback(c *gin.Context) {
|
||||
func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthSource, userInfo *model.OAuthUserInfo) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := db.DB(ctx).First(&user, "id = ?", userID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
@@ -637,12 +636,12 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS
|
||||
ExternalUsername: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
user.LastLoginAt = time.Now()
|
||||
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(&user, "bound")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(&user, "bound")))
|
||||
}
|
||||
|
||||
// handleCallbackLogin 处理 OAuth 回调中的登录流程(查找已有帐号或自动注册)
|
||||
@@ -653,7 +652,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
switch {
|
||||
case err == nil:
|
||||
if err := db.DB(ctx).First(&user, "id = ?", account.UserID).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
@@ -663,14 +662,14 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
}
|
||||
user = newUser
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user.LastLoginAt = time.Now()
|
||||
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
||||
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -678,7 +677,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(&user, "logged_in")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(&user, "logged_in")))
|
||||
}
|
||||
|
||||
// handleCallbackRegister 处理 OAuth 回调中的自动注册流程
|
||||
@@ -690,20 +689,20 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
}
|
||||
|
||||
if !registrationEnabled {
|
||||
c.JSON(http.StatusOK, util.OK(buildCallbackResult(nil, "need_bind")))
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(nil, "need_bind")))
|
||||
return model.User{}, false
|
||||
}
|
||||
|
||||
username, uniqueErr := uniqueUsername(ctx, userInfo.Username)
|
||||
if uniqueErr != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(uniqueErr.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(uniqueErr.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
userInfo.Username = username
|
||||
|
||||
var user model.User
|
||||
if err := user.CreateUser(ctx, db.DB(ctx), userInfo); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
if err := model.BindExternalAccount(ctx, &model.ExternalAccount{
|
||||
@@ -713,7 +712,7 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
ExternalUsername: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return model.User{}, false
|
||||
}
|
||||
logger.InfoF(ctx, "[LoginAudit] successful OAuth registration via source: %s, external ID: %s, user: %s, ID: %d, IP: %s", source.Name, userInfo.Sub, user.Username, user.ID, c.ClientIP())
|
||||
@@ -727,18 +726,18 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.ExternalAccountView} "外部帐号列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]model.ExternalAccountView} "外部帐号列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/oauth/external-accounts [get]
|
||||
func ListExternalAccounts(c *gin.Context) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
accounts, err := model.ListExternalAccountsByUserID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(accounts))
|
||||
c.JSON(http.StatusOK, response.OK(accounts))
|
||||
}
|
||||
|
||||
// DeleteExternalAccount 解除外部帐号绑定
|
||||
@@ -748,25 +747,25 @@ func ListExternalAccounts(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param id path uint64 true "外部帐号绑定记录 ID"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "解除绑定成功"
|
||||
// @Failure 400 {object} util.ResponseAny "ID 无效或解除失败"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=string} "解除绑定成功"
|
||||
// @Failure 400 {object} response.Any "ID 无效或解除失败"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/oauth/external-accounts/{id}/delete [post]
|
||||
func DeleteExternalAccount(c *gin.Context) {
|
||||
userID := GetUserIDFromContext(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(common.UnAuthorized))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(common.UnAuthorized))
|
||||
return
|
||||
}
|
||||
rawID := strings.TrimSpace(c.Param("id"))
|
||||
id, err := strconv.ParseUint(rawID, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(errInvalidExternalAccountBindingID))
|
||||
c.JSON(http.StatusBadRequest, response.Err(errInvalidExternalAccountBindingID))
|
||||
return
|
||||
}
|
||||
if err := model.DeleteExternalAccountForUser(c.Request.Context(), id, userID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// Package risk_control 提供风险控制中间件
|
||||
package risk_control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
import ("encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +14,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
// RiskControlMiddleware 全局日志采集中间件
|
||||
func RiskControlMiddleware() gin.HandlerFunc {
|
||||
@@ -28,7 +28,7 @@ func RiskControlMiddleware() gin.HandlerFunc {
|
||||
|
||||
// 1. 限流背压检测(检测本地缓冲队列是否已满)
|
||||
if IsBufferFull() {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, util.Err("系统繁忙,请稍后再试"))
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, response.Err("系统繁忙,请稍后再试"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
@@ -46,16 +47,16 @@ type listFilesResponse struct {
|
||||
// @Param extension query string false "扩展名过滤"
|
||||
// @Param user_id query uint64 false "上传用户 ID"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=listFilesResponse} "查询成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Success 200 {object} response.Any{data=listFilesResponse} "查询成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Router /api/v1/admin/uploads [get]
|
||||
func ListFiles(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req listFilesRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidParams))
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
@@ -83,18 +84,18 @@ func ListFiles(c *gin.Context) {
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryFileCountFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryFileCountFailed))
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryFileListFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryFileListFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(listFilesResponse{
|
||||
c.JSON(http.StatusOK, response.OK(listFilesResponse{
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
@@ -109,20 +110,20 @@ func ListFiles(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param id path string true "文件 ID"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny "删除成功"
|
||||
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||||
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Failure 403 {object} response.Any "无权操作"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Router /api/v1/admin/uploads/{id} [delete]
|
||||
func DeleteFile(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
c.JSON(http.StatusConflict, response.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
|
||||
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidFileID))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,14 +133,14 @@ func DeleteFile(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrDeleteFileFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrDeleteFileFailed))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
// GetDistinctUploadTypes 获取数据库中所有已存在的文件业务类型
|
||||
@@ -148,10 +149,10 @@ func DeleteFile(c *gin.Context) {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]string} "业务类型列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=[]string} "业务类型列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/uploads/types [get]
|
||||
func GetDistinctUploadTypes(c *gin.Context) {
|
||||
var dbTypes []string
|
||||
@@ -159,11 +160,11 @@ func GetDistinctUploadTypes(c *gin.Context) {
|
||||
Where("type IS NOT NULL AND type != ''").
|
||||
Distinct().
|
||||
Pluck("type", &dbTypes).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
c.JSON(http.StatusInternalServerError, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
sort.Strings(dbTypes)
|
||||
c.JSON(http.StatusOK, util.OK(dbTypes))
|
||||
c.JSON(http.StatusOK, response.OK(dbTypes))
|
||||
}
|
||||
|
||||
type listMyFilesRequest struct {
|
||||
@@ -192,8 +193,8 @@ type listMyFilesResponse struct {
|
||||
// @Param type query string false "业务分类过滤"
|
||||
// @Param extension query string false "扩展名过滤"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=listMyFilesResponse} "查询成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=listMyFilesResponse} "查询成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/upload/my [get]
|
||||
func ListMyFiles(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
@@ -201,7 +202,7 @@ func ListMyFiles(c *gin.Context) {
|
||||
|
||||
var req listMyFilesRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidParams))
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
@@ -226,18 +227,18 @@ func ListMyFiles(c *gin.Context) {
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryFileCountFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryFileCountFailed))
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryFileListFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryFileListFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(listMyFilesResponse{
|
||||
c.JSON(http.StatusOK, response.OK(listMyFilesResponse{
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
@@ -252,21 +253,21 @@ func ListMyFiles(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param id path string true "文件 ID"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny "删除成功"
|
||||
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||||
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||
// @Success 200 {object} response.Any "删除成功"
|
||||
// @Failure 403 {object} response.Any "无权操作"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Router /api/v1/upload/{id} [delete]
|
||||
func DeleteMyFile(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
c.JSON(http.StatusConflict, response.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
|
||||
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidFileID))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,7 +277,7 @@ func DeleteMyFile(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -286,10 +287,10 @@ func DeleteMyFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrDeleteFileFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrDeleteFileFailed))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
type updateMyFileRequest struct {
|
||||
@@ -306,27 +307,27 @@ type updateMyFileRequest struct {
|
||||
// @Param id path string true "文件 ID"
|
||||
// @Param request body updateMyFileRequest true "更新字段"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Upload} "更新成功"
|
||||
// @Failure 403 {object} util.ResponseAny "无权操作"
|
||||
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||
// @Success 200 {object} response.Any{data=model.Upload} "更新成功"
|
||||
// @Failure 403 {object} response.Any "无权操作"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Router /api/v1/upload/{id} [put]
|
||||
func UpdateMyFile(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
c.JSON(http.StatusConflict, response.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
|
||||
uploadID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidFileID))
|
||||
return
|
||||
}
|
||||
|
||||
var req updateMyFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidParams))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -336,7 +337,7 @@ func UpdateMyFile(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -355,10 +356,10 @@ func UpdateMyFile(c *gin.Context) {
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := db.DB(ctx).Model(&upload).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err("更新文件记录失败"))
|
||||
c.JSON(http.StatusOK, response.Err("更新文件记录失败"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(upload))
|
||||
c.JSON(http.StatusOK, response.OK(upload))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -34,10 +33,10 @@ import (
|
||||
// @Param id path string true "文件 ID"
|
||||
// @Param quality query string false "图片质量 (low, medium, high, origin),默认为 origin"
|
||||
// @Success 200 {file} file "成功获取文件内容"
|
||||
// @Failure 400 {object} util.ResponseAny "文件 ID 格式错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 404 {object} util.ResponseAny "文件未找到"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Failure 400 {object} response.Any "文件 ID 格式错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 404 {object} response.Any "文件未找到"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /f/{id} [get]
|
||||
func ServeFileByID(c *gin.Context) {
|
||||
upload, err := getUploadRecordByID(c)
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
package upload
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
import ("archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
@@ -32,6 +31,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
type batchDownloadRequest struct {
|
||||
@@ -48,10 +48,10 @@ type batchDownloadRequest struct {
|
||||
// @Param type formData string false "业务分类 (例如: avatar, attachment, doc,默认为 generic)"
|
||||
// @Param metadata formData string false "额外的 JSON 格式元数据"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=model.Upload} "上传成功"
|
||||
// @Failure 400 {object} util.ResponseAny "请求参数错误或文件受限"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=model.Upload} "上传成功"
|
||||
// @Failure 400 {object} response.Any "请求参数错误或文件受限"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/upload [post]
|
||||
//
|
||||
//nolint:revive
|
||||
@@ -67,20 +67,20 @@ func UploadFile(c *gin.Context) {
|
||||
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrNoFileSelected))
|
||||
c.JSON(http.StatusOK, response.Err(ErrNoFileSelected))
|
||||
return
|
||||
}
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrOpenFileFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrOpenFileFailed))
|
||||
return
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
// 校验大小
|
||||
if header.Size > maxUploadSize {
|
||||
c.JSON(http.StatusOK, util.Err(ErrGenericFileTooLarge))
|
||||
c.JSON(http.StatusOK, response.Err(ErrGenericFileTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func UploadFile(c *gin.Context) {
|
||||
|
||||
// 3. 校验文件后缀是否在允许的系统配置列表中
|
||||
if errMsg := validateUploadExtension(ctx, ext); errMsg != "" {
|
||||
c.JSON(http.StatusOK, util.Err(errMsg))
|
||||
c.JSON(http.StatusOK, response.Err(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func UploadFile(c *gin.Context) {
|
||||
var buf bytes.Buffer
|
||||
size, err := io.Copy(&buf, io.TeeReader(file, hashWriter))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrProcessFileFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrProcessFileFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func UploadFile(c *gin.Context) {
|
||||
|
||||
// 校验真实 MIME Type 是否与常见图片扩展名匹配,防止 Polyglot / HTML 注入攻击
|
||||
if isImageExtension(ext) && !strings.HasPrefix(mimeType, "image/") {
|
||||
c.JSON(http.StatusOK, util.Err(ErrFileContentExtensionMismatch))
|
||||
c.JSON(http.StatusOK, response.Err(ErrFileContentExtensionMismatch))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func UploadFile(c *gin.Context) {
|
||||
var err error
|
||||
accessMode, err = strconv.Atoi(accessModeStr)
|
||||
if err != nil || (accessMode != 0 && accessMode != 1) {
|
||||
c.JSON(http.StatusOK, util.Err("无效的 access_mode 参数"))
|
||||
c.JSON(http.StatusOK, response.Err("无效的 access_mode 参数"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -140,14 +140,14 @@ func UploadFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if lookupErr != nil && !errors.Is(lookupErr, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusOK, util.Err(ErrFileValidationFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrFileValidationFailed))
|
||||
return
|
||||
}
|
||||
|
||||
// 7. 解析可选元数据字段
|
||||
meta, errMsg := parseUploadMetadata(c, mimeType)
|
||||
if errMsg != "" {
|
||||
c.JSON(http.StatusOK, util.Err(errMsg))
|
||||
c.JSON(http.StatusOK, response.Err(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func UploadFile(c *gin.Context) {
|
||||
// 8. 写入当前活动存储驱动。
|
||||
storageDriver, subPath, errMsg := storeUploadFile(ctx, subPath, size, mimeType, &buf, &meta)
|
||||
if errMsg != "" {
|
||||
c.JSON(http.StatusOK, util.Err(errMsg))
|
||||
c.JSON(http.StatusOK, response.Err(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,11 +179,11 @@ func UploadFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := saveUploadRecord(ctx, &newUpload, storageDriver, subPath); err != "" {
|
||||
c.JSON(http.StatusOK, util.Err(err))
|
||||
c.JSON(http.StatusOK, response.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(newUpload))
|
||||
c.JSON(http.StatusOK, response.OK(newUpload))
|
||||
}
|
||||
|
||||
// DownloadFile 通用单文件下载接口
|
||||
@@ -195,9 +195,9 @@ func UploadFile(c *gin.Context) {
|
||||
// @Param quality query string false "图片质量 (low, medium, high, origin),默认为 origin"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {file} file "成功下载文件"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 404 {object} util.ResponseAny "文件不存在"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/admin/uploads/download/{id} [get]
|
||||
func DownloadFile(c *gin.Context) {
|
||||
upload, err := getUploadRecordByID(c)
|
||||
@@ -207,10 +207,10 @@ func DownloadFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if _, ok := err.(*strconv.NumError); ok {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidFileID))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidFileID))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.Err(ErrQueryUploadRecordFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrQueryUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,15 +247,15 @@ func DownloadFile(c *gin.Context) {
|
||||
// @Param request body upload.batchDownloadRequest true "包含文件 ID 数组 of string 的请求体"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {file} file "成功下载打包后的 ZIP"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Failure 500 {object} util.ResponseAny "打包失败"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Failure 500 {object} response.Any "打包失败"
|
||||
// @Router /api/v1/admin/uploads/download/batch [post]
|
||||
func BatchDownloadFiles(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req batchDownloadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrInvalidBatchDownloadRequest))
|
||||
c.JSON(http.StatusOK, response.Err(ErrInvalidBatchDownloadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
for _, idStr := range req.IDs {
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(fmt.Sprintf(ErrInvalidIDValueFormat, idStr)))
|
||||
c.JSON(http.StatusOK, response.Err(fmt.Sprintf(ErrInvalidIDValueFormat, idStr)))
|
||||
return
|
||||
}
|
||||
ids = append(ids, id)
|
||||
@@ -273,12 +273,12 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
// 查库获取所有匹配且正常的文件记录
|
||||
var uploads []model.Upload
|
||||
if err := db.DB(ctx).Where("id IN ? AND status IN (?, ?)", ids, model.UploadStatusPending, model.UploadStatusUsed).Find(&uploads).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrRetrieveUploadRecordsFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrRetrieveUploadRecordsFailed))
|
||||
return
|
||||
}
|
||||
|
||||
if len(uploads) == 0 {
|
||||
c.JSON(http.StatusOK, util.Err(ErrNoValidFilesForArchive))
|
||||
c.JSON(http.StatusOK, response.Err(ErrNoValidFilesForArchive))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
return false, err
|
||||
}
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
c.JSON(http.StatusConflict, response.Err(ErrStorageReadOnly))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -385,12 +385,12 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&newUpload).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveUploadRecordFailed))
|
||||
c.JSON(http.StatusOK, response.Err(ErrSaveUploadRecordFailed))
|
||||
return true, err
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", id, existing.FilePath)
|
||||
c.JSON(http.StatusOK, util.OK(newUpload))
|
||||
c.JSON(http.StatusOK, response.OK(newUpload))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
|
||||
package upload
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
const (
|
||||
catImage = "图片"
|
||||
@@ -49,10 +48,10 @@ type fileStatsResponse struct {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=fileStatsResponse} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Success 200 {object} response.Any{data=fileStatsResponse} "获取成功"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Failure 403 {object} response.Any "无管理员权限"
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/uploads/stats [get]
|
||||
func GetFileStats(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
@@ -67,7 +66,7 @@ func GetFileStats(c *gin.Context) {
|
||||
Where("status != ?", model.UploadStatusDeleted).
|
||||
Scan(&summary).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ func GetFileStats(c *gin.Context) {
|
||||
Group("type").
|
||||
Scan(&typeRaw).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,7 +112,7 @@ func GetFileStats(c *gin.Context) {
|
||||
Where("status != ?", model.UploadStatusDeleted).
|
||||
Scan(&fileRaws).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,7 +153,7 @@ func GetFileStats(c *gin.Context) {
|
||||
Where("status != ? AND created_at >= ?", model.UploadStatusDeleted, startTime).
|
||||
Scan(&trendRaws).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -185,7 +184,7 @@ func GetFileStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(fileStatsResponse{
|
||||
c.JSON(http.StatusOK, response.OK(fileStatsResponse{
|
||||
TotalCount: summary.TotalCount,
|
||||
TotalSize: summary.TotalSize,
|
||||
Trend: trend,
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
// Package user 提供用户认证与帐户管理功能
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
import ("net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -15,7 +14,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
type createTokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
@@ -33,8 +33,8 @@ type tokenResponse struct {
|
||||
// @Tags user
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.AccessToken} "令牌列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=[]model.AccessToken} "令牌列表"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/user/access-tokens [get]
|
||||
// ListAccessTokens 获取当前用户的 AccessToken 列表
|
||||
func ListAccessTokens(c *gin.Context) {
|
||||
@@ -43,11 +43,11 @@ func ListAccessTokens(c *gin.Context) {
|
||||
|
||||
var tokens []model.AccessToken
|
||||
if err := db.DB(ctx).Where("user_id = ?", currUser.ID).Order("created_at desc").Find(&tokens).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tokens))
|
||||
c.JSON(http.StatusOK, response.OK(tokens))
|
||||
}
|
||||
|
||||
// CreateAccessToken 创建一个新的 AccessToken
|
||||
@@ -58,8 +58,8 @@ func ListAccessTokens(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param request body user.createTokenRequest true "令牌名称"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=user.tokenResponse} "新建令牌成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或超限"
|
||||
// @Success 200 {object} response.Any{data=user.tokenResponse} "新建令牌成功"
|
||||
// @Failure 400 {object} response.Any "参数错误或超限"
|
||||
// @Router /api/v1/user/access-tokens [post]
|
||||
func CreateAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
@@ -67,19 +67,19 @@ func CreateAccessToken(c *gin.Context) {
|
||||
|
||||
var req createTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errBindParamsFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errBindParamsFailed))
|
||||
return
|
||||
}
|
||||
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errTokenNameRequired))
|
||||
c.JSON(http.StatusOK, response.Err(errTokenNameRequired))
|
||||
return
|
||||
}
|
||||
|
||||
// 只有管理员才能创建具有管理员权限的令牌
|
||||
if req.IsAdmin && !currUser.IsAdmin {
|
||||
c.JSON(http.StatusOK, util.Err(errAdminTokenRequiresAdmin))
|
||||
c.JSON(http.StatusOK, response.Err(errAdminTokenRequiresAdmin))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,19 +91,19 @@ func CreateAccessToken(c *gin.Context) {
|
||||
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.AccessToken{}).Where("user_id = ?", currUser.ID).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if int(count) >= maxLimit {
|
||||
c.JSON(http.StatusOK, util.Err(errAccessTokenLimitReached))
|
||||
c.JSON(http.StatusOK, response.Err(errAccessTokenLimitReached))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 Token
|
||||
tokenStr, err := model.GenerateTokenString()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errGenerateTokenFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errGenerateTokenFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ func CreateAccessToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&tokenRecord).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tokenResponse{
|
||||
c.JSON(http.StatusOK, response.OK(tokenResponse{
|
||||
Token: tokenStr,
|
||||
Record: tokenRecord,
|
||||
}))
|
||||
@@ -136,8 +136,8 @@ func CreateAccessToken(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param id path string true "令牌ID"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Success 200 {object} response.Any{data=string} "删除成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/user/access-tokens/{id} [delete]
|
||||
func DeleteAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
@@ -146,22 +146,22 @@ func DeleteAccessToken(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errInvalidTokenID))
|
||||
c.JSON(http.StatusOK, response.Err(errInvalidTokenID))
|
||||
return
|
||||
}
|
||||
|
||||
tx := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).Delete(&model.AccessToken{})
|
||||
if tx.Error != nil {
|
||||
c.JSON(http.StatusOK, util.Err(tx.Error.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(tx.Error.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if tx.RowsAffected == 0 {
|
||||
c.JSON(http.StatusOK, util.Err(errTokenNotFoundOrForbidden))
|
||||
c.JSON(http.StatusOK, response.Err(errTokenNotFoundOrForbidden))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK("删除成功"))
|
||||
c.JSON(http.StatusOK, response.OK("删除成功"))
|
||||
}
|
||||
|
||||
// RotateAccessToken 轮换一个 AccessToken
|
||||
@@ -171,8 +171,8 @@ func DeleteAccessToken(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param id path string true "令牌ID"
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=user.tokenResponse} "令牌轮换成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Success 200 {object} response.Any{data=user.tokenResponse} "令牌轮换成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/user/access-tokens/{id}/rotate [post]
|
||||
func RotateAccessToken(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
@@ -181,20 +181,20 @@ func RotateAccessToken(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errInvalidTokenID))
|
||||
c.JSON(http.StatusOK, response.Err(errInvalidTokenID))
|
||||
return
|
||||
}
|
||||
|
||||
var tokenRecord model.AccessToken
|
||||
if err := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).First(&tokenRecord).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errTokenNotFoundOrForbidden))
|
||||
c.JSON(http.StatusOK, response.Err(errTokenNotFoundOrForbidden))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的 Token
|
||||
newTokenStr, err := model.GenerateTokenString()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errGenerateTokenFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errGenerateTokenFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,11 +205,11 @@ func RotateAccessToken(c *gin.Context) {
|
||||
tokenRecord.MaskedToken = newMaskedToken
|
||||
|
||||
if err := db.DB(ctx).Save(&tokenRecord).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(tokenResponse{
|
||||
c.JSON(http.StatusOK, response.OK(tokenResponse{
|
||||
Token: newTokenStr,
|
||||
Record: tokenRecord,
|
||||
}))
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -22,7 +21,8 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
pkgu "github.com/Rain-kl/Wavelet/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
type sendEmailCodeRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
@@ -139,7 +139,7 @@ func verifyEmailCode(ctx context.Context, email, scene, code string) bool {
|
||||
func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *loginRequest, user *model.User) error {
|
||||
if req.Code != "" {
|
||||
if !verifyEmailCode(ctx, user.Email, "login", req.Code) {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailCodeInvalidOrExpired))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailCodeInvalidOrExpired))
|
||||
return errors.New("handled")
|
||||
}
|
||||
return nil
|
||||
@@ -149,7 +149,7 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
|
||||
if !isSMTPConfigured(ctx) || user.Email == "" {
|
||||
codeKey := getEmailCodeKey("login", user.Email)
|
||||
if err := db.SetJSON(ctx, codeKey, "888888", emailCodeExpiry); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errGenerateEmailCodeFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errGenerateEmailCodeFailed))
|
||||
return errors.New("handled")
|
||||
}
|
||||
var msg string
|
||||
@@ -158,7 +158,7 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
|
||||
} else {
|
||||
msg = errSMTPInvalidUseTempCodePrefix + "该账号未绑定邮箱,使用临时码登录"
|
||||
}
|
||||
c.JSON(http.StatusOK, util.Err(msg))
|
||||
c.JSON(http.StatusOK, response.Err(msg))
|
||||
return errors.New("handled")
|
||||
}
|
||||
|
||||
@@ -167,13 +167,13 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
|
||||
err := db.GetJSON(ctx, cooldownKey, &temp)
|
||||
if err != nil {
|
||||
if err := sendEmailVerificationCode(ctx, user.Email, "login", "login_email"); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return errors.New("handled")
|
||||
}
|
||||
}
|
||||
|
||||
maskedEmail := pkgu.MaskEmail(user.Email)
|
||||
c.JSON(http.StatusOK, util.Err(errNeedEmailCodePrefix+maskedEmail))
|
||||
c.JSON(http.StatusOK, response.Err(errNeedEmailCodePrefix+maskedEmail))
|
||||
return errors.New("handled")
|
||||
}
|
||||
|
||||
@@ -184,24 +184,24 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.sendEmailCodeRequest true "发送验证码请求参数"
|
||||
// @Success 200 {object} util.ResponseAny "发送成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||
// @Success 200 {object} response.Any "发送成功"
|
||||
// @Failure 400 {object} response.Any "参数错误"
|
||||
// @Router /api/v1/user/send-email-code [post]
|
||||
func SendEmailCode(c *gin.Context) {
|
||||
var req sendEmailCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
if req.Email == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailRequired))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailRequired))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Scene != "register" {
|
||||
c.JSON(http.StatusOK, util.Err(errUnsupportedEmailScene))
|
||||
c.JSON(http.StatusOK, response.Err(errUnsupportedEmailScene))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,11 +209,11 @@ func SendEmailCode(c *gin.Context) {
|
||||
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("email = ?", req.Email).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailAlreadyRegistered))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailAlreadyRegistered))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,16 +221,16 @@ func SendEmailCode(c *gin.Context) {
|
||||
var temp string
|
||||
err := db.GetJSON(ctx, cooldownKey, &temp)
|
||||
if err == nil {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailCodeCooldown))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailCodeCooldown))
|
||||
return
|
||||
}
|
||||
|
||||
if err := sendEmailVerificationCode(ctx, req.Email, "register", "register_email"); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
func validateRegisterEmailVerification(ctx context.Context, req *registerRequest) error {
|
||||
@@ -264,44 +264,44 @@ type updateProfileRequest struct {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.updateProfileRequest true "更新请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "修改成功,返回更新后的用户信息"
|
||||
// @Failure 400 {object} util.ResponseAny "邮箱已被占用或参数错误"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "修改成功,返回更新后的用户信息"
|
||||
// @Failure 400 {object} response.Any "邮箱已被占用或参数错误"
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/user/profile [put]
|
||||
func UpdateProfile(c *gin.Context) {
|
||||
var req updateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if userObj == nil {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(errLoginRequired))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(errLoginRequired))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var dbUser model.User
|
||||
if err := db.DB(ctx).Where("id = ?", userObj.ID).First(&dbUser).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errUserNotFound))
|
||||
c.JSON(http.StatusOK, response.Err(errUserNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
if req.Email != "" && req.Email != dbUser.Email {
|
||||
if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailFormatInvalid))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailFormatInvalid))
|
||||
return
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("email = ? AND id != ?", req.Email, dbUser.ID).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailAlreadyBound))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailAlreadyBound))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -319,12 +319,12 @@ func UpdateProfile(c *gin.Context) {
|
||||
dbUser.Location = strings.TrimSpace(req.Location)
|
||||
|
||||
if err := db.DB(ctx).Save(&dbUser).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
needChange := session.Get("need_change_password") == true
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&dbUser, needChange)))
|
||||
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&dbUser, needChange)))
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
import ("context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
@@ -83,14 +83,14 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
|
||||
isSessionCookie = true
|
||||
}
|
||||
}
|
||||
session.Options(util.GetSessionOptions(maxAge))
|
||||
session.Options(oauth.GetSessionOptions(maxAge))
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isSessionCookie {
|
||||
util.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
oauth.StripCookieMaxAgeAndExpires(c.Writer.Header(), config.Config.App.SessionCookieName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -103,23 +103,23 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.loginRequest true "登录请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "登录成功,返回用户信息"
|
||||
// @Failure 400 {object} util.ResponseAny "用户名或密码错误、帐号已禁用等"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "登录成功,返回用户信息"
|
||||
// @Failure 400 {object} response.Any "用户名或密码错误、帐号已禁用等"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
if !isPasswordLoginEnabled() {
|
||||
c.JSON(http.StatusOK, util.Err(errPasswordLoginDisabled))
|
||||
c.JSON(http.StatusOK, response.Err(errPasswordLoginDisabled))
|
||||
return
|
||||
}
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
if req.Username == "" || req.Password == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(errInvalidParams))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,12 +127,12 @@ func Login(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if err := db.DB(ctx).Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
|
||||
logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", req.Username, c.ClientIP())
|
||||
c.JSON(http.StatusOK, util.Err(errUsernameOrPasswordWrong))
|
||||
c.JSON(http.StatusOK, response.Err(errUsernameOrPasswordWrong))
|
||||
return
|
||||
}
|
||||
if !user.IsActive {
|
||||
logger.WarnF(ctx, "[LoginAudit] banned user login attempt for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
|
||||
c.JSON(http.StatusOK, util.Err(common.BannedAccount))
|
||||
c.JSON(http.StatusOK, response.Err(common.BannedAccount))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func Login(c *gin.Context) {
|
||||
|
||||
if !user.CheckPassword(req.Password) {
|
||||
logger.WarnF(ctx, "[LoginAudit] failed login attempt (incorrect password) for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
|
||||
c.JSON(http.StatusOK, util.Err(errUsernameOrPasswordWrong))
|
||||
c.JSON(http.StatusOK, response.Err(errUsernameOrPasswordWrong))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,11 +162,11 @@ func Login(c *gin.Context) {
|
||||
|
||||
user.LastLoginAt = time.Now()
|
||||
if err := db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errSaveSessionFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func Login(c *gin.Context) {
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
|
||||
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
@@ -184,19 +184,19 @@ func Login(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.registerRequest true "注册请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=oauth.BasicUserInfo} "注册并登录成功,返回用户信息"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误、用户名已存在或注册已关闭"
|
||||
// @Failure 500 {object} util.ResponseAny "服务内部错误"
|
||||
// @Success 200 {object} response.Any{data=oauth.BasicUserInfo} "注册并登录成功,返回用户信息"
|
||||
// @Failure 400 {object} response.Any "参数错误、用户名已存在或注册已关闭"
|
||||
// @Failure 500 {object} response.Any "服务内部错误"
|
||||
// @Router /api/v1/user/register [post]
|
||||
func Register(c *gin.Context) {
|
||||
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
|
||||
c.JSON(http.StatusOK, util.Err(errRegistrationDisabled))
|
||||
c.JSON(http.StatusOK, response.Err(errRegistrationDisabled))
|
||||
return
|
||||
}
|
||||
|
||||
var req registerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,15 +208,15 @@ func Register(c *gin.Context) {
|
||||
req.Code = strings.TrimSpace(req.Code)
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(errInvalidParams))
|
||||
return
|
||||
}
|
||||
if req.Email == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errEmailRequired))
|
||||
c.JSON(http.StatusOK, response.Err(errEmailRequired))
|
||||
return
|
||||
}
|
||||
if len(req.Password) < minPasswordLength {
|
||||
c.JSON(http.StatusOK, util.Err(errPasswordTooShort))
|
||||
c.JSON(http.StatusOK, response.Err(errPasswordTooShort))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func Register(c *gin.Context) {
|
||||
|
||||
// 邮箱注册验证校验
|
||||
if err := validateRegisterEmailVerification(ctx, &req); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -245,21 +245,21 @@ func Register(c *gin.Context) {
|
||||
user.Nickname = req.Username
|
||||
}
|
||||
if err := user.SetEncryptedPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.RegisterUser(ctx, db.DB(ctx)); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errSaveSessionFailed))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, false)))
|
||||
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&user, false)))
|
||||
}
|
||||
|
||||
// Logout 用户退出登录
|
||||
@@ -268,8 +268,8 @@ func Register(c *gin.Context) {
|
||||
// @Tags user
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "退出成功"
|
||||
// @Failure 500 {object} util.ResponseAny "Session 清除失败"
|
||||
// @Success 200 {object} response.Any{data=string} "退出成功"
|
||||
// @Failure 500 {object} response.Any "Session 清除失败"
|
||||
// @Router /api/v1/user/logout [get]
|
||||
func Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
@@ -278,13 +278,13 @@ func Logout(c *gin.Context) {
|
||||
if userID != nil {
|
||||
logger.InfoF(c.Request.Context(), "[LoginAudit] user logged out: %v, ID: %v, IP: %s", username, userID, c.ClientIP())
|
||||
}
|
||||
session.Options(util.GetSessionOptions(-1))
|
||||
session.Options(oauth.GetSessionOptions(-1))
|
||||
session.Clear()
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(""))
|
||||
c.JSON(http.StatusOK, response.OK(""))
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
@@ -299,14 +299,14 @@ type changePasswordRequest struct {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.changePasswordRequest true "修改密码请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "修改密码成功"
|
||||
// @Failure 400 {object} util.ResponseAny "原密码错误或新密码不符合要求"
|
||||
// @Failure 401 {object} util.ResponseAny "请先登录"
|
||||
// @Success 200 {object} response.Any{data=string} "修改密码成功"
|
||||
// @Failure 400 {object} response.Any "原密码错误或新密码不符合要求"
|
||||
// @Failure 401 {object} response.Any "请先登录"
|
||||
// @Router /api/v1/user/change-password [post]
|
||||
func ChangePassword(c *gin.Context) {
|
||||
var req changePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
c.JSON(http.StatusBadRequest, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -314,47 +314,47 @@ func ChangePassword(c *gin.Context) {
|
||||
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
||||
|
||||
if req.OldPassword == "" || req.NewPassword == "" {
|
||||
c.JSON(http.StatusOK, util.Err(errInvalidParams))
|
||||
c.JSON(http.StatusOK, response.Err(errInvalidParams))
|
||||
return
|
||||
}
|
||||
if len(req.NewPassword) < minPasswordLength {
|
||||
c.JSON(http.StatusOK, util.Err(errNewPasswordTooShort))
|
||||
c.JSON(http.StatusOK, response.Err(errNewPasswordTooShort))
|
||||
return
|
||||
}
|
||||
|
||||
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if userObj == nil {
|
||||
c.JSON(http.StatusUnauthorized, util.Err(errLoginRequired))
|
||||
c.JSON(http.StatusUnauthorized, response.Err(errLoginRequired))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var dbUser model.User
|
||||
if err := db.DB(ctx).Where("id = ?", userObj.ID).First(&dbUser).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errUserNotFound))
|
||||
c.JSON(http.StatusOK, response.Err(errUserNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if !dbUser.CheckPassword(req.OldPassword) {
|
||||
c.JSON(http.StatusOK, util.Err(errOldPasswordIncorrect))
|
||||
c.JSON(http.StatusOK, response.Err(errOldPasswordIncorrect))
|
||||
return
|
||||
}
|
||||
|
||||
// 加密并更新为新密码
|
||||
if err := dbUser.SetEncryptedPassword(req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(errPasswordEncryptFailed))
|
||||
c.JSON(http.StatusOK, response.Err(errPasswordEncryptFailed))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&dbUser).Update("password", dbUser.Password).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 吊销该用户所有的 Access Token
|
||||
if err := db.DB(ctx).Where("user_id = ?", dbUser.ID).Delete(&model.AccessToken{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err("吊销 Access Token 失败: "+err.Error()))
|
||||
c.JSON(http.StatusOK, response.Err("吊销 Access Token 失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -363,5 +363,5 @@ func ChangePassword(c *gin.Context) {
|
||||
session.Clear()
|
||||
_ = session.Save()
|
||||
|
||||
c.JSON(http.StatusOK, util.OK("密码修改成功"))
|
||||
c.JSON(http.StatusOK, response.OK("密码修改成功"))
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
import ("bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -18,11 +17,11 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func setupUserTestRouter(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
@@ -49,7 +48,7 @@ func setupUserTestRouter(t *testing.T) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
store := cookie.NewStore([]byte(config.Config.App.SessionSecret))
|
||||
store.Options(util.GetSessionOptions(3600))
|
||||
store.Options(oauth.GetSessionOptions(3600))
|
||||
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, store))
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
@@ -92,7 +91,7 @@ func sessionCookieFromResponse(t *testing.T, w *httptest.ResponseRecorder) *http
|
||||
func basicUserInfoFromResponse(t *testing.T, w *httptest.ResponseRecorder) oauth.BasicUserInfo {
|
||||
t.Helper()
|
||||
|
||||
var resp util.ResponseAny
|
||||
var resp response.Any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("basicUserInfoFromResponse() decode response failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package util
|
||||
// Package response provides shared HTTP API response structures.
|
||||
package response
|
||||
|
||||
// Response 通用响应体
|
||||
type Response[T any] struct {
|
||||
@@ -10,9 +11,9 @@ type Response[T any] struct {
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
// ResponseAny 用于 Swagger 文档的响应类型(非泛型)
|
||||
// Any 用于 Swagger 文档的响应类型(非泛型)
|
||||
// swag 不支持泛型,使用此类型替代 Response[T]
|
||||
type ResponseAny struct {
|
||||
type Any struct {
|
||||
ErrorMsg string `json:"error_msg" example:""`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/user"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
capUtil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||
|
||||
// Swagger 文档生成
|
||||
@@ -98,7 +97,7 @@ func Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
sessionStore.Options(util.GetSessionOptions(config.Config.App.SessionAge))
|
||||
sessionStore.Options(oauth.GetSessionOptions(config.Config.App.SessionAge))
|
||||
|
||||
r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user