2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 03:07:20 +08:00

feat: 面板ip证书

This commit is contained in:
2026-01-08 18:03:23 +08:00
parent b3c546dc13
commit d850292622
17 changed files with 396 additions and 58 deletions

View File

@@ -151,7 +151,7 @@ func initWeb() (*app.Web, error) {
return nil, err
}
gormigrate := bootstrap.NewMigrate(db)
jobs := job.NewJobs(db, logger, settingRepo, certRepo, backupRepo, cacheRepo, taskRepo)
jobs := job.NewJobs(config, db, logger, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo)
cron, err := bootstrap.NewCron(config, logger, jobs)
if err != nil {
return nil, err

View File

@@ -68,7 +68,7 @@ func initCli() (*app.Cli, error) {
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo)
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
cli := route.NewCli(locale, cliService)
command := bootstrap.NewCli(locale, cli)
gormigrate := bootstrap.NewMigrate(db)

View File

@@ -38,6 +38,7 @@ type CertRepo interface {
Delete(id uint) error
ObtainAuto(id uint) (*acme.Certificate, error)
ObtainManual(id uint) (*acme.Certificate, error)
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
ObtainSelfSigned(id uint) error
Renew(id uint) (*acme.Certificate, error)
ManualDNS(id uint) ([]acme.DNSRecord, error)

View File

@@ -9,6 +9,7 @@ import (
type SettingKey string
const (
SettingKeyIP SettingKey = "ip"
SettingKeyName SettingKey = "name"
SettingKeyVersion SettingKey = "version"
SettingKeyChannel SettingKey = "channel"

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"time"
@@ -233,6 +234,21 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
return &ssl, nil
}
func (r *certRepo) ObtainPanel(account *biz.CertAccount, ips []string) ([]byte, []byte, error) {
client, err := acme.NewPrivateKeyAccount(account.Email, account.PrivateKey, acme.CALetsEncrypt, nil, r.log)
if err != nil {
return nil, nil, err
}
client.UsePanel(ips, filepath.Join(app.Root, "server/nginx/conf/acme.conf"))
ssl, err := client.ObtainCertificate(context.Background(), ips, acme.KeyEC256)
if err != nil {
return nil, nil, err
}
return ssl.ChainPEM, ssl.PrivateKey, nil
}
func (r *certRepo) ObtainSelfSigned(id uint) error {
cert, err := r.Get(id)
if err != nil {

View File

@@ -2,8 +2,12 @@ package job
import (
"log/slog"
"path/filepath"
"time"
"github.com/acepanel/panel/pkg/config"
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/systemctl"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
@@ -13,16 +17,22 @@ import (
// CertRenew 证书续签
type CertRenew struct {
db *gorm.DB
log *slog.Logger
certRepo biz.CertRepo
conf *config.Config
db *gorm.DB
log *slog.Logger
settingRepo biz.SettingRepo
certRepo biz.CertRepo
certAccountRepo biz.CertAccountRepo
}
func NewCertRenew(db *gorm.DB, log *slog.Logger, cert biz.CertRepo) *CertRenew {
func NewCertRenew(conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) *CertRenew {
return &CertRenew{
db: db,
log: log,
certRepo: cert,
conf: conf,
db: db,
log: log,
settingRepo: setting,
certRepo: cert,
certAccountRepo: certAccount,
}
}
@@ -57,4 +67,52 @@ func (r *CertRenew) Run() {
r.log.Warn("[CertRenew] failed to renew cert", slog.Any("err", err))
}
}
// 面板证书续签
if r.conf.HTTP.ACME {
decode, err := pkgcert.ParseCert(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
r.log.Warn("[CertRenew] failed to parse panel cert", slog.Any("err", err))
return
}
// 结束时间大于 2 天不续签
if time.Until(decode.NotAfter) > 24*2*time.Hour {
return
}
ip, err := r.settingRepo.Get(biz.SettingKeyIP)
if err != nil || ip == "" {
r.log.Warn("[CertRenew] failed to get panel IP", slog.Any("err", err))
return
}
var user biz.User
if err = r.db.First(&user).Error; err != nil {
r.log.Warn("[CertRenew] failed to get a panel user", slog.Any("err", err))
return
}
account, err := r.certAccountRepo.GetDefault(user.ID)
if err != nil {
r.log.Warn("[CertRenew] failed to get panel ACME account", slog.Any("err", err))
return
}
crt, key, err := r.certRepo.ObtainPanel(account, []string{ip})
if err != nil {
r.log.Warn("[CertRenew] failed to obtain ACME cert", slog.Any("err", err))
return
}
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil {
r.log.Warn("[CertRenew] failed to write panel cert", slog.Any("err", err))
return
}
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), string(key), 0644); err != nil {
r.log.Warn("[CertRenew] failed to write panel cert key", slog.Any("err", err))
return
}
r.log.Info("[CertRenew] panel cert renewed successfully")
_ = systemctl.Restart("panel")
}
}

View File

@@ -3,6 +3,7 @@ package job
import (
"log/slog"
"github.com/acepanel/panel/pkg/config"
"github.com/google/wire"
"github.com/robfig/cron/v3"
"gorm.io/gorm"
@@ -13,24 +14,28 @@ import (
var ProviderSet = wire.NewSet(NewJobs)
type Jobs struct {
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
cert biz.CertRepo
backup biz.BackupRepo
cache biz.CacheRepo
task biz.TaskRepo
conf *config.Config
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
cert biz.CertRepo
certAccount biz.CertAccountRepo
backup biz.BackupRepo
cache biz.CacheRepo
task biz.TaskRepo
}
func NewJobs(db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, backup biz.BackupRepo, cache biz.CacheRepo, task biz.TaskRepo) *Jobs {
func NewJobs(conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, backup biz.BackupRepo, cache biz.CacheRepo, task biz.TaskRepo) *Jobs {
return &Jobs{
db: db,
log: log,
setting: setting,
cert: cert,
backup: backup,
cache: cache,
task: task,
conf: conf,
db: db,
log: log,
setting: setting,
cert: cert,
certAccount: certAccount,
backup: backup,
cache: cache,
task: task,
}
}
@@ -38,10 +43,9 @@ func (r *Jobs) Register(c *cron.Cron) error {
if _, err := c.AddJob("* * * * *", NewMonitoring(r.db, r.log, r.setting)); err != nil {
return err
}
if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.db, r.log, r.cert)); err != nil {
if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.conf, r.db, r.log, r.setting, r.cert, r.certAccount)); err != nil {
return err
}
if _, err := c.AddJob("0 2 * * *", NewPanelTask(r.db, r.log, r.backup, r.cache, r.task, r.setting)); err != nil {
return err
}

View File

@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/acepanel/panel/pkg/cert"
"github.com/leonelquinteros/gotext"
"github.com/libtnb/utils/collect"
"github.com/libtnb/utils/hash"
@@ -23,7 +24,6 @@ import (
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/api"
"github.com/acepanel/panel/pkg/cert"
"github.com/acepanel/panel/pkg/config"
"github.com/acepanel/panel/pkg/firewall"
"github.com/acepanel/panel/pkg/io"
@@ -46,10 +46,12 @@ type CliService struct {
backupRepo biz.BackupRepo
websiteRepo biz.WebsiteRepo
databaseServerRepo biz.DatabaseServerRepo
certRepo biz.CertRepo
certAccountRepo biz.CertAccountRepo
hash hash.Hasher
}
func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo) *CliService {
func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) *CliService {
return &CliService{
hr: `+----------------------------------------------------`,
api: api.NewAPI(app.Version, app.Locale),
@@ -63,6 +65,8 @@ func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo b
backupRepo: backup,
websiteRepo: website,
databaseServerRepo: databaseServer,
certRepo: cert,
certAccountRepo: certAccount,
hash: hash.NewArgon2id(),
}
}
@@ -218,9 +222,8 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error {
if err := s.db.Where("username", oldUsername).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(s.t.Get("User not exists"))
} else {
return errors.New(s.t.Get("Failed to get user: %v", err))
}
return errors.New(s.t.Get("Failed to get user: %v", err))
}
user.Username = newUsername
@@ -246,9 +249,8 @@ func (s *CliService) UserPassword(ctx context.Context, cmd *cli.Command) error {
if err := s.db.Where("username", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(s.t.Get("User not exists"))
} else {
return errors.New(s.t.Get("Failed to get user: %v", err))
}
return errors.New(s.t.Get("Failed to get user: %v", err))
}
hashed, err := s.hash.Make(password)
@@ -274,9 +276,8 @@ func (s *CliService) UserTwoFA(ctx context.Context, cmd *cli.Command) error {
if err := s.db.Where("username", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(s.t.Get("User not exists"))
} else {
return errors.New(s.t.Get("Failed to get user: %v", err))
}
return errors.New(s.t.Get("Failed to get user: %v", err))
}
// 已开启关闭2FA
@@ -359,6 +360,26 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error
return err
}
if s.conf.HTTP.ACME {
ip, err := s.settingRepo.Get(biz.SettingKeyIP)
if err != nil || ip == "" {
return errors.New(s.t.Get("Please set the panel IP in settings first for ACME certificate generation: %v", err))
}
var user biz.User
if err := s.db.First(&user).Error; err != nil {
return errors.New(s.t.Get("Failed to get a panel user: %v", err))
}
account, err := s.certAccountRepo.GetDefault(user.ID)
if err != nil {
return errors.New(s.t.Get("Failed to get ACME account: %v", err))
}
crt, key, err = s.certRepo.ObtainPanel(account, names)
if err != nil {
return errors.New(s.t.Get("Failed to obtain ACME certificate: %v", err))
}
}
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil {
return err
}
@@ -558,7 +579,7 @@ func (s *CliService) DatabaseAddServer(ctx context.Context, cmd *cli.Command) er
Type: cmd.String("type"),
Name: cmd.String("name"),
Host: cmd.String("host"),
Port: uint(cmd.Uint("port")),
Port: cmd.Uint("port"),
Username: cmd.String("username"),
Password: cmd.String("password"),
Remark: cmd.String("remark"),
@@ -646,7 +667,7 @@ func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
fmt.Println(s.t.Get("|-Cleaning type: %s", cmd.String("type")))
fmt.Println(s.t.Get("|-Cleaning target: %s", cmd.String("file")))
fmt.Println(s.t.Get("|-Keep count: %d", cmd.Int("save")))
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), cmd.Int("save")); err != nil {
return errors.New(s.t.Get("Cleaning failed: %v", err))
}
fmt.Println(s.hr)
@@ -694,7 +715,7 @@ func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
fmt.Println(s.t.Get("|-Cleaning type: %s", cmd.String("type")))
fmt.Println(s.t.Get("|-Cleaning target: %s", cmd.String("file")))
fmt.Println(s.t.Get("|-Keep count: %d", cmd.Int("save")))
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), cmd.Int("save")); err != nil {
return err
}
fmt.Println(s.hr)
@@ -868,19 +889,33 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
return errors.New(s.t.Get("Already initialized"))
}
ip := ""
acme := false
rv6, err := tools.GetPublicIPv6()
if err == nil {
ip = rv6
acme = true
}
rv4, err := tools.GetPublicIPv4()
if err == nil {
ip = rv4
acme = true
}
settings := []biz.Setting{
{Key: biz.SettingKeyIP, Value: ip},
{Key: biz.SettingKeyName, Value: "AcePanel"},
{Key: biz.SettingKeyChannel, Value: "stable"},
{Key: biz.SettingKeyVersion, Value: app.Version},
{Key: biz.SettingKeyMonitor, Value: "true"},
{Key: biz.SettingKeyMonitorDays, Value: "30"},
{Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")},
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")},
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "sites")},
{Key: biz.SettingKeyOfflineMode, Value: "false"},
{Key: biz.SettingKeyAutoUpdate, Value: "true"},
{Key: biz.SettingHiddenMenu, Value: "[]"},
}
if err := s.db.Create(&settings).Error; err != nil {
if err = s.db.Create(&settings).Error; err != nil {
return errors.New(s.t.Get("Initialization failed: %v", err))
}
@@ -894,10 +929,6 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
return errors.New(s.t.Get("Initialization failed: %v", err))
}
if err = s.HTTPSGenerate(ctx, cmd); err != nil {
return errors.New(s.t.Get("Initialization failed: %v", err))
}
conf, err := config.Load()
if err != nil {
return err
@@ -907,6 +938,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
conf.App.APIEndpoint = "api.acepanel.net"
conf.App.DownloadEndpoint = "dl.acepanel.net"
conf.HTTP.Entrance = "/" + str.Random(6)
conf.HTTP.ACME = acme
// 随机默认端口
checkPort:
@@ -930,5 +962,11 @@ checkPort:
return err
}
s.conf = conf // 更新配置否则后续签发证书不会使用ACME
if err = s.HTTPSGenerate(ctx, cmd); err != nil {
return errors.New(s.t.Get("Initialization failed: %v", err))
}
return nil
}

View File

@@ -52,7 +52,6 @@ func (c *Client) UseManualDns(check ...bool) {
// UseHTTP 使用 HTTP 验证
// conf nginx 配置文件路径
// path 验证文件存放路径
func (c *Client) UseHTTP(conf string) {
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: httpSolver{
@@ -61,6 +60,18 @@ func (c *Client) UseHTTP(conf string) {
}
}
// UsePanel 使用面板 HTTP 验证
// ip 外网访问 IP 地址
// conf nginx 配置文件路径
func (c *Client) UsePanel(ip []string, conf string) {
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &panelSolver{
ip: ip,
conf: conf,
},
}
}
// ObtainCertificate 签发 SSL 证书
func (c *Client) ObtainCertificate(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
certPrivateKey, err := generatePrivateKey(keyType)

View File

@@ -2,7 +2,9 @@ package acme
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
@@ -20,10 +22,106 @@ import (
"github.com/mholt/acmez/v3/acme"
"golang.org/x/net/publicsuffix"
pkgos "github.com/acepanel/panel/pkg/os"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/systemctl"
)
type panelSolver struct {
ip []string
conf string
server *http.Server
}
func (s *panelSolver) Present(_ context.Context, challenge acme.Challenge) error {
// 如果 80 端口没有被占用,则直接起一个内置的 HTTP 服务器验证
if !pkgos.TCPPortInUse(80) {
return s.presentPanel(challenge)
}
conf := fmt.Sprintf(`server {
listen 80;
server_name %s;
location = %s {
default_type text/plain;
return 200 %q;
}
}
`, s.ip, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization)
f, err := os.OpenFile(s.conf, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open nginx config %q: %w", s.conf, err)
}
defer func(f *os.File) { _ = f.Close() }(f)
if _, err = f.Write([]byte(conf)); err != nil {
return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err)
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return fmt.Errorf("failed to reload nginx: %w", err)
}
return nil
}
func (s *panelSolver) presentPanel(challenge acme.Challenge) error {
mux := http.NewServeMux()
mux.HandleFunc(challenge.HTTP01ResourcePath(), func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(challenge.KeyAuthorization))
})
s.server = &http.Server{
Addr: ":80",
Handler: mux,
}
errChan := make(chan error, 1)
go func() {
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errChan <- err
}
close(errChan)
}()
// 等待一小段时间确保服务器启动成功
select {
case err := <-errChan:
return fmt.Errorf("failed to start HTTP server: %w", err)
case <-time.After(100 * time.Millisecond):
return nil
}
}
// CleanUp cleans up the HTTP server if it is the last one to finish.
func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error {
// 如果启动了内置 HTTP 服务器,则关闭它
if s.server != nil {
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := s.server.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("failed to shutdown HTTP server: %w", err)
}
s.server = nil
return nil
}
// 否则清理 nginx 配置
if err := os.WriteFile(s.conf, []byte(""), 0644); err != nil {
return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err)
}
if err := systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return fmt.Errorf("failed to reload nginx: %w", err)
}
return nil
}
type httpSolver struct {
conf string
}

View File

@@ -34,6 +34,7 @@ type HTTPConfig struct {
Entrance string `yaml:"entrance"`
EntranceError string `yaml:"entrance_error"`
TLS bool `yaml:"tls"`
ACME bool `yaml:"acme"`
LoginCaptcha bool `yaml:"login_captcha"`
IPHeader string `yaml:"ip_header"`
BindDomain []string `yaml:"bind_domain"`

View File

@@ -496,8 +496,6 @@ func (v *baseVhost) SetSSLConfig(cfg *types.SSLConfig) error {
}
// 设置 OCSP
_ = v.parser.Clear("server.ssl_stapling")
_ = v.parser.Clear("server.ssl_stapling_verify")
if cfg.OCSP {
if err = v.parser.Set("server", []*config.Directive{
{
@@ -535,6 +533,11 @@ func (v *baseVhost) ClearSSL() error {
_ = v.parser.Clear("server.ssl_ciphers")
_ = v.parser.Clear("server.ssl_prefer_server_ciphers")
_ = v.parser.Clear("server.ssl_early_data")
_ = v.parser.Clear("server.ssl_stapling")
_ = v.parser.Clear("server.ssl_stapling_verify")
_ = v.setHSTS(false)
_ = v.setHTTPSRedirect(false)
_ = v.setAltSvc("")
return nil
}

View File

@@ -0,0 +1,14 @@
import { http } from '@/utils'
export default {
// 负载状态
load: (): any => http.Get('/apps/openresty/load'),
// 获取配置
config: (): any => http.Get('/apps/openresty/config'),
// 保存配置
saveConfig: (config: string): any => http.Post('/apps/openresty/config', { config }),
// 获取错误日志
errorLog: (): any => http.Get('/apps/openresty/error_log'),
// 清空错误日志
clearErrorLog: (): any => http.Post('/apps/openresty/clear_error_log')
}

View File

@@ -1,9 +0,0 @@
export interface Task {
id: number
name: string
status: string
shell: string
log: string
created_at: string
updated_at: string
}

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
defineOptions({
name: 'apps-openresty-index'
})
import { NButton, NDataTable } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import openresty from '@/api/apps/openresty'
import ServiceStatus from '@/components/common/ServiceStatus.vue'
const { $gettext } = useGettext()
const currentTab = ref('status')
const { data: config } = useRequest(openresty.config, {
initialData: ''
})
const { data: errorLog } = useRequest(openresty.errorLog, {
initialData: ''
})
const { data: load } = useRequest(openresty.load, {
initialData: []
})
const columns: any = [
{
title: $gettext('Property'),
key: 'name',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: $gettext('Current Value'),
key: 'value',
minWidth: 200,
ellipsis: { tooltip: true }
}
]
const handleSaveConfig = () => {
useRequest(openresty.saveConfig(config.value)).onSuccess(() => {
window.$message.success($gettext('Saved successfully'))
})
}
const handleClearErrorLog = () => {
useRequest(openresty.clearErrorLog()).onSuccess(() => {
window.$message.success($gettext('Cleared successfully'))
})
}
</script>
<template>
<common-page show-footer>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="status" :tab="$gettext('Running Status')">
<service-status service="nginx" show-reload />
</n-tab-pane>
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
<n-flex vertical>
<n-alert type="warning">
{{
$gettext(
'This modifies the OpenResty main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
)
}}
</n-alert>
<common-editor v-model:value="config" lang="nginx" height="60vh" />
<n-flex>
<n-button type="primary" @click="handleSaveConfig">
{{ $gettext('Save') }}
</n-button>
</n-flex>
</n-flex>
</n-tab-pane>
<n-tab-pane name="load" :tab="$gettext('Load Status')">
<n-data-table
striped
remote
:scroll-x="400"
:loading="false"
:columns="columns"
:data="load"
/>
</n-tab-pane>
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
<realtime-log service="nginx" />
</n-tab-pane>
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
<n-flex vertical>
<n-flex>
<n-button type="primary" @click="handleClearErrorLog">
{{ $gettext('Clear Log') }}
</n-button>
</n-flex>
<realtime-log :path="errorLog" />
</n-flex>
</n-tab-pane>
</n-tabs>
</common-page>
</template>

View File

@@ -11,7 +11,7 @@ export default {
{
name: 'apps-openresty-index',
path: '',
component: () => import('../nginx/IndexView.vue'),
component: () => import('./IndexView.vue'),
meta: {
title: 'OpenResty',
role: ['admin'],

View File

@@ -50,10 +50,10 @@ const channels = [
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" w-full />
</n-form-item>
<n-form-item :label="$gettext('Default Website Directory')">
<n-input v-model:value="model.website_path" :placeholder="$gettext('/www/wwwroot')" />
<n-input v-model:value="model.website_path" :placeholder="$gettext('/opt/ace/sites')" />
</n-form-item>
<n-form-item :label="$gettext('Default Backup Directory')">
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/www/backup')" />
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/opt/ace/backup')" />
</n-form-item>
</n-form>
</n-flex>