From b0f60d8de53b75517063643c2b1571f65c0201d2 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 23 Oct 2025 09:32:55 +0200 Subject: [PATCH] Refactor slack messages Signed-off-by: Sacha Al Himdani --- pkg/coredata/entity_type_reg.go | 1 - pkg/coredata/migrations/20251020T163914Z.sql | 18 +- pkg/coredata/slack_message.go | 280 +++++++++-- pkg/coredata/slack_message_update.go | 182 ------- pkg/server/api/trust/v1/slack_handler.go | 93 ++-- pkg/slack/sender.go | 76 +-- pkg/trust/slack_message_service.go | 477 +++++++++++-------- pkg/trust/templates/access-request.json.tmpl | 2 +- pkg/trust/trust_center_access_service.go | 11 +- 9 files changed, 611 insertions(+), 529 deletions(-) delete mode 100644 pkg/coredata/slack_message_update.go diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 6794eeb1c..8c402b035 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -62,5 +62,4 @@ const ( InvitationEntityType MembershipEntityType SlackMessageEntityType - SlackMessageUpdateEntityType ) diff --git a/pkg/coredata/migrations/20251020T163914Z.sql b/pkg/coredata/migrations/20251020T163914Z.sql index 6a7bda16c..44537cefc 100644 --- a/pkg/coredata/migrations/20251020T163914Z.sql +++ b/pkg/coredata/migrations/20251020T163914Z.sql @@ -5,17 +5,7 @@ ALTER TABLE slack_messages ADD COLUMN message_ts TEXT; ALTER TABLE slack_messages ADD COLUMN channel_id TEXT; ALTER TABLE slack_messages ADD COLUMN requester_email TEXT; ALTER TABLE slack_messages ADD COLUMN type slack_message_type NOT NULL; - -ALTER TABLE slack_messages ADD CONSTRAINT unique_slack_message_org_ts_channel UNIQUE (organization_id, message_ts, channel_id); - -CREATE TABLE slack_message_updates ( - id TEXT PRIMARY KEY, - tenant_id TEXT NOT NULL, - slack_message_id TEXT NOT NULL, - body JSONB NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - sent_at TIMESTAMP WITH TIME ZONE, - error TEXT, - CONSTRAINT fk_slack_message_updates_slack_message_id FOREIGN KEY (slack_message_id) REFERENCES slack_messages(id) ON UPDATE CASCADE ON DELETE CASCADE -); +ALTER TABLE slack_messages ADD COLUMN metadata JSONB; +ALTER TABLE slack_messages ADD COLUMN initial_slack_message_id TEXT NOT NULL; +ALTER TABLE slack_messages ADD CONSTRAINT fk_slack_messages_initial_slack_message_id + FOREIGN KEY (initial_slack_message_id) REFERENCES slack_messages(id) ON DELETE CASCADE; diff --git a/pkg/coredata/slack_message.go b/pkg/coredata/slack_message.go index 6f2025c1b..84d741989 100644 --- a/pkg/coredata/slack_message.go +++ b/pkg/coredata/slack_message.go @@ -28,26 +28,34 @@ import ( type ( SlackMessage struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Type SlackMessageType `db:"type"` - Body map[string]any `db:"body"` - MessageTS *string `db:"message_ts"` - ChannelID *string `db:"channel_id"` - RequesterEmail *string `db:"requester_email"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - SentAt *time.Time `db:"sent_at"` - Error *string `db:"error"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Type SlackMessageType `db:"type"` + Body map[string]any `db:"body"` + MessageTS *string `db:"message_ts"` + ChannelID *string `db:"channel_id"` + RequesterEmail *string `db:"requester_email"` + Metadata map[string]any `db:"metadata"` + InitialSlackMessageID gid.GID `db:"initial_slack_message_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + SentAt *time.Time `db:"sent_at"` + Error *string `db:"error"` } ErrNoUnsentSlackMessage struct{} + + ErrSlackMessageNotFound struct{} ) func (e ErrNoUnsentSlackMessage) Error() string { return "no unsent slack message found" } +func (e ErrSlackMessageNotFound) Error() string { + return "slack message not found" +} + func NewSlackMessage( scope Scoper, organizationID gid.GID, @@ -56,14 +64,16 @@ func NewSlackMessage( requesterEmail *string, ) *SlackMessage { now := time.Now() + id := gid.New(scope.GetTenantID(), SlackMessageEntityType) return &SlackMessage{ - ID: gid.New(scope.GetTenantID(), SlackMessageEntityType), - OrganizationID: organizationID, - Type: messageType, - Body: body, - RequesterEmail: requesterEmail, - CreatedAt: now, - UpdatedAt: now, + ID: id, + OrganizationID: organizationID, + Type: messageType, + Body: body, + RequesterEmail: requesterEmail, + InitialSlackMessageID: id, + CreatedAt: now, + UpdatedAt: now, } } @@ -73,19 +83,43 @@ func (s *SlackMessage) Insert( scope Scoper, ) error { q := ` -INSERT INTO slack_messages (id, tenant_id, organization_id, type, body, requester_email, created_at, updated_at) -VALUES (@id, @tenant_id, @organization_id, @type, @body, @requester_email, @created_at, @updated_at) + INSERT INTO slack_messages ( + id, + tenant_id, + organization_id, + type, + body, + requester_email, + metadata, + initial_slack_message_id, + created_at, + updated_at + ) + VALUES ( + @id, + @tenant_id, + @organization_id, + @type, + @body, + @requester_email, + @metadata, + @initial_slack_message_id, + @created_at, + @updated_at + ) ` args := pgx.StrictNamedArgs{ - "id": s.ID, - "tenant_id": scope.GetTenantID(), - "organization_id": s.OrganizationID, - "type": s.Type, - "body": s.Body, - "requester_email": s.RequesterEmail, - "created_at": s.CreatedAt, - "updated_at": s.UpdatedAt, + "id": s.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": s.OrganizationID, + "type": s.Type, + "body": s.Body, + "requester_email": s.RequesterEmail, + "metadata": s.Metadata, + "initial_slack_message_id": s.InitialSlackMessageID, + "created_at": s.CreatedAt, + "updated_at": s.UpdatedAt, } _, err := conn.Exec(ctx, q, args) @@ -101,7 +135,7 @@ func (s *SlackMessage) LoadNextUnsentForUpdate( conn pg.Conn, ) error { q := ` -SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, created_at, updated_at, sent_at, error +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error FROM slack_messages WHERE sent_at IS NULL AND error IS NULL ORDER BY created_at ASC @@ -128,17 +162,99 @@ FOR UPDATE return nil } -// This is used for Slack webhook verification where we don't know the tenant yet -func (s *SlackMessage) LoadByChannelAndTSUnscoped( +func (s *SlackMessage) LoadNextInitalUnsentForUpdate( ctx context.Context, conn pg.Conn, +) error { + q := ` +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error +FROM slack_messages +WHERE sent_at IS NULL AND error IS NULL AND id = initial_slack_message_id +ORDER BY created_at ASC +LIMIT 1 +FOR UPDATE + ` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query slack messages: %w", err) + } + + message, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessage]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNoUnsentSlackMessage{} + } + + return fmt.Errorf("cannot collect slack message: %w", err) + } + + *s = message + + return nil +} + +func (s *SlackMessage) LoadNextUpdateUnsentForUpdate( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT + sm.id, + sm.organization_id, + sm.type, + sm.body, + COALESCE(sm.message_ts, original.message_ts) as message_ts, + COALESCE(sm.channel_id, original.channel_id) as channel_id, + sm.requester_email, + sm.metadata, + sm.initial_slack_message_id, + sm.created_at, + sm.updated_at, + sm.sent_at, + sm.error +FROM slack_messages sm +INNER JOIN slack_messages original ON sm.initial_slack_message_id = original.id +WHERE sm.sent_at IS NULL + AND sm.error IS NULL + AND sm.id != sm.initial_slack_message_id + AND original.sent_at IS NOT NULL + AND original.error IS NULL +ORDER BY sm.created_at ASC +LIMIT 1 +FOR UPDATE OF sm + ` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query slack messages: %w", err) + } + + message, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessage]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNoUnsentSlackMessage{} + } + + return fmt.Errorf("cannot collect slack message: %w", err) + } + + *s = message + + return nil +} + +func (s *SlackMessage) LoadInitialByChannelAndTS( + ctx context.Context, + conn pg.Conn, + scope Scoper, channelID string, messageTS string, ) error { q := ` -SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, created_at, updated_at, sent_at, error +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error FROM slack_messages -WHERE message_ts = @message_ts AND channel_id = @channel_id +WHERE message_ts = @message_ts AND channel_id = @channel_id AND id = initial_slack_message_id AND %s LIMIT 1 ` @@ -146,6 +262,9 @@ LIMIT 1 "message_ts": messageTS, "channel_id": channelID, } + maps.Copy(args, scope.SQLArguments()) + + q = fmt.Sprintf(q, scope.SQLFragment()) rows, err := conn.Query(ctx, q, args) if err != nil { @@ -168,23 +287,25 @@ LIMIT 1 func (s *SlackMessage) Update( ctx context.Context, conn pg.Conn, + scope Scoper, ) error { q := ` UPDATE slack_messages -SET body = @body, sent_at = @sent_at, updated_at = @updated_at, error = @error, message_ts = @message_ts, channel_id = @channel_id -WHERE id = @id +SET sent_at = @sent_at, updated_at = @updated_at, error = @error +WHERE id = @id AND %s ` args := pgx.StrictNamedArgs{ "id": s.ID, - "body": s.Body, "sent_at": s.SentAt, "updated_at": s.UpdatedAt, "error": s.Error, - "message_ts": s.MessageTS, - "channel_id": s.ChannelID, } + q = fmt.Sprintf(q, scope.SQLFragment()) + + maps.Copy(args, scope.SQLArguments()) + _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot update slack message: %w", err) @@ -193,6 +314,40 @@ WHERE id = @id return nil } +func (s *SlackMessage) UpdateChannelAndTSByInitialMessageID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + initialSlackMessageID gid.GID, + channelID string, + messageTS string, + updatedAt time.Time, +) error { + q := ` +UPDATE slack_messages +SET channel_id = @channel_id, message_ts = @message_ts, updated_at = @updated_at +WHERE initial_slack_message_id = @initial_slack_message_id AND %s + ` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "initial_slack_message_id": initialSlackMessageID, + "channel_id": channelID, + "message_ts": messageTS, + "updated_at": updatedAt, + } + + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update slack messages with initial message id: %w", err) + } + + return nil +} + func (s *SlackMessage) LoadById( ctx context.Context, conn pg.Conn, @@ -200,13 +355,15 @@ func (s *SlackMessage) LoadById( slackMessageID gid.GID, ) error { q := ` -SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, created_at, updated_at, sent_at, error +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error FROM slack_messages WHERE id = @id AND %s LIMIT 1 ` + q = fmt.Sprintf(q, scope.SQLFragment()) + args := pgx.StrictNamedArgs{ "id": slackMessageID, } @@ -230,6 +387,46 @@ LIMIT 1 return nil } +func (s *SlackMessage) LoadLatestByInitialMessageID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + initialSlackMessageID gid.GID, +) error { + q := ` +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error +FROM slack_messages +WHERE %s + AND initial_slack_message_id = @initial_slack_message_id +ORDER BY created_at DESC +LIMIT 1 + ` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "initial_slack_message_id": initialSlackMessageID, + } + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query slack message: %w", err) + } + + message, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessage]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrSlackMessageNotFound{} + } + return err + } + + *s = message + + return nil +} + func (s *SlackMessage) LoadLatestByRequesterEmailAndType( ctx context.Context, conn pg.Conn, @@ -240,7 +437,7 @@ func (s *SlackMessage) LoadLatestByRequesterEmailAndType( since time.Time, ) error { q := ` -SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, created_at, updated_at, sent_at, error +SELECT id, organization_id, type, body, message_ts, channel_id, requester_email, metadata, initial_slack_message_id, created_at, updated_at, sent_at, error FROM slack_messages WHERE %s AND organization_id = @organization_id @@ -268,6 +465,9 @@ LIMIT 1 message, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessage]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrSlackMessageNotFound{} + } return err } diff --git a/pkg/coredata/slack_message_update.go b/pkg/coredata/slack_message_update.go deleted file mode 100644 index ff1b75936..000000000 --- a/pkg/coredata/slack_message_update.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package coredata - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/getprobo/probo/pkg/gid" - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" -) - -type ( - SlackMessageUpdate struct { - ID gid.GID `db:"id"` - SlackMessageID gid.GID `db:"slack_message_id"` - Body map[string]any `db:"body"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - SentAt *time.Time `db:"sent_at"` - Error *string `db:"error"` - } - - ErrNoUnsentSlackMessageUpdate struct{} -) - -func (e ErrNoUnsentSlackMessageUpdate) Error() string { - return "no unsent slack message update found" -} - -func NewSlackMessageUpdate( - scope Scoper, - slackMessageID gid.GID, - body map[string]any, -) *SlackMessageUpdate { - now := time.Now() - return &SlackMessageUpdate{ - ID: gid.New(scope.GetTenantID(), SlackMessageUpdateEntityType), - SlackMessageID: slackMessageID, - Body: body, - CreatedAt: now, - UpdatedAt: now, - } -} - -func (s *SlackMessageUpdate) Insert( - ctx context.Context, - conn pg.Conn, - scope Scoper, -) error { - q := ` -INSERT INTO slack_message_updates (id, tenant_id, slack_message_id, body, created_at, updated_at, sent_at, error) -VALUES (@id, @tenant_id, @slack_message_id, @body, @created_at, @updated_at, @sent_at, @error) - ` - - args := pgx.StrictNamedArgs{ - "id": s.ID, - "tenant_id": scope.GetTenantID(), - "slack_message_id": s.SlackMessageID, - "body": s.Body, - "created_at": s.CreatedAt, - "updated_at": s.UpdatedAt, - "sent_at": s.SentAt, - "error": s.Error, - } - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot insert slack message update: %w", err) - } - - return nil -} - -func (s *SlackMessageUpdate) LoadLatestBySlackMessageID( - ctx context.Context, - conn pg.Conn, - slackMessageID gid.GID, -) error { - q := ` -SELECT id, slack_message_id, body, created_at, updated_at, sent_at, error -FROM slack_message_updates -WHERE slack_message_id = @slack_message_id -ORDER BY created_at DESC -LIMIT 1 - ` - - args := pgx.StrictNamedArgs{ - "slack_message_id": slackMessageID, - } - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query slack message updates: %w", err) - } - - update, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessageUpdate]) - if err != nil { - return err - } - - *s = update - - return nil -} - -func (s *SlackMessageUpdate) LoadNextUnsentForUpdate( - ctx context.Context, - conn pg.Conn, -) error { - q := ` -SELECT smu.id, smu.slack_message_id, smu.body, smu.created_at, smu.updated_at, smu.sent_at, smu.error -FROM slack_message_updates smu -INNER JOIN slack_messages sm ON smu.slack_message_id = sm.id -WHERE smu.sent_at IS NULL - AND smu.error IS NULL - AND sm.sent_at IS NOT NULL - AND sm.error IS NULL -ORDER BY smu.created_at ASC -LIMIT 1 -FOR UPDATE OF smu - ` - - rows, err := conn.Query(ctx, q) - if err != nil { - return fmt.Errorf("cannot query slack message updates: %w", err) - } - - update, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessageUpdate]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrNoUnsentSlackMessageUpdate{} - } - - return fmt.Errorf("cannot collect slack message update: %w", err) - } - - *s = update - - return nil -} - -func (s *SlackMessageUpdate) Update( - ctx context.Context, - conn pg.Conn, -) error { - q := ` -UPDATE slack_message_updates -SET body = @body, updated_at = @updated_at, sent_at = @sent_at, error = @error -WHERE id = @id - ` - - args := pgx.StrictNamedArgs{ - "id": s.ID, - "body": s.Body, - "updated_at": s.UpdatedAt, - "sent_at": s.SentAt, - "error": s.Error, - } - - _, err := conn.Exec(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot update slack message update: %w", err) - } - - return nil -} diff --git a/pkg/server/api/trust/v1/slack_handler.go b/pkg/server/api/trust/v1/slack_handler.go index a12bc35a9..f7ec6cfc1 100644 --- a/pkg/server/api/trust/v1/slack_handler.go +++ b/pkg/server/api/trust/v1/slack_handler.go @@ -123,37 +123,45 @@ func slackHandler(trustSvc *trust.Service, slackSigningSecret string, logger *lo return } + initialSlackMessage, err := trustSvc.GetInitialSlackMessageByChannelAndTS(ctx, slackPayload.Container.ChannelID, slackPayload.Container.MessageTS) + if err != nil { + logger.ErrorCtx(ctx, "cannot load slack message", log.Error(err)) + httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return + } + + //TODO: Update the message when it is too old to be updated + fourteenDaysAgo := time.Now().Add(-14 * 24 * time.Hour) + if initialSlackMessage.CreatedAt.Before(fourteenDaysAgo) { + httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "this message is too old to be updated (older than 14 days)"}) + return + } + + if initialSlackMessage.RequesterEmail == nil || *initialSlackMessage.RequesterEmail == "" { + logger.ErrorCtx(ctx, "missing requester email", log.String("slack_message_id", initialSlackMessage.ID.String())) + httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return + } + requesterEmail := *initialSlackMessage.RequesterEmail + + tenantSvc := trustSvc.WithTenant(initialSlackMessage.OrganizationID.TenantID()) + var documentIDs []gid.GID var reportIDs []gid.GID switch action.ActionID { case "accept_all": - var acceptAllData struct { - DocumentIDs []string `json:"document_ids"` - ReportIDs []string `json:"report_ids"` - } - if err := json.NewDecoder(strings.NewReader(action.Value)).Decode(&acceptAllData); err != nil { - logger.ErrorCtx(ctx, "failed to parse accept_all value", log.Error(err)) - httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "invalid accept_all value"}) + currentMessageId, err := gid.ParseGID(action.Value) + if err != nil { + httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "invalid message ID"}) return } - for _, idStr := range acceptAllData.DocumentIDs { - docID, err := gid.ParseGID(idStr) - if err != nil { - httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "invalid document ID"}) - return - } - documentIDs = append(documentIDs, docID) - } - - for _, idStr := range acceptAllData.ReportIDs { - repID, err := gid.ParseGID(idStr) - if err != nil { - httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "invalid report ID"}) - return - } - reportIDs = append(reportIDs, repID) + documentIDs, reportIDs, err = tenantSvc.SlackMessages.GetSlackMessageMetadataByID(ctx, currentMessageId) + if err != nil { + logger.ErrorCtx(ctx, "cannot load slack message metadata by ID", log.Error(err)) + httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) + return } case "accept_document": @@ -177,41 +185,9 @@ func slackHandler(trustSvc *trust.Service, slackSigningSecret string, logger *lo return } - // Load the slack message without tenant scope - slackMessage, err := trustSvc.WithTenant(gid.TenantID{}).SlackMessages.LoadSlackMessageUnscoped( - ctx, - slackPayload.Container.ChannelID, - slackPayload.Container.MessageTS, - ) - if err != nil { - logger.ErrorCtx(ctx, "cannot load slack message", log.Error(err)) - httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) - return - } - - fourteenDaysAgo := time.Now().Add(-14 * 24 * time.Hour) - if slackMessage.CreatedAt.Before(fourteenDaysAgo) { - httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "this message is too old to be updated (older than 14 days)"}) - return - } - - if slackMessage.RequesterEmail == nil || *slackMessage.RequesterEmail == "" { - httpserver.RenderJSON(w, http.StatusBadRequest, SlackInteractiveResponse{Success: false, Message: "missing requester_email"}) - return - } - requesterEmail := *slackMessage.RequesterEmail - - tenantSvc := trustSvc.WithTenant(slackMessage.OrganizationID.TenantID()) - trustCenter, err := tenantSvc.TrustCenters.GetByOrganizationID(ctx, slackMessage.OrganizationID) - if err != nil { - logger.ErrorCtx(ctx, "cannot load trust center", log.Error(err)) - httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) - return - } - if err := tenantSvc.TrustCenterAccesses.AcceptByIDs( ctx, - trustCenter.ID, + initialSlackMessage.OrganizationID, requesterEmail, documentIDs, reportIDs, @@ -223,10 +199,9 @@ func slackHandler(trustSvc *trust.Service, slackSigningSecret string, logger *lo if err := tenantSvc.SlackMessages.UpdateSlackAccessMessage( ctx, - slackMessage.ID, - action.ActionID, - action.Value, + initialSlackMessage.ID, slackPayload.ResponseURL, + requesterEmail, ); err != nil { logger.ErrorCtx(ctx, "failed to update Slack message", log.Error(err)) httpserver.RenderJSON(w, http.StatusInternalServerError, SlackInteractiveResponse{Success: false, Message: "internal server error"}) diff --git a/pkg/slack/sender.go b/pkg/slack/sender.go index 24cb3fef8..f525bb90a 100644 --- a/pkg/slack/sender.go +++ b/pkg/slack/sender.go @@ -81,7 +81,8 @@ func (s *Sender) batchSendMessages(ctx context.Context) error { message.Error = &panicErr message.UpdatedAt = time.Now() - if updateErr := message.Update(ctx, tx); updateErr != nil { + scope := coredata.NewScope(message.ID.TenantID()) + if updateErr := message.Update(ctx, tx, scope); updateErr != nil { s.logger.ErrorCtx(ctx, "cannot update slack message after panic", log.Error(updateErr)) } @@ -90,24 +91,32 @@ func (s *Sender) batchSendMessages(ctx context.Context) error { } }() - err = message.LoadNextUnsentForUpdate(ctx, tx) + err = message.LoadNextInitalUnsentForUpdate(ctx, tx) if err != nil { return err } + scope := coredata.NewScope(message.ID.TenantID()) channelID, messageTS, sendErr := s.sendMessage(ctx, tx, message) - message.ChannelID = channelID - message.MessageTS = messageTS now := time.Now() message.UpdatedAt = now + if channelID != nil && messageTS != nil { + message.ChannelID = channelID + message.MessageTS = messageTS + message.UpdatedAt = now + + if err := message.UpdateChannelAndTSByInitialMessageID(ctx, tx, scope, message.ID, *channelID, *messageTS, now); err != nil { + return fmt.Errorf("cannot update all messages with initial message id: %w", err) + } + } + if sendErr != nil { errorMsg := sendErr.Error() message.Error = &errorMsg - message.UpdatedAt = time.Now() - if err := message.Update(ctx, tx); err != nil { + if err := message.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update slack message with error: %w", err) } @@ -117,7 +126,7 @@ func (s *Sender) batchSendMessages(ctx context.Context) error { message.SentAt = &now - if err := message.Update(ctx, tx); err != nil { + if err := message.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update slack message: %w", err) } @@ -196,62 +205,59 @@ func (s *Sender) batchUpdateMessages(ctx context.Context) error { err := s.pg.WithTx( ctx, func(tx pg.Conn) (err error) { - update := &coredata.SlackMessageUpdate{} + updateMessage := &coredata.SlackMessage{} defer func() { if r := recover(); r != nil { panicErr := fmt.Sprintf("panic recovered: %v", r) - update.Error = &panicErr - update.UpdatedAt = time.Now() + updateMessage.Error = &panicErr + updateMessage.UpdatedAt = time.Now() - if updateErr := update.Update(ctx, tx); updateErr != nil { - s.logger.ErrorCtx(ctx, "cannot update slack message update after panic", log.Error(updateErr)) + scope := coredata.NewScope(updateMessage.ID.TenantID()) + if updateErr := updateMessage.Update(ctx, tx, scope); updateErr != nil { + s.logger.ErrorCtx(ctx, "cannot update slack message after panic", log.Error(updateErr)) } - s.logger.ErrorCtx(ctx, "panic while updating slack message", log.String("error", panicErr), log.String("update_id", update.ID.String())) + s.logger.ErrorCtx(ctx, "panic while updating slack message", log.String("error", panicErr), log.String("message_id", updateMessage.ID.String())) err = fmt.Errorf("panic recovered: %v", r) } }() - err = update.LoadNextUnsentForUpdate(ctx, tx) + err = updateMessage.LoadNextUpdateUnsentForUpdate(ctx, tx) if err != nil { return err } - message := &coredata.SlackMessage{} - if err := message.LoadById(ctx, tx, coredata.NewScope(update.SlackMessageID.TenantID()), update.SlackMessageID); err != nil { - return fmt.Errorf("cannot load slack message: %w", err) - } - - updateErr := s.updateMessage(ctx, tx, message, update) + scope := coredata.NewScope(updateMessage.ID.TenantID()) + updateErr := s.updateMessage(ctx, tx, updateMessage) now := time.Now() - update.UpdatedAt = now + updateMessage.UpdatedAt = now if updateErr != nil { errorMsg := updateErr.Error() - update.Error = &errorMsg - update.UpdatedAt = time.Now() + updateMessage.Error = &errorMsg + updateMessage.UpdatedAt = time.Now() - if err := update.Update(ctx, tx); err != nil { - return fmt.Errorf("cannot update slack message update with error: %w", err) + if err := updateMessage.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update slack message with error: %w", err) } - s.logger.ErrorCtx(ctx, "error updating slack message", log.Error(updateErr), log.String("update_id", update.ID.String())) + s.logger.ErrorCtx(ctx, "error updating slack message", log.Error(updateErr), log.String("message_id", updateMessage.ID.String())) return nil } - update.SentAt = &now + updateMessage.SentAt = &now - if err := update.Update(ctx, tx); err != nil { - return fmt.Errorf("cannot update slack message update: %w", err) + if err := updateMessage.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update slack message: %w", err) } return nil }, ) - if errors.Is(err, coredata.ErrNoUnsentSlackMessageUpdate{}) { + if errors.Is(err, coredata.ErrNoUnsentSlackMessage{}) { return nil } @@ -261,12 +267,12 @@ func (s *Sender) batchUpdateMessages(ctx context.Context) error { } } -func (s *Sender) updateMessage(ctx context.Context, tx pg.Conn, message *coredata.SlackMessage, update *coredata.SlackMessageUpdate) error { - if message.ChannelID == nil || message.MessageTS == nil { +func (s *Sender) updateMessage(ctx context.Context, tx pg.Conn, updateMessage *coredata.SlackMessage) error { + if updateMessage.ChannelID == nil || updateMessage.MessageTS == nil { return fmt.Errorf("slack message has no channel ID or message TS") } - tenantID := message.ID.TenantID() + tenantID := updateMessage.ID.TenantID() scope := coredata.NewScope(tenantID) var connectors coredata.Connectors @@ -274,7 +280,7 @@ func (s *Sender) updateMessage(ctx context.Context, tx pg.Conn, message *coredat ctx, tx, scope, - message.OrganizationID, + updateMessage.OrganizationID, coredata.ConnectorProtocolOAuth2, coredata.ConnectorProviderSlack, s.encryptionKey, @@ -302,7 +308,7 @@ func (s *Sender) updateMessage(ctx context.Context, tx pg.Conn, message *coredat client := NewClient(s.logger) - if err := client.UpdateMessage(ctx, slackConn.AccessToken, *message.ChannelID, *message.MessageTS, update.Body); err != nil { + if err := client.UpdateMessage(ctx, slackConn.AccessToken, *updateMessage.ChannelID, *updateMessage.MessageTS, updateMessage.Body); err != nil { s.logger.ErrorCtx(ctx, "failed to update message on Slack", log.Error(err)) return fmt.Errorf("failed to update message on Slack: %w", err) } diff --git a/pkg/trust/slack_message_service.go b/pkg/trust/slack_message_service.go index de672d5c7..180480595 100644 --- a/pkg/trust/slack_message_service.go +++ b/pkg/trust/slack_message_service.go @@ -18,8 +18,8 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" - "maps" "time" "github.com/getprobo/probo/pkg/coredata" @@ -33,20 +33,47 @@ const ( trustCenterAccessURLFormat = "https://%s/organizations/%s/trust-center/access" ) -type SlackMessageService struct { - svc *TenantService - slackClient *slack.Client +type ( + SlackMessageService struct { + svc *TenantService + slackClient *slack.Client + } + + SlackMessageDocument struct { + ID string + Title string + Granted bool + } + + SlackMessageReport struct { + ID string + Title string + AuditID string + Granted bool + } + + SlackMessageMetadata struct { + Documents []SlackMessageDocument + Reports []SlackMessageReport + } +) + +func (m SlackMessageMetadata) toMap() map[string]any { + return map[string]any{ + "documents": m.Documents, + "reports": m.Reports, + } } -func (s *SlackMessageService) LoadSlackMessageUnscoped( +func (s *Service) GetInitialSlackMessageByChannelAndTS( ctx context.Context, channelID string, messageTS string, ) (*coredata.SlackMessage, error) { var slackMessage coredata.SlackMessage - err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { - if err := slackMessage.LoadByChannelAndTSUnscoped(ctx, conn, channelID, messageTS); err != nil { + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + if err := slackMessage.LoadInitialByChannelAndTS(ctx, conn, coredata.NewNoScope(), channelID, messageTS); err != nil { return fmt.Errorf("cannot load slack message: %w", err) } @@ -60,12 +87,74 @@ func (s *SlackMessageService) LoadSlackMessageUnscoped( return &slackMessage, nil } +func (s *SlackMessageService) GetSlackMessageMetadataByID( + ctx context.Context, + slackMessageID gid.GID, +) (documentIDs []gid.GID, reportIDs []gid.GID, err error) { + var slackMessage coredata.SlackMessage + + err = s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + if err := slackMessage.LoadById(ctx, conn, s.svc.scope, slackMessageID); err != nil { + return fmt.Errorf("cannot load slack message: %w", err) + } + + return nil + }) + + if err != nil { + return nil, nil, err + } + + documents, ok := slackMessage.Metadata["documents"].([]any) + if !ok { + return nil, nil, fmt.Errorf("invalid documents metadata") + } + + for _, docAny := range documents { + doc, ok := docAny.(map[string]any) + if !ok { + continue + } + idStr, ok := doc["ID"].(string) + if !ok { + continue + } + docID, err := gid.ParseGID(idStr) + if err != nil { + continue + } + documentIDs = append(documentIDs, docID) + } + + reports, ok := slackMessage.Metadata["reports"].([]any) + if !ok { + return nil, nil, fmt.Errorf("invalid reports metadata") + } + + for _, repAny := range reports { + rep, ok := repAny.(map[string]any) + if !ok { + continue + } + idStr, ok := rep["ID"].(string) + if !ok { + continue + } + repID, err := gid.ParseGID(idStr) + if err != nil { + continue + } + reportIDs = append(reportIDs, repID) + } + + return documentIDs, reportIDs, nil +} + func (s *SlackMessageService) UpdateSlackAccessMessage( ctx context.Context, slackMessageID gid.GID, - actionID string, - value string, responseURL string, + requesterEmail string, ) error { return s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { var slackMessage coredata.SlackMessage @@ -73,20 +162,58 @@ func (s *SlackMessageService) UpdateSlackAccessMessage( return fmt.Errorf("cannot load slack message: %w", err) } - baseBody := slackMessage.Body - var latestUpdate coredata.SlackMessageUpdate - if err := latestUpdate.LoadLatestBySlackMessageID(ctx, tx, slackMessage.ID); err == nil { - baseBody = latestUpdate.Body + var trustCenter coredata.TrustCenter + if err := trustCenter.LoadByOrganizationID(ctx, tx, s.svc.scope, slackMessage.OrganizationID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) } - accessTabURL := fmt.Sprintf(trustCenterAccessURLFormat, s.svc.hostname, slackMessage.OrganizationID) - updatedBody := s.changeButton(baseBody, actionID, value, accessTabURL) + var trustCenterAccess coredata.TrustCenterAccess + if err := trustCenterAccess.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, trustCenter.ID, requesterEmail); err != nil { + return fmt.Errorf("cannot load trust center access: %w", err) + } + + documents, reports, err := s.loadDocumentsAndReportsFromAccesses(ctx, tx, trustCenterAccess.ID) + if err != nil { + return err + } + + newSlackMessageID := gid.New(s.svc.scope.GetTenantID(), coredata.SlackMessageEntityType) + + updatedBody, err := s.buildAccessRequestMessage( + newSlackMessageID, + trustCenterAccess.Name, + requesterEmail, + trustCenter.OrganizationID, + documents, + reports, + ) + if err != nil { + return err + } + + metadata := SlackMessageMetadata{ + Documents: documents, + Reports: reports, + } - slackMessageUpdate := coredata.NewSlackMessageUpdate(s.svc.scope, slackMessage.ID, updatedBody) now := time.Now() - slackMessageUpdate.SentAt = &now - if err := slackMessageUpdate.Insert(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert slack message update: %w", err) + newSlackMessage := &coredata.SlackMessage{ + ID: newSlackMessageID, + OrganizationID: slackMessage.OrganizationID, + Type: slackMessage.Type, + Body: updatedBody, + MessageTS: slackMessage.MessageTS, + ChannelID: slackMessage.ChannelID, + RequesterEmail: slackMessage.RequesterEmail, + Metadata: metadata.toMap(), + InitialSlackMessageID: slackMessage.InitialSlackMessageID, + CreatedAt: now, + UpdatedAt: now, + SentAt: &now, + } + + if err := newSlackMessage.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert slack message: %w", err) } if err := s.slackClient.UpdateInteractiveMessage(ctx, responseURL, updatedBody); err != nil { @@ -113,120 +240,46 @@ func (s *SlackMessageService) QueueSlackNotification( return fmt.Errorf("cannot load trust center: %w", err) } - var accesses coredata.TrustCenterDocumentAccesses - if err := accesses.LoadAllByTrustCenterAccessID(ctx, tx, s.svc.scope, trustCenterAccess.ID); err != nil { - return fmt.Errorf("cannot load trust center document accesses: %w", err) + documents, reports, err := s.loadDocumentsAndReportsFromAccesses(ctx, tx, trustCenterAccess.ID) + if err != nil { + return fmt.Errorf("cannot load documents and reports: %w", err) } - var documentIDs []string - var reportIDs []string - var documents []struct { - ID string - Title string - Granted bool - } - var reports []struct { - ID string - Title string - AuditID string - Granted bool + slackMessageID := gid.New(s.svc.scope.GetTenantID(), coredata.SlackMessageEntityType) + + body, err := s.buildAccessRequestMessage( + slackMessageID, + trustCenterAccess.Name, + requesterEmail, + trustCenter.OrganizationID, + documents, + reports, + ) + if err != nil { + return fmt.Errorf("cannot build access request message: %w", err) } - for _, access := range accesses { - if access.DocumentID != nil { - doc := &coredata.Document{} - if err := doc.LoadByID(ctx, tx, s.svc.scope, *access.DocumentID); err != nil { - return fmt.Errorf("cannot load document: %w", err) - } - documentIDs = append(documentIDs, access.DocumentID.String()) - documents = append(documents, struct { - ID string - Title string - Granted bool - }{ - ID: access.DocumentID.String(), - Title: doc.Title, - Granted: access.Active, - }) - } - - if access.ReportID != nil { - rep := &coredata.Report{} - if err := rep.LoadByID(ctx, tx, s.svc.scope, *access.ReportID); err != nil { - return fmt.Errorf("cannot load report: %w", err) - } - - audit := &coredata.Audit{} - if err := audit.LoadByReportID(ctx, tx, s.svc.scope, *access.ReportID); err != nil { - return fmt.Errorf("cannot load audit: %w", err) - } - - framework := &coredata.Framework{} - if err := framework.LoadByID(ctx, tx, s.svc.scope, audit.FrameworkID); err != nil { - return fmt.Errorf("cannot load framework: %w", err) - } - - label := framework.Name - if audit.Name != nil && *audit.Name != "" { - label = label + " - " + *audit.Name - } - reportIDs = append(reportIDs, access.ReportID.String()) - reports = append(reports, struct { - ID string - Title string - AuditID string - Granted bool - }{ - ID: access.ReportID.String(), - Title: label, - AuditID: audit.ID.String(), - Granted: access.Active, - }) - } + metadata := SlackMessageMetadata{ + Documents: documents, + Reports: reports, } - templateData := struct { - RequesterName string - RequesterEmail string - OrganizationID string - Domain string - DocumentIDs []string - ReportIDs []string - Documents []struct { - ID string - Title string - Granted bool - } - Reports []struct { - ID string - Title string - AuditID string - Granted bool - } - }{ - RequesterName: trustCenterAccess.Name, - RequesterEmail: requesterEmail, - OrganizationID: trustCenter.OrganizationID.String(), - Domain: s.svc.hostname, - DocumentIDs: documentIDs, - ReportIDs: reportIDs, - Documents: documents, - Reports: reports, + now := time.Now() + slackMessage := &coredata.SlackMessage{ + ID: slackMessageID, + OrganizationID: trustCenter.OrganizationID, + Type: coredata.SlackMessageTypeTrustCenterAccessRequest, + Body: body, + RequesterEmail: &requesterEmail, + Metadata: metadata.toMap(), + CreatedAt: now, + UpdatedAt: now, } - var buf bytes.Buffer - if err := accessRequestTemplate.Execute(&buf, templateData); err != nil { - return fmt.Errorf("failed to execute template: %w", err) - } + sevenDaysAgo := now.Add(-slackMessageDeduplicationWindow) - var body map[string]any - if err := json.NewDecoder(&buf).Decode(&body); err != nil { - return fmt.Errorf("failed to parse template JSON: %w", err) - } - - sevenDaysAgo := time.Now().Add(-slackMessageDeduplicationWindow) var existingMessage coredata.SlackMessage - err := existingMessage.LoadLatestByRequesterEmailAndType( + err = existingMessage.LoadLatestByRequesterEmailAndType( ctx, tx, s.svc.scope, @@ -235,17 +288,23 @@ func (s *SlackMessageService) QueueSlackNotification( coredata.SlackMessageTypeTrustCenterAccessRequest, sevenDaysAgo, ) + if err == nil { + slackMessage.MessageTS = existingMessage.MessageTS + slackMessage.ChannelID = existingMessage.ChannelID + slackMessage.InitialSlackMessageID = existingMessage.InitialSlackMessageID - if err == nil && existingMessage.MessageTS != nil && existingMessage.ChannelID != nil { - slackMessageUpdate := coredata.NewSlackMessageUpdate(s.svc.scope, existingMessage.ID, body) - if err := slackMessageUpdate.Insert(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert slack message update: %w", err) + if err := slackMessage.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert slack message: %w", err) } return nil } + var notFoundErr coredata.ErrSlackMessageNotFound + if !errors.Is(err, notFoundErr) { + return fmt.Errorf("cannot load existing slack message: %w", err) + } - slackMessage := coredata.NewSlackMessage(s.svc.scope, trustCenter.OrganizationID, coredata.SlackMessageTypeTrustCenterAccessRequest, body, &requesterEmail) + slackMessage.InitialSlackMessageID = slackMessageID if err := slackMessage.Insert(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot insert slack message: %w", err) } @@ -254,84 +313,114 @@ func (s *SlackMessageService) QueueSlackNotification( }) } -func (s *SlackMessageService) changeButton(body map[string]any, actionID string, value string, accessTabURL string) map[string]any { - blocks, ok := body["blocks"].([]any) - if !ok { - return body +func (s *SlackMessageService) loadDocumentsAndReportsFromAccesses( + ctx context.Context, + conn pg.Conn, + trustCenterAccessID gid.GID, +) ( + documents []SlackMessageDocument, + reports []SlackMessageReport, + err error, +) { + var accesses coredata.TrustCenterDocumentAccesses + if err := accesses.LoadAllByTrustCenterAccessID(ctx, conn, s.svc.scope, trustCenterAccessID); err != nil { + return nil, nil, fmt.Errorf("cannot load trust center document accesses: %w", err) } - isAcceptAll := actionID == "accept_all" - - updatedBlocks := make([]any, len(blocks)) - for i, blockAny := range blocks { - block, ok := blockAny.(map[string]any) - if !ok { - updatedBlocks[i] = blockAny - continue - } - - blockCopy := make(map[string]any) - maps.Copy(blockCopy, block) - - if blockType, ok := block["type"].(string); ok && blockType == "section" { - if acc, ok := block["accessory"].(map[string]any); ok { - if s.shouldChangeButton(acc, actionID, value, isAcceptAll) { - blockCopy["accessory"] = s.makeStaticButton(accessTabURL) - } + for _, access := range accesses { + if access.DocumentID != nil { + doc := &coredata.Document{} + if err := doc.LoadByID(ctx, conn, s.svc.scope, *access.DocumentID); err != nil { + return nil, nil, fmt.Errorf("cannot load document: %w", err) } + documents = append(documents, SlackMessageDocument{ + ID: access.DocumentID.String(), + Title: doc.Title, + Granted: access.Active, + }) } - if blockType, ok := block["type"].(string); ok && blockType == "actions" { - if elements, ok := block["elements"].([]any); ok { - updatedElements := make([]any, len(elements)) - for j, elemAny := range elements { - elem, ok := elemAny.(map[string]any) - if !ok { - updatedElements[j] = elemAny - continue - } - - if s.shouldChangeButton(elem, actionID, value, isAcceptAll) { - updatedElements[j] = s.makeStaticButton(accessTabURL) - } else { - updatedElements[j] = elem - } - } - blockCopy["elements"] = updatedElements + if access.ReportID != nil { + rep := &coredata.Report{} + if err := rep.LoadByID(ctx, conn, s.svc.scope, *access.ReportID); err != nil { + return nil, nil, fmt.Errorf("cannot load report: %w", err) } + + audit := &coredata.Audit{} + if err := audit.LoadByReportID(ctx, conn, s.svc.scope, *access.ReportID); err != nil { + return nil, nil, fmt.Errorf("cannot load audit: %w", err) + } + + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, audit.FrameworkID); err != nil { + return nil, nil, fmt.Errorf("cannot load framework: %w", err) + } + + label := framework.Name + if audit.Name != nil && *audit.Name != "" { + label = label + " - " + *audit.Name + } + reports = append(reports, SlackMessageReport{ + ID: access.ReportID.String(), + Title: label, + AuditID: audit.ID.String(), + Granted: access.Active, + }) } - - updatedBlocks[i] = blockCopy } - updatedBody := make(map[string]any) - maps.Copy(updatedBody, body) - updatedBody["blocks"] = updatedBlocks - - return updatedBody + return documents, reports, nil } -func (s *SlackMessageService) shouldChangeButton(button map[string]any, actionID string, value string, isAcceptAll bool) bool { - if button["type"] != "button" { - return false +func (s *SlackMessageService) buildAccessRequestMessage( + slackMessageID gid.GID, + requesterName string, + requesterEmail string, + organizationID gid.GID, + documents []SlackMessageDocument, + reports []SlackMessageReport, +) (map[string]any, error) { + var documentIDs []string + var reportIDs []string + + for _, doc := range documents { + documentIDs = append(documentIDs, doc.ID) + } + for _, rep := range reports { + reportIDs = append(reportIDs, rep.ID) } - btnActionID, _ := button["action_id"].(string) - btnValue, _ := button["value"].(string) - - isExactMatch := btnActionID == actionID && btnValue == value - isAcceptAllMatch := isAcceptAll && (btnActionID == "accept_document" || btnActionID == "accept_report") - - return isExactMatch || isAcceptAllMatch -} - -func (s *SlackMessageService) makeStaticButton(accessTabURL string) map[string]any { - return map[string]any{ - "type": "button", - "text": map[string]any{ - "type": "plain_text", - "text": "✓ Granted", - }, - "url": accessTabURL, + templateData := struct { + RequesterName string + RequesterEmail string + OrganizationID string + Domain string + SlackMessageID string + DocumentIDs []string + ReportIDs []string + Documents []SlackMessageDocument + Reports []SlackMessageReport + }{ + RequesterName: requesterName, + RequesterEmail: requesterEmail, + OrganizationID: organizationID.String(), + Domain: s.svc.hostname, + SlackMessageID: slackMessageID.String(), + DocumentIDs: documentIDs, + ReportIDs: reportIDs, + Documents: documents, + Reports: reports, } + + var buf bytes.Buffer + if err := accessRequestTemplate.Execute(&buf, templateData); err != nil { + return nil, fmt.Errorf("failed to execute template: %w", err) + } + + var body map[string]any + if err := json.NewDecoder(&buf).Decode(&body); err != nil { + return nil, fmt.Errorf("failed to parse template JSON: %w", err) + } + + return body, nil } diff --git a/pkg/trust/templates/access-request.json.tmpl b/pkg/trust/templates/access-request.json.tmpl index 824fc644e..0dc7b3921 100644 --- a/pkg/trust/templates/access-request.json.tmpl +++ b/pkg/trust/templates/access-request.json.tmpl @@ -27,7 +27,7 @@ "text": "✅ Accept All" }, "action_id": "accept_all", - "value": "{{buildAcceptAllValue .DocumentIDs .ReportIDs}}", + "value": "{{.SlackMessageID}}", "style": "primary" }, { diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index b24e28add..09acb0046 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -208,7 +208,7 @@ func (s TrustCenterAccessService) Request( } if err := s.svc.SlackMessages.QueueSlackNotification(ctx, access.Email, req.TrustCenterID); err != nil { - s.logger.ErrorCtx(ctx, "cannot queue slack notification") + s.logger.ErrorCtx(ctx, "cannot queue slack notification", log.Error(err)) } return access, nil @@ -337,14 +337,19 @@ func (s TrustCenterAccessService) LoadReportAccess( func (s *TrustCenterAccessService) AcceptByIDs( ctx context.Context, - trustCenterID gid.GID, + organizationID gid.GID, email string, documentIDs []gid.GID, reportIDs []gid.GID, ) error { return s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByOrganizationID(ctx, tx, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + access := &coredata.TrustCenterAccess{} - if err := access.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, trustCenterID, email); err != nil { + if err := access.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, trustCenter.ID, email); err != nil { return fmt.Errorf("cannot load trust center access: %w", err) }