mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 22:46:38 +08:00
[优化] 改名
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAccessLogs godoc
|
||||
// @Summary List access logs
|
||||
// @Tags AccessLogs
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param node_id query string false "Node ID"
|
||||
// @Param p query int false "Page index"
|
||||
// @Param page_size query int false "Page size"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/access-logs/ [get]
|
||||
func GetAccessLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("p", "0"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "0"))
|
||||
logs, err := service.ListAccessLogs(c.Query("node_id"), page, pageSize)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, logs)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"openflare/model"
|
||||
"openflare/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AgentRegister godoc
|
||||
// @Summary Register or discover agent node
|
||||
// @Tags Agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security AgentTokenAuth
|
||||
// @Param payload body service.AgentNodePayload true "Agent node payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/agent/nodes/register [post]
|
||||
func AgentRegister(c *gin.Context) {
|
||||
var payload service.AgentNodePayload
|
||||
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
result *service.AgentRegistrationResponse
|
||||
err error
|
||||
)
|
||||
if authNode, ok := c.Get("agent_node"); ok {
|
||||
result, err = service.RegisterNodeWithAgentToken(authNode.(*model.Node), payload)
|
||||
} else {
|
||||
result, err = service.RegisterNodeWithDiscovery(payload)
|
||||
}
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, result)
|
||||
}
|
||||
|
||||
// AgentHeartbeat godoc
|
||||
// @Summary Report agent heartbeat
|
||||
// @Tags Agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security AgentTokenAuth
|
||||
// @Param payload body service.AgentNodePayload true "Agent heartbeat payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/agent/nodes/heartbeat [post]
|
||||
func AgentHeartbeat(c *gin.Context) {
|
||||
var payload service.AgentNodePayload
|
||||
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
authNode, ok := c.Get("agent_node")
|
||||
if !ok {
|
||||
respondUnauthorized(c, "鏃犳潈杩涜姝ゆ搷浣滐紝Agent Token 鏃犳晥")
|
||||
return
|
||||
}
|
||||
|
||||
node, err := service.HeartbeatNode(authNode.(*model.Node), payload)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccessWithExtras(c, node.Node, gin.H{
|
||||
"agent_settings": node.AgentSettings,
|
||||
"active_config": node.ActiveConfig,
|
||||
})
|
||||
}
|
||||
|
||||
// AgentGetActiveConfig godoc
|
||||
// @Summary Get active config for agent
|
||||
// @Tags Agent
|
||||
// @Produce json
|
||||
// @Security AgentTokenAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/agent/config-versions/active [get]
|
||||
func AgentGetActiveConfig(c *gin.Context) {
|
||||
config, err := service.GetActiveConfigForAgent()
|
||||
if err != nil {
|
||||
respondFailure(c, "当前没有激活版本")
|
||||
return
|
||||
}
|
||||
respondSuccess(c, config)
|
||||
}
|
||||
|
||||
// AgentReportApplyLog godoc
|
||||
// @Summary Report agent apply result
|
||||
// @Tags Agent
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security AgentTokenAuth
|
||||
// @Param payload body service.ApplyLogPayload true "Apply log payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/agent/apply-logs [post]
|
||||
func AgentReportApplyLog(c *gin.Context) {
|
||||
var payload service.ApplyLogPayload
|
||||
if err := decodeJSONBody(c.Request.Body, &payload); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
if authNode, ok := c.Get("agent_node"); ok {
|
||||
payload.NodeID = authNode.(*model.Node).NodeID
|
||||
}
|
||||
|
||||
log, err := service.ReportApplyLog(payload)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, log)
|
||||
}
|
||||
|
||||
// GetNodes godoc
|
||||
// @Summary List nodes
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/nodes/ [get]
|
||||
func GetNodes(c *gin.Context) {
|
||||
nodes, err := service.ListNodeViews()
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, nodes)
|
||||
}
|
||||
|
||||
// GetApplyLogs godoc
|
||||
// @Summary List apply logs
|
||||
// @Tags ApplyLogs
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param node_id query string false "Node ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/apply-logs/ [get]
|
||||
func GetApplyLogs(c *gin.Context) {
|
||||
logs, err := service.ListApplyLogs(c.Query("node_id"))
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, logs)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetConfigVersions godoc
|
||||
// @Summary List config versions
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/ [get]
|
||||
func GetConfigVersions(c *gin.Context) {
|
||||
versions, err := service.ListConfigVersions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": versions,
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveConfigVersion godoc
|
||||
// @Summary Get active config version
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/active [get]
|
||||
func GetActiveConfigVersion(c *gin.Context) {
|
||||
version, err := service.GetActiveConfigVersion()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "当前没有激活版本",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": version,
|
||||
})
|
||||
}
|
||||
|
||||
// PreviewConfigVersion godoc
|
||||
// @Summary Preview config rendering
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/preview [get]
|
||||
func PreviewConfigVersion(c *gin.Context) {
|
||||
preview, err := service.PreviewConfigVersion()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": preview,
|
||||
})
|
||||
}
|
||||
|
||||
// DiffConfigVersion godoc
|
||||
// @Summary Diff current draft against active version
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/diff [get]
|
||||
func DiffConfigVersion(c *gin.Context) {
|
||||
diff, err := service.DiffConfigVersion()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": diff,
|
||||
})
|
||||
}
|
||||
|
||||
// PublishConfigVersion godoc
|
||||
// @Summary Publish a new config version
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/publish [post]
|
||||
func PublishConfigVersion(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
result, err := service.PublishConfigVersion(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": result.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// ActivateConfigVersion godoc
|
||||
// @Summary Activate an existing config version
|
||||
// @Tags ConfigVersions
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Version ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/config-versions/{id}/activate [put]
|
||||
func ActivateConfigVersion(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
version, err := service.ActivateConfigVersion(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": version,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"openflare/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetDashboardOverview godoc
|
||||
// @Summary Get dashboard overview
|
||||
// @Tags Dashboard
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/dashboard/overview [get]
|
||||
func GetDashboardOverview(c *gin.Context) {
|
||||
view, err := service.GetDashboardOverview()
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, view)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"openflare/utils"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetAllFiles(c *gin.Context) {
|
||||
p, _ := strconv.Atoi(c.Query("p"))
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
files, err := model.GetAllFiles(p*common.ItemsPerPage, common.ItemsPerPage)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": files,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func SearchFiles(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
files, err := model.SearchFiles(keyword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": files,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func UploadFile(c *gin.Context) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
uploadPath := common.UploadPath
|
||||
description := c.PostForm("description")
|
||||
if description == "" {
|
||||
description = "无描述信息"
|
||||
}
|
||||
uploader := c.GetString("username")
|
||||
if uploader == "" {
|
||||
uploader = "访客用户"
|
||||
}
|
||||
uploaderId := c.GetInt("id")
|
||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||
files := form.File["file"]
|
||||
for _, file := range files {
|
||||
filename := filepath.Base(file.Filename)
|
||||
ext := filepath.Ext(filename)
|
||||
link := utils.GetUUID() + ext
|
||||
savePath := filepath.Join(uploadPath, link) // both parts are checked, so this path should be safe to use
|
||||
if err := c.SaveUploadedFile(file, savePath); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// save to database
|
||||
fileObj := &model.File{
|
||||
Description: description,
|
||||
Uploader: uploader,
|
||||
UploadTime: currentTime,
|
||||
UploaderId: uploaderId,
|
||||
Link: link,
|
||||
Filename: filename,
|
||||
}
|
||||
err = fileObj.Insert()
|
||||
if err != nil {
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteFile(c *gin.Context) {
|
||||
fileIdStr := c.Param("id")
|
||||
fileId, err := strconv.Atoi(fileIdStr)
|
||||
if err != nil || fileId == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fileObj := &model.File{
|
||||
Id: fileId,
|
||||
}
|
||||
model.DB.Where("id = ?", fileId).First(&fileObj)
|
||||
if fileObj.Link == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "文件不存在!",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = fileObj.Delete()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
} else {
|
||||
message := "文件删除成功"
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func DownloadFile(c *gin.Context) {
|
||||
path := c.Param("file")
|
||||
fullPath := filepath.Join(common.UploadPath, path)
|
||||
if !strings.HasPrefix(fullPath, common.UploadPath) {
|
||||
// We may being attacked!
|
||||
c.Status(403)
|
||||
return
|
||||
}
|
||||
c.File(fullPath)
|
||||
// Update download counter
|
||||
go func() {
|
||||
model.UpdateDownloadCounter(path)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"openflare/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type geoIPLookupRequest struct {
|
||||
Provider string `json:"provider"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// LookupGeoIP godoc
|
||||
// @Summary Test GeoIP lookup
|
||||
// @Tags Options
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body geoIPLookupRequest true "GeoIP lookup payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/option/geoip/lookup [post]
|
||||
func LookupGeoIP(c *gin.Context) {
|
||||
var request geoIPLookupRequest
|
||||
if err := decodeJSONBody(c.Request.Body, &request); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
view, err := service.LookupGeoIP(request.Provider, request.IP)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, view)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GitHubOAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type GitHubUser struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("无效的参数")
|
||||
}
|
||||
values := map[string]string{"client_id": common.GitHubClientId, "client_secret": common.GitHubClientSecret, "code": code}
|
||||
jsonData, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
slog.Error("github oauth access token request failed", "error", err)
|
||||
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var oAuthResponse GitHubOAuthResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
|
||||
res2, err := client.Do(req)
|
||||
if err != nil {
|
||||
slog.Error("github user info request failed", "error", err)
|
||||
return nil, errors.New("无法连接至 GitHub 服务器,请稍后重试!")
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
var githubUser GitHubUser
|
||||
err = json.NewDecoder(res2.Body).Decode(&githubUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if githubUser.Login == "" {
|
||||
return nil, errors.New("返回值非法,用户字段为空,请稍后重试!")
|
||||
}
|
||||
return &githubUser, nil
|
||||
}
|
||||
|
||||
func GitHubOAuth(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
username := session.Get("username")
|
||||
if username != nil {
|
||||
GitHubBind(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !common.GitHubOAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "管理员未开启通过 GitHub 登录以及注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
githubUser, err := getGitHubUserInfoByCode(code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
GitHubId: githubUser.Login,
|
||||
}
|
||||
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
||||
err := user.FillUserByGitHubId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if common.RegisterEnabled {
|
||||
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
if githubUser.Name != "" {
|
||||
user.DisplayName = githubUser.Name
|
||||
} else {
|
||||
user.DisplayName = "GitHub User"
|
||||
}
|
||||
user.Email = githubUser.Email
|
||||
user.Role = common.RoleCommonUser
|
||||
user.Status = common.UserStatusEnabled
|
||||
|
||||
if err := user.Insert(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "管理员关闭了新用户注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user.Status != common.UserStatusEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "用户已被封禁",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
setupLogin(&user, c)
|
||||
}
|
||||
|
||||
func GitHubBind(c *gin.Context) {
|
||||
if !common.GitHubOAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "管理员未开启通过 GitHub 登录以及注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
githubUser, err := getGitHubUserInfoByCode(code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
GitHubId: githubUser.Login,
|
||||
}
|
||||
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该 GitHub 账户已被绑定",
|
||||
})
|
||||
return
|
||||
}
|
||||
session := sessions.Default(c)
|
||||
id := session.Get("id")
|
||||
// id := c.GetInt("id") // critical bug!
|
||||
user.Id = id.(int)
|
||||
err = user.FillUserById()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.GitHubId = githubUser.Login
|
||||
err = user.Update(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "bind",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetManagedDomains godoc
|
||||
// @Summary List managed domains
|
||||
// @Tags ManagedDomains
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/managed-domains/ [get]
|
||||
func GetManagedDomains(c *gin.Context) {
|
||||
domains, err := service.ListManagedDomains()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": domains,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateManagedDomain godoc
|
||||
// @Summary Create managed domain
|
||||
// @Tags ManagedDomains
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param payload body service.ManagedDomainInput true "Managed domain payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/managed-domains/ [post]
|
||||
func CreateManagedDomain(c *gin.Context) {
|
||||
var input service.ManagedDomainInput
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
domain, err := service.CreateManagedDomain(input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": domain,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateManagedDomain godoc
|
||||
// @Summary Update managed domain
|
||||
// @Tags ManagedDomains
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Managed domain ID"
|
||||
// @Param payload body service.ManagedDomainInput true "Managed domain payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/managed-domains/{id} [put]
|
||||
func UpdateManagedDomain(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
var input service.ManagedDomainInput
|
||||
if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
domain, err := service.UpdateManagedDomain(uint(id), input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": domain,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteManagedDomain godoc
|
||||
// @Summary Delete managed domain
|
||||
// @Tags ManagedDomains
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Managed domain ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/managed-domains/{id} [delete]
|
||||
func DeleteManagedDomain(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = service.DeleteManagedDomain(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
|
||||
// MatchManagedDomainCertificate godoc
|
||||
// @Summary Match certificate for domain
|
||||
// @Tags ManagedDomains
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param domain query string true "Domain"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/managed-domains/match [get]
|
||||
func MatchManagedDomainCertificate(c *gin.Context) {
|
||||
domain := strings.TrimSpace(c.Query("domain"))
|
||||
result, err := service.MatchManagedDomainCertificate(domain)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"openflare/utils/mail"
|
||||
"openflare/utils/security"
|
||||
"openflare/utils/validation"
|
||||
)
|
||||
|
||||
// GetStatus godoc
|
||||
// @Summary Get server status
|
||||
// @Tags Public
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/status [get]
|
||||
func GetStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": gin.H{
|
||||
"version": common.Version,
|
||||
"start_time": common.StartTime,
|
||||
"email_verification": common.EmailVerificationEnabled,
|
||||
"github_oauth": common.GitHubOAuthEnabled,
|
||||
"github_client_id": common.GitHubClientId,
|
||||
"system_name": common.SystemName,
|
||||
"home_page_link": common.HomePageLink,
|
||||
"footer_html": common.Footer,
|
||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||
"wechat_login": common.WeChatAuthEnabled,
|
||||
"server_address": common.ServerAddress,
|
||||
"turnstile_check": common.TurnstileCheckEnabled,
|
||||
"turnstile_site_key": common.TurnstileSiteKey,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetNotice(c *gin.Context) {
|
||||
common.OptionMapRWMutex.RLock()
|
||||
defer common.OptionMapRWMutex.RUnlock()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": common.OptionMap["Notice"],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetAbout(c *gin.Context) {
|
||||
common.OptionMapRWMutex.RLock()
|
||||
defer common.OptionMapRWMutex.RUnlock()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": common.OptionMap["About"],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func SendEmailVerification(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if err := validation.Validate.Var(email, "required,email"); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if model.IsEmailAlreadyTaken(email) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "邮箱地址已被占用",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := security.GenerateVerificationCode(6)
|
||||
security.RegisterVerificationCodeWithKey(email, code, security.EmailVerificationPurpose)
|
||||
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, security.VerificationValidMinutes)
|
||||
err := mail.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func SendPasswordResetEmail(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
if err := validation.Validate.Var(email, "required,email"); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !model.IsEmailAlreadyTaken(email) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该邮箱地址未注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := security.GenerateVerificationCode(0)
|
||||
security.RegisterVerificationCodeWithKey(email, code, security.PasswordResetPurpose)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code)
|
||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, security.VerificationValidMinutes)
|
||||
err := mail.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type PasswordResetRequest struct {
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func ResetPassword(c *gin.Context) {
|
||||
var req PasswordResetRequest
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
||||
if req.Email == "" || req.Token == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !security.VerifyCodeWithKey(req.Email, req.Token, security.PasswordResetPurpose) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "重置链接非法或已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
password := security.GenerateVerificationCode(12)
|
||||
err = model.ResetUserPasswordByEmail(req.Email, password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
security.DeleteKey(req.Email, security.PasswordResetPurpose)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": password,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type nodeAgentUpdateRequest struct {
|
||||
Channel string `json:"channel"`
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
type nodeObservabilityQuery struct {
|
||||
Hours int `form:"hours"`
|
||||
Limit int `form:"limit"`
|
||||
}
|
||||
|
||||
// CreateNode godoc
|
||||
// @Summary Create node
|
||||
// @Tags Nodes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param payload body service.NodeInput true "Node payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/ [post]
|
||||
func CreateNode(c *gin.Context) {
|
||||
var input service.NodeInput
|
||||
if err := decodeJSONBody(c.Request.Body, &input); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
node, err := service.CreateNode(input)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, node)
|
||||
}
|
||||
|
||||
// GetNodeBootstrapToken godoc
|
||||
// @Summary Get global discovery token
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/nodes/bootstrap-token [get]
|
||||
func GetNodeBootstrapToken(c *gin.Context) {
|
||||
bootstrap, err := service.GetNodeBootstrapView()
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, bootstrap)
|
||||
}
|
||||
|
||||
// RotateNodeBootstrapToken godoc
|
||||
// @Summary Rotate global discovery token
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/nodes/bootstrap-token/rotate [post]
|
||||
func RotateNodeBootstrapToken(c *gin.Context) {
|
||||
bootstrap, err := service.RotateGlobalDiscoveryToken()
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, bootstrap)
|
||||
}
|
||||
|
||||
// UpdateNode godoc
|
||||
// @Summary Update node
|
||||
// @Tags Nodes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Param payload body service.NodeInput true "Node payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id} [put]
|
||||
func UpdateNode(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
var input service.NodeInput
|
||||
if err = decodeJSONBody(c.Request.Body, &input); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
node, err := service.UpdateNode(uint(id), input)
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, node)
|
||||
}
|
||||
|
||||
// DeleteNode godoc
|
||||
// @Summary Delete node
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id} [delete]
|
||||
func DeleteNode(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
if err = service.DeleteNode(uint(id)); err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccessMessage(c, "")
|
||||
}
|
||||
|
||||
// RequestNodeAgentUpdate godoc
|
||||
// @Summary Request agent self-update on node
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id}/agent-update [post]
|
||||
func RequestNodeAgentUpdate(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
var request nodeAgentUpdateRequest
|
||||
if c.Request.ContentLength > 0 {
|
||||
if err = decodeOptionalJSONBody(c.Request.Body, &request); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
node, err := service.RequestNodeAgentUpdate(uint(id), service.NodeAgentUpdateInput{
|
||||
Channel: request.Channel,
|
||||
TagName: request.TagName,
|
||||
})
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, node)
|
||||
}
|
||||
|
||||
// RequestNodeOpenrestyRestart godoc
|
||||
// @Summary Request openresty restart on node
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id}/openresty-restart [post]
|
||||
func RequestNodeOpenrestyRestart(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
node, err := service.RequestNodeOpenrestyRestart(uint(id))
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, node)
|
||||
}
|
||||
|
||||
// GetNodeAgentRelease godoc
|
||||
// @Summary Check latest agent release for node
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Param channel query string false "stable or preview"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id}/agent-release [get]
|
||||
func GetNodeAgentRelease(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
release, err := service.GetNodeAgentRelease(c.Request.Context(), uint(id), c.Query("channel"))
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, release)
|
||||
}
|
||||
|
||||
// GetNodeObservability godoc
|
||||
// @Summary Get node observability details
|
||||
// @Tags Nodes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Node ID"
|
||||
// @Param hours query int false "Lookback window in hours"
|
||||
// @Param limit query int false "Max records per section"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/nodes/{id}/observability [get]
|
||||
func GetNodeObservability(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
var query nodeObservabilityQuery
|
||||
if err = c.ShouldBindQuery(&query); err != nil {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
view, err := service.GetNodeObservability(uint(id), service.NodeObservabilityQuery{
|
||||
Hours: query.Hours,
|
||||
Limit: query.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
respondFailure(c, err.Error())
|
||||
return
|
||||
}
|
||||
respondSuccess(c, view)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"openflare/service"
|
||||
"openflare/utils"
|
||||
"openflare/utils/geoip"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
openRestySizePattern = regexp.MustCompile(`^\d+[kKmMgG]?$`)
|
||||
openRestyProxyBuffersPattern = regexp.MustCompile(`^\d+\s+\d+[kKmMgG]?$`)
|
||||
openRestyCacheLevelsPattern = regexp.MustCompile(`^\d{1,2}(?::\d{1,2}){0,2}$`)
|
||||
openRestyDurationTokenPattern = regexp.MustCompile(`^\d+[smhdwSMHDW]$`)
|
||||
)
|
||||
|
||||
func validateRateLimitOption(key string, value string) error {
|
||||
maxDurationSeconds := int(common.RateLimitKeyExpirationDuration.Seconds())
|
||||
|
||||
switch key {
|
||||
case "GlobalApiRateLimitNum", "GlobalWebRateLimitNum", "UploadRateLimitNum", "DownloadRateLimitNum", "CriticalRateLimitNum":
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil || intValue <= 0 {
|
||||
return fmt.Errorf("%s 必须为大于 0 的整数", key)
|
||||
}
|
||||
return nil
|
||||
case "GlobalApiRateLimitDuration", "GlobalWebRateLimitDuration", "UploadRateLimitDuration", "DownloadRateLimitDuration", "CriticalRateLimitDuration":
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil || intValue <= 0 {
|
||||
return fmt.Errorf("%s 必须为大于 0 的整数秒", key)
|
||||
}
|
||||
if intValue > maxDurationSeconds {
|
||||
return fmt.Errorf("%s 不能大于 %d 秒", key, maxDurationSeconds)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validatePositiveIntegerOption(key string, value string) error {
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil || intValue <= 0 {
|
||||
return fmt.Errorf("%s 必须为大于 0 的整数", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBooleanOption(key string, value string) error {
|
||||
switch value {
|
||||
case "true", "false":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%s 必须为 true 或 false", key)
|
||||
}
|
||||
}
|
||||
|
||||
func validateGeoIPOption(key string, value string) error {
|
||||
if key != "GeoIPProvider" {
|
||||
return nil
|
||||
}
|
||||
if !geoip.IsValidProvider(value) {
|
||||
return fmt.Errorf("%s 仅支持 disabled、mmdb、ip-api、geojs、ipinfo", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOpenRestyOption(key string, value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
|
||||
switch key {
|
||||
case "OpenRestyWorkerProcesses":
|
||||
if trimmed == "auto" {
|
||||
return nil
|
||||
}
|
||||
return validatePositiveIntegerOption(key, trimmed)
|
||||
case "OpenRestyWorkerConnections",
|
||||
"OpenRestyWorkerRlimitNofile",
|
||||
"OpenRestyKeepaliveTimeout",
|
||||
"OpenRestyKeepaliveRequests",
|
||||
"OpenRestyClientHeaderTimeout",
|
||||
"OpenRestyClientBodyTimeout",
|
||||
"OpenRestySendTimeout",
|
||||
"OpenRestyProxyConnectTimeout",
|
||||
"OpenRestyProxySendTimeout",
|
||||
"OpenRestyProxyReadTimeout",
|
||||
"OpenRestyGzipMinLength":
|
||||
return validatePositiveIntegerOption(key, trimmed)
|
||||
case "OpenRestyGzipCompLevel":
|
||||
if err := validatePositiveIntegerOption(key, trimmed); err != nil {
|
||||
return err
|
||||
}
|
||||
level, _ := strconv.Atoi(trimmed)
|
||||
if level > 9 {
|
||||
return fmt.Errorf("%s 不能大于 9", key)
|
||||
}
|
||||
return nil
|
||||
case "OpenRestyEventsUse":
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
switch trimmed {
|
||||
case "epoll", "kqueue", "poll", "select", "rtsig", "/dev/poll", "eventport":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key)
|
||||
}
|
||||
case "OpenRestyEventsMultiAcceptEnabled",
|
||||
"OpenRestyWebsocketEnabled",
|
||||
"OpenRestyProxyRequestBufferingEnabled",
|
||||
"OpenRestyProxyBufferingEnabled",
|
||||
"OpenRestyGzipEnabled",
|
||||
"OpenRestyCacheEnabled",
|
||||
"OpenRestyCacheLockEnabled":
|
||||
return validateBooleanOption(key, trimmed)
|
||||
case "OpenRestyProxyBuffers", "OpenRestyLargeClientHeaderBuffers":
|
||||
if openRestyProxyBuffersPattern.MatchString(trimmed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s 格式必须类似 \"16 16k\"", key)
|
||||
case "OpenRestyProxyBufferSize", "OpenRestyProxyBusyBuffersSize", "OpenRestyCacheMaxSize", "OpenRestyClientMaxBodySize":
|
||||
if openRestySizePattern.MatchString(trimmed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s 格式必须为整数或带 k/m/g 单位的大小值", key)
|
||||
case "OpenRestyCachePath":
|
||||
if strings.ContainsAny(trimmed, "\r\n\t") {
|
||||
return fmt.Errorf("%s 不能包含换行或制表符", key)
|
||||
}
|
||||
return nil
|
||||
case "OpenRestyCacheLevels":
|
||||
if openRestyCacheLevelsPattern.MatchString(trimmed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s 格式必须类似 \"1:2\" 或 \"1:2:2\"", key)
|
||||
case "OpenRestyCacheInactive", "OpenRestyCacheLockTimeout":
|
||||
if openRestyDurationTokenPattern.MatchString(trimmed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s 格式必须为带单位的时长,例如 30m 或 5s", key)
|
||||
case "OpenRestyCacheKeyTemplate":
|
||||
if trimmed == "" {
|
||||
return fmt.Errorf("%s 不能为空", key)
|
||||
}
|
||||
if strings.ContainsAny(trimmed, "\r\n") {
|
||||
return fmt.Errorf("%s 不能包含换行", key)
|
||||
}
|
||||
return nil
|
||||
case "OpenRestyCacheUseStale":
|
||||
if trimmed == "" {
|
||||
return fmt.Errorf("%s 不能为空", key)
|
||||
}
|
||||
allowedTokens := map[string]struct{}{
|
||||
"error": {}, "timeout": {}, "invalid_header": {}, "updating": {},
|
||||
"http_500": {}, "http_502": {}, "http_503": {}, "http_504": {},
|
||||
"http_403": {}, "http_404": {}, "http_429": {}, "off": {},
|
||||
}
|
||||
for _, token := range strings.Fields(trimmed) {
|
||||
if _, ok := allowedTokens[token]; !ok {
|
||||
return fmt.Errorf("%s 包含不支持的值 %q", key, token)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "OpenRestyMainConfigTemplate":
|
||||
return service.ValidateOpenRestyMainConfigTemplate(value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetOptions godoc
|
||||
// @Summary List editable options
|
||||
// @Tags Options
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/option/ [get]
|
||||
func GetOptions(c *gin.Context) {
|
||||
var options []*model.Option
|
||||
common.OptionMapRWMutex.Lock()
|
||||
for k, v := range common.OptionMap {
|
||||
if strings.Contains(k, "Token") || strings.Contains(k, "Secret") {
|
||||
continue
|
||||
}
|
||||
options = append(options, &model.Option{
|
||||
Key: k,
|
||||
Value: utils.Interface2String(v),
|
||||
})
|
||||
}
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": options,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateOption godoc
|
||||
// @Summary Update option
|
||||
// @Tags Options
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body model.Option true "Option payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/option/ [put]
|
||||
func UpdateOption(c *gin.Context) {
|
||||
var option model.Option
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&option)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
switch option.Key {
|
||||
case "GitHubOAuthEnabled":
|
||||
if option.Value == "true" && common.GitHubClientId == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!",
|
||||
})
|
||||
return
|
||||
}
|
||||
case "WeChatAuthEnabled":
|
||||
if option.Value == "true" && common.WeChatServerAddress == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法启用微信登录,请先填入微信登录相关配置信息!",
|
||||
})
|
||||
return
|
||||
}
|
||||
case "TurnstileCheckEnabled":
|
||||
if option.Value == "true" && common.TurnstileSiteKey == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = validateRateLimitOption(option.Key, option.Value); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = validateOpenRestyOption(option.Key, option.Value); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = validateGeoIPOption(option.Key, option.Value); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
err = model.UpdateOption(option.Key, option.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package controller
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateOpenRestyOption(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "worker processes auto", key: "OpenRestyWorkerProcesses", value: "auto"},
|
||||
{name: "worker processes number", key: "OpenRestyWorkerProcesses", value: "8"},
|
||||
{name: "worker processes invalid", key: "OpenRestyWorkerProcesses", value: "0", wantErr: true},
|
||||
{name: "events use empty", key: "OpenRestyEventsUse", value: ""},
|
||||
{name: "events use invalid", key: "OpenRestyEventsUse", value: "io_uring", wantErr: true},
|
||||
{name: "proxy buffers valid", key: "OpenRestyProxyBuffers", value: "16 16k"},
|
||||
{name: "proxy buffers invalid", key: "OpenRestyProxyBuffers", value: "16x16k", wantErr: true},
|
||||
{name: "cache max size valid", key: "OpenRestyCacheMaxSize", value: "2g"},
|
||||
{name: "cache max size invalid", key: "OpenRestyCacheMaxSize", value: "2gb", wantErr: true},
|
||||
{name: "client max body size valid", key: "OpenRestyClientMaxBodySize", value: "64m"},
|
||||
{name: "client max body size invalid", key: "OpenRestyClientMaxBodySize", value: "64mb", wantErr: true},
|
||||
{name: "large client header buffers valid", key: "OpenRestyLargeClientHeaderBuffers", value: "4 16k"},
|
||||
{name: "large client header buffers invalid", key: "OpenRestyLargeClientHeaderBuffers", value: "4x16k", wantErr: true},
|
||||
{name: "proxy request buffering valid", key: "OpenRestyProxyRequestBufferingEnabled", value: "true"},
|
||||
{name: "proxy request buffering invalid", key: "OpenRestyProxyRequestBufferingEnabled", value: "on", wantErr: true},
|
||||
{name: "websocket valid", key: "OpenRestyWebsocketEnabled", value: "false"},
|
||||
{name: "websocket invalid", key: "OpenRestyWebsocketEnabled", value: "off", wantErr: true},
|
||||
{name: "cache inactive valid", key: "OpenRestyCacheInactive", value: "30m"},
|
||||
{name: "cache inactive invalid", key: "OpenRestyCacheInactive", value: "30", wantErr: true},
|
||||
{name: "cache use stale valid", key: "OpenRestyCacheUseStale", value: "error timeout http_500"},
|
||||
{name: "cache use stale invalid", key: "OpenRestyCacheUseStale", value: "error whatever", wantErr: true},
|
||||
{name: "gzip level valid", key: "OpenRestyGzipCompLevel", value: "9"},
|
||||
{name: "gzip level invalid", key: "OpenRestyGzipCompLevel", value: "10", wantErr: true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateOpenRestyOption(testCase.key, testCase.value)
|
||||
if testCase.wantErr && err == nil {
|
||||
t.Fatalf("%s: expected error", testCase.name)
|
||||
}
|
||||
if !testCase.wantErr && err != nil {
|
||||
t.Fatalf("%s: unexpected error: %v", testCase.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetProxyRoutes godoc
|
||||
// @Summary List proxy routes
|
||||
// @Tags ProxyRoutes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/proxy-routes/ [get]
|
||||
func GetProxyRoutes(c *gin.Context) {
|
||||
routes, err := service.ListProxyRoutes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": routes,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProxyRoute godoc
|
||||
// @Summary Create proxy route
|
||||
// @Tags ProxyRoutes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param payload body service.ProxyRouteInput true "Proxy route payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/proxy-routes/ [post]
|
||||
func CreateProxyRoute(c *gin.Context) {
|
||||
var input service.ProxyRouteInput
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
route, err := service.CreateProxyRoute(input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": route,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProxyRoute godoc
|
||||
// @Summary Update proxy route
|
||||
// @Tags ProxyRoutes
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Route ID"
|
||||
// @Param payload body service.ProxyRouteInput true "Proxy route payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/proxy-routes/{id} [put]
|
||||
func UpdateProxyRoute(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
var input service.ProxyRouteInput
|
||||
if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
route, err := service.UpdateProxyRoute(uint(id), input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": route,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteProxyRoute godoc
|
||||
// @Summary Delete proxy route
|
||||
// @Tags ProxyRoutes
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Route ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/proxy-routes/{id} [delete]
|
||||
func DeleteProxyRoute(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = service.DeleteProxyRoute(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const invalidParamsMessage = "鏃犳晥鐨勫弬鏁?"
|
||||
|
||||
func respondSuccess(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func respondSuccessWithExtras(c *gin.Context, data any, extras gin.H) {
|
||||
payload := gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": data,
|
||||
}
|
||||
for key, value := range extras {
|
||||
payload[key] = value
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func respondSuccessMessage(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func respondFailure(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func respondBadRequest(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = invalidParamsMessage
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func respondUnauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func decodeJSONBody(body io.Reader, target any) error {
|
||||
return json.NewDecoder(body).Decode(target)
|
||||
}
|
||||
|
||||
func decodeOptionalJSONBody(body io.Reader, target any) error {
|
||||
if err := json.NewDecoder(body).Decode(target); err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetTLSCertificates godoc
|
||||
// @Summary List TLS certificates
|
||||
// @Tags TLSCertificates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/ [get]
|
||||
func GetTLSCertificates(c *gin.Context) {
|
||||
certificates, err := service.ListTLSCertificates()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": certificates,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTLSCertificate godoc
|
||||
// @Summary Get TLS certificate detail
|
||||
// @Tags TLSCertificates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Certificate ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/{id} [get]
|
||||
func GetTLSCertificate(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
certificate, err := service.GetTLSCertificate(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": certificate,
|
||||
})
|
||||
}
|
||||
|
||||
// GetTLSCertificateContent godoc
|
||||
// @Summary Get TLS certificate PEM content
|
||||
// @Tags TLSCertificates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Certificate ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/{id}/content [get]
|
||||
func GetTLSCertificateContent(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := service.GetTLSCertificateContent(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": content,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTLSCertificate godoc
|
||||
// @Summary Create TLS certificate from PEM
|
||||
// @Tags TLSCertificates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param payload body service.TLSCertificateInput true "TLS certificate payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/ [post]
|
||||
func CreateTLSCertificate(c *gin.Context) {
|
||||
var input service.TLSCertificateInput
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
certificate, err := service.CreateTLSCertificate(input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": certificate,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTLSCertificate godoc
|
||||
// @Summary Update TLS certificate from PEM
|
||||
// @Tags TLSCertificates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Certificate ID"
|
||||
// @Param payload body service.TLSCertificateInput true "TLS certificate payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/{id} [put]
|
||||
func UpdateTLSCertificate(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var input service.TLSCertificateInput
|
||||
if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
certificate, err := service.UpdateTLSCertificate(uint(id), input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": certificate,
|
||||
})
|
||||
}
|
||||
|
||||
// ImportTLSCertificateFile godoc
|
||||
// @Summary Import TLS certificate from files
|
||||
// @Tags TLSCertificates
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param name formData string true "Certificate name"
|
||||
// @Param remark formData string false "Remark"
|
||||
// @Param cert_file formData file true "Certificate file"
|
||||
// @Param key_file formData file true "Private key file"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/import-file [post]
|
||||
func ImportTLSCertificateFile(c *gin.Context) {
|
||||
name := c.PostForm("name")
|
||||
remark := c.PostForm("remark")
|
||||
certFile, err := c.FormFile("cert_file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "缺少证书文件",
|
||||
})
|
||||
return
|
||||
}
|
||||
keyFile, err := c.FormFile("key_file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "缺少私钥文件",
|
||||
})
|
||||
return
|
||||
}
|
||||
certificate, err := service.CreateTLSCertificateFromFiles(name, certFile, keyFile, remark)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": certificate,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTLSCertificate godoc
|
||||
// @Summary Delete TLS certificate
|
||||
// @Tags TLSCertificates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Certificate ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]interface{}
|
||||
// @Router /api/tls-certificates/{id} [delete]
|
||||
func DeleteTLSCertificate(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = service.DeleteTLSCertificate(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"openflare/service"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type confirmManualUpgradeRequest struct {
|
||||
UploadToken string `json:"upload_token"`
|
||||
}
|
||||
|
||||
type serverUpgradeRequest struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// GetLatestRelease godoc
|
||||
// @Summary Get latest GitHub release
|
||||
// @Tags Update
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/update/latest-release [get]
|
||||
func GetLatestRelease(c *gin.Context) {
|
||||
release, err := service.GetLatestServerRelease(c.Request.Context(), c.Query("channel"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": release,
|
||||
})
|
||||
}
|
||||
|
||||
// UpgradeServer godoc
|
||||
// @Summary Upgrade server binary from latest GitHub release
|
||||
// @Tags Update
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/update/upgrade [post]
|
||||
func UpgradeServer(c *gin.Context) {
|
||||
var request serverUpgradeRequest
|
||||
if c.Request.ContentLength > 0 {
|
||||
if err := c.ShouldBindJSON(&request); err != nil && !errors.Is(err, io.EOF) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
release, err := service.ScheduleServerUpgrade(request.Channel)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "服务升级任务已启动,下载完成后将自动重启。",
|
||||
"data": release,
|
||||
})
|
||||
}
|
||||
|
||||
// StreamServerUpgradeLogs godoc
|
||||
// @Summary Stream server upgrade logs over websocket
|
||||
// @Tags Update
|
||||
// @Router /api/update/logs/ws [get]
|
||||
func StreamServerUpgradeLogs(c *gin.Context) {
|
||||
websocket.Handler(func(conn *websocket.Conn) {
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
updates, unsubscribe := service.SubscribeServerUpgradeStream()
|
||||
defer unsubscribe()
|
||||
|
||||
heartbeatTicker := time.NewTicker(15 * time.Second)
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case snapshot, ok := <-updates:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := websocket.JSON.Send(conn, snapshot); err != nil {
|
||||
return
|
||||
}
|
||||
case <-heartbeatTicker.C:
|
||||
if err := websocket.JSON.Send(conn, service.ServerUpgradeStreamSnapshot{}); err != nil {
|
||||
return
|
||||
}
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}).ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
// UploadManualServerBinary godoc
|
||||
// @Summary Upload server binary and inspect version before upgrade
|
||||
// @Tags Update
|
||||
// @Accept mpfd
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/update/manual-upload [post]
|
||||
func UploadManualServerBinary(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("binary")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "请先选择要上传的服务端二进制文件。",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "读取上传文件失败。",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
info, err := service.UploadManualServerBinary(c.Request.Context(), fileHeader.Filename, file)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(info.ComparisonMessage)
|
||||
if message == "" {
|
||||
message = "已完成上传并检查升级包版本。"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": message,
|
||||
"data": info,
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmManualServerUpgrade godoc
|
||||
// @Summary Confirm upgrade with previously uploaded server binary
|
||||
// @Tags Update
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/update/manual-upgrade [post]
|
||||
func ConfirmManualServerUpgrade(c *gin.Context) {
|
||||
var request confirmManualUpgradeRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "升级确认参数无效。",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := service.ConfirmManualServerUpgrade(request.UploadToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "服务升级任务已启动,确认无误后将自动重启。",
|
||||
"data": info,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"openflare/utils/security"
|
||||
"openflare/utils/validation"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
if !common.PasswordLoginEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员关闭了密码登录",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
var loginRequest LoginRequest
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&loginRequest)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "无效的参数",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
username := loginRequest.Username
|
||||
password := loginRequest.Password
|
||||
if username == "" || password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "无效的参数",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
err = user.ValidateAndFill()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
setupLogin(&user, c)
|
||||
}
|
||||
|
||||
// setup session & cookies and then return user info
|
||||
func setupLogin(user *model.User, c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Set("id", user.Id)
|
||||
session.Set("username", user.Username)
|
||||
session.Set("role", user.Role)
|
||||
session.Set("status", user.Status)
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "无法保存会话信息,请重试",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
cleanUser := model.User{
|
||||
Id: user.Id,
|
||||
Username: user.Username,
|
||||
DisplayName: user.DisplayName,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "",
|
||||
"success": true,
|
||||
"data": cleanUser,
|
||||
})
|
||||
}
|
||||
|
||||
func Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "",
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func Register(c *gin.Context) {
|
||||
if !common.RegisterEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员关闭了新用户注册",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
if !common.PasswordRegisterEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := validation.Validate.Struct(&user); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if common.EmailVerificationEnabled {
|
||||
if user.Email == "" || user.VerificationCode == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "管理员开启了邮箱验证,请输入邮箱地址和验证码",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !security.VerifyCodeWithKey(user.Email, user.VerificationCode, security.EmailVerificationPurpose) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "验证码错误或已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
cleanUser := model.User{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
DisplayName: user.Username,
|
||||
}
|
||||
if common.EmailVerificationEnabled {
|
||||
cleanUser.Email = user.Email
|
||||
}
|
||||
if err := cleanUser.Insert(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetAllUsers(c *gin.Context) {
|
||||
p, _ := strconv.Atoi(c.Query("p"))
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": users,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func SearchUsers(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
users, err := model.SearchUsers(keyword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": users,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetUser(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
myRole := c.GetInt("role")
|
||||
if myRole <= user.Role {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权获取同级或更高等级用户的信息",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": user,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateToken(c *gin.Context) {
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.Token = uuid.New().String()
|
||||
user.Token = strings.Replace(user.Token, "-", "", -1)
|
||||
|
||||
if model.DB.Where("token = ?", user.Token).First(user).RowsAffected != 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "请重试,系统生成的 UUID 竟然重复了!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Update(false); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": user.Token,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetSelf(c *gin.Context) {
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": user,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateUser(c *gin.Context) {
|
||||
var updatedUser model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
||||
if err != nil || updatedUser.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if updatedUser.Password == "" {
|
||||
updatedUser.Password = "$I_LOVE_U" // make Validator happy :)
|
||||
}
|
||||
if err := validation.Validate.Struct(&updatedUser); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
originUser, err := model.GetUserById(updatedUser.Id, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
myRole := c.GetInt("role")
|
||||
if myRole <= originUser.Role {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
||||
})
|
||||
return
|
||||
}
|
||||
if myRole <= updatedUser.Role {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权将其他用户权限等级提升到大于等于自己的权限等级",
|
||||
})
|
||||
return
|
||||
}
|
||||
if updatedUser.Password == "$I_LOVE_U" {
|
||||
updatedUser.Password = "" // rollback to what it should be
|
||||
}
|
||||
updatePassword := updatedUser.Password != ""
|
||||
if err := updatedUser.Update(updatePassword); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateSelf(c *gin.Context) {
|
||||
var user model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Password == "" {
|
||||
user.Password = "$I_LOVE_U" // make Validator happy :)
|
||||
}
|
||||
if err := validation.Validate.Struct(&user); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cleanUser := model.User{
|
||||
Id: c.GetInt("id"),
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
DisplayName: user.DisplayName,
|
||||
}
|
||||
if user.Password == "$I_LOVE_U" {
|
||||
user.Password = "" // rollback to what it should be
|
||||
cleanUser.Password = ""
|
||||
}
|
||||
updatePassword := user.Password != ""
|
||||
if err := cleanUser.Update(updatePassword); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteUser(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
originUser, err := model.GetUserById(id, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
myRole := c.GetInt("role")
|
||||
if myRole <= originUser.Role {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权删除同权限等级或更高权限等级的用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = model.DeleteUserById(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSelf(c *gin.Context) {
|
||||
id := c.GetInt("id")
|
||||
err := model.DeleteUserById(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func CreateUser(c *gin.Context) {
|
||||
var user model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||
if err != nil || user.Username == "" || user.Password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.DisplayName == "" {
|
||||
user.DisplayName = user.Username
|
||||
}
|
||||
myRole := c.GetInt("role")
|
||||
if user.Role >= myRole {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法创建权限大于等于自己的用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Even for admin users, we cannot fully trust them!
|
||||
cleanUser := model.User{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
DisplayName: user.DisplayName,
|
||||
}
|
||||
if err := cleanUser.Insert(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type ManageRequest struct {
|
||||
Username string `json:"username"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// ManageUser Only admin user can do this
|
||||
func ManageUser(c *gin.Context) {
|
||||
var req ManageRequest
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&req)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
Username: req.Username,
|
||||
}
|
||||
// Fill attributes
|
||||
model.DB.Where(&user).First(&user)
|
||||
if user.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "用户不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
myRole := c.GetInt("role")
|
||||
if myRole <= user.Role && myRole != common.RoleRootUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权更新同权限等级或更高权限等级的用户信息",
|
||||
})
|
||||
return
|
||||
}
|
||||
switch req.Action {
|
||||
case "disable":
|
||||
user.Status = common.UserStatusDisabled
|
||||
if user.Role == common.RoleRootUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法禁用超级管理员用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
case "enable":
|
||||
user.Status = common.UserStatusEnabled
|
||||
case "delete":
|
||||
if user.Role == common.RoleRootUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法删除超级管理员用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := user.Delete(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
case "promote":
|
||||
if myRole != common.RoleRootUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "普通管理员用户无法提升其他用户为管理员",
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Role >= common.RoleAdminUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该用户已经是管理员",
|
||||
})
|
||||
return
|
||||
}
|
||||
user.Role = common.RoleAdminUser
|
||||
case "demote":
|
||||
if user.Role == common.RoleRootUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法降级超级管理员用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Role == common.RoleCommonUser {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该用户已经是普通用户",
|
||||
})
|
||||
return
|
||||
}
|
||||
user.Role = common.RoleCommonUser
|
||||
}
|
||||
|
||||
if err := user.Update(false); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
clearUser := model.User{
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": clearUser,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func EmailBind(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
code := c.Query("code")
|
||||
if !security.VerifyCodeWithKey(email, code, security.EmailVerificationPurpose) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "验证码错误或已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
id := c.GetInt("id")
|
||||
user := model.User{
|
||||
Id: id,
|
||||
}
|
||||
err := user.FillUserById()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.Email = email
|
||||
// no need to check if this email already taken, because we have used verification code to check it
|
||||
err = user.Update(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wechatLoginResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func getWeChatIdByCode(code string) (string, error) {
|
||||
if code == "" {
|
||||
return "", errors.New("无效的参数")
|
||||
}
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, code), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", common.WeChatServerToken)
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
httpResponse, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
var res wechatLoginResponse
|
||||
err = json.NewDecoder(httpResponse.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !res.Success {
|
||||
return "", errors.New(res.Message)
|
||||
}
|
||||
if res.Data == "" {
|
||||
return "", errors.New("验证码错误或已过期")
|
||||
}
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func WeChatAuth(c *gin.Context) {
|
||||
if !common.WeChatAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员未开启通过微信登录以及注册",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
wechatId, err := getWeChatIdByCode(code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
user := model.User{
|
||||
WeChatId: wechatId,
|
||||
}
|
||||
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
||||
err := user.FillUserByWeChatId()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if common.RegisterEnabled {
|
||||
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||
user.DisplayName = "WeChat User"
|
||||
user.Role = common.RoleCommonUser
|
||||
user.Status = common.UserStatusEnabled
|
||||
|
||||
if err := user.Insert(); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "管理员关闭了新用户注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user.Status != common.UserStatusEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "用户已被封禁",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
setupLogin(&user, c)
|
||||
}
|
||||
|
||||
func WeChatBind(c *gin.Context) {
|
||||
if !common.WeChatAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员未开启通过微信登录以及注册",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
wechatId, err := getWeChatIdByCode(code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
if model.IsWeChatIdAlreadyTaken(wechatId) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该微信账号已被绑定",
|
||||
})
|
||||
return
|
||||
}
|
||||
id := c.GetInt("id")
|
||||
user := model.User{
|
||||
Id: id,
|
||||
}
|
||||
err = user.FillUserById()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.WeChatId = wechatId
|
||||
err = user.Update(false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user