Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c58c4f866a | ||
|
|
fc9f1b6bcc | ||
|
|
be7b6e9c5e | ||
|
|
a503f0cf40 | ||
|
|
0b7f43b149 | ||
|
|
55f5c89c1c | ||
|
|
937696c26d | ||
|
|
08aaea8244 | ||
|
|
b886f70e48 | ||
|
|
12c6b8a6f6 | ||
|
|
ebcc8a6efb | ||
|
|
c4d8b63d32 | ||
|
|
318da135b9 | ||
|
|
da74bf4749 | ||
|
|
3250283ef9 | ||
|
|
2b40040ec5 | ||
|
|
60ada33ee9 | ||
|
|
7940e010e0 |
@@ -21,7 +21,7 @@ jobs:
|
|||||||
name: Build artifacts
|
name: Build artifacts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: goreleaser/goreleaser-cross:v1.21
|
image: goreleaser/goreleaser-cross:v1.23
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.21.x"
|
go-version: "1.23.x"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
@@ -75,7 +75,8 @@ jobs:
|
|||||||
for file in assets/*/*/*; do
|
for file in assets/*/*/*; do
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
chmod +x "$file"
|
chmod +x "$file"
|
||||||
zip -jr "$file.zip" "$file"
|
export fileWithoutExt=${file%.*}
|
||||||
|
zip -jr "$fileWithoutExt.zip" "$file"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.21.x"
|
go-version: "1.23.x"
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
run: |
|
run: |
|
||||||
@@ -42,4 +42,4 @@ jobs:
|
|||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
uses: securego/gosec@master
|
uses: securego/gosec@master
|
||||||
with:
|
with:
|
||||||
args: --exclude=G104,G402 ./...
|
args: --exclude=G104,G402,G115,G203 ./...
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||||
<br><br>
|
<br><br>
|
||||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.19.0-brightgreen?style=for-the-badge&logo=linux">
|
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.19.2-brightgreen?style=for-the-badge&logo=linux">
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
||||||
@@ -59,6 +59,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
|
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
|
||||||
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
|
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
|
||||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
|
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
|
||||||
|
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
|
||||||
<a href="https://github.com/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
|
<a href="https://github.com/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
|
||||||
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
|
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
|
||||||
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
|
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
|
||||||
@@ -75,9 +76,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
|
<a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
|
||||||
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
|
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
|
||||||
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
|
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
|
||||||
|
<a href="https://github.com/lyj0309" title="lyj"><img src="https://avatars.githubusercontent.com/u/50474995?v=4" width="50;" alt="lyj"/></a>
|
||||||
<a href="https://github.com/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
|
<a href="https://github.com/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
|
||||||
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
|
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
|
||||||
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
|
|
||||||
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
|
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
|
||||||
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
|
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
|
||||||
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
|
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,28 +34,16 @@ func ServeWeb(port uint) *http.Server {
|
|||||||
pprof.Register(r)
|
pprof.Register(r)
|
||||||
}
|
}
|
||||||
r.Use(natGateway)
|
r.Use(natGateway)
|
||||||
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" {
|
tmpl := template.New("").Funcs(funcMap)
|
||||||
r.SetFuncMap(funcMap)
|
var err error
|
||||||
r.Use(mygin.RecordPath)
|
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
||||||
r.Static("/static", "resource/static")
|
if err != nil {
|
||||||
r.LoadHTMLGlob("resource/template/**/*.html")
|
panic(err)
|
||||||
} else {
|
|
||||||
tmpl := template.New("").Funcs(funcMap)
|
|
||||||
var err error
|
|
||||||
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
tmpl = loadThirdPartyTemplates(tmpl)
|
|
||||||
r.SetHTMLTemplate(tmpl)
|
|
||||||
r.Use(mygin.RecordPath)
|
|
||||||
staticFs, err := fs.Sub(resource.StaticFS, "static")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
r.StaticFS("/static", http.FS(staticFs))
|
|
||||||
}
|
}
|
||||||
r.Static("/static-custom", "resource/static/custom")
|
tmpl = loadThirdPartyTemplates(tmpl)
|
||||||
|
r.SetHTMLTemplate(tmpl)
|
||||||
|
r.Use(mygin.RecordPath)
|
||||||
|
r.StaticFS("/static", http.FS(resource.StaticFS))
|
||||||
routers(r)
|
routers(r)
|
||||||
page404 := func(c *gin.Context) {
|
page404 := func(c *gin.Context) {
|
||||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||||
@@ -96,7 +84,7 @@ func routers(r *gin.Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
||||||
var ret = tmpl
|
ret := tmpl
|
||||||
themes, err := os.ReadDir("resource/template")
|
themes, err := os.ReadDir("resource/template")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
||||||
@@ -106,17 +94,61 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
|||||||
if !theme.IsDir() {
|
if !theme.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// load templates
|
|
||||||
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
|
themeDir := theme.Name()
|
||||||
if err != nil {
|
if themeDir == "theme-custom" {
|
||||||
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
|
// for backward compatibility
|
||||||
|
// note: will remove this in future versions
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ret = t
|
|
||||||
|
if strings.HasPrefix(themeDir, "dashboard-") {
|
||||||
|
// load dashboard templates, ignore desc file
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(themeDir, "theme-") {
|
||||||
|
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
|
||||||
|
desc, err := os.ReadFile(filepath.Clean(descPath))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
themeName, err := utils.GjsonGet(desc, "name")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// load templates
|
||||||
|
ret = loadTemplates(ret, themeDir)
|
||||||
|
|
||||||
|
themeKey := strings.TrimPrefix(themeDir, "theme-")
|
||||||
|
model.Themes[themeKey] = themeName.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
|
||||||
|
// load templates
|
||||||
|
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
|
||||||
|
t, err := tmpl.ParseGlob(templatePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
|
||||||
|
return tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
var funcMap = template.FuncMap{
|
var funcMap = template.FuncMap{
|
||||||
"tr": func(id string, dataAndCount ...interface{}) string {
|
"tr": func(id string, dataAndCount ...interface{}) string {
|
||||||
conf := i18n.LocalizeConfig{
|
conf := i18n.LocalizeConfig{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
@@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {
|
|||||||
|
|
||||||
mr.GET("/search-server", ma.searchServer)
|
mr.GET("/search-server", ma.searchServer)
|
||||||
mr.GET("/search-tasks", ma.searchTask)
|
mr.GET("/search-tasks", ma.searchTask)
|
||||||
|
mr.GET("/search-ddns", ma.searchDDNS)
|
||||||
mr.POST("/server", ma.addOrEditServer)
|
mr.POST("/server", ma.addOrEditServer)
|
||||||
mr.POST("/monitor", ma.addOrEditMonitor)
|
mr.POST("/monitor", ma.addOrEditMonitor)
|
||||||
mr.POST("/cron", ma.addOrEditCron)
|
mr.POST("/cron", ma.addOrEditCron)
|
||||||
@@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
|
|||||||
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
||||||
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
||||||
mr.POST("/notification", ma.addOrEditNotification)
|
mr.POST("/notification", ma.addOrEditNotification)
|
||||||
|
mr.POST("/ddns", ma.addOrEditDDNS)
|
||||||
mr.POST("/nat", ma.addOrEditNAT)
|
mr.POST("/nat", ma.addOrEditNAT)
|
||||||
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
||||||
mr.POST("/setting", ma.updateSetting)
|
mr.POST("/setting", ma.updateSetting)
|
||||||
@@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
singleton.OnDeleteNotification(id)
|
singleton.OnDeleteNotification(id)
|
||||||
}
|
}
|
||||||
|
case "ddns":
|
||||||
|
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
|
||||||
|
if err == nil {
|
||||||
|
singleton.OnDDNSUpdate()
|
||||||
|
}
|
||||||
case "nat":
|
case "nat":
|
||||||
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -299,20 +307,38 @@ func (ma *memberAPI) searchTask(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ma *memberAPI) searchDDNS(c *gin.Context) {
|
||||||
|
var ddns []model.DDNSProfile
|
||||||
|
likeWord := "%" + c.Query("word") + "%"
|
||||||
|
singleton.DB.Select("id,name").Where("id = ? OR name LIKE ?",
|
||||||
|
c.Query("word"), likeWord).Find(&ddns)
|
||||||
|
|
||||||
|
var resp []searchResult
|
||||||
|
for i := 0; i < len(ddns); i++ {
|
||||||
|
resp = append(resp, searchResult{
|
||||||
|
Value: ddns[i].ID,
|
||||||
|
Name: ddns[i].Name,
|
||||||
|
Text: ddns[i].Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"results": resp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type serverForm struct {
|
type serverForm struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
Name string `binding:"required"`
|
Name string `binding:"required"`
|
||||||
DisplayIndex int
|
DisplayIndex int
|
||||||
Secret string
|
Secret string
|
||||||
Tag string
|
Tag string
|
||||||
Note string
|
Note string
|
||||||
PublicNote string
|
PublicNote string
|
||||||
HideForGuest string
|
HideForGuest string
|
||||||
EnableDDNS string
|
EnableDDNS string
|
||||||
EnableIPv4 string
|
DDNSProfilesRaw string
|
||||||
EnableIpv6 string
|
|
||||||
DDNSDomain string
|
|
||||||
DDNSProfile string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||||
@@ -330,18 +356,18 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
|||||||
s.PublicNote = sf.PublicNote
|
s.PublicNote = sf.PublicNote
|
||||||
s.HideForGuest = sf.HideForGuest == "on"
|
s.HideForGuest = sf.HideForGuest == "on"
|
||||||
s.EnableDDNS = sf.EnableDDNS == "on"
|
s.EnableDDNS = sf.EnableDDNS == "on"
|
||||||
s.EnableIPv4 = sf.EnableIPv4 == "on"
|
s.DDNSProfilesRaw = sf.DDNSProfilesRaw
|
||||||
s.EnableIpv6 = sf.EnableIpv6 == "on"
|
err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
|
||||||
s.DDNSDomain = sf.DDNSDomain
|
if err == nil {
|
||||||
s.DDNSProfile = sf.DDNSProfile
|
if s.ID == 0 {
|
||||||
if s.ID == 0 {
|
s.Secret, err = utils.GenerateRandomString(18)
|
||||||
s.Secret, err = utils.GenerateRandomString(18)
|
if err == nil {
|
||||||
if err == nil {
|
err = singleton.DB.Create(&s).Error
|
||||||
err = singleton.DB.Create(&s).Error
|
}
|
||||||
|
} else {
|
||||||
|
isEdit = true
|
||||||
|
err = singleton.DB.Save(&s).Error
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
isEdit = true
|
|
||||||
err = singleton.DB.Save(&s).Error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -743,6 +769,81 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ddnsForm struct {
|
||||||
|
ID uint64
|
||||||
|
MaxRetries uint64
|
||||||
|
EnableIPv4 string
|
||||||
|
EnableIPv6 string
|
||||||
|
Name string
|
||||||
|
Provider uint8
|
||||||
|
DomainsRaw string
|
||||||
|
AccessID string
|
||||||
|
AccessSecret string
|
||||||
|
WebhookURL string
|
||||||
|
WebhookMethod uint8
|
||||||
|
WebhookRequestType uint8
|
||||||
|
WebhookRequestBody string
|
||||||
|
WebhookHeaders string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
|
||||||
|
var df ddnsForm
|
||||||
|
var p model.DDNSProfile
|
||||||
|
err := c.ShouldBindJSON(&df)
|
||||||
|
if err == nil {
|
||||||
|
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||||
|
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
p.Name = df.Name
|
||||||
|
p.ID = df.ID
|
||||||
|
enableIPv4 := df.EnableIPv4 == "on"
|
||||||
|
enableIPv6 := df.EnableIPv6 == "on"
|
||||||
|
p.EnableIPv4 = &enableIPv4
|
||||||
|
p.EnableIPv6 = &enableIPv6
|
||||||
|
p.MaxRetries = df.MaxRetries
|
||||||
|
p.Provider = df.Provider
|
||||||
|
p.DomainsRaw = df.DomainsRaw
|
||||||
|
p.Domains = strings.Split(p.DomainsRaw, ",")
|
||||||
|
p.AccessID = df.AccessID
|
||||||
|
p.AccessSecret = df.AccessSecret
|
||||||
|
p.WebhookURL = df.WebhookURL
|
||||||
|
p.WebhookMethod = df.WebhookMethod
|
||||||
|
p.WebhookRequestType = df.WebhookRequestType
|
||||||
|
p.WebhookRequestBody = df.WebhookRequestBody
|
||||||
|
p.WebhookHeaders = df.WebhookHeaders
|
||||||
|
|
||||||
|
for n, domain := range p.Domains {
|
||||||
|
// IDN to ASCII
|
||||||
|
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||||
|
if domainErr != nil {
|
||||||
|
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.Domains[n] = domainValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if p.ID == 0 {
|
||||||
|
err = singleton.DB.Create(&p).Error
|
||||||
|
} else {
|
||||||
|
err = singleton.DB.Save(&p).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: fmt.Sprintf("请求错误:%s", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
singleton.OnDDNSUpdate()
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type natForm struct {
|
type natForm struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
Name string
|
Name string
|
||||||
@@ -940,7 +1041,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, yes := model.Themes[sf.DashboardTheme]; !yes {
|
if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
|
||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
|
|||||||
mr.GET("/monitor", mp.monitor)
|
mr.GET("/monitor", mp.monitor)
|
||||||
mr.GET("/cron", mp.cron)
|
mr.GET("/cron", mp.cron)
|
||||||
mr.GET("/notification", mp.notification)
|
mr.GET("/notification", mp.notification)
|
||||||
|
mr.GET("/ddns", mp.ddns)
|
||||||
mr.GET("/nat", mp.nat)
|
mr.GET("/nat", mp.nat)
|
||||||
mr.GET("/setting", mp.setting)
|
mr.GET("/setting", mp.setting)
|
||||||
mr.GET("/api", mp.api)
|
mr.GET("/api", mp.api)
|
||||||
@@ -78,6 +79,17 @@ func (mp *memberPage) notification(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mp *memberPage) ddns(c *gin.Context) {
|
||||||
|
var data []model.DDNSProfile
|
||||||
|
singleton.DB.Find(&data)
|
||||||
|
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
|
||||||
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
|
||||||
|
"DDNS": data,
|
||||||
|
"ProviderMap": model.ProviderMap,
|
||||||
|
"ProviderList": model.ProviderList,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (mp *memberPage) nat(c *gin.Context) {
|
func (mp *memberPage) nat(c *gin.Context) {
|
||||||
var data []model.NAT
|
var data []model.NAT
|
||||||
singleton.DB.Find(&data)
|
singleton.DB.Find(&data)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ require (
|
|||||||
github.com/hashicorp/go-uuid v1.0.3
|
github.com/hashicorp/go-uuid v1.0.3
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
|
github.com/libdns/cloudflare v0.1.1
|
||||||
|
github.com/libdns/libdns v0.2.2
|
||||||
|
github.com/libdns/tencentcloud v1.0.0
|
||||||
|
github.com/miekg/dns v1.1.62
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
github.com/ory/graceful v0.1.3
|
github.com/ory/graceful v0.1.3
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
@@ -21,6 +25,7 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/xanzy/go-gitlab v0.103.0
|
github.com/xanzy/go-gitlab v0.103.0
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
@@ -70,14 +75,19 @@ require (
|
|||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||||
|
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||||
|
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||||
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
|
||||||
|
github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -116,6 +122,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -180,6 +188,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
@@ -203,6 +219,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
|||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
@@ -232,8 +250,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
|||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ var Themes = map[string]string{
|
|||||||
"hotaru": "Hotaru",
|
"hotaru": "Hotaru",
|
||||||
"angel-kanade": "AngelKanade",
|
"angel-kanade": "AngelKanade",
|
||||||
"server-status": "ServerStatus",
|
"server-status": "ServerStatus",
|
||||||
"custom": "Custom(local)",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var DashboardThemes = map[string]string{
|
var DashboardThemes = map[string]string{
|
||||||
@@ -126,30 +125,6 @@ type Config struct {
|
|||||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||||
MaxTCPPingValue int32
|
MaxTCPPingValue int32
|
||||||
AvgPingCount int
|
AvgPingCount int
|
||||||
|
|
||||||
// 动态域名解析更新
|
|
||||||
DDNS struct {
|
|
||||||
Enable bool
|
|
||||||
Provider string
|
|
||||||
AccessID string
|
|
||||||
AccessSecret string
|
|
||||||
WebhookURL string
|
|
||||||
WebhookMethod string
|
|
||||||
WebhookRequestBody string
|
|
||||||
WebhookHeaders string
|
|
||||||
MaxRetries uint32
|
|
||||||
Profiles map[string]DDNSProfile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProfile struct {
|
|
||||||
Provider string
|
|
||||||
AccessID string
|
|
||||||
AccessSecret string
|
|
||||||
WebhookURL string
|
|
||||||
WebhookMethod string
|
|
||||||
WebhookRequestBody string
|
|
||||||
WebhookHeaders string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 读取配置文件并应用
|
// Read 读取配置文件并应用
|
||||||
@@ -190,9 +165,6 @@ func (c *Config) Read(path string) error {
|
|||||||
if c.AvgPingCount == 0 {
|
if c.AvgPingCount == 0 {
|
||||||
c.AvgPingCount = 2
|
c.AvgPingCount = 2
|
||||||
}
|
}
|
||||||
if c.DDNS.MaxRetries == 0 {
|
|
||||||
c.DDNS.MaxRetries = 3
|
|
||||||
}
|
|
||||||
if c.Oauth2.OidcScopes == "" {
|
if c.Oauth2.OidcScopes == "" {
|
||||||
c.Oauth2.OidcScopes = "openid,profile,email"
|
c.Oauth2.OidcScopes = "openid,profile,email"
|
||||||
}
|
}
|
||||||
|
|||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderDummy = iota
|
||||||
|
ProviderWebHook
|
||||||
|
ProviderCloudflare
|
||||||
|
ProviderTencentCloud
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_Dummy = "dummy"
|
||||||
|
_WebHook = "webhook"
|
||||||
|
_Cloudflare = "cloudflare"
|
||||||
|
_TencentCloud = "tencentcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ProviderMap = map[uint8]string{
|
||||||
|
ProviderDummy: _Dummy,
|
||||||
|
ProviderWebHook: _WebHook,
|
||||||
|
ProviderCloudflare: _Cloudflare,
|
||||||
|
ProviderTencentCloud: _TencentCloud,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProviderList = []DDNSProvider{
|
||||||
|
{
|
||||||
|
Name: _Dummy,
|
||||||
|
ID: ProviderDummy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _Cloudflare,
|
||||||
|
ID: ProviderCloudflare,
|
||||||
|
AccessSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: _TencentCloud,
|
||||||
|
ID: ProviderTencentCloud,
|
||||||
|
AccessID: true,
|
||||||
|
AccessSecret: true,
|
||||||
|
},
|
||||||
|
// Least frequently used, always place this at the end
|
||||||
|
{
|
||||||
|
Name: _WebHook,
|
||||||
|
ID: ProviderWebHook,
|
||||||
|
AccessID: true,
|
||||||
|
AccessSecret: true,
|
||||||
|
WebhookURL: true,
|
||||||
|
WebhookMethod: true,
|
||||||
|
WebhookRequestType: true,
|
||||||
|
WebhookRequestBody: true,
|
||||||
|
WebhookHeaders: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type DDNSProfile struct {
|
||||||
|
Common
|
||||||
|
EnableIPv4 *bool
|
||||||
|
EnableIPv6 *bool
|
||||||
|
MaxRetries uint64
|
||||||
|
Name string
|
||||||
|
Provider uint8
|
||||||
|
AccessID string
|
||||||
|
AccessSecret string
|
||||||
|
WebhookURL string
|
||||||
|
WebhookMethod uint8
|
||||||
|
WebhookRequestType uint8
|
||||||
|
WebhookRequestBody string
|
||||||
|
WebhookHeaders string
|
||||||
|
|
||||||
|
Domains []string `gorm:"-"`
|
||||||
|
DomainsRaw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DDNSProfile) TableName() string {
|
||||||
|
return "ddns"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
|
||||||
|
if d.DomainsRaw != "" {
|
||||||
|
d.Domains = strings.Split(d.DomainsRaw, ",")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DDNSProvider struct {
|
||||||
|
Name string
|
||||||
|
ID uint8
|
||||||
|
AccessID bool
|
||||||
|
AccessSecret bool
|
||||||
|
WebhookURL bool
|
||||||
|
WebhookMethod bool
|
||||||
|
WebhookRequestType bool
|
||||||
|
WebhookRequestBody bool
|
||||||
|
WebhookHeaders bool
|
||||||
|
}
|
||||||
@@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
|
|||||||
return string(msgBytes)[1 : len(msgBytes)-1]
|
return string(msgBytes)[1 : len(msgBytes)-1]
|
||||||
}), nil
|
}), nil
|
||||||
case NotificationRequestTypeForm:
|
case NotificationRequestTypeForm:
|
||||||
var data map[string]string
|
data, err := utils.GjsonParseStringMap(n.RequestBody)
|
||||||
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
|||||||
if n.RequestHeader == "" {
|
if n.RequestHeader == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var m map[string]string
|
m, err := utils.GjsonParseStringMap(n.RequestHeader)
|
||||||
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
|
|||||||
+24
-14
@@ -3,27 +3,28 @@ package model
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
pb "github.com/naiba/nezha/proto"
|
pb "github.com/naiba/nezha/proto"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Common
|
Common
|
||||||
Name string
|
Name string
|
||||||
Tag string // 分组名
|
Tag string // 分组名
|
||||||
Secret string `gorm:"uniqueIndex" json:"-"`
|
Secret string `gorm:"uniqueIndex" json:"-"`
|
||||||
Note string `json:"-"` // 管理员可见备注
|
Note string `json:"-"` // 管理员可见备注
|
||||||
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
|
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
|
||||||
DisplayIndex int // 展示排序,越大越靠前
|
DisplayIndex int // 展示排序,越大越靠前
|
||||||
HideForGuest bool // 对游客隐藏
|
HideForGuest bool // 对游客隐藏
|
||||||
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
|
EnableDDNS bool // 启用DDNS
|
||||||
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4
|
DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
|
||||||
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
|
|
||||||
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
|
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
||||||
DDNSProfile string `json:"-"` // DDNS配置
|
|
||||||
|
|
||||||
Host *Host `gorm:"-"`
|
Host *Host `gorm:"-"`
|
||||||
State *HostState `gorm:"-"`
|
State *HostState `gorm:"-"`
|
||||||
@@ -48,6 +49,16 @@ func (s *Server) CopyFromRunningServer(old *Server) {
|
|||||||
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) AfterFind(tx *gorm.DB) error {
|
||||||
|
if s.DDNSProfilesRaw != "" {
|
||||||
|
if err := utils.Json.Unmarshal([]byte(s.DDNSProfilesRaw), &s.DDNSProfiles); err != nil {
|
||||||
|
log.Println("NEZHA>> Server.AfterFind:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func boolToString(b bool) string {
|
func boolToString(b bool) string {
|
||||||
if b {
|
if b {
|
||||||
return "true"
|
return "true"
|
||||||
@@ -60,8 +71,7 @@ func (s Server) MarshalForDashboard() template.JS {
|
|||||||
tag, _ := utils.Json.Marshal(s.Tag)
|
tag, _ := utils.Json.Marshal(s.Tag)
|
||||||
note, _ := utils.Json.Marshal(s.Note)
|
note, _ := utils.Json.Marshal(s.Note)
|
||||||
secret, _ := utils.Json.Marshal(s.Secret)
|
secret, _ := utils.Json.Marshal(s.Secret)
|
||||||
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
|
ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
|
||||||
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
|
|
||||||
publicNote, _ := utils.Json.Marshal(s.PublicNote)
|
publicNote, _ := utils.Json.Marshal(s.PublicNote)
|
||||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile, publicNote))
|
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
|
|
||||||
|
|
||||||
type ProviderCloudflare struct {
|
|
||||||
secret string
|
|
||||||
zoneId string
|
|
||||||
recordId string
|
|
||||||
domainConfig *DomainConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type cfReq struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
TTL uint32 `json:"ttl"`
|
|
||||||
Proxied bool `json:"proxied"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cfResp struct {
|
|
||||||
Result []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProviderCloudflare(s string) *ProviderCloudflare {
|
|
||||||
return &ProviderCloudflare{
|
|
||||||
secret: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return fmt.Errorf("获取 DDNS 配置失败")
|
|
||||||
}
|
|
||||||
provider.domainConfig = domainConfig
|
|
||||||
|
|
||||||
err := provider.getZoneID()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("无法获取 zone ID: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当IPv4和IPv6同时成功才算作成功
|
|
||||||
if provider.domainConfig.EnableIPv4 {
|
|
||||||
if err = provider.addDomainRecord(true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.domainConfig.EnableIpv6 {
|
|
||||||
if err = provider.addDomainRecord(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
|
|
||||||
err := provider.findDNSRecord(isIpv4)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.recordId == "" {
|
|
||||||
// 添加 DNS 记录
|
|
||||||
return provider.createDNSRecord(isIpv4)
|
|
||||||
} else {
|
|
||||||
// 更新 DNS 记录
|
|
||||||
return provider.updateDNSRecord(isIpv4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) getZoneID() error {
|
|
||||||
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
||||||
zu, _ := url.Parse(baseEndpoint)
|
|
||||||
|
|
||||||
q := zu.Query()
|
|
||||||
q.Set("name", realDomain)
|
|
||||||
zu.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
body, err := provider.sendRequest("GET", zu.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &cfResp{}
|
|
||||||
err = utils.Json.Unmarshal(body, res)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res.Result
|
|
||||||
if len(result) > 0 {
|
|
||||||
provider.zoneId = result[0].ID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("找不到 Zone ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
}
|
|
||||||
|
|
||||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
|
|
||||||
du, _ := url.Parse(de)
|
|
||||||
|
|
||||||
q := du.Query()
|
|
||||||
q.Set("name", provider.domainConfig.FullDomain)
|
|
||||||
q.Set("type", ipType)
|
|
||||||
du.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
body, err := provider.sendRequest("GET", du.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &cfResp{}
|
|
||||||
err = utils.Json.Unmarshal(body, res)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res.Result
|
|
||||||
if len(result) > 0 {
|
|
||||||
provider.recordId = result[0].ID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType, ipAddr string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
ipAddr = provider.domainConfig.Ipv4Addr
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = provider.domainConfig.Ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
|
|
||||||
data := &cfReq{
|
|
||||||
Name: provider.domainConfig.FullDomain,
|
|
||||||
Type: ipType,
|
|
||||||
Content: ipAddr,
|
|
||||||
TTL: 60,
|
|
||||||
Proxied: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, _ := utils.Json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("POST", de, jsonData)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType, ipAddr string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
ipAddr = provider.domainConfig.Ipv4Addr
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = provider.domainConfig.Ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
|
|
||||||
data := &cfReq{
|
|
||||||
Name: provider.domainConfig.FullDomain,
|
|
||||||
Type: ipType,
|
|
||||||
Content: ipAddr,
|
|
||||||
TTL: 60,
|
|
||||||
Proxied: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, _ := utils.Json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("PATCH", de, jsonData)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为辅助方法,如发送 HTTP 请求等
|
|
||||||
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
|
||||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.secret))
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := utils.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
+118
-14
@@ -1,22 +1,126 @@
|
|||||||
package ddns
|
package ddns
|
||||||
|
|
||||||
import "golang.org/x/net/publicsuffix"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
type DomainConfig struct {
|
"github.com/libdns/libdns"
|
||||||
EnableIPv4 bool
|
"github.com/miekg/dns"
|
||||||
EnableIpv6 bool
|
|
||||||
FullDomain string
|
"github.com/naiba/nezha/model"
|
||||||
Ipv4Addr string
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
Ipv6Addr string
|
)
|
||||||
|
|
||||||
|
var dnsTimeOut = 10 * time.Second
|
||||||
|
|
||||||
|
type IP struct {
|
||||||
|
Ipv4Addr string
|
||||||
|
Ipv6Addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider struct {
|
||||||
// UpdateDomain Return is updated
|
ctx context.Context
|
||||||
UpdateDomain(*DomainConfig) error
|
ipAddr string
|
||||||
|
recordType string
|
||||||
|
domain string
|
||||||
|
prefix string
|
||||||
|
zone string
|
||||||
|
|
||||||
|
DDNSProfile *model.DDNSProfile
|
||||||
|
IPAddrs *IP
|
||||||
|
Setter libdns.RecordSetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitDomain(domain string) (prefix string, realDomain string) {
|
func (provider *Provider) UpdateDomain(ctx context.Context) {
|
||||||
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
|
provider.ctx = ctx
|
||||||
prefix = domain[:len(domain)-len(realDomain)-1]
|
for _, domain := range provider.DDNSProfile.Domains {
|
||||||
return prefix, realDomain
|
for retries := 0; retries < int(provider.DDNSProfile.MaxRetries); retries++ {
|
||||||
|
provider.domain = domain
|
||||||
|
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", provider.domain, retries+1, provider.DDNSProfile.MaxRetries)
|
||||||
|
if err := provider.updateDomain(); err != nil {
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", provider.domain, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", provider.domain)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) updateDomain() error {
|
||||||
|
var err error
|
||||||
|
provider.prefix, provider.zone, err = splitDomainSOA(provider.domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当IPv4和IPv6同时成功才算作成功
|
||||||
|
if *provider.DDNSProfile.EnableIPv4 {
|
||||||
|
provider.recordType = getRecordString(true)
|
||||||
|
provider.ipAddr = provider.IPAddrs.Ipv4Addr
|
||||||
|
if err = provider.addDomainRecord(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *provider.DDNSProfile.EnableIPv6 {
|
||||||
|
provider.recordType = getRecordString(false)
|
||||||
|
provider.ipAddr = provider.IPAddrs.Ipv6Addr
|
||||||
|
if err = provider.addDomainRecord(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) addDomainRecord() error {
|
||||||
|
_, err := provider.Setter.SetRecords(provider.ctx, provider.zone,
|
||||||
|
[]libdns.Record{
|
||||||
|
{
|
||||||
|
Type: provider.recordType,
|
||||||
|
Name: provider.prefix,
|
||||||
|
Value: provider.ipAddr,
|
||||||
|
TTL: time.Minute,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitDomainSOA(domain string) (prefix string, zone string, err error) {
|
||||||
|
c := &dns.Client{Timeout: dnsTimeOut}
|
||||||
|
|
||||||
|
domain += "."
|
||||||
|
indexes := dns.Split(domain)
|
||||||
|
|
||||||
|
var r *dns.Msg
|
||||||
|
for _, idx := range indexes {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(domain[idx:], dns.TypeSOA)
|
||||||
|
|
||||||
|
for _, server := range utils.DNSServers {
|
||||||
|
r, _, err = c.Exchange(m, server)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(r.Answer) > 0 {
|
||||||
|
if soa, ok := r.Answer[0].(*dns.SOA); ok {
|
||||||
|
zone = soa.Hdr.Name
|
||||||
|
prefix = libdns.RelativeName(domain, zone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecordString(isIpv4 bool) string {
|
||||||
|
if isIpv4 {
|
||||||
|
return "A"
|
||||||
|
}
|
||||||
|
return "AAAA"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSt struct {
|
||||||
|
domain string
|
||||||
|
zone string
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitDomainSOA(t *testing.T) {
|
||||||
|
if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testSt{
|
||||||
|
{
|
||||||
|
domain: "www.example.co.uk",
|
||||||
|
zone: "example.co.uk.",
|
||||||
|
prefix: "www",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "abc.example.com",
|
||||||
|
zone: "example.com.",
|
||||||
|
prefix: "abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
zone: "example.com.",
|
||||||
|
prefix: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
prefix, zone, err := splitDomainSOA(c.domain)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if prefix != c.prefix {
|
||||||
|
t.Fatalf("Expected prefix %s, but got %s", c.prefix, prefix)
|
||||||
|
}
|
||||||
|
if zone != c.zone {
|
||||||
|
t.Fatalf("Expected zone %s, but got %s", c.zone, zone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
type ProviderDummy struct{}
|
|
||||||
|
|
||||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dummy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/libdns/libdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Internal use
|
||||||
|
type Provider struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||||
|
recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
|
return recs, nil
|
||||||
|
}
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const te = "https://dnspod.tencentcloudapi.com"
|
|
||||||
|
|
||||||
type ProviderTencentCloud struct {
|
|
||||||
secretID string
|
|
||||||
secretKey string
|
|
||||||
domainConfig *DomainConfig
|
|
||||||
resp *tcResp
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcReq struct {
|
|
||||||
RecordType string `json:"RecordType"`
|
|
||||||
Domain string `json:"Domain"`
|
|
||||||
RecordLine string `json:"RecordLine"`
|
|
||||||
Subdomain string `json:"Subdomain,omitempty"`
|
|
||||||
SubDomain string `json:"SubDomain,omitempty"` // As is
|
|
||||||
Value string `json:"Value,omitempty"`
|
|
||||||
TTL uint32 `json:"TTL,omitempty"`
|
|
||||||
RecordId uint64 `json:"RecordId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcResp struct {
|
|
||||||
Response struct {
|
|
||||||
RecordList []struct {
|
|
||||||
RecordId uint64
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
Error struct {
|
|
||||||
Code string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
|
|
||||||
return &ProviderTencentCloud{
|
|
||||||
secretID: id,
|
|
||||||
secretKey: key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return fmt.Errorf("获取 DDNS 配置失败")
|
|
||||||
}
|
|
||||||
provider.domainConfig = domainConfig
|
|
||||||
|
|
||||||
// 当IPv4和IPv6同时成功才算作成功
|
|
||||||
var err error
|
|
||||||
if provider.domainConfig.EnableIPv4 {
|
|
||||||
if err = provider.addDomainRecord(true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.domainConfig.EnableIpv6 {
|
|
||||||
if err = provider.addDomainRecord(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
|
|
||||||
err := provider.findDNSRecord(isIpv4)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
|
|
||||||
return provider.createDNSRecord(isIpv4)
|
|
||||||
} else if provider.resp.Response.Error.Code != "" {
|
|
||||||
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认情况下更新 DNS 记录
|
|
||||||
return provider.updateDNSRecord(isIpv4)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
||||||
data := &tcReq{
|
|
||||||
RecordType: ipType,
|
|
||||||
Domain: realDomain,
|
|
||||||
RecordLine: "默认",
|
|
||||||
Subdomain: prefix,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, _ := utils.Json.Marshal(data)
|
|
||||||
body, err := provider.sendRequest("DescribeRecordList", jsonData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.resp = &tcResp{}
|
|
||||||
err = utils.Json.Unmarshal(body, provider.resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType, ipAddr string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
ipAddr = provider.domainConfig.Ipv4Addr
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = provider.domainConfig.Ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
||||||
data := &tcReq{
|
|
||||||
RecordType: ipType,
|
|
||||||
RecordLine: "默认",
|
|
||||||
Domain: realDomain,
|
|
||||||
SubDomain: prefix,
|
|
||||||
Value: ipAddr,
|
|
||||||
TTL: 600,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, _ := utils.Json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("CreateRecord", jsonData)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
|
|
||||||
var ipType, ipAddr string
|
|
||||||
if isIPv4 {
|
|
||||||
ipType = "A"
|
|
||||||
ipAddr = provider.domainConfig.Ipv4Addr
|
|
||||||
} else {
|
|
||||||
ipType = "AAAA"
|
|
||||||
ipAddr = provider.domainConfig.Ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
|
||||||
data := &tcReq{
|
|
||||||
RecordType: ipType,
|
|
||||||
RecordLine: "默认",
|
|
||||||
Domain: realDomain,
|
|
||||||
SubDomain: prefix,
|
|
||||||
Value: ipAddr,
|
|
||||||
TTL: 600,
|
|
||||||
RecordId: provider.resp.Response.RecordList[0].RecordId,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, _ := utils.Json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("ModifyRecord", jsonData)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为辅助方法,如发送 HTTP 请求等
|
|
||||||
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
|
||||||
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-TC-Version", "2021-03-23")
|
|
||||||
|
|
||||||
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
|
|
||||||
resp, err := utils.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) sha256hex(s string) string {
|
|
||||||
b := sha256.Sum256([]byte(s))
|
|
||||||
return hex.EncodeToString(b[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
|
|
||||||
hashed := hmac.New(sha256.New, []byte(key))
|
|
||||||
hashed.Write([]byte(s))
|
|
||||||
return string(hashed.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, str := range strs {
|
|
||||||
b.WriteString(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
|
||||||
algorithm := "TC3-HMAC-SHA256"
|
|
||||||
service := "dnspod"
|
|
||||||
host := provider.WriteString(service, ".tencentcloudapi.com")
|
|
||||||
timestamp := time.Now().Unix()
|
|
||||||
timestampStr := strconv.FormatInt(timestamp, 10)
|
|
||||||
|
|
||||||
// 步骤 1:拼接规范请求串
|
|
||||||
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
|
|
||||||
signedHeaders := "content-type;host;x-tc-action"
|
|
||||||
hashedRequestPayload := provider.sha256hex(payload)
|
|
||||||
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
|
|
||||||
|
|
||||||
// 步骤 2:拼接待签名字符串
|
|
||||||
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
|
|
||||||
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
|
|
||||||
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
|
|
||||||
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
|
|
||||||
|
|
||||||
// 步骤 3:计算签名
|
|
||||||
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
|
|
||||||
secretService := provider.hmacsha256(service, secretDate)
|
|
||||||
secretSigning := provider.hmacsha256("tc3_request", secretService)
|
|
||||||
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
|
|
||||||
|
|
||||||
// 步骤 4:拼接 Authorization
|
|
||||||
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
|
|
||||||
|
|
||||||
r.Header.Add("Authorization", authorization)
|
|
||||||
r.Header.Set("Host", host)
|
|
||||||
r.Header.Set("X-TC-Action", action)
|
|
||||||
r.Header.Add("X-TC-Timestamp", timestampStr)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package ddns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProviderWebHook struct {
|
|
||||||
url string
|
|
||||||
requestMethod string
|
|
||||||
requestBody string
|
|
||||||
requestHeader string
|
|
||||||
domainConfig *DomainConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
|
|
||||||
return &ProviderWebHook{
|
|
||||||
url: s,
|
|
||||||
requestMethod: rm,
|
|
||||||
requestBody: rb,
|
|
||||||
requestHeader: rh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return fmt.Errorf("获取 DDNS 配置失败")
|
|
||||||
}
|
|
||||||
provider.domainConfig = domainConfig
|
|
||||||
|
|
||||||
if provider.domainConfig.FullDomain == "" {
|
|
||||||
return fmt.Errorf("failed to update an empty domain")
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
|
|
||||||
req, err := provider.prepareRequest(true)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
|
||||||
}
|
|
||||||
if _, err := utils.HttpClient.Do(req); err != nil {
|
|
||||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
|
|
||||||
req, err := provider.prepareRequest(false)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
|
||||||
}
|
|
||||||
if _, err := utils.HttpClient.Do(req); err != nil {
|
|
||||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
|
|
||||||
u, err := url.Parse(provider.url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed parsing url: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only handle queries here
|
|
||||||
q := u.Query()
|
|
||||||
for p, vals := range q {
|
|
||||||
for n, v := range vals {
|
|
||||||
vals[n] = provider.formatWebhookString(v, isIPv4)
|
|
||||||
}
|
|
||||||
q[p] = vals
|
|
||||||
}
|
|
||||||
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
body := provider.formatWebhookString(provider.requestBody, isIPv4)
|
|
||||||
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
|
|
||||||
headers := strings.Split(header, "\n")
|
|
||||||
|
|
||||||
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed creating new request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SetStringHeadersToRequest(req, headers)
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
|
|
||||||
var ipAddr, ipType string
|
|
||||||
if isIPv4 {
|
|
||||||
ipAddr = provider.domainConfig.Ipv4Addr
|
|
||||||
ipType = "ipv4"
|
|
||||||
} else {
|
|
||||||
ipAddr = provider.domainConfig.Ipv6Addr
|
|
||||||
ipType = "ipv6"
|
|
||||||
}
|
|
||||||
|
|
||||||
r := strings.NewReplacer(
|
|
||||||
"{ip}", ipAddr,
|
|
||||||
"{domain}", provider.domainConfig.FullDomain,
|
|
||||||
"{type}", ipType,
|
|
||||||
"\r", "",
|
|
||||||
)
|
|
||||||
|
|
||||||
result := r.Replace(strings.TrimSpace(s))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/libdns/libdns"
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
methodGET
|
||||||
|
methodPOST
|
||||||
|
methodPATCH
|
||||||
|
methodDELETE
|
||||||
|
methodPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
requestTypeJSON
|
||||||
|
requestTypeForm
|
||||||
|
)
|
||||||
|
|
||||||
|
var requestTypes = map[uint8]string{
|
||||||
|
methodGET: "GET",
|
||||||
|
methodPOST: "POST",
|
||||||
|
methodPATCH: "PATCH",
|
||||||
|
methodDELETE: "DELETE",
|
||||||
|
methodPUT: "PUT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal use
|
||||||
|
type Provider struct {
|
||||||
|
ipAddr string
|
||||||
|
ipType string
|
||||||
|
recordType string
|
||||||
|
domain string
|
||||||
|
|
||||||
|
DDNSProfile *model.DDNSProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||||
|
recs []libdns.Record) ([]libdns.Record, error) {
|
||||||
|
for _, rec := range recs {
|
||||||
|
provider.recordType = rec.Type
|
||||||
|
provider.ipType = recordToIPType(provider.recordType)
|
||||||
|
provider.ipAddr = rec.Value
|
||||||
|
provider.domain = fmt.Sprintf("%s.%s", rec.Name, strings.TrimSuffix(zone, "."))
|
||||||
|
|
||||||
|
req, err := provider.prepareRequest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||||
|
}
|
||||||
|
if _, err := utils.HttpClient.Do(req); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) prepareRequest(ctx context.Context) (*http.Request, error) {
|
||||||
|
u, err := provider.reqUrl()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := provider.reqBody()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := utils.GjsonParseStringMap(
|
||||||
|
provider.formatWebhookString(provider.DDNSProfile.WebhookHeaders))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, requestTypes[provider.DDNSProfile.WebhookMethod], u.String(), strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.setContentType(req)
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) setContentType(req *http.Request) {
|
||||||
|
if provider.DDNSProfile.WebhookMethod == methodGET {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if provider.DDNSProfile.WebhookRequestType == requestTypeForm {
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) reqUrl() (*url.URL, error) {
|
||||||
|
formattedUrl := strings.ReplaceAll(provider.DDNSProfile.WebhookURL, "#", "%23")
|
||||||
|
|
||||||
|
u, err := url.Parse(formattedUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle queries here
|
||||||
|
q := u.Query()
|
||||||
|
for p, vals := range q {
|
||||||
|
for n, v := range vals {
|
||||||
|
vals[n] = provider.formatWebhookString(v)
|
||||||
|
}
|
||||||
|
q[p] = vals
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) reqBody() (string, error) {
|
||||||
|
if provider.DDNSProfile.WebhookMethod == methodGET ||
|
||||||
|
provider.DDNSProfile.WebhookMethod == methodDELETE {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch provider.DDNSProfile.WebhookRequestType {
|
||||||
|
case requestTypeJSON:
|
||||||
|
return provider.formatWebhookString(provider.DDNSProfile.WebhookRequestBody), nil
|
||||||
|
case requestTypeForm:
|
||||||
|
data, err := utils.GjsonParseStringMap(provider.DDNSProfile.WebhookRequestBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
for k, v := range data {
|
||||||
|
params.Add(k, provider.formatWebhookString(v))
|
||||||
|
}
|
||||||
|
return params.Encode(), nil
|
||||||
|
default:
|
||||||
|
return "", errors.New("request type not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) formatWebhookString(s string) string {
|
||||||
|
r := strings.NewReplacer(
|
||||||
|
"#ip#", provider.ipAddr,
|
||||||
|
"#domain#", provider.domain,
|
||||||
|
"#type#", provider.ipType,
|
||||||
|
"#record#", provider.recordType,
|
||||||
|
"#access_id#", provider.DDNSProfile.AccessID,
|
||||||
|
"#access_secret#", provider.DDNSProfile.AccessSecret,
|
||||||
|
"\r", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
result := r.Replace(strings.TrimSpace(s))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordToIPType(record string) string {
|
||||||
|
switch record {
|
||||||
|
case "A":
|
||||||
|
return "ipv4"
|
||||||
|
case "AAAA":
|
||||||
|
return "ipv6"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reqTypeForm = "application/x-www-form-urlencoded"
|
||||||
|
reqTypeJSON = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSt struct {
|
||||||
|
profile model.DDNSProfile
|
||||||
|
expectURL string
|
||||||
|
expectBody string
|
||||||
|
expectContentType string
|
||||||
|
expectHeader map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCase(t *testing.T, item testSt) {
|
||||||
|
pw := Provider{DDNSProfile: &item.profile}
|
||||||
|
pw.ipAddr = "1.1.1.1"
|
||||||
|
pw.domain = item.profile.Domains[0]
|
||||||
|
pw.ipType = "ipv4"
|
||||||
|
pw.recordType = "A"
|
||||||
|
pw.DDNSProfile = &item.profile
|
||||||
|
|
||||||
|
reqUrl, err := pw.reqUrl()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if item.expectURL != reqUrl.String() {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectURL, reqUrl.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := pw.reqBody()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
if item.expectBody != reqBody {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := pw.prepareRequest(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.expectContentType != req.Header.Get("Content-Type") {
|
||||||
|
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range item.expectHeader {
|
||||||
|
if v != req.Header.Get(k) {
|
||||||
|
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookRequest(t *testing.T) {
|
||||||
|
ipv4 := true
|
||||||
|
|
||||||
|
cases := []testSt{
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/?ip=#ip#",
|
||||||
|
WebhookMethod: methodGET,
|
||||||
|
WebhookHeaders: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/?ip=1.1.1.1",
|
||||||
|
expectContentType: "",
|
||||||
|
expectHeader: map[string]string{
|
||||||
|
"ip": "1.1.1.1",
|
||||||
|
"record": "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/api",
|
||||||
|
WebhookMethod: methodPOST,
|
||||||
|
WebhookRequestType: requestTypeJSON,
|
||||||
|
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/api",
|
||||||
|
expectContentType: reqTypeJSON,
|
||||||
|
expectBody: `{"ip":"1.1.1.1","record":"A"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: model.DDNSProfile{
|
||||||
|
Domains: []string{"www.example.com"},
|
||||||
|
MaxRetries: 1,
|
||||||
|
EnableIPv4: &ipv4,
|
||||||
|
WebhookURL: "http://ddns.example.com/api",
|
||||||
|
WebhookMethod: methodPOST,
|
||||||
|
WebhookRequestType: requestTypeForm,
|
||||||
|
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||||
|
},
|
||||||
|
expectURL: "http://ddns.example.com/api",
|
||||||
|
expectContentType: reqTypeForm,
|
||||||
|
expectBody: "ip=1.1.1.1&record=A",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
execCase(t, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
|
|||||||
"/monitor": true,
|
"/monitor": true,
|
||||||
"/setting": true,
|
"/setting": true,
|
||||||
"/notification": true,
|
"/notification": true,
|
||||||
|
"/ddns": true,
|
||||||
"/nat": true,
|
"/nat": true,
|
||||||
"/cron": true,
|
"/cron": true,
|
||||||
"/api": true,
|
"/api": true,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrGjsonNotFound = errors.New("specified path does not exist")
|
||||||
|
ErrGjsonWrongType = errors.New("wrong type")
|
||||||
|
)
|
||||||
|
|
||||||
|
func GjsonGet(json []byte, path string) (gjson.Result, error) {
|
||||||
|
result := gjson.GetBytes(json, path)
|
||||||
|
if !result.Exists() {
|
||||||
|
return result, ErrGjsonNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
|
||||||
|
if jsonObject == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := gjson.Parse(jsonObject)
|
||||||
|
if !result.IsObject() {
|
||||||
|
return nil, ErrGjsonWrongType
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]string)
|
||||||
|
result.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
ret[key.String()] = value.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HybridFS combines embed.FS and os.DirFS.
|
||||||
|
type HybridFS struct {
|
||||||
|
embedFS, dir fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
|
||||||
|
subFS, err := fs.Sub(embed, subDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HybridFS{
|
||||||
|
embedFS: subFS,
|
||||||
|
dir: os.DirFS(localDir),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hfs *HybridFS) Open(name string) (fs.File, error) {
|
||||||
|
// Ensure embed files are not replaced
|
||||||
|
if file, err := hfs.embedFS.Open(name); err == nil {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return hfs.dir.Open(name)
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ func init() {
|
|||||||
SkipVerifySSL: false,
|
SkipVerifySSL: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
http.DefaultClient.Timeout = time.Minute * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
type _httpTransport struct {
|
type _httpTransport struct {
|
||||||
|
|||||||
+5
-14
@@ -3,7 +3,6 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -11,7 +10,11 @@ import (
|
|||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
var (
|
||||||
|
Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
|
||||||
|
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53", "[2606:4700:4700::1111]:53", "[2400:3200::1]:53"}
|
||||||
|
)
|
||||||
|
|
||||||
func IsWindows() bool {
|
func IsWindows() bool {
|
||||||
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
||||||
@@ -87,15 +90,3 @@ func Uint64SubInt64(a uint64, b int64) uint64 {
|
|||||||
}
|
}
|
||||||
return a - uint64(b)
|
return a - uint64(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
|
||||||
if req == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, element := range headers {
|
|
||||||
kv := strings.SplitN(element, ":", 2)
|
|
||||||
if len(kv) == 2 {
|
|
||||||
req.Header.Add(kv[0], kv[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+95
-38
@@ -23,19 +23,19 @@ other = "Home"
|
|||||||
other = "Back to Homepage"
|
other = "Back to Homepage"
|
||||||
|
|
||||||
[AdminPanel]
|
[AdminPanel]
|
||||||
other = "Admin Panel"
|
other = "Dashboard"
|
||||||
|
|
||||||
[Logout]
|
[Logout]
|
||||||
other = "Logout"
|
other = "Log out"
|
||||||
|
|
||||||
[Login]
|
[Login]
|
||||||
other = "Login"
|
other = "Log in"
|
||||||
|
|
||||||
[ConfirmLogout]
|
[ConfirmLogout]
|
||||||
other = "Do you want to Logout?"
|
other = "Confirm Logging Out?"
|
||||||
|
|
||||||
[AfterLoggingOutYouHaveToLoginAgain]
|
[AfterLoggingOutYouHaveToLoginAgain]
|
||||||
other = "You need to Login again to continue"
|
other = "You will need to log in again to continue"
|
||||||
|
|
||||||
[Cancel]
|
[Cancel]
|
||||||
other = "Cancel"
|
other = "Cancel"
|
||||||
@@ -56,7 +56,7 @@ other = "Cron Expression"
|
|||||||
other = "Backup"
|
other = "Backup"
|
||||||
|
|
||||||
[3amDaily]
|
[3amDaily]
|
||||||
other = "(Daily at 3 AM)"
|
other = "(At 3 AM)"
|
||||||
|
|
||||||
[Command]
|
[Command]
|
||||||
other = "Command"
|
other = "Command"
|
||||||
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
|
|||||||
other = "Cover All, Except Specific Servers"
|
other = "Cover All, Except Specific Servers"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "Execute on Trigger Server"
|
other = "Execute on Alarmed Servers"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "Specific Servers"
|
other = "Specific Servers"
|
||||||
@@ -95,7 +95,7 @@ other = "Cron Task"
|
|||||||
other = "Trigger Task"
|
other = "Trigger Task"
|
||||||
|
|
||||||
[TheFormaOfTheScheduleIs]
|
[TheFormaOfTheScheduleIs]
|
||||||
other = "The Cron Expression is like:"
|
other = "The Cron Expression format is:"
|
||||||
|
|
||||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||||
other = "Sec Min Hour Day Month Week, see details in "
|
other = "Sec Min Hour Day Month Week, see details in "
|
||||||
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
|
|||||||
other = "CRON Expression Format"
|
other = "CRON Expression Format"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "Introduction of Commands: Writing commands is similar to shell/bat scripts. It's advised not to break lines, and multiple commands can be connected with <code>&&</code> or <code>&</code>. If a command cannot be found, it might be due to <code>PATH</code> environment variable configuration issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or use the command's absolute path."
|
other = "Note on writing commands: It is similar to shell/bat scripts. but it's advised not to start a newline, and connect multiple commands with <code>&&</code> or <code>;</code>. If a command cannot be found, it is possibly due to <code>PATH</code> environment variable issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or just use the absolute path."
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "Add Service Monitor"
|
other = "Add Service Monitor"
|
||||||
@@ -119,7 +119,7 @@ other = "Target"
|
|||||||
other = "Type"
|
other = "Type"
|
||||||
|
|
||||||
[SslExpirationOrChange]
|
[SslExpirationOrChange]
|
||||||
other = "(SSL Expiration or Change)"
|
other = "(Certificate Expiration and Changes)"
|
||||||
|
|
||||||
[Duration]
|
[Duration]
|
||||||
other = "Interval"
|
other = "Interval"
|
||||||
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
|
|||||||
other = "Failure Notification"
|
other = "Failure Notification"
|
||||||
|
|
||||||
[MaxLatency]
|
[MaxLatency]
|
||||||
other = "Max Latency (ms)"
|
other = "Maximum Latency (ms)"
|
||||||
|
|
||||||
[MinLatency]
|
[MinLatency]
|
||||||
other = "Min Latency (ms)"
|
other = "Minimum Latency (ms)"
|
||||||
|
|
||||||
[EnableLatencyNotification]
|
[EnableLatencyNotification]
|
||||||
other = "Enable Latency Notification"
|
other = "Enable Latency Notification"
|
||||||
@@ -147,7 +147,7 @@ other = "Latency Notification"
|
|||||||
|
|
||||||
[IntroductionOfMonitor]
|
[IntroductionOfMonitor]
|
||||||
other = """
|
other = """
|
||||||
For type <b>HTTP-GET</b>, enter URL (with http/https, HTTPS protocol will also monitor SSL certificate);<br>
|
For type <b>HTTP-GET</b>, enter URL (with http/https scheme, HTTPS protocol will also monitor SSL certificate);<br>
|
||||||
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
|
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
|
||||||
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
|
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
|
||||||
|
|
||||||
@@ -182,16 +182,16 @@ other = "Notification Trigger Mode"
|
|||||||
other = "Always Trigger"
|
other = "Always Trigger"
|
||||||
|
|
||||||
[ModeOnetimeTrigger]
|
[ModeOnetimeTrigger]
|
||||||
other = "Always Trigger"
|
other = "Trigger Once"
|
||||||
|
|
||||||
[EnableTriggerTask]
|
[EnableTriggerTask]
|
||||||
other = "Enable Trigger Task"
|
other = "Enable Trigger Task"
|
||||||
|
|
||||||
[FailTriggerTasks]
|
[FailTriggerTasks]
|
||||||
other = "Tasks triggered during an alarm"
|
other = "Tasks to trigger on an alarm"
|
||||||
|
|
||||||
[RecoverTriggerTasks]
|
[RecoverTriggerTasks]
|
||||||
other = "Tasks triggered during recovery"
|
other = "Tasks to trigger after recovery"
|
||||||
|
|
||||||
[Enable]
|
[Enable]
|
||||||
other = "Enable"
|
other = "Enable"
|
||||||
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
|
|||||||
other = "Display Index"
|
other = "Display Index"
|
||||||
|
|
||||||
[TheLargerTheNumberTheHigherThePriority]
|
[TheLargerTheNumberTheHigherThePriority]
|
||||||
other = "Higher Number, Higher Priority"
|
other = "Greater Number, Higher Priority"
|
||||||
|
|
||||||
[Secret]
|
[Secret]
|
||||||
other = "Secret"
|
other = "Secret"
|
||||||
@@ -230,10 +230,10 @@ other = "Note"
|
|||||||
other = "Public Note"
|
other = "Public Note"
|
||||||
|
|
||||||
[LinuxOneKeyInstall]
|
[LinuxOneKeyInstall]
|
||||||
other = "Linux One-Command Install"
|
other = "Linux Installation Command"
|
||||||
|
|
||||||
[NoDomainAlert]
|
[NoDomainAlert]
|
||||||
other = "Set CDN bypassed Domain/IP in setting before using this function"
|
other = "Please set the IP (or a domain pointed to it) of the original server in settings"
|
||||||
|
|
||||||
[PushSuccessfully]
|
[PushSuccessfully]
|
||||||
other = "Send Success Notification"
|
other = "Send Success Notification"
|
||||||
@@ -254,13 +254,13 @@ other = "Cover All"
|
|||||||
other = "Ignore All"
|
other = "Ignore All"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "Execute by Trigger"
|
other = "Alarmed Servers"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "Delete Scheduled Task"
|
other = "Delete Scheduled Task"
|
||||||
|
|
||||||
[ConfirmToDeleteThisScheduledTask]
|
[ConfirmToDeleteThisScheduledTask]
|
||||||
other = "Are you sure you want to Delete?"
|
other = "Confirm deletion?"
|
||||||
|
|
||||||
[AccessDenied]
|
[AccessDenied]
|
||||||
other = "Access Denied"
|
other = "Access Denied"
|
||||||
@@ -281,22 +281,22 @@ other = "Don't Have an Account?"
|
|||||||
other = "HTTP(S)/SSL Certificate"
|
other = "HTTP(S)/SSL Certificate"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP Port"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "Remove Service Monitor"
|
other = "Remove Service Monitor"
|
||||||
|
|
||||||
[ConfirmToDeleteThisService]
|
[ConfirmToDeleteThisService]
|
||||||
other = "Confirm to Delete This Service?"
|
other = "Confirm Deleting This Service?"
|
||||||
|
|
||||||
[DeleteNotificationMethod]
|
[DeleteNotificationMethod]
|
||||||
other = "Delete Notification Method"
|
other = "Delete Notification Method"
|
||||||
|
|
||||||
[ConfirmToDeleteThisNotificationMethod]
|
[ConfirmToDeleteThisNotificationMethod]
|
||||||
other = "Confirm to Delete This Notification Method?"
|
other = "Confirm Deleting This Notification Method?"
|
||||||
|
|
||||||
[ForceUpdate]
|
[ForceUpdate]
|
||||||
other = "Force to Update"
|
other = "Trigger Agent Update"
|
||||||
|
|
||||||
[SelectAll]
|
[SelectAll]
|
||||||
other = "Select All"
|
other = "Select All"
|
||||||
@@ -305,7 +305,7 @@ other = "Select All"
|
|||||||
other = "Version"
|
other = "Version"
|
||||||
|
|
||||||
[OneKeyInstall]
|
[OneKeyInstall]
|
||||||
other = "One-Command Install"
|
other = "Installation commands"
|
||||||
|
|
||||||
[ClickToCopy]
|
[ClickToCopy]
|
||||||
other = "Click To Copy"
|
other = "Click To Copy"
|
||||||
@@ -314,7 +314,7 @@ other = "Click To Copy"
|
|||||||
other = "Delete Server"
|
other = "Delete Server"
|
||||||
|
|
||||||
[ConfirmToDeleteServer]
|
[ConfirmToDeleteServer]
|
||||||
other = "Confirm to Delete Server?"
|
other = "Confirm Deleting This Server?"
|
||||||
|
|
||||||
[NoServerSelected]
|
[NoServerSelected]
|
||||||
other = "No Server Selected"
|
other = "No Server Selected"
|
||||||
@@ -545,7 +545,7 @@ other = "Scheduled Tasks"
|
|||||||
other = "API Management"
|
other = "API Management"
|
||||||
|
|
||||||
[IssueNewApiToken]
|
[IssueNewApiToken]
|
||||||
other = "Issue New API Token"
|
other = "Generate New API Token"
|
||||||
|
|
||||||
[Token]
|
[Token]
|
||||||
other = "Token"
|
other = "Token"
|
||||||
@@ -554,10 +554,10 @@ other = "Token"
|
|||||||
other = "Delete Token"
|
other = "Delete Token"
|
||||||
|
|
||||||
[ConfirmToDeleteThisToken]
|
[ConfirmToDeleteThisToken]
|
||||||
other = "Confirm to Delete This Token?"
|
other = "Confirm Deleting This Token?"
|
||||||
|
|
||||||
[YouAreNotAuthorized]
|
[YouAreNotAuthorized]
|
||||||
other = "Not Authorized for This Page"
|
other = "This Page needs Authorization"
|
||||||
|
|
||||||
[WrongAccessPassword]
|
[WrongAccessPassword]
|
||||||
other = "Wrong Access Password"
|
other = "Wrong Access Password"
|
||||||
@@ -605,13 +605,13 @@ other = "IP Changed"
|
|||||||
other = "Remaining Data"
|
other = "Remaining Data"
|
||||||
|
|
||||||
[DashboardTheme]
|
[DashboardTheme]
|
||||||
other = "Admin Panel Theme"
|
other = "Dashboard Theme"
|
||||||
|
|
||||||
[Info]
|
[Info]
|
||||||
other = "Info"
|
other = "Info"
|
||||||
|
|
||||||
[HideForGuest]
|
[HideForGuest]
|
||||||
other = "Hide for Guest"
|
other = "Hide from Guest"
|
||||||
|
|
||||||
[Menu]
|
[Menu]
|
||||||
other = "Menu"
|
other = "Menu"
|
||||||
@@ -622,20 +622,62 @@ other = "Network"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "Enable Show in Service"
|
other = "Enable Show in Service"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "Dynamic DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS Profiles"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "New Profile"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "Enable DDNS"
|
other = "Enable DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "Enable DDNS IPv4"
|
other = "IPv4 Enabled"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "Enable DDNS IPv6"
|
other = "IPv6 Enabled"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS Domain"
|
other = "Domains"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS Profile Name"
|
other = "Domains (separate with comma)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDNS Provider"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "Maximum retry attempts"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "Credential 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "Credential 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "Token ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "Token Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook URL"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook Request Method"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook Request Type"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook Request Headers"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook Request Body"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Feature"
|
other = "Feature"
|
||||||
@@ -690,3 +732,18 @@ other = "Cancel"
|
|||||||
|
|
||||||
[FMError]
|
[FMError]
|
||||||
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
|
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "Remaining"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "Lifetime"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "Price"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "Expired"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "d"
|
||||||
|
|||||||
Vendored
+67
-10
@@ -71,7 +71,7 @@ other = "Ignorar todo, ejecutar solo a través de servidores específicos"
|
|||||||
other = "Incluir todo, excepto servidores específicos"
|
other = "Incluir todo, excepto servidores específicos"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "Ejecutar por servidor disparador"
|
other = "Ejecutar en servidores con alarma"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "Servidores Específicos"
|
other = "Servidores Específicos"
|
||||||
@@ -104,7 +104,7 @@ other = "Segundos, Minutos, Horas, Días, Meses, Semanas - Ver detalles a"
|
|||||||
other = "Formato de Expresión de Cron"
|
other = "Formato de Expresión de Cron"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>&</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>;</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "Agregar Monitor de Servicio"
|
other = "Agregar Monitor de Servicio"
|
||||||
@@ -254,7 +254,7 @@ other = "Cubrir Todo"
|
|||||||
other = "Ignorar Todo"
|
other = "Ignorar Todo"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "Ejecutar por Disparador"
|
other = "Servidores con alarma"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "Eliminar Tarea Programada"
|
other = "Eliminar Tarea Programada"
|
||||||
@@ -281,7 +281,7 @@ other = "¿No tiene una cuenta?"
|
|||||||
other = "Certificados HTTP(S)/SSL"
|
other = "Certificados HTTP(S)/SSL"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "Puerto TCP"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "Eliminar Servicio"
|
other = "Eliminar Servicio"
|
||||||
@@ -622,20 +622,62 @@ other = "Red"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "Mostrar en servicio"
|
other = "Mostrar en servicio"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "DNS Dinámico"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "Perfiles DDNS"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "Nuevo Perfil"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "Habilitar DDNS"
|
other = "Habilitar DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "Habilitar DDNS IPv4"
|
other = "IPv4 Activado"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "Habilitar DDNS IPv6"
|
other = "IPv6 Activado"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "Dominio DDNS"
|
other = "Dominios"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "Nombre del perfil de DDNS"
|
other = "Dominios (separados por comas)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "Proveedor DDNS"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "Número máximo de intentos de reintento"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "Credencial 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "Credencial 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "ID del Token"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "Secreto del Token"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "URL del Webhook"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Método de Solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Tipo de solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Encabezados de Solicitud del Webhook"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Cuerpo de Solicitud del Webhook"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "Característica"
|
other = "Característica"
|
||||||
@@ -690,3 +732,18 @@ other = "Cancelar"
|
|||||||
|
|
||||||
[FMError]
|
[FMError]
|
||||||
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
|
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "Remaining"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "Lifetime"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "Price"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "Expired"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "d"
|
||||||
|
|||||||
Vendored
+69
-12
@@ -71,7 +71,7 @@ other = "忽略所有,仅通过特定服务器执行"
|
|||||||
other = "覆盖所有,仅特定服务器不执行"
|
other = "覆盖所有,仅特定服务器不执行"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "由触发的服务器执行"
|
other = "由触发告警的服务器执行"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "特定服务器"
|
other = "特定服务器"
|
||||||
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
|
|||||||
other = "计划表达式格式"
|
other = "计划表达式格式"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>&</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
|
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>;</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "新增监控"
|
other = "新增监控"
|
||||||
@@ -254,7 +254,7 @@ other = "覆盖所有"
|
|||||||
other = "忽略所有"
|
other = "忽略所有"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "触发执行"
|
other = "触发告警的服务器"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "删除计划任务"
|
other = "删除计划任务"
|
||||||
@@ -281,7 +281,7 @@ other = "没有账号?"
|
|||||||
other = "HTTP(S)/SSL证书"
|
other = "HTTP(S)/SSL证书"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP 端口"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "移除服务"
|
other = "移除服务"
|
||||||
@@ -425,7 +425,7 @@ other = "活动"
|
|||||||
other = "版本"
|
other = "版本"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "网络"
|
other = "网速"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "在线"
|
other = "在线"
|
||||||
@@ -622,20 +622,62 @@ other = "网络"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "在服务中显示"
|
other = "在服务中显示"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "动态 DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS配置"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "新配置"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "启用DDNS"
|
other = "启用 DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "启用DDNS IPv4"
|
other = "启用 DDNS IPv4"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "启用DDNS IPv6"
|
other = "启用 DDNS IPv6"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS域名"
|
other = "DDNS 域名"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS配置名"
|
other = "域名(逗号分隔)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDNS 供应商"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "最大重试次数"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "DDNS 凭据 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "DDNS 凭据 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "令牌 ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "令牌 Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook 地址"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook 请求方式"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook 请求类型"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook 请求头"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook 请求体"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
@@ -690,3 +732,18 @@ other = "取消"
|
|||||||
|
|
||||||
[FMError]
|
[FMError]
|
||||||
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
|
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "剩余"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "永续"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "价格"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "已到期"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "天"
|
||||||
|
|||||||
Vendored
+69
-12
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
|
|||||||
other = "覆蓋所有,僅特定伺服器不執行"
|
other = "覆蓋所有,僅特定伺服器不執行"
|
||||||
|
|
||||||
[ExecuteByTriggerServer]
|
[ExecuteByTriggerServer]
|
||||||
other = "由觸發的伺服器執行"
|
other = "由觸發告警的伺服器執行"
|
||||||
|
|
||||||
[SpecificServers]
|
[SpecificServers]
|
||||||
other = "特定伺服器"
|
other = "特定伺服器"
|
||||||
@@ -104,7 +104,7 @@ other = "秒 分 時 天 月 星期,詳情見"
|
|||||||
other = "排程表達式格式"
|
other = "排程表達式格式"
|
||||||
|
|
||||||
[IntroductionOfCommands]
|
[IntroductionOfCommands]
|
||||||
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>;</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
||||||
|
|
||||||
[AddMonitor]
|
[AddMonitor]
|
||||||
other = "新增監控"
|
other = "新增監控"
|
||||||
@@ -254,7 +254,7 @@ other = "覆蓋所有"
|
|||||||
other = "忽略所有"
|
other = "忽略所有"
|
||||||
|
|
||||||
[ByTrigger]
|
[ByTrigger]
|
||||||
other = "觸發執行"
|
other = "觸發告警的伺服器"
|
||||||
|
|
||||||
[DeleteScheduledTask]
|
[DeleteScheduledTask]
|
||||||
other = "刪除排程任務"
|
other = "刪除排程任務"
|
||||||
@@ -281,7 +281,7 @@ other = "沒有帳號?"
|
|||||||
other = "HTTP(S)/SSL證書"
|
other = "HTTP(S)/SSL證書"
|
||||||
|
|
||||||
[TCPPort]
|
[TCPPort]
|
||||||
other = "TCP 端口"
|
other = "TCPing"
|
||||||
|
|
||||||
[DeleteService]
|
[DeleteService]
|
||||||
other = "移除服務"
|
other = "移除服務"
|
||||||
@@ -425,7 +425,7 @@ other = "活動"
|
|||||||
other = "版本"
|
other = "版本"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "網路"
|
other = "網速"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "在線"
|
other = "在線"
|
||||||
@@ -622,20 +622,62 @@ other = "網路"
|
|||||||
[EnableShowInService]
|
[EnableShowInService]
|
||||||
other = "在服務中顯示"
|
other = "在服務中顯示"
|
||||||
|
|
||||||
|
[DDNS]
|
||||||
|
other = "動態 DNS"
|
||||||
|
|
||||||
|
[DDNSProfiles]
|
||||||
|
other = "DDNS配置"
|
||||||
|
|
||||||
|
[AddDDNSProfile]
|
||||||
|
other = "新增配置"
|
||||||
|
|
||||||
[EnableDDNS]
|
[EnableDDNS]
|
||||||
other = "啟用DDNS"
|
other = "啟用 DDNS"
|
||||||
|
|
||||||
[EnableIPv4]
|
[EnableIPv4]
|
||||||
other = "啟用DDNS IPv4"
|
other = "啟用 DDNS IPv4"
|
||||||
|
|
||||||
[EnableIpv6]
|
[EnableIPv6]
|
||||||
other = "啟用DDNS IPv6"
|
other = "啟用 DDNS IPv6"
|
||||||
|
|
||||||
[DDNSDomain]
|
[DDNSDomain]
|
||||||
other = "DDNS網域"
|
other = "DDNS 域名"
|
||||||
|
|
||||||
[DDNSProfile]
|
[DDNSDomains]
|
||||||
other = "DDNS設定名"
|
other = "域名(逗號分隔)"
|
||||||
|
|
||||||
|
[DDNSProvider]
|
||||||
|
other = "DDN S供應商"
|
||||||
|
|
||||||
|
[MaxRetries]
|
||||||
|
other = "最大重試次數"
|
||||||
|
|
||||||
|
[DDNSAccessID]
|
||||||
|
other = "DDNS 憑據 1"
|
||||||
|
|
||||||
|
[DDNSAccessSecret]
|
||||||
|
other = "DDNS 憑據 2"
|
||||||
|
|
||||||
|
[DDNSTokenID]
|
||||||
|
other = "令牌 ID"
|
||||||
|
|
||||||
|
[DDNSTokenSecret]
|
||||||
|
other = "令牌 Secret"
|
||||||
|
|
||||||
|
[WebhookURL]
|
||||||
|
other = "Webhook 地址"
|
||||||
|
|
||||||
|
[WebhookMethod]
|
||||||
|
other = "Webhook 請求方式"
|
||||||
|
|
||||||
|
[WebhookRequestType]
|
||||||
|
other = "Webhook 請求類型"
|
||||||
|
|
||||||
|
[WebhookHeaders]
|
||||||
|
other = "Webhook 請求頭"
|
||||||
|
|
||||||
|
[WebhookRequestBody]
|
||||||
|
other = "Webhook 請求體"
|
||||||
|
|
||||||
[Feature]
|
[Feature]
|
||||||
other = "功能"
|
other = "功能"
|
||||||
@@ -690,3 +732,18 @@ other = "取消"
|
|||||||
|
|
||||||
[FMError]
|
[FMError]
|
||||||
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
|
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
|
||||||
|
|
||||||
|
[Remaining]
|
||||||
|
other = "剩餘"
|
||||||
|
|
||||||
|
[Lifetime]
|
||||||
|
other = "永續"
|
||||||
|
|
||||||
|
[Price]
|
||||||
|
other = "價格"
|
||||||
|
|
||||||
|
[Expired]
|
||||||
|
other = "已到期"
|
||||||
|
|
||||||
|
[Days]
|
||||||
|
other = "天"
|
||||||
|
|||||||
Vendored
+13
-1
@@ -2,10 +2,14 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var StaticFS *utils.HybridFS
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var StaticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
//go:embed template
|
//go:embed template
|
||||||
var TemplateFS embed.FS
|
var TemplateFS embed.FS
|
||||||
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
|
|||||||
//go:embed l10n
|
//go:embed l10n
|
||||||
var I18nFS embed.FS
|
var I18nFS embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IsTemplateFileExist(name string) bool {
|
func IsTemplateFileExist(name string) bool {
|
||||||
_, err := TemplateFS.Open(name)
|
_, err := TemplateFS.Open(name)
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|||||||
+110
-16
@@ -99,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
|||||||
item.name === "DisplayIndex" ||
|
item.name === "DisplayIndex" ||
|
||||||
item.name === "Type" ||
|
item.name === "Type" ||
|
||||||
item.name === "Cover" ||
|
item.name === "Cover" ||
|
||||||
item.name === "Duration"
|
item.name === "Duration" ||
|
||||||
|
item.name === "MaxRetries" ||
|
||||||
|
item.name === "Provider" ||
|
||||||
|
item.name === "WebhookMethod" ||
|
||||||
|
item.name === "WebhookRequestType"
|
||||||
) {
|
) {
|
||||||
obj[item.name] = parseInt(item.value);
|
obj[item.name] = parseInt(item.value);
|
||||||
} else if (item.name.endsWith("Latency")) {
|
} else if (item.name.endsWith("Latency")) {
|
||||||
@@ -128,6 +132,16 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.name.endsWith("DDNSProfilesRaw")) {
|
||||||
|
if (item.value.length > 2) {
|
||||||
|
obj[item.name] = JSON.stringify(
|
||||||
|
[...item.value.matchAll(/\d+/gm)].map((k) =>
|
||||||
|
parseInt(k[0])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
$.post(URL, JSON.stringify(data))
|
$.post(URL, JSON.stringify(data))
|
||||||
@@ -207,6 +221,7 @@ function addOrEditAlertRule(rule) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=FailTriggerTasksRaw]")
|
.find("input[name=FailTriggerTasksRaw]")
|
||||||
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||||
@@ -256,6 +271,55 @@ function addOrEditNotification(notification) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addOrEditDDNS(ddns) {
|
||||||
|
const modal = $(".ddns.modal");
|
||||||
|
modal.children(".header").text((ddns ? LANG.Edit : LANG.Add));
|
||||||
|
modal
|
||||||
|
.find(".nezha-primary-btn.button")
|
||||||
|
.html(
|
||||||
|
ddns
|
||||||
|
? LANG.Edit + '<i class="edit icon"></i>'
|
||||||
|
: LANG.Add + '<i class="add icon"></i>'
|
||||||
|
);
|
||||||
|
modal.find("input[name=ID]").val(ddns ? ddns.ID : null);
|
||||||
|
modal.find("input[name=Name]").val(ddns ? ddns.Name : null);
|
||||||
|
modal.find("input[name=DomainsRaw]").val(ddns ? ddns.DomainsRaw : null);
|
||||||
|
modal.find("input[name=AccessID]").val(ddns ? ddns.AccessID : null);
|
||||||
|
modal.find("input[name=AccessSecret]").val(ddns ? ddns.AccessSecret : null);
|
||||||
|
modal.find("input[name=MaxRetries]").val(ddns ? ddns.MaxRetries : 3);
|
||||||
|
modal.find("input[name=WebhookURL]").val(ddns ? ddns.WebhookURL : null);
|
||||||
|
modal
|
||||||
|
.find("textarea[name=WebhookHeaders]")
|
||||||
|
.val(ddns ? ddns.WebhookHeaders : null);
|
||||||
|
modal
|
||||||
|
.find("textarea[name=WebhookRequestBody]")
|
||||||
|
.val(ddns ? ddns.WebhookRequestBody : null);
|
||||||
|
modal
|
||||||
|
.find("select[name=Provider]")
|
||||||
|
.val(ddns ? ddns.Provider : 0);
|
||||||
|
modal
|
||||||
|
.find("select[name=WebhookMethod]")
|
||||||
|
.val(ddns ? ddns.WebhookMethod : 1);
|
||||||
|
modal
|
||||||
|
.find("select[name=WebhookRequestType]")
|
||||||
|
.val(ddns ? ddns.WebhookRequestType : 1);
|
||||||
|
if (ddns && ddns.EnableIPv4) {
|
||||||
|
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
if (ddns && ddns.EnableIPv6) {
|
||||||
|
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
||||||
|
} else {
|
||||||
|
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
||||||
|
}
|
||||||
|
showFormModal(
|
||||||
|
".ddns.modal",
|
||||||
|
"#ddnsForm",
|
||||||
|
"/api/ddns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function addOrEditNAT(nat) {
|
function addOrEditNAT(nat) {
|
||||||
const modal = $(".nat.modal");
|
const modal = $(".nat.modal");
|
||||||
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
|
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
|
||||||
@@ -325,8 +389,33 @@ function addOrEditServer(server, conf) {
|
|||||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||||
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
|
modal.find("a.ui.label.visible").each((i, el) => {
|
||||||
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null);
|
el.remove();
|
||||||
|
});
|
||||||
|
var ddns;
|
||||||
|
if (server) {
|
||||||
|
ddns = server.DDNSProfilesRaw;
|
||||||
|
let serverList;
|
||||||
|
try {
|
||||||
|
serverList = JSON.parse(ddns);
|
||||||
|
} catch (error) {
|
||||||
|
serverList = "[]";
|
||||||
|
}
|
||||||
|
const node = modal.find("i.dropdown.icon.ddnsProfiles");
|
||||||
|
for (let i = 0; i < serverList.length; i++) {
|
||||||
|
node.after(
|
||||||
|
'<a class="ui label transition visible" data-value="' +
|
||||||
|
serverList[i] +
|
||||||
|
'" style="display: inline-block !important;">ID:' +
|
||||||
|
serverList[i] +
|
||||||
|
'<i class="delete icon"></i></a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
|
modal
|
||||||
|
.find("input[name=DDNSProfilesRaw]")
|
||||||
|
.val(server ? "[]," + ddns.substr(1, ddns.length - 2) : "[]");
|
||||||
modal
|
modal
|
||||||
.find("input[name=DisplayIndex]")
|
.find("input[name=DisplayIndex]")
|
||||||
.val(server ? server.DisplayIndex : null);
|
.val(server ? server.DisplayIndex : null);
|
||||||
@@ -342,26 +431,17 @@ function addOrEditServer(server, conf) {
|
|||||||
modal.find(".command.field").attr("style", "display:none");
|
modal.find(".command.field").attr("style", "display:none");
|
||||||
modal.find("input[name=secret]").val("");
|
modal.find("input[name=secret]").val("");
|
||||||
}
|
}
|
||||||
if (server && server.HideForGuest) {
|
|
||||||
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
|
||||||
} else {
|
|
||||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
|
||||||
}
|
|
||||||
if (server && server.EnableDDNS) {
|
if (server && server.EnableDDNS) {
|
||||||
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
||||||
} else {
|
} else {
|
||||||
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
||||||
}
|
}
|
||||||
if (server && server.EnableIPv4) {
|
if (server && server.HideForGuest) {
|
||||||
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
||||||
} else {
|
} else {
|
||||||
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||||
}
|
|
||||||
if (server && server.EnableIpv6) {
|
|
||||||
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
|
||||||
} else {
|
|
||||||
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +527,7 @@ function addOrEditMonitor(monitor) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=FailTriggerTasksRaw]")
|
.find("input[name=FailTriggerTasksRaw]")
|
||||||
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||||
@@ -492,6 +573,7 @@ function addOrEditCron(cron) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 需要在 showFormModal 进一步拼接数组
|
||||||
modal
|
modal
|
||||||
.find("input[name=ServersRaw]")
|
.find("input[name=ServersRaw]")
|
||||||
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
||||||
@@ -621,3 +703,15 @@ $(document).ready(() => {
|
|||||||
});
|
});
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).ready(() => {
|
||||||
|
try {
|
||||||
|
$(".ui.ddns.search.dropdown").dropdown({
|
||||||
|
clearable: true,
|
||||||
|
apiSettings: {
|
||||||
|
url: "/api/search-ddns?word={query}",
|
||||||
|
cache: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) { }
|
||||||
|
});
|
||||||
|
|||||||
+19
-14
@@ -8,6 +8,11 @@ body[theme="dark"] .navbar .navbar-brand {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
|
||||||
|
body[theme="dark"] .navbar .navbar-nav li.m-active a {
|
||||||
|
color: rgba(73, 146, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
body[theme="dark"] .navbar .dropdown-menu {
|
body[theme="dark"] .navbar .dropdown-menu {
|
||||||
background-color: rgba(23, 26, 30, 1);
|
background-color: rgba(23, 26, 30, 1);
|
||||||
border-color: rgba(49, 54, 59, 1);
|
border-color: rgba(49, 54, 59, 1);
|
||||||
@@ -27,7 +32,7 @@ body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
|
|||||||
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
|
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
|
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .active > a,
|
body[theme="dark"] .navbar .navbar-nav > .active > a,
|
||||||
body[theme="dark"] .navbar .navbar-nav > .open > a{
|
body[theme="dark"] .navbar .navbar-nav > .open > a {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -53,7 +58,7 @@ body[theme="dark"] .table {
|
|||||||
body[theme="dark"] .table > tbody > tr > td:before,
|
body[theme="dark"] .table > tbody > tr > td:before,
|
||||||
body[theme="dark"] .table > tfoot > tr > td:before,
|
body[theme="dark"] .table > tfoot > tr > td:before,
|
||||||
body[theme="dark"] .table > thead > tr > td:before,
|
body[theme="dark"] .table > thead > tr > td:before,
|
||||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||||
background-color: rgba(58, 62, 65, 1);
|
background-color: rgba(58, 62, 65, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ body[theme="dark"] .table-striped tbody > tr.odd > th {
|
|||||||
background-color: rgba(34, 35, 46, 1);
|
background-color: rgba(34, 35, 46, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table-hover > tbody > tr:hover{
|
body[theme="dark"] .table-hover > tbody > tr:hover {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,18 +83,18 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: rgba(34, 35, 46, 1);
|
background-color: rgba(34, 35, 46, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(28, 29, 38, 1);
|
||||||
}
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
|
|
||||||
body[theme="dark"] .progress {
|
body[theme="dark"] .progress {
|
||||||
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%);
|
background-image: none;
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(255, 255, 255, 0.075);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .progress-bar {
|
body[theme="dark"] .progress-bar {
|
||||||
@@ -150,29 +155,29 @@ body[theme="dark"] footer p a:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-content{
|
body[theme="dark"] .modal-content {
|
||||||
background-color: rgba(28, 29, 38, 1);
|
background-color: rgba(28, 29, 38, 1);
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] #earthChartBox .modal-content{
|
body[theme="dark"] #earthChartBox .modal-content {
|
||||||
background-color: rgba(0, 0, 0, 1);
|
background-color: rgba(0, 0, 0, 1);
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-header{
|
body[theme="dark"] .modal-header {
|
||||||
border-bottom-color:#080808;
|
border-bottom-color:#080808;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .modal-header i.xclose{
|
body[theme="dark"] .modal-header i.xclose {
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .toolbox i{
|
body[theme="dark"] .toolbox i {
|
||||||
color: rgba(241, 241, 241, 1);
|
color: rgba(241, 241, 241, 1);
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .network-box .network-box-header{
|
body[theme="dark"] .network-box .network-box-header {
|
||||||
border-bottom: 1px solid rgba(110, 112, 121, 0.25);
|
border-bottom-color: rgba(110, 112, 121, 0.25);
|
||||||
}
|
}
|
||||||
+3
-3
@@ -86,7 +86,7 @@ body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
|
|||||||
body[theme="dark"] .table > tbody > tr > td:before,
|
body[theme="dark"] .table > tbody > tr > td:before,
|
||||||
body[theme="dark"] .table > tfoot > tr > td:before,
|
body[theme="dark"] .table > tfoot > tr > td:before,
|
||||||
body[theme="dark"] .table > thead > tr > td:before,
|
body[theme="dark"] .table > thead > tr > td:before,
|
||||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||||
/** border-bottom 颜色 **/
|
/** border-bottom 颜色 **/
|
||||||
background-color: rgba(155, 155, 155, 0.1);
|
background-color: rgba(155, 155, 155, 0.1);
|
||||||
}
|
}
|
||||||
@@ -96,11 +96,11 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
|
|||||||
+19
-14
@@ -16,10 +16,15 @@ body[theme="light"] .navbar .navbar-brand {
|
|||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-nav > li > a{
|
body[theme="light"] .navbar .navbar-nav > li > a {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="light"] .navbar .navbar-nav li.pc-active a,
|
||||||
|
body[theme="light"] .navbar .navbar-nav li.m-active a {
|
||||||
|
color: rgba(73, 146, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-nav > .open > a:focus,
|
body[theme="light"] .navbar .navbar-nav > .open > a:focus,
|
||||||
body[theme="light"] .navbar .navbar-nav > .open > a:hover,
|
body[theme="light"] .navbar .navbar-nav > .open > a:hover,
|
||||||
body[theme="light"] .navbar .navbar-nav > .active > a,
|
body[theme="light"] .navbar .navbar-nav > .active > a,
|
||||||
@@ -42,16 +47,16 @@ body[theme="light"] .navbar .navbar-nav .open .dropdown-menu > li > a {
|
|||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle{
|
body[theme="light"] .navbar .navbar-toggle {
|
||||||
border:none;
|
border:none;
|
||||||
padding:9px 12px;
|
padding:9px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-collapse{
|
body[theme="light"] .navbar .navbar-collapse {
|
||||||
border-color:rgba(0, 0, 0, 0.085);
|
border-color:rgba(0, 0, 0, 0.085);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle .icon-bar{
|
body[theme="light"] .navbar .navbar-toggle .icon-bar {
|
||||||
background-color: rgba(0, 0, 0, 0.87);
|
background-color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +95,12 @@ body[theme="light"] .table-striped tbody > tr.odd > th {
|
|||||||
body[theme="light"] .table > tbody > tr > td:before,
|
body[theme="light"] .table > tbody > tr > td:before,
|
||||||
body[theme="light"] .table > tfoot > tr > td:before,
|
body[theme="light"] .table > tfoot > tr > td:before,
|
||||||
body[theme="light"] .table > thead > tr > td:before,
|
body[theme="light"] .table > thead > tr > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
|
||||||
background-color: rgba(221, 221, 221, 1);
|
background-color: rgba(221, 221, 221, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .table > tbody > tr.expandRow > td:before,
|
body[theme="light"] .table > tbody > tr.expandRow > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-tag > th:before{
|
body[theme="light"] .table > thead > tr.node-group-tag > th:before {
|
||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,36 +174,36 @@ body[theme="light"] footer p a:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox i{
|
body[theme="light"] .toolbox i {
|
||||||
color: rgba(94, 114, 228, .9);
|
color: rgba(94, 114, 228, .9);
|
||||||
background-color: rgba(255, 255, 255, .5);
|
background-color: rgba(255, 255, 255, .5);
|
||||||
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox .mytoggle .tooltip-inner{
|
body[theme="light"] .toolbox .mytoggle .tooltip-inner {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox .mytoggle li:hover{
|
body[theme="light"] .toolbox .mytoggle li:hover {
|
||||||
background-color:#e6e6e6;
|
background-color:#e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-content{
|
body[theme="light"] .modal-content {
|
||||||
background-color: rgba(249, 249, 249, 1);
|
background-color: rgba(249, 249, 249, 1);
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-header{
|
body[theme="light"] .modal-header {
|
||||||
border-bottom-color:#f1f1f1;
|
border-bottom-color:#f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .modal-header i.xclose{
|
body[theme="light"] .modal-header i.xclose {
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .network-box .network-box-header{
|
body[theme="light"] .network-box .network-box-header {
|
||||||
border-bottom: 1px solid rgba(224, 230, 241, 0.6);
|
border-bottom-color: rgba(224, 230, 241, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
|
|||||||
+7
-7
@@ -36,16 +36,16 @@ body[theme="light"] .navbar .dropdown-menu {
|
|||||||
|
|
||||||
|
|
||||||
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
|
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
|
||||||
body[theme="light"] .navbar .dropdown-menu > li > a:hover{
|
body[theme="light"] .navbar .dropdown-menu > li > a:hover {
|
||||||
/** 二级导航鼠标悬停选中背景 **/
|
/** 二级导航鼠标悬停选中背景 **/
|
||||||
background-color: rgba(0, 0, 0, .05);
|
background-color: rgba(0, 0, 0, .05);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-collapse{
|
body[theme="light"] .navbar .navbar-collapse {
|
||||||
border-color:rgba(0, 0, 0, 0.085);
|
border-color:rgba(0, 0, 0, 0.085);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .navbar .navbar-toggle .icon-bar{
|
body[theme="light"] .navbar .navbar-toggle .icon-bar {
|
||||||
background-color: rgba(0, 0, 0, 0.9);
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ body[theme="light"] .table > thead > tr.node-group-tag > th:before {
|
|||||||
body[theme="light"] .table > tbody > tr > td:before,
|
body[theme="light"] .table > tbody > tr > td:before,
|
||||||
body[theme="light"] .table > tfoot > tr > td:before,
|
body[theme="light"] .table > tfoot > tr > td:before,
|
||||||
body[theme="light"] .table > thead > tr > td:before,
|
body[theme="light"] .table > thead > tr > td:before,
|
||||||
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
|
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
|
||||||
/** border-bottom 颜色 **/
|
/** border-bottom 颜色 **/
|
||||||
background-color: rgba(0, 0, 0, 0.075);
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
}
|
}
|
||||||
@@ -97,11 +97,11 @@ body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* expandRow展开部分样式 */
|
/* expandRow展开部分样式 */
|
||||||
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{
|
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .table > tbody > tr.expandRow.even > td:before{
|
body[theme="light"] .table > tbody > tr.expandRow.even > td:before {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
}
|
}
|
||||||
/* expandRow展开部分样式结束 */
|
/* expandRow展开部分样式结束 */
|
||||||
@@ -123,7 +123,7 @@ body[theme="light"] .progress-bar-danger {
|
|||||||
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
|
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme="light"] .toolbox i{
|
body[theme="light"] .toolbox i {
|
||||||
background-color: rgba(255, 255, 255, .55);
|
background-color: rgba(255, 255, 255, .55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-36
@@ -250,6 +250,24 @@ tr.accordion-toggle{
|
|||||||
max-width: 50px;
|
max-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining {
|
||||||
|
min-width: 65px;
|
||||||
|
max-width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining .additional {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-cell.remaining .additional small{
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
.temp-detail {
|
.temp-detail {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -322,15 +340,17 @@ td.ping-network-quality {
|
|||||||
/* 服务页 正文结束 */
|
/* 服务页 正文结束 */
|
||||||
|
|
||||||
/* 网络页 正文*/
|
/* 网络页 正文*/
|
||||||
.network-box .btn-group.open .dropdown-toggle{
|
.network-box .btn-group.open .dropdown-toggle {
|
||||||
box-shadow:unset;
|
box-shadow:unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header{
|
.network-box .network-box-header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding: 5px 0px 15px 5px;
|
padding: 5px 0px 15px 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu {
|
.network-box .network-box-header .dropdown-menu {
|
||||||
@@ -345,12 +365,12 @@ td.ping-network-quality {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li{
|
.network-box .network-box-header .dropdown-menu li {
|
||||||
height: 2em;
|
height: 2em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li.input-group{
|
.network-box .network-box-header .dropdown-menu li.input-group {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -358,7 +378,7 @@ td.ping-network-quality {
|
|||||||
z-index: 99999997;
|
z-index: 99999997;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .network-box-header .dropdown-menu li.input-group input{
|
.network-box .network-box-header .dropdown-menu li.input-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -387,7 +407,7 @@ td.ping-network-quality {
|
|||||||
margin: 18px 0px 15px 0px;
|
margin: 18px 0px 15px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-box .chartTitle i.chartCountryCode{
|
.network-box .chartTitle i.chartCountryCode {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 12.5%;
|
border-radius: 12.5%;
|
||||||
}
|
}
|
||||||
@@ -395,17 +415,17 @@ td.ping-network-quality {
|
|||||||
/* 网络页 正文结束 */
|
/* 网络页 正文结束 */
|
||||||
|
|
||||||
/* 地图版服务器分布图 */
|
/* 地图版服务器分布图 */
|
||||||
#mapChartBox{
|
#mapChartBox {
|
||||||
z-index: 999999999;
|
z-index: 999999999;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-open .modal{
|
.modal-open .modal {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-dialog-centered{
|
.modal-dialog-centered {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -415,7 +435,7 @@ td.ping-network-quality {
|
|||||||
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
|
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content{
|
.modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -423,15 +443,15 @@ td.ping-network-quality {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header{
|
.modal-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h4{
|
.modal-header h4 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header i.xclose{
|
.modal-header i.xclose {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -439,32 +459,32 @@ td.ping-network-quality {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body{
|
.modal-body {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.template-item, a.cdn-item{
|
a.template-item, a.cdn-item {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer{
|
footer {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer p{
|
footer p {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
.accordian-body{
|
.accordian-body {
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
table.monitor-detail{
|
table.monitor-detail {
|
||||||
margin:0px 0px 2px 0px;
|
margin:0px 0px 2px 0px;
|
||||||
}
|
}
|
||||||
.table .network-thead-tr th,
|
.table .network-thead-tr th,
|
||||||
.table .node-group-tag th{
|
.table .node-group-tag th {
|
||||||
font-size:16px;
|
font-size:16px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
@@ -492,10 +512,10 @@ footer p{
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
footer{
|
footer {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
.cycle-transfer-stats .dataTables_wrapper{
|
.cycle-transfer-stats .dataTables_wrapper {
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
}
|
}
|
||||||
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
||||||
@@ -517,50 +537,58 @@ footer p{
|
|||||||
}
|
}
|
||||||
.node-cell.os,
|
.node-cell.os,
|
||||||
.node-cell.uptime,
|
.node-cell.uptime,
|
||||||
.node-cell.traffic{
|
.node-cell.traffic {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.node-cell.location{
|
.node-cell.location {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
.node-cell-os-text,.node-cell-location-text{
|
.node-cell-os-text,.node-cell-location-text {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.accordian-body{
|
.node-cell.network {
|
||||||
|
min-width: 95px;
|
||||||
|
max-width: 95px;
|
||||||
|
}
|
||||||
|
.node-cell.remaining {
|
||||||
|
min-width: 75px;
|
||||||
|
max-width: 75px;
|
||||||
|
}
|
||||||
|
.accordian-body {
|
||||||
margin: 5px 0px 5px 10px;
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
table.monitor-detail{
|
table.monitor-detail {
|
||||||
margin:0px 0px 2px 0px;
|
margin:0px 0px 2px 0px;
|
||||||
}
|
}
|
||||||
.table .network-thead-tr th,
|
.table .network-thead-tr th,
|
||||||
.table .node-group-tag th{
|
.table .node-group-tag th {
|
||||||
font-size:14px;
|
font-size:14px;
|
||||||
padding-bottom:6px;
|
padding-bottom:6px;
|
||||||
}
|
}
|
||||||
.table tr.network td{
|
.table tr.network td {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.service-status .service-status-th{
|
.service-status .service-status-th {
|
||||||
min-width:30px;
|
min-width:30px;
|
||||||
}
|
}
|
||||||
.service-status .delay-today{
|
.service-status .delay-today {
|
||||||
margin-top:4px;
|
margin-top:4px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.service-status .delay-today i{
|
.service-status .delay-today i {
|
||||||
margin-right:0px;
|
margin-right:0px;
|
||||||
}
|
}
|
||||||
.service-status .delay-today-text{
|
.service-status .delay-today-text {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.service-status .service-averagelatency-th{
|
.service-status .service-averagelatency-th {
|
||||||
min-width:70px;
|
min-width:70px;
|
||||||
}
|
}
|
||||||
.service-status .service-30daysonline-th{
|
.service-status .service-30daysonline-th {
|
||||||
min-width:75px;
|
min-width:75px;
|
||||||
}
|
}
|
||||||
.toolbox {
|
.toolbox {
|
||||||
@@ -572,12 +600,13 @@ footer p{
|
|||||||
.network-box .network-box-header {
|
.network-box .network-box-header {
|
||||||
margin: 8px 0px 0px 8px;
|
margin: 8px 0px 0px 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.network-box .chartTitle {
|
.network-box .chartTitle {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 10px 0px 10px 0px;
|
margin: 10px 0px 10px 0px;
|
||||||
}
|
}
|
||||||
.network-box .chartTitle i.chartCountryCode{
|
.network-box .chartTitle i.chartCountryCode {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border-radius: 12.5%;
|
border-radius: 12.5%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ const mixinsVue = {
|
|||||||
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
return window.innerWidth <= 768;
|
return window.innerWidth <= 768;
|
||||||
},
|
},
|
||||||
|
isMenuActive(page){
|
||||||
|
if(page == this.$root.page) {
|
||||||
|
return this.isMobile ? 'm-active' : 'pc-active';
|
||||||
|
}
|
||||||
|
},
|
||||||
setBenchmarkHeight() {
|
setBenchmarkHeight() {
|
||||||
let vh = window.innerHeight * 0.01;
|
let vh = window.innerHeight * 0.01;
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
|
|||||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v2024927"></script>
|
<script src="/static/main.js?v20241011"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
updateLang({{.LANG }});
|
updateLang({{.LANG }});
|
||||||
|
|||||||
Vendored
+1
@@ -9,6 +9,7 @@
|
|||||||
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
|
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
|
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
|
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
|
||||||
|
<a class='item{{if eq .MatchedPath "/ddns"}} active{{end}}' href="/ddns"><i class="globe icon"></i>{{tr "DDNS"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
|
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
|
||||||
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
||||||
<i class="settings icon"></i>{{tr "Settings"}}
|
<i class="settings icon"></i>{{tr "Settings"}}
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
{{define "component/ddns"}}
|
||||||
|
<div class="ui tiny ddns modal transition hidden">
|
||||||
|
<div class="header">Add</div>
|
||||||
|
<div class="content">
|
||||||
|
<form id="ddnsForm" class="ui form">
|
||||||
|
<input type="hidden" name="ID">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "Name"}}</label>
|
||||||
|
<input type="text" name="Name">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSProvider"}}</label>
|
||||||
|
<select name="Provider" class="ui fluid dropdown" id="providerSelect" onchange="toggleFields()">
|
||||||
|
{{ range $provider := .ProviderList }}
|
||||||
|
<option value="{{ $provider.ID }}">
|
||||||
|
{{ $provider.Name }}
|
||||||
|
</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSDomains"}}</label>
|
||||||
|
<input type="text" name="DomainsRaw" placeholder="www.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSAccessID"}}</label>
|
||||||
|
<input type="text" name="AccessID" placeholder="{{tr "DDNSTokenID"}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "DDNSAccessSecret"}}</label>
|
||||||
|
<input type="text" name="AccessSecret" placeholder="{{tr "DDNSTokenSecret"}}">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "MaxRetries"}}</label>
|
||||||
|
<input type="number" name="MaxRetries" placeholder="3">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookURL"}}</label>
|
||||||
|
<input type="text" name="WebhookURL" placeholder="https://ddns.example.com/?record=#record#">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookMethod"}}</label>
|
||||||
|
<select name="WebhookMethod" class="ui fluid dropdown">
|
||||||
|
<option value="1">GET</option>
|
||||||
|
<option value="2">POST</option>
|
||||||
|
<option value="3">PATCH</option>
|
||||||
|
<option value="4">DELETE</option>
|
||||||
|
<option value="5">PUT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookRequestType"}}</label>
|
||||||
|
<select name="WebhookRequestType" class="ui fluid dropdown">
|
||||||
|
<option value="1">JSON</option>
|
||||||
|
<option value="2">Form</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookHeaders"}}</label>
|
||||||
|
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{tr "WebhookRequestBody"}}</label>
|
||||||
|
<textarea name="WebhookRequestBody" placeholder='{ "ip": #ip#, "domain": "#domain#" }'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableipv4 checkbox">
|
||||||
|
<input name="EnableIPv4" type="checkbox" tabindex="0" class="hidden">
|
||||||
|
<label>{{tr "EnableIPv4"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui enableipv6 checkbox">
|
||||||
|
<input name="EnableIPv6" type="checkbox" tabindex="0" class="hidden">
|
||||||
|
<label>{{tr "EnableIPv6"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="ui negative button">{{tr "Cancel"}}</div>
|
||||||
|
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
+10
-21
@@ -21,37 +21,26 @@
|
|||||||
<input type="text" name="secret">
|
<input type="text" name="secret">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui hideforguest checkbox">
|
<label>{{tr "DDNSProfiles"}}</label>
|
||||||
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
|
<div class="ui fluid multiple ddns search selection dropdown">
|
||||||
<label>{{tr "HideForGuest"}}</label>
|
<input type="hidden" name="DDNSProfilesRaw">
|
||||||
|
<i class="dropdown icon ddnsProfiles"></i>
|
||||||
|
<div class="default text">{{tr "EnterIdAndNameToSearch"}}</div>
|
||||||
|
<div class="menu"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui enableddns checkbox">
|
<div class="ui enableddns checkbox">
|
||||||
<input name="EnableDDNS" type="checkbox" tabindex="0" />
|
<input name="EnableDDNS" type="checkbox" tabindex="0" class="hidden" />
|
||||||
<label>{{tr "EnableDDNS"}}</label>
|
<label>{{tr "EnableDDNS"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui enableipv4 checkbox">
|
<div class="ui hideforguest checkbox">
|
||||||
<input name="EnableIPv4" type="checkbox" tabindex="0" />
|
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
|
||||||
<label>{{tr "EnableIPv4"}}</label>
|
<label>{{tr "HideForGuest"}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<div class="ui enableipv6 checkbox">
|
|
||||||
<input name="EnableIpv6" type="checkbox" tabindex="0" />
|
|
||||||
<label>{{tr "EnableIpv6"}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{tr "DDNSDomain"}}</label>
|
|
||||||
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>{{tr "DDNSProfile"}}</label>
|
|
||||||
<input type="text" name="DDNSProfile" placeholder="{{tr "DDNSProfile"}}">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{tr "Note"}}</label>
|
<label>{{tr "Note"}}</label>
|
||||||
<textarea name="Note"></textarea>
|
<textarea name="Note"></textarea>
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
{{define "dashboard-default/ddns"}}
|
||||||
|
{{template "common/header" .}}
|
||||||
|
{{template "common/menu" .}}
|
||||||
|
<div class="nb-container">
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui grid">
|
||||||
|
<div class="right floated right aligned twelve wide column">
|
||||||
|
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditDDNS()"><i
|
||||||
|
class="add icon"></i> {{tr "AddDDNSProfile"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="ui basic table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>{{tr "Name"}}</th>
|
||||||
|
<th>{{tr "EnableIPv4"}}</th>
|
||||||
|
<th>{{tr "EnableIPv6"}}</th>
|
||||||
|
<th>{{tr "DDNSProvider"}}</th>
|
||||||
|
<th>{{tr "DDNSDomain"}}</th>
|
||||||
|
<th>{{tr "MaxRetries"}}</th>
|
||||||
|
<th>{{tr "Administration"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $item := .DDNS}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$item.ID}}</td>
|
||||||
|
<td>{{$item.Name}}</td>
|
||||||
|
<td>{{$item.EnableIPv4}}</td>
|
||||||
|
<td>{{$item.EnableIPv6}}</td>
|
||||||
|
<td>{{index $.ProviderMap $item.Provider}}</td>
|
||||||
|
<td>{{$item.DomainsRaw}}</td>
|
||||||
|
<td>{{$item.MaxRetries}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui mini icon buttons">
|
||||||
|
<button class="ui button" onclick="addOrEditDDNS({{$item}})">
|
||||||
|
<i class="edit icon"></i>
|
||||||
|
</button>
|
||||||
|
<button class="ui button"
|
||||||
|
onclick="showConfirm('确定删除DDNS配置?','确认删除',deleteRequest,'/api/ddns/'+{{$item.ID}})">
|
||||||
|
<i class="trash alternate outline icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "component/ddns" .}}
|
||||||
|
{{template "common/footer" .}}
|
||||||
|
<script>
|
||||||
|
$('.checkbox').checkbox()
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
+14
-12
@@ -28,11 +28,8 @@
|
|||||||
<th>{{tr "ServerGroup"}}</th>
|
<th>{{tr "ServerGroup"}}</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>{{tr "VersionNumber"}}</th>
|
<th>{{tr "VersionNumber"}}</th>
|
||||||
<th>{{tr "HideForGuest"}}</th>
|
|
||||||
<th>{{tr "EnableDDNS"}}</th>
|
<th>{{tr "EnableDDNS"}}</th>
|
||||||
<th>{{tr "EnableIPv4"}}</th>
|
<th>{{tr "HideForGuest"}}</th>
|
||||||
<th>{{tr "EnableIpv6"}}</th>
|
|
||||||
<th>{{tr "DDNSDomain"}}</th>
|
|
||||||
<th>{{tr "Secret"}}</th>
|
<th>{{tr "Secret"}}</th>
|
||||||
<th>{{tr "OneKeyInstall"}}</th>
|
<th>{{tr "OneKeyInstall"}}</th>
|
||||||
<th>{{tr "Note"}}</th>
|
<th>{{tr "Note"}}</th>
|
||||||
@@ -49,11 +46,8 @@
|
|||||||
<td>{{$server.Tag}}</td>
|
<td>{{$server.Tag}}</td>
|
||||||
<td>{{$server.Host.IP}}</td>
|
<td>{{$server.Host.IP}}</td>
|
||||||
<td>{{$server.Host.Version}}</td>
|
<td>{{$server.Host.Version}}</td>
|
||||||
<td>{{$server.HideForGuest}}</td>
|
|
||||||
<td>{{$server.EnableDDNS}}</td>
|
<td>{{$server.EnableDDNS}}</td>
|
||||||
<td>{{$server.EnableIPv4}}</td>
|
<td>{{$server.HideForGuest}}</td>
|
||||||
<td>{{$server.EnableIpv6}}</td>
|
|
||||||
<td>{{$server.DDNSDomain}}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
||||||
<i class="copy icon"></i>
|
<i class="copy icon"></i>
|
||||||
@@ -76,8 +70,16 @@
|
|||||||
<i class="apple icon"></i>
|
<i class="apple icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td>
|
<td>
|
||||||
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.PublicNote}}</td>
|
{{if $server.Note}}
|
||||||
|
<button class="ui icon green mini button" title="{{ $server.Note }}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky note icon"></i></button>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $server.PublicNote}}
|
||||||
|
<button class="ui icon green mini button" title="{{$server.PublicNote}}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky clipboard icon"></i></button>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui mini icon buttons">
|
<div class="ui mini icon buttons">
|
||||||
<button class="ui button" onclick="connectToServer({{$server.ID}})">
|
<button class="ui button" onclick="connectToServer({{$server.ID}})">
|
||||||
@@ -157,8 +159,8 @@
|
|||||||
}
|
}
|
||||||
function batchEditServerGroup() {
|
function batchEditServerGroup() {
|
||||||
let groupName = prompt('{{tr "InputServerGroupName"}}')
|
let groupName = prompt('{{tr "InputServerGroupName"}}')
|
||||||
if (!groupName) {
|
if (groupName === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const servers = []
|
const servers = []
|
||||||
checkBoxList.forEach(cb => {
|
checkBoxList.forEach(cb => {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||||
<script src="/static/main.js?v20240330"></script>
|
<script src="/static/main.js?v20241011"></script>
|
||||||
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
+2
-2
@@ -28,8 +28,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</aside>
|
</aside>
|
||||||
<template v-if="semiTransparent">
|
<template v-if="semiTransparent">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20241008">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20241008">
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
|
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
|
||||||
|
|||||||
+4
-4
@@ -26,10 +26,10 @@
|
|||||||
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||||
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240909">
|
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20241008">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20241008">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
|
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20241008">
|
||||||
<script src="/static/theme-server-status/js/mixin.js?v20240907"></script>
|
<script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
+30
-6
@@ -6,6 +6,7 @@
|
|||||||
<th class="node-cell name center">{{tr "Name"}}</th>
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
<th class="node-cell os center">{{tr "Platform"}}</th>
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
<th class="node-cell location center">{{tr "Location"}}</th>
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
|
||||||
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
<th class="node-cell load center">{{tr "Load"}}</th>
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
<th v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="servers">
|
<tbody id="servers">
|
||||||
@@ -30,11 +32,18 @@
|
|||||||
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
||||||
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
|
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell location">
|
<td class="node-cell location center">
|
||||||
<i :class="'fi fi-' + node.location"></i>
|
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'rb') : '')"></i>
|
||||||
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
|
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'RB') : ''#@</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
|
||||||
|
<template v-if="node.additional && node.additional.price">
|
||||||
|
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
|
||||||
|
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
@@ -59,6 +68,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<template v-if="node.additional && node.additional.remaining.format">
|
||||||
|
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
|
||||||
|
<div class="additional">
|
||||||
|
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
|
||||||
|
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
|
||||||
|
<small v-else>
|
||||||
|
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
|
||||||
|
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
<td colspan="16">
|
<td colspan="16">
|
||||||
@@ -66,8 +90,8 @@
|
|||||||
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
<span class="node-cell-expand">
|
<span class="node-cell-expand">
|
||||||
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
|
||||||
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="node-cell-expand" v-if="node.host.CPU">
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
<span class="node-cell-expand-label">CPU:</span>
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
|
|||||||
+30
-6
@@ -9,6 +9,7 @@
|
|||||||
<th class="node-cell name center">{{tr "Name"}}</th>
|
<th class="node-cell name center">{{tr "Name"}}</th>
|
||||||
<th class="node-cell os center">{{tr "Platform"}}</th>
|
<th class="node-cell os center">{{tr "Platform"}}</th>
|
||||||
<th class="node-cell location center">{{tr "Location"}}</th>
|
<th class="node-cell location center">{{tr "Location"}}</th>
|
||||||
|
<th v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
|
||||||
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
|
||||||
<th class="node-cell load center">{{tr "Load"}}</th>
|
<th class="node-cell load center">{{tr "Load"}}</th>
|
||||||
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
|
||||||
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
|
||||||
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
|
||||||
|
<th v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="servers">
|
<tbody id="servers">
|
||||||
@@ -33,11 +35,18 @@
|
|||||||
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
|
||||||
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
|
||||||
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
|
||||||
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
|
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell location">
|
<td class="node-cell location center">
|
||||||
<i :class="'fi fi-' + node.location"></i>
|
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'rb') : '')"></i>
|
||||||
<span class="node-cell-location-text text-uppercase"> @#node.location#@</span>
|
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'RB') : ''#@</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
|
||||||
|
<template v-if="node.additional && node.additional.price">
|
||||||
|
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
|
||||||
|
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
|
||||||
|
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
|
||||||
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
<td style="text-align: center;" class="node-cell load">@#node.load#@</td>
|
||||||
@@ -62,6 +71,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
|
||||||
|
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
|
||||||
|
<template v-if="node.additional && node.additional.remaining.format">
|
||||||
|
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
|
||||||
|
<div class="additional">
|
||||||
|
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
|
||||||
|
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
|
||||||
|
<small v-else>
|
||||||
|
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
|
||||||
|
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
<tr class="expandRow" :class="index % 2 === 0 ? 'odd': 'even'">
|
||||||
<td colspan="16">
|
<td colspan="16">
|
||||||
@@ -69,8 +93,8 @@
|
|||||||
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
|
||||||
<span class="node-cell-expand">
|
<span class="node-cell-expand">
|
||||||
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
|
||||||
@#node.host.Platform#@-@#node.host.PlatformVersion#@
|
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
|
||||||
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
|
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="node-cell-expand" v-if="node.host.CPU">
|
<span class="node-cell-expand" v-if="node.host.CPU">
|
||||||
<span class="node-cell-expand-label">CPU:</span>
|
<span class="node-cell-expand-label">CPU:</span>
|
||||||
|
|||||||
+425
-2
@@ -42,6 +42,7 @@
|
|||||||
servers: [],
|
servers: [],
|
||||||
nodesTag: [],
|
nodesTag: [],
|
||||||
nodesNoTag: [],
|
nodesNoTag: [],
|
||||||
|
additional: {},
|
||||||
chartDataList: [],
|
chartDataList: [],
|
||||||
ws: null,
|
ws: null,
|
||||||
language: {{.Conf.Language}},
|
language: {{.Conf.Language}},
|
||||||
@@ -71,17 +72,32 @@
|
|||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
created() {
|
created() {
|
||||||
this.servers = JSON.parse('{{.Servers}}').servers;
|
this.servers = JSON.parse('{{.Servers}}').servers;
|
||||||
|
this.additional = this.initAdditional(this.servers);
|
||||||
if(this.showGroup) {
|
if(this.showGroup) {
|
||||||
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
|
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
|
||||||
|
this.nodesTag.forEach(group => {
|
||||||
|
group.data.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.nodesNoTag = this.handleNodes(this.servers);
|
this.nodesNoTag = this.handleNodes(this.servers);
|
||||||
|
this.nodesNoTag.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.initCountryMap().then(countryMap => {
|
this.initCountryMap().then(countryMap => {
|
||||||
this.countryMap = countryMap;
|
this.countryMap = countryMap;
|
||||||
this.countryNameMap = this.initCountryNameMap();
|
this.countryNameMap = this.initCountryNameMap();
|
||||||
this.countryServer = this.initCountryServer();
|
this.countryServer = this.initCountryServer();
|
||||||
this.countryMapChartData = this.initCountryMapChartData();
|
this.countryMapChartData = this.initCountryMapChartData();
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// 初始化时建立WebSocket连接
|
// 初始化时建立WebSocket连接
|
||||||
@@ -98,6 +114,49 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
initAdditional(servers) {
|
||||||
|
let nodes = {};
|
||||||
|
servers?.forEach(server => {
|
||||||
|
if (server.PublicNote) {
|
||||||
|
const remainingFormat = this.getRemainingFormat(server.live, server.PublicNote);
|
||||||
|
const remainingDays = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote);
|
||||||
|
const remainingPercent = this.getRemainingPercent(
|
||||||
|
this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"),
|
||||||
|
this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),
|
||||||
|
server.PublicNote
|
||||||
|
);
|
||||||
|
const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
|
||||||
|
const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
|
||||||
|
|
||||||
|
// 初始化节点
|
||||||
|
nodes[server.ID] = {
|
||||||
|
"remaining": {},
|
||||||
|
"price": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (remainingFormat) {
|
||||||
|
nodes[server.ID].remaining.format = remainingFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingDays) {
|
||||||
|
nodes[server.ID].remaining.days = remainingDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingPercent) {
|
||||||
|
nodes[server.ID].remaining.percent = this.toFixed2(100 - remainingPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priceAmount) {
|
||||||
|
nodes[server.ID].price.amount = priceAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priceCycle && priceAmount) {
|
||||||
|
nodes[server.ID].price.cycle = priceCycle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
initCountryMap() {
|
initCountryMap() {
|
||||||
return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
|
return fetch(this.staticUrl + '/maps/nezha.countrymap.json')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -395,8 +454,22 @@
|
|||||||
}
|
}
|
||||||
if(this.showGroup) {
|
if(this.showGroup) {
|
||||||
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
|
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
|
||||||
|
this.nodesTag.forEach(group => {
|
||||||
|
group.data.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.nodesNoTag = this.handleNodes(data.servers);
|
this.nodesNoTag = this.handleNodes(data.servers);
|
||||||
|
this.nodesNoTag.forEach(item => {
|
||||||
|
const additionalData = this.additional[item.ID];
|
||||||
|
if (additionalData) {
|
||||||
|
item.additional = additionalData; // 添加 additional 属性
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
@@ -507,7 +580,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching data:', error);
|
console.log('Error fetching data:', error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderCharts(id, reload = false) {
|
renderCharts(id, reload = false) {
|
||||||
@@ -722,6 +795,356 @@
|
|||||||
this.renderCharts(id,true);
|
this.renderCharts(id,true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
getNoteElementValue(string, elementName, childElementName) {
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj = JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elementName) return null;
|
||||||
|
|
||||||
|
if (childElementName) {
|
||||||
|
return obj[elementName] && childElementName in obj[elementName] ? obj[elementName][childElementName] : null;
|
||||||
|
} else {
|
||||||
|
return elementName in obj ? obj[elementName] : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getBillingCycle(billingCycle) {
|
||||||
|
// 统一转换为小写进行比较
|
||||||
|
const cycle = billingCycle.toLowerCase();
|
||||||
|
|
||||||
|
switch (cycle) {
|
||||||
|
case '月':
|
||||||
|
case 'mo':
|
||||||
|
case 'month':
|
||||||
|
case 'monthly':
|
||||||
|
case 'm':
|
||||||
|
return "M";
|
||||||
|
case '季':
|
||||||
|
case 'quarterly':
|
||||||
|
case 'q':
|
||||||
|
return "Q";
|
||||||
|
case '半':
|
||||||
|
case '半年':
|
||||||
|
case 'half':
|
||||||
|
case 'semi-annually':
|
||||||
|
case 'h':
|
||||||
|
return "H";
|
||||||
|
case '年':
|
||||||
|
case 'yr':
|
||||||
|
case 'year':
|
||||||
|
case 'annually':
|
||||||
|
case 'y':
|
||||||
|
return "Y";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAdjustTimezone(reference, target) {
|
||||||
|
// 获取时区
|
||||||
|
const referenceTimezoneOffset = reference.getTimezoneOffset();
|
||||||
|
const targetTimezoneOffset = target.getTimezoneOffset();
|
||||||
|
|
||||||
|
// 计算时区差异
|
||||||
|
const timezoneDifference = (referenceTimezoneOffset - targetTimezoneOffset) * 60 * 1000;
|
||||||
|
|
||||||
|
// 将 target 日期调整到 reference 时区
|
||||||
|
return new Date(target.getTime() + timezoneDifference);
|
||||||
|
},
|
||||||
|
getAutoRenewalEndDate(endDate, billingCycle) {
|
||||||
|
const expiration = new Date(endDate);
|
||||||
|
const current = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 如果 expiration 无效,返回 null 并记录日志
|
||||||
|
if (isNaN(expiration.getTime())) {
|
||||||
|
console.log("getAutoRenewalEndDate: Invalid expiration format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
flag: 1, // 1表示需要更新
|
||||||
|
check: 0, // 判断逻辑标记
|
||||||
|
count: 0 // 周期累计计数
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果当前时间还没到到期时间,直接返回到期时间
|
||||||
|
if (current < expiration) {
|
||||||
|
result.flag = 0;
|
||||||
|
result.date = expiration;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextExpiration = new Date(expiration); // 初始化为原到期时间
|
||||||
|
let newExpirationMonth = expiration.getMonth(); // 获取expiration初始月份
|
||||||
|
|
||||||
|
switch (billingCycle.toUpperCase()) {
|
||||||
|
case 'M': // 月度
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
const monthCheck = current < new Date(current.getFullYear(), current.getMonth(), expiration.getDate());
|
||||||
|
|
||||||
|
// 检查当前月是否在有效期内
|
||||||
|
if (monthCheck) {
|
||||||
|
nextExpiration.setMonth(current.getMonth());
|
||||||
|
result.check = 1;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 1);
|
||||||
|
result.check = 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Q': // 季度
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
// 每次增加 3 个月,直到新的月份大于当前月份
|
||||||
|
while (newExpirationMonth < current.getMonth()) {
|
||||||
|
newExpirationMonth += 3;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextExpiration设置获取到的新月份
|
||||||
|
nextExpiration.setMonth(newExpirationMonth);
|
||||||
|
|
||||||
|
const quarterlyCheck = current < nextExpiration;
|
||||||
|
|
||||||
|
// 检查新月份是否在有效期内
|
||||||
|
if (quarterlyCheck) {
|
||||||
|
result.check = 3;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 3);
|
||||||
|
result.check = 4;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'H': // 半年
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
|
||||||
|
// 每次增加 6 个月,直到新的月份大于当前月份
|
||||||
|
while (newExpirationMonth < current.getMonth()) {
|
||||||
|
newExpirationMonth += 6;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextExpiration设置获取到的新月份
|
||||||
|
nextExpiration.setMonth(newExpirationMonth);
|
||||||
|
|
||||||
|
const halfCheck = current < nextExpiration;
|
||||||
|
|
||||||
|
// 检查新月份是否在有效期内
|
||||||
|
if (halfCheck) {
|
||||||
|
result.check = 5;
|
||||||
|
} else {
|
||||||
|
nextExpiration.setMonth(current.getMonth() + 6);
|
||||||
|
result.check = 6;
|
||||||
|
result.count += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Y': // 年度
|
||||||
|
const yearCheck = current < new Date(current.getFullYear(), expiration.getMonth(), expiration.getDate());
|
||||||
|
|
||||||
|
// 如果当前时间比这一年有效期早,则到期为本年
|
||||||
|
if (yearCheck) {
|
||||||
|
nextExpiration.setFullYear(current.getFullYear());
|
||||||
|
result.check = 7;
|
||||||
|
} else {
|
||||||
|
// 否则推到下一年
|
||||||
|
nextExpiration.setFullYear(current.getFullYear() + 1);
|
||||||
|
result.check = 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid billing cycle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保持原到期时间的时分秒
|
||||||
|
nextExpiration.setHours(expiration.getHours());
|
||||||
|
nextExpiration.setMinutes(expiration.getMinutes());
|
||||||
|
nextExpiration.setSeconds(expiration.getSeconds());
|
||||||
|
|
||||||
|
result.date = nextExpiration;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
getAutoRenewalStartDate(flag, startDate, check, count) {
|
||||||
|
//1.判断什么时候改变 2.如何改变
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const current = this.getAdjustTimezone(start, new Date());
|
||||||
|
|
||||||
|
// 检查 startDate 格式是否有效,若无效返回 null
|
||||||
|
if (isNaN(start.getTime())) {
|
||||||
|
console.log("getAutoRenewalStartDate: Invalid startDate format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 flag 为 0,直接返回开始日期
|
||||||
|
if (flag === 0) {
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化新的开始日期
|
||||||
|
const newStart = new Date(start);
|
||||||
|
|
||||||
|
switch (check) {
|
||||||
|
case 1: // 处理月份:设置为上个月
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(current.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // 处理月份:设置为当前月
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(current.getMonth());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
case 4: // 处理季度
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(start.getMonth() + 3 * count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
case 6: // 处理半年
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
newStart.setMonth(start.getMonth() + 6 * count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7: // 处理年份:设置为上一年
|
||||||
|
newStart.setFullYear(current.getFullYear() - 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8: // 处理年份:保持当前年份
|
||||||
|
newStart.setFullYear(current.getFullYear());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 默认处理:直接返回
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newStart;
|
||||||
|
},
|
||||||
|
getRemainingFormat(online, note) {
|
||||||
|
if (!note) return null;
|
||||||
|
const startDate = this.getNoteElementValue(note, "billingDataMod", "startDate");
|
||||||
|
const endDate = this.getNoteElementValue(note, "billingDataMod", "endDate");
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否有效
|
||||||
|
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
|
||||||
|
return null; // 如果无效,返回 null 或其他错误处理逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
|
||||||
|
return this.formatPercents(online, this.toFixed2(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
|
||||||
|
return {
|
||||||
|
"class": "",
|
||||||
|
"style": "width: 0%",
|
||||||
|
"percent": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//处理特殊时间格式
|
||||||
|
const percent = this.getRemainingPercent(startDate, endDate, note);
|
||||||
|
return this.formatPercents(online, this.toFixed2(percent));
|
||||||
|
|
||||||
|
},
|
||||||
|
getRemainingDays(endDate, note) {
|
||||||
|
// 检查 endDate 是否有效
|
||||||
|
if (!endDate || typeof endDate !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (endDate.includes("0000-00-00")) {
|
||||||
|
return "lifetime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(endDate).getTime())) {
|
||||||
|
return "NaN";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间,并调整时区
|
||||||
|
const currentTime = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 获取计费周期和自动续订日期
|
||||||
|
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
|
||||||
|
const autoEndDate = this.getAutoRenewalEndDate(
|
||||||
|
this.getNoteElementValue(note, "billingDataMod", "endDate"),
|
||||||
|
this.getBillingCycle(billingCycle)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查 autoRenewal 状态
|
||||||
|
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
|
||||||
|
|
||||||
|
// 确定到期时间
|
||||||
|
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
|
||||||
|
|
||||||
|
// 计算剩余天数
|
||||||
|
const timeDiff = end - currentTime;
|
||||||
|
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
return daysDiff;
|
||||||
|
},
|
||||||
|
getRemainingPercent(startDate, endDate, note) {
|
||||||
|
// 检查 startDate 和 endDate 是否为有效字符串并处理特殊格式
|
||||||
|
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊时间格式
|
||||||
|
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 startDate 和 endDate 是否为合法的Date
|
||||||
|
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
|
||||||
|
return "NaN";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间并调整时区
|
||||||
|
const now = this.getAdjustTimezone(new Date(endDate), new Date());
|
||||||
|
|
||||||
|
// 获取计费周期
|
||||||
|
const billingCycle = this.getNoteElementValue(note, "billingDataMod", "cycle") || "月";
|
||||||
|
|
||||||
|
// 自动获取结束日期
|
||||||
|
const autoEndDate = this.getAutoRenewalEndDate(
|
||||||
|
this.getNoteElementValue(note, "billingDataMod", "endDate"),
|
||||||
|
this.getBillingCycle(billingCycle)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自动获取开始日期
|
||||||
|
const autoStartDate = autoEndDate.flag == 1
|
||||||
|
? this.getAutoRenewalStartDate(autoEndDate.flag, this.getNoteElementValue(note, "billingDataMod", "startDate"), autoEndDate.check, autoEndDate.count)
|
||||||
|
: new Date(startDate);
|
||||||
|
|
||||||
|
// 计算开始和结束时间
|
||||||
|
const autoRenewal = this.getNoteElementValue(note, "billingDataMod", "autoRenewal") == 1;
|
||||||
|
const start = autoRenewal ? autoStartDate : new Date(startDate);
|
||||||
|
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
|
||||||
|
|
||||||
|
// 计算剩余百分比
|
||||||
|
if (now < start) {
|
||||||
|
return 0.01; //避免当条件判断!percent被过滤
|
||||||
|
}
|
||||||
|
if (now >= end) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDuration = end - start;
|
||||||
|
const elapsedDuration = now - start;
|
||||||
|
const percent = (elapsedDuration / totalDuration) * 100;
|
||||||
|
|
||||||
|
// 确保百分比在 0-100 之间
|
||||||
|
return Math.min(Math.max(percent, 0), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-14
@@ -15,20 +15,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav id="navbar" class="navbar-collapse collapse">
|
<nav id="navbar" class="navbar-collapse collapse">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
|
<li :class="isMenuActive('index')"><a href="/"><i class="home icon"></i>{{tr "Home" }}</a></li>
|
||||||
<template v-if="isMobile">
|
<li :class="isMenuActive('service')"><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
||||||
<li class="dropdown">
|
<li :class="isMenuActive('network')"><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
||||||
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
|
|
||||||
<ul class="dropdown-menu" style="min-width:100px;">
|
|
||||||
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
|
||||||
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
|
|
||||||
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
|
|
||||||
</template>
|
|
||||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
|
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
|
||||||
|
|||||||
@@ -12,22 +12,3 @@ site:
|
|||||||
brand: "nz_site_title"
|
brand: "nz_site_title"
|
||||||
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
||||||
theme: "default"
|
theme: "default"
|
||||||
ddns:
|
|
||||||
enable: false
|
|
||||||
provider: "webhook" # 如需使用多配置功能,请把此项留空
|
|
||||||
accessid: ""
|
|
||||||
accesssecret: ""
|
|
||||||
webhookmethod: ""
|
|
||||||
webhookurl: ""
|
|
||||||
webhookrequestbody: ""
|
|
||||||
webhookheaders: ""
|
|
||||||
maxretries: 3
|
|
||||||
profiles:
|
|
||||||
example:
|
|
||||||
provider: ""
|
|
||||||
accessid: ""
|
|
||||||
accesssecret: ""
|
|
||||||
webhookmethod: ""
|
|
||||||
webhookurl: ""
|
|
||||||
webhookrequestbody: ""
|
|
||||||
webhookheaders: ""
|
|
||||||
+13
-9
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
|
|||||||
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.19.0"
|
NZ_VERSION="v0.19.2"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
@@ -65,6 +65,8 @@ geo_check() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre_check() {
|
pre_check() {
|
||||||
|
umask 077
|
||||||
|
|
||||||
## os_arch
|
## os_arch
|
||||||
if uname -m | grep -q 'x86_64'; then
|
if uname -m | grep -q 'x86_64'; then
|
||||||
os_arch="amd64"
|
os_arch="amd64"
|
||||||
@@ -288,8 +290,6 @@ install_dashboard() {
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo chmod -R 700 $NZ_DASHBOARD_PATH
|
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
install_dashboard_docker
|
install_dashboard_docker
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
@@ -375,7 +375,6 @@ install_agent() {
|
|||||||
|
|
||||||
# 哪吒监控文件夹
|
# 哪吒监控文件夹
|
||||||
sudo mkdir -p $NZ_AGENT_PATH
|
sudo mkdir -p $NZ_AGENT_PATH
|
||||||
sudo chmod -R 700 $NZ_AGENT_PATH
|
|
||||||
|
|
||||||
echo "正在下载监控端"
|
echo "正在下载监控端"
|
||||||
if [ -z "$CN" ]; then
|
if [ -z "$CN" ]; then
|
||||||
@@ -454,11 +453,16 @@ modify_dashboard_config() {
|
|||||||
echo "> 修改面板配置"
|
echo "> 修改面板配置"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
echo "正在下载 Docker 脚本"
|
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
echo "正在下载 Docker 脚本"
|
||||||
if [ $? != 0 ]; then
|
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
||||||
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
if [ $? != 0 ]; then
|
||||||
return 0
|
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
|
||||||
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+13
-9
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
|
|||||||
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
|
||||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||||
NZ_VERSION="v0.19.0"
|
NZ_VERSION="v0.19.2"
|
||||||
|
|
||||||
red='\033[0;31m'
|
red='\033[0;31m'
|
||||||
green='\033[0;32m'
|
green='\033[0;32m'
|
||||||
@@ -65,6 +65,8 @@ geo_check() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre_check() {
|
pre_check() {
|
||||||
|
umask 077
|
||||||
|
|
||||||
## os_arch
|
## os_arch
|
||||||
if uname -m | grep -q 'x86_64'; then
|
if uname -m | grep -q 'x86_64'; then
|
||||||
os_arch="amd64"
|
os_arch="amd64"
|
||||||
@@ -287,8 +289,6 @@ install_dashboard() {
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo chmod -R 700 $NZ_DASHBOARD_PATH
|
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
install_dashboard_docker
|
install_dashboard_docker
|
||||||
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
|
||||||
@@ -374,7 +374,6 @@ install_agent() {
|
|||||||
|
|
||||||
# Nezha Monitoring Folder
|
# Nezha Monitoring Folder
|
||||||
sudo mkdir -p $NZ_AGENT_PATH
|
sudo mkdir -p $NZ_AGENT_PATH
|
||||||
sudo chmod -R 700 $NZ_AGENT_PATH
|
|
||||||
|
|
||||||
echo "Downloading Agent"
|
echo "Downloading Agent"
|
||||||
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
|
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
|
||||||
@@ -448,11 +447,16 @@ modify_dashboard_config() {
|
|||||||
echo "> Modify Dashboard Configuration"
|
echo "> Modify Dashboard Configuration"
|
||||||
|
|
||||||
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
|
||||||
echo "Download Docker Script"
|
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then
|
||||||
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
echo "Download Docker Script"
|
||||||
if [ $? != 0 ]; then
|
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
|
||||||
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
if [ $? != 0 ]; then
|
||||||
return 0
|
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
|
||||||
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+9
-24
@@ -125,7 +125,6 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re
|
|||||||
|
|
||||||
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
|
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
|
||||||
var clientID uint64
|
var clientID uint64
|
||||||
var provider ddns.Provider
|
|
||||||
var err error
|
var err error
|
||||||
if clientID, err = s.Auth.Check(c); err != nil {
|
if clientID, err = s.Auth.Check(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -135,33 +134,19 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
defer singleton.ServerLock.RUnlock()
|
defer singleton.ServerLock.RUnlock()
|
||||||
|
|
||||||
// 检查并更新DDNS
|
// 检查并更新DDNS
|
||||||
if singleton.Conf.DDNS.Enable &&
|
if singleton.ServerList[clientID].EnableDDNS && host.IP != "" &&
|
||||||
singleton.ServerList[clientID].EnableDDNS &&
|
|
||||||
host.IP != "" &&
|
|
||||||
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
|
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
|
||||||
serverDomain := singleton.ServerList[clientID].DDNSDomain
|
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
||||||
if singleton.Conf.DDNS.Provider == "" {
|
providers, err := singleton.GetDDNSProvidersFromProfiles(singleton.ServerList[clientID].DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
|
||||||
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile)
|
if err == nil {
|
||||||
} else {
|
for _, provider := range providers {
|
||||||
provider, err = singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
|
go func(provider *ddns.Provider) {
|
||||||
}
|
provider.UpdateDomain(context.Background())
|
||||||
if err == nil && serverDomain != "" {
|
}(provider)
|
||||||
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
|
||||||
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
|
||||||
config := &ddns.DomainConfig{
|
|
||||||
EnableIPv4: singleton.ServerList[clientID].EnableIPv4,
|
|
||||||
EnableIpv6: singleton.ServerList[clientID].EnableIpv6,
|
|
||||||
FullDomain: serverDomain,
|
|
||||||
Ipv4Addr: ipv4,
|
|
||||||
Ipv6Addr: ipv6,
|
|
||||||
}
|
}
|
||||||
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
|
log.Printf("NEZHA>> 获取DDNS配置时发生错误: %v", err)
|
||||||
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送IP变动通知
|
// 发送IP变动通知
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type CommonServerInfo struct {
|
|||||||
IPV6 string `json:"ipv6"`
|
IPV6 string `json:"ipv6"`
|
||||||
ValidIP string `json:"valid_ip"`
|
ValidIP string `json:"valid_ip"`
|
||||||
DisplayIndex int `json:"display_index"`
|
DisplayIndex int `json:"display_index"`
|
||||||
|
HideForGuest bool `json:"hide_for_guest"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
|
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
|
||||||
@@ -150,6 +151,7 @@ func (s *ServerAPIService) GetAllStatus() *ServerStatusResponse {
|
|||||||
IPV6: ipv6,
|
IPV6: ipv6,
|
||||||
ValidIP: validIP,
|
ValidIP: validIP,
|
||||||
DisplayIndex: v.DisplayIndex,
|
DisplayIndex: v.DisplayIndex,
|
||||||
|
HideForGuest: v.HideForGuest,
|
||||||
}
|
}
|
||||||
res.Result = append(res.Result, &StatusResponse{
|
res.Result = append(res.Result, &StatusResponse{
|
||||||
CommonServerInfo: info,
|
CommonServerInfo: info,
|
||||||
|
|||||||
+50
-55
@@ -2,73 +2,68 @@ package singleton
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"sync"
|
||||||
"slices"
|
|
||||||
|
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
"github.com/libdns/tencentcloud"
|
||||||
|
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns/dummy"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
ProviderWebHook = "webhook"
|
ddnsCache map[uint64]*model.DDNSProfile
|
||||||
ProviderCloudflare = "cloudflare"
|
ddnsCacheLock sync.RWMutex
|
||||||
ProviderTencentCloud = "tencentcloud"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider
|
func initDDNS() {
|
||||||
|
OnDDNSUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) {
|
func OnDDNSUpdate() {
|
||||||
if domainConfig == nil {
|
var ddns []*model.DDNSProfile
|
||||||
return
|
DB.Find(&ddns)
|
||||||
|
ddnsCacheLock.Lock()
|
||||||
|
defer ddnsCacheLock.Unlock()
|
||||||
|
ddnsCache = make(map[uint64]*model.DDNSProfile)
|
||||||
|
for i := 0; i < len(ddns); i++ {
|
||||||
|
ddnsCache[ddns[i].ID] = ddns[i]
|
||||||
}
|
}
|
||||||
for retries := 0; retries < maxRetries; retries++ {
|
}
|
||||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", domainConfig.FullDomain, retries+1, maxRetries)
|
|
||||||
if err := provider.UpdateDomain(domainConfig); err != nil {
|
func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
|
||||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err)
|
profiles := make([]*model.DDNSProfile, 0, len(profileId))
|
||||||
|
ddnsCacheLock.RLock()
|
||||||
|
for _, id := range profileId {
|
||||||
|
if profile, ok := ddnsCache[id]; ok {
|
||||||
|
profiles = append(profiles, profile)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain)
|
return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
ddnsCacheLock.RUnlock()
|
||||||
|
|
||||||
// Deprecated
|
providers := make([]*ddns2.Provider, 0, len(profiles))
|
||||||
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
for _, profile := range profiles {
|
||||||
switch provider {
|
provider := &ddns2.Provider{DDNSProfile: profile, IPAddrs: ip}
|
||||||
case ProviderWebHook:
|
switch profile.Provider {
|
||||||
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil
|
case model.ProviderDummy:
|
||||||
case ProviderCloudflare:
|
provider.Setter = &dummy.Provider{}
|
||||||
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil
|
providers = append(providers, provider)
|
||||||
case ProviderTencentCloud:
|
case model.ProviderWebHook:
|
||||||
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil
|
provider.Setter = &webhook.Provider{DDNSProfile: profile}
|
||||||
default:
|
providers = append(providers, provider)
|
||||||
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider)
|
case model.ProviderCloudflare:
|
||||||
}
|
provider.Setter = &cloudflare.Provider{APIToken: profile.AccessSecret}
|
||||||
}
|
providers = append(providers, provider)
|
||||||
|
case model.ProviderTencentCloud:
|
||||||
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
|
provider.Setter = &tencentcloud.Provider{SecretId: profile.AccessID, SecretKey: profile.AccessSecret}
|
||||||
profile, ok := Conf.DDNS.Profiles[profileName]
|
providers = append(providers, provider)
|
||||||
if !ok {
|
default:
|
||||||
return new(ddns2.ProviderDummy), fmt.Errorf("未找到配置项 %s", profileName)
|
return nil, fmt.Errorf("无法找到配置的DDNS提供者ID %d", profile.Provider)
|
||||||
}
|
|
||||||
|
|
||||||
switch profile.Provider {
|
|
||||||
case ProviderWebHook:
|
|
||||||
return ddns2.NewProviderWebHook(profile.WebhookURL, profile.WebhookMethod, profile.WebhookRequestBody, profile.WebhookHeaders), nil
|
|
||||||
case ProviderCloudflare:
|
|
||||||
return ddns2.NewProviderCloudflare(profile.AccessSecret), nil
|
|
||||||
case ProviderTencentCloud:
|
|
||||||
return ddns2.NewProviderTencentCloud(profile.AccessID, profile.AccessSecret), nil
|
|
||||||
default:
|
|
||||||
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", profile.Provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateDDNSProvidersFromProfiles() error {
|
|
||||||
validProviders := []string{ProviderWebHook, ProviderCloudflare, ProviderTencentCloud}
|
|
||||||
for _, profile := range Conf.DDNS.Profiles {
|
|
||||||
if ok := slices.Contains(validProviders, profile.Provider); !ok {
|
|
||||||
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return providers, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package singleton
|
package singleton
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,6 +38,7 @@ func LoadSingleton() {
|
|||||||
loadCronTasks() // 加载定时任务
|
loadCronTasks() // 加载定时任务
|
||||||
loadAPI()
|
loadAPI()
|
||||||
initNAT()
|
initNAT()
|
||||||
|
initDDNS()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitConfigFromPath 从给出的文件路径中加载配置
|
// InitConfigFromPath 从给出的文件路径中加载配置
|
||||||
@@ -48,25 +48,6 @@ func InitConfigFromPath(path string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
validateConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateConfig 验证配置文件有效性
|
|
||||||
func validateConfig() {
|
|
||||||
var err error
|
|
||||||
if Conf.DDNS.Provider == "" {
|
|
||||||
err = ValidateDDNSProvidersFromProfiles()
|
|
||||||
} else {
|
|
||||||
_, err = GetDDNSProviderFromString(Conf.DDNS.Provider)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if Conf.DDNS.Enable {
|
|
||||||
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
|
|
||||||
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitDBFromPath 从给出的文件路径中加载数据库
|
// InitDBFromPath 从给出的文件路径中加载数据库
|
||||||
@@ -84,7 +65,7 @@ func InitDBFromPath(path string) {
|
|||||||
err = DB.AutoMigrate(model.Server{}, model.User{},
|
err = DB.AutoMigrate(model.Server{}, model.User{},
|
||||||
model.Notification{}, model.AlertRule{}, model.Monitor{},
|
model.Notification{}, model.AlertRule{}, model.Monitor{},
|
||||||
model.MonitorHistory{}, model.Cron{}, model.Transfer{},
|
model.MonitorHistory{}, model.Cron{}, model.Transfer{},
|
||||||
model.ApiToken{}, model.NAT{})
|
model.ApiToken{}, model.NAT{}, model.DDNSProfile{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user