feat: maintenance mode #739

This commit is contained in:
Jacky 2025-04-07 18:38:04 +08:00
parent 191ddea309
commit 5d3f478086
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
33 changed files with 3698 additions and 2222 deletions

78
api/pages/maintenance.go Normal file
View file

@ -0,0 +1,78 @@
package pages
import (
"embed"
"html/template"
"net/http"
"strings"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
)
//go:embed *.tmpl
var tmplFS embed.FS
// MaintenancePageData maintenance page data structure
type MaintenancePageData struct {
Title string `json:"title"`
Message string `json:"message"`
Description string `json:"description"`
ICPNumber string `json:"icp_number"`
PublicSecurityNumber string `json:"public_security_number"`
}
const (
Title = "System Maintenance"
Message = "We are currently performing system maintenance to improve your experience."
Description = "Please check back later. Thank you for your understanding and patience."
)
// MaintenancePage returns a maintenance page
func MaintenancePage(c *gin.Context) {
// Prepare template data
data := MaintenancePageData{
Title: Title,
Message: Message,
Description: Description,
ICPNumber: settings.NodeSettings.ICPNumber,
PublicSecurityNumber: settings.NodeSettings.PublicSecurityNumber,
}
// Check User-Agent
userAgent := c.GetHeader("User-Agent")
isBrowser := len(userAgent) > 0 && (contains(userAgent, "Mozilla") ||
contains(userAgent, "Chrome") ||
contains(userAgent, "Safari") ||
contains(userAgent, "Edge") ||
contains(userAgent, "Firefox") ||
contains(userAgent, "Opera"))
if !isBrowser {
c.JSON(http.StatusServiceUnavailable, data)
return
}
// Parse template
tmpl, err := template.ParseFS(tmplFS, "maintenance.tmpl")
if err != nil {
c.String(http.StatusInternalServerError, "503 Service Unavailable")
return
}
// Set content type
c.Header("Content-Type", "text/html; charset=utf-8")
c.Status(http.StatusServiceUnavailable)
// Render template
err = tmpl.Execute(c.Writer, data)
if err != nil {
c.String(http.StatusInternalServerError, "503 Service Unavailable")
return
}
}
// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}

View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<title>{{.Title}} | Nginx UI</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f4f5f7;
color: #333;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
flex-direction: column;
}
.maintenance-container {
max-width: 600px;
padding: 40px;
background: white;
border-radius: 8px;
@media (max-width: 768px) {
border-radius: 0;
}
box-shadow: 0 0 30px #c8c8c840;
margin-bottom: 20px;
}
h1 {
color:rgb(0, 128, 247);
margin-bottom: 20px;
font-size: 28px;
}
a {
color: #1890ff;
text-decoration: none;
}
p {
font-size: 14px;
line-height: 1.6;
margin-bottom: 20px;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
.footer {
margin-top: 30px;
font-size: 12px;
color: #888;
}
.beian-info {
font-size: 12px;
color: #888;
margin-top: 10px;
}
.beian-info p {
margin: 5px 0;
}
.beian-info img {
vertical-align: middle;
margin-right: 5px;
}
</style>
</head>
<body>
<div>
<div class="maintenance-container">
<div class="icon">🛠️</div>
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
<p>{{.Description}}</p>
<div class="footer">
<p>Powered by <a href="https://nginxui.com" target="_blank">Nginx UI</a></p>
</div>
</div>
<div class="beian-info">
{{if .ICPNumber}}
<p><a href="https://beian.miit.gov.cn/" target="_blank">{{.ICPNumber}}</a></p>
{{end}}
{{if .PublicSecurityNumber}}
<p><img src="//www.beian.gov.cn/img/new/gongan.png" alt="公安备案"><a href="http://www.beian.gov.cn/portal/index" target="_blank">{{.PublicSecurityNumber}}</a></p>
{{end}}
</div>
</div>
</body>
</html>

11
api/pages/router.go Normal file
View file

@ -0,0 +1,11 @@
package pages
import (
"github.com/gin-gonic/gin"
)
// InitRouter initializes the pages routes
func InitRouter(r *gin.Engine) {
// Register maintenance page route
r.GET("/pages/maintenance", MaintenancePage)
}

View file

@ -19,7 +19,7 @@ import (
func GetSiteList(c *gin.Context) {
name := c.Query("name")
enabled := c.Query("enabled")
status := c.Query("status")
orderBy := c.Query("sort_by")
sort := c.DefaultQuery("order", "desc")
queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
@ -50,9 +50,23 @@ func GetSiteList(c *gin.Context) {
return filepath.Base(item.Path), item
})
enabledConfigMap := make(map[string]bool)
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
configStatusMap := make(map[string]config.ConfigStatus)
for _, site := range configFiles {
configStatusMap[site.Name()] = config.StatusDisabled
}
// Check for enabled sites and maintenance mode sites
for _, enabledSite := range enabledConfig {
name := enabledSite.Name()
// Check if this is a maintenance mode configuration
if strings.HasSuffix(name, site.MaintenanceSuffix) {
// Extract the original site name by removing maintenance suffix
originalName := strings.TrimSuffix(name, site.MaintenanceSuffix)
configStatusMap[originalName] = config.StatusMaintenance
} else {
configStatusMap[name] = config.StatusEnabled
}
}
var configs []config.Config
@ -68,14 +82,10 @@ func GetSiteList(c *gin.Context) {
continue
}
// status filter
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
if status != "" && configStatusMap[file.Name()] != config.ConfigStatus(status) {
continue
}
var (
envGroupId uint64
envGroup *model.EnvGroup
@ -98,7 +108,7 @@ func GetSiteList(c *gin.Context) {
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
Status: configStatusMap[file.Name()],
EnvGroupID: envGroupId,
EnvGroup: envGroup,
Urls: indexedSite.Urls,

View file

@ -22,4 +22,8 @@ func InitRouter(r *gin.RouterGroup) {
r.DELETE("sites/:name", DeleteSite)
// duplicate site
r.POST("sites/:name/duplicate", DuplicateSite)
// enable maintenance mode for site
r.POST("sites/:name/maintenance/enable", EnableMaintenanceSite)
// disable maintenance mode for site
r.POST("sites/:name/maintenance/disable", DisableMaintenanceSite)
}

View file

@ -215,3 +215,27 @@ func BatchUpdateSites(c *gin.Context) {
ctx.BatchEffectedIDs = effectedPath
}).BatchModify()
}
func EnableMaintenanceSite(c *gin.Context) {
err := site.EnableMaintenance(c.Param("name"))
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}
func DisableMaintenanceSite(c *gin.Context) {
err := site.DisableMaintenance(c.Param("name"))
if err != nil {
cosy.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
}

View file

@ -36,7 +36,7 @@ type Stream struct {
func GetStreams(c *gin.Context) {
name := c.Query("name")
enabled := c.Query("enabled")
status := c.Query("status")
orderBy := c.Query("order_by")
sort := c.DefaultQuery("sort", "desc")
queryEnvGroupId := cast.ToUint64(c.Query("env_group_id"))
@ -53,9 +53,12 @@ func GetStreams(c *gin.Context) {
return
}
enabledConfigMap := make(map[string]bool)
enabledConfigMap := make(map[string]config.ConfigStatus)
for _, file := range configFiles {
enabledConfigMap[file.Name()] = config.StatusDisabled
}
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
enabledConfigMap[enabledConfig[i].Name()] = config.StatusEnabled
}
var configs []config.Config
@ -107,13 +110,8 @@ func GetStreams(c *gin.Context) {
}
// Apply enabled status filter if specified
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
if status != "" && enabledConfigMap[file.Name()] != config.ConfigStatus(status) {
continue
}
var (
@ -138,7 +136,7 @@ func GetStreams(c *gin.Context) {
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
Status: enabledConfigMap[file.Name()],
EnvGroupID: envGroupId,
EnvGroup: envGroup,
})

View file

@ -23,6 +23,7 @@ export interface Site extends ModelBase {
env_group?: EnvGroup
sync_node_ids: number[]
urls?: string[]
status: string
}
export interface AutoCertRequest {
@ -65,6 +66,14 @@ class SiteCurd extends Curd<Site> {
advance_mode(name: string, data: { advanced: boolean }) {
return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/advance`, data)
}
enableMaintenance(name: string) {
return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/maintenance/enable`)
}
disableMaintenance(name: string) {
return http.post(`${this.baseUrl}/${encodeURIComponent(name)}/maintenance/disable`)
}
}
const site = new SiteCurd('/sites')

View file

@ -4,12 +4,6 @@
const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {
// user module notifications
'All Recovery Codes Have Been Used': {
title: () => $gettext('All Recovery Codes Have Been Used'),
content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
},
// cluster module notifications
'Reload Remote Nginx Error': {
title: () => $gettext('Reload Remote Nginx Error'),
@ -81,6 +75,22 @@ const notifications: Record<string, { title: () => string, content: (args: any)
title: () => $gettext('Enable Remote Site Success'),
content: (args: any) => $gettext('Enable site %{name} on %{node} successfully', args),
},
'Enable Remote Site Maintenance Error': {
title: () => $gettext('Enable Remote Site Maintenance Error'),
content: (args: any) => $gettext('Enable site %{name} maintenance on %{node} failed', args),
},
'Enable Remote Site Maintenance Success': {
title: () => $gettext('Enable Remote Site Maintenance Success'),
content: (args: any) => $gettext('Enable site %{name} maintenance on %{node} successfully', args),
},
'Disable Remote Site Maintenance Error': {
title: () => $gettext('Disable Remote Site Maintenance Error'),
content: (args: any) => $gettext('Disable site %{name} maintenance on %{node} failed', args),
},
'Disable Remote Site Maintenance Success': {
title: () => $gettext('Disable Remote Site Maintenance Success'),
content: (args: any) => $gettext('Disable site %{name} maintenance on %{node} successfully', args),
},
'Rename Remote Site Error': {
title: () => $gettext('Rename Remote Site Error'),
content: (args: any) => $gettext('Rename site %{name} to %{new_name} on %{node} failed', args),
@ -139,6 +149,12 @@ const notifications: Record<string, { title: () => string, content: (args: any)
title: () => $gettext('Save Remote Stream Success'),
content: (args: any) => $gettext('Save stream %{name} to %{node} successfully', args),
},
// user module notifications
'All Recovery Codes Have Been Used': {
title: () => $gettext('All Recovery Codes Have Been Used'),
content: (args: any) => $gettext('Please generate new recovery codes in the preferences immediately to prevent lockout.', args),
},
}
export default notifications

View file

@ -5,4 +5,5 @@ export default {
50004: () => $gettext('Certificate parse error'),
50005: () => $gettext('Payload resource is nil'),
50006: () => $gettext('Path: {0} is not under the nginx conf dir: {1}'),
50007: () => $gettext('Certificate path is empty'),
}

View file

@ -1,5 +1,11 @@
export const DATE_FORMAT = 'YYYY-MM-DD'
export enum ConfigStatus {
Enabled = 'enabled',
Disabled = 'disabled',
Maintenance = 'maintenance',
}
export enum AutoCertState {
Disable = 0,
Enable = 1,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ import site from '@/api/site'
import EnvGroupTabs from '@/components/EnvGroupTabs/EnvGroupTabs.vue'
import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
import { ConfigStatus } from '@/constants'
import InspectConfig from '@/views/config/InspectConfig.vue'
import columns from '@/views/site/site_list/columns'
import SiteDuplicate from '@/views/site/site_list/SiteDuplicate.vue'
@ -94,8 +95,6 @@ function enable(name: string) {
message.success($gettext('Enabled successfully'))
table.value?.get_list()
inspect_config.value?.test()
}).catch(r => {
message.error($gettext('Failed to enable %{msg}', { msg: r.message ?? '' }), 10)
})
}
@ -104,8 +103,22 @@ function disable(name: string) {
message.success($gettext('Disabled successfully'))
table.value?.get_list()
inspect_config.value?.test()
}).catch(r => {
message.error($gettext('Failed to disable %{msg}', { msg: r.message ?? '' }))
})
}
function enableMaintenance(name: string) {
site.enableMaintenance(name).then(() => {
message.success($gettext('Maintenance mode enabled successfully'))
table.value?.get_list()
inspect_config.value?.test()
})
}
function disableMaintenance(name: string) {
site.disableMaintenance(name).then(() => {
message.success($gettext('Maintenance mode disabled successfully'))
table.value?.get_list()
inspect_config.value?.test()
})
}
@ -172,7 +185,7 @@ function handleBatchUpdated() {
>
<template #actions="{ record }">
<AButton
v-if="record.enabled"
v-if="record.status !== ConfigStatus.Disabled"
type="link"
size="small"
@click="disable(record.name)"
@ -180,13 +193,29 @@ function handleBatchUpdated() {
{{ $gettext('Disable') }}
</AButton>
<AButton
v-else
v-else-if="record.status !== ConfigStatus.Enabled"
type="link"
size="small"
@click="enable(record.name)"
>
{{ $gettext('Enable') }}
</AButton>
<AButton
v-if="record.status === ConfigStatus.Maintenance"
type="link"
size="small"
@click="disableMaintenance(record.name)"
>
{{ $gettext('Exit Maintenance') }}
</AButton>
<AButton
v-else-if="record.status !== ConfigStatus.Maintenance"
type="link"
size="small"
@click="enableMaintenance(record.name)"
>
{{ $gettext('Enter Maintenance') }}
</AButton>
<AButton
type="link"
size="small"

View file

@ -8,6 +8,7 @@ import {
datetime,
} from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { input, select, selector } from '@/components/StdDesign/StdDataEntry'
import { ConfigStatus } from '@/constants'
import envGroupColumns from '@/views/environments/group/columns'
import { Badge, Tag } from 'ant-design-vue'
@ -64,26 +65,31 @@ const columns: Column[] = [{
width: 100,
}, {
title: () => $gettext('Status'),
dataIndex: 'enabled',
dataIndex: 'status',
customRender: (args: CustomRender) => {
const template: JSXElements = []
const { text } = args
if (text === true || text > 0) {
if (text === ConfigStatus.Enabled) {
template.push(<Badge status="success" />)
template.push($gettext('Enabled'))
}
else {
else if (text === ConfigStatus.Disabled) {
template.push(<Badge status="warning" />)
template.push($gettext('Disabled'))
}
else if (text === ConfigStatus.Maintenance) {
template.push(<Badge color="volcano" />)
template.push($gettext('Maintenance'))
}
return h('div', template)
},
search: {
type: select,
mask: {
true: $gettext('Enabled'),
false: $gettext('Disabled'),
[ConfigStatus.Enabled]: $gettext('Enabled'),
[ConfigStatus.Disabled]: $gettext('Disabled'),
[ConfigStatus.Maintenance]: $gettext('Maintenance'),
},
},
sorter: true,

View file

@ -10,6 +10,7 @@ import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
import { actualValueRender, datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { input, selector } from '@/components/StdDesign/StdDataEntry'
import { ConfigStatus } from '@/constants'
import InspectConfig from '@/views/config/InspectConfig.vue'
import envGroupColumns from '@/views/environments/group/columns'
import StreamDuplicate from '@/views/stream/components/StreamDuplicate.vue'
@ -44,15 +45,15 @@ const columns: Column[] = [{
width: 150,
}, {
title: () => $gettext('Status'),
dataIndex: 'enabled',
dataIndex: 'status',
customRender: (args: CustomRender) => {
const template: JSXElements = []
const { text } = args
if (text === true || text > 0) {
if (text === ConfigStatus.Enabled) {
template.push(<Badge status="success" />)
template.push($gettext('Enabled'))
}
else {
else if (text === ConfigStatus.Disabled) {
template.push(<Badge status="warning" />)
template.push($gettext('Disabled'))
}

View file

@ -7,6 +7,14 @@ import (
"github.com/sashabaranov/go-openai"
)
type ConfigStatus string
const (
StatusEnabled ConfigStatus = "enabled"
StatusDisabled ConfigStatus = "disabled"
StatusMaintenance ConfigStatus = "maintenance"
)
type Config struct {
Name string `json:"name"`
Content string `json:"content"`
@ -17,7 +25,7 @@ type Config struct {
IsDir bool `json:"is_dir"`
EnvGroupID uint64 `json:"env_group_id"`
EnvGroup *model.EnvGroup `json:"env_group,omitempty"`
Enabled bool `json:"enabled"`
Status ConfigStatus `json:"status"`
Dir string `json:"dir"`
Urls []string `json:"urls,omitempty"`
}

View file

@ -31,8 +31,8 @@ func (c ConfigsSort) Less(i, j int) bool {
flag = c.ConfigList[i].ModifiedAt.After(c.ConfigList[j].ModifiedAt)
case "is_dir":
flag = boolToInt(c.ConfigList[i].IsDir) > boolToInt(c.ConfigList[j].IsDir)
case "enabled":
flag = boolToInt(c.ConfigList[i].Enabled) > boolToInt(c.ConfigList[j].Enabled)
case "status":
flag = c.ConfigList[i].Status > c.ConfigList[j].Status
case "env_group_id":
flag = c.ConfigList[i].EnvGroupID > c.ConfigList[j].EnvGroupID
}

View file

@ -0,0 +1,371 @@
package site
import (
"fmt"
"net/http"
"os"
"runtime"
"strings"
"sync"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/internal/notification"
"github.com/0xJacky/Nginx-UI/model"
"github.com/go-resty/resty/v2"
"github.com/tufanbarisyildirim/gonginx/config"
"github.com/tufanbarisyildirim/gonginx/parser"
"github.com/uozi-tech/cosy/logger"
"github.com/uozi-tech/cosy/settings"
)
const MaintenanceSuffix = "_nginx_ui_maintenance"
// EnableMaintenance enables maintenance mode for a site
func EnableMaintenance(name string) (err error) {
// Check if the site exists in sites-available
configFilePath := nginx.GetConfPath("sites-available", name)
_, err = os.Stat(configFilePath)
if err != nil {
return
}
// Path for the maintenance configuration file
maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
// Path for original configuration in sites-enabled
originalEnabledPath := nginx.GetConfPath("sites-enabled", name)
// Check if the site is already in maintenance mode
if helper.FileExists(maintenanceConfigPath) {
return
}
// Read the original configuration file
content, err := os.ReadFile(configFilePath)
if err != nil {
return
}
// Parse the nginx configuration
p := parser.NewStringParser(string(content), parser.WithSkipValidDirectivesErr())
conf, err := p.Parse()
if err != nil {
return fmt.Errorf("failed to parse nginx configuration: %s", err)
}
// Create new maintenance configuration
maintenanceConfig := createMaintenanceConfig(conf)
// Write maintenance configuration to file
err = os.WriteFile(maintenanceConfigPath, []byte(maintenanceConfig), 0644)
if err != nil {
return
}
// Remove the original symlink from sites-enabled if it exists
if helper.FileExists(originalEnabledPath) {
err = os.Remove(originalEnabledPath)
if err != nil {
// If we couldn't remove the original, remove the maintenance file and return the error
_ = os.Remove(maintenanceConfigPath)
return
}
}
// Test nginx config, if not pass, then restore original configuration
output := nginx.TestConf()
if nginx.GetLogLevel(output) > nginx.Warn {
// Configuration error, cleanup and revert
_ = os.Remove(maintenanceConfigPath)
if helper.FileExists(originalEnabledPath + "_backup") {
_ = os.Rename(originalEnabledPath+"_backup", originalEnabledPath)
}
return fmt.Errorf("%s", output)
}
// Reload nginx
output = nginx.Reload()
if nginx.GetLogLevel(output) > nginx.Warn {
return fmt.Errorf("%s", output)
}
// Synchronize with other nodes
go syncEnableMaintenance(name)
return nil
}
// DisableMaintenance disables maintenance mode for a site
func DisableMaintenance(name string) (err error) {
// Check if the site is in maintenance mode
maintenanceConfigPath := nginx.GetConfPath("sites-enabled", name+MaintenanceSuffix)
_, err = os.Stat(maintenanceConfigPath)
if err != nil {
return
}
// Original configuration paths
configFilePath := nginx.GetConfPath("sites-available", name)
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
// Check if the original configuration exists
_, err = os.Stat(configFilePath)
if err != nil {
return
}
// Create symlink to original configuration
err = os.Symlink(configFilePath, enabledConfigFilePath)
if err != nil {
return
}
// Remove maintenance configuration
err = os.Remove(maintenanceConfigPath)
if err != nil {
// If we couldn't remove the maintenance file, remove the new symlink and return the error
_ = os.Remove(enabledConfigFilePath)
return
}
// Test nginx config, if not pass, then revert
output := nginx.TestConf()
if nginx.GetLogLevel(output) > nginx.Warn {
// Configuration error, cleanup and revert
_ = os.Remove(enabledConfigFilePath)
_ = os.Symlink(configFilePath, maintenanceConfigPath)
return fmt.Errorf("%s", output)
}
// Reload nginx
output = nginx.Reload()
if nginx.GetLogLevel(output) > nginx.Warn {
return fmt.Errorf("%s", output)
}
// Synchronize with other nodes
go syncDisableMaintenance(name)
return nil
}
// createMaintenanceConfig creates a maintenance configuration based on the original config
func createMaintenanceConfig(conf *config.Config) string {
nginxUIPort := settings.ServerSettings.Port
schema := "http"
if settings.ServerSettings.EnableHTTPS {
schema = "https"
}
// Create new configuration
ngxConfig := nginx.NewNgxConfig("")
// Find all server blocks in the original configuration
serverBlocks := findServerBlocks(conf.Block)
// Create maintenance mode configuration for each server block
for _, server := range serverBlocks {
ngxServer := nginx.NewNgxServer()
// Copy listen directives
listenDirectives := extractDirectives(server, "listen")
for _, directive := range listenDirectives {
ngxDirective := &nginx.NgxDirective{
Directive: directive.GetName(),
Params: strings.Join(extractParams(directive), " "),
}
ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
}
// Copy server_name directives
serverNameDirectives := extractDirectives(server, "server_name")
for _, directive := range serverNameDirectives {
ngxDirective := &nginx.NgxDirective{
Directive: directive.GetName(),
Params: strings.Join(extractParams(directive), " "),
}
ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
}
// Copy SSL certificate directives
sslCertDirectives := extractDirectives(server, "ssl_certificate")
for _, directive := range sslCertDirectives {
ngxDirective := &nginx.NgxDirective{
Directive: directive.GetName(),
Params: strings.Join(extractParams(directive), " "),
}
ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
}
// Copy SSL certificate key directives
sslKeyDirectives := extractDirectives(server, "ssl_certificate_key")
for _, directive := range sslKeyDirectives {
ngxDirective := &nginx.NgxDirective{
Directive: directive.GetName(),
Params: strings.Join(extractParams(directive), " "),
}
ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
}
// Copy http2 directives
http2Directives := extractDirectives(server, "http2")
for _, directive := range http2Directives {
ngxDirective := &nginx.NgxDirective{
Directive: directive.GetName(),
Params: strings.Join(extractParams(directive), " "),
}
ngxServer.Directives = append(ngxServer.Directives, ngxDirective)
}
// Add maintenance mode location
location := &nginx.NgxLocation{
Path: "~ .*",
}
// Build location content using string builder
var locationContent strings.Builder
locationContent.WriteString("proxy_set_header Host $host;\n")
locationContent.WriteString("proxy_set_header X-Real-IP $remote_addr;\n")
locationContent.WriteString("proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
locationContent.WriteString("proxy_set_header X-Forwarded-Proto $scheme;\n")
locationContent.WriteString(fmt.Sprintf("rewrite ^ /pages/maintenance break;\n"))
locationContent.WriteString(fmt.Sprintf("proxy_pass %s://127.0.0.1:%d;\n", schema, nginxUIPort))
location.Content = locationContent.String()
ngxServer.Locations = append(ngxServer.Locations, location)
// Add to configuration
ngxConfig.Servers = append(ngxConfig.Servers, ngxServer)
}
// Generate configuration file content
content, err := ngxConfig.BuildConfig()
if err != nil {
logger.Error("Failed to build maintenance config", err)
return ""
}
return content
}
// findServerBlocks finds all server blocks in a configuration
func findServerBlocks(block config.IBlock) []config.IDirective {
var servers []config.IDirective
if block == nil {
return servers
}
for _, directive := range block.GetDirectives() {
if directive.GetName() == "server" {
servers = append(servers, directive)
}
}
return servers
}
// extractDirectives extracts all directives with a specific name from a server block
func extractDirectives(server config.IDirective, name string) []config.IDirective {
var directives []config.IDirective
if server.GetBlock() == nil {
return directives
}
for _, directive := range server.GetBlock().GetDirectives() {
if directive.GetName() == name {
directives = append(directives, directive)
}
}
return directives
}
// extractParams extracts all parameters from a directive
func extractParams(directive config.IDirective) []string {
var params []string
for _, param := range directive.GetParameters() {
params = append(params, param.Value)
}
return params
}
// syncEnableMaintenance synchronizes enabling maintenance mode with other nodes
func syncEnableMaintenance(name string) {
nodes := getSyncNodes(name)
wg := &sync.WaitGroup{}
wg.Add(len(nodes))
for _, node := range nodes {
go func(node *model.Environment) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, 1024)
runtime.Stack(buf, false)
logger.Error(err)
}
}()
defer wg.Done()
client := resty.New()
client.SetBaseURL(node.URL)
resp, err := client.R().
SetHeader("X-Node-Secret", node.Token).
Post(fmt.Sprintf("/api/sites/%s/maintenance/enable", name))
if err != nil {
notification.Error("Enable Remote Site Maintenance Error", err.Error(), nil)
return
}
if resp.StatusCode() != http.StatusOK {
notification.Error("Enable Remote Site Maintenance Error", "Enable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
return
}
notification.Success("Enable Remote Site Maintenance Success", "Enable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
}(node)
}
wg.Wait()
}
// syncDisableMaintenance synchronizes disabling maintenance mode with other nodes
func syncDisableMaintenance(name string) {
nodes := getSyncNodes(name)
wg := &sync.WaitGroup{}
wg.Add(len(nodes))
for _, node := range nodes {
go func(node *model.Environment) {
defer func() {
if err := recover(); err != nil {
buf := make([]byte, 1024)
runtime.Stack(buf, false)
logger.Error(err)
}
}()
defer wg.Done()
client := resty.New()
client.SetBaseURL(node.URL)
resp, err := client.R().
SetHeader("X-Node-Secret", node.Token).
Post(fmt.Sprintf("/api/sites/%s/maintenance/disable", name))
if err != nil {
notification.Error("Disable Remote Site Maintenance Error", err.Error(), nil)
return
}
if resp.StatusCode() != http.StatusOK {
notification.Error("Disable Remote Site Maintenance Error", "Disable site %{name} maintenance on %{node} failed", NewSyncResult(node.Name, name, resp))
return
}
notification.Success("Disable Remote Site Maintenance Success", "Disable site %{name} maintenance on %{node} successfully", NewSyncResult(node.Name, name, resp))
}(node)
}
wg.Wait()
}

View file

@ -33,7 +33,7 @@ func newConfigBackup(db *gorm.DB, opts ...gen.DOOption) configBackup {
_configBackup.UpdatedAt = field.NewTime(tableName, "updated_at")
_configBackup.DeletedAt = field.NewField(tableName, "deleted_at")
_configBackup.Name = field.NewString(tableName, "name")
_configBackup.FilePath = field.NewString(tableName, "file_path")
_configBackup.FilePath = field.NewString(tableName, "filepath")
_configBackup.Content = field.NewString(tableName, "content")
_configBackup.fillFieldMap()
@ -73,7 +73,7 @@ func (c *configBackup) updateTableName(table string) *configBackup {
c.UpdatedAt = field.NewTime(table, "updated_at")
c.DeletedAt = field.NewField(table, "deleted_at")
c.Name = field.NewString(table, "name")
c.FilePath = field.NewString(table, "file_path")
c.FilePath = field.NewString(table, "filepath")
c.Content = field.NewString(table, "content")
c.fillFieldMap()
@ -97,7 +97,7 @@ func (c *configBackup) fillFieldMap() {
c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt
c.fieldMap["name"] = c.Name
c.fieldMap["file_path"] = c.FilePath
c.fieldMap["filepath"] = c.FilePath
c.fieldMap["content"] = c.Content
}

View file

@ -35,6 +35,7 @@ func newEnvGroup(db *gorm.DB, opts ...gen.DOOption) envGroup {
_envGroup.Name = field.NewString(tableName, "name")
_envGroup.SyncNodeIds = field.NewField(tableName, "sync_node_ids")
_envGroup.OrderID = field.NewInt(tableName, "order_id")
_envGroup.PostSyncAction = field.NewString(tableName, "post_sync_action")
_envGroup.fillFieldMap()
@ -44,14 +45,15 @@ func newEnvGroup(db *gorm.DB, opts ...gen.DOOption) envGroup {
type envGroup struct {
envGroupDo
ALL field.Asterisk
ID field.Uint64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
SyncNodeIds field.Field
OrderID field.Int
ALL field.Asterisk
ID field.Uint64
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
SyncNodeIds field.Field
OrderID field.Int
PostSyncAction field.String
fieldMap map[string]field.Expr
}
@ -75,6 +77,7 @@ func (e *envGroup) updateTableName(table string) *envGroup {
e.Name = field.NewString(table, "name")
e.SyncNodeIds = field.NewField(table, "sync_node_ids")
e.OrderID = field.NewInt(table, "order_id")
e.PostSyncAction = field.NewString(table, "post_sync_action")
e.fillFieldMap()
@ -91,7 +94,7 @@ func (e *envGroup) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (e *envGroup) fillFieldMap() {
e.fieldMap = make(map[string]field.Expr, 7)
e.fieldMap = make(map[string]field.Expr, 8)
e.fieldMap["id"] = e.ID
e.fieldMap["created_at"] = e.CreatedAt
e.fieldMap["updated_at"] = e.UpdatedAt
@ -99,6 +102,7 @@ func (e *envGroup) fillFieldMap() {
e.fieldMap["name"] = e.Name
e.fieldMap["sync_node_ids"] = e.SyncNodeIds
e.fieldMap["order_id"] = e.OrderID
e.fieldMap["post_sync_action"] = e.PostSyncAction
}
func (e envGroup) clone(db *gorm.DB) envGroup {

View file

@ -15,6 +15,7 @@ import (
nginxLog "github.com/0xJacky/Nginx-UI/api/nginx_log"
"github.com/0xJacky/Nginx-UI/api/notification"
"github.com/0xJacky/Nginx-UI/api/openai"
"github.com/0xJacky/Nginx-UI/api/pages"
"github.com/0xJacky/Nginx-UI/api/public"
"github.com/0xJacky/Nginx-UI/api/settings"
"github.com/0xJacky/Nginx-UI/api/sites"
@ -35,13 +36,15 @@ func InitRouter() {
initEmbedRoute(r)
pages.InitRouter(r)
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"message": "not found",
})
})
root := r.Group("/api")
root := r.Group("/api", middleware.IPWhiteList())
{
public.InitRouter(root)
crypto.InitPublicRouter(root)

View file

@ -9,9 +9,7 @@ import (
)
func initEmbedRoute(r *gin.Engine) {
r.Use(
middleware.CacheJs(),
middleware.IPWhiteList(),
static.Serve("/", middleware.MustFs("")),
)
r.Use(middleware.CacheJs())
r.GET("/", middleware.IPWhiteList(), static.Serve("/", middleware.MustFs("")))
}