Files
warmbly/internal/api/handler/admin.go
T
Matthew Meszaros 1c4fcff558 feat: make the admin panel's broken queries run: seven statements referenced schema that does not exist and failed 100% of the time, so a force-stop wrote status = 'stopped' and stopped_at to a campaign_status enum and a campaigns table that have neither, the plan writes named a duration column that became duration_id long ago, the user rate-limit read and write named a daily_email_limit column that user_rate_limits has never had, and the user preview compared email_accounts.user_id (uuid) against a text parameter and then swallowed the error so every operator saw an empty mailbox list; the same uuid = text defect in GetUserEmails, which the prepare sweep cannot see because that WHERE clause is assembled at runtime, was a live 500 on GET /admin/users/:id/emails; a stop now parks the campaign at 'paused' like the owner-facing stop and records the reason the UI has always sent and the backend has always dropped into both the audit log and the owner's campaign feed, the plan writes resolve durations.title to duration_id and answer 400 rather than a constraint violation on an unknown period, the rate-limit editor now covers the seven real limit columns instead of one that never existed and patches insert-then-update in a transaction because every column is NOT NULL, AdminWorkerEmail.LastSyncedAt is a pointer so a never-synced mailbox stops being silently dropped from every admin list, and TestLiveEveryQueryPrepares now fails on undefined columns, tables, operators and enum values instead of only reporting them
2026-08-27 03:09:40 -07:00

1039 lines
26 KiB
Go

package handler
import (
"net/http"
"strconv"
"strings"
"time"
"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 Management Handlers
// AdminSearchUsers searches for users with pagination
func (h *Handler) AdminSearchUsers(c *gin.Context) {
var search models.AdminUserSearch
if err := c.ShouldBindQuery(&search); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid query parameters"))
return
}
result, xerr := h.AdminService.SearchUsers(c.Request.Context(), &search)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetUser gets a user's details
func (h *Handler) AdminGetUser(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
user, xerr := h.AdminService.GetUserDetail(c.Request.Context(), userID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, user)
}
// AdminGetUserPreview gets a full preview of a user's account
func (h *Handler) AdminGetUserPreview(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
preview, xerr := h.AdminService.GetUserPreview(c.Request.Context(), userID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, preview)
}
// AdminBanUser bans a user
func (h *Handler) AdminBanUser(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
var req models.BanUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.BanUser(c.Request.Context(), *adminID, userID, req.Reason, req.Scope, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "user banned successfully"})
}
// AdminUnbanUser unbans a user
func (h *Handler) AdminUnbanUser(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
var req models.UnbanUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.UnbanUser(c.Request.Context(), *adminID, userID, req.Reason, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "user unbanned successfully"})
}
// AdminGetUserBans gets the ban history for a user
func (h *Handler) AdminGetUserBans(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
bans, xerr := h.AdminService.GetUserBans(c.Request.Context(), userID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"bans": bans})
}
// AdminGetUserCampaigns gets a user's campaigns
func (h *Handler) AdminGetUserCampaigns(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.GetUserCampaigns(c.Request.Context(), userID, cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetUserEmails gets a user's email accounts
func (h *Handler) AdminGetUserEmails(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
emails, pagination, xerr := h.AdminService.GetUserEmails(c.Request.Context(), userID, cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"data": emails, "pagination": pagination})
}
// AdminGetUserRateLimits gets a user's rate limits
func (h *Handler) AdminGetUserRateLimits(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
limits, xerr := h.AdminService.GetUserRateLimits(c.Request.Context(), userID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, limits)
}
// AdminUpdateUserRateLimits updates a user's rate limits
func (h *Handler) AdminUpdateUserRateLimits(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
var req models.UpdateUserRateLimitsRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
limits, xerr := h.AdminService.UpdateUserRateLimits(c.Request.Context(), *adminID, userID, &req, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, limits)
}
// Worker Management Handlers
// AdminListWorkers lists all workers
func (h *Handler) AdminListWorkers(c *gin.Context) {
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.ListWorkers(c.Request.Context(), cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetWorker gets a worker's details
func (h *Handler) AdminGetWorker(c *gin.Context) {
workerID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker ID"))
return
}
worker, xerr := h.AdminService.GetWorkerDetail(c.Request.Context(), workerID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, worker)
}
// AdminUpdateWorker updates a worker
func (h *Handler) AdminUpdateWorker(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
workerID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker ID"))
return
}
var req models.AdminUpdateWorker
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.UpdateWorker(c.Request.Context(), *adminID, workerID, &req, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "worker updated successfully"})
}
// AdminGetWorkerEmails gets emails connected to a worker
func (h *Handler) AdminGetWorkerEmails(c *gin.Context) {
workerID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker ID"))
return
}
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
emails, pagination, xerr := h.AdminService.GetWorkerEmails(c.Request.Context(), workerID, cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"data": emails, "pagination": pagination})
}
// AdminGetWorkerStats gets statistics for a worker
func (h *Handler) AdminGetWorkerStats(c *gin.Context) {
workerID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker ID"))
return
}
stats, xerr := h.AdminService.GetWorkerStats(c.Request.Context(), workerID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, stats)
}
// AdminReassignEmails reassigns emails to a new worker
func (h *Handler) AdminReassignEmails(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
workerID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid worker ID"))
return
}
var req models.ReassignEmailsRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
// Use the workerID from the URL as the target
xerr := h.AdminService.ReassignEmails(c.Request.Context(), *adminID, req.EmailIDs, workerID, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "emails reassigned successfully"})
}
// Warmup Management Handlers
// AdminListWarmupPools lists all warmup pools
func (h *Handler) AdminListWarmupPools(c *gin.Context) {
pools, xerr := h.AdminService.ListWarmupPools(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"pools": pools})
}
// AdminGetWarmupHealthSummary returns an aggregate health overview of all warmup pools
func (h *Handler) AdminGetWarmupHealthSummary(c *gin.Context) {
if h.WarmupService == nil {
errx.JSON(c, errx.New(errx.Internal, "warmup service not available"))
return
}
summary, xerr := h.WarmupService.GetPoolHealthSummary(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, summary)
}
// AdminGetPoolParticipants gets participants in a warmup pool
func (h *Handler) AdminGetPoolParticipants(c *gin.Context) {
poolType := c.Param("type")
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.GetPoolParticipants(c.Request.Context(), poolType, cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminListBlockedAccounts lists blocked warmup accounts
func (h *Handler) AdminListBlockedAccounts(c *gin.Context) {
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.ListBlockedAccounts(c.Request.Context(), cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminBlockAccount blocks an account from warmup
func (h *Handler) AdminBlockAccount(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
accountID, err := uuid.Parse(c.Param("accountId"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid account ID"))
return
}
var req models.BlockAccountRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.BlockAccount(c.Request.Context(), *adminID, accountID, req.Reason, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "account blocked successfully"})
}
// AdminUnblockAccount unblocks an account from warmup
func (h *Handler) AdminUnblockAccount(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
accountID, err := uuid.Parse(c.Param("accountId"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid account ID"))
return
}
xerr := h.AdminService.UnblockAccount(c.Request.Context(), *adminID, accountID, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "account unblocked successfully"})
}
// Appeal Handlers
// AdminListAppeals lists warmup appeals
func (h *Handler) AdminListAppeals(c *gin.Context) {
status := c.Query("status")
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.ListAppeals(c.Request.Context(), status, cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetAppeal gets a specific appeal
func (h *Handler) AdminGetAppeal(c *gin.Context) {
appealID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid appeal ID"))
return
}
appeal, xerr := h.AdminService.GetAppeal(c.Request.Context(), appealID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, appeal)
}
// AdminApproveAppeal approves a warmup appeal
func (h *Handler) AdminApproveAppeal(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
appealID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid appeal ID"))
return
}
var req models.ReviewAppealRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.ErrInvalid)
return
}
xerr := h.AdminService.ReviewAppeal(c.Request.Context(), *adminID, appealID, true, req.Notes, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "appeal approved successfully"})
}
// AdminRejectAppeal rejects a warmup appeal
func (h *Handler) AdminRejectAppeal(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
appealID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid appeal ID"))
return
}
var req models.ReviewAppealRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.ErrInvalid)
return
}
xerr := h.AdminService.ReviewAppeal(c.Request.Context(), *adminID, appealID, false, req.Notes, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "appeal rejected"})
}
// Campaign Management Handlers
// AdminSearchCampaigns searches for campaigns
func (h *Handler) AdminSearchCampaigns(c *gin.Context) {
var search models.AdminCampaignSearch
if err := c.ShouldBindQuery(&search); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid query parameters"))
return
}
result, xerr := h.AdminService.SearchCampaigns(c.Request.Context(), &search)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetCampaign gets a campaign's details
func (h *Handler) AdminGetCampaign(c *gin.Context) {
campaignID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid campaign ID"))
return
}
campaign, xerr := h.AdminService.GetCampaignDetail(c.Request.Context(), campaignID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, campaign)
}
// AdminStopCampaign force-stops a campaign
func (h *Handler) AdminStopCampaign(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
campaignID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid campaign ID"))
return
}
var req models.AdminStopCampaignRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "a reason is required to force-stop a campaign"))
return
}
reason := strings.TrimSpace(req.Reason)
if reason == "" {
errx.JSON(c, errx.New(errx.BadRequest, "a reason is required to force-stop a campaign"))
return
}
xerr := h.AdminService.StopCampaign(c.Request.Context(), *adminID, campaignID, reason, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "campaign stopped successfully"})
}
// Analytics Handlers
// AdminGetPlatformOverview gets platform overview statistics
func (h *Handler) AdminGetPlatformOverview(c *gin.Context) {
overview, xerr := h.AdminService.GetPlatformOverview(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, overview)
}
// AdminGetAnalyticsTrends gets analytics trend data
func (h *Handler) AdminGetAnalyticsTrends(c *gin.Context) {
trends, xerr := h.AdminService.GetAnalyticsTrends(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, trends)
}
// AdminGetDailyEmailStats gets daily email statistics
func (h *Handler) AdminGetDailyEmailStats(c *gin.Context) {
startDate, endDate := parseDateRange(c)
stats, xerr := h.AdminService.GetDailyEmailStats(c.Request.Context(), startDate, endDate)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"stats": stats})
}
// AdminGetHourlyEmailStats gets hourly email statistics
func (h *Handler) AdminGetHourlyEmailStats(c *gin.Context) {
dateStr := c.Query("date")
date := time.Now()
if dateStr != "" {
if parsed, err := time.Parse("2006-01-02", dateStr); err == nil {
date = parsed
}
}
stats, xerr := h.AdminService.GetHourlyEmailStats(c.Request.Context(), date)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"stats": stats})
}
// AdminGetWorkerLoadStats gets worker load statistics
func (h *Handler) AdminGetWorkerLoadStats(c *gin.Context) {
stats, xerr := h.AdminService.GetWorkerLoadStats(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"stats": stats})
}
// AdminGetEmailDistribution gets email distribution across workers
func (h *Handler) AdminGetEmailDistribution(c *gin.Context) {
dist, xerr := h.AdminService.GetEmailDistribution(c.Request.Context())
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"distribution": dist})
}
// AdminGetUserGrowthStats gets user growth statistics
func (h *Handler) AdminGetUserGrowthStats(c *gin.Context) {
startDate, endDate := parseDateRange(c)
stats, xerr := h.AdminService.GetUserGrowthStats(c.Request.Context(), startDate, endDate)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"stats": stats})
}
// Plan Management Handlers
// AdminListPlans lists the plan catalog with the shared faceted query params;
// returns the standard {data, pagination} envelope.
func (h *Handler) AdminListPlans(c *gin.Context) {
var search models.AdminPlanSearch
if err := c.ShouldBindQuery(&search); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid query parameters"))
return
}
// duration is durations.title (free text, no DB enum) — validate app-side.
if search.Duration != "" && search.Duration != "month" && search.Duration != "year" {
errx.JSON(c, errx.New(errx.BadRequest, "invalid duration"))
return
}
result, xerr := h.AdminService.SearchPlansForAdmin(c.Request.Context(), &search)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminCreatePlan creates a new plan
func (h *Handler) AdminCreatePlan(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
var req models.CreatePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
plan, xerr := h.AdminService.CreatePlan(c.Request.Context(), *adminID, &req, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusCreated, plan)
}
// AdminGetPlan gets a plan by ID
func (h *Handler) AdminGetPlan(c *gin.Context) {
planID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid plan ID"))
return
}
plan, xerr := h.AdminService.GetPlan(c.Request.Context(), planID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, plan)
}
// AdminUpdatePlan updates a plan
func (h *Handler) AdminUpdatePlan(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
planID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid plan ID"))
return
}
var req models.UpdatePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
plan, xerr := h.AdminService.UpdatePlan(c.Request.Context(), *adminID, planID, &req, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, plan)
}
// AdminDeletePlan deletes a plan
func (h *Handler) AdminDeletePlan(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
planID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid plan ID"))
return
}
xerr := h.AdminService.DeletePlan(c.Request.Context(), *adminID, planID, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "plan deleted successfully"})
}
// Enterprise Inquiry Handlers
// AdminListEnterpriseInquiries lists enterprise inquiries with the shared
// faceted query params; returns the standard {data, pagination} envelope.
func (h *Handler) AdminListEnterpriseInquiries(c *gin.Context) {
var search models.AdminEnterpriseInquirySearch
if err := c.ShouldBindQuery(&search); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid query parameters"))
return
}
result, xerr := h.AdminService.ListEnterpriseInquiries(c.Request.Context(), &search)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetEnterpriseInquiry gets a specific enterprise inquiry
func (h *Handler) AdminGetEnterpriseInquiry(c *gin.Context) {
inquiryID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid inquiry ID"))
return
}
inquiry, xerr := h.AdminService.GetEnterpriseInquiry(c.Request.Context(), inquiryID)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, inquiry)
}
// AdminUpdateEnterpriseInquiry updates an enterprise inquiry
func (h *Handler) AdminUpdateEnterpriseInquiry(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
inquiryID, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid inquiry ID"))
return
}
var req models.UpdateEnterpriseInquiryRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.UpdateEnterpriseInquiry(c.Request.Context(), *adminID, inquiryID, &req, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "inquiry updated successfully"})
}
// Admin Management Handlers
// AdminListAdmins lists all admin users
func (h *Handler) AdminListAdmins(c *gin.Context) {
cursor := parseCursor(c.Query("cursor"))
limit := parseLimit(c.Query("limit"), 50)
result, xerr := h.AdminService.ListAdmins(c.Request.Context(), cursor, limit)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGrantPermissions grants admin permissions to a user
func (h *Handler) AdminGrantPermissions(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
userID, err := uuid.Parse(c.Param("userId"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
var req models.GrantAdminRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
return
}
xerr := h.AdminService.GrantAdminPermissions(c.Request.Context(), *adminID, userID, req.Permissions, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "admin permissions granted successfully"})
}
// AdminRevokePermissions revokes admin permissions from a user
func (h *Handler) AdminRevokePermissions(c *gin.Context) {
adminID := middleware.GetAdminUserID(c)
if adminID == nil {
errx.JSON(c, errx.ErrUnauthorized)
return
}
userID, err := uuid.Parse(c.Param("userId"))
if err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid user ID"))
return
}
xerr := h.AdminService.RevokeAdminPermissions(c.Request.Context(), *adminID, userID, c.ClientIP(), c.GetHeader("User-Agent"))
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"message": "admin permissions revoked successfully"})
}
// AdminSearchAuditLogs searches audit logs
func (h *Handler) AdminSearchAuditLogs(c *gin.Context) {
var search models.AdminAuditLogSearch
if err := c.ShouldBindQuery(&search); err != nil {
errx.JSON(c, errx.New(errx.BadRequest, "invalid query parameters"))
return
}
result, xerr := h.AdminService.SearchAuditLogs(c.Request.Context(), &search)
if xerr != nil {
errx.JSON(c, xerr)
return
}
c.JSON(http.StatusOK, result)
}
// AdminGetPermissionList returns the list of available admin permissions
func (h *Handler) AdminGetPermissionList(c *gin.Context) {
permissions := models.GetAllPermissionInfos()
c.JSON(http.StatusOK, gin.H{"permissions": permissions})
}
// Helper functions
func parseCursor(s string) *uuid.UUID {
if s == "" {
return nil
}
id, err := uuid.Parse(s)
if err != nil {
return nil
}
return &id
}
func parseLimit(s string, defaultLimit int) int {
if s == "" {
return defaultLimit
}
limit, err := strconv.Atoi(s)
if err != nil || limit <= 0 {
return defaultLimit
}
if limit > 100 {
return 100
}
return limit
}
func parseDateRange(c *gin.Context) (time.Time, time.Time) {
now := time.Now()
endDate := now
startDate := now.AddDate(0, 0, -30) // Default to last 30 days
if s := c.Query("start_date"); s != "" {
if t, err := time.Parse("2006-01-02", s); err == nil {
startDate = t
}
}
if s := c.Query("end_date"); s != "" {
if t, err := time.Parse("2006-01-02", s); err == nil {
endDate = t
}
}
return startDate, endDate
}