mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: 完善证书设置
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user