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

feat: 完善证书设置

This commit is contained in:
2026-01-08 21:26:31 +08:00
parent 2099d2ca57
commit 2d525e9680
10 changed files with 183 additions and 44 deletions

View File

@@ -114,7 +114,7 @@ func initWeb() (*app.Web, error) {
fileService := service.NewFileService(locale, taskRepo)
monitorRepo := data.NewMonitorRepo(db, settingRepo)
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
settingService := service.NewSettingService(settingRepo)
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
systemctlService := service.NewSystemctlService(locale)
toolboxSystemService := service.NewToolboxSystemService(locale)
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)

View File

@@ -9,7 +9,6 @@ import (
type SettingKey string
const (
SettingKeyIP SettingKey = "ip"
SettingKeyName SettingKey = "name"
SettingKeyVersion SettingKey = "version"
SettingKeyChannel SettingKey = "channel"
@@ -21,6 +20,7 @@ const (
SettingKeyOfflineMode SettingKey = "offline_mode"
SettingKeyAutoUpdate SettingKey = "auto_update"
SettingKeyWebserver SettingKey = "webserver"
SettingKeyPublicIPs SettingKey = "public_ips"
SettingHiddenMenu SettingKey = "hidden_menu"
SettingKeyCustomLogo SettingKey = "custom_logo"
)

View File

@@ -221,6 +221,14 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
if err != nil {
return nil, err
}
ip, err := r.Get(biz.SettingKeyPublicIPs)
if err != nil {
return nil, err
}
publicIP := make([]string, 0)
if err = json.Unmarshal([]byte(ip), &publicIP); err != nil {
return nil, err
}
crt, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
@@ -247,6 +255,8 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
BackupPath: backupPath,
Port: r.conf.HTTP.Port,
HTTPS: r.conf.HTTP.TLS,
ACME: r.conf.HTTP.ACME,
PublicIP: publicIP,
Cert: crt,
Key: key,
}, nil
@@ -271,6 +281,13 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
return false, err
}
publicIPBytes, err := json.Marshal(req.PublicIP)
if err != nil {
return false, err
}
if err = r.Set(biz.SettingKeyPublicIPs, string(publicIPBytes)); err != nil {
return false, err
}
// 下面是需要需要重启的设置
// 面板HTTPS
@@ -326,6 +343,7 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
conf.HTTP.Port = req.Port
conf.HTTP.Entrance = req.Entrance
conf.HTTP.TLS = req.HTTPS
conf.HTTP.ACME = req.ACME
conf.HTTP.IPHeader = req.IPHeader
conf.HTTP.BindDomain = req.BindDomain
conf.HTTP.BindIP = req.BindIP

View File

@@ -19,6 +19,8 @@ type SettingPanel struct {
BackupPath string `json:"backup_path" validate:"required"`
Port uint `json:"port" validate:"required|min:1|max:65535"`
HTTPS bool `json:"https"`
ACME bool `json:"acme"`
PublicIP []string `json:"public_ip"`
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
}

View File

@@ -1,13 +1,14 @@
package job
import (
"encoding/json"
"log/slog"
"path/filepath"
"time"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/config"
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/systemctl"
"github.com/acepanel/panel/pkg/tools"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
@@ -80,11 +81,16 @@ func (r *CertRenew) Run() {
return
}
ip, err := r.settingRepo.Get(biz.SettingKeyIP)
if err != nil || ip == "" {
ip, err := r.settingRepo.Get(biz.SettingKeyPublicIPs)
if err != nil {
r.log.Warn("[CertRenew] failed to get panel IP", slog.Any("err", err))
return
}
var ips []string
if err = json.Unmarshal([]byte(ip), &ips); err != nil || len(ips) == 0 {
r.log.Warn("[CertRenew] panel public IPs not set", slog.Any("err", err))
return
}
var user biz.User
if err = r.db.First(&user).Error; err != nil {
@@ -96,23 +102,22 @@ func (r *CertRenew) Run() {
r.log.Warn("[CertRenew] failed to get panel ACME account", slog.Any("err", err))
return
}
crt, key, err := r.certRepo.ObtainPanel(account, []string{ip})
crt, key, err := r.certRepo.ObtainPanel(account, ips)
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))
if err = r.settingRepo.UpdateCert(&request.SettingCert{
Cert: string(crt),
Key: string(key),
}); err != nil {
r.log.Warn("[CertRenew] failed to update panel cert", slog.Any("err", err))
return
}
r.log.Info("[CertRenew] panel cert renewed successfully")
_ = systemctl.Restart("panel")
tools.RestartPanel()
}
}

View File

@@ -3,6 +3,7 @@ package service
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"math/rand/v2"
@@ -361,9 +362,13 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error
}
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))
ip, err := s.settingRepo.Get(biz.SettingKeyPublicIPs)
if err != nil {
return err
}
var ips []string
if err = json.Unmarshal([]byte(ip), &ips); err != nil || len(ips) == 0 {
return errors.New(s.t.Get("Please set the panel IP in settings first for ACME certificate generation"))
}
var user biz.User
@@ -374,10 +379,11 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error
if err != nil {
return errors.New(s.t.Get("Failed to get ACME account: %v", err))
}
crt, key, err = s.certRepo.ObtainPanel(account, []string{ip})
crt, key, err = s.certRepo.ObtainPanel(account, ips)
if err != nil {
return errors.New(s.t.Get("Failed to obtain ACME certificate: %v", err))
}
fmt.Println(s.t.Get("Successfully obtained ACME certificate"))
}
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil {
@@ -889,21 +895,25 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
return errors.New(s.t.Get("Already initialized"))
}
ip := ""
ips := make([]string, 0)
acme := false
rv6, err := tools.GetPublicIPv6()
if err == nil {
ip = rv6
ips = append(ips, rv6)
acme = true
}
rv4, err := tools.GetPublicIPv4()
if err == nil {
ip = rv4
ips = append(ips, rv4)
acme = true
}
ip, err := json.Marshal(ips)
if err != nil {
ip = []byte("[]")
}
settings := []biz.Setting{
{Key: biz.SettingKeyIP, Value: ip},
{Key: biz.SettingKeyPublicIPs, Value: string(ip)},
{Key: biz.SettingKeyName, Value: "AcePanel"},
{Key: biz.SettingKeyChannel, Value: "stable"},
{Key: biz.SettingKeyVersion, Value: app.Version},

View File

@@ -1,20 +1,33 @@
package service
import (
"encoding/json"
"net/http"
"github.com/leonelquinteros/gotext"
"github.com/libtnb/chix"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/tools"
)
type SettingService struct {
settingRepo biz.SettingRepo
t *gotext.Locale
db *gorm.DB
settingRepo biz.SettingRepo
certRepo biz.CertRepo
certAccountRepo biz.CertAccountRepo
}
func NewSettingService(setting biz.SettingRepo) *SettingService {
func NewSettingService(t *gotext.Locale, db *gorm.DB, setting biz.SettingRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) *SettingService {
return &SettingService{
settingRepo: setting,
t: t,
db: db,
settingRepo: setting,
certRepo: cert,
certAccountRepo: certAccount,
}
}
@@ -45,6 +58,54 @@ func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) {
tools.RestartPanel()
}
Success(w, chix.M{
"restart": restart,
})
}
func (s *SettingService) ObtainCert(w http.ResponseWriter, r *http.Request) {
ip, err := s.settingRepo.Get(biz.SettingKeyPublicIPs)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
var ips []string
if err = json.Unmarshal([]byte(ip), &ips); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if len(ips) == 0 {
Error(w, http.StatusBadRequest, s.t.Get("please set public ips first"))
return
}
var user biz.User
if err = s.db.First(&user).Error; err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
account, err := s.certAccountRepo.GetDefault(user.ID)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
crt, key, err := s.certRepo.ObtainPanel(account, ips)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = s.settingRepo.UpdateCert(&request.SettingCert{
Cert: string(crt),
Key: string(key),
}); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
tools.RestartPanel()
Success(w, nil)
}

View File

@@ -36,6 +36,7 @@ const { data: model } = useRequest(setting.list, {
backup_path: '',
https: false,
acme: false,
public_ip: [],
cert: '',
key: ''
}
@@ -45,14 +46,25 @@ const handleSave = () => {
if (model.value.entrance.trim() === '') {
model.value.entrance = '/'
}
useRequest(setting.update(model.value)).onSuccess(() => {
useRequest(setting.update(model.value)).onSuccess(({ data }) => {
window.$message.success($gettext('Saved successfully'))
// 更新语言设置
if (model.value.locale !== themeStore.locale) {
themeStore.setLocale(model.value.locale)
window.$message.info($gettext('Panel is restarting, page will refresh in 3 seconds'))
}
// 如果需要重启,则自动刷新页面
if (data.restart) {
window.$message.info($gettext('Panel is restarting, page will refresh in 5 seconds'))
setTimeout(() => {
window.location.reload()
}, 3000)
const protocol = model.value.https ? 'https:' : 'http:'
const hostname = window.location.hostname
const port = model.value.port
const entrance = model.value.entrance || '/'
// 构建新的 URL
window.location.href = `${protocol}//${hostname}:${port}${entrance}`
}, 5000)
}
})
}

View File

@@ -29,13 +29,6 @@ const channels = [
<template>
<n-flex vertical>
<n-alert type="info">
{{
$gettext(
'Modifying panel port/entrance requires corresponding changes in the browser address bar to access the panel!'
)
}}
</n-alert>
<n-form>
<n-form-item :label="$gettext('Panel Name')">
<n-input v-model:value="model.name" :placeholder="$gettext('Panel Name')" />

View File

@@ -1,9 +1,40 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const model = defineModel<any>('model', { type: Object, required: true })
// HTTPS 模式off / acme / custom
const httpsMode = computed({
get: () => {
if (!model.value.https) return 'off'
return model.value.acme ? 'acme' : 'custom'
},
set: (value: string) => {
switch (value) {
case 'off':
model.value.https = false
model.value.acme = false
break
case 'acme':
model.value.https = true
model.value.acme = true
break
case 'custom':
model.value.https = true
model.value.acme = false
break
}
}
})
const httpsModeOptions = computed(() => [
{ label: $gettext('Disabled'), value: 'off' },
{ label: $gettext('ACME (Auto)'), value: 'acme' },
{ label: $gettext('Custom Certificate'), value: 'custom' }
])
</script>
<template>
@@ -182,39 +213,46 @@ const model = defineModel<any>('model', { type: Object, required: true })
</template>
{{
$gettext(
'Enable HTTPS for the panel to ensure secure communication. You need to provide a valid SSL certificate and private key'
'Enable HTTPS for the panel. ACME will automatically obtain and renew certificates (requires panel accessible via public IP). Custom allows you to provide your own certificate'
)
}}
</n-tooltip>
</template>
<n-switch v-model:value="model.https" />
<n-radio-group v-model:value="httpsMode">
<n-radio-button
v-for="option in httpsModeOptions"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item>
<n-form-item v-if="httpsMode === 'acme'" :label="$gettext('Panel Public IP')">
<template #label>
<n-tooltip>
<template #trigger>
<div class="flex items-center">
{{ $gettext('ACME') }}
{{ $gettext('Panel Public IP') }}
<the-icon :size="16" icon="mdi:help-circle-outline" class="ml-1" />
</div>
</template>
{{
$gettext(
'Use ACME protocol to automatically obtain and renew SSL certificates for the panel. Make sure your panel is accessible via the ip you provide'
'Panel public IP is used to issue HTTPS certificates using ACME. Ensure that the entered IP address is accessible from the public network.'
)
}}
</n-tooltip>
</template>
<n-switch v-model:value="model.acme" />
<n-dynamic-input v-model:value="model.public_ip" placeholder="127.0.0.1" show-sort-button />
</n-form-item>
<n-form-item v-if="model.https && !model.acme" :label="$gettext('Certificate')">
<n-form-item v-if="httpsMode === 'custom'" :label="$gettext('Certificate')">
<n-input
v-model:value="model.cert"
type="textarea"
:autosize="{ minRows: 10, maxRows: 15 }"
/>
</n-form-item>
<n-form-item v-if="model.https && !model.acme" :label="$gettext('Private Key')">
<n-form-item v-if="httpsMode === 'custom'" :label="$gettext('Private Key')">
<n-input
v-model:value="model.key"
type="textarea"