This commit is contained in:
wjqserver 2025-06-20 16:33:27 +08:00
commit b10790c212
40 changed files with 4149 additions and 0 deletions

92
api/api.go Normal file
View file

@ -0,0 +1,92 @@
package api
import (
"caddydash/apic"
"caddydash/config"
"caddydash/db"
"caddydash/user"
"github.com/infinite-iroha/touka"
)
func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) {
api := v0.Group("/api")
api.GET("/config/filenames", func(c *touka.Context) {
filenames, err := cdb.GetFileNames()
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, filenames)
})
// 配置参数相关
api.GET("/config/file/:filename", GetConfig(cdb)) // 读取配置(与写入一致)
api.PUT("/config/file/:filename", PutConfig(cdb, cfg)) // 写入配置
api.DELETE("/config/file/:filename", DeleteConfig(cdb, cfg)) //删除配置
api.GET("/config/files/params", FilesParams(cdb)) // 获取所有配置, 需进行decode
api.GET("/config/files/templates", FilesTemplates(cdb)) // 获取所有模板
api.GET("/config/files/rendered", FilesRendered(cdb)) // 获取所有渲染产物
api.GET("/config/templates", GetTemplates(cdb)) // 获取可用模板名称
// caddy实例相关
api.POST("/caddy/stop", apic.StopCaddy()) // 无需payload
api.POST("/caddy/run", apic.StartCaddy(cfg))
api.POST("/caddy/restart", apic.RestartCaddy(cfg))
api.GET("/caddy/status", apic.IsCaddyRunning())
auth := api.Group("/auth")
{
auth.POST("/login", func(c *touka.Context) {
AuthLogin(c, cfg, cdb)
})
auth.POST("/logout", func(c *touka.Context) {
AuthLogout(c)
})
auth.GET("/logout", func(c *touka.Context) {
// 重定向到/
c.Redirect(302, "/")
c.Abort()
return
})
auth.GET("/init", func(c *touka.Context) {
// 返回是否init管理员
isInit := user.IsAdminInit()
if isInit {
c.JSON(200, touka.H{"admin_init": true})
} else {
c.JSON(200, touka.H{"admin_init": false})
}
})
auth.POST("/init", func(c *touka.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
// 验证是否为空
if username == "" || password == "" {
c.JSON(400, touka.H{"error": "username and password are required"})
return
}
// 初始化管理员
err := user.InitAdminUser(username, password, cdb)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, touka.H{"message": "admin initialized"})
})
}
}
// GetTemplates 获取可用的tmpls name
func GetTemplates(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
templates, err := cdb.RangeTemplates()
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, templates)
}
}

127
api/auth.go Normal file
View file

@ -0,0 +1,127 @@
package api
import (
"caddydash/config"
"caddydash/db"
"caddydash/user"
"net/http"
"strings"
"github.com/fenthope/sessions"
"github.com/infinite-iroha/touka"
)
var (
exactMatchPaths = map[string]struct{}{
"/login": {},
"/login.html": {},
"/v0/api/auth/login": {},
"/v0/api/auth/init": {},
"/init.html": {},
"/favicon.ico": {},
}
prefixMatchPaths = []string{ // 保持前缀匹配,因为数量少
"/js/",
"/css/",
}
loginMatchPaths = map[string]struct{}{
"/login": {},
"/login.html": {},
"/v0/api/auth/login": {},
}
initMatchPaths = map[string]struct{}{
"/v0/api/auth/init": {},
"/init.html": {},
}
)
func isPassPath(requestPath string) bool {
// 精确匹配
if _, ok := exactMatchPaths[requestPath]; ok {
return true
}
// 前缀匹配
for _, prefix := range prefixMatchPaths {
if strings.HasPrefix(requestPath, prefix) {
return true
}
}
return false
}
func isLoginPath(requestPath string) bool {
if _, ok := loginMatchPaths[requestPath]; ok {
return true
}
return false
}
func isInitPath(requestPath string) bool {
if _, ok := initMatchPaths[requestPath]; ok {
return true
}
return false
}
func SessionMiddleware(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
session := sessions.Default(c)
requestPath := c.Request.URL.Path
pass := isPassPath(requestPath)
if !user.IsAdminInit() && !pass || !user.IsAdminInit() && isLoginPath(requestPath) {
c.Redirect(http.StatusFound, "/init.html")
c.Abort()
return
} else if user.IsAdminInit() && isInitPath(requestPath) {
c.Redirect(http.StatusFound, "/login.html")
c.Abort()
return
}
if session.Get("authenticated") != true && !pass {
c.Redirect(http.StatusFound, "/login.html")
c.Abort()
return
}
c.Next()
}
}
func AuthLogin(c *touka.Context, cfg *config.Config, cdb *db.ConfigDB) {
username := c.PostForm("username")
password := c.PostForm("password")
// 输入验证
if username == "" || password == "" {
c.Errorf("Username or password not provided")
c.JSON(http.StatusBadRequest, touka.H{"error": "Need username and password"})
return
}
// 验证账户密码
pass, err := user.CheckLogin(username, password, cdb)
if err != nil {
c.Errorf("Failed to check login: %v", err)
c.JSON(http.StatusInternalServerError, touka.H{"error": "Internal Auth Check Error"})
return
}
if !pass {
c.Errorf("Invalid username or password")
c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid username or password"})
return
}
session := sessions.Default(c)
session.Set("authenticated", true)
session.Save()
c.Infof("Login successful for user: %s", username)
c.JSON(http.StatusOK, touka.H{"success": true})
}
func AuthLogout(c *touka.Context) {
session := sessions.Default(c)
session.Clear()
session.Set("authenticated", false)
session.Save()
c.Redirect(http.StatusFound, "/login.html")
}

225
api/config.go Normal file
View file

@ -0,0 +1,225 @@
package api
import (
"caddydash/config"
"caddydash/db"
"caddydash/gen"
"fmt"
"os"
"github.com/infinite-iroha/touka"
)
func GetConfig(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
filename := c.Param("filename")
params, err := cdb.GetParams(filename)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
// 解码[]byte的gob数据
var config gen.CaddyUniConfig
err = gen.DecodeGobConfig(params.ParamsOrigin, &config)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, config)
}
}
func PutConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
filename := c.Param("filename")
var config gen.CaddyUniConfig
err := c.ShouldBindJSON(&config)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
var paramsGOB []byte
var paramsOrigin []byte
// Mode标识符固定为uni, 模板已被统合为只有uni
paramsGOB, err = gen.EncodeGobConfig(config)
if err != nil {
c.Warnf("encode gob config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
// 把json变为gob []byte
paramsOrigin, err = gen.EncodeGobConfig(config)
if err != nil {
c.Warnf("encode origin config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
paramsEntry := db.ParamsEntry{
Filename: filename,
TemplateType: config.Mode,
ParamsGOB: paramsGOB,
ParamsOrigin: paramsOrigin,
}
err = WriteConfig(cdb, paramsEntry, cfg, filename)
if err != nil {
c.Warnf("write config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, touka.H{"message": "config saved and rendered"})
return
}
}
// 渲染并写入配置
func WriteConfig(cdb *db.ConfigDB, paramsEntry db.ParamsEntry, cfg *config.Config, filename string) error {
var err error
err = cdb.SaveParams(paramsEntry)
if err != nil {
err = fmt.Errorf("save params error: %w", err)
return err
}
err = gen.RenderConfig(filename, cdb)
if err != nil {
err = fmt.Errorf("render config error: %w", err)
return err
}
// 写入文件
renderedEntry, err := cdb.GetRenderedConfig(filename)
if err != nil {
err = fmt.Errorf("get rendered config error: %w", err)
return err
}
err = os.WriteFile(cfg.Server.CaddyDir+"config.d/"+filename, renderedEntry.RenderedContent, 0644)
if err != nil {
err = fmt.Errorf("write rendered config file error: %w", err)
return err
}
return nil
}
func DeleteConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
filename := c.Param("filename")
err := cdb.DeleteParams(filename)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
// 删除文件
err = os.Remove(cfg.Server.CaddyDir + "config.d/" + filename)
if err != nil {
c.Warnf("delete rendered config file error: %v", err)
}
c.JSON(200, touka.H{"message": "config deleted"})
}
}
/*
func PutConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc {
return func(c *touka.Context) {
filename := c.Param("filename")
var config gen.CaddyUniConfig
err := c.ShouldBindJSON(&config)
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
var (
paramsGOB []byte
paramsOrigin []byte
)
switch config.TmplType {
case "file_server":
caddyfscfg := gen.CaddyFileServerConfig{
Domain: config.Domain,
FileDirPath: config.FileServer.FileDirPath,
EnableBrowser: config.FileServer.EnableBrowser,
Headers: gen.HeadersMapToHeadersUp(config.Headers),
EnableLog: config.Log.EnableLog,
LogDomain: config.Log.LogDomain,
EnableErrorPage: config.ErrorPage.EnableErrorPage,
EnableEncode: config.Encode.EnableEncode,
}
paramsGOB, err = gen.EncodeGobConfig(caddyfscfg)
if err != nil {
c.Warnf("encode gob config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
// 把json变为gob []byte
paramsOrigin, err = gen.EncodeGobConfig(config)
if err != nil {
c.Warnf("encode origin config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
paramsEntry := db.ParamsEntry{
Filename: filename,
TemplateType: config.TmplType,
ParamsGOB: paramsGOB,
ParamsOrigin: paramsOrigin,
}
err = WriteConfig(cdb, paramsEntry, cfg, filename)
if err != nil {
c.Warnf("write config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, touka.H{"message": "config saved and rendered"})
return
case "reverse_proxy":
caddyrpCfg := gen.CaddyReverseProxyConfig{
Domain: config.Domain,
ReverseProxy: config.UpStream.UpStream,
Headers: gen.HeadersMapToHeadersUp(config.Headers),
HeadersUp: gen.HeadersMapToHeadersUp(config.UpStream.UpStreamHeaders),
EnableLog: config.Log.EnableLog,
LogDomain: config.Log.LogDomain,
EnableErrorPage: config.ErrorPage.EnableErrorPage,
EnableEncode: config.Encode.EnableEncode,
}
paramsGOB, err = gen.EncodeGobConfig(caddyrpCfg)
if err != nil {
c.Warnf("encode gob config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
// 把json变为gob []byte
paramsOrigin, err = gen.EncodeGobConfig(config)
if err != nil {
c.Warnf("encode origin config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
paramsEntry := db.ParamsEntry{
Filename: filename,
TemplateType: config.TmplType,
ParamsGOB: paramsGOB,
ParamsOrigin: paramsOrigin,
}
err = WriteConfig(cdb, paramsEntry, cfg, filename)
if err != nil {
c.Warnf("write config error: %v", err)
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, touka.H{"message": "config saved and rendered"})
return
default:
c.JSON(500, touka.H{"error": "unknown template type"})
return
}
}
}
*/

40
api/files.go Normal file
View file

@ -0,0 +1,40 @@
package api
import (
"caddydash/db"
"github.com/infinite-iroha/touka"
)
func FilesParams(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
params, err := cdb.RangeAllParams()
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, params)
}
}
func FilesTemplates(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
templates, err := cdb.GetAllTemplates()
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, templates)
}
}
func FilesRendered(cdb *db.ConfigDB) touka.HandlerFunc {
return func(c *touka.Context) {
rendered, err := cdb.RangeAllReandered()
if err != nil {
c.JSON(500, touka.H{"error": err.Error()})
return
}
c.JSON(200, rendered)
}
}