mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
HA scheduler: the job scheduler and alert evaluator hold session-scoped PostgreSQL advisory locks on pinned pool connections — replicas scan for due work exactly once, failover verified live with two replicas. Manual and hook-triggered runs stay available on any replica. Email surface: mailer renders registered templates (invitation); every send is logged to core.email_log with status and error text. New GET /projects/:id/emails endpoint; Members page shows an Email log card. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/tdvorak/primora/apps/backend/internal/middleware"
|
|
apperrors "github.com/tdvorak/primora/apps/backend/internal/response"
|
|
"github.com/tdvorak/primora/apps/backend/internal/services"
|
|
)
|
|
|
|
const maxInboundBody = 256 * 1024 // external POSTs capped at 256 KiB
|
|
|
|
func (h *HTTPHandler) inboundError(c *gin.Context, err error) {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "hook not found")
|
|
return
|
|
}
|
|
msg := err.Error()
|
|
switch {
|
|
case strings.Contains(msg, "invalid signature"):
|
|
apperrors.Abort(c, http.StatusUnauthorized, "unauthorized", msg)
|
|
case strings.Contains(msg, "invalid") || strings.Contains(msg, "already exists") || strings.Contains(msg, "disabled") || strings.Contains(msg, "requires") || strings.Contains(msg, "not found in this project"):
|
|
apperrors.Abort(c, http.StatusBadRequest, "bad_request", msg)
|
|
default:
|
|
h.handleError(c, err)
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) listInboundHooks(c *gin.Context) {
|
|
actor, projectID, ok := h.actorAndProject(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListInboundHooks(c.Request.Context(), actor, projectID)
|
|
if err != nil {
|
|
h.inboundError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": result})
|
|
}
|
|
|
|
func (h *HTTPHandler) createInboundHook(c *gin.Context) {
|
|
actor, projectID, ok := h.actorAndProject(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateInboundHookInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateInboundHook(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.inboundError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteInboundHook(c *gin.Context) {
|
|
actor, projectID, ok := h.actorAndProject(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
hookID, err := uuid.Parse(c.Param("hookID"))
|
|
if err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "bad_request", "invalid hook id")
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteInboundHook(c.Request.Context(), actor, projectID, hookID, middleware.RequestIDFromContext(c)); err != nil {
|
|
h.inboundError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) listEmailLog(c *gin.Context) {
|
|
actor, projectID, ok := h.actorAndProject(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var limit int64
|
|
if raw := c.Query("limit"); raw != "" {
|
|
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
|
limit = n
|
|
}
|
|
}
|
|
result, err := h.Platform.ListEmailLog(c.Request.Context(), actor, projectID, limit)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": result})
|
|
}
|
|
|
|
// receiveInboundHook is the public ingest endpoint — the URL token is the
|
|
// credential, so no actor is required.
|
|
func (h *HTTPHandler) receiveInboundHook(c *gin.Context) {
|
|
body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxInboundBody))
|
|
if err != nil {
|
|
apperrors.Abort(c, http.StatusRequestEntityTooLarge, "too_large", "body exceeds 256 KiB")
|
|
return
|
|
}
|
|
result, err := h.Platform.ReceiveInboundHook(c.Request.Context(), c.Param("token"), c.GetHeader("X-Primora-Signature"), body)
|
|
if err != nil {
|
|
h.inboundError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusAccepted, result)
|
|
}
|