目录重构完成,但是Preview通道尚未可用

This commit is contained in:
2026-08-07 19:42:49 +08:00
parent 3a381c85ed
commit 86df4bf3c5
16 changed files with 1620 additions and 24 deletions
+417
View File
@@ -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
}
+74
View File
@@ -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
}