63c12a2f44
eula support * eula support * docs
770 lines
24 KiB
Go
770 lines
24 KiB
Go
// Package eula provides EULA version management and user agreement tracking.
|
|
// It handles creating/activating EULA versions and recording user consent with
|
|
// associated metadata like timestamp and IP address.
|
|
package eula
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
"queryorchestration/internal/database/repository"
|
|
"queryorchestration/internal/serviceconfig"
|
|
"queryorchestration/internal/usermanagement"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
// ErrNoCurrentVersion indicates no EULA version is currently active.
|
|
var ErrNoCurrentVersion = errors.New("no current EULA version configured")
|
|
|
|
// ErrVersionNotFound indicates the requested EULA version does not exist.
|
|
var ErrVersionNotFound = errors.New("EULA version not found")
|
|
|
|
// ErrAlreadyAgreed indicates the user has already agreed to the specified EULA version.
|
|
var ErrAlreadyAgreed = errors.New("user has already agreed to this EULA version")
|
|
|
|
// ErrConcurrentActivation indicates another activation occurred during the transaction.
|
|
var ErrConcurrentActivation = errors.New("concurrent activation conflict - another version was activated")
|
|
|
|
// Service provides EULA management operations including version creation,
|
|
// activation, and user agreement tracking.
|
|
type Service struct {
|
|
cfg serviceconfig.ConfigProvider
|
|
}
|
|
|
|
// New creates a new EULA service instance.
|
|
//
|
|
// Parameters:
|
|
// - cfg: Configuration provider for database access
|
|
//
|
|
// Returns:
|
|
// - A new Service instance
|
|
func New(cfg serviceconfig.ConfigProvider) *Service {
|
|
return &Service{cfg: cfg}
|
|
}
|
|
|
|
// GetCurrentVersion retrieves the current active EULA version.
|
|
//
|
|
// Returns:
|
|
// - The current EULA version, or nil with ErrNoCurrentVersion if none is active
|
|
// - An error if the database query fails
|
|
func (s *Service) GetCurrentVersion(ctx context.Context) (*repository.Eulaversion, error) {
|
|
version, err := s.cfg.GetDBQueries().GetCurrentEulaVersion(ctx)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNoCurrentVersion
|
|
}
|
|
return nil, fmt.Errorf("failed to get current EULA version: %w", err)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// CreateVersionInput contains the parameters for creating a new EULA version.
|
|
type CreateVersionInput struct {
|
|
Version string // Unique version string (e.g., "1.0", "2024-01")
|
|
Title string // Human-readable title
|
|
Content string // Full EULA text in Markdown format
|
|
EffectiveDate *time.Time // When this version becomes effective (optional)
|
|
CreatedBy string // Email of admin who created this version
|
|
}
|
|
|
|
// CreateVersion creates a new EULA version.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - input: CreateVersionInput with version details
|
|
//
|
|
// Returns:
|
|
// - The created EULA version
|
|
// - An error if validation fails or version string already exists
|
|
func (s *Service) CreateVersion(ctx context.Context, input *CreateVersionInput) (*repository.Eulaversion, error) {
|
|
if input.Version == "" {
|
|
return nil, fmt.Errorf("version string cannot be empty")
|
|
}
|
|
if input.Title == "" {
|
|
return nil, fmt.Errorf("title cannot be empty")
|
|
}
|
|
if input.Content == "" {
|
|
return nil, fmt.Errorf("content cannot be empty")
|
|
}
|
|
if input.CreatedBy == "" {
|
|
return nil, fmt.Errorf("createdBy cannot be empty")
|
|
}
|
|
|
|
// Convert time.Time to pgtype.Timestamptz (optional - may be nil)
|
|
var effectiveDate pgtype.Timestamptz
|
|
if input.EffectiveDate != nil {
|
|
effectiveDate = pgtype.Timestamptz{
|
|
Time: *input.EffectiveDate,
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
version, err := s.cfg.GetDBQueries().CreateEulaVersion(ctx, &repository.CreateEulaVersionParams{
|
|
Version: input.Version,
|
|
Title: input.Title,
|
|
Content: input.Content,
|
|
Effectivedate: effectiveDate,
|
|
Createdby: input.CreatedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create EULA version: %w", err)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// ListVersionsInput contains pagination parameters for listing EULA versions.
|
|
type ListVersionsInput struct {
|
|
Page int32 // Page number (1-based)
|
|
PageSize int32 // Items per page
|
|
}
|
|
|
|
// ListVersionsResult contains the paginated list of EULA versions.
|
|
type ListVersionsResult struct {
|
|
Versions []*repository.Eulaversion
|
|
Total int64
|
|
HasMore bool
|
|
}
|
|
|
|
// ListVersions retrieves a paginated list of all EULA versions.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - input: Pagination parameters
|
|
//
|
|
// Returns:
|
|
// - ListVersionsResult with versions and pagination metadata
|
|
// - An error if the database query fails
|
|
func (s *Service) ListVersions(ctx context.Context, input *ListVersionsInput) (*ListVersionsResult, error) {
|
|
if input.Page < 1 {
|
|
input.Page = 1
|
|
}
|
|
if input.PageSize < 1 {
|
|
input.PageSize = 20
|
|
}
|
|
if input.PageSize > 100 {
|
|
input.PageSize = 100
|
|
}
|
|
|
|
offset := int64((input.Page - 1) * input.PageSize)
|
|
limit := int64(input.PageSize)
|
|
|
|
versions, err := s.cfg.GetDBQueries().ListEulaVersions(ctx, &repository.ListEulaVersionsParams{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list EULA versions: %w", err)
|
|
}
|
|
|
|
total, err := s.cfg.GetDBQueries().CountEulaVersions(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to count EULA versions: %w", err)
|
|
}
|
|
|
|
return &ListVersionsResult{
|
|
Versions: versions,
|
|
Total: total,
|
|
HasMore: offset+int64(len(versions)) < total,
|
|
}, nil
|
|
}
|
|
|
|
// GetVersionByID retrieves a specific EULA version by its ID.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - id: The EULA version UUID
|
|
//
|
|
// Returns:
|
|
// - The EULA version, or nil with ErrVersionNotFound if not found
|
|
// - An error if the database query fails
|
|
func (s *Service) GetVersionByID(ctx context.Context, id uuid.UUID) (*repository.Eulaversion, error) {
|
|
version, err := s.cfg.GetDBQueries().GetEulaVersionById(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrVersionNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get EULA version: %w", err)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// UpdateVersionInput contains the parameters for updating EULA version metadata.
|
|
// Only title and effective date can be updated; content is immutable.
|
|
type UpdateVersionInput struct {
|
|
Title *string // New title (nil to keep unchanged)
|
|
EffectiveDate *time.Time // New effective date (nil to keep unchanged)
|
|
}
|
|
|
|
// UpdateVersion updates the metadata (title and effective date) of an EULA version.
|
|
// Content cannot be modified to maintain audit integrity.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - id: The EULA version UUID
|
|
// - input: Fields to update
|
|
//
|
|
// Returns:
|
|
// - The updated EULA version
|
|
// - ErrVersionNotFound if version doesn't exist
|
|
// - An error if the database update fails
|
|
func (s *Service) UpdateVersion(ctx context.Context, id uuid.UUID, input *UpdateVersionInput) (*repository.Eulaversion, error) {
|
|
// First get the existing version
|
|
existing, err := s.GetVersionByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply updates
|
|
title := existing.Title
|
|
if input.Title != nil {
|
|
title = *input.Title
|
|
}
|
|
|
|
effectiveDate := existing.Effectivedate
|
|
if input.EffectiveDate != nil {
|
|
effectiveDate = pgtype.Timestamptz{
|
|
Time: *input.EffectiveDate,
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
version, err := s.cfg.GetDBQueries().UpdateEulaVersionMetadata(ctx, &repository.UpdateEulaVersionMetadataParams{
|
|
ID: id,
|
|
Title: title,
|
|
Effectivedate: effectiveDate,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update EULA version: %w", err)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// ActivateVersion sets the specified version as the current active EULA.
|
|
// Uses a database transaction to atomically clear the previous current flag and set the new one.
|
|
// The partial unique index ensures only one version can be current at any time.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - id: The EULA version UUID to activate
|
|
// - activatedBy: Email of admin performing the activation
|
|
//
|
|
// Returns:
|
|
// - The activated EULA version with isCurrent=true, activatedAt, and activatedBy populated
|
|
// - ErrVersionNotFound if version doesn't exist
|
|
// - ErrConcurrentActivation if another activation occurred during the transaction
|
|
// - An error if the database operations fail
|
|
func (s *Service) ActivateVersion(ctx context.Context, id uuid.UUID, activatedBy string) (*repository.Eulaversion, error) {
|
|
// First verify the version exists
|
|
_, err := s.GetVersionByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Begin transaction
|
|
tx, err := s.cfg.GetDBPool().Begin(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback(ctx)
|
|
}()
|
|
|
|
queries := s.cfg.GetDBQueries().WithTx(tx)
|
|
|
|
// Clear all current flags
|
|
err = queries.ClearCurrentEulaVersions(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to clear current EULA versions: %w", err)
|
|
}
|
|
|
|
// Set the new current version
|
|
activatedByPtr := &activatedBy
|
|
version, err := queries.SetEulaVersionCurrent(ctx, &repository.SetEulaVersionCurrentParams{
|
|
ID: id,
|
|
Activatedby: activatedByPtr,
|
|
})
|
|
if err != nil {
|
|
// The partial unique index will cause a conflict if another transaction
|
|
// already set a different version as current
|
|
return nil, fmt.Errorf("failed to activate EULA version: %w", err)
|
|
}
|
|
|
|
// Commit transaction
|
|
err = tx.Commit(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to commit activation: %w", err)
|
|
}
|
|
|
|
return version, nil
|
|
}
|
|
|
|
// RecordAgreementInput contains the parameters for recording a user's EULA agreement.
|
|
type RecordAgreementInput struct {
|
|
CognitoSubjectID string // User's Cognito subject ID
|
|
UserEmail string // User's email at time of agreement
|
|
EulaVersionID uuid.UUID // The EULA version being agreed to
|
|
IPAddress string // IP address from which the user agreed
|
|
}
|
|
|
|
// RecordAgreementResult contains the result of recording an agreement.
|
|
type RecordAgreementResult struct {
|
|
Agreement *repository.Eulaagreement
|
|
AlreadyAgreed bool // True if user had already agreed (returned existing)
|
|
}
|
|
|
|
// RecordAgreement records a user's agreement to an EULA version.
|
|
// If the user has already agreed to this version, returns the existing agreement
|
|
// with AlreadyAgreed=true (idempotent operation).
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - input: RecordAgreementInput with user and agreement details
|
|
//
|
|
// Returns:
|
|
// - RecordAgreementResult with the agreement and whether it was pre-existing
|
|
// - An error if validation fails or the database operation fails
|
|
func (s *Service) RecordAgreement(ctx context.Context, input *RecordAgreementInput) (*RecordAgreementResult, error) {
|
|
if input.CognitoSubjectID == "" {
|
|
return nil, fmt.Errorf("cognitoSubjectID cannot be empty")
|
|
}
|
|
if input.UserEmail == "" {
|
|
return nil, fmt.Errorf("userEmail cannot be empty")
|
|
}
|
|
if input.EulaVersionID == uuid.Nil {
|
|
return nil, fmt.Errorf("eulaVersionID cannot be nil")
|
|
}
|
|
if input.IPAddress == "" {
|
|
return nil, fmt.Errorf("ipAddress cannot be empty")
|
|
}
|
|
|
|
// Check if already agreed (for idempotency)
|
|
existing, err := s.cfg.GetDBQueries().GetUserEulaAgreement(ctx, &repository.GetUserEulaAgreementParams{
|
|
Cognitosubjectid: input.CognitoSubjectID,
|
|
Eulaversionid: input.EulaVersionID,
|
|
})
|
|
if err == nil {
|
|
// User already agreed - return existing (idempotent)
|
|
return &RecordAgreementResult{
|
|
Agreement: existing,
|
|
AlreadyAgreed: true,
|
|
}, nil
|
|
}
|
|
if !errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, fmt.Errorf("failed to check existing agreement: %w", err)
|
|
}
|
|
|
|
// Create new agreement
|
|
agreement, err := s.cfg.GetDBQueries().CreateEulaAgreement(ctx, &repository.CreateEulaAgreementParams{
|
|
Cognitosubjectid: input.CognitoSubjectID,
|
|
Useremail: input.UserEmail,
|
|
Eulaversionid: input.EulaVersionID,
|
|
Agreedfromip: input.IPAddress,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create EULA agreement: %w", err)
|
|
}
|
|
|
|
return &RecordAgreementResult{
|
|
Agreement: agreement,
|
|
AlreadyAgreed: false,
|
|
}, nil
|
|
}
|
|
|
|
// HasUserAgreedToCurrent checks if a user has agreed to the current active EULA version.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - cognitoSubjectID: User's Cognito subject ID
|
|
//
|
|
// Returns:
|
|
// - true if the user has agreed to the current EULA, false otherwise
|
|
// - ErrNoCurrentVersion if no EULA is currently active
|
|
// - An error if the database query fails
|
|
func (s *Service) HasUserAgreedToCurrent(ctx context.Context, cognitoSubjectID string) (bool, error) {
|
|
_, err := s.cfg.GetDBQueries().GetUserCurrentEulaAgreement(ctx, cognitoSubjectID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("failed to check user's current EULA agreement: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// UserEulaStatus contains the user's EULA agreement status information.
|
|
type UserEulaStatus struct {
|
|
HasAgreed bool
|
|
CurrentVersion string
|
|
CurrentVersionID uuid.UUID
|
|
AgreedAt *time.Time
|
|
AgreedVersion *string
|
|
}
|
|
|
|
// GetUserEulaStatus gets the full status information for UI display.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - cognitoSubjectID: User's Cognito subject ID
|
|
//
|
|
// Returns:
|
|
// - UserEulaStatus with full status information
|
|
// - ErrNoCurrentVersion if no EULA is currently active
|
|
// - An error if the database queries fail
|
|
func (s *Service) GetUserEulaStatus(ctx context.Context, cognitoSubjectID string) (*UserEulaStatus, error) {
|
|
// Get current version
|
|
currentVersion, err := s.GetCurrentVersion(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status := &UserEulaStatus{
|
|
HasAgreed: false,
|
|
CurrentVersion: currentVersion.Version,
|
|
CurrentVersionID: currentVersion.ID,
|
|
}
|
|
|
|
// Check if user has agreed to current version
|
|
agreement, err := s.cfg.GetDBQueries().GetUserCurrentEulaAgreement(ctx, cognitoSubjectID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return status, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to get user's current EULA agreement: %w", err)
|
|
}
|
|
|
|
status.HasAgreed = true
|
|
if agreement.Agreedat.Valid {
|
|
agreedAt := agreement.Agreedat.Time
|
|
status.AgreedAt = &agreedAt
|
|
}
|
|
status.AgreedVersion = ¤tVersion.Version
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// ListAgreementsInput contains pagination and filter parameters for listing agreements.
|
|
type ListAgreementsInput struct {
|
|
Page int32 // Page number (1-based)
|
|
PageSize int32 // Items per page
|
|
VersionID *uuid.UUID // Optional: filter by EULA version
|
|
UserID *string // Optional: filter by Cognito subject ID
|
|
}
|
|
|
|
// ListAgreementsResult contains the paginated list of agreements.
|
|
type ListAgreementsResult struct {
|
|
Agreements []*repository.ListEulaAgreementsRow
|
|
Total int64
|
|
HasMore bool
|
|
}
|
|
|
|
// ListAgreements retrieves a paginated list of EULA agreements with optional filters.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - input: Pagination and filter parameters
|
|
//
|
|
// Returns:
|
|
// - ListAgreementsResult with agreements and pagination metadata
|
|
// - An error if the database query fails
|
|
func (s *Service) ListAgreements(ctx context.Context, input *ListAgreementsInput) (*ListAgreementsResult, error) {
|
|
if input.Page < 1 {
|
|
input.Page = 1
|
|
}
|
|
if input.PageSize < 1 {
|
|
input.PageSize = 20
|
|
}
|
|
if input.PageSize > 100 {
|
|
input.PageSize = 100
|
|
}
|
|
|
|
offset := int64((input.Page - 1) * input.PageSize)
|
|
limit := int64(input.PageSize)
|
|
|
|
agreements, err := s.cfg.GetDBQueries().ListEulaAgreements(ctx, &repository.ListEulaAgreementsParams{
|
|
Limit: limit,
|
|
Offset: offset,
|
|
VersionID: input.VersionID,
|
|
UserID: input.UserID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list EULA agreements: %w", err)
|
|
}
|
|
|
|
total, err := s.cfg.GetDBQueries().CountEulaAgreements(ctx, &repository.CountEulaAgreementsParams{
|
|
VersionID: input.VersionID,
|
|
UserID: input.UserID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to count EULA agreements: %w", err)
|
|
}
|
|
|
|
return &ListAgreementsResult{
|
|
Agreements: agreements,
|
|
Total: total,
|
|
HasMore: offset+int64(len(agreements)) < total,
|
|
}, nil
|
|
}
|
|
|
|
// GetUserAgreementHistory retrieves all EULA versions a specific user has agreed to.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - cognitoSubjectID: User's Cognito subject ID
|
|
//
|
|
// Returns:
|
|
// - A list of agreements with version details
|
|
// - An error if the database query fails
|
|
func (s *Service) GetUserAgreementHistory(ctx context.Context, cognitoSubjectID string) ([]*repository.ListEulaAgreementsByUserRow, error) {
|
|
history, err := s.cfg.GetDBQueries().ListEulaAgreementsByUser(ctx, cognitoSubjectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user agreement history: %w", err)
|
|
}
|
|
return history, nil
|
|
}
|
|
|
|
// ComplianceReportInput contains parameters for generating a compliance report.
|
|
type ComplianceReportInput struct {
|
|
VersionID *uuid.UUID // nil = use current version
|
|
Page int32 // Page number (1-based)
|
|
PageSize int32 // Items per page (max 100)
|
|
Agreed *bool // nil = all users, true = agreed only, false = not agreed only
|
|
}
|
|
|
|
// ComplianceReportUser represents a user in the compliance report.
|
|
type ComplianceReportUser struct {
|
|
CognitoSubjectID string // User's Cognito subject ID
|
|
Email *string // Email at time of agreement (nil if not agreed)
|
|
CurrentEmail string // Current email from Cognito
|
|
Agreed bool // Whether the user has agreed
|
|
AgreedAt *time.Time // When the user agreed (nil if not agreed)
|
|
AgreedFromIP *string // IP address from which user agreed (nil if not agreed)
|
|
}
|
|
|
|
// ComplianceReportSummary contains aggregate statistics.
|
|
type ComplianceReportSummary struct {
|
|
TotalUsers int32 // Total enabled users in Cognito
|
|
AgreedCount int32 // Users who have agreed to this version
|
|
NotAgreedCount int32 // Users who have not agreed
|
|
CompliancePercentage float32 // Percentage of users who have agreed (0-100)
|
|
}
|
|
|
|
// ComplianceReportResult contains the full compliance report.
|
|
type ComplianceReportResult struct {
|
|
Version *repository.Eulaversion // The EULA version being reported on
|
|
Summary ComplianceReportSummary // Aggregate statistics
|
|
Users []ComplianceReportUser // Paginated list of users
|
|
Page int32 // Current page number
|
|
PageSize int32 // Items per page
|
|
TotalPages int32 // Total number of pages
|
|
TotalItems int32 // Total number of items (after filtering)
|
|
}
|
|
|
|
// ApplyCompliancePaginationDefaults sets default pagination values for compliance report input.
|
|
func (s *Service) ApplyCompliancePaginationDefaults(input *ComplianceReportInput) {
|
|
if input.Page < 1 {
|
|
input.Page = 1
|
|
}
|
|
if input.PageSize < 1 {
|
|
input.PageSize = 50
|
|
}
|
|
if input.PageSize > 100 {
|
|
input.PageSize = 100
|
|
}
|
|
}
|
|
|
|
// ResolveComplianceVersion resolves the target EULA version for compliance reporting.
|
|
// If versionID is provided, fetches that version; otherwise fetches the current version.
|
|
func (s *Service) ResolveComplianceVersion(ctx context.Context, versionID *uuid.UUID) (*repository.Eulaversion, error) {
|
|
if versionID != nil {
|
|
return s.GetVersionByID(ctx, *versionID)
|
|
}
|
|
return s.GetCurrentVersion(ctx)
|
|
}
|
|
|
|
// BuildComplianceUserList builds the compliance user list from Cognito users and agreements.
|
|
// Returns the user list and summary statistics.
|
|
func (s *Service) BuildComplianceUserList(
|
|
cognitoUsers []usermanagement.CognitoUserResponse,
|
|
agreements []*repository.Eulaagreement,
|
|
) ([]ComplianceReportUser, ComplianceReportSummary) {
|
|
// Build agreement lookup map
|
|
agreementMap := make(map[string]*repository.Eulaagreement, len(agreements))
|
|
for _, agreement := range agreements {
|
|
agreementMap[agreement.Cognitosubjectid] = agreement
|
|
}
|
|
|
|
// Build user list
|
|
allUsers := make([]ComplianceReportUser, 0, len(cognitoUsers))
|
|
var agreedCount int32
|
|
|
|
for _, cognitoUser := range cognitoUsers {
|
|
user := s.BuildComplianceUser(cognitoUser, agreementMap)
|
|
if user.Agreed {
|
|
agreedCount++
|
|
}
|
|
allUsers = append(allUsers, user)
|
|
}
|
|
|
|
// Calculate summary with safe int conversion
|
|
totalUsers := SafeIntToInt32(len(cognitoUsers))
|
|
notAgreedCount := totalUsers - agreedCount
|
|
var compliancePercentage float32
|
|
if totalUsers > 0 {
|
|
compliancePercentage = float32(agreedCount) / float32(totalUsers) * 100
|
|
}
|
|
|
|
summary := ComplianceReportSummary{
|
|
TotalUsers: totalUsers,
|
|
AgreedCount: agreedCount,
|
|
NotAgreedCount: notAgreedCount,
|
|
CompliancePercentage: compliancePercentage,
|
|
}
|
|
|
|
return allUsers, summary
|
|
}
|
|
|
|
// BuildComplianceUser builds a single compliance user record from Cognito user and agreement map.
|
|
func (s *Service) BuildComplianceUser(
|
|
cognitoUser usermanagement.CognitoUserResponse,
|
|
agreementMap map[string]*repository.Eulaagreement,
|
|
) ComplianceReportUser {
|
|
user := ComplianceReportUser{
|
|
CognitoSubjectID: cognitoUser.SubjectID,
|
|
CurrentEmail: cognitoUser.Email,
|
|
Agreed: false,
|
|
}
|
|
|
|
if agreement, found := agreementMap[cognitoUser.SubjectID]; found {
|
|
user.Agreed = true
|
|
user.Email = &agreement.Useremail
|
|
if agreement.Agreedat.Valid {
|
|
agreedAt := agreement.Agreedat.Time
|
|
user.AgreedAt = &agreedAt
|
|
}
|
|
user.AgreedFromIP = &agreement.Agreedfromip
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
// FilterAndSortComplianceUsers filters users by agreement status and sorts them.
|
|
// Agreed users come first (sorted by agreedAt DESC), then not-agreed (sorted by email ASC).
|
|
func (s *Service) FilterAndSortComplianceUsers(users []ComplianceReportUser, agreedFilter *bool) []ComplianceReportUser {
|
|
// Apply filter if specified
|
|
filtered := users
|
|
if agreedFilter != nil {
|
|
filtered = make([]ComplianceReportUser, 0)
|
|
for _, user := range users {
|
|
if user.Agreed == *agreedFilter {
|
|
filtered = append(filtered, user)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort: agreed first (by agreedAt DESC), then not-agreed (by email ASC)
|
|
sort.Slice(filtered, func(i, j int) bool {
|
|
return CompareComplianceUsers(filtered[i], filtered[j])
|
|
})
|
|
|
|
return filtered
|
|
}
|
|
|
|
// CompareComplianceUsers compares two compliance users for sorting.
|
|
// Returns true if user i should come before user j.
|
|
func CompareComplianceUsers(i, j ComplianceReportUser) bool {
|
|
// Agreed users come first
|
|
if i.Agreed != j.Agreed {
|
|
return i.Agreed
|
|
}
|
|
// Among agreed users, sort by agreedAt DESC (most recent first)
|
|
if i.Agreed && j.Agreed {
|
|
if i.AgreedAt != nil && j.AgreedAt != nil {
|
|
return i.AgreedAt.After(*j.AgreedAt)
|
|
}
|
|
return i.AgreedAt != nil
|
|
}
|
|
// Among not-agreed users, sort by email ASC
|
|
return i.CurrentEmail < j.CurrentEmail
|
|
}
|
|
|
|
// PaginateComplianceUsers applies pagination to the filtered user list.
|
|
// Returns the paginated slice, total items count, and total pages count.
|
|
func (s *Service) PaginateComplianceUsers(users []ComplianceReportUser, page, pageSize int32) ([]ComplianceReportUser, int32, int32) {
|
|
totalItems := SafeIntToInt32(len(users))
|
|
totalPages := (totalItems + pageSize - 1) / pageSize
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
end := offset + pageSize
|
|
|
|
// Handle bounds
|
|
if offset > totalItems {
|
|
offset = totalItems
|
|
}
|
|
if end > totalItems {
|
|
end = totalItems
|
|
}
|
|
|
|
return users[offset:end], totalItems, totalPages
|
|
}
|
|
|
|
// SafeIntToInt32 safely converts an int to int32, capping at math.MaxInt32 if needed.
|
|
func SafeIntToInt32(n int) int32 {
|
|
if n > math.MaxInt32 {
|
|
return math.MaxInt32
|
|
}
|
|
if n < 0 {
|
|
return 0
|
|
}
|
|
return int32(n) // #nosec G115 -- bounds checked above
|
|
}
|
|
|
|
// FetchAllAgreementsForVersion fetches all agreements for a specific EULA version.
|
|
// Uses pagination to handle large numbers of agreements.
|
|
//
|
|
// Parameters:
|
|
// - ctx: Context for cancellation and timeout
|
|
// - versionID: The EULA version UUID
|
|
//
|
|
// Returns:
|
|
// - A slice of all agreements for this version
|
|
// - An error if the database query fails
|
|
func (s *Service) FetchAllAgreementsForVersion(
|
|
ctx context.Context,
|
|
versionID uuid.UUID,
|
|
) ([]*repository.Eulaagreement, error) {
|
|
const batchSize = 1000
|
|
var allAgreements []*repository.Eulaagreement
|
|
var offset int64
|
|
|
|
for {
|
|
agreements, err := s.cfg.GetDBQueries().ListAgreedUsersForVersion(ctx, &repository.ListAgreedUsersForVersionParams{
|
|
Eulaversionid: versionID,
|
|
Limit: batchSize,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allAgreements = append(allAgreements, agreements...)
|
|
|
|
// Check if there are more records
|
|
if len(agreements) < batchSize {
|
|
break
|
|
}
|
|
offset += batchSize
|
|
}
|
|
|
|
return allAgreements, nil
|
|
}
|