// Package handlers provides HTTP handlers for the rdev API. package handlers import ( "context" "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/pkg/api" ) // NotifyHandler handles notify domain status and verification endpoints. type NotifyHandler struct { notifyProvisioner port.NotifyProvisioner // may be nil if not configured credStore port.CredentialStore // may be nil logger *slog.Logger } // NewNotifyHandler creates a new notify handler. func NewNotifyHandler(notifyProvisioner port.NotifyProvisioner, credStore port.CredentialStore, logger *slog.Logger) *NotifyHandler { return &NotifyHandler{ notifyProvisioner: notifyProvisioner, credStore: credStore, logger: logger, } } // Mount registers the notify routes. func (h *NotifyHandler) Mount(r api.Router) { r.Route("/projects/{projectID}/notify", func(r chi.Router) { r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)). Get("/status", h.GetStatus) r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)). Post("/verify", h.Verify) }) } // NotifyDomainStatusResponse is the response for GET /projects/{projectID}/notify/status. type NotifyDomainStatusResponse struct { Host string `json:"host"` ResendDomainID string `json:"resend_domain_id"` Status string `json:"status"` } // GetStatus returns the Resend domain verification status for the project. // GET /projects/{projectID}/notify/status func (h *NotifyHandler) GetStatus(w http.ResponseWriter, r *http.Request) { if h.notifyProvisioner == nil { api.WriteError(w, r, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", "notify not configured") return } projectID := chi.URLParam(r, "projectID") if projectID == "" { api.WriteBadRequest(w, r, "project ID is required") return } ctx, cancel := context.WithTimeout(r.Context(), TimeoutLookup) defer cancel() log := logging.FromContext(ctx).WithHandler("NotifyGetStatus") host, resendDomainID := h.lookupNotifyCredentials(ctx, projectID) status, err := h.notifyProvisioner.GetNotifyDomainStatus(ctx, host, resendDomainID) if err != nil { log.Error("failed to get notify domain status", logging.FieldError, err, logging.FieldProjectID, projectID, ) api.WriteInternalError(w, r, "failed to get notify domain status") return } api.WriteSuccess(w, r, NotifyDomainStatusResponse{ Host: status.Host, ResendDomainID: status.ResendDomainID, Status: status.Status, }) } // Verify triggers domain re-verification for the project's Resend email domain. // POST /projects/{projectID}/notify/verify func (h *NotifyHandler) Verify(w http.ResponseWriter, r *http.Request) { if h.notifyProvisioner == nil { api.WriteError(w, r, http.StatusServiceUnavailable, "SERVICE_UNAVAILABLE", "notify not configured") return } projectID := chi.URLParam(r, "projectID") if projectID == "" { api.WriteBadRequest(w, r, "project ID is required") return } ctx, cancel := context.WithTimeout(r.Context(), TimeoutStandard) defer cancel() log := logging.FromContext(ctx).WithHandler("NotifyVerify") _, resendDomainID := h.lookupNotifyCredentials(ctx, projectID) if err := h.notifyProvisioner.VerifyProjectNotify(ctx, projectID, resendDomainID); err != nil { log.Error("failed to trigger notify domain verification", logging.FieldError, err, logging.FieldProjectID, projectID, ) api.WriteInternalError(w, r, "failed to trigger domain verification") return } api.WriteSuccess(w, r, map[string]string{ "message": "verification triggered", }) } // lookupNotifyCredentials fetches NOTIFY_HOST and NOTIFY_RESEND_DOMAIN_ID from the credential store. // Returns empty strings if credStore is nil or the credentials are not found. func (h *NotifyHandler) lookupNotifyCredentials(ctx context.Context, projectID string) (host, resendDomainID string) { if h.credStore == nil { return "", "" } // Credentials are stored with project-scoped keys: "projectID:CRED_KEY" creds, err := h.credStore.GetMultiple(ctx, []string{ projectID + ":" + domain.CredKeyNotifyHost, projectID + ":" + domain.CredKeyNotifyResendDomainID, }) if err != nil { logging.FromContext(ctx).WithHandler("NotifyHandler").Warn( "failed to fetch notify credentials from store", logging.FieldError, err, logging.FieldProjectID, projectID, ) return "", "" } host = creds[projectID+":"+domain.CredKeyNotifyHost] resendDomainID = creds[projectID+":"+domain.CredKeyNotifyResendDomainID] return host, resendDomainID }