Files
hop-gate/internal/admin/http.go
dalbodeule 98aed77342 [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`.
2025-12-02 20:35:45 +09:00

291 lines
7.8 KiB
Go

package admin
import (
"encoding/json"
"net/http"
"strings"
"github.com/dalbodeule/hop-gate/internal/logging"
)
// Handler 는 /api/v1/admin 관리 plane HTTP 엔드포인트를 제공합니다.
type Handler struct {
Logger logging.Logger
AdminAPIKey string
Service DomainService
}
// NewHandler 는 새로운 Handler 를 생성합니다.
func NewHandler(logger logging.Logger, adminAPIKey string, svc DomainService) *Handler {
return &Handler{
Logger: logger.With(logging.Fields{"component": "admin_api"}),
AdminAPIKey: strings.TrimSpace(adminAPIKey),
Service: svc,
}
}
// 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} 헤더를 검증합니다.
func (h *Handler) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !h.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(map[string]any{
"success": false,
"error": "unauthorized",
})
return
}
next.ServeHTTP(w, r)
})
}
func (h *Handler) authenticate(r *http.Request) bool {
if h.AdminAPIKey == "" {
// Admin API 키가 설정되지 않았다면 모든 요청을 거부
return false
}
auth := r.Header.Get("Authorization")
if auth == "" {
return false
}
const prefix = "Bearer "
if !strings.HasPrefix(auth, prefix) {
return false
}
token := strings.TrimSpace(strings.TrimPrefix(auth, prefix))
return token == h.AdminAPIKey
}
type domainRegisterRequest struct {
Domain string `json:"domain"`
Memo string `json:"memo"`
}
type domainRegisterResponse struct {
ClientAPIKey string `json:"client_api_key,omitempty"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
func (h *Handler) handleDomainRegister(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.writeMethodNotAllowed(w, r)
return
}
var req domainRegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.Logger.Warn("invalid register request body", logging.Fields{"error": err.Error()})
h.writeJSON(w, http.StatusBadRequest, domainRegisterResponse{
Success: false,
Error: "invalid request body",
})
return
}
req.Domain = strings.TrimSpace(req.Domain)
if req.Domain == "" {
h.writeJSON(w, http.StatusBadRequest, domainRegisterResponse{
Success: false,
Error: "domain is required",
})
return
}
clientKey, err := h.Service.RegisterDomain(r.Context(), req.Domain, req.Memo)
if err != nil {
h.Logger.Error("failed to register domain", logging.Fields{
"domain": req.Domain,
"error": err.Error(),
})
h.writeJSON(w, http.StatusInternalServerError, domainRegisterResponse{
Success: false,
Error: "internal error",
})
return
}
h.writeJSON(w, http.StatusOK, domainRegisterResponse{
Success: true,
ClientAPIKey: clientKey,
})
}
type domainUnregisterRequest struct {
Domain string `json:"domain"`
ClientAPIKey string `json:"client_api_key"`
}
type domainUnregisterResponse struct {
Success bool `json:"success"`
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)
return
}
var req domainUnregisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.Logger.Warn("invalid unregister request body", logging.Fields{"error": err.Error()})
h.writeJSON(w, http.StatusBadRequest, domainUnregisterResponse{
Success: false,
Error: "invalid request body",
})
return
}
req.Domain = strings.TrimSpace(req.Domain)
req.ClientAPIKey = strings.TrimSpace(req.ClientAPIKey)
if req.Domain == "" || req.ClientAPIKey == "" {
h.writeJSON(w, http.StatusBadRequest, domainUnregisterResponse{
Success: false,
Error: "domain and client_api_key are required",
})
return
}
if err := h.Service.UnregisterDomain(r.Context(), req.Domain, req.ClientAPIKey); err != nil {
h.Logger.Error("failed to unregister domain", logging.Fields{
"domain": req.Domain,
"client_api_key": "***",
"error": err.Error(),
})
h.writeJSON(w, http.StatusInternalServerError, domainUnregisterResponse{
Success: false,
Error: "internal error",
})
return
}
h.writeJSON(w, http.StatusOK, domainUnregisterResponse{
Success: true,
})
}
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,
"error": "method not allowed",
})
}
func (h *Handler) writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
h.Logger.Error("failed to write json response", logging.Fields{"error": err.Error()})
}
}