mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
Merge pull request #421 from fudiwei/feat/new-workflow
feat: enhance workflow
This commit is contained in:
commit
0d3e426dff
2
go.mod
2
go.mod
@ -39,7 +39,7 @@ require (
|
||||
github.com/volcengine/volc-sdk-golang v1.0.189
|
||||
github.com/volcengine/volcengine-go-sdk v1.0.177
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
||||
k8s.io/api v0.32.0
|
||||
k8s.io/apimachinery v0.32.0
|
||||
k8s.io/client-go v0.32.0
|
||||
|
4
go.sum
4
go.sum
@ -957,8 +957,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
@ -13,30 +13,30 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultExpireSubject = "您有 ${COUNT} 张证书即将过期"
|
||||
defaultExpireSubject = "有 ${COUNT} 张证书即将过期"
|
||||
defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!"
|
||||
)
|
||||
|
||||
type CertificateRepository interface {
|
||||
ListExpireSoon(ctx context.Context) ([]domain.Certificate, error)
|
||||
type certificateRepository interface {
|
||||
ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error)
|
||||
}
|
||||
|
||||
type certificateService struct {
|
||||
repo CertificateRepository
|
||||
type CertificateService struct {
|
||||
repo certificateRepository
|
||||
}
|
||||
|
||||
func NewCertificateService(repo CertificateRepository) *certificateService {
|
||||
return &certificateService{
|
||||
func NewCertificateService(repo certificateRepository) *CertificateService {
|
||||
return &CertificateService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *certificateService) InitSchedule(ctx context.Context) error {
|
||||
func (s *CertificateService) InitSchedule(ctx context.Context) error {
|
||||
scheduler := app.GetScheduler()
|
||||
err := scheduler.Add("certificate", "0 0 * * *", func() {
|
||||
certs, err := s.repo.ListExpireSoon(context.Background())
|
||||
if err != nil {
|
||||
app.GetLogger().Error("failed to get expire soon certificate", "err", err)
|
||||
app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ func (s *certificateService) InitSchedule(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil {
|
||||
app.GetLogger().Error("failed to send expire soon certificate", "err", err)
|
||||
app.GetLogger().Error("failed to send notification", "err", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
@ -58,13 +58,11 @@ func (s *certificateService) InitSchedule(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type certificateNotification struct {
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func buildExpireSoonNotification(records []domain.Certificate) *certificateNotification {
|
||||
if len(records) == 0 {
|
||||
func buildExpireSoonNotification(certificates []*domain.Certificate) *struct {
|
||||
Subject string
|
||||
Message string
|
||||
} {
|
||||
if len(certificates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -85,9 +83,9 @@ func buildExpireSoonNotification(records []domain.Certificate) *certificateNotif
|
||||
}
|
||||
|
||||
// 替换变量
|
||||
count := len(records)
|
||||
count := len(certificates)
|
||||
domains := make([]string, count)
|
||||
for i, record := range records {
|
||||
for i, record := range certificates {
|
||||
domains[i] = record.SubjectAltNames
|
||||
}
|
||||
countStr := strconv.Itoa(count)
|
||||
@ -98,8 +96,8 @@ func buildExpireSoonNotification(records []domain.Certificate) *certificateNotif
|
||||
message = strings.ReplaceAll(message, "${DOMAINS}", domainStr)
|
||||
|
||||
// 返回消息
|
||||
return &certificateNotification{
|
||||
Subject: subject,
|
||||
Message: message,
|
||||
}
|
||||
return &struct {
|
||||
Subject string
|
||||
Message string
|
||||
}{Subject: subject, Message: message}
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
|
||||
type Access struct {
|
||||
Meta
|
||||
Name string `json:"name" db:"name"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
Config string `json:"config" db:"config"`
|
||||
Usage string `json:"usage" db:"usage"`
|
||||
DeletedAt time.Time `json:"deleted" db:"deleted"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
Config string `json:"config" db:"config"`
|
||||
Usage string `json:"usage" db:"usage"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
||||
func (a *Access) UnmarshalConfigToMap() (map[string]any, error) {
|
||||
|
@ -23,4 +23,5 @@ type Certificate struct {
|
||||
WorkflowId string `json:"workflowId" db:"workflowId"`
|
||||
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
|
||||
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
|
||||
DeletedAt *time.Time `json:"deleted" db:"deleted"`
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
package domain
|
||||
|
||||
var (
|
||||
ErrInvalidParams = NewXError(400, "invalid params")
|
||||
ErrRecordNotFound = NewXError(404, "record not found")
|
||||
)
|
||||
|
||||
func IsRecordNotFound(err error) bool {
|
||||
if e, ok := err.(*XError); ok {
|
||||
return e.GetCode() == ErrRecordNotFound.GetCode()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type XError struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewXError(code int, msg string) *XError {
|
||||
return &XError{code, msg}
|
||||
}
|
||||
|
||||
func (e *XError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func (e *XError) GetCode() int {
|
||||
if e.Code == 0 {
|
||||
return 100
|
||||
}
|
||||
return e.Code
|
||||
}
|
30
internal/domain/error.go
Normal file
30
internal/domain/error.go
Normal file
@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
var (
|
||||
ErrInvalidParams = NewError(400, "invalid params")
|
||||
ErrRecordNotFound = NewError(404, "record not found")
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func NewError(code int, msg string) *Error {
|
||||
if code == 0 {
|
||||
code = -1
|
||||
}
|
||||
|
||||
return &Error{code, msg}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func IsRecordNotFoundError(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Code == ErrRecordNotFound.Code
|
||||
}
|
||||
return false
|
||||
}
|
@ -12,15 +12,15 @@ const (
|
||||
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
|
||||
)
|
||||
|
||||
type SettingsRepository interface {
|
||||
type settingsRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
|
||||
type NotifyService struct {
|
||||
settingRepo SettingsRepository
|
||||
settingRepo settingsRepository
|
||||
}
|
||||
|
||||
func NewNotifyService(settingRepo SettingsRepository) *NotifyService {
|
||||
func NewNotifyService(settingRepo settingsRepository) *NotifyService {
|
||||
return &NotifyService{
|
||||
settingRepo: settingRepo,
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem s
|
||||
if d.config.PreCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
return nil, xerrors.Wrapf(err, "failed to execute pre-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("pre-command executed", stdout)
|
||||
@ -132,7 +132,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem s
|
||||
if d.config.PostCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
return nil, xerrors.Wrapf(err, "failed to execute post-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("post-command executed", stdout)
|
||||
@ -154,7 +154,7 @@ func execCommand(shellEnv ShellEnvType, command string) (string, string, error)
|
||||
case SHELL_ENV_POWERSHELL:
|
||||
cmd = exec.Command("powershell", "-Command", command)
|
||||
|
||||
case "":
|
||||
case ShellEnvType(""):
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
} else {
|
||||
@ -165,14 +165,13 @@ func execCommand(shellEnv ShellEnvType, command string) (string, string, error)
|
||||
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
stdoutBuf := bytes.NewBuffer(nil)
|
||||
cmd.Stdout = stdoutBuf
|
||||
stderrBuf := bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderrBuf
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", "", xerrors.Wrap(err, "failed to execute shell command")
|
||||
return stdoutBuf.String(), stderrBuf.String(), xerrors.Wrap(err, "failed to execute command")
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
|
@ -20,6 +20,9 @@ var (
|
||||
fJksAlias string
|
||||
fJksKeypass string
|
||||
fJksStorepass string
|
||||
fShellEnv string
|
||||
fPreCommand string
|
||||
fPostCommand string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -33,6 +36,9 @@ func init() {
|
||||
flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "")
|
||||
flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "")
|
||||
flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "")
|
||||
flag.StringVar(&fShellEnv, argsPrefix+"SHELLENV", "", "")
|
||||
flag.StringVar(&fPreCommand, argsPrefix+"PRECOMMAND", "", "")
|
||||
flag.StringVar(&fPostCommand, argsPrefix+"POSTCOMMAND", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
@ -46,7 +52,10 @@ Shell command to run this test:
|
||||
--CERTIMATE_DEPLOYER_LOCAL_PFXPASSWORD="your-pfx-password" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSALIAS="your-jks-alias" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSKEYPASS="your-jks-keypass" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass"
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_SHELLENV="sh" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_PRECOMMAND="echo 'hello world'" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_POSTCOMMAND="echo 'bye-bye world'"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
@ -58,11 +67,18 @@ func TestDeploy(t *testing.T) {
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
fmt.Sprintf("SHELLENV: %v", fShellEnv),
|
||||
fmt.Sprintf("PRECOMMAND: %v", fPreCommand),
|
||||
fmt.Sprintf("POSTCOMMAND: %v", fPostCommand),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
OutputFormat: provider.OUTPUT_FORMAT_PEM,
|
||||
OutputCertPath: fOutputCertPath + ".pem",
|
||||
OutputKeyPath: fOutputKeyPath + ".pem",
|
||||
ShellEnv: provider.ShellEnvType(fShellEnv),
|
||||
PreCommand: fPreCommand,
|
||||
PostCommand: fPostCommand,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
@ -77,7 +93,7 @@ func TestDeploy(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
fstat1, err := os.Stat(fOutputCertPath)
|
||||
fstat1, err := os.Stat(fOutputCertPath + ".pem")
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
@ -86,7 +102,7 @@ func TestDeploy(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
fstat2, err := os.Stat(fOutputKeyPath)
|
||||
fstat2, err := os.Stat(fOutputKeyPath + ".pem")
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
@ -104,14 +120,12 @@ func TestDeploy(t *testing.T) {
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputFormat: provider.OUTPUT_FORMAT_PFX,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
OutputCertPath: fOutputCertPath + ".pfx",
|
||||
PfxPassword: fPfxPassword,
|
||||
})
|
||||
if err != nil {
|
||||
@ -127,7 +141,7 @@ func TestDeploy(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := os.Stat(fOutputCertPath)
|
||||
fstat, err := os.Stat(fOutputCertPath + ".pfx")
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
@ -145,7 +159,6 @@ func TestDeploy(t *testing.T) {
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
fmt.Sprintf("JKSALIAS: %v", fJksAlias),
|
||||
fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass),
|
||||
fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass),
|
||||
@ -153,8 +166,7 @@ func TestDeploy(t *testing.T) {
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputFormat: provider.OUTPUT_FORMAT_JKS,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
OutputCertPath: fOutputCertPath + ".jks",
|
||||
JksAlias: fJksAlias,
|
||||
JksKeypass: fJksKeypass,
|
||||
JksStorepass: fJksStorepass,
|
||||
@ -172,7 +184,7 @@ func TestDeploy(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := os.Stat(fOutputCertPath)
|
||||
fstat, err := os.Stat(fOutputCertPath + ".jks")
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
|
@ -103,7 +103,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str
|
||||
if d.config.PreCommand != "" {
|
||||
stdout, stderr, err := execSshCommand(client, d.config.PreCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
|
||||
return nil, xerrors.Wrapf(err, "failed to execute pre-command: stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("SSH pre-command executed", stdout)
|
||||
@ -160,7 +160,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str
|
||||
if d.config.PostCommand != "" {
|
||||
stdout, stderr, err := execSshCommand(client, d.config.PostCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
return nil, xerrors.Wrapf(err, "failed to execute post-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("SSH post-command executed", stdout)
|
||||
@ -211,13 +211,13 @@ func execSshCommand(sshCli *ssh.Client, command string) (string, string, error)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
stdoutBuf := bytes.NewBuffer(nil)
|
||||
session.Stdout = stdoutBuf
|
||||
stderrBuf := bytes.NewBuffer(nil)
|
||||
session.Stderr = stderrBuf
|
||||
err = session.Run(command)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return stdoutBuf.String(), stderrBuf.String(), xerrors.Wrap(err, "failed to execute ssh command")
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
|
@ -69,6 +69,7 @@ func TestDeploy(t *testing.T) {
|
||||
SshPort: int32(fSshPort),
|
||||
SshUsername: fSshUsername,
|
||||
SshPassword: fSshPassword,
|
||||
OutputFormat: provider.OUTPUT_FORMAT_PEM,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
})
|
||||
|
25
internal/pkg/utils/types/types.go
Normal file
25
internal/pkg/utils/types/types.go
Normal file
@ -0,0 +1,25 @@
|
||||
package types
|
||||
|
||||
import "reflect"
|
||||
|
||||
// 判断对象是否为 nil。
|
||||
//
|
||||
// 入参:
|
||||
// - value:待判断的对象。
|
||||
//
|
||||
// 出参:
|
||||
// - 如果对象值为 nil,则返回 true;否则返回 false。
|
||||
func IsNil(obj any) bool {
|
||||
if obj == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(obj)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
return v.IsNil()
|
||||
} else if v.Kind() == reflect.Interface {
|
||||
return v.Elem().IsNil()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/usual2970/certimate/internal/app"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
@ -15,7 +17,7 @@ func NewAccessRepository() *AccessRepository {
|
||||
return &AccessRepository{}
|
||||
}
|
||||
|
||||
func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) {
|
||||
func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) {
|
||||
record, err := app.GetApp().Dao().FindRecordById("access", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@ -24,6 +26,18 @@ func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Acce
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !record.GetDateTime("deleted").Time().IsZero() {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return r.castRecordToModel(record)
|
||||
}
|
||||
|
||||
func (r *AccessRepository) castRecordToModel(record *models.Record) (*domain.Access, error) {
|
||||
if record == nil {
|
||||
return nil, fmt.Errorf("record is nil")
|
||||
}
|
||||
|
||||
access := &domain.Access{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
|
@ -22,7 +22,11 @@ var g singleflight.Group
|
||||
|
||||
func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) {
|
||||
resp, err, _ := g.Do(fmt.Sprintf("acme_account_%s_%s", ca, email), func() (interface{}, error) {
|
||||
resp, err := app.GetApp().Dao().FindFirstRecordByFilter("acme_accounts", "ca={:ca} && email={:email}", dbx.Params{"ca": ca, "email": email})
|
||||
resp, err := app.GetApp().Dao().FindFirstRecordByFilter(
|
||||
"acme_accounts",
|
||||
"ca={:ca} && email={:email}",
|
||||
dbx.Params{"ca": ca, "email": email},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -33,30 +37,15 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("acme account not found")
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
record, ok := resp.(*models.Record)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("acme account not found")
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
resource := ®istration.Resource{}
|
||||
if err := record.UnmarshalJSONField("resource", resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.AcmeAccount{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
CreatedAt: record.GetCreated().Time(),
|
||||
UpdatedAt: record.GetUpdated().Time(),
|
||||
},
|
||||
CA: record.GetString("ca"),
|
||||
Email: record.GetString("email"),
|
||||
Key: record.GetString("key"),
|
||||
Resource: resource,
|
||||
}, nil
|
||||
return r.castRecordToModel(record)
|
||||
}
|
||||
|
||||
func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error {
|
||||
@ -72,3 +61,27 @@ func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registrati
|
||||
record.Set("resource", resource)
|
||||
return app.GetApp().Dao().Save(record)
|
||||
}
|
||||
|
||||
func (r *AcmeAccountRepository) castRecordToModel(record *models.Record) (*domain.AcmeAccount, error) {
|
||||
if record == nil {
|
||||
return nil, fmt.Errorf("record is nil")
|
||||
}
|
||||
|
||||
resource := ®istration.Resource{}
|
||||
if err := record.UnmarshalJSONField("resource", resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acmeAccount := &domain.AcmeAccount{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
CreatedAt: record.GetCreated().Time(),
|
||||
UpdatedAt: record.GetUpdated().Time(),
|
||||
},
|
||||
CA: record.GetString("ca"),
|
||||
Email: record.GetString("email"),
|
||||
Key: record.GetString("key"),
|
||||
Resource: resource,
|
||||
}
|
||||
return acmeAccount, nil
|
||||
}
|
||||
|
@ -2,7 +2,12 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/usual2970/certimate/internal/app"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
@ -13,14 +18,89 @@ func NewCertificateRepository() *CertificateRepository {
|
||||
return &CertificateRepository{}
|
||||
}
|
||||
|
||||
func (c *CertificateRepository) ListExpireSoon(ctx context.Context) ([]domain.Certificate, error) {
|
||||
certificates := []domain.Certificate{}
|
||||
err := app.GetApp().Dao().DB().
|
||||
NewQuery("SELECT * FROM certificate WHERE expireAt > DATETIME('now') AND expireAt < DATETIME('now', '+20 days')").
|
||||
All(&certificates)
|
||||
func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter(
|
||||
"certificate",
|
||||
"expireAt>DATETIME('now') && expireAt<DATETIME('now', '+20 days') && deleted=null",
|
||||
"-created",
|
||||
0, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificates := make([]*domain.Certificate, 0)
|
||||
for _, record := range records {
|
||||
certificate, err := r.castRecordToModel(record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificates = append(certificates, certificate)
|
||||
}
|
||||
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
func (r *CertificateRepository) GetById(ctx context.Context, id string) (*domain.Certificate, error) {
|
||||
record, err := app.GetApp().Dao().FindRecordById("certificate", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !record.GetDateTime("deleted").Time().IsZero() {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return r.castRecordToModel(record)
|
||||
}
|
||||
|
||||
func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter(
|
||||
"certificate",
|
||||
"workflowNodeId={:workflowNodeId} && deleted=null",
|
||||
"-created", 1, 0,
|
||||
dbx.Params{"workflowNodeId": workflowNodeId},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return r.castRecordToModel(records[0])
|
||||
}
|
||||
|
||||
func (r *CertificateRepository) castRecordToModel(record *models.Record) (*domain.Certificate, error) {
|
||||
if record == nil {
|
||||
return nil, fmt.Errorf("record is nil")
|
||||
}
|
||||
|
||||
certificate := &domain.Certificate{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
CreatedAt: record.GetCreated().Time(),
|
||||
UpdatedAt: record.GetUpdated().Time(),
|
||||
},
|
||||
Source: domain.CertificateSourceType(record.GetString("source")),
|
||||
SubjectAltNames: record.GetString("subjectAltNames"),
|
||||
Certificate: record.GetString("certificate"),
|
||||
PrivateKey: record.GetString("privateKey"),
|
||||
IssuerCertificate: record.GetString("issuerCertificate"),
|
||||
EffectAt: record.GetDateTime("effectAt").Time(),
|
||||
ExpireAt: record.GetDateTime("expireAt").Time(),
|
||||
ACMECertUrl: record.GetString("acmeCertUrl"),
|
||||
ACMECertStableUrl: record.GetString("acmeCertStableUrl"),
|
||||
WorkflowId: record.GetString("workflowId"),
|
||||
WorkflowNodeId: record.GetString("workflowNodeId"),
|
||||
WorkflowOutputId: record.GetString("workflowOutputId"),
|
||||
}
|
||||
return certificate, nil
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/usual2970/certimate/internal/app"
|
||||
@ -14,9 +16,16 @@ func NewSettingsRepository() *SettingsRepository {
|
||||
return &SettingsRepository{}
|
||||
}
|
||||
|
||||
func (s *SettingsRepository) GetByName(ctx context.Context, name string) (*domain.Settings, error) {
|
||||
record, err := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name={:name}", dbx.Params{"name": name})
|
||||
func (r *SettingsRepository) GetByName(ctx context.Context, name string) (*domain.Settings, error) {
|
||||
record, err := app.GetApp().Dao().FindFirstRecordByFilter(
|
||||
"settings",
|
||||
"name={:name}",
|
||||
dbx.Params{"name": name},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
@ -18,29 +19,44 @@ func NewWorkflowRepository() *WorkflowRepository {
|
||||
return &WorkflowRepository{}
|
||||
}
|
||||
|
||||
func (w *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]domain.Workflow, error) {
|
||||
func (r *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter(
|
||||
"workflow",
|
||||
"enabled={:enabled} && trigger={:trigger}",
|
||||
"-created", 1000, 0, dbx.Params{"enabled": true, "trigger": domain.WorkflowTriggerTypeAuto},
|
||||
"-created",
|
||||
0, 0,
|
||||
dbx.Params{"enabled": true, "trigger": domain.WorkflowTriggerTypeAuto},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs := make([]domain.Workflow, 0)
|
||||
workflows := make([]*domain.Workflow, 0)
|
||||
for _, record := range records {
|
||||
workflow, err := record2Workflow(record)
|
||||
workflow, err := r.castRecordToModel(record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs = append(rs, *workflow)
|
||||
|
||||
workflows = append(workflows, workflow)
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
return workflows, nil
|
||||
}
|
||||
|
||||
func (w *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) error {
|
||||
func (r *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Workflow, error) {
|
||||
record, err := app.GetApp().Dao().FindRecordById("workflow", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.castRecordToModel(record)
|
||||
}
|
||||
|
||||
func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) error {
|
||||
collection, err := app.GetApp().Dao().FindCollectionByNameOrId(workflow.Table())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -73,7 +89,7 @@ func (w *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow
|
||||
return app.GetApp().Dao().SaveRecord(record)
|
||||
}
|
||||
|
||||
func (w *WorkflowRepository) SaveRun(ctx context.Context, run *domain.WorkflowRun) error {
|
||||
func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) error {
|
||||
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("workflow_run")
|
||||
if err != nil {
|
||||
return err
|
||||
@ -81,20 +97,20 @@ func (w *WorkflowRepository) SaveRun(ctx context.Context, run *domain.WorkflowRu
|
||||
|
||||
err = app.GetApp().Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("workflowId", run.WorkflowId)
|
||||
record.Set("trigger", string(run.Trigger))
|
||||
record.Set("status", string(run.Status))
|
||||
record.Set("startedAt", run.StartedAt)
|
||||
record.Set("endedAt", run.EndedAt)
|
||||
record.Set("logs", run.Logs)
|
||||
record.Set("error", run.Error)
|
||||
record.Set("workflowId", workflowRun.WorkflowId)
|
||||
record.Set("trigger", string(workflowRun.Trigger))
|
||||
record.Set("status", string(workflowRun.Status))
|
||||
record.Set("startedAt", workflowRun.StartedAt)
|
||||
record.Set("endedAt", workflowRun.EndedAt)
|
||||
record.Set("logs", workflowRun.Logs)
|
||||
record.Set("error", workflowRun.Error)
|
||||
err = txDao.SaveRecord(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// unable trigger sse using DB()
|
||||
workflowRecord, err := txDao.FindRecordById("workflow", run.WorkflowId)
|
||||
workflowRecord, err := txDao.FindRecordById("workflow", workflowRun.WorkflowId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -112,19 +128,11 @@ func (w *WorkflowRepository) SaveRun(ctx context.Context, run *domain.WorkflowRu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Workflow, error) {
|
||||
record, err := app.GetApp().Dao().FindRecordById("workflow", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
func (r *WorkflowRepository) castRecordToModel(record *models.Record) (*domain.Workflow, error) {
|
||||
if record == nil {
|
||||
return nil, fmt.Errorf("record is nil")
|
||||
}
|
||||
|
||||
return record2Workflow(record)
|
||||
}
|
||||
|
||||
func record2Workflow(record *models.Record) (*domain.Workflow, error) {
|
||||
content := &domain.WorkflowNode{}
|
||||
if err := record.UnmarshalJSONField("content", content); err != nil {
|
||||
return nil, err
|
||||
|
@ -17,8 +17,14 @@ func NewWorkflowOutputRepository() *WorkflowOutputRepository {
|
||||
return &WorkflowOutputRepository{}
|
||||
}
|
||||
|
||||
func (w *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter("workflow_output", "nodeId={:nodeId}", "-created", 1, 0, dbx.Params{"nodeId": nodeId})
|
||||
func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter(
|
||||
"workflow_output",
|
||||
"nodeId={:nodeId}",
|
||||
"-created",
|
||||
1, 0,
|
||||
dbx.Params{"nodeId": nodeId},
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
@ -56,44 +62,8 @@ func (w *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (w *WorkflowOutputRepository) GetCertificateByNodeId(ctx context.Context, nodeId string) (*domain.Certificate, error) {
|
||||
records, err := app.GetApp().Dao().FindRecordsByFilter("certificate", "workflowNodeId={:workflowNodeId}", "-created", 1, 0, dbx.Params{"workflowNodeId": nodeId})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
record := records[0]
|
||||
|
||||
rs := &domain.Certificate{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
CreatedAt: record.GetCreated().Time(),
|
||||
UpdatedAt: record.GetUpdated().Time(),
|
||||
},
|
||||
Source: domain.CertificateSourceType(record.GetString("source")),
|
||||
SubjectAltNames: record.GetString("subjectAltNames"),
|
||||
Certificate: record.GetString("certificate"),
|
||||
PrivateKey: record.GetString("privateKey"),
|
||||
IssuerCertificate: record.GetString("issuerCertificate"),
|
||||
EffectAt: record.GetDateTime("effectAt").Time(),
|
||||
ExpireAt: record.GetDateTime("expireAt").Time(),
|
||||
ACMECertUrl: record.GetString("acmeCertUrl"),
|
||||
ACMECertStableUrl: record.GetString("acmeCertStableUrl"),
|
||||
WorkflowId: record.GetString("workflowId"),
|
||||
WorkflowNodeId: record.GetString("workflowNodeId"),
|
||||
WorkflowOutputId: record.GetString("workflowOutputId"),
|
||||
}
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// 保存节点输出
|
||||
func (w *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error {
|
||||
func (r *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error {
|
||||
var record *models.Record
|
||||
var err error
|
||||
|
||||
|
@ -37,5 +37,5 @@ func (handler *notifyHandler) test(c echo.Context) error {
|
||||
return resp.Err(c, err)
|
||||
}
|
||||
|
||||
return resp.Succ(c, nil)
|
||||
return resp.Ok(c, nil)
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ type Response struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func Succ(e echo.Context, data interface{}) error {
|
||||
func Ok(e echo.Context, data interface{}) error {
|
||||
rs := &Response{
|
||||
Code: 0,
|
||||
Msg: "success",
|
||||
@ -24,10 +24,11 @@ func Succ(e echo.Context, data interface{}) error {
|
||||
}
|
||||
|
||||
func Err(e echo.Context, err error) error {
|
||||
xerr, ok := err.(*domain.XError)
|
||||
code := 100
|
||||
code := 500
|
||||
|
||||
xerr, ok := err.(*domain.Error)
|
||||
if ok {
|
||||
code = xerr.GetCode()
|
||||
code = xerr.Code
|
||||
}
|
||||
|
||||
rs := &Response{
|
||||
|
@ -30,6 +30,6 @@ func (handler *statisticsHandler) get(c echo.Context) error {
|
||||
if statistics, err := handler.service.Get(c.Request().Context()); err != nil {
|
||||
return resp.Err(c, err)
|
||||
} else {
|
||||
return resp.Succ(c, statistics)
|
||||
return resp.Ok(c, statistics)
|
||||
}
|
||||
}
|
||||
|
@ -36,5 +36,5 @@ func (handler *workflowHandler) run(c echo.Context) error {
|
||||
if err := handler.service.Run(c.Request().Context(), req); err != nil {
|
||||
return resp.Err(c, err)
|
||||
}
|
||||
return resp.Succ(c, nil)
|
||||
return resp.Ok(c, nil)
|
||||
}
|
||||
|
@ -6,15 +6,15 @@ import (
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type StatisticsRepository interface {
|
||||
type statisticsRepository interface {
|
||||
Get(ctx context.Context) (*domain.Statistics, error)
|
||||
}
|
||||
|
||||
type StatisticsService struct {
|
||||
repo StatisticsRepository
|
||||
repo statisticsRepository
|
||||
}
|
||||
|
||||
func NewStatisticsService(repo StatisticsRepository) *StatisticsService {
|
||||
func NewStatisticsService(repo statisticsRepository) *StatisticsService {
|
||||
return &StatisticsService{
|
||||
repo: repo,
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
const tableName = "workflow"
|
||||
|
||||
func RegisterEvents() error {
|
||||
func Register() {
|
||||
app := app.GetApp()
|
||||
|
||||
app.OnRecordAfterCreateRequest(tableName).Add(func(e *core.RecordCreateEvent) error {
|
||||
@ -28,8 +28,6 @@ func RegisterEvents() error {
|
||||
app.OnRecordAfterDeleteRequest(tableName).Add(func(e *core.RecordDeleteEvent) error {
|
||||
return delete(e.HttpContext.Request().Context(), e.Record)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func update(ctx context.Context, record *models.Record) error {
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/certs"
|
||||
@ -13,60 +15,45 @@ import (
|
||||
|
||||
type applyNode struct {
|
||||
node *domain.WorkflowNode
|
||||
outputRepo WorkflowOutputRepository
|
||||
*Logger
|
||||
certRepo certificateRepository
|
||||
outputRepo workflowOutputRepository
|
||||
*nodeLogger
|
||||
}
|
||||
|
||||
var validityDuration = time.Hour * 24 * 10
|
||||
|
||||
func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
||||
return &applyNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
nodeLogger: NewNodeLogger(node),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
certRepo: repository.NewCertificateRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowOutputRepository interface {
|
||||
// 查询节点输出
|
||||
GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error)
|
||||
|
||||
// 查询申请节点的证书
|
||||
GetCertificateByNodeId(ctx context.Context, nodeId string) (*domain.Certificate, error)
|
||||
|
||||
// 保存节点输出
|
||||
Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error
|
||||
}
|
||||
|
||||
// 申请节点根据申请类型执行不同的操作
|
||||
func (a *applyNode) Run(ctx context.Context) error {
|
||||
a.AddOutput(ctx, a.node.Name, "开始执行")
|
||||
// 查询是否申请过,已申请过则直接返回
|
||||
// TODO: 先保持和 v0.2 一致,后续增加是否强制申请的参数
|
||||
output, err := a.outputRepo.GetByNodeId(ctx, a.node.Id)
|
||||
if err != nil && !domain.IsRecordNotFound(err) {
|
||||
|
||||
// 查询上次执行结果
|
||||
lastOutput, err := a.outputRepo.GetByNodeId(ctx, a.node.Id)
|
||||
if err != nil && !domain.IsRecordNotFoundError(err) {
|
||||
a.AddOutput(ctx, a.node.Name, "查询申请记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if output != nil && output.Succeeded {
|
||||
lastCertificate, _ := a.outputRepo.GetCertificateByNodeId(ctx, a.node.Id)
|
||||
if lastCertificate != nil {
|
||||
if time.Until(lastCertificate.ExpireAt) > validityDuration {
|
||||
a.AddOutput(ctx, a.node.Name, "已申请过证书,且证书在有效期内")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// 检测是否可以跳过本次执行
|
||||
if skippable, skipReason := a.checkCanSkip(ctx, lastOutput); skippable {
|
||||
a.AddOutput(ctx, a.node.Name, skipReason)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取Applicant
|
||||
// 初始化申请器
|
||||
applicant, err := applicant.NewWithApplyNode(a.node)
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, a.node.Name, "获取申请对象失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 申请
|
||||
// 申请证书
|
||||
applyResult, err := applicant.Apply()
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, a.node.Name, "申请失败", err.Error())
|
||||
@ -74,27 +61,12 @@ func (a *applyNode) Run(ctx context.Context) error {
|
||||
}
|
||||
a.AddOutput(ctx, a.node.Name, "申请成功")
|
||||
|
||||
// 记录申请结果
|
||||
// 保持一个节点只有一个输出
|
||||
outputId := ""
|
||||
if output != nil {
|
||||
outputId = output.Id
|
||||
}
|
||||
output = &domain.WorkflowOutput{
|
||||
Meta: domain.Meta{Id: outputId},
|
||||
WorkflowId: GetWorkflowId(ctx),
|
||||
NodeId: a.node.Id,
|
||||
Node: a.node,
|
||||
Succeeded: true,
|
||||
Outputs: a.node.Outputs,
|
||||
}
|
||||
|
||||
// 解析证书并生成实体
|
||||
certX509, err := certs.ParseCertificateFromPEM(applyResult.CertificateFullChain)
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, a.node.Name, "解析证书失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
certificate := &domain.Certificate{
|
||||
Source: domain.CertificateSourceTypeWorkflow,
|
||||
SubjectAltNames: strings.Join(certX509.DNSNames, ";"),
|
||||
@ -109,7 +81,19 @@ func (a *applyNode) Run(ctx context.Context) error {
|
||||
WorkflowNodeId: a.node.Id,
|
||||
}
|
||||
|
||||
if err := a.outputRepo.Save(ctx, output, certificate, func(id string) error {
|
||||
// 保存执行结果
|
||||
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制
|
||||
currentOutput := &domain.WorkflowOutput{
|
||||
WorkflowId: GetWorkflowId(ctx),
|
||||
NodeId: a.node.Id,
|
||||
Node: a.node,
|
||||
Succeeded: true,
|
||||
Outputs: a.node.Outputs,
|
||||
}
|
||||
if lastOutput != nil {
|
||||
currentOutput.Id = lastOutput.Id
|
||||
}
|
||||
if err := a.outputRepo.Save(ctx, currentOutput, certificate, func(id string) error {
|
||||
if certificate != nil {
|
||||
certificate.WorkflowOutputId = id
|
||||
}
|
||||
@ -119,8 +103,38 @@ func (a *applyNode) Run(ctx context.Context) error {
|
||||
a.AddOutput(ctx, a.node.Name, "保存申请记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
a.AddOutput(ctx, a.node.Name, "保存申请记录成功")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) {
|
||||
const validityDuration = time.Hour * 24 * 10
|
||||
|
||||
// TODO: 可控制是否强制申请
|
||||
if lastOutput != nil && lastOutput.Succeeded {
|
||||
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
||||
if lastOutput.Node.GetConfigString("domains") != a.node.GetConfigString("domains") {
|
||||
return false, "配置项变化:域名"
|
||||
}
|
||||
if lastOutput.Node.GetConfigString("contactEmail") != a.node.GetConfigString("contactEmail") {
|
||||
return false, "配置项变化:联系邮箱"
|
||||
}
|
||||
if lastOutput.Node.GetConfigString("provider") != a.node.GetConfigString("provider") {
|
||||
return false, "配置项变化:DNS 提供商授权"
|
||||
}
|
||||
if !maps.Equal(lastOutput.Node.GetConfigMap("providerConfig"), a.node.GetConfigMap("providerConfig")) {
|
||||
return false, "配置项变化:DNS 提供商参数"
|
||||
}
|
||||
if lastOutput.Node.GetConfigString("keyAlgorithm") != a.node.GetConfigString("keyAlgorithm") {
|
||||
return false, "配置项变化:数字签名算法"
|
||||
}
|
||||
|
||||
lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id)
|
||||
if lastCertificate != nil && time.Until(lastCertificate.ExpireAt) > validityDuration {
|
||||
return true, "已申请过证书,且证书尚未临近过期"
|
||||
}
|
||||
}
|
||||
|
||||
return false, "无历史申请记录"
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ import (
|
||||
|
||||
type conditionNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*Logger
|
||||
*nodeLogger
|
||||
}
|
||||
|
||||
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
|
||||
return &conditionNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
node: node,
|
||||
nodeLogger: NewNodeLogger(node),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,95 +8,109 @@ import (
|
||||
"github.com/usual2970/certimate/internal/deployer"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type deployNode struct {
|
||||
node *domain.WorkflowNode
|
||||
outputRepo WorkflowOutputRepository
|
||||
*Logger
|
||||
certRepo certificateRepository
|
||||
outputRepo workflowOutputRepository
|
||||
*nodeLogger
|
||||
}
|
||||
|
||||
func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
||||
return &deployNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
nodeLogger: NewNodeLogger(node),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
certRepo: repository.NewCertificateRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deployNode) Run(ctx context.Context) error {
|
||||
d.AddOutput(ctx, d.node.Name, "开始执行")
|
||||
// 检查是否部署过(部署过则直接返回,和 v0.2 暂时保持一致)
|
||||
output, err := d.outputRepo.GetByNodeId(ctx, d.node.Id)
|
||||
if err != nil && !domain.IsRecordNotFound(err) {
|
||||
|
||||
// 查询上次执行结果
|
||||
lastOutput, err := d.outputRepo.GetByNodeId(ctx, d.node.Id)
|
||||
if err != nil && !domain.IsRecordNotFoundError(err) {
|
||||
d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
// 获取部署对象
|
||||
// 获取证书
|
||||
certSource := d.node.GetConfigString("certificate")
|
||||
|
||||
// 获取前序节点输出证书
|
||||
certSource := d.node.GetConfigString("certificate")
|
||||
certSourceSlice := strings.Split(certSource, "#")
|
||||
if len(certSourceSlice) != 2 {
|
||||
d.AddOutput(ctx, d.node.Name, "证书来源配置错误", certSource)
|
||||
return fmt.Errorf("证书来源配置错误: %s", certSource)
|
||||
}
|
||||
|
||||
cert, err := d.outputRepo.GetCertificateByNodeId(ctx, certSourceSlice[0])
|
||||
certificate, err := d.certRepo.GetByWorkflowNodeId(ctx, certSourceSlice[0])
|
||||
if err != nil {
|
||||
d.AddOutput(ctx, d.node.Name, "获取证书失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 未部署过,开始部署
|
||||
// 部署过但是证书更新了,重新部署
|
||||
// 部署过且证书未更新,直接返回
|
||||
|
||||
if d.deployed(output) && cert.CreatedAt.Before(output.UpdatedAt) {
|
||||
d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新")
|
||||
// 检测是否可以跳过本次执行
|
||||
if skippable, skipReason := d.checkCanSkip(ctx, lastOutput); skippable {
|
||||
if certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
|
||||
d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新")
|
||||
} else {
|
||||
d.AddOutput(ctx, d.node.Name, skipReason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 初始化部署器
|
||||
deploy, err := deployer.NewWithDeployNode(d.node, struct {
|
||||
Certificate string
|
||||
PrivateKey string
|
||||
}{Certificate: cert.Certificate, PrivateKey: cert.PrivateKey})
|
||||
}{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey})
|
||||
if err != nil {
|
||||
d.AddOutput(ctx, d.node.Name, "获取部署对象失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 部署
|
||||
// 部署证书
|
||||
if err := deploy.Deploy(ctx); err != nil {
|
||||
d.AddOutput(ctx, d.node.Name, "部署失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
d.AddOutput(ctx, d.node.Name, "部署成功")
|
||||
|
||||
// 记录部署结果
|
||||
outputId := ""
|
||||
if output != nil {
|
||||
outputId = output.Id
|
||||
}
|
||||
output = &domain.WorkflowOutput{
|
||||
Meta: domain.Meta{Id: outputId},
|
||||
// 保存执行结果
|
||||
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制
|
||||
currentOutput := &domain.WorkflowOutput{
|
||||
Meta: domain.Meta{},
|
||||
WorkflowId: GetWorkflowId(ctx),
|
||||
NodeId: d.node.Id,
|
||||
Node: d.node,
|
||||
Succeeded: true,
|
||||
}
|
||||
|
||||
if err := d.outputRepo.Save(ctx, output, nil, nil); err != nil {
|
||||
if lastOutput != nil {
|
||||
currentOutput.Id = lastOutput.Id
|
||||
}
|
||||
if err := d.outputRepo.Save(ctx, currentOutput, nil, nil); err != nil {
|
||||
d.AddOutput(ctx, d.node.Name, "保存部署记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
d.AddOutput(ctx, d.node.Name, "保存部署记录成功")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deployNode) deployed(output *domain.WorkflowOutput) bool {
|
||||
return output != nil && output.Succeeded
|
||||
func (d *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) {
|
||||
// TODO: 可控制是否强制部署
|
||||
if lastOutput != nil && lastOutput.Succeeded {
|
||||
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
||||
if lastOutput.Node.GetConfigString("provider") != d.node.GetConfigString("provider") {
|
||||
return false, "配置项变化:主机提供商授权"
|
||||
}
|
||||
if !maps.Equal(lastOutput.Node.GetConfigMap("providerConfig"), d.node.GetConfigMap("providerConfig")) {
|
||||
return false, "配置项变化:主机提供商参数"
|
||||
}
|
||||
|
||||
return true, "已部署过证书"
|
||||
}
|
||||
|
||||
return false, "无历史部署记录"
|
||||
}
|
||||
|
@ -8,20 +8,17 @@ import (
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
)
|
||||
|
||||
type SettingRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
type notifyNode struct {
|
||||
node *domain.WorkflowNode
|
||||
settingRepo SettingRepository
|
||||
*Logger
|
||||
node *domain.WorkflowNode
|
||||
settingsRepo settingRepository
|
||||
*nodeLogger
|
||||
}
|
||||
|
||||
func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
|
||||
return ¬ifyNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
settingRepo: repository.NewSettingsRepository(),
|
||||
node: node,
|
||||
nodeLogger: NewNodeLogger(node),
|
||||
settingsRepo: repository.NewSettingsRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,18 +26,20 @@ func (n *notifyNode) Run(ctx context.Context) error {
|
||||
n.AddOutput(ctx, n.node.Name, "开始执行")
|
||||
|
||||
// 获取通知配置
|
||||
setting, err := n.settingRepo.GetByName(ctx, "notifyChannels")
|
||||
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
|
||||
if err != nil {
|
||||
n.AddOutput(ctx, n.node.Name, "获取通知配置失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
channelConfig, err := setting.GetNotifyChannelConfig(n.node.GetConfigString("channel"))
|
||||
// 获取通知渠道
|
||||
channelConfig, err := settings.GetNotifyChannelConfig(n.node.GetConfigString("channel"))
|
||||
if err != nil {
|
||||
n.AddOutput(ctx, n.node.Name, "获取通知渠道配置失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
if err := notify.SendToChannel(n.node.GetConfigString("subject"),
|
||||
n.node.GetConfigString("message"),
|
||||
n.node.GetConfigString("channel"),
|
||||
@ -49,7 +48,7 @@ func (n *notifyNode) Run(ctx context.Context) error {
|
||||
n.AddOutput(ctx, n.node.Name, "发送通知失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
n.AddOutput(ctx, n.node.Name, "发送通知成功")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -8,18 +8,18 @@ import (
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type NodeProcessor interface {
|
||||
type nodeProcessor interface {
|
||||
Run(ctx context.Context) error
|
||||
Log(ctx context.Context) *domain.WorkflowRunLog
|
||||
AddOutput(ctx context.Context, title, content string, err ...string)
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
type nodeLogger struct {
|
||||
log *domain.WorkflowRunLog
|
||||
}
|
||||
|
||||
func NewLogger(node *domain.WorkflowNode) *Logger {
|
||||
return &Logger{
|
||||
func NewNodeLogger(node *domain.WorkflowNode) *nodeLogger {
|
||||
return &nodeLogger{
|
||||
log: &domain.WorkflowRunLog{
|
||||
NodeId: node.Id,
|
||||
NodeName: node.Name,
|
||||
@ -28,11 +28,11 @@ func NewLogger(node *domain.WorkflowNode) *Logger {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Log(ctx context.Context) *domain.WorkflowRunLog {
|
||||
func (l *nodeLogger) Log(ctx context.Context) *domain.WorkflowRunLog {
|
||||
return l.log
|
||||
}
|
||||
|
||||
func (l *Logger) AddOutput(ctx context.Context, title, content string, err ...string) {
|
||||
func (l *nodeLogger) AddOutput(ctx context.Context, title, content string, err ...string) {
|
||||
output := domain.WorkflowRunLogOutput{
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
Title: title,
|
||||
@ -45,7 +45,7 @@ func (l *Logger) AddOutput(ctx context.Context, title, content string, err ...st
|
||||
l.log.Outputs = append(l.log.Outputs, output)
|
||||
}
|
||||
|
||||
func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
|
||||
func GetProcessor(node *domain.WorkflowNode) (nodeProcessor, error) {
|
||||
switch node.Type {
|
||||
case domain.WorkflowNodeTypeStart:
|
||||
return NewStartNode(node), nil
|
||||
@ -60,3 +60,16 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type certificateRepository interface {
|
||||
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
||||
}
|
||||
|
||||
type workflowOutputRepository interface {
|
||||
GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error)
|
||||
Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error
|
||||
}
|
||||
|
||||
type settingRepository interface {
|
||||
GetByName(ctx context.Context, name string) (*domain.Settings, error)
|
||||
}
|
||||
|
@ -8,21 +8,19 @@ import (
|
||||
|
||||
type startNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*Logger
|
||||
*nodeLogger
|
||||
}
|
||||
|
||||
func NewStartNode(node *domain.WorkflowNode) *startNode {
|
||||
return &startNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
node: node,
|
||||
nodeLogger: NewNodeLogger(node),
|
||||
}
|
||||
}
|
||||
|
||||
// 开始节点没有任何操作
|
||||
func (s *startNode) Run(ctx context.Context) error {
|
||||
s.AddOutput(ctx,
|
||||
s.node.Name,
|
||||
"完成",
|
||||
)
|
||||
// 开始节点没有任何操作
|
||||
s.AddOutput(ctx, s.node.Name, "完成")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -19,21 +19,21 @@ type workflowRunData struct {
|
||||
Options *domain.WorkflowRunReq
|
||||
}
|
||||
|
||||
type WorkflowRepository interface {
|
||||
type workflowRepository interface {
|
||||
ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error)
|
||||
GetById(ctx context.Context, id string) (*domain.Workflow, error)
|
||||
SaveRun(ctx context.Context, run *domain.WorkflowRun) error
|
||||
Save(ctx context.Context, workflow *domain.Workflow) error
|
||||
ListEnabledAuto(ctx context.Context) ([]domain.Workflow, error)
|
||||
SaveRun(ctx context.Context, run *domain.WorkflowRun) error
|
||||
}
|
||||
|
||||
type WorkflowService struct {
|
||||
ch chan *workflowRunData
|
||||
repo WorkflowRepository
|
||||
repo workflowRepository
|
||||
wg sync.WaitGroup
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewWorkflowService(repo WorkflowRepository) *WorkflowService {
|
||||
func NewWorkflowService(repo workflowRepository) *WorkflowService {
|
||||
rs := &WorkflowService{
|
||||
repo: repo,
|
||||
ch: make(chan *workflowRunData, 1),
|
||||
|
25
main.go
25
main.go
@ -25,27 +25,24 @@ import (
|
||||
func main() {
|
||||
app := app.GetApp()
|
||||
|
||||
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
||||
|
||||
// 获取启动命令中的http参数
|
||||
var httpFlag string
|
||||
flag.StringVar(&httpFlag, "http", "127.0.0.1:8090", "HTTP server address")
|
||||
// "serve"影响解析
|
||||
_ = flag.CommandLine.Parse(os.Args[2:])
|
||||
var flagHttp string
|
||||
var flagDir string
|
||||
flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address")
|
||||
flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory")
|
||||
_ = flag.CommandLine.Parse(os.Args[2:]) // skip the first two arguments: "main.go serve"
|
||||
|
||||
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||
// (the isGoRun check is to enable it only during development)
|
||||
Automigrate: isGoRun,
|
||||
Automigrate: strings.HasPrefix(os.Args[0], os.TempDir()),
|
||||
})
|
||||
|
||||
workflow.RegisterEvents()
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
routes.Register(e.Router)
|
||||
|
||||
scheduler.Register()
|
||||
|
||||
workflow.Register()
|
||||
|
||||
routes.Register(e.Router)
|
||||
e.Router.GET(
|
||||
"/*",
|
||||
echo.StaticDirectoryHandler(ui.DistDirFS, false),
|
||||
@ -57,11 +54,13 @@ func main() {
|
||||
|
||||
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||
routes.Unregister()
|
||||
|
||||
log.Println("Exit!")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Printf("Visit the website: http://%s", httpFlag)
|
||||
log.Printf("Visit the website: http://%s", flagHttp)
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
54
migrations/1737019549_updated_certificate.go
Normal file
54
migrations/1737019549_updated_certificate.go
Normal file
@ -0,0 +1,54 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("4szxr9x43tpj6np")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add
|
||||
new_deleted := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "klyf4nlq",
|
||||
"name": "deleted",
|
||||
"type": "date",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
}`), new_deleted); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_deleted)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("4szxr9x43tpj6np")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("klyf4nlq")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
@ -5,7 +5,7 @@ import { getPocketBase } from "@/repository/pocketbase";
|
||||
export const notifyTest = async (channel: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send("/api/notify/test", {
|
||||
const resp = await pb.send<BaseResponse>("/api/notify/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -6,7 +6,7 @@ import { getPocketBase } from "@/repository/pocketbase";
|
||||
export const get = async () => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send("/api/statistics/get", {
|
||||
const resp = await pb.send<BaseResponse<Statistics>>("/api/statistics/get", {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@ -14,5 +14,5 @@ export const get = async () => {
|
||||
throw new ClientResponseError({ status: resp.code, response: resp, data: {} });
|
||||
}
|
||||
|
||||
return resp.data as Statistics;
|
||||
return resp;
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ClientResponseError, type RecordSubscription } from "pocketbase";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowModel } from "@/domain/workflow";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { getPocketBase } from "@/repository/pocketbase";
|
||||
|
||||
export const run = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send("/api/workflow/run", {
|
||||
const resp = await pb.send<BaseResponse>("/api/workflow/run", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -23,15 +23,3 @@ export const run = async (id: string) => {
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").subscribe(id, cb);
|
||||
};
|
||||
|
||||
export const unsubscribe = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").unsubscribe(id);
|
||||
};
|
||||
|
@ -129,7 +129,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
const { loading } = useRequest(
|
||||
const { loading, error: loadedError } = useRequest(
|
||||
() => {
|
||||
return listWorkflowRuns({
|
||||
workflowId: workflowId,
|
||||
@ -139,9 +139,9 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
},
|
||||
{
|
||||
refreshDeps: [workflowId, page, pageSize],
|
||||
onSuccess: (data) => {
|
||||
setTableData(data.items);
|
||||
setTableTotal(data.totalItems);
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
@ -150,6 +150,8 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -164,7 +166,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : undefined} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
|
@ -8,7 +8,7 @@ export interface CertificateModel extends BaseModel {
|
||||
effectAt: ISO8601String;
|
||||
expireAt: ISO8601String;
|
||||
workflowId: string;
|
||||
expand: {
|
||||
expand?: {
|
||||
workflowId?: WorkflowModel; // TODO: ugly, maybe to use an alias?
|
||||
};
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export type NotifyTemplate = {
|
||||
};
|
||||
|
||||
export const defaultNotifyTemplate: NotifyTemplate = {
|
||||
subject: "您有 ${COUNT} 张证书即将过期",
|
||||
subject: "有 ${COUNT} 张证书即将过期",
|
||||
message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!",
|
||||
};
|
||||
// #endregion
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
"certificate.action.view": "View certificate",
|
||||
"certificate.action.delete": "Delete certificate",
|
||||
"certificate.action.delete.confirm": "Are you sure to delete this certificate?",
|
||||
"certificate.action.download": "Download certificate",
|
||||
|
||||
"certificate.props.subject_alt_names": "Name",
|
||||
|
@ -13,6 +13,6 @@
|
||||
"dashboard.quick_actions": "Quick actions",
|
||||
"dashboard.quick_actions.create_workflow": "Create workflow",
|
||||
"dashboard.quick_actions.change_login_password": "Change login password",
|
||||
"dashboard.quick_actions.notification_settings": "Notification settings",
|
||||
"dashboard.quick_actions.certificate_authority_configuration": "Certificate authority configuration"
|
||||
"dashboard.quick_actions.cofigure_notification": "Configure notificaion",
|
||||
"dashboard.quick_actions.configure_ca": "Configure certificate authority"
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
"access.form.name.placeholder": "请输入授权名称",
|
||||
"access.form.provider.label": "提供商",
|
||||
"access.form.provider.placeholder": "请选择提供商",
|
||||
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】您的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】您的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
|
||||
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
|
||||
"access.form.acmehttpreq_endpoint.label": "服务端点",
|
||||
"access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点",
|
||||
"access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://go-acme.github.io/lego/dns/httpreq/\" target=\"_blank\">https://go-acme.github.io/lego/dns/httpreq/</a>",
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
"certificate.action.view": "查看证书",
|
||||
"certificate.action.delete": "删除证书",
|
||||
"certificate.action.delete.confirm": "确定要删除此证书吗?",
|
||||
"certificate.action.download": "下载证书",
|
||||
|
||||
"certificate.props.subject_alt_names": "名称",
|
||||
|
@ -13,7 +13,6 @@
|
||||
"dashboard.quick_actions": "快捷操作",
|
||||
"dashboard.quick_actions.create_workflow": "新建工作流",
|
||||
"dashboard.quick_actions.change_login_password": "修改登录密码",
|
||||
"dashboard.quick_actions.notification_settings": "消息推送设置",
|
||||
"dashboard.quick_actions.certificate_authority_configuration": "证书颁发机构配置"
|
||||
"dashboard.quick_actions.cofigure_notification": "消息推送设置",
|
||||
"dashboard.quick_actions.configure_ca": "证书颁发机构配置"
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
||||
"workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?",
|
||||
"workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未配置",
|
||||
"workflow.detail.orchestration.action.run": "执行",
|
||||
"workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。你确定要以最近一次发布的版本继续执行吗?",
|
||||
"workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?",
|
||||
"workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史",
|
||||
"workflow.detail.runs.tab": "执行历史"
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"workflow_node.action.rename_branch": "重命名",
|
||||
"workflow_node.action.remove_branch": "删除分支",
|
||||
|
||||
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。你确定要关闭面板吗?",
|
||||
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
|
||||
|
||||
"workflow_node.start.label": "开始",
|
||||
"workflow_node.start.form.trigger.label": "触发方式",
|
||||
|
@ -130,7 +130,7 @@ const AccessList = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { loading } = useRequest(
|
||||
const { loading, run: refreshData } = useRequest(
|
||||
() => {
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
@ -142,9 +142,9 @@ const AccessList = () => {
|
||||
},
|
||||
{
|
||||
refreshDeps: [accesses, page, pageSize],
|
||||
onSuccess: (data) => {
|
||||
setTableData(data.items);
|
||||
setTableTotal(data.totalItems);
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -157,6 +157,7 @@ const AccessList = () => {
|
||||
// TODO: 有关联数据的不允许被删除
|
||||
try {
|
||||
await deleteAccess(data);
|
||||
refreshData();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
@ -4,13 +4,13 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Divider, Empty, Menu, type MenuProps, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
|
||||
import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||
import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate";
|
||||
import { type ListCertificateRequest, list as listCertificate } from "@/repository/certificate";
|
||||
import { type ListCertificateRequest, list as listCertificate, remove as removeCertificate } from "@/repository/certificate";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const CertificateList = () => {
|
||||
@ -21,6 +21,7 @@ const CertificateList = () => {
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const [modalApi, ModalContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const tableColumns: TableProps<CertificateModel>["columns"] = [
|
||||
@ -169,14 +170,7 @@ const CertificateList = () => {
|
||||
/>
|
||||
|
||||
<Tooltip title={t("certificate.action.delete")}>
|
||||
<Button
|
||||
color="danger"
|
||||
icon={<DeleteOutlinedIcon />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
alert("TODO: 暂时不支持删除证书");
|
||||
}}
|
||||
/>
|
||||
<Button color="danger" icon={<DeleteOutlinedIcon />} variant="text" onClick={() => handleDeleteClick(record)} />
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
),
|
||||
@ -194,7 +188,11 @@ const CertificateList = () => {
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10);
|
||||
|
||||
const { loading } = useRequest(
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listCertificate({
|
||||
page: page,
|
||||
@ -204,9 +202,9 @@ const CertificateList = () => {
|
||||
},
|
||||
{
|
||||
refreshDeps: [filters, page, pageSize],
|
||||
onSuccess: (data) => {
|
||||
setTableData(data.items);
|
||||
setTableTotal(data.totalItems);
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
@ -215,12 +213,34 @@ const CertificateList = () => {
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleDeleteClick = (certificate: CertificateModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("certificate.action.delete"),
|
||||
content: t("certificate.action.delete.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const resp = await removeCertificate(certificate);
|
||||
if (resp) {
|
||||
setTableData((prev) => prev.filter((item) => item.id !== certificate.id));
|
||||
refreshData();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{ModalContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
<PageHeader title={t("certificate.page.title")} />
|
||||
@ -230,7 +250,7 @@ const CertificateList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("certificate.nodata")} />,
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("certificate.nodata")} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
|
@ -29,31 +29,56 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { get as getStatistics } from "@/api/statistics";
|
||||
import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
|
||||
import { type Statistics } from "@/domain/statistics";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { list as listWorkflowRuns } from "@/repository/workflowRun";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const breakpoints = Grid.useBreakpoint();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const statisticsGridSpans = {
|
||||
xs: { flex: "50%" },
|
||||
md: { flex: "50%" },
|
||||
lg: { flex: "33.3333%" },
|
||||
xl: { flex: "33.3333%" },
|
||||
xxl: { flex: "20%" },
|
||||
};
|
||||
const [statistics, setStatistics] = useState<Statistics>();
|
||||
const { loading: statisticsLoading } = useRequest(
|
||||
() => {
|
||||
return getStatistics();
|
||||
},
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setStatistics(res.data);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
align: "center",
|
||||
fixed: "left",
|
||||
width: 50,
|
||||
render: (_, __, index) => (page - 1) * pageSize + index + 1,
|
||||
render: (_, __, index) => index + 1,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
@ -98,20 +123,6 @@ const Dashboard = () => {
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "trigger",
|
||||
title: t("workflow_run.props.trigger"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
if (record.trigger === WORKFLOW_TRIGGERS.AUTO) {
|
||||
return t("workflow_run.props.trigger.auto");
|
||||
} else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) {
|
||||
return t("workflow_run.props.trigger.manual");
|
||||
}
|
||||
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "startedAt",
|
||||
title: t("workflow_run.props.started_at"),
|
||||
@ -139,7 +150,6 @@ const Dashboard = () => {
|
||||
{
|
||||
key: "$action",
|
||||
align: "end",
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Button.Group>
|
||||
@ -149,24 +159,17 @@ const Dashboard = () => {
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);
|
||||
const [_tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [page, _setPage] = useState<number>(1);
|
||||
const [pageSize, _setPageSize] = useState<number>(3);
|
||||
|
||||
const { loading: loadingWorkflowRun } = useRequest(
|
||||
const { loading: tableLoading } = useRequest(
|
||||
() => {
|
||||
return listWorkflowRuns({
|
||||
page: page,
|
||||
perPage: pageSize,
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
expand: true,
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [page, pageSize],
|
||||
onSuccess: (data) => {
|
||||
setTableData(data.items);
|
||||
setTableTotal(data.totalItems > 3 ? 3 : data.totalItems);
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
@ -175,34 +178,8 @@ const Dashboard = () => {
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const statisticsGridSpans = {
|
||||
xs: { flex: "50%" },
|
||||
md: { flex: "50%" },
|
||||
lg: { flex: "33.3333%" },
|
||||
xl: { flex: "33.3333%" },
|
||||
xxl: { flex: "20%" },
|
||||
};
|
||||
const [statistics, setStatistics] = useState<Statistics>();
|
||||
|
||||
const { loading } = useRequest(
|
||||
() => {
|
||||
return getStatistics();
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setStatistics(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
throw err;
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -218,7 +195,7 @@ const Dashboard = () => {
|
||||
<StatisticCard
|
||||
icon={<SquareSigmaIcon size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_certificates")}
|
||||
loading={loading}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates")}
|
||||
@ -228,7 +205,7 @@ const Dashboard = () => {
|
||||
<StatisticCard
|
||||
icon={<CalendarClockIcon size={48} strokeWidth={1} color={themeToken.colorWarning} />}
|
||||
label={t("dashboard.statistics.expire_soon_certificates")}
|
||||
loading={loading}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpireSoon ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expireSoon")}
|
||||
@ -238,7 +215,7 @@ const Dashboard = () => {
|
||||
<StatisticCard
|
||||
icon={<CalendarX2Icon size={48} strokeWidth={1} color={themeToken.colorError} />}
|
||||
label={t("dashboard.statistics.expired_certificates")}
|
||||
loading={loading}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpired ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expired")}
|
||||
@ -248,7 +225,7 @@ const Dashboard = () => {
|
||||
<StatisticCard
|
||||
icon={<WorkflowIcon size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_workflows")}
|
||||
loading={loading}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows")}
|
||||
@ -258,7 +235,7 @@ const Dashboard = () => {
|
||||
<StatisticCard
|
||||
icon={<FolderCheckIcon size={48} strokeWidth={1} color={themeToken.colorSuccess} />}
|
||||
label={t("dashboard.statistics.enabled_workflows")}
|
||||
loading={loading}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowEnabled ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows?state=enabled")}
|
||||
@ -268,34 +245,32 @@ const Dashboard = () => {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Flex vertical={!screens.md} gap={16}>
|
||||
<Card className="sm:h-full sm:w-[500px] sm:pb-32">
|
||||
<div className="text-lg font-semibold">{t("dashboard.quick_actions")}</div>
|
||||
<div className="mt-9">
|
||||
<Button className="w-full" type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/workflows/new")}>
|
||||
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
|
||||
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
|
||||
<Space className="w-full" direction="vertical" size="large">
|
||||
<Button block type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate("/workflows/new")}>
|
||||
{t("dashboard.quick_actions.create_workflow")}
|
||||
</Button>
|
||||
<Button className="mt-5 w-full" size="large" icon={<LockOutlined />} onClick={() => navigate("/settings/password")}>
|
||||
<Button block size="large" icon={<LockOutlined />} onClick={() => navigate("/settings/password")}>
|
||||
{t("dashboard.quick_actions.change_login_password")}
|
||||
</Button>
|
||||
<Button className="mt-5 w-full" size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}>
|
||||
{t("dashboard.quick_actions.notification_settings")}
|
||||
<Button block size="large" icon={<SendOutlined />} onClick={() => navigate("/settings/notification")}>
|
||||
{t("dashboard.quick_actions.cofigure_notification")}
|
||||
</Button>
|
||||
<Button className="mt-5 w-full" size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}>
|
||||
{t("dashboard.quick_actions.certificate_authority_configuration")}
|
||||
<Button block size="large" icon={<ApiOutlined />} onClick={() => navigate("/settings/ssl-provider")}>
|
||||
{t("dashboard.quick_actions.configure_ca")}
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card className="size-full">
|
||||
<div className="text-lg font-semibold">{t("dashboard.latest_workflow_run")} </div>
|
||||
<Card className="flex-1" title={t("dashboard.latest_workflow_run")}>
|
||||
<Table<WorkflowRunModel>
|
||||
className="mt-5"
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loadingWorkflowRun}
|
||||
loading={tableLoading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey={(record: WorkflowRunModel) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
|
@ -16,7 +16,7 @@ import { createSchemaFieldRule } from "antd-zod";
|
||||
import { isEqual } from "radash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { run as runWorkflow, subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/api/workflow";
|
||||
import { run as runWorkflow } from "@/api/workflow";
|
||||
import ModalForm from "@/components/ModalForm";
|
||||
import Show from "@/components/Show";
|
||||
import WorkflowElements from "@/components/workflow/WorkflowElements";
|
||||
@ -24,7 +24,7 @@ import WorkflowRuns from "@/components/workflow/WorkflowRuns";
|
||||
import { isAllNodesValidated } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
||||
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
||||
import { remove as removeWorkflow } from "@/repository/workflow";
|
||||
import { remove as removeWorkflow, subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/repository/workflow";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
@ -109,7 +109,7 @@ const WorkflowDetail = () => {
|
||||
content: t("workflow.action.delete.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const resp: boolean = await removeWorkflow(workflow);
|
||||
const resp = await removeWorkflow(workflow);
|
||||
if (resp) {
|
||||
navigate("/workflows", { replace: true });
|
||||
}
|
||||
|
@ -240,7 +240,11 @@ const WorkflowList = () => {
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10);
|
||||
|
||||
const { loading } = useRequest(
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listWorkflow({
|
||||
page: page,
|
||||
@ -250,9 +254,9 @@ const WorkflowList = () => {
|
||||
},
|
||||
{
|
||||
refreshDeps: [filters, page, pageSize],
|
||||
onSuccess: (data) => {
|
||||
setTableData(data.items);
|
||||
setTableTotal(data.totalItems);
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
@ -261,6 +265,8 @@ const WorkflowList = () => {
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -302,9 +308,10 @@ const WorkflowList = () => {
|
||||
content: t("workflow.action.delete.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const resp: boolean = await removeWorkflow(workflow);
|
||||
const resp = await removeWorkflow(workflow);
|
||||
if (resp) {
|
||||
setTableData((prev) => prev.filter((item) => item.id !== workflow.id));
|
||||
refreshData();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -341,7 +348,7 @@ const WorkflowList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("workflow.nodata")} />,
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("workflow.nodata")} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
|
@ -30,4 +30,5 @@ export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => {
|
||||
if ("provider" in record && record.provider === "pdns") record.provider = "powerdns";
|
||||
|
||||
await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id!, record);
|
||||
return true;
|
||||
};
|
||||
|
@ -19,17 +19,18 @@ export const list = async (request: ListCertificateRequest) => {
|
||||
const perPage = request.perPage || 10;
|
||||
|
||||
const options: RecordListOptions = {
|
||||
sort: "-created",
|
||||
expand: "workflowId",
|
||||
filter: "deleted=null",
|
||||
sort: "-created",
|
||||
requestKey: null,
|
||||
};
|
||||
|
||||
if (request.state === "expireSoon") {
|
||||
options.filter = pb.filter("expireAt<{:expiredAt}", {
|
||||
expiredAt: dayjs().add(15, "d").toDate(),
|
||||
options.filter = pb.filter("expireAt<{:expiredAt} && deleted=null", {
|
||||
expiredAt: dayjs().add(20, "d").toDate(),
|
||||
});
|
||||
} else if (request.state === "expired") {
|
||||
options.filter = pb.filter("expireAt<={:expiredAt}", {
|
||||
options.filter = pb.filter("expireAt<={:expiredAt} && deleted=null", {
|
||||
expiredAt: new Date(),
|
||||
});
|
||||
}
|
||||
@ -41,4 +42,5 @@ export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) =
|
||||
record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") };
|
||||
|
||||
await getPocketBase().collection(COLLECTION_NAME).update<CertificateModel>(record.id!, record);
|
||||
return true;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type RecordListOptions } from "pocketbase";
|
||||
import { type RecordListOptions, type RecordSubscription } from "pocketbase";
|
||||
|
||||
import { type WorkflowModel } from "@/domain/workflow";
|
||||
import { getPocketBase } from "./pocketbase";
|
||||
@ -48,3 +48,15 @@ export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
|
||||
export const remove = async (record: MaybeModelRecordWithId<WorkflowModel>) => {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id);
|
||||
};
|
||||
|
||||
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").subscribe(id, cb);
|
||||
};
|
||||
|
||||
export const unsubscribe = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").unsubscribe(id);
|
||||
};
|
||||
|
6
ui/types/global.d.ts
vendored
6
ui/types/global.d.ts
vendored
@ -12,6 +12,12 @@ declare global {
|
||||
declare type MaybeModelRecord<T extends BaseModel = BaseModel> = T | Omit<T, "id" | "created" | "updated" | "deleted">;
|
||||
|
||||
declare type MaybeModelRecordWithId<T extends BaseModel = BaseModel> = T | Pick<T, "id">;
|
||||
|
||||
declare interface BaseResponse<T = any> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
Loading…
x
Reference in New Issue
Block a user