diff --git a/.env b/.env new file mode 100644 index 0000000..7ee97e3 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PORT=8080 +ADMIN_USER=admin diff --git a/autossl b/autossl new file mode 100755 index 0000000..cbd6dc9 Binary files /dev/null and b/autossl differ diff --git a/backend/config/credential_store.go b/backend/config/credential_store.go index 0e52118..e85e483 100644 --- a/backend/config/credential_store.go +++ b/backend/config/credential_store.go @@ -35,7 +35,9 @@ func CredentialTypeMeta(ctype CredentialType) []CredentialField { } case CredentialCloudflare: return []CredentialField{ - {Key: "cf_api_token", Label: "API Token", Type: "password", Required: true}, + {Key: "cf_api_token", Label: "API Token", Type: "password", Required: false}, + {Key: "cf_email", Label: "Email (Global API Key 方式)", Type: "text", Required: false}, + {Key: "cf_api_key", Label: "Global API Key", Type: "password", Required: false}, } case CredentialDNSPod: return []CredentialField{ diff --git a/backend/handlers/credential.go b/backend/handlers/credential.go index 6ea2554..164e8b7 100644 --- a/backend/handlers/credential.go +++ b/backend/handlers/credential.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" ) @@ -67,6 +68,11 @@ func (h *CredentialHandler) CreateCredential(c *gin.Context) { return } + // Trim whitespace from all values + for k, v := range testData { + testData[k] = strings.TrimSpace(v) + } + // Validate fields for the provider type fields := config.CredentialTypeMeta(req.Type) for _, f := range fields { @@ -78,10 +84,23 @@ func (h *CredentialHandler) CreateCredential(c *gin.Context) { } } + // Cloudflare: require either API Token or Email+Key + if req.Type == config.CredentialCloudflare { + hasToken := testData["cf_api_token"] != "" + hasEmailKey := testData["cf_email"] != "" && testData["cf_api_key"] != "" + if !hasToken && !hasEmailKey { + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 API Token,或者同时填写 Email 和 Global API Key"}) + return + } + } + + // Re-serialize trimmed data + trimmedData, _ := json.Marshal(testData) + cred := &config.Credential{ Name: req.Name, Type: req.Type, - Data: req.Data, + Data: string(trimmedData), } if err := config.CredStore.Create(cred); err != nil { @@ -122,16 +141,28 @@ func (h *CredentialHandler) UpdateCredential(c *gin.Context) { existing.Type = req.Type } if req.Data != "" { - // If it's the masked placeholder, keep existing data - if req.Data == "********" { - existing.Data = existing.Data - } else { + // If it's the masked placeholder, keep existing data unchanged + if req.Data != "********" { var testData map[string]string if err := json.Unmarshal([]byte(req.Data), &testData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "data must be valid JSON"}) return } - existing.Data = req.Data + // Trim whitespace from all values + for k, v := range testData { + testData[k] = strings.TrimSpace(v) + } + // Cloudflare: require either API Token or Email+Key + if existing.Type == config.CredentialCloudflare { + hasToken := testData["cf_api_token"] != "" + hasEmailKey := testData["cf_email"] != "" && testData["cf_api_key"] != "" + if !hasToken && !hasEmailKey { + c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 API Token,或者同时填写 Email 和 Global API Key"}) + return + } + } + trimmedData, _ := json.Marshal(testData) + existing.Data = string(trimmedData) } } diff --git a/backend/services/acme.go b/backend/services/acme.go index 82ff489..6410cb7 100644 --- a/backend/services/acme.go +++ b/backend/services/acme.go @@ -52,6 +52,8 @@ type DNSConfig struct { // Cloudflare CFAPIToken string `json:"cf_api_token,omitempty"` + CFEmail string `json:"cf_email,omitempty"` + CFAPIKey string `json:"cf_api_key,omitempty"` // DNSPod DNSPodID string `json:"dnspod_id,omitempty"` @@ -84,8 +86,8 @@ func GetACMECertificate(cert *config.Certificate, cfg *config.Config) error { // Use custom DNS timeout for better reliability if err := client.Challenge.SetDNS01Provider(provider, - dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"8.8.8.8:53", "1.1.1.1:53"})), - dns01.AddDNSTimeout(15*time.Second), + dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"223.5.5.5:53", "119.29.29.29:53", "8.8.8.8:53"})), + dns01.AddDNSTimeout(120*time.Second), ); err != nil { return fmt.Errorf("failed to set DNS-01 provider: %v", err) } @@ -155,8 +157,8 @@ func RenewCertificate(cert *config.Certificate, cfg *config.Config) error { // Use custom DNS timeout for better reliability if err := client.Challenge.SetDNS01Provider(provider, - dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"8.8.8.8:53", "1.1.1.1:53"})), - dns01.AddDNSTimeout(15*time.Second), + dns01.AddRecursiveNameservers(dns01.ParseNameservers([]string{"223.5.5.5:53", "119.29.29.29:53", "8.8.8.8:53"})), + dns01.AddDNSTimeout(120*time.Second), ); err != nil { return fmt.Errorf("failed to set DNS-01 provider: %v", err) } @@ -327,8 +329,14 @@ func getDNSProvider(cert *config.Certificate) (challenge.Provider, error) { case "cloudflare": cfg := cloudflareprov.NewDefaultConfig() - if dnsCfg.CFAPIToken != "" { - cfg.AuthToken = dnsCfg.CFAPIToken + token := strings.TrimSpace(dnsCfg.CFAPIToken) + if token != "" { + cfg.AuthToken = token + } + // Also support email + Global API Key auth + if dnsCfg.CFEmail != "" && dnsCfg.CFAPIKey != "" { + cfg.AuthEmail = strings.TrimSpace(dnsCfg.CFEmail) + cfg.AuthKey = strings.TrimSpace(dnsCfg.CFAPIKey) } provider, err := cloudflareprov.NewDNSProviderConfig(cfg) if err != nil { diff --git a/data/accounts/717192502_at_qq_dot_com/account.key b/data/accounts/717192502_at_qq_dot_com/account.key new file mode 100644 index 0000000..128137d --- /dev/null +++ b/data/accounts/717192502_at_qq_dot_com/account.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAvBPT8bw9RlsWdzY+Ga/bZcaGoRC1o9KQylxwMt2xpwoAoGCCqGSM49 +AwEHoUQDQgAEtsNoIrvUk9RE8/tXhzlGTqOZH2oQjxrrlyvnVV7wj2YWo2Q5diNp +oL1EDmDWVBMBO6enEgGkU3OtNyqCmTFBKA== +-----END EC PRIVATE KEY----- diff --git a/data/accounts/717192502_at_qq_dot_com/registration.json b/data/accounts/717192502_at_qq_dot_com/registration.json new file mode 100644 index 0000000..51e041c --- /dev/null +++ b/data/accounts/717192502_at_qq_dot_com/registration.json @@ -0,0 +1,6 @@ +{ + "body": { + "status": "valid" + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/3565382145" +} \ No newline at end of file diff --git a/data/accounts/a_at_qq_dot_com/account.key b/data/accounts/a_at_qq_dot_com/account.key new file mode 100644 index 0000000..5dd41ee --- /dev/null +++ b/data/accounts/a_at_qq_dot_com/account.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIF9G4xzfZxxH/8Zp+EabrGlDlH8s5ftlLFXqd5di4wIYoAoGCCqGSM49 +AwEHoUQDQgAEo4RIUksl7FCK3+QnlsUSWEBSWuVPwWWsqGeQxXdGu8qqiNGtypXE +ESu0EWaXjQcbZE476xH67DDmtHsEcz0q8g== +-----END EC PRIVATE KEY----- diff --git a/data/accounts/a_at_qq_dot_com/registration.json b/data/accounts/a_at_qq_dot_com/registration.json new file mode 100644 index 0000000..942543b --- /dev/null +++ b/data/accounts/a_at_qq_dot_com/registration.json @@ -0,0 +1,6 @@ +{ + "body": { + "status": "valid" + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/3565389605" +} \ No newline at end of file diff --git a/data/certs.json b/data/certs.json new file mode 100644 index 0000000..eb92cde --- /dev/null +++ b/data/certs.json @@ -0,0 +1,36 @@ +[ + { + "id": 3, + "created_at": "2026-07-25T00:06:36.635806194+08:00", + "updated_at": "2026-07-25T00:08:43.014181606+08:00", + "domain": "harbor.yunwei.blog", + "email": "a@qq.com", + "provider": "letsencrypt", + "challenge_type": "dns", + "dns_provider": "cloudflare", + "dns_config": "{\"cf_api_token\":\"cfut_VLxUPTOTAWBwTmY8WM4uuZYQW0iztWSdXEWhQYB348f7e25a\"}", + "credential_id": 1, + "status": "error", + "error_message": "failed to obtain certificate: error: one or more domains had a problem:\n[harbor.yunwei.blog] propagation: time limit exceeded: last error: authoritative nameservers: NS blakely.ns.cloudflare.com.:53 did not return the expected TXT record [fqdn: _acme-challenge.harbor.yunwei.blog., value: iLtFsQrgk5mSpKlWk69-KKxf0kBHCUK4PivtBRC-vNI]: \n", + "auto_renew": true, + "renew_days": 30 + }, + { + "id": 1, + "created_at": "2026-07-24T23:10:52.210274514+08:00", + "updated_at": "2026-07-25T00:02:51.1655217+08:00", + "domain": "*.k8s.yunwei.blog", + "email": "a@qq.com", + "provider": "letsencrypt", + "challenge_type": "dns", + "dns_provider": "cloudflare", + "dns_config": "{\"cf_api_key\":\"6eabf25cb487d39f166c3ff4dc3145b5c5fb3\",\"cf_email\":\"mrtom0506@gmail.com\"}", + "credential_id": 1, + "status": "active", + "cert_url": "https://acme-v02.api.letsencrypt.org/acme/cert/06f366c19b0ae2a6bfdedfbfcdecbc5a0f4a", + "expires_at": "2026-10-22T15:04:18Z", + "last_renewed_at": "2026-07-25T00:02:51.165423851+08:00", + "auto_renew": true, + "renew_days": 30 + } +] \ No newline at end of file diff --git a/data/certs/wildcard__k8s_yunwei_blog/chain.pem b/data/certs/wildcard__k8s_yunwei_blog/chain.pem new file mode 100644 index 0000000..1d23c4b --- /dev/null +++ b/data/certs/wildcard__k8s_yunwei_blog/chain.pem @@ -0,0 +1,64 @@ + +-----BEGIN CERTIFICATE----- +MIIE2jCCAsKgAwIBAgIQTr0klH4k05SALYSlL9WzGTANBgkqhkiG9w0BAQsFADAu +MQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjAe +Fw0yNTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZUjIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDZ0LxwBppqh84luqMerV/eeL/fXQ7mLQQv1Lnp +WKZbyvGpx6wh6AfnslAnF6ewTkcHA+gSOoBvm3Dfm06AuGiF+KRut4fAcowqnAQQ +CW98+QPP/eOv/wug7Iyk4NkOxf2I6g2f55T6nJoOTLFcukeRq80JGQEYan+dPFr9 +OGUgQK2hGKgNkW87pappsOAuUJcroYhRt5uUis4qaZireiseu32gzDJNBAiKtsvd +6HX4v25bpkRNcS/B/Gtc9kVbUpD+2PLPxdei3Tim55k4tfAEXwD2qyiPTxrTNq6l +N+AMr5g2c1dNqkOTwjxeV6L5lpP1rGiYvLnRaPlOqyZRPW+5AgMBAAGjge4wgesw +DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdEwEB/wQI +MAYBAf8CAQAwHQYDVR0OBBYEFEAVLSZ57TIgnt+ach3WMh+BDIEMMB8GA1UdIwQY +MBaAFN7nW2DQIm1AKH0/DQH+pLVStFGUMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEF +BQcwAoYWaHR0cDovL3lyLmkubGVuY3Iub3JnLzATBgNVHSAEDDAKMAgGBmeBDAEC +ATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veXIuYy5sZW5jci5vcmcvMA0GCSqG +SIb3DQEBCwUAA4ICAQB0ZUQWZ9/Yn9COEpo+JfecMnB0h0vwDm/M66IqXqw3LoaL +mx9lZvRTeDIS67PUeI3yCA2W6PKRD0/FE/G57lOmS+Xy5AaaL00ICGOqjNcCaMWW +8o8nevHOd4i4lqgtznE/28QwlcdJyF8yBiWHpnyjhEpmNWJURgOCOg2xpwRMBCsj +MScqYPtOhBeuYQvSwAEeTML2Ukh6uGuX4E14q65Ja8cdjF5bAldnP1eE4FBaAwsZ +G2fOqqrKV03Y85Nw2btedP1AtliQuJZs/Jo/gXxXdc7LrH3McgnpnbTiAncX7yES +hP6kzQejllqMCIt52HOjxDGWafS7Xw+DKwqmH+Eqy8dcbOuag/1AYlQoKNVK3F5q +Hh6tEDiMqQcLIibGKteE6iHo4A/bIScbzrhXUYuism42ZYzmc48FMVIH3qy4L84E +TdAH2gtxw0PAhvRVXp8HP7wfngpzsN/8xOTpeRSbM4+Qbc56G6+Bifmv6sk1ieQb +NA3wJdl4DDUuQSV8hBgx6zoI1ZSGORprDFux7c6rhc77QZMSRrEgomBeklervEve +86ylWmZ3WWHV6RLMi8xNvjd71r4EPIGgY7BZU/VPBkq+uA7Gb6mbJnFgV43uh3xy +LRFgxIAphIukwTGSMZZR+AI+Qnp0BYTWovHXozOf3H8r6hozEoT02JHn0AeTfA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/data/certs/wildcard__k8s_yunwei_blog/fullchain.pem b/data/certs/wildcard__k8s_yunwei_blog/fullchain.pem new file mode 100644 index 0000000..a17d367 --- /dev/null +++ b/data/certs/wildcard__k8s_yunwei_blog/fullchain.pem @@ -0,0 +1,93 @@ +-----BEGIN CERTIFICATE----- +MIIE/TCCA+WgAwIBAgISBvNmwZsK4qa/3t+/zey8Wg9KMA0GCSqGSIb3DQEBCwUA +MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD +EwNZUjIwHhcNMjYwNzI0MTUwNDE5WhcNMjYxMDIyMTUwNDE4WjAcMRowGAYDVQQD +DBEqLms4cy55dW53ZWkuYmxvZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANMWZF5U34pLTRMceClLMSZhlGasXb6/25sO6aRUJ8gPIT/pKydq2f2tyXTd +GSD7XljUYJLDHzlEKQykzfFmVqwGoEEMLiycwZvynnIqhG+xKb3v7cKC6ZYRu8HJ +kzfn654owO7zUAPgBJj0OZxKz7xNf4ezclzX4F6rY+rg0L/B7U8hHSwuoyJclSvX +8bdy/AQ0TGS3+DPU6JKO1teub3yJtgh+s/37Pag99msL6nR/0Yy/aAdUn/9g8jLz +QotNIFNy4kD8D209Z2BmUHkssQUt+9Q9QShfO09M/fCRUeq5WU5OPpDku7cBS83X +/nfE6G4oL1+ho2a7xVwfz4bk9LcCAwEAAaOCAiAwggIcMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRi +t3Gdgnf4n0DVXGx/A6TPpo5W/DAfBgNVHSMEGDAWgBRAFS0mee0yIJ7fmnId1jIf +gQyBDDAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAKGF2h0dHA6Ly95cjIuaS5s +ZW5jci5vcmcvMBwGA1UdEQQVMBOCESouazhzLnl1bndlaS5ibG9nMBMGA1UdIAQM +MAowCAYGZ4EMAQIBMC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly95cjIuYy5sZW5j +ci5vcmcvNzUuY3JsMIIBDQYKKwYBBAHWeQIEAgSB/gSB+wD5AHYA1219ENGn9XfC +x+lf1wC/+YLJM1pl4dCzAXMXwMjFaXcAAAGflN14PgAABAMARzBFAiAvOKW3dS0K +rOjEFyssS6V75ZIQzDFFu7VhUXuvpI/iUAIhAIq95wCyjIan2CFVVGMbeVsIITtF +MIG7yKUO/pFJoufvAH8AbP5QGUOoXqkWvFLRM+TcyR7xQRx9JYQg0XOAnhgY6zoA +AAGflN15ewAIAAAFABc0C34EAwBIMEYCIQCuzaieKwTF6J6fImxKxDk7Rt/xxDGM +sBTDNxke3FM0XgIhAPZgnXt1Y1ikndEY77Rf13C5iN1iIVcr0YrQxO6wmVIDMA0G +CSqGSIb3DQEBCwUAA4IBAQBDXJJa77KhUucsvKuMm+GKSJb5uDQIlh7JEiD/Hijx +2Nor+Yz2SlE4feht5Zt7cDRjacm51SjjySFJ4hqntZGHpCsNYF+VJaE93fFSSVVd +YjqG22XWYcXImChoJ3jseTwLff5m8Pm8umA2Z/TnvpwiQZf08ccLxQ3Qdb5W4GDL +9qzDrkRT1PL1M0WdNeWc+0wRlmYq12dJirn6mGxTQedyfQMqhchX8iGkZ33KiNd/ +kcgbURVttvw60hEjPbcURz2Rwl9dZz0qGPdG9bTpnpPtyBHNolNMfRRm5SuPDqQh +X1Wyfhmu3yj2P6SokmVH+lXaMd1vGbZhvLvz1TUou5iG +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIE2jCCAsKgAwIBAgIQTr0klH4k05SALYSlL9WzGTANBgkqhkiG9w0BAQsFADAu +MQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjAe +Fw0yNTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZUjIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDZ0LxwBppqh84luqMerV/eeL/fXQ7mLQQv1Lnp +WKZbyvGpx6wh6AfnslAnF6ewTkcHA+gSOoBvm3Dfm06AuGiF+KRut4fAcowqnAQQ +CW98+QPP/eOv/wug7Iyk4NkOxf2I6g2f55T6nJoOTLFcukeRq80JGQEYan+dPFr9 +OGUgQK2hGKgNkW87pappsOAuUJcroYhRt5uUis4qaZireiseu32gzDJNBAiKtsvd +6HX4v25bpkRNcS/B/Gtc9kVbUpD+2PLPxdei3Tim55k4tfAEXwD2qyiPTxrTNq6l +N+AMr5g2c1dNqkOTwjxeV6L5lpP1rGiYvLnRaPlOqyZRPW+5AgMBAAGjge4wgesw +DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdEwEB/wQI +MAYBAf8CAQAwHQYDVR0OBBYEFEAVLSZ57TIgnt+ach3WMh+BDIEMMB8GA1UdIwQY +MBaAFN7nW2DQIm1AKH0/DQH+pLVStFGUMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEF +BQcwAoYWaHR0cDovL3lyLmkubGVuY3Iub3JnLzATBgNVHSAEDDAKMAgGBmeBDAEC +ATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veXIuYy5sZW5jci5vcmcvMA0GCSqG +SIb3DQEBCwUAA4ICAQB0ZUQWZ9/Yn9COEpo+JfecMnB0h0vwDm/M66IqXqw3LoaL +mx9lZvRTeDIS67PUeI3yCA2W6PKRD0/FE/G57lOmS+Xy5AaaL00ICGOqjNcCaMWW +8o8nevHOd4i4lqgtznE/28QwlcdJyF8yBiWHpnyjhEpmNWJURgOCOg2xpwRMBCsj +MScqYPtOhBeuYQvSwAEeTML2Ukh6uGuX4E14q65Ja8cdjF5bAldnP1eE4FBaAwsZ +G2fOqqrKV03Y85Nw2btedP1AtliQuJZs/Jo/gXxXdc7LrH3McgnpnbTiAncX7yES +hP6kzQejllqMCIt52HOjxDGWafS7Xw+DKwqmH+Eqy8dcbOuag/1AYlQoKNVK3F5q +Hh6tEDiMqQcLIibGKteE6iHo4A/bIScbzrhXUYuism42ZYzmc48FMVIH3qy4L84E +TdAH2gtxw0PAhvRVXp8HP7wfngpzsN/8xOTpeRSbM4+Qbc56G6+Bifmv6sk1ieQb +NA3wJdl4DDUuQSV8hBgx6zoI1ZSGORprDFux7c6rhc77QZMSRrEgomBeklervEve +86ylWmZ3WWHV6RLMi8xNvjd71r4EPIGgY7BZU/VPBkq+uA7Gb6mbJnFgV43uh3xy +LRFgxIAphIukwTGSMZZR+AI+Qnp0BYTWovHXozOf3H8r6hozEoT02JHn0AeTfA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/data/certs/wildcard__k8s_yunwei_blog/privkey.pem b/data/certs/wildcard__k8s_yunwei_blog/privkey.pem new file mode 100644 index 0000000..5291571 --- /dev/null +++ b/data/certs/wildcard__k8s_yunwei_blog/privkey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0xZkXlTfiktNExx4KUsxJmGUZqxdvr/bmw7ppFQnyA8hP+kr +J2rZ/a3JdN0ZIPteWNRgksMfOUQpDKTN8WZWrAagQQwuLJzBm/KeciqEb7Epve/t +woLplhG7wcmTN+frnijA7vNQA+AEmPQ5nErPvE1/h7NyXNfgXqtj6uDQv8HtTyEd +LC6jIlyVK9fxt3L8BDRMZLf4M9Toko7W165vfIm2CH6z/fs9qD32awvqdH/RjL9o +B1Sf/2DyMvNCi00gU3LiQPwPbT1nYGZQeSyxBS371D1BKF87T0z98JFR6rlZTk4+ +kOS7twFLzdf+d8TobigvX6GjZrvFXB/PhuT0twIDAQABAoIBABTfEe2B1tiFuYk6 +N2dd1+5Dl7jPMjW6IqyiPNfSulw9orv/AhVTq1o55MF5WxK3yAxgc2wPfvbalnR5 +lyNXSxqXTSEhYy9mEH6q7p+7NH0Wt1ngb1HcP9xmxHnUxImylMl+nhUCWdcIP2wg +OdIKU5/6ybFRPO2RKs4xhB+lOv66sAsZzMjLl94ufKg9CC/yXxaoP00wDi/aIUPR +SLrf7f5pOipeYTtKeahFlYe4tgtTABTCyJi6sh/lhznFFkHav3a10n9cl1DLoMsv +pjq62ZsNLlZBoca+jvCquWI2MDUvr9eMWhydX/wkha8QPyRlJb7nUbB6mXKFBRb3 +KuHXuoECgYEA8ZbzhllP81+O7HgfC5yTyh0NrvST7vKMRFjHiR3OY93Zv2G6AH/o +jd9wLorgZdu1N4wkJPlpnif2/gAtZP6D2tODI9sC71fW3ZYt3F1pZHQfcf0h+qRi +TO4I+rL9ZngtrPgE7NlpV2lqC1Sjog4l/L9UB9wHWL1+TFj3KGCHoRsCgYEA362s +9isIYP6aB+IdYr9uuC7rmCqio32i/Mz083zaAZJkZ6ex6XiNwBxFVZJ9x3Cco88w +iqim8Z7a0XXrylAQpYVqo6U4EkfaJkf9nMjqUWPo36t+MpbwEIs8B8lJ3lspf83n +Rz+4NSba0HNIBuVXuZZK9yqflqBziFdgNGJPkJUCgYA1PcofYpFvFjEeRf65hWFA +mpSIaRgtAcEN0u7udeYlNiAlwDjIoWez1HkZmLuJVxkhy72vdFPlafNR2OIoLrsK +P7DX9w03iAkvPkiPFvsCC2FesqWJYi7Tu2W5MjcMHRIb23Dibat8exvGsXl6Xlna +0KOJM+Femiw6USTu/g8F3wKBgQCSfWL+YZZ+N6BXM149ctQnags/wAR+AGVDHVP7 +NzRrvR1myWv6VMq5G4rJDUS03mqn2aSX2vZi3b+/WHRRLniIfsUlYapERqx3T8Sn +7umO3lkAS4c9yJ52p0lsbAalypx2KDqk7fVNUiISTL3KTl9SmzyDfXXBJ1oi296U +0CH9tQKBgA4dX9/5GoWWZ3/5dRny3lb4z8AQQFoCj2fTKIN5usNUzsAOUMZp+p24 +h/BzmUtcGUlKNPwg4LYjndIuK4Eembuyaiif5OdaDWWePacsP5pudIYlibXUyZqb +Ad3K4WIp0pVjzqt7HGXwDluBS3y1YApX/kky1o3b4vxoh8me8AX4 +-----END RSA PRIVATE KEY----- diff --git a/data/credentials.json b/data/credentials.json new file mode 100644 index 0000000..dc65ce9 --- /dev/null +++ b/data/credentials.json @@ -0,0 +1,20 @@ +[ + { + "id": 1, + "name": "cloudflare_yunweiblog", + "type": "cloudflare", + "data": "{\"cf_api_token\":\"cfut_VLxUPTOTAWBwTmY8WM4uuZYQW0iztWSdXEWhQYB348f7e25a\"}", + "masked": "{\"cf_api_token\":\"cfut****\"}", + "created_at": "2026-07-24T23:08:11.019112775+08:00", + "updated_at": "2026-07-24T23:08:11.019112775+08:00" + }, + { + "id": 3, + "name": "cloudflare_globalcloudflare_global", + "type": "cloudflare", + "data": "{\"cf_api_key\":\"6eabf25cb487d39f166c3ff4dc3145b5c5fb3\",\"cf_email\":\"mrtom0506@gmail.com\"}", + "masked": "{\"cf_api_key\":\"6eab****\",\"cf_email\":\"mrto****\"}", + "created_at": "2026-07-24T23:52:19.826553941+08:00", + "updated_at": "2026-07-24T23:52:19.826553941+08:00" + } +] \ No newline at end of file diff --git a/dist b/dist new file mode 120000 index 0000000..0ff66ac --- /dev/null +++ b/dist @@ -0,0 +1 @@ +frontend/dist \ No newline at end of file diff --git a/frontend/src/views/CertCreate.vue b/frontend/src/views/CertCreate.vue index 6043b74..66a5842 100644 --- a/frontend/src/views/CertCreate.vue +++ b/frontend/src/views/CertCreate.vue @@ -81,7 +81,14 @@ @@ -162,6 +169,8 @@ const manualConfig = reactive({ ali_key: '', ali_secret: '', cf_api_token: '', + cf_email: '', + cf_api_key: '', dnspod_id: '', dnspod_key: '', }) @@ -220,6 +229,8 @@ const buildDnsConfigJson = (): string => { case 'cloudflare': return JSON.stringify({ cf_api_token: manualConfig.cf_api_token, + cf_email: manualConfig.cf_email, + cf_api_key: manualConfig.cf_api_key, }) case 'dnspod': return JSON.stringify({ @@ -246,9 +257,13 @@ const handleSubmit = async () => { ElMessage.warning('请填写阿里云 AccessKey') return } - if (form.dns_provider === 'cloudflare' && !manualConfig.cf_api_token) { - ElMessage.warning('请填写 Cloudflare API Token') - return + if (form.dns_provider === 'cloudflare') { + const hasToken = !!manualConfig.cf_api_token + const hasEmailKey = !!manualConfig.cf_email && !!manualConfig.cf_api_key + if (!hasToken && !hasEmailKey) { + ElMessage.warning('请填写 Cloudflare API Token,或者同时填写 Email 和 Global API Key') + return + } } if (form.dns_provider === 'dnspod' && (!manualConfig.dnspod_id || !manualConfig.dnspod_key)) { ElMessage.warning('请填写 DNSPod 凭证')