[feat](server): add ACME standalone-only mode for certificate management

- Introduced `HOP_ACME_STANDALONE_ONLY` env variable to run the ACME client without starting HTTP/DTLS servers.
- Allows certificate issuance/renewal solely and exits upon completion.
- Includes initialization of the ACME manager with domain verification, certificate management, and caching mechanisms. DomainService and expand Admin API

- Added `DomainServiceImpl` with support for registering, unregistering, and querying domains.
- Expanded Admin API with new endpoints:
  - `GET /api/v1/admin/domains/exists` to check domain registration status.
  - `GET /api/v1/admin/domains/status` to retrieve detailed domain information.
- Updated server initialization to wire `DomainService` and Admin API routes.
- Documented new Admin API endpoints in `API.md`.
This commit is contained in:
dalbodeule
2025-12-02 20:35:45 +09:00
parent 61466a5397
commit 98aed77342
5 changed files with 838 additions and 6 deletions

View File

@@ -27,9 +27,13 @@ func NewHandler(logger logging.Logger, adminAPIKey string, svc DomainService) *H
// RegisterRoutes 는 전달받은 mux 에 관리 API 라우트를 등록합니다.
// - POST /api/v1/admin/domains/register
// - POST /api/v1/admin/domains/unregister
// - GET /api/v1/admin/domains/exists
// - GET /api/v1/admin/domains/status
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.Handle("/api/v1/admin/domains/register", h.authMiddleware(http.HandlerFunc(h.handleDomainRegister)))
mux.Handle("/api/v1/admin/domains/unregister", h.authMiddleware(http.HandlerFunc(h.handleDomainUnregister)))
mux.Handle("/api/v1/admin/domains/exists", h.authMiddleware(http.HandlerFunc(h.handleDomainExists)))
mux.Handle("/api/v1/admin/domains/status", h.authMiddleware(http.HandlerFunc(h.handleDomainStatus)))
}
// authMiddleware 는 Authorization: Bearer {ADMIN_API_KEY} 헤더를 검증합니다.
@@ -130,6 +134,22 @@ type domainUnregisterResponse struct {
Error string `json:"error,omitempty"`
}
type domainExistsResponse struct {
Success bool `json:"success"`
Exists bool `json:"exists"`
Error string `json:"error,omitempty"`
}
type domainStatusResponse struct {
Success bool `json:"success"`
Exists bool `json:"exists"`
Domain string `json:"domain,omitempty"`
Memo string `json:"memo,omitempty"`
CreatedAt any `json:"created_at,omitempty"`
UpdatedAt any `json:"updated_at,omitempty"`
Error string `json:"error,omitempty"`
}
func (h *Handler) handleDomainUnregister(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.writeMethodNotAllowed(w, r)
@@ -174,6 +194,86 @@ func (h *Handler) handleDomainUnregister(w http.ResponseWriter, r *http.Request)
})
}
func (h *Handler) handleDomainExists(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeMethodNotAllowed(w, r)
return
}
domain := strings.TrimSpace(r.URL.Query().Get("domain"))
if domain == "" {
h.writeJSON(w, http.StatusBadRequest, domainExistsResponse{
Success: false,
Error: "domain is required",
})
return
}
exists, err := h.Service.IsDomainRegistered(r.Context(), domain)
if err != nil {
h.Logger.Error("failed to check domain existence", logging.Fields{
"domain": domain,
"error": err.Error(),
})
h.writeJSON(w, http.StatusInternalServerError, domainExistsResponse{
Success: false,
Error: "internal error",
})
return
}
h.writeJSON(w, http.StatusOK, domainExistsResponse{
Success: true,
Exists: exists,
})
}
func (h *Handler) handleDomainStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeMethodNotAllowed(w, r)
return
}
domain := strings.TrimSpace(r.URL.Query().Get("domain"))
if domain == "" {
h.writeJSON(w, http.StatusBadRequest, domainStatusResponse{
Success: false,
Error: "domain is required",
})
return
}
row, err := h.Service.GetDomain(r.Context(), domain)
if err != nil {
if err == ErrDomainNotFound {
h.writeJSON(w, http.StatusOK, domainStatusResponse{
Success: true,
Exists: false,
})
return
}
h.Logger.Error("failed to get domain status", logging.Fields{
"domain": domain,
"error": err.Error(),
})
h.writeJSON(w, http.StatusInternalServerError, domainStatusResponse{
Success: false,
Error: "internal error",
})
return
}
h.writeJSON(w, http.StatusOK, domainStatusResponse{
Success: true,
Exists: true,
Domain: row.Domain,
Memo: row.Memo,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
})
}
func (h *Handler) writeMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
h.writeJSON(w, http.StatusMethodNotAllowed, map[string]any{
"success": false,