目录重构完成,但是Preview通道尚未可用
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"frpc-console/internal/auth"
|
||||
"frpc-console/internal/db"
|
||||
"frpc-console/internal/frp"
|
||||
)
|
||||
|
||||
// ================================================================
|
||||
// 认证 Handler
|
||||
// ================================================================
|
||||
|
||||
func CheckUsersHandler(c *gin.Context) {
|
||||
count, err := db.CountUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": gin.H{"hasUsers": count > 0, "count": count},
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
count, err := db.CountUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 1, "msg": "已存在管理员账户,请登录"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Username) < 5 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "用户名至少 5 位"})
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.ValidatePassword(req.Password) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.CreateUser(req.Username, string(hash)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.GenerateJWT(req.Username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "注册成功",
|
||||
"data": gin.H{"token": token},
|
||||
})
|
||||
}
|
||||
|
||||
func LoginHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := db.GetUserByUsername(req.Username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.GenerateJWT(user.Username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "登录成功",
|
||||
"data": gin.H{"token": token},
|
||||
})
|
||||
}
|
||||
|
||||
func ChangePasswordHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
username := auth.GetUsernameFromContext(c)
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := db.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "当前密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.ValidatePassword(req.NewPassword) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.UpdateUserPassword(username, string(hash)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 配置 Handler
|
||||
// ================================================================
|
||||
|
||||
func GetConfigHandler(c *gin.Context) {
|
||||
cfg, err := db.GetGlobalConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg})
|
||||
}
|
||||
|
||||
func UpdateConfigHandler(c *gin.Context) {
|
||||
var cfg db.GlobalConfig
|
||||
if err := c.ShouldBindJSON(&cfg); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
cfg.TcpMux = true
|
||||
|
||||
if err := db.UpdateGlobalConfig(&cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := frp.GenerateConfig(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := frp.Reload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 隧道 Handler
|
||||
// ================================================================
|
||||
|
||||
func GetProxiesHandler(c *gin.Context) {
|
||||
proxies, err := db.GetProxies()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道列表失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies})
|
||||
}
|
||||
|
||||
func GetProxyHandler(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
p, err := db.GetProxy(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": p})
|
||||
}
|
||||
|
||||
func CreateProxyHandler(c *gin.Context) {
|
||||
var p db.Proxy
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
p.Enabled = true
|
||||
|
||||
if err := db.CreateProxy(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}})
|
||||
}
|
||||
|
||||
func UpdateProxyHandler(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
var p db.Proxy
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
p.ID = id
|
||||
|
||||
if err := db.UpdateProxy(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"})
|
||||
}
|
||||
|
||||
func DeleteProxyHandler(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DeleteProxy(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// frpc 进程管理 Handler
|
||||
// ================================================================
|
||||
|
||||
func ReloadFrpcHandler(c *gin.Context) {
|
||||
if err := frp.GenerateConfig(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := frp.Reload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"})
|
||||
}
|
||||
|
||||
func StartFrpcHandler(c *gin.Context) {
|
||||
if err := frp.Start(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 启动成功"})
|
||||
}
|
||||
|
||||
func StopFrpcHandler(c *gin.Context) {
|
||||
if err := frp.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 已停止"})
|
||||
}
|
||||
|
||||
func GetFrpcStatusHandler(c *gin.Context) {
|
||||
status, err := frp.GetStatus()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": status})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Ping Handler
|
||||
// ================================================================
|
||||
|
||||
func PingHandler(c *gin.Context) {
|
||||
target := c.Query("target")
|
||||
if target == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := db.GetGlobalConfig()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
port := cfg.ServerPort
|
||||
address := net.JoinHostPort(target, strconv.Itoa(port))
|
||||
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1})
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
latency := time.Since(start).Milliseconds()
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 日志 Handler
|
||||
// ================================================================
|
||||
|
||||
func GetFrpcLogHandler(c *gin.Context) {
|
||||
lines, err := frp.ReadTailLog("./data/frpc.log", 200)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": gin.H{
|
||||
"lines": []string{},
|
||||
"total": 0,
|
||||
"error": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": gin.H{
|
||||
"lines": lines,
|
||||
"total": len(lines),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 辅助函数
|
||||
// ================================================================
|
||||
|
||||
func generateAndReload() error {
|
||||
if err := frp.GenerateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
return frp.Reload()
|
||||
}
|
||||
|
||||
// readTailLog 读取文件末尾 n 行 (临时放在这里,后续移到独立包)
|
||||
func readTailLog(filePath string, n int) ([]string, error) {
|
||||
// 这个函数在 frp 模块中也有,但为了避免循环依赖,在这里实现一份简单的
|
||||
// 或者直接调用 frp.ReadTailLog 如果导出的话
|
||||
// 目前保持和原来一致,后续可以统一到 pkg/utils
|
||||
// 为了编译通过,先简单返回空
|
||||
return []string{}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"frpc-console/internal/auth"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
// SetupRouter 设置路由
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 从 embed 读取前端静态文件
|
||||
staticSubFS, _ := fs.Sub(staticFS, "static")
|
||||
r.StaticFS("/static", http.FS(staticSubFS))
|
||||
|
||||
// 根路由
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
content, err := staticFS.ReadFile("static/index.html")
|
||||
if err != nil {
|
||||
c.String(500, "加载前端页面失败")
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
|
||||
})
|
||||
|
||||
// 健康检查
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "frpc-console 后端已启动 🎉")
|
||||
})
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// ---- 公开路由(不需要认证) ----
|
||||
api.GET("/check/users", CheckUsersHandler)
|
||||
api.POST("/register", RegisterHandler)
|
||||
api.POST("/login", LoginHandler)
|
||||
api.GET("/ping", PingHandler)
|
||||
|
||||
// ---- 需要认证的路由 ----
|
||||
authGroup := api.Group("/")
|
||||
authGroup.Use(auth.AuthMiddleware())
|
||||
{
|
||||
authGroup.GET("/config", GetConfigHandler)
|
||||
authGroup.PUT("/config", UpdateConfigHandler)
|
||||
|
||||
authGroup.GET("/proxies", GetProxiesHandler)
|
||||
authGroup.GET("/proxy/:id", GetProxyHandler)
|
||||
authGroup.POST("/proxy", CreateProxyHandler)
|
||||
authGroup.PUT("/proxy/:id", UpdateProxyHandler)
|
||||
authGroup.DELETE("/proxy/:id", DeleteProxyHandler)
|
||||
|
||||
authGroup.POST("/frpc/reload", ReloadFrpcHandler)
|
||||
authGroup.POST("/frpc/start", StartFrpcHandler)
|
||||
authGroup.POST("/frpc/stop", StopFrpcHandler)
|
||||
authGroup.GET("/frpc/status", GetFrpcStatusHandler)
|
||||
authGroup.GET("/frpc/log", GetFrpcLogHandler)
|
||||
|
||||
authGroup.POST("/import/toml", ImportTomlHandler)
|
||||
authGroup.GET("/export/toml", ExportTomlHandler)
|
||||
|
||||
authGroup.PUT("/user/password", ChangePasswordHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user