mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-04 00:02:05 +00:00
242 lines
7.4 KiB
Go
242 lines
7.4 KiB
Go
// Slack app endpoints: the three request URLs Slack calls (verified against
|
|
// SLACK_SIGNING_SECRET before anything is parsed) and the dashboard's Slack
|
|
// panel (status, channels, settings, member links).
|
|
package handler
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/app/slackapp"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// slackMaxBody caps what is read from Slack before the signature is checked.
|
|
const slackMaxBody = 1 << 20
|
|
|
|
// slackIngress reads the capped raw body, verifies Slack's signature over it,
|
|
// then hands it to serve. Nothing is parsed before verification.
|
|
func (h *Handler) slackIngress(c *gin.Context, serve func(ctx context.Context, body []byte) (any, error)) {
|
|
if h.SlackService == nil || !h.SlackService.Interactive() {
|
|
errx.JSON(c, slackapp.ErrSlackNotConfigured)
|
|
return
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(c.Request.Body, slackMaxBody+1))
|
|
if err != nil || len(body) > slackMaxBody {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "Request body is missing or too large."))
|
|
return
|
|
}
|
|
if err := h.SlackService.Verify(c.GetHeader("X-Slack-Request-Timestamp"), c.GetHeader("X-Slack-Signature"), body); err != nil {
|
|
errx.JSON(c, errx.New(errx.Unauthorized, "Invalid Slack request signature."))
|
|
return
|
|
}
|
|
out, err := serve(c.Request.Context(), body)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "Unreadable Slack payload."))
|
|
return
|
|
}
|
|
if out == nil {
|
|
c.Status(http.StatusOK)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// SlackEvents — POST /api/v1/integrations/slack/events
|
|
func (h *Handler) SlackEvents(c *gin.Context) {
|
|
h.slackIngress(c, func(ctx context.Context, body []byte) (any, error) {
|
|
return h.SlackService.HandleEvents(ctx, body)
|
|
})
|
|
}
|
|
|
|
// SlackInteractivity — POST /api/v1/integrations/slack/interactivity
|
|
func (h *Handler) SlackInteractivity(c *gin.Context) {
|
|
h.slackIngress(c, func(ctx context.Context, body []byte) (any, error) {
|
|
return h.SlackService.HandleInteractivity(ctx, body)
|
|
})
|
|
}
|
|
|
|
// SlackCommands — POST /api/v1/integrations/slack/commands
|
|
func (h *Handler) SlackCommands(c *gin.Context) {
|
|
h.slackIngress(c, func(ctx context.Context, body []byte) (any, error) {
|
|
return h.SlackService.HandleCommand(ctx, body)
|
|
})
|
|
}
|
|
|
|
// slackCaller resolves the signed-in user and, when required, the org.
|
|
func (h *Handler) slackCaller(c *gin.Context, needOrg bool) (uuid.UUID, uuid.UUID, bool) {
|
|
if h.SlackService == nil {
|
|
errx.JSON(c, slackapp.ErrSlackNotConfigured)
|
|
return uuid.Nil, uuid.Nil, false
|
|
}
|
|
userID, err := middleware.GetUserUUID(c)
|
|
if err != nil {
|
|
errx.JSON(c, errx.ErrUnauthorized)
|
|
return uuid.Nil, uuid.Nil, false
|
|
}
|
|
if !needOrg {
|
|
return uuid.Nil, userID, true
|
|
}
|
|
orgID := middleware.GetOrganizationID(c)
|
|
if orgID == nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
|
|
return uuid.Nil, uuid.Nil, false
|
|
}
|
|
return *orgID, userID, true
|
|
}
|
|
|
|
// GetSlackStatus — GET /v1/integrations/slack/status
|
|
func (h *Handler) GetSlackStatus(c *gin.Context) {
|
|
orgID, userID, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
member, xerr := h.OrganizationService.GetMembership(c.Request.Context(), orgID, userID)
|
|
if xerr != nil || member == nil {
|
|
errx.JSON(c, errx.New(errx.Forbidden, "not a member of this organization"))
|
|
return
|
|
}
|
|
st, xerr := h.SlackService.Status(c.Request.Context(), orgID, userID, member.Permissions.HasPermission(models.PermManageSettings))
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, st)
|
|
}
|
|
|
|
// ListSlackChannels — GET /v1/integrations/slack/channels?q=
|
|
func (h *Handler) ListSlackChannels(c *gin.Context) {
|
|
orgID, _, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
list, more, xerr := h.SlackService.Channels(c.Request.Context(), orgID, c.Query("q"))
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": list, "pagination": gin.H{"next_cursor": nil, "has_more": more}})
|
|
}
|
|
|
|
// UpdateSlackSettings — PUT /v1/integrations/slack/settings
|
|
func (h *Handler) UpdateSlackSettings(c *gin.Context) {
|
|
orgID, _, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req models.SlackSettings
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.InvalidBody(err))
|
|
return
|
|
}
|
|
st, xerr := h.SlackService.UpdateSettings(c.Request.Context(), orgID, req)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityIntegration, nil, nil, map[string]string{"slack": "settings"})
|
|
c.JSON(http.StatusOK, st)
|
|
}
|
|
|
|
// PreviewSlackLink — GET /v1/integrations/slack/link/:code
|
|
func (h *Handler) PreviewSlackLink(c *gin.Context) {
|
|
_, userID, ok := h.slackCaller(c, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
p, xerr := h.SlackService.PreviewLink(c.Request.Context(), userID, c.Param("code"))
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, p)
|
|
}
|
|
|
|
// ConfirmSlackLink — POST /v1/integrations/slack/link
|
|
func (h *Handler) ConfirmSlackLink(c *gin.Context) {
|
|
_, userID, ok := h.slackCaller(c, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
Code string `json:"code" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.InvalidBody(err))
|
|
return
|
|
}
|
|
link, xerr := h.SlackService.ConfirmLink(c.Request.Context(), userID, req.Code)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
// The org comes from the code, so the audit row is written against it.
|
|
if h.AuditService != nil {
|
|
h.AuditService.LogAction(c.Request.Context(), link.OrganizationID, userID, models.AuditActionCreate, models.AuditEntityIntegration,
|
|
&link.ConnectionID, c.ClientIP(), c.Request.UserAgent(), nil, map[string]string{"slack_link": "linked"})
|
|
}
|
|
c.JSON(http.StatusCreated, link)
|
|
}
|
|
|
|
// UpdateMySlackLink — PATCH /v1/integrations/slack/link
|
|
func (h *Handler) UpdateMySlackLink(c *gin.Context) {
|
|
orgID, userID, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
DMNotifications *bool `json:"dm_notifications" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.InvalidBody(err))
|
|
return
|
|
}
|
|
link, xerr := h.SlackService.UpdateMyLink(c.Request.Context(), orgID, userID, *req.DMNotifications)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityIntegration, &link.ConnectionID, nil, map[string]string{"slack_link": "dm_notifications"})
|
|
c.JSON(http.StatusOK, link)
|
|
}
|
|
|
|
// DeleteMySlackLink — DELETE /v1/integrations/slack/link
|
|
func (h *Handler) DeleteMySlackLink(c *gin.Context) {
|
|
orgID, userID, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if xerr := h.SlackService.UnlinkMine(c.Request.Context(), orgID, userID); xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionDelete, models.AuditEntityIntegration, nil, nil, map[string]string{"slack_link": "unlinked"})
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// RemoveSlackLink — DELETE /v1/integrations/slack/links/:id
|
|
func (h *Handler) RemoveSlackLink(c *gin.Context) {
|
|
orgID, _, ok := h.slackCaller(c, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.ErrUuid)
|
|
return
|
|
}
|
|
link, xerr := h.SlackService.RemoveLink(c.Request.Context(), orgID, id)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionDelete, models.AuditEntityIntegration, &link.ConnectionID, nil, map[string]string{"slack_link": "removed"})
|
|
c.Status(http.StatusNoContent)
|
|
}
|