mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
e6753f7c24
Wire the customer self-serve path for asking "please give me more
mailboxes / campaigns / contacts." Migration 000046 adds
limit_increase_requests with a partial unique index ensuring only one
pending request per (org, field) so the queue can't be spammed, plus
a CHECK requiring requested > current_effective so no-op rows never
reach an admin.
Service layer:
- SubmitLimitIncreaseRequest validates membership, rejects unknown
fields, snapshots the user's current effective limit at submission
time so the queue row carries the context the admin needs.
- CancelLimitRequest lets the original submitter walk back a pending
request; approved/rejected rows are immutable as the audit record.
- ApproveLimitRequest stamps the row and writes the corresponding
column on organization_limit_overrides via SetLimitOverrides —
same write path direct admin overrides use, so granted_by and
notes carry through and the audit log treats both flows uniformly.
- RejectLimitRequest stamps the row with required review notes.
Routes:
POST /v1/organization/:orgId/limit-requests
GET /v1/organization/:orgId/limit-requests
DELETE /v1/limit-requests/:id (submitter only)
GET /admin/limit-requests?status=pending
POST /admin/limit-requests/:id/approve
POST /admin/limit-requests/:id/reject
Admin approval and rejection both fire admin audit log entries with
field + requested + notes so the decision history survives any future
reorg of the request table.
UI (admin queue page + dashboard request form) plus the ToS clause
giving Warmbly the right to refuse any increase land in the next commit.
167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// User-facing endpoints under /v1/organizations/:orgId/limit-requests.
|
|
|
|
// SubmitLimitIncreaseRequest is POST /v1/organizations/:orgId/limit-requests.
|
|
func (h *Handler) SubmitLimitIncreaseRequest(c *gin.Context) {
|
|
session := middleware.GetSession(c)
|
|
if session == nil {
|
|
errx.JSON(c, errx.ErrUnauthorized)
|
|
return
|
|
}
|
|
orgID, err := uuid.Parse(c.Param("orgId"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid organization ID"))
|
|
return
|
|
}
|
|
var req models.CreateLimitIncreaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
|
|
return
|
|
}
|
|
lr, xerr := h.OrganizationService.SubmitLimitIncreaseRequest(c.Request.Context(), orgID, session.UserID, &req)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, lr)
|
|
}
|
|
|
|
// ListOrgLimitRequests is GET /v1/organizations/:orgId/limit-requests.
|
|
func (h *Handler) ListOrgLimitRequests(c *gin.Context) {
|
|
orgID, err := uuid.Parse(c.Param("orgId"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid organization ID"))
|
|
return
|
|
}
|
|
rows, xerr := h.OrganizationService.ListLimitRequestsForOrg(c.Request.Context(), orgID)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
// CancelLimitRequest is DELETE /v1/limit-requests/:id (submitter only).
|
|
func (h *Handler) CancelLimitRequest(c *gin.Context) {
|
|
session := middleware.GetSession(c)
|
|
if session == nil {
|
|
errx.JSON(c, errx.ErrUnauthorized)
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request ID"))
|
|
return
|
|
}
|
|
if xerr := h.OrganizationService.CancelLimitRequest(c.Request.Context(), id, session.UserID); xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// Admin-facing endpoints under /admin/limit-requests.
|
|
|
|
// AdminListLimitRequests is GET /admin/limit-requests?status=pending&limit=50.
|
|
func (h *Handler) AdminListLimitRequests(c *gin.Context) {
|
|
status := c.DefaultQuery("status", "pending")
|
|
limit, _ := strconv.Atoi(c.Query("limit"))
|
|
rows, xerr := h.OrganizationService.AdminListLimitRequests(c.Request.Context(), status, limit)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
// AdminApproveLimitRequest is POST /admin/limit-requests/:id/approve.
|
|
func (h *Handler) AdminApproveLimitRequest(c *gin.Context) {
|
|
adminID := middleware.GetAdminUserID(c)
|
|
if adminID == nil {
|
|
errx.JSON(c, errx.ErrUnauthorized)
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request ID"))
|
|
return
|
|
}
|
|
var body models.ReviewLimitRequestBody
|
|
_ = c.ShouldBindJSON(&body) // notes are optional
|
|
|
|
lr, xerr := h.OrganizationService.ApproveLimitRequest(c.Request.Context(), id, *adminID, body.Notes)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
|
|
h.AdminService.LogAdminAction(
|
|
c.Request.Context(),
|
|
*adminID,
|
|
"approve_limit_request",
|
|
"limit_request",
|
|
&id,
|
|
map[string]any{
|
|
"field": lr.Field,
|
|
"requested": lr.Requested,
|
|
"notes": body.Notes,
|
|
},
|
|
c.ClientIP(),
|
|
c.Request.UserAgent(),
|
|
)
|
|
c.JSON(http.StatusOK, lr)
|
|
}
|
|
|
|
// AdminRejectLimitRequest is POST /admin/limit-requests/:id/reject.
|
|
func (h *Handler) AdminRejectLimitRequest(c *gin.Context) {
|
|
adminID := middleware.GetAdminUserID(c)
|
|
if adminID == nil {
|
|
errx.JSON(c, errx.ErrUnauthorized)
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request ID"))
|
|
return
|
|
}
|
|
var body models.ReviewLimitRequestBody
|
|
if err := c.ShouldBindJSON(&body); err != nil || body.Notes == "" {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "review notes are required when rejecting"))
|
|
return
|
|
}
|
|
|
|
lr, xerr := h.OrganizationService.RejectLimitRequest(c.Request.Context(), id, *adminID, body.Notes)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
|
|
h.AdminService.LogAdminAction(
|
|
c.Request.Context(),
|
|
*adminID,
|
|
"reject_limit_request",
|
|
"limit_request",
|
|
&id,
|
|
map[string]any{
|
|
"field": lr.Field,
|
|
"requested": lr.Requested,
|
|
"notes": body.Notes,
|
|
},
|
|
c.ClientIP(),
|
|
c.Request.UserAgent(),
|
|
)
|
|
c.JSON(http.StatusOK, lr)
|
|
}
|