diff --git a/internal/biz/cert.go b/internal/biz/cert.go index c2e283d4..b62ba461 100644 --- a/internal/biz/cert.go +++ b/internal/biz/cert.go @@ -5,6 +5,7 @@ import ( "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/types" ) type Cert struct { @@ -27,9 +28,10 @@ type Cert struct { } type CertRepo interface { - List(page, limit uint) ([]*Cert, int64, error) + List(page, limit uint) ([]*types.CertList, int64, error) Get(id uint) (*Cert, error) GetByWebsite(WebsiteID uint) (*Cert, error) + Upload(req *request.CertUpload) (*Cert, error) Create(req *request.CertCreate) (*Cert, error) Update(req *request.CertUpdate) error Delete(id uint) error diff --git a/internal/data/cert.go b/internal/data/cert.go index 88ba932f..972a6e53 100644 --- a/internal/data/cert.go +++ b/internal/data/cert.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "time" @@ -11,9 +12,11 @@ import ( "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/acme" + pkgcert "github.com/TheTNB/panel/pkg/cert" "github.com/TheTNB/panel/pkg/io" "github.com/TheTNB/panel/pkg/shell" "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" ) type certRepo struct { @@ -24,11 +27,37 @@ func NewCertRepo() biz.CertRepo { return &certRepo{} } -func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) { +func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) { var certs []*biz.Cert var total int64 err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error - return certs, total, err + + var list []*types.CertList + for cert := range slices.Values(certs) { + item := &types.CertList{ + ID: cert.ID, + AccountID: cert.AccountID, + WebsiteID: cert.WebsiteID, + DNSID: cert.DNSID, + Type: cert.Type, + Domains: cert.Domains, + AutoRenew: cert.AutoRenew, + Cert: cert.Cert, + Key: cert.Key, + CreatedAt: cert.CreatedAt, + UpdatedAt: cert.UpdatedAt, + } + if decode, err := pkgcert.ParseCert(cert.Cert); err == nil { + item.NotBefore = decode.NotBefore + item.NotAfter = decode.NotAfter + item.Issuer = decode.Issuer.CommonName + item.OCSPServer = decode.OCSPServer + item.DNSNames = decode.DNSNames + } + list = append(list, item) + } + + return list, total, err } func (r *certRepo) Get(id uint) (*biz.Cert, error) { @@ -43,6 +72,25 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) { return cert, err } +func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) { + info, err := pkgcert.ParseCert(req.Cert) + if err != nil { + return nil, err + } + + cert := &biz.Cert{ + Type: "upload", + Domains: info.DNSNames, + Cert: req.Cert, + Key: req.Key, + } + if err = app.Orm.Create(cert).Error; err != nil { + return nil, err + } + + return cert, nil +} + func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) { cert := &biz.Cert{ AccountID: req.AccountID, @@ -59,12 +107,22 @@ func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) { } func (r *certRepo) Update(req *request.CertUpdate) error { + info, err := pkgcert.ParseCert(req.Cert) + if err != nil { + return err + } + if req.Type == "upload" { + req.Domains = info.DNSNames + } + return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{ ID: req.ID, AccountID: req.AccountID, WebsiteID: req.WebsiteID, DNSID: req.DNSID, Type: req.Type, + Cert: req.Cert, + Key: req.Key, Domains: req.Domains, AutoRenew: req.AutoRenew, }).Error diff --git a/internal/data/website.go b/internal/data/website.go index e09e0339..148f6fa2 100644 --- a/internal/data/website.go +++ b/internal/data/website.go @@ -7,6 +7,7 @@ import ( "path/filepath" "slices" "strings" + "time" "github.com/samber/lo" "github.com/spf13/cast" @@ -137,8 +138,8 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { setting.SSLCertificateKey = key // 解析证书信息 if decode, err := cert.ParseCert(crt); err == nil { - setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") - setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05") + setting.SSLNotBefore = decode.NotBefore.Format(time.DateTime) + setting.SSLNotAfter = decode.NotAfter.Format(time.DateTime) setting.SSLIssuer = decode.Issuer.CommonName setting.SSLOCSPServer = decode.OCSPServer setting.SSLDNSNames = decode.DNSNames diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go index 9a36460e..c69e1a0e 100644 --- a/internal/http/request/cert.go +++ b/internal/http/request/cert.go @@ -1,5 +1,10 @@ package request +type CertUpload struct { + Cert string `form:"cert" json:"cert" validate:"required"` + Key string `form:"key" json:"key" validate:"required"` +} + type CertCreate struct { Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"` Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"` @@ -11,8 +16,10 @@ type CertCreate struct { type CertUpdate struct { ID uint `form:"id" json:"id" validate:"required,exists=certs id"` - Type string `form:"type" json:"type" validate:"required,oneof=P256 P384 2048 3072 4096"` + Type string `form:"type" json:"type" validate:"required,oneof=upload P256 P384 2048 3072 4096"` Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"` + Cert string `form:"cert" json:"cert" validate:"required"` + Key string `form:"key" json:"key" validate:"required"` AutoRenew bool `form:"auto_renew" json:"auto_renew"` AccountID uint `form:"account_id" json:"account_id"` DNSID uint `form:"dns_id" json:"dns_id"` diff --git a/internal/job/cert_renew.go b/internal/job/cert_renew.go index 18433664..280d16fa 100644 --- a/internal/job/cert_renew.go +++ b/internal/job/cert_renew.go @@ -34,7 +34,7 @@ func (r *CertRenew) Run() { } for _, cert := range certs { - if !cert.AutoRenew { + if cert.Type == "upload" || !cert.AutoRenew { continue } diff --git a/internal/job/monitoring.go b/internal/job/monitoring.go index ca3c0ba1..89146b62 100644 --- a/internal/job/monitoring.go +++ b/internal/job/monitoring.go @@ -61,7 +61,7 @@ func (r *Monitoring) Run() { if day <= 0 || app.Status != app.StatusNormal { return } - if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format("2006-01-02 15:04:05")).Delete(&biz.Monitor{}).Error; err != nil { + if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format(time.DateTime)).Delete(&biz.Monitor{}).Error; err != nil { app.Logger.Error("删除过期系统监控失败", zap.Error(err)) return } diff --git a/internal/route/http.go b/internal/route/http.go index 275752ba..161ee787 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -82,6 +82,7 @@ func Http(r chi.Router) { r.Route("/cert", func(r chi.Router) { r.Get("/", cert.List) r.Post("/", cert.Create) + r.Post("/upload", cert.Upload) r.Put("/{id}", cert.Update) r.Get("/{id}", cert.Get) r.Delete("/{id}", cert.Delete) diff --git a/internal/service/cert.go b/internal/service/cert.go index cc176e44..05c82aca 100644 --- a/internal/service/cert.go +++ b/internal/service/cert.go @@ -115,6 +115,22 @@ func (s *CertService) List(w http.ResponseWriter, r *http.Request) { }) } +func (s *CertService) Upload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertUpload](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + cert, err := s.certRepo.Upload(req) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, cert) +} + func (s *CertService) Create(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.CertCreate](r) if err != nil { diff --git a/internal/service/cli.go b/internal/service/cli.go index 22a14686..5afb2665 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -168,7 +168,7 @@ func (s *CliService) UserList(ctx context.Context, cmd *cli.Command) error { } for _, user := range users { - fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s, 创建日期: %s\n", user.ID, user.Username, user.Email, user.CreatedAt.Format(time.DateTime)) } return nil diff --git a/pkg/ntp/ntp.go b/pkg/ntp/ntp.go index 337955d7..1f7807d1 100644 --- a/pkg/ntp/ntp.go +++ b/pkg/ntp/ntp.go @@ -46,13 +46,13 @@ func Now(address ...string) (time.Time, error) { return now, nil } -func UpdateSystemTime(time time.Time) error { - _, err := shell.Execf(`date -s '%s'`, time.Format("2006-01-02 15:04:05")) +func UpdateSystemTime(t time.Time) error { + _, err := shell.Execf(`date -s '%s'`, t.Format(time.DateTime)) return err } -func UpdateSystemTimeZone(timezone string) error { - _, err := shell.Execf(`timedatectl set-timezone '%s'`, timezone) +func UpdateSystemTimeZone(tz string) error { + _, err := shell.Execf(`timedatectl set-timezone '%s'`, tz) return err } diff --git a/pkg/types/cert.go b/pkg/types/cert.go new file mode 100644 index 00000000..3d012345 --- /dev/null +++ b/pkg/types/cert.go @@ -0,0 +1,22 @@ +package types + +import "time" + +type CertList struct { + ID uint `json:"id"` + AccountID uint `json:"account_id"` + WebsiteID uint `json:"website_id"` + DNSID uint `json:"dns_id"` + Type string `json:"type"` + Domains []string `json:"domains"` + AutoRenew bool `json:"auto_renew"` + Cert string `json:"cert"` + Key string `json:"key"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + Issuer string `json:"issuer"` + OCSPServer []string `json:"ocsp_server"` + DNSNames []string `json:"dns_names"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/web/src/api/panel/cert/index.ts b/web/src/api/panel/cert/index.ts index 244358f1..ebe7906c 100644 --- a/web/src/api/panel/cert/index.ts +++ b/web/src/api/panel/cert/index.ts @@ -38,6 +38,8 @@ export default { request.get('/cert/cert', { params: { page, limit } }), // 证书详情 certInfo: (id: number): Promise> => request.get(`/cert/cert/${id}`), + // 证书上传 + certUpload: (data: any): Promise> => request.post('/cert/cert/upload', data), // 证书添加 certCreate: (data: any): Promise> => request.post('/cert/cert', data), // 证书更新 diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss index 513e658f..c2fa5ac2 100644 --- a/web/src/styles/index.scss +++ b/web/src/styles/index.scss @@ -8,7 +8,10 @@ body { height: 100%; overflow: hidden; background-color: #f2f2f2; - font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;; + font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, + 'Liberation Sans', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Noto Sans SC', + 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei', + 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif; } #app { diff --git a/web/src/views/cert/CertView.vue b/web/src/views/cert/CertView.vue index afdf1fde..9bc904bc 100644 --- a/web/src/views/cert/CertView.vue +++ b/web/src/views/cert/CertView.vue @@ -4,37 +4,39 @@ import type { MessageReactive } from 'naive-ui' import { NButton, NDataTable, NFlex, NPopconfirm, NSpace, NSwitch, NTable, NTag } from 'naive-ui' import cert from '@/api/panel/cert' +import { formatDateTime } from '@/utils' import type { Cert } from '@/views/cert/types' const props = defineProps({ algorithms: Array, websites: Array, accounts: Array, - dns: Array, - caProviders: Array + dns: Array }) -const { algorithms, websites, accounts, dns, caProviders } = toRefs(props) +const { algorithms, websites, accounts, dns } = toRefs(props) let messageReactive: MessageReactive | null = null -const updateCertModel = ref({ +const updateModel = ref({ domains: [], type: 'P256', dns_id: null, account_id: null, website_id: null, - auto_renew: true -}) -const updateCertModal = ref(false) -const updateCert = ref() -const showModal = ref(false) -const showCertModel = ref({ + auto_renew: true, cert: '', key: '' }) -const deployCertModal = ref(false) -const deployCertModel = ref({ +const updateModal = ref(false) +const updateCert = ref() +const showModal = ref(false) +const showModel = ref({ + cert: '', + key: '' +}) +const deployModal = ref(false) +const deployModel = ref({ id: null, websites: [] }) @@ -43,7 +45,7 @@ const columns: any = [ { title: '域名', key: 'domains', - minWidth: 200, + minWidth: 150, resizable: true, ellipsis: { tooltip: true }, render(row: any) { @@ -53,7 +55,12 @@ const columns: any = [ type: row.status == 'active' ? 'success' : 'error' }, { - default: () => row.domains.join(', ') + default: () => { + if (row.domains == null || row.domains.length == 0) { + return '无' + } + return row.domains.join(', ') + } } ) } @@ -83,7 +90,7 @@ const columns: any = [ case '4096': return 'RSA 4096' default: - return '未知' + return '上传' } } } @@ -93,7 +100,7 @@ const columns: any = [ { title: '关联账号', key: 'account_id', - minWidth: 400, + minWidth: 200, resizable: true, ellipsis: { tooltip: true }, render(row: any) { @@ -102,51 +109,44 @@ const columns: any = [ } return h(NFlex, null, { default: () => [ - h(NTag, null, { default: () => (row.account?.email == null ? '无' : row.account.email) }), h(NTag, null, { default: () => - caProviders?.value?.find((item: any) => item.value === row.account?.ca)?.label + row.account_id == 0 + ? '无' + : accounts?.value?.find((item: any) => item.value === row.account_id)?.label }) ] }) } }, { - title: '关联网站', - key: 'website_id', - minWidth: 150, + title: '颁发者', + key: 'issuer', + minWidth: 100, resizable: true, - ellipsis: { tooltip: true }, render(row: any) { return h( NTag, { - type: row.website == null ? 'error' : 'success', + type: 'info', bordered: false }, { - default: () => (row.website?.name == null ? '无' : row.website.name) + default: () => { + return row.issuer == '' ? '无' : row.issuer + } } ) } }, { - title: '关联DNS', - key: 'dns_id', - width: 150, + title: '过期时间', + key: 'not_after', + width: 200, resizable: true, ellipsis: { tooltip: true }, render(row: any) { - return h( - NTag, - { - type: row.dns == null ? 'error' : 'success', - bordered: false - }, - { - default: () => (row.dns?.name == null ? '无' : row.dns.name) - } - ) + return formatDateTime(row.not_after) } }, { @@ -258,11 +258,11 @@ const columns: any = [ size: 'small', type: 'info', onClick: () => { - deployCertModel.value.id = row.id + deployModel.value.id = row.id if (row.website_id != 0) { - deployCertModel.value.websites.push(row.website_id) + deployModel.value.websites.push(row.website_id) } - deployCertModal.value = true + deployModal.value = true } }, { @@ -270,7 +270,7 @@ const columns: any = [ } ) : null, - row.cert != '' && row.key != '' + row.cert != '' && row.key != '' && row.type != 'upload' ? h( NButton, { @@ -300,8 +300,8 @@ const columns: any = [ type: 'tertiary', style: 'margin-left: 15px;', onClick: () => { - showCertModel.value.cert = row.cert - showCertModel.value.key = row.key + showModel.value.cert = row.cert + showModel.value.key = row.key showModal.value = true } }, @@ -318,13 +318,15 @@ const columns: any = [ style: 'margin-left: 15px;', onClick: () => { updateCert.value = row.id - updateCertModel.value.domains = row.domains - updateCertModel.value.type = row.type - updateCertModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id - updateCertModel.value.account_id = row.account_id == 0 ? null : row.account_id - updateCertModel.value.website_id = row.website_id == 0 ? null : row.website_id - updateCertModel.value.auto_renew = row.auto_renew - updateCertModal.value = true + updateModel.value.domains = row.domains + updateModel.value.type = row.type + updateModel.value.dns_id = row.dns_id == 0 ? null : row.dns_id + updateModel.value.account_id = row.account_id == 0 ? null : row.account_id + updateModel.value.website_id = row.website_id == 0 ? null : row.website_id + updateModel.value.auto_renew = row.auto_renew + updateModel.value.cert = row.cert + updateModel.value.key = row.key + updateModal.value = true } }, { @@ -395,31 +397,33 @@ const getCertList = async (page: number, limit: number) => { } const handleUpdateCert = async () => { - await cert.certUpdate(updateCert.value, updateCertModel.value) + await cert.certUpdate(updateCert.value, updateModel.value) window.$message.success('更新成功') - updateCertModal.value = false + updateModal.value = false onPageChange(1) - updateCertModel.value.domains = [] - updateCertModel.value.type = 'P256' - updateCertModel.value.dns_id = null - updateCertModel.value.account_id = null - updateCertModel.value.website_id = null - updateCertModel.value.auto_renew = true + updateModel.value.domains = [] + updateModel.value.type = 'P256' + updateModel.value.dns_id = null + updateModel.value.account_id = null + updateModel.value.website_id = null + updateModel.value.auto_renew = true + updateModel.value.cert = '' + updateModel.value.key = '' } const handleDeployCert = async () => { - for (const website of deployCertModel.value.websites) { - await cert.deploy(deployCertModel.value.id, website) + for (const website of deployModel.value.websites) { + await cert.deploy(deployModel.value.id, website) } window.$message.success('部署成功') - deployCertModal.value = false - deployCertModel.value.id = null - deployCertModel.value.websites = [] + deployModal.value = false + deployModel.value.id = null + deployModel.value.websites = [] } const handleShowModalClose = () => { - showCertModel.value.cert = '' - showCertModel.value.key = '' + showModel.value.cert = '' + showModel.value.key = '' } onMounted(() => { @@ -450,7 +454,7 @@ onUnmounted(() => { /> { 可以通过选择网站 / DNS 中的任意一项来自动签发和部署证书,也可以手动输入域名并设置 DNS 解析来签发证书 - - + + - + { - + - + + + + + + + 提交 { :segmented="false" > - + { { +import UploadCertModal from '@/views/cert/UploadCertModal.vue' + defineOptions({ name: 'cert-index' }) @@ -17,8 +19,9 @@ import DnsView from '@/views/cert/DnsView.vue' const currentTab = ref('cert') -const createDNS = ref(false) +const uploadCert = ref(false) const createCert = ref(false) +const createDNS = ref(false) const createAccount = ref(false) const algorithms = ref([]) @@ -83,28 +86,28 @@ onUnmounted(() => {