chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fonu/fonu/internal/acme"
|
||||
)
|
||||
|
||||
type CertHandler struct {
|
||||
svc *acme.Service
|
||||
}
|
||||
|
||||
func NewCertHandler(svc *acme.Service) *CertHandler {
|
||||
return &CertHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CertHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
records, err := h.svc.List(r.Context())
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusInternalServerError, "读取证书失败")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, records)
|
||||
}
|
||||
|
||||
func (h *CertHandler) Options(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, []map[string]string{
|
||||
{"value": acme.CALetsEncrypt, "label": acme.CALabel(acme.CALetsEncrypt)},
|
||||
{"value": acme.CAZeroSSL, "label": acme.CALabel(acme.CAZeroSSL)},
|
||||
{"value": acme.CABuypass, "label": acme.CALabel(acme.CABuypass)},
|
||||
{"value": acme.CALetsEncryptStaging, "label": acme.CALabel(acme.CALetsEncryptStaging)},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CertHandler) Apply(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Domain string `json:"domain"`
|
||||
DNSZone string `json:"dns_zone"`
|
||||
DDNSConfigID int64 `json:"ddns_config_id"`
|
||||
Domains []string `json:"domains"`
|
||||
CA string `json:"ca"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
domains := req.Domains
|
||||
if len(domains) == 0 && strings.TrimSpace(req.Domain) != "" {
|
||||
domains = []string{req.Domain}
|
||||
}
|
||||
if req.DDNSConfigID <= 0 {
|
||||
writeError(r, w, http.StatusBadRequest, "请选择 DNS 任务")
|
||||
return
|
||||
}
|
||||
jobID, err := h.svc.StartApply(r.Context(), domains, req.CA, req.Email, req.DDNSConfigID)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"job_id": jobID})
|
||||
}
|
||||
|
||||
func (h *CertHandler) ApplyJobStream(w http.ResponseWriter, r *http.Request) {
|
||||
jobID := strings.TrimSpace(r.PathValue("id"))
|
||||
if jobID == "" {
|
||||
writeError(r, w, http.StatusBadRequest, "无效的任务 ID")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
writeError(r, w, http.StatusInternalServerError, "SSE 不可用")
|
||||
return
|
||||
}
|
||||
if err := h.svc.StreamJob(r.Context(), w, jobID, func() error {
|
||||
flusher.Flush()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CertHandler) Import(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
CertPath string `json:"cert_path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||
return
|
||||
}
|
||||
record, err := h.svc.Import(r.Context(), req.Certificate, req.PrivateKey, req.CertPath, req.KeyPath)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, record)
|
||||
}
|
||||
|
||||
func (h *CertHandler) Download(w http.ResponseWriter, r *http.Request) {
|
||||
domain := strings.TrimSpace(r.PathValue("domain"))
|
||||
if domain == "" {
|
||||
writeError(r, w, http.StatusBadRequest, "域名不能为空")
|
||||
return
|
||||
}
|
||||
part := r.URL.Query().Get("part")
|
||||
filename, contentType, data, err := h.svc.Download(r.Context(), domain, part)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (h *CertHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
domain := strings.TrimSpace(r.PathValue("domain"))
|
||||
if domain == "" {
|
||||
writeError(r, w, http.StatusBadRequest, "域名不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.svc.Delete(r.Context(), domain); err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "证书已删除"})
|
||||
}
|
||||
|
||||
func (h *CertHandler) Renew(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Domain string `json:"domain"`
|
||||
CA string `json:"ca"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
record, err := h.svc.Renew(r.Context(), req.Domain, req.CA)
|
||||
if err != nil {
|
||||
writeError(r, w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, record)
|
||||
}
|
||||
Reference in New Issue
Block a user