package mail import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/ultisuite/ulti-backend/internal/api/query" "github.com/ultisuite/ulti-backend/internal/securityaudit" ) type SignaturesList struct { Signatures []map[string]any `json:"signatures"` Pagination query.PaginationMeta `json:"pagination,omitempty"` } func scanSignature(id, name, html string, sortOrder int, createdAt, updatedAt any) map[string]any { return map[string]any{ "id": id, "name": name, "html": html, "sort_order": sortOrder, "created_at": createdAt, "updated_at": updatedAt, } } func (s *Service) verifySignatureOwnership(ctx context.Context, externalID, signatureID string) error { var exists bool err := s.db.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM mail_signatures ms JOIN users u ON ms.user_id = u.id WHERE ms.id = $1 AND u.external_id = $2 ) `, signatureID, externalID).Scan(&exists) if err != nil { return err } if !exists { return ErrNotFound } return nil } func (s *Service) ListSignatures(ctx context.Context, externalID string, params query.ListParams) (SignaturesList, error) { var total int64 if err := s.db.QueryRow(ctx, ` SELECT COUNT(*) FROM mail_signatures ms JOIN users u ON ms.user_id = u.id WHERE u.external_id = $1 `, externalID).Scan(&total); err != nil { return SignaturesList{}, err } rows, err := s.db.Query(ctx, ` SELECT ms.id, ms.name, ms.html, ms.sort_order, ms.created_at, ms.updated_at FROM mail_signatures ms JOIN users u ON ms.user_id = u.id WHERE u.external_id = $1 ORDER BY ms.sort_order ASC, ms.created_at ASC LIMIT $2 OFFSET $3 `, externalID, params.Limit(), params.Offset()) if err != nil { return SignaturesList{}, err } defer rows.Close() signatures := make([]map[string]any, 0) for rows.Next() { var id, name, html string var sortOrder int var createdAt, updatedAt any if err := rows.Scan(&id, &name, &html, &sortOrder, &createdAt, &updatedAt); err != nil { return SignaturesList{}, err } signatures = append(signatures, scanSignature(id, name, html, sortOrder, createdAt, updatedAt)) } if err := rows.Err(); err != nil { return SignaturesList{}, err } return SignaturesList{ Signatures: signatures, Pagination: params.Meta(&total), }, nil } func (s *Service) GetSignature(ctx context.Context, externalID, signatureID string) (map[string]any, error) { var id, name, html string var sortOrder int var createdAt, updatedAt any err := s.db.QueryRow(ctx, ` SELECT ms.id, ms.name, ms.html, ms.sort_order, ms.created_at, ms.updated_at FROM mail_signatures ms JOIN users u ON ms.user_id = u.id WHERE ms.id = $1 AND u.external_id = $2 `, signatureID, externalID).Scan(&id, &name, &html, &sortOrder, &createdAt, &updatedAt) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } return nil, err } return scanSignature(id, name, html, sortOrder, createdAt, updatedAt), nil } func (s *Service) CreateSignature(ctx context.Context, externalID string, req *createSignatureRequest) (string, error) { userID, err := s.ResolveUserID(ctx, externalID) if err != nil { return "", err } var id string err = s.db.QueryRow(ctx, ` INSERT INTO mail_signatures (user_id, name, html) VALUES ($1, $2, $3) RETURNING id `, userID, req.Name, req.HTML).Scan(&id) if err != nil { return "", err } return id, nil } func (s *Service) UpdateSignature(ctx context.Context, externalID, signatureID string, req *updateSignatureRequest) error { result, err := s.db.Exec(ctx, ` UPDATE mail_signatures ms SET name = $1, html = $2, updated_at = NOW() FROM users u WHERE ms.id = $3 AND ms.user_id = u.id AND u.external_id = $4 `, req.Name, req.HTML, signatureID, externalID) if err != nil { return err } if result.RowsAffected() == 0 { return ErrNotFound } return nil } func (s *Service) DeleteSignature(ctx context.Context, externalID, signatureID string) error { result, err := s.db.Exec(ctx, ` DELETE FROM mail_signatures ms USING users u WHERE ms.id = $1 AND ms.user_id = u.id AND u.external_id = $2 `, signatureID, externalID) if err != nil { return err } if result.RowsAffected() == 0 { return ErrNotFound } if s.audit != nil { s.audit.Log(ctx, externalID, securityaudit.ActionCriticalDeletion, map[string]any{ "target": "mail_signature", "signature_id": signatureID, }) } return nil }