mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
c790812207
feat: ship api keys dashboard with smart limits and analytics
543 lines
33 KiB
Go
543 lines
33 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/warmbly/warmbly/internal/api/handler"
|
|
"github.com/warmbly/warmbly/internal/api/handler/grouph"
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
func Run(
|
|
h *handler.Handler,
|
|
m *middleware.Handler,
|
|
oidcm *middleware.OidcHandler,
|
|
addr, ginMode string,
|
|
allowedOrigins []string,
|
|
) *gin.Engine {
|
|
gin.SetMode(ginMode)
|
|
|
|
r := gin.Default()
|
|
|
|
r.GET("/health", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// Public webhook for GitHub release events. Auth comes from
|
|
// X-Hub-Signature-256 (HMAC-SHA256 with RELEASES_WEBHOOK_SECRET).
|
|
r.POST("/webhooks/github/releases", h.GithubReleasesWebhook)
|
|
|
|
// Public OAuth-bouncer pages used by the mailbox onboarding popup.
|
|
// The provider redirects here; the page postMessages the code/state
|
|
// back to the SPA opener which then calls /emails/onboarding/oauth/finish.
|
|
r.GET("/addresses/google/callback", h.EmailOAuthCallbackGmail)
|
|
r.GET("/addresses/outlook/callback", h.EmailOAuthCallbackOutlook)
|
|
|
|
corsConfig := cors.Config{
|
|
AllowMethods: []string{"POST", "GET", "PATCH", "OPTIONS", "DELETE"},
|
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
MaxAge: 12 * time.Hour,
|
|
}
|
|
switch {
|
|
case len(allowedOrigins) == 0 && ginMode != gin.ReleaseMode:
|
|
corsConfig.AllowOrigins = []string{
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:3000",
|
|
"http://localhost:4173",
|
|
"http://127.0.0.1:4173",
|
|
"http://localhost:5173",
|
|
"http://127.0.0.1:5173",
|
|
}
|
|
corsConfig.AllowCredentials = true
|
|
case len(allowedOrigins) == 1 && allowedOrigins[0] == "*":
|
|
corsConfig.AllowAllOrigins = true
|
|
corsConfig.AllowCredentials = false
|
|
default:
|
|
corsConfig.AllowOrigins = allowedOrigins
|
|
corsConfig.AllowCredentials = true
|
|
}
|
|
|
|
r.Use(cors.New(corsConfig))
|
|
|
|
// Limit request body size to 10MB to prevent OOM
|
|
r.Use(func(c *gin.Context) {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10<<20)
|
|
c.Next()
|
|
})
|
|
|
|
auth := r.Group("/auth")
|
|
{
|
|
auth.POST("/login", h.LoginStart)
|
|
auth.POST("/login/confirm", h.LoginConfirm)
|
|
auth.POST("/register", h.RegistrationStart)
|
|
auth.POST("/register/confirm", h.RegistrationConfirm)
|
|
auth.POST("/refresh", h.RefreshToken)
|
|
auth.POST("/reset-password", h.ResetPasswordStart)
|
|
auth.POST("/reset-password/confirm", h.ResetPasswordConfirm)
|
|
}
|
|
|
|
protectedAuth := auth.Group("")
|
|
protectedAuth.Use(m.AuthMiddleware())
|
|
{
|
|
protectedAuth.POST("/logout", h.Logout)
|
|
protectedAuth.POST("/logout-all", h.LogoutAll)
|
|
protectedAuth.GET("/me", h.GetUser)
|
|
protectedAuth.PATCH("/me/onboarding", h.CompleteOnboarding)
|
|
protectedAuth.POST("/me/avatar", h.UploadUserAvatar)
|
|
protectedAuth.DELETE("/me/avatar", h.DeleteUserAvatar)
|
|
}
|
|
|
|
// JWT-only protected group: routes that are tied to a human session and
|
|
// must never be reachable via a long-lived API key. This is the safety
|
|
// boundary for billing, organization governance, websocket bootstrapping,
|
|
// and the email onboarding flow (which writes user-encrypted secrets).
|
|
jwtOnly := r.Group("")
|
|
jwtOnly.Use(m.AuthMiddleware())
|
|
|
|
// API-accessible protected group: routes that accept either a JWT or an
|
|
// API key. CombinedAuthMiddleware sets the same context keys for both
|
|
// auth types; APIKeyUsageMiddleware records one log row per API-key
|
|
// request (JWT requests are skipped).
|
|
protected := r.Group("")
|
|
protected.Use(m.CombinedAuthMiddleware(), m.APIKeyUsageMiddleware())
|
|
{
|
|
emails := protected.Group("/emails")
|
|
emails.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
emails.GET("", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadEmails), h.EmailsSearch)
|
|
emails.GET("/:id", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadEmails), h.GetEmail)
|
|
emails.PATCH("/:id", m.RequireAccess(models.PermManageEmails, models.APIPermWriteEmails), h.UpdateEmail)
|
|
emails.PATCH("/:id/track", m.RequireAccess(models.PermManageEmails, models.APIPermWriteEmails), h.UpdateEmailTrackingDomain)
|
|
emails.DELETE("/:id", m.RequireAccess(models.PermManageEmails, models.APIPermWriteEmails), h.DeleteEmail)
|
|
emails.POST("/:id/send", m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns), h.SendEmailFromAccount)
|
|
}
|
|
|
|
// Email onboarding is JWT-only — it writes user-encrypted refresh
|
|
// tokens via the SPA popup flow and shouldn't be triggerable by an
|
|
// API key with a long lifetime.
|
|
onboardingEmails := jwtOnly.Group("/emails/onboarding")
|
|
onboardingEmails.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
onboardingEmails.POST("/oauth/start", h.StartEmailOAuth)
|
|
onboardingEmails.POST("/oauth/finish", h.FinishEmailOAuth)
|
|
onboardingEmails.POST("/smtp-imap", h.ConnectEmailSMTPIMAP)
|
|
}
|
|
|
|
campaigns := protected.Group("/campaigns")
|
|
campaigns.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
campaigns.GET("", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.SearchCampaigns)
|
|
campaigns.POST("", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.CreateCampaign)
|
|
campaigns.GET("/:id", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.GetCampaign)
|
|
campaigns.PATCH("/:id", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.UpdateCampaign)
|
|
campaigns.DELETE("/:id", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.DeleteCampaign)
|
|
|
|
// Advanced campaign controls
|
|
campaigns.GET("/:id/advanced", m.RequireOrganization(), m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.GetCampaignAdvancedSettings)
|
|
campaigns.PATCH("/:id/advanced", m.RequireOrganization(), m.RequireAccess(models.PermManageSettings, models.APIPermWriteCampaigns), h.UpdateCampaignAdvancedSettings)
|
|
campaigns.GET("/:id/ab-variants", m.RequireOrganization(), m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.ListCampaignABVariants)
|
|
campaigns.POST("/:id/ab-variants", m.RequireOrganization(), m.RequireAccess(models.PermManageSettings, models.APIPermWriteCampaigns), h.CreateCampaignABVariant)
|
|
campaigns.PATCH("/:id/ab-variants/:variantId", m.RequireOrganization(), m.RequireAccess(models.PermManageSettings, models.APIPermWriteCampaigns), h.UpdateCampaignABVariant)
|
|
campaigns.DELETE("/:id/ab-variants/:variantId", m.RequireOrganization(), m.RequireAccess(models.PermManageSettings, models.APIPermWriteCampaigns), h.DeleteCampaignABVariant)
|
|
campaigns.POST("/:id/preflight", m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns), h.RunCampaignPreflight)
|
|
campaigns.GET("/:id/ab-analysis", m.RequireOrganization(), m.RequireAccess(models.PermViewAnalytics, models.APIPermReadAnalytics), h.GetCampaignABAnalysis)
|
|
campaigns.POST("/:id/test-email", m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns), h.SendTestEmail)
|
|
|
|
// Campaign start/stop
|
|
campaigns.POST("/:id/start", m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns), h.StartCampaign)
|
|
campaigns.POST("/:id/stop", m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns), h.StopCampaign)
|
|
campaigns.GET("/:id/logs", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.GetCampaignLogs)
|
|
|
|
sequences := campaigns.Group("/:id/sequences")
|
|
{
|
|
sequences.GET("", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadCampaigns), h.GetSequences)
|
|
sequences.POST("", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.CreateSequence)
|
|
sequences.PATCH("/:sid", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.UpdateSequence)
|
|
sequences.DELETE("/:sid", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteCampaigns), h.DeleteSequence)
|
|
}
|
|
}
|
|
|
|
contacts := protected.Group("/contacts")
|
|
contacts.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
contacts.POST("/search", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.SearchContacts)
|
|
contacts.POST("", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.AddContacts)
|
|
contacts.DELETE("", m.RequireAccess(models.PermManageContacts, models.APIPermBulkContacts), h.DeleteContactBulk)
|
|
contacts.PATCH("", m.RequireAccess(models.PermManageContacts, models.APIPermBulkContacts), h.UpdateContactBulk)
|
|
// Import + export power-tools. Read-only export gates on
|
|
// ReadContacts; the import endpoints write and so use the
|
|
// stricter Write/Bulk scopes that the rest of the contact
|
|
// write paths already use.
|
|
contacts.POST("/export", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.ExportContacts)
|
|
contacts.POST("/import/preview", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.ImportPreviewContacts)
|
|
contacts.POST("/import/commit", m.RequireAccess(models.PermManageContacts, models.APIPermBulkContacts), h.ImportCommitContacts)
|
|
contacts.PATCH("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.UpdateContact)
|
|
contacts.DELETE("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.DeleteContact)
|
|
|
|
// Contact 360 view: hydrated detail, every email sent to
|
|
// the contact, and the merged activity timeline.
|
|
contacts.GET("/:id", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.GetContact)
|
|
contacts.GET("/:id/emails", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.ListContactEmails)
|
|
contacts.GET("/:id/timeline", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.ListContactTimeline)
|
|
|
|
// CRM: Notes & Activities (under contacts)
|
|
contacts.GET("/:id/notes", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.ListContactNotes)
|
|
contacts.POST("/:id/notes", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.CreateContactNote)
|
|
contacts.PATCH("/:id/notes/:noteId", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.UpdateContactNote)
|
|
contacts.DELETE("/:id/notes/:noteId", m.RequireAccess(models.PermManageContacts, models.APIPermWriteContacts), h.DeleteContactNote)
|
|
contacts.GET("/:id/activities", m.RequireAccess(models.PermViewContacts, models.APIPermReadContacts), h.ListContactActivities)
|
|
contacts.GET("/:id/deals", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.GetDealsByContact)
|
|
}
|
|
|
|
// Group endpoints (folders / tags / categories) don't yet have
|
|
// dedicated permission bits — gate them on the broadest read scope
|
|
// for now so an API key needs at least one collection permission.
|
|
grouph.New(protected, h.FolderService, "folders")
|
|
grouph.New(protected, h.TagService, "tags")
|
|
grouph.New(protected, h.CategoryService, "categories")
|
|
|
|
unibox := protected.Group("/unibox")
|
|
unibox.Use(m.RateLimitMiddleware(models.RateLimitRead))
|
|
{
|
|
unibox.GET("", m.RequireAccess(models.PermAccessUnibox, models.APIPermReadUnibox), h.GetUniboxIncoming)
|
|
unibox.GET("/count", m.RequireAccess(models.PermAccessUnibox, models.APIPermReadUnibox), h.GetUnseenCount)
|
|
unibox.GET("/thread", m.RequireAccess(models.PermAccessUnibox, models.APIPermReadUnibox), h.GetUniboxThread)
|
|
unibox.PATCH("/seen", m.RequireAccess(models.PermAccessUnibox, models.APIPermWriteUnibox), h.UniboxMarkSeen)
|
|
unibox.POST("/reply", m.RequireOrganization(), m.RequireAccess(models.PermAccessUnibox, models.APIPermWriteUnibox), h.UniboxReply)
|
|
unibox.GET("/:id", m.RequireAccess(models.PermAccessUnibox, models.APIPermReadUnibox), h.GetUniboxEmail)
|
|
}
|
|
|
|
// API key management. JWT users need PermManageAPIKeys; API keys
|
|
// need the APIPermAPIKeys self-service bit. This lets an integration
|
|
// rotate its own keys without going through the dashboard.
|
|
apiKeys := protected.Group("/api-keys")
|
|
apiKeys.Use(m.RequireOrganization(), m.RequireAccess(models.PermManageAPIKeys, models.APIPermAPIKeys))
|
|
apiKeys.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
apiKeys.GET("", h.ListAPIKeys)
|
|
apiKeys.POST("", h.CreateAPIKey)
|
|
apiKeys.GET("/permissions", h.ListAPIPermissions)
|
|
apiKeys.GET("/usage/summary", h.GetAPIKeyUsageSummary)
|
|
apiKeys.GET("/usage/analytics", h.GetAPIKeyAnalytics)
|
|
apiKeys.GET("/:id", h.GetAPIKey)
|
|
apiKeys.PATCH("/:id", h.UpdateAPIKey)
|
|
apiKeys.DELETE("/:id", h.RevokeAPIKey)
|
|
apiKeys.GET("/:id/analytics", h.GetAPIKeyAnalytics)
|
|
apiKeys.GET("/:id/logs", h.ListAPIKeyUsageLogs)
|
|
}
|
|
|
|
// Analytics endpoints
|
|
analytics := protected.Group("/analytics")
|
|
analytics.Use(m.RateLimitMiddleware(models.RateLimitAnalytics), m.RequireAccess(models.PermViewAnalytics, models.APIPermReadAnalytics))
|
|
{
|
|
analytics.GET("/dashboard", h.GetDashboardAnalytics)
|
|
analytics.GET("/deliverability", m.RequireOrganization(), h.GetDeliverabilityDashboard)
|
|
analytics.GET("/warmup", h.GetWarmupAnalytics)
|
|
analytics.GET("/campaigns/compare", h.CompareCampaigns)
|
|
analytics.GET("/campaigns/:id", h.GetCampaignAnalytics)
|
|
analytics.GET("/campaigns/:id/daily", h.GetCampaignDailyStats)
|
|
analytics.GET("/campaigns/:id/hourly", h.GetCampaignHourlyStats)
|
|
analytics.GET("/accounts", h.GetAllAccountStatuses)
|
|
analytics.GET("/accounts/:id", h.GetAccountStatus)
|
|
analytics.GET("/usage", h.GetUsageOverview)
|
|
}
|
|
|
|
// Audit logs
|
|
auditLogs := protected.Group("/audit-logs")
|
|
auditLogs.Use(m.RateLimitMiddleware(models.RateLimitRead), m.RequireAccess(models.PermViewAnalytics, models.APIPermReadAuditLogs))
|
|
{
|
|
auditLogs.GET("", h.GetAuditLogs)
|
|
}
|
|
|
|
// Realtime websocket bootstrap is JWT-only — the websocket itself
|
|
// has its own session-based auth.
|
|
realtime := jwtOnly.Group("/realtime")
|
|
{
|
|
realtime.GET("/info", h.GetRealtimeInfo)
|
|
}
|
|
|
|
// Advanced outreach controls (org-scoped)
|
|
outreach := protected.Group("/outreach")
|
|
outreach.Use(m.RequireOrganization(), m.RequireAccess(models.PermManageSettings, models.APIPermWriteCampaigns))
|
|
{
|
|
outreach.GET("/settings", h.GetOutreachSettings)
|
|
outreach.PATCH("/settings", h.UpdateOutreachSettings)
|
|
}
|
|
|
|
// Deliverability event ingestion (org-scoped). API-key callable so
|
|
// downstream pipelines (e.g. SES bounce processors) can post events
|
|
// without a human in the loop.
|
|
deliverability := protected.Group("/deliverability")
|
|
deliverability.Use(m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermWriteCampaigns))
|
|
{
|
|
deliverability.POST("/events", h.IngestDeliverabilityEvent)
|
|
}
|
|
|
|
// Task dead letter operations (org-scoped). Requires SendCampaigns
|
|
// because a replay actually re-dispatches mail.
|
|
taskOps := protected.Group("/tasks")
|
|
taskOps.Use(m.RequireOrganization(), m.RequireAccess(models.PermSendCampaigns, models.APIPermSendCampaigns))
|
|
{
|
|
taskOps.GET("/dlq", h.ListTaskDeadLetters)
|
|
taskOps.POST("/dlq/:id/replay", h.ReplayTaskDeadLetter)
|
|
}
|
|
|
|
// Reply templates (org-scoped)
|
|
templates := protected.Group("/templates")
|
|
templates.Use(m.RequireOrganization(), m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
templates.GET("", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadTemplates), h.ListTemplates)
|
|
templates.POST("", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteTemplates), h.CreateTemplate)
|
|
templates.PATCH("/reorder", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteTemplates), h.ReorderTemplates)
|
|
templates.GET("/:id", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadTemplates), h.GetTemplate)
|
|
templates.PATCH("/:id", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteTemplates), h.UpdateTemplate)
|
|
templates.DELETE("/:id", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteTemplates), h.DeleteTemplate)
|
|
templates.POST("/:id/duplicate", m.RequireAccess(models.PermManageCampaigns, models.APIPermWriteTemplates), h.DuplicateTemplate)
|
|
templates.POST("/:id/render", m.RequireAccess(models.PermViewCampaigns, models.APIPermReadTemplates), h.RenderTemplate)
|
|
}
|
|
|
|
// CRM routes (require org)
|
|
crmGroup := protected.Group("/crm")
|
|
crmGroup.Use(m.RequireOrganization(), m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
pipelines := crmGroup.Group("/pipelines")
|
|
{
|
|
pipelines.GET("", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.ListPipelines)
|
|
pipelines.POST("", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.CreatePipeline)
|
|
pipelines.GET("/:id", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.GetPipeline)
|
|
pipelines.PATCH("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.UpdatePipeline)
|
|
pipelines.DELETE("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.DeletePipeline)
|
|
pipelines.POST("/:id/stages", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.CreateStage)
|
|
pipelines.PATCH("/:id/stages/:stageId", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.UpdateStage)
|
|
pipelines.DELETE("/:id/stages/:stageId", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.DeleteStage)
|
|
}
|
|
|
|
deals := crmGroup.Group("/deals")
|
|
{
|
|
deals.GET("", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.ListDeals)
|
|
deals.POST("", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.CreateDeal)
|
|
deals.GET("/:id", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.GetDeal)
|
|
deals.PATCH("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.UpdateDeal)
|
|
deals.DELETE("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.DeleteDeal)
|
|
}
|
|
|
|
crmTasks := crmGroup.Group("/tasks")
|
|
{
|
|
crmTasks.GET("", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.ListCRMTasks)
|
|
crmTasks.POST("", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.CreateCRMTask)
|
|
crmTasks.GET("/:id", m.RequireAccess(models.PermViewContacts, models.APIPermReadCRM), h.GetCRMTask)
|
|
crmTasks.PATCH("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.UpdateCRMTask)
|
|
crmTasks.DELETE("/:id", m.RequireAccess(models.PermManageContacts, models.APIPermWriteCRM), h.DeleteCRMTask)
|
|
}
|
|
}
|
|
|
|
// Plans and timezones are essentially public reference data — auth
|
|
// gates them only to avoid being scraped. Cheap to expose to keys.
|
|
protected.GET("/plans", h.ListPlans)
|
|
protected.GET("/timezones", h.GetTimezones)
|
|
}
|
|
|
|
// Sensitive routes below — JWT only. Organization governance, billing,
|
|
// websocket bootstrap, danger-zone destructions, and pending invitations
|
|
// all live here. None of these are reachable via an API key.
|
|
{
|
|
org := jwtOnly.Group("/organization")
|
|
org.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
org.POST("", h.CreateOrganization)
|
|
org.GET("", h.GetUserOrganizations)
|
|
org.POST("/switch/:id", h.SwitchOrganization)
|
|
|
|
org.GET("/current", h.GetCurrentOrganization)
|
|
org.PATCH("/current", m.RequireOrganization(), m.RequirePermission(models.PermManageSettings), h.UpdateOrganization)
|
|
org.GET("/current/limits", m.RequireOrganization(), h.GetOrganizationLimits)
|
|
|
|
org.GET("/members", m.RequireOrganization(), h.GetMembers)
|
|
org.POST("/members/invite", m.RequireOrganization(), m.RequirePermission(models.PermManageTeam), h.InviteMember)
|
|
org.PATCH("/members/:id", m.RequireOrganization(), m.RequirePermission(models.PermManageTeam), h.UpdateMemberRole)
|
|
org.DELETE("/members/:id", m.RequireOrganization(), m.RequirePermission(models.PermManageTeam), h.RemoveMember)
|
|
|
|
org.GET("/invitations", m.RequireOrganization(), m.RequirePermission(models.PermManageTeam), h.GetPendingInvitations)
|
|
org.DELETE("/invitations/:id", m.RequireOrganization(), m.RequirePermission(models.PermManageTeam), h.CancelInvitation)
|
|
|
|
org.POST("/transfer-ownership", m.RequireOrganization(), m.RequirePermission(models.PermTransferOwnership), h.TransferOwnership)
|
|
|
|
org.POST("/avatar", m.RequireOrganization(), h.UploadOrganizationAvatar)
|
|
org.DELETE("/avatar", m.RequireOrganization(), h.DeleteOrganizationAvatar)
|
|
|
|
org.GET("/current/danger-zone", m.RequireOrganization(), h.GetOrganizationDangerZone)
|
|
org.POST("/current/danger-zone/delete", m.RequireOrganization(), h.ScheduleOrganizationDeletion)
|
|
org.DELETE("/current/danger-zone/delete", m.RequireOrganization(), h.CancelOrganizationDeletion)
|
|
}
|
|
|
|
account := jwtOnly.Group("/me")
|
|
{
|
|
account.GET("/danger-zone", h.GetAccountDangerZone)
|
|
account.POST("/danger-zone/delete", h.ScheduleAccountDeletion)
|
|
account.DELETE("/danger-zone/delete", h.CancelAccountDeletion)
|
|
}
|
|
|
|
jwtOnly.GET("/invitations", h.GetMyPendingInvitations)
|
|
jwtOnly.POST("/invitations/accept", h.AcceptInvitation)
|
|
|
|
// Websocket bootstrap. The token returned here is single-session.
|
|
jwtOnly.POST("/getaway", h.GenerateWebsocket)
|
|
|
|
subscriptions := jwtOnly.Group("/subscription")
|
|
subscriptions.Use(m.RateLimitMiddleware(models.RateLimitWrite))
|
|
{
|
|
subscriptions.GET("", h.GetSubscription)
|
|
subscriptions.GET("/limits", h.GetSubscriptionLimits)
|
|
subscriptions.GET("/trial", h.GetTrialStatus)
|
|
subscriptions.GET("/features", h.GetFeatureStatus)
|
|
subscriptions.POST("/checkout", h.CreateCheckoutSession)
|
|
subscriptions.POST("/portal", h.CreateBillingPortalSession)
|
|
subscriptions.POST("/cancel", h.CancelSubscription)
|
|
|
|
subscriptions.POST("/change-plan", m.RequireOrganization(), m.RequirePermission(models.PermManageBilling), h.ChangePlan)
|
|
subscriptions.GET("/preview-change", m.RequireOrganization(), m.RequirePermission(models.PermManageBilling), h.PreviewPlanChange)
|
|
|
|
subscriptions.POST("/enterprise-inquiry", h.SubmitEnterpriseInquiry)
|
|
}
|
|
}
|
|
|
|
// Admin routes (requires admin permissions)
|
|
adminRoutes := r.Group("/admin")
|
|
adminRoutes.Use(m.AuthMiddleware(), m.AdminMiddleware())
|
|
{
|
|
// User Management
|
|
adminRoutes.GET("/users", middleware.RequireAdminPermission(models.AdminPermViewUsers), h.AdminSearchUsers)
|
|
adminRoutes.GET("/users/:id", middleware.RequireAdminPermission(models.AdminPermViewUsers), h.AdminGetUser)
|
|
adminRoutes.GET("/users/:id/preview", middleware.RequireAdminPermission(models.AdminPermViewUsers), h.AdminGetUserPreview)
|
|
adminRoutes.POST("/users/:id/ban", middleware.RequireAdminPermission(models.AdminPermBanUsers), h.AdminBanUser)
|
|
adminRoutes.POST("/users/:id/unban", middleware.RequireAdminPermission(models.AdminPermBanUsers), h.AdminUnbanUser)
|
|
adminRoutes.GET("/users/:id/bans", middleware.RequireAdminPermission(models.AdminPermViewUsers), h.AdminGetUserBans)
|
|
adminRoutes.GET("/users/:id/campaigns", middleware.RequireAdminPermission(models.AdminPermViewCampaigns), h.AdminGetUserCampaigns)
|
|
adminRoutes.GET("/users/:id/emails", middleware.RequireAdminPermission(models.AdminPermViewUsers), h.AdminGetUserEmails)
|
|
adminRoutes.GET("/users/:id/rate-limits", middleware.RequireAdminPermission(models.AdminPermManageRateLimits), h.AdminGetUserRateLimits)
|
|
adminRoutes.PATCH("/users/:id/rate-limits", middleware.RequireAdminPermission(models.AdminPermManageRateLimits), h.AdminUpdateUserRateLimits)
|
|
|
|
// Worker Management
|
|
adminRoutes.GET("/workers", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminListWorkers)
|
|
adminRoutes.GET("/workers/:id", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminGetWorker)
|
|
adminRoutes.PATCH("/workers/:id", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminUpdateWorker)
|
|
adminRoutes.GET("/workers/:id/emails", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminGetWorkerEmails)
|
|
adminRoutes.GET("/workers/:id/stats", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminGetWorkerStats)
|
|
adminRoutes.POST("/workers/:id/reassign", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminReassignEmails)
|
|
|
|
// SSH-managed worker lifecycle (admin-driven add / install / restart / logs)
|
|
adminRoutes.GET("/workers/managed", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminListSSHWorkers)
|
|
adminRoutes.POST("/workers", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminCreateWorker)
|
|
adminRoutes.GET("/workers/:id/managed", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminGetSSHWorker)
|
|
adminRoutes.POST("/workers/:id/test", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminTestWorker)
|
|
adminRoutes.POST("/workers/:id/install", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminInstallWorker)
|
|
adminRoutes.POST("/workers/:id/restart", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminRestartWorker)
|
|
adminRoutes.POST("/workers/:id/upgrade", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminUpdateWorkerImage)
|
|
adminRoutes.POST("/workers/:id/uninstall", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminUninstallWorker)
|
|
adminRoutes.POST("/workers/:id/rotate-keys", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminRotateWorkerKeys)
|
|
adminRoutes.GET("/workers/:id/live-status", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminWorkerStatusLive)
|
|
adminRoutes.GET("/workers/:id/logs", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminWorkerLogs)
|
|
adminRoutes.DELETE("/workers/:id", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminDeleteSSHWorker)
|
|
adminRoutes.PUT("/workers/:id/profile", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminAssignWorkerProfile)
|
|
adminRoutes.POST("/workers/:id/apply", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminApplyWorkerConfig)
|
|
adminRoutes.POST("/workers/:id/system-update", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminSystemUpdate)
|
|
adminRoutes.POST("/workers/:id/reboot", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminRebootWorker)
|
|
adminRoutes.POST("/workers/:id/convert-to-dedicated", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminConvertWorkerToDedicated)
|
|
adminRoutes.PUT("/workers/:id/risk-pool", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminSetWorkerRiskPool)
|
|
adminRoutes.POST("/workers/preflight", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminPreflightWorker)
|
|
adminRoutes.GET("/workers/tags", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminListWorkerTags)
|
|
adminRoutes.PUT("/workers/:id/tags", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminSetWorkerTags)
|
|
|
|
// Reusable AWS credentials (gated under AdminPermManageSettings — these
|
|
// hold real production secrets, not just worker assignments).
|
|
adminRoutes.GET("/aws-credentials", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminListAWSCreds)
|
|
adminRoutes.POST("/aws-credentials", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminCreateAWSCreds)
|
|
adminRoutes.GET("/aws-credentials/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminGetAWSCreds)
|
|
adminRoutes.PATCH("/aws-credentials/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminUpdateAWSCreds)
|
|
adminRoutes.DELETE("/aws-credentials/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminDeleteAWSCreds)
|
|
|
|
// Reusable worker profiles
|
|
adminRoutes.GET("/worker-profiles", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminListProfiles)
|
|
adminRoutes.POST("/worker-profiles", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminCreateProfile)
|
|
adminRoutes.GET("/worker-profiles/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminGetProfile)
|
|
adminRoutes.PATCH("/worker-profiles/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminUpdateProfile)
|
|
adminRoutes.DELETE("/worker-profiles/:id", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminDeleteProfile)
|
|
adminRoutes.GET("/worker-profiles/:id/workers", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminListProfileWorkers)
|
|
adminRoutes.POST("/worker-profiles/:id/apply", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminApplyProfile)
|
|
adminRoutes.PUT("/worker-profiles/:id/release", middleware.RequireAdminPermission(models.AdminPermManageSettings), h.AdminSetProfileRelease)
|
|
|
|
// Release auto-update: manual trigger + last-known state for the UI.
|
|
adminRoutes.POST("/releases/check", middleware.RequireAdminPermission(models.AdminPermManageWorkers), h.AdminCheckReleases)
|
|
adminRoutes.GET("/releases/state", middleware.RequireAdminPermission(models.AdminPermViewWorkers), h.AdminReleasesState)
|
|
|
|
// Warmup Management
|
|
adminRoutes.GET("/warmup/pools", middleware.RequireAdminPermission(models.AdminPermViewWarmupPool), h.AdminListWarmupPools)
|
|
adminRoutes.GET("/warmup/health", middleware.RequireAdminPermission(models.AdminPermViewWarmupPool), h.AdminGetWarmupHealthSummary)
|
|
adminRoutes.GET("/warmup/pools/:type/participants", middleware.RequireAdminPermission(models.AdminPermViewWarmupPool), h.AdminGetPoolParticipants)
|
|
adminRoutes.GET("/warmup/blocked", middleware.RequireAdminPermission(models.AdminPermViewWarmupPool), h.AdminListBlockedAccounts)
|
|
adminRoutes.POST("/warmup/block/:accountId", middleware.RequireAdminPermission(models.AdminPermManageWarmupBans), h.AdminBlockAccount)
|
|
adminRoutes.POST("/warmup/unblock/:accountId", middleware.RequireAdminPermission(models.AdminPermManageWarmupBans), h.AdminUnblockAccount)
|
|
|
|
// Warmup Appeals
|
|
adminRoutes.GET("/warmup/appeals", middleware.RequireAdminPermission(models.AdminPermReviewAppeals), h.AdminListAppeals)
|
|
adminRoutes.GET("/warmup/appeals/:id", middleware.RequireAdminPermission(models.AdminPermReviewAppeals), h.AdminGetAppeal)
|
|
adminRoutes.POST("/warmup/appeals/:id/approve", middleware.RequireAdminPermission(models.AdminPermReviewAppeals), h.AdminApproveAppeal)
|
|
adminRoutes.POST("/warmup/appeals/:id/reject", middleware.RequireAdminPermission(models.AdminPermReviewAppeals), h.AdminRejectAppeal)
|
|
|
|
// Campaign Management
|
|
adminRoutes.GET("/campaigns", middleware.RequireAdminPermission(models.AdminPermViewCampaigns), h.AdminSearchCampaigns)
|
|
adminRoutes.GET("/campaigns/:id", middleware.RequireAdminPermission(models.AdminPermViewCampaigns), h.AdminGetCampaign)
|
|
adminRoutes.POST("/campaigns/:id/stop", middleware.RequireAdminPermission(models.AdminPermStopCampaigns), h.AdminStopCampaign)
|
|
|
|
// Analytics Dashboard
|
|
adminRoutes.GET("/analytics/overview", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetPlatformOverview)
|
|
adminRoutes.GET("/analytics/trends", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetAnalyticsTrends)
|
|
adminRoutes.GET("/analytics/emails/daily", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetDailyEmailStats)
|
|
adminRoutes.GET("/analytics/emails/hourly", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetHourlyEmailStats)
|
|
adminRoutes.GET("/analytics/workers/load", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetWorkerLoadStats)
|
|
adminRoutes.GET("/analytics/workers/distribution", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetEmailDistribution)
|
|
adminRoutes.GET("/analytics/users/growth", middleware.RequireAdminPermission(models.AdminPermViewAnalytics), h.AdminGetUserGrowthStats)
|
|
|
|
// Plans Management
|
|
adminRoutes.GET("/plans", middleware.RequireAdminPermission(models.AdminPermManagePlans), h.AdminListPlans)
|
|
adminRoutes.POST("/plans", middleware.RequireAdminPermission(models.AdminPermManagePlans), h.AdminCreatePlan)
|
|
adminRoutes.GET("/plans/:id", middleware.RequireAdminPermission(models.AdminPermManagePlans), h.AdminGetPlan)
|
|
adminRoutes.PATCH("/plans/:id", middleware.RequireAdminPermission(models.AdminPermManagePlans), h.AdminUpdatePlan)
|
|
adminRoutes.DELETE("/plans/:id", middleware.RequireAdminPermission(models.AdminPermManagePlans), h.AdminDeletePlan)
|
|
|
|
// Enterprise Inquiries
|
|
adminRoutes.GET("/enterprise/inquiries", middleware.RequireAdminPermission(models.AdminPermViewEnterpriseInquiries), h.AdminListEnterpriseInquiries)
|
|
adminRoutes.GET("/enterprise/inquiries/:id", middleware.RequireAdminPermission(models.AdminPermViewEnterpriseInquiries), h.AdminGetEnterpriseInquiry)
|
|
adminRoutes.PATCH("/enterprise/inquiries/:id", middleware.RequireAdminPermission(models.AdminPermManageEnterpriseInquiries), h.AdminUpdateEnterpriseInquiry)
|
|
|
|
// Admin Management
|
|
adminRoutes.GET("/admins", middleware.RequireAdminPermission(models.AdminPermGrantAdminAccess), h.AdminListAdmins)
|
|
adminRoutes.POST("/admins/:userId/grant", middleware.RequireAdminPermission(models.AdminPermGrantAdminAccess), h.AdminGrantPermissions)
|
|
adminRoutes.POST("/admins/:userId/revoke", middleware.RequireAdminPermission(models.AdminPermGrantAdminAccess), h.AdminRevokePermissions)
|
|
|
|
// Audit Logs
|
|
adminRoutes.GET("/audit-logs", middleware.RequireAdminPermission(models.AdminPermViewAuditLogs), h.AdminSearchAuditLogs)
|
|
|
|
// Permission list (for admin UI)
|
|
adminRoutes.GET("/permissions", h.AdminGetPermissionList)
|
|
}
|
|
|
|
webhook := r.Group("/webhook")
|
|
webhook.Use(oidcm.Middleware())
|
|
{
|
|
webhook.POST("/campaign", h.HandleCampaignTasks)
|
|
webhook.POST("/email", h.HandleEmailTask)
|
|
webhook.POST("/user-email", h.HandleUserEmailTask)
|
|
}
|
|
|
|
// Stripe webhook (no auth - uses signature verification)
|
|
r.POST("/webhook/stripe", h.HandleStripeWebhook)
|
|
|
|
return r
|
|
}
|