加入首次使用的引导页、写入readme文件
This commit is contained in:
@@ -11,59 +11,126 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// SetupRouter 配置所有路由
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 静态文件(前端)
|
||||
r.Static("/static", "./static")
|
||||
|
||||
// 健康检查
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "frpc-console 后端已启动 🎉")
|
||||
})
|
||||
|
||||
// 前端入口
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.File("./static/index.html")
|
||||
})
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 登录(无需鉴权)
|
||||
// 公开接口
|
||||
api.GET("/check/users", checkUsersHandler)
|
||||
api.POST("/register", registerHandler)
|
||||
api.POST("/login", loginHandler)
|
||||
|
||||
// 需要鉴权的路由
|
||||
// 需鉴权
|
||||
auth := api.Group("/")
|
||||
auth.Use(AuthMiddleware())
|
||||
{
|
||||
// 全局配置
|
||||
auth.GET("/config", getConfigHandler)
|
||||
auth.PUT("/config", updateConfigHandler)
|
||||
|
||||
// 隧道管理
|
||||
auth.GET("/proxies", getProxiesHandler)
|
||||
auth.GET("/proxy/:id", getProxyHandler)
|
||||
auth.POST("/proxy", createProxyHandler)
|
||||
auth.PUT("/proxy/:id", updateProxyHandler)
|
||||
auth.DELETE("/proxy/:id", deleteProxyHandler)
|
||||
|
||||
// frpc 控制
|
||||
auth.POST("/frpc/reload", reloadFrpcHandler)
|
||||
auth.POST("/frpc/start", startFrpcHandler)
|
||||
auth.POST("/frpc/stop", stopFrpcHandler)
|
||||
auth.GET("/frpc/status", getFrpcStatusHandler)
|
||||
|
||||
// 🟢 导入 TOML(新增)
|
||||
auth.POST("/import/toml", importTomlHandler)
|
||||
auth.GET("/export/toml", ExportTomlHandler)
|
||||
|
||||
auth.PUT("/user/password", changePasswordHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// ========== 检查是否有用户 ==========
|
||||
func checkUsersHandler(c *gin.Context) {
|
||||
count, err := 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 := 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 !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 := CreateUser(req.Username, string(hash)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := 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 {
|
||||
@@ -99,6 +166,56 @@ func loginHandler(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 修改密码 ==========
|
||||
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, exists := c.Get("username")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := GetUserByUsername(username.(string))
|
||||
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 !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 := UpdatePassword(username.(string), string(hash)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
||||
}
|
||||
|
||||
// ========== 全局配置 ==========
|
||||
func getConfigHandler(c *gin.Context) {
|
||||
cfg, err := GetGlobalConfig()
|
||||
@@ -115,8 +232,6 @@ func updateConfigHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 强制 tcpMux 为 true
|
||||
cfg.TcpMux = true
|
||||
|
||||
if err := UpdateGlobalConfig(&cfg); err != nil {
|
||||
@@ -188,7 +303,6 @@ func updateProxyHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var p Proxy
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
@@ -267,7 +381,7 @@ func getFrpcStatusHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
|
||||
}
|
||||
|
||||
// ========== 🟢 导入 TOML(新增) ==========
|
||||
// ========== 导入 TOML ==========
|
||||
func importTomlHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -294,20 +408,13 @@ func importTomlHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新全局配置(保持当前 tcpMux 值)
|
||||
cfg := parsed.ToGlobalConfig()
|
||||
currentCfg, _ := GetGlobalConfig()
|
||||
if currentCfg != nil {
|
||||
cfg.TcpMux = currentCfg.TcpMux // 保持当前值不变
|
||||
}
|
||||
cfg.TcpMux = true // 强制开启
|
||||
|
||||
cfg.TcpMux = true
|
||||
if err := UpdateGlobalConfig(cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 清空并导入隧道
|
||||
if _, err := DB.Exec("DELETE FROM proxies"); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"})
|
||||
return
|
||||
@@ -321,13 +428,11 @@ func importTomlHandler(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成配置并热加载
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := ReloadFrpc(); err != nil {
|
||||
// 导入成功但热加载失败,不影响整体结果
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()),
|
||||
@@ -341,7 +446,7 @@ func importTomlHandler(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ExportTomlHandler 导出 TOML 配置文件
|
||||
// ========== 导出 TOML ==========
|
||||
func ExportTomlHandler(c *gin.Context) {
|
||||
cfg, err := GetGlobalConfig()
|
||||
if err != nil {
|
||||
@@ -370,7 +475,6 @@ func ExportTomlHandler(c *gin.Context) {
|
||||
Proxies: activeProxies,
|
||||
}
|
||||
|
||||
// 解析模板(直接使用 frp.go 中的 frpcTemplateContent)
|
||||
tmpl, err := template.New("frpc").Parse(FrpcTemplateContent)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()})
|
||||
@@ -383,13 +487,12 @@ func ExportTomlHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 返回文件下载
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.Header("Content-Disposition", "attachment; filename=frpc.toml")
|
||||
c.String(http.StatusOK, buf.String())
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
// ========== 辅助 ==========
|
||||
func generateAndReload() error {
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user