diff --git a/openflare_server/controller/tls_certificate.go b/openflare_server/controller/tls_certificate.go index dda205b3..79b02b35 100644 --- a/openflare_server/controller/tls_certificate.go +++ b/openflare_server/controller/tls_certificate.go @@ -334,6 +334,50 @@ func UpdateAcmeCertificate(c *gin.Context) { }) } +// ConvertTLSCertificateToAcme godoc +// @Summary Convert uploaded TLS certificate to ACME managed certificate +// @Tags TLSCertificates +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "Certificate ID" +// @Param payload body service.TLSApplyInput true "TLS apply payload" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]interface{} +// @Router /api/tls-certificates/{id}/convert-acme [post] +func ConvertTLSCertificateToAcme(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "invalid request", + }) + return + } + + var input service.TLSApplyInput + if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "无效的参数", + }) + return + } + certificate, err := service.ConvertTLSCertificateToAcme(uint(id), input) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": certificate, + }) +} + // RenewTLSCertificate godoc // @Summary Renew TLS certificate // @Tags TLSCertificates diff --git a/openflare_server/docs/docs.go b/openflare_server/docs/docs.go index 173ddda8..78e49c4a 100644 --- a/openflare_server/docs/docs.go +++ b/openflare_server/docs/docs.go @@ -326,6 +326,31 @@ const docTemplate = `{ } } }, + "/api/acme-accounts/default": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AcmeAccounts" + ], + "summary": "Get default ACME account", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/agent/apply-logs": { "post": { "security": [ @@ -600,6 +625,49 @@ const docTemplate = `{ } } }, + "/api/config-versions/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ConfigVersions" + ], + "summary": "Cleanup old config versions", + "parameters": [ + { + "description": "Cleanup request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.CleanupConfigVersionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/diff": { "get": { "security": [ @@ -789,6 +857,148 @@ const docTemplate = `{ } } }, + "/api/dns-accounts/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "List DNS accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Create DNS account", + "parameters": [ + { + "description": "DNS account payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.DnsAccountInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/dns-accounts/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Delete DNS account", + "parameters": [ + { + "type": "integer", + "description": "DNS Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/dns-accounts/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Update DNS account", + "parameters": [ + { + "type": "integer", + "description": "DNS Account ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "DNS account payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.DnsAccountInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/managed-domains/": { "get": { "security": [ @@ -1552,6 +1762,47 @@ const docTemplate = `{ } } }, + "/api/option/update-batch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Options" + ], + "summary": "Batch update options", + "parameters": [ + { + "description": "Batch option payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.optionBatchPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/proxy-routes/": { "get": { "security": [ @@ -1621,6 +1872,47 @@ const docTemplate = `{ } } }, + "/api/proxy-routes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ProxyRoutes" + ], + "summary": "Get proxy route detail", + "parameters": [ + { + "type": "integer", + "description": "Route ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/proxy-routes/{id}/delete": { "post": { "security": [ @@ -1804,6 +2096,52 @@ const docTemplate = `{ } } }, + "/api/tls-certificates/apply": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Apply TLS certificate via ACME", + "parameters": [ + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/import-file": { "post": { "security": [ @@ -1950,6 +2288,59 @@ const docTemplate = `{ } } }, + "/api/tls-certificates/{id}/convert-acme": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Convert uploaded TLS certificate to ACME managed certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/{id}/delete": { "post": { "security": [ @@ -1991,6 +2382,47 @@ const docTemplate = `{ } } }, + "/api/tls-certificates/{id}/renew": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Renew TLS certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/{id}/update": { "post": { "security": [ @@ -2044,6 +2476,59 @@ const docTemplate = `{ } } }, + "/api/tls-certificates/{id}/update-acme": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Update ACME TLS certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/update/latest-release": { "get": { "produces": [ @@ -2141,6 +2626,32 @@ const docTemplate = `{ } }, "definitions": { + "controller.CleanupConfigVersionRequest": { + "type": "object", + "required": [ + "keep_count" + ], + "properties": { + "keep_count": { + "type": "integer", + "minimum": 3 + } + } + }, + "controller.DnsAccountInput": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "controller.geoIPLookupRequest": { "type": "object", "properties": { @@ -2152,6 +2663,17 @@ const docTemplate = `{ } } }, + "controller.optionBatchPayload": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Option" + } + } + } + }, "model.Option": { "type": "object", "properties": { @@ -2491,6 +3013,15 @@ const docTemplate = `{ "service.ProxyRouteInput": { "type": "object", "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, "cache_enabled": { "type": "boolean" }, @@ -2506,6 +3037,12 @@ const docTemplate = `{ "cert_id": { "type": "integer" }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, "custom_headers": { "type": "array", "items": { @@ -2515,24 +3052,69 @@ const docTemplate = `{ "domain": { "type": "string" }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, "enable_https": { "type": "boolean" }, "enabled": { "type": "boolean" }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_address": { + "type": "string" + }, "origin_host": { "type": "string" }, + "origin_id": { + "type": "integer" + }, + "origin_port": { + "type": "string" + }, + "origin_scheme": { + "type": "string" + }, + "origin_uri": { + "type": "string" + }, "origin_url": { "type": "string" }, + "pow_config": { + "type": "string" + }, + "pow_enabled": { + "type": "boolean" + }, "redirect_http": { "type": "boolean" }, "remark": { "type": "string" }, + "site_name": { + "type": "string" + }, "upstreams": { "type": "array", "items": { @@ -2541,6 +3123,47 @@ const docTemplate = `{ } } }, + "service.TLSApplyInput": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, "service.TLSCertificateInput": { "type": "object", "properties": { diff --git a/openflare_server/docs/swagger.json b/openflare_server/docs/swagger.json index 0e2b163e..c8acf721 100644 --- a/openflare_server/docs/swagger.json +++ b/openflare_server/docs/swagger.json @@ -323,6 +323,31 @@ } } }, + "/api/acme-accounts/default": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AcmeAccounts" + ], + "summary": "Get default ACME account", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/agent/apply-logs": { "post": { "security": [ @@ -597,6 +622,49 @@ } } }, + "/api/config-versions/cleanup": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ConfigVersions" + ], + "summary": "Cleanup old config versions", + "parameters": [ + { + "description": "Cleanup request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.CleanupConfigVersionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/config-versions/diff": { "get": { "security": [ @@ -786,6 +854,148 @@ } } }, + "/api/dns-accounts/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "List DNS accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Create DNS account", + "parameters": [ + { + "description": "DNS account payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.DnsAccountInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/dns-accounts/{id}/delete": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Delete DNS account", + "parameters": [ + { + "type": "integer", + "description": "DNS Account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/dns-accounts/{id}/update": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DnsAccounts" + ], + "summary": "Update DNS account", + "parameters": [ + { + "type": "integer", + "description": "DNS Account ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "DNS account payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.DnsAccountInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/managed-domains/": { "get": { "security": [ @@ -1549,6 +1759,47 @@ } } }, + "/api/option/update-batch": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Options" + ], + "summary": "Batch update options", + "parameters": [ + { + "description": "Batch option payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.optionBatchPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/proxy-routes/": { "get": { "security": [ @@ -1618,6 +1869,47 @@ } } }, + "/api/proxy-routes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ProxyRoutes" + ], + "summary": "Get proxy route detail", + "parameters": [ + { + "type": "integer", + "description": "Route ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/proxy-routes/{id}/delete": { "post": { "security": [ @@ -1801,6 +2093,52 @@ } } }, + "/api/tls-certificates/apply": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Apply TLS certificate via ACME", + "parameters": [ + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/import-file": { "post": { "security": [ @@ -1947,6 +2285,59 @@ } } }, + "/api/tls-certificates/{id}/convert-acme": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Convert uploaded TLS certificate to ACME managed certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/{id}/delete": { "post": { "security": [ @@ -1988,6 +2379,47 @@ } } }, + "/api/tls-certificates/{id}/renew": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Renew TLS certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/tls-certificates/{id}/update": { "post": { "security": [ @@ -2041,6 +2473,59 @@ } } }, + "/api/tls-certificates/{id}/update-acme": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TLSCertificates" + ], + "summary": "Update ACME TLS certificate", + "parameters": [ + { + "type": "integer", + "description": "Certificate ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "TLS apply payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.TLSApplyInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/update/latest-release": { "get": { "produces": [ @@ -2138,6 +2623,32 @@ } }, "definitions": { + "controller.CleanupConfigVersionRequest": { + "type": "object", + "required": [ + "keep_count" + ], + "properties": { + "keep_count": { + "type": "integer", + "minimum": 3 + } + } + }, + "controller.DnsAccountInput": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "controller.geoIPLookupRequest": { "type": "object", "properties": { @@ -2149,6 +2660,17 @@ } } }, + "controller.optionBatchPayload": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Option" + } + } + } + }, "model.Option": { "type": "object", "properties": { @@ -2488,6 +3010,15 @@ "service.ProxyRouteInput": { "type": "object", "properties": { + "basic_auth_enabled": { + "type": "boolean" + }, + "basic_auth_password": { + "type": "string" + }, + "basic_auth_username": { + "type": "string" + }, "cache_enabled": { "type": "boolean" }, @@ -2503,6 +3034,12 @@ "cert_id": { "type": "integer" }, + "cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, "custom_headers": { "type": "array", "items": { @@ -2512,24 +3049,69 @@ "domain": { "type": "string" }, + "domain_cert_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, "enable_https": { "type": "boolean" }, "enabled": { "type": "boolean" }, + "limit_conn_per_ip": { + "type": "integer" + }, + "limit_conn_per_server": { + "type": "integer" + }, + "limit_rate": { + "type": "string" + }, + "origin_address": { + "type": "string" + }, "origin_host": { "type": "string" }, + "origin_id": { + "type": "integer" + }, + "origin_port": { + "type": "string" + }, + "origin_scheme": { + "type": "string" + }, + "origin_uri": { + "type": "string" + }, "origin_url": { "type": "string" }, + "pow_config": { + "type": "string" + }, + "pow_enabled": { + "type": "boolean" + }, "redirect_http": { "type": "boolean" }, "remark": { "type": "string" }, + "site_name": { + "type": "string" + }, "upstreams": { "type": "array", "items": { @@ -2538,6 +3120,47 @@ } } }, + "service.TLSApplyInput": { + "type": "object", + "properties": { + "acme_account_id": { + "type": "integer" + }, + "auto_renew": { + "type": "boolean" + }, + "disable_cname": { + "type": "boolean" + }, + "dns1": { + "type": "string" + }, + "dns2": { + "type": "string" + }, + "dns_account_id": { + "type": "integer" + }, + "key_algorithm": { + "type": "string" + }, + "name": { + "type": "string" + }, + "other_domains": { + "type": "string" + }, + "primary_domain": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "skip_dns": { + "type": "boolean" + } + } + }, "service.TLSCertificateInput": { "type": "object", "properties": { diff --git a/openflare_server/docs/swagger.yaml b/openflare_server/docs/swagger.yaml index aa431802..40813ce7 100644 --- a/openflare_server/docs/swagger.yaml +++ b/openflare_server/docs/swagger.yaml @@ -1,5 +1,22 @@ basePath: / definitions: + controller.CleanupConfigVersionRequest: + properties: + keep_count: + minimum: 3 + type: integer + required: + - keep_count + type: object + controller.DnsAccountInput: + properties: + authorization: + type: string + name: + type: string + type: + type: string + type: object controller.geoIPLookupRequest: properties: ip: @@ -7,6 +24,13 @@ definitions: provider: type: string type: object + controller.optionBatchPayload: + properties: + options: + items: + $ref: '#/definitions/model.Option' + type: array + type: object model.Option: properties: key: @@ -229,6 +253,12 @@ definitions: type: object service.ProxyRouteInput: properties: + basic_auth_enabled: + type: boolean + basic_auth_password: + type: string + basic_auth_username: + type: string cache_enabled: type: boolean cache_policy: @@ -239,29 +269,90 @@ definitions: type: array cert_id: type: integer + cert_ids: + items: + type: integer + type: array custom_headers: items: $ref: '#/definitions/service.ProxyRouteCustomHeaderInput' type: array domain: type: string + domain_cert_ids: + items: + type: integer + type: array + domains: + items: + type: string + type: array enable_https: type: boolean enabled: type: boolean + limit_conn_per_ip: + type: integer + limit_conn_per_server: + type: integer + limit_rate: + type: string + origin_address: + type: string origin_host: type: string + origin_id: + type: integer + origin_port: + type: string + origin_scheme: + type: string + origin_uri: + type: string origin_url: type: string + pow_config: + type: string + pow_enabled: + type: boolean redirect_http: type: boolean remark: type: string + site_name: + type: string upstreams: items: type: string type: array type: object + service.TLSApplyInput: + properties: + acme_account_id: + type: integer + auto_renew: + type: boolean + disable_cname: + type: boolean + dns_account_id: + type: integer + dns1: + type: string + dns2: + type: string + key_algorithm: + type: string + name: + type: string + other_domains: + type: string + primary_domain: + type: string + remark: + type: string + skip_dns: + type: boolean + type: object service.TLSCertificateInput: properties: cert_pem: @@ -477,6 +568,21 @@ paths: summary: Get access log IP trend tags: - AccessLogs + /api/acme-accounts/default: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get default ACME account + tags: + - AcmeAccounts /api/agent/apply-logs: post: consumes: @@ -698,6 +804,33 @@ paths: summary: Get active config version tags: - ConfigVersions + /api/config-versions/cleanup: + post: + parameters: + - description: Cleanup request + in: body + name: request + required: true + schema: + $ref: '#/definitions/controller.CleanupConfigVersionRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Cleanup old config versions + tags: + - ConfigVersions /api/config-versions/diff: get: produces: @@ -763,6 +896,94 @@ paths: summary: Get dashboard overview tags: - Dashboard + /api/dns-accounts/: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: List DNS accounts + tags: + - DnsAccounts + post: + consumes: + - application/json + parameters: + - description: DNS account payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/controller.DnsAccountInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Create DNS account + tags: + - DnsAccounts + /api/dns-accounts/{id}/delete: + post: + parameters: + - description: DNS Account ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Delete DNS account + tags: + - DnsAccounts + /api/dns-accounts/{id}/update: + post: + consumes: + - application/json + parameters: + - description: DNS Account ID + in: path + name: id + required: true + type: integer + - description: DNS account payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/controller.DnsAccountInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Update DNS account + tags: + - DnsAccounts /api/managed-domains/: get: produces: @@ -1246,6 +1467,33 @@ paths: summary: Update option tags: - Options + /api/option/update-batch: + post: + consumes: + - application/json + parameters: + - description: Batch option payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/controller.optionBatchPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + summary: Batch update options + tags: + - Options /api/proxy-routes/: get: produces: @@ -1289,6 +1537,32 @@ paths: summary: Create proxy route tags: - ProxyRoutes + /api/proxy-routes/{id}: + get: + parameters: + - description: Route ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Get proxy route detail + tags: + - ProxyRoutes /api/proxy-routes/{id}/delete: post: parameters: @@ -1457,6 +1731,40 @@ paths: summary: Get TLS certificate PEM content tags: - TLSCertificates + /api/tls-certificates/{id}/convert-acme: + post: + consumes: + - application/json + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + - description: TLS apply payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service.TLSApplyInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Convert uploaded TLS certificate to ACME managed certificate + tags: + - TLSCertificates /api/tls-certificates/{id}/delete: post: parameters: @@ -1483,6 +1791,32 @@ paths: summary: Delete TLS certificate tags: - TLSCertificates + /api/tls-certificates/{id}/renew: + post: + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Renew TLS certificate + tags: + - TLSCertificates /api/tls-certificates/{id}/update: post: consumes: @@ -1517,6 +1851,69 @@ paths: summary: Update TLS certificate from PEM tags: - TLSCertificates + /api/tls-certificates/{id}/update-acme: + post: + consumes: + - application/json + parameters: + - description: Certificate ID + in: path + name: id + required: true + type: integer + - description: TLS apply payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service.TLSApplyInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Update ACME TLS certificate + tags: + - TLSCertificates + /api/tls-certificates/apply: + post: + consumes: + - application/json + parameters: + - description: TLS apply payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/service.TLSApplyInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: Apply TLS certificate via ACME + tags: + - TLSCertificates /api/tls-certificates/import-file: post: consumes: diff --git a/openflare_server/router/api-router.go b/openflare_server/router/api-router.go index 498ac108..2cbce3ec 100644 --- a/openflare_server/router/api-router.go +++ b/openflare_server/router/api-router.go @@ -121,6 +121,7 @@ func SetApiRouter(router *gin.Engine) { tlsCertificateRoute.POST("/", controller.CreateTLSCertificate) tlsCertificateRoute.POST("/:id/update", controller.UpdateTLSCertificate) tlsCertificateRoute.POST("/:id/update-acme", controller.UpdateAcmeCertificate) + tlsCertificateRoute.POST("/:id/convert-acme", controller.ConvertTLSCertificateToAcme) tlsCertificateRoute.POST("/import-file", controller.ImportTLSCertificateFile) tlsCertificateRoute.POST("/:id/delete", controller.DeleteTLSCertificate) tlsCertificateRoute.POST("/apply", controller.ApplyTLSCertificate) diff --git a/openflare_server/router/api_phase1_test.go b/openflare_server/router/api_phase1_test.go index 06e87df5..aa04177f 100644 --- a/openflare_server/router/api_phase1_test.go +++ b/openflare_server/router/api_phase1_test.go @@ -8,6 +8,7 @@ import ( "crypto/x509/pkix" "encoding/json" "encoding/pem" + "errors" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" @@ -387,6 +388,95 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) { } } +func TestTLSCertificateConvertAcmeAPI(t *testing.T) { + gin.SetMode(gin.TestMode) + common.RedisEnabled = false + setupTestDB(t) + + engine := gin.New() + engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret")))) + router.SetApiRouter(engine) + + token := prepareRootToken(t) + certPEM, keyPEM := generateCertificatePairForRouterTest(t, []string{"manual.example.com"}) + createResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/tls-certificates/", map[string]any{ + "name": "manual-example", + "cert_pem": certPEM, + "key_pem": keyPEM, + }) + var certificate model.TLSCertificate + decodeResponseData(t, createResp, &certificate) + + started := make(chan struct{}, 1) + release := make(chan struct{}) + done := make(chan struct{}) + restore := service.SetTLSCertificateObtainFuncForTest(func(c *model.TLSCertificate) error { + defer close(done) + started <- struct{}{} + <-release + return errors.New("stop test conversion before external ACME call") + }) + t.Cleanup(func() { + close(release) + <-done + restore() + }) + + convertResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/tls-certificates/"+toString(certificate.ID)+"/convert-acme", map[string]any{ + "name": "managed-example", + "remark": "convert via api", + "acme_account_id": 1, + "dns_account_id": 2, + "key_algorithm": "EC256", + "auto_renew": true, + "primary_domain": "manual.example.com", + }) + var converted model.TLSCertificate + decodeResponseData(t, convertResp, &converted) + if converted.ID != certificate.ID || converted.Provider != "upload" || converted.ApplyStatus != "applying" { + t.Fatalf("expected conversion API to keep upload provider while applying, got %+v", converted) + } + + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("expected conversion task to start") + } + + duplicateResp := performJSONRequestNoFatal(t, engine, token, http.MethodPost, "/api/tls-certificates/"+toString(certificate.ID)+"/convert-acme", map[string]any{ + "name": "managed-example", + "primary_domain": "manual.example.com", + }) + if duplicateResp.Success || !strings.Contains(duplicateResp.Message, "already applying") { + t.Fatalf("expected duplicate conversion to fail, got %+v", duplicateResp) + } + + invalidResp := performJSONRequestNoFatal(t, engine, token, http.MethodPost, "/api/tls-certificates/not-a-number/convert-acme", map[string]any{}) + if invalidResp.Success || !strings.Contains(invalidResp.Message, "invalid request") { + t.Fatalf("expected invalid id to fail, got %+v", invalidResp) + } + + acmeCertPEM, acmeKeyPEM := generateCertificatePairForRouterTest(t, []string{"acme.example.com"}) + acmeResp := performJSONRequest(t, engine, token, http.MethodPost, "/api/tls-certificates/", map[string]any{ + "name": "already-acme", + "cert_pem": acmeCertPEM, + "key_pem": acmeKeyPEM, + }) + var acmeCertificate model.TLSCertificate + decodeResponseData(t, acmeResp, &acmeCertificate) + acmeCertificate.Provider = "acme" + if err := acmeCertificate.Update(); err != nil { + t.Fatalf("failed to mark certificate acme: %v", err) + } + nonUploadResp := performJSONRequestNoFatal(t, engine, token, http.MethodPost, "/api/tls-certificates/"+toString(acmeCertificate.ID)+"/convert-acme", map[string]any{ + "name": "already-acme", + "primary_domain": "acme.example.com", + }) + if nonUploadResp.Success || !strings.Contains(nonUploadResp.Message, "only uploaded") { + t.Fatalf("expected non-upload conversion to fail, got %+v", nonUploadResp) + } +} + func setupTestDB(t *testing.T) { t.Helper() dbPath := filepath.Join(t.TempDir(), "phase1.db") @@ -445,6 +535,33 @@ func performJSONRequest(t *testing.T, engine http.Handler, token string, method return resp } +func performJSONRequestNoFatal(t *testing.T, engine http.Handler, token string, method string, path string, body any) apiResponse { + t.Helper() + var payload []byte + var err error + if body != nil { + payload, err = json.Marshal(body) + if err != nil { + t.Fatalf("failed to marshal request body: %v", err) + } + } + req := httptest.NewRequest(method, path, bytes.NewReader(payload)) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("Authorization", "Bearer "+token) + recorder := httptest.NewRecorder() + engine.ServeHTTP(recorder, req) + if recorder.Code != http.StatusOK && recorder.Code != http.StatusBadRequest { + t.Fatalf("unexpected status %d for %s %s: %s", recorder.Code, method, path, recorder.Body.String()) + } + var resp apiResponse + if err = json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + return resp +} + func decodeResponseData(t *testing.T, resp apiResponse, target any) { t.Helper() if err := json.Unmarshal(resp.Data, target); err != nil { diff --git a/openflare_server/service/tls_acme_test.go b/openflare_server/service/tls_acme_test.go index 8c01ac4b..c1811ab1 100644 --- a/openflare_server/service/tls_acme_test.go +++ b/openflare_server/service/tls_acme_test.go @@ -1,7 +1,9 @@ package service import ( + "errors" "openflare/model" + "strings" "testing" "time" ) @@ -84,3 +86,186 @@ func TestAcmeAndDnsIntegration(t *testing.T) { t.Fatalf("Failed to delete DNS Account after cert cleanup: %v", err) } } + +func TestConvertTLSCertificateToAcmePreservesUploadUntilSuccess(t *testing.T) { + setupServiceTestDB(t) + + originalCertPEM, originalKeyPEM := generateCertificatePair(t, []string{"manual.example.com"}) + cert, err := CreateTLSCertificate(TLSCertificateInput{ + Name: "manual-cert", + CertPEM: originalCertPEM, + KeyPEM: originalKeyPEM, + Remark: "manual upload", + }) + if err != nil { + t.Fatalf("CreateTLSCertificate failed: %v", err) + } + originalCertPEM = cert.CertPEM + originalKeyPEM = cert.KeyPEM + + newCertPEM, newKeyPEM := generateCertificatePair(t, []string{"managed.example.com"}) + started := make(chan struct{}, 1) + release := make(chan struct{}) + restore := SetTLSCertificateObtainFuncForTest(func(c *model.TLSCertificate) error { + started <- struct{}{} + <-release + c.CertPEM = newCertPEM + c.KeyPEM = newKeyPEM + c.NotBefore = time.Now().Add(-time.Hour) + c.NotAfter = time.Now().Add(90 * 24 * time.Hour) + c.ApplyStatus = "ready" + c.ApplyMessage = "" + return model.DB.Save(c).Error + }) + t.Cleanup(restore) + + converted, err := ConvertTLSCertificateToAcme(cert.ID, TLSApplyInput{ + Name: "managed-cert", + Remark: "converted", + AcmeAccountID: 1, + DnsAccountID: 2, + KeyAlgorithm: "EC256", + AutoRenew: true, + PrimaryDomain: "managed.example.com", + OtherDomains: "www.managed.example.com", + }) + if err != nil { + t.Fatalf("ConvertTLSCertificateToAcme failed: %v", err) + } + if converted.ID != cert.ID { + t.Fatalf("expected converted certificate to keep id %d, got %d", cert.ID, converted.ID) + } + + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("expected conversion obtain task to start") + } + + applying, err := model.GetTLSCertificateByID(cert.ID) + if err != nil { + t.Fatalf("reload applying certificate failed: %v", err) + } + if applying.Provider != "upload" { + t.Fatalf("expected provider to remain upload while applying, got %s", applying.Provider) + } + if applying.ApplyStatus != "applying" { + t.Fatalf("expected applying status, got %s", applying.ApplyStatus) + } + if applying.CertPEM != originalCertPEM || applying.KeyPEM != originalKeyPEM { + t.Fatal("expected original PEM payloads to be preserved while applying") + } + + close(release) + + finalCert := waitForCertificateState(t, cert.ID, func(c *model.TLSCertificate) bool { + return c.Provider == "acme" && c.ApplyStatus == "ready" + }) + if finalCert.CertPEM != newCertPEM || finalCert.KeyPEM != newKeyPEM { + t.Fatal("expected successful conversion to replace PEM payloads") + } + if !finalCert.AutoRenew { + t.Fatal("expected converted certificate to keep auto renew enabled") + } + if finalCert.PrimaryDomain != "managed.example.com" || finalCert.OtherDomains != "www.managed.example.com" { + t.Fatalf("expected converted certificate to persist ACME domains, got %+v", finalCert) + } +} + +func TestConvertTLSCertificateToAcmePreservesUploadOnFailure(t *testing.T) { + setupServiceTestDB(t) + + originalCertPEM, originalKeyPEM := generateCertificatePair(t, []string{"manual.example.com"}) + cert, err := CreateTLSCertificate(TLSCertificateInput{ + Name: "manual-cert", + CertPEM: originalCertPEM, + KeyPEM: originalKeyPEM, + }) + if err != nil { + t.Fatalf("CreateTLSCertificate failed: %v", err) + } + originalCertPEM = cert.CertPEM + originalKeyPEM = cert.KeyPEM + + restore := SetTLSCertificateObtainFuncForTest(func(c *model.TLSCertificate) error { + err := errors.New("dns challenge failed") + updateCertError(c, err.Error()) + return err + }) + t.Cleanup(restore) + + if _, err := ConvertTLSCertificateToAcme(cert.ID, TLSApplyInput{ + Name: "manual-cert", + DnsAccountID: 1, + PrimaryDomain: "manual.example.com", + }); err != nil { + t.Fatalf("ConvertTLSCertificateToAcme failed: %v", err) + } + + finalCert := waitForCertificateState(t, cert.ID, func(c *model.TLSCertificate) bool { + return c.ApplyStatus == "error" + }) + if finalCert.Provider != "upload" { + t.Fatalf("expected failed conversion to keep upload provider, got %s", finalCert.Provider) + } + if finalCert.CertPEM != originalCertPEM || finalCert.KeyPEM != originalKeyPEM { + t.Fatal("expected failed conversion to preserve original PEM payloads") + } + if !strings.Contains(finalCert.ApplyMessage, "dns challenge failed") { + t.Fatalf("expected conversion error message, got %q", finalCert.ApplyMessage) + } +} + +func TestConvertTLSCertificateToAcmeRejectsInvalidStates(t *testing.T) { + setupServiceTestDB(t) + + certPEM, keyPEM := generateCertificatePair(t, []string{"manual.example.com"}) + cert, err := CreateTLSCertificate(TLSCertificateInput{ + Name: "manual-cert", + CertPEM: certPEM, + KeyPEM: keyPEM, + }) + if err != nil { + t.Fatalf("CreateTLSCertificate failed: %v", err) + } + + cert.Provider = "acme" + if err := cert.Update(); err != nil { + t.Fatalf("failed to mark certificate acme: %v", err) + } + if _, err := ConvertTLSCertificateToAcme(cert.ID, TLSApplyInput{Name: "manual-cert"}); err == nil || !strings.Contains(err.Error(), "only uploaded") { + t.Fatalf("expected non-upload conversion to fail, got %v", err) + } + + cert.Provider = "upload" + cert.ApplyStatus = "applying" + if err := cert.Update(); err != nil { + t.Fatalf("failed to mark certificate applying: %v", err) + } + if _, err := ConvertTLSCertificateToAcme(cert.ID, TLSApplyInput{Name: "manual-cert"}); err == nil || !strings.Contains(err.Error(), "already applying") { + t.Fatalf("expected applying conversion to fail, got %v", err) + } +} + +func waitForCertificateState(t *testing.T, id uint, matches func(*model.TLSCertificate) bool) *model.TLSCertificate { + t.Helper() + + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + cert, err := model.GetTLSCertificateByID(id) + if err != nil { + t.Fatalf("reload certificate %d failed: %v", id, err) + } + if matches(cert) { + return cert + } + time.Sleep(10 * time.Millisecond) + } + + cert, err := model.GetTLSCertificateByID(id) + if err != nil { + t.Fatalf("reload certificate %d failed: %v", id, err) + } + t.Fatalf("certificate %d did not reach expected state: %+v", id, cert) + return nil +} diff --git a/openflare_server/service/tls_certificate.go b/openflare_server/service/tls_certificate.go index cd870a0f..097b9191 100644 --- a/openflare_server/service/tls_certificate.go +++ b/openflare_server/service/tls_certificate.go @@ -40,6 +40,16 @@ type TLSApplyInput struct { DNS2 string `json:"dns2"` } +var obtainTLSCertificate = ObtainSSL + +func SetTLSCertificateObtainFuncForTest(fn func(*model.TLSCertificate) error) func() { + previous := obtainTLSCertificate + obtainTLSCertificate = fn + return func() { + obtainTLSCertificate = previous + } +} + func ListTLSCertificates() ([]*model.TLSCertificate, error) { return model.ListTLSCertificates() } @@ -191,7 +201,7 @@ func ApplyTLSCertificate(input TLSApplyInput) (*model.TLSCertificate, error) { // Async obtain SSL go func(c *model.TLSCertificate) { - _ = ObtainSSL(c) + _ = obtainTLSCertificate(c) }(cert) return cert, nil @@ -233,7 +243,64 @@ func UpdateAcmeCertificate(id uint, input TLSApplyInput) (*model.TLSCertificate, // Async obtain SSL with updated config go func(c *model.TLSCertificate) { - _ = ObtainSSL(c) + _ = obtainTLSCertificate(c) + }(cert) + + return cert, nil +} + +func ConvertTLSCertificateToAcme(id uint, input TLSApplyInput) (*model.TLSCertificate, error) { + cert, err := model.GetTLSCertificateByID(id) + if err != nil { + return nil, err + } + if cert.Provider != "upload" { + return nil, errors.New("only uploaded certificates can be converted to acme") + } + if cert.ApplyStatus == "applying" { + return nil, errors.New("certificate is already applying") + } + + name := strings.TrimSpace(input.Name) + if name == "" { + return nil, errors.New("certificate name cannot be empty") + } + + cert.Name = name + cert.Remark = strings.TrimSpace(input.Remark) + cert.AcmeAccountID = input.AcmeAccountID + cert.DnsAccountID = input.DnsAccountID + cert.KeyAlgorithm = input.KeyAlgorithm + cert.AutoRenew = input.AutoRenew + cert.PrimaryDomain = strings.TrimSpace(input.PrimaryDomain) + cert.OtherDomains = strings.TrimSpace(input.OtherDomains) + cert.DisableCNAME = input.DisableCNAME + cert.SkipDNS = input.SkipDNS + cert.DNS1 = strings.TrimSpace(input.DNS1) + cert.DNS2 = strings.TrimSpace(input.DNS2) + cert.ApplyStatus = "applying" + cert.ApplyMessage = "" + + if err := cert.Update(); err != nil { + if isUniqueConstraintError(err) { + return nil, errors.New("certificate name already exists") + } + return nil, err + } + + go func(c *model.TLSCertificate) { + if err := obtainTLSCertificate(c); err != nil { + return + } + + latest, err := model.GetTLSCertificateByID(c.ID) + if err != nil { + return + } + latest.Provider = "acme" + latest.ApplyStatus = "ready" + latest.ApplyMessage = "" + _ = latest.Update() }(cert) return cert, nil @@ -250,7 +317,7 @@ func RenewTLSCertificate(id uint) (*model.TLSCertificate, error) { // Async obtain SSL go func(c *model.TLSCertificate) { - _ = ObtainSSL(c) + _ = obtainTLSCertificate(c) }(cert) cert.ApplyStatus = "applying" diff --git a/openflare_server/web/features/dns-accounts/components/dns-accounts-page.tsx b/openflare_server/web/features/dns-accounts/components/dns-accounts-page.tsx index f67c0538..e33e59b7 100644 --- a/openflare_server/web/features/dns-accounts/components/dns-accounts-page.tsx +++ b/openflare_server/web/features/dns-accounts/components/dns-accounts-page.tsx @@ -158,15 +158,15 @@ function DnsAccountCreateModal({ isOpen, onClose, onCreated }: { isOpen: boolean