Merged in feature/eula.part1 (pull request #206)
eula support * eula support * docs
This commit is contained in:
@@ -21,6 +21,10 @@ func isPublicPath(requestPath string, config auth.ConfigProvider) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Allow public access to current EULA version (GET /eula only)
|
||||
if requestPath == "/eula" {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(requestPath, "/swagger/") || strings.HasPrefix(requestPath, "/metrics")
|
||||
}
|
||||
|
||||
@@ -63,7 +67,9 @@ func extractToken(c echo.Context, config auth.ConfigProvider) (string, error) {
|
||||
// This enables users to discover their own roles/permissions before making other API calls.
|
||||
func isAuthOnlyPath(requestPath string) bool {
|
||||
authOnlyPaths := []string{
|
||||
"/identity", // Users need to discover their roles without having any specific role
|
||||
"/identity", // Users need to discover their roles without having any specific role
|
||||
"/eula/status", // Users need to check their EULA agreement status
|
||||
"/eula/agree", // Users need to be able to agree to the EULA
|
||||
}
|
||||
for _, path := range authOnlyPaths {
|
||||
if requestPath == path {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package cognitoauth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// TestUserMiddleware injects test user identity from headers when DISABLE_AUTH=true.
|
||||
//
|
||||
// This middleware enables testing of user-authenticated endpoints without real JWT
|
||||
// authentication. It only activates when the DISABLE_AUTH environment variable is
|
||||
// set to "true". In production (DISABLE_AUTH=false or unset), this middleware does
|
||||
// nothing and passes requests through unchanged.
|
||||
//
|
||||
// When active, it checks for the X-Test-User-Subject header. If present, it injects
|
||||
// user identity into the request context, allowing handlers to retrieve user info
|
||||
// via GetUserSubject() and GetUserInfo() as if a real JWT had been validated.
|
||||
//
|
||||
// Headers:
|
||||
// - X-Test-User-Subject: The user's subject ID (required for injection)
|
||||
// - X-Test-User-Email: The user's email (optional, defaults to <subject>@test.local)
|
||||
//
|
||||
// Usage in test scripts:
|
||||
//
|
||||
// curl -X GET http://localhost:8080/eula/status \
|
||||
// -H "X-Test-User-Subject: test-user-123" \
|
||||
// -H "X-Test-User-Email: testuser@example.com"
|
||||
//
|
||||
// This middleware should be registered early in the middleware chain so that
|
||||
// subsequent handlers and middleware can access the injected user context.
|
||||
//
|
||||
// Returns:
|
||||
// - echo.MiddlewareFunc: Middleware that conditionally injects test user identity
|
||||
func TestUserMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Only active when DISABLE_AUTH=true (test mode)
|
||||
disableAuth := os.Getenv("DISABLE_AUTH")
|
||||
if strings.ToLower(disableAuth) != "true" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Check for test user header
|
||||
subject := c.Request().Header.Get("X-Test-User-Subject")
|
||||
if subject == "" {
|
||||
// No test user header, continue normally
|
||||
// (admin endpoints will use system user via their own logic)
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// Get email from header or generate default
|
||||
email := c.Request().Header.Get("X-Test-User-Email")
|
||||
if email == "" {
|
||||
email = subject + "@test.local"
|
||||
}
|
||||
|
||||
// Inject user context (same structure as real auth middleware)
|
||||
c.Set("user_claims", map[string]any{
|
||||
"sub": subject,
|
||||
"email": email,
|
||||
})
|
||||
c.Set("user_info", UserInfo{
|
||||
Email: email,
|
||||
Username: subject,
|
||||
})
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Rollback: Drop eulaAgreements and eulaVersions tables with indexes
|
||||
DROP INDEX IF EXISTS idx_eulaagreements_user_time;
|
||||
DROP INDEX IF EXISTS idx_eulaagreements_version_time;
|
||||
DROP INDEX IF EXISTS idx_eulaagreements_versionid;
|
||||
DROP INDEX IF EXISTS idx_eulaagreements_subjectid;
|
||||
DROP TABLE IF EXISTS eulaAgreements;
|
||||
|
||||
DROP INDEX IF EXISTS idx_eulaversions_iscurrent;
|
||||
DROP INDEX IF EXISTS idx_eulaversions_effectivedate;
|
||||
DROP INDEX IF EXISTS idx_eulaversions_one_current;
|
||||
DROP TABLE IF EXISTS eulaVersions;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Create eulaVersions table for storing EULA versions
|
||||
CREATE TABLE eulaVersions (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
version VARCHAR(50) NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
effectiveDate TIMESTAMPTZ NOT NULL,
|
||||
createdAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
createdBy VARCHAR(320) NOT NULL,
|
||||
isCurrent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
activatedAt TIMESTAMPTZ,
|
||||
activatedBy VARCHAR(320)
|
||||
);
|
||||
|
||||
-- Partial unique index enforces only ONE row can have isCurrent=true at any time
|
||||
CREATE UNIQUE INDEX idx_eulaversions_one_current ON eulaVersions(isCurrent) WHERE isCurrent = TRUE;
|
||||
|
||||
-- Index for quick lookup of current version
|
||||
CREATE INDEX idx_eulaversions_effectivedate ON eulaVersions(effectiveDate DESC);
|
||||
|
||||
-- Index for isCurrent lookup (used in GetCurrentEulaVersion)
|
||||
CREATE INDEX idx_eulaversions_iscurrent ON eulaVersions(isCurrent);
|
||||
|
||||
-- Create eulaAgreements table for tracking user agreements
|
||||
CREATE TABLE eulaAgreements (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
|
||||
cognitoSubjectId VARCHAR(256) NOT NULL,
|
||||
userEmail VARCHAR(320) NOT NULL,
|
||||
eulaVersionId uuid NOT NULL REFERENCES eulaVersions(id) ON DELETE RESTRICT,
|
||||
agreedAt TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
agreedFromIp VARCHAR(45) NOT NULL,
|
||||
UNIQUE(cognitoSubjectId, eulaVersionId)
|
||||
);
|
||||
|
||||
-- Basic indexes
|
||||
CREATE INDEX idx_eulaagreements_subjectid ON eulaAgreements(cognitoSubjectId);
|
||||
CREATE INDEX idx_eulaagreements_versionid ON eulaAgreements(eulaVersionId);
|
||||
|
||||
-- Composite indexes for common read patterns
|
||||
CREATE INDEX idx_eulaagreements_version_time ON eulaAgreements(eulaVersionId, agreedAt DESC);
|
||||
CREATE INDEX idx_eulaagreements_user_time ON eulaAgreements(cognitoSubjectId, agreedAt DESC);
|
||||
@@ -0,0 +1,110 @@
|
||||
-- name: CreateEulaVersion :one
|
||||
-- Creates a new EULA version
|
||||
INSERT INTO eulaVersions (version, title, content, effectiveDate, createdBy)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy;
|
||||
|
||||
-- name: GetEulaVersionById :one
|
||||
-- Retrieves a specific EULA version by its ID
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetCurrentEulaVersion :one
|
||||
-- Retrieves the current active EULA version
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
WHERE isCurrent = TRUE;
|
||||
|
||||
-- name: ListEulaVersions :many
|
||||
-- Lists all EULA versions with pagination, ordered by createdAt descending
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
ORDER BY createdAt DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountEulaVersions :one
|
||||
-- Counts total number of EULA versions
|
||||
SELECT COUNT(*) as total
|
||||
FROM eulaVersions;
|
||||
|
||||
-- name: UpdateEulaVersionMetadata :one
|
||||
-- Updates only the metadata (title and effectiveDate) of an EULA version
|
||||
UPDATE eulaVersions
|
||||
SET title = $2, effectiveDate = $3
|
||||
WHERE id = $1
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy;
|
||||
|
||||
-- name: ClearCurrentEulaVersions :exec
|
||||
-- Clears the isCurrent flag from all versions (does not clear activatedAt/By for audit trail)
|
||||
UPDATE eulaVersions
|
||||
SET isCurrent = FALSE
|
||||
WHERE isCurrent = TRUE;
|
||||
|
||||
-- name: SetEulaVersionCurrent :one
|
||||
-- Sets a specific version as the current active version with audit fields
|
||||
UPDATE eulaVersions
|
||||
SET isCurrent = TRUE, activatedAt = NOW(), activatedBy = $2
|
||||
WHERE id = $1
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy;
|
||||
|
||||
-- name: CreateEulaAgreement :one
|
||||
-- Records a user's agreement to an EULA version
|
||||
INSERT INTO eulaAgreements (cognitoSubjectId, userEmail, eulaVersionId, agreedFromIp)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp;
|
||||
|
||||
-- name: GetUserEulaAgreement :one
|
||||
-- Checks if a user has agreed to a specific EULA version
|
||||
SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
FROM eulaAgreements
|
||||
WHERE cognitoSubjectId = $1 AND eulaVersionId = $2;
|
||||
|
||||
-- name: GetUserCurrentEulaAgreement :one
|
||||
-- Checks if a user has agreed to the current EULA version
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE a.cognitoSubjectId = $1 AND v.isCurrent = TRUE;
|
||||
|
||||
-- name: ListEulaAgreements :many
|
||||
-- Lists agreements with optional filtering by version_id or user_id
|
||||
-- Pass NULL for filters to skip them
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
v.version as eulaVersionString
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE (sqlc.narg(version_id)::uuid IS NULL OR a.eulaVersionId = sqlc.narg(version_id)::uuid)
|
||||
AND (sqlc.narg(user_id)::text IS NULL OR a.cognitoSubjectId = sqlc.narg(user_id)::text)
|
||||
ORDER BY a.agreedAt DESC
|
||||
LIMIT $1 OFFSET $2;
|
||||
|
||||
-- name: CountEulaAgreements :one
|
||||
-- Counts agreements with optional filtering (matches ListEulaAgreements filters)
|
||||
SELECT COUNT(*) as total
|
||||
FROM eulaAgreements a
|
||||
WHERE (sqlc.narg(version_id)::uuid IS NULL OR a.eulaVersionId = sqlc.narg(version_id)::uuid)
|
||||
AND (sqlc.narg(user_id)::text IS NULL OR a.cognitoSubjectId = sqlc.narg(user_id)::text);
|
||||
|
||||
-- name: ListEulaAgreementsByUser :many
|
||||
-- Gets all EULA versions a specific user has agreed to (user's complete history)
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
v.version as eulaVersionString, v.title as eulaVersionTitle
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE a.cognitoSubjectId = $1
|
||||
ORDER BY a.agreedAt DESC;
|
||||
|
||||
-- name: CountEulaAgreementsByVersion :one
|
||||
-- Counts how many users have agreed to a specific EULA version
|
||||
SELECT COUNT(*) as count
|
||||
FROM eulaAgreements
|
||||
WHERE eulaVersionId = $1;
|
||||
|
||||
-- name: ListAgreedUsersForVersion :many
|
||||
-- Lists all users who have agreed to a specific EULA version
|
||||
SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
FROM eulaAgreements
|
||||
WHERE eulaVersionId = $1
|
||||
ORDER BY agreedAt DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
@@ -0,0 +1,582 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: eula.sql
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const clearCurrentEulaVersions = `-- name: ClearCurrentEulaVersions :exec
|
||||
UPDATE eulaVersions
|
||||
SET isCurrent = FALSE
|
||||
WHERE isCurrent = TRUE
|
||||
`
|
||||
|
||||
// Clears the isCurrent flag from all versions (does not clear activatedAt/By for audit trail)
|
||||
//
|
||||
// UPDATE eulaVersions
|
||||
// SET isCurrent = FALSE
|
||||
// WHERE isCurrent = TRUE
|
||||
func (q *Queries) ClearCurrentEulaVersions(ctx context.Context) error {
|
||||
_, err := q.db.Exec(ctx, clearCurrentEulaVersions)
|
||||
return err
|
||||
}
|
||||
|
||||
const countEulaAgreements = `-- name: CountEulaAgreements :one
|
||||
SELECT COUNT(*) as total
|
||||
FROM eulaAgreements a
|
||||
WHERE ($1::uuid IS NULL OR a.eulaVersionId = $1::uuid)
|
||||
AND ($2::text IS NULL OR a.cognitoSubjectId = $2::text)
|
||||
`
|
||||
|
||||
type CountEulaAgreementsParams struct {
|
||||
VersionID *uuid.UUID `db:"version_id"`
|
||||
UserID *string `db:"user_id"`
|
||||
}
|
||||
|
||||
// Counts agreements with optional filtering (matches ListEulaAgreements filters)
|
||||
//
|
||||
// SELECT COUNT(*) as total
|
||||
// FROM eulaAgreements a
|
||||
// WHERE ($1::uuid IS NULL OR a.eulaVersionId = $1::uuid)
|
||||
// AND ($2::text IS NULL OR a.cognitoSubjectId = $2::text)
|
||||
func (q *Queries) CountEulaAgreements(ctx context.Context, arg *CountEulaAgreementsParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countEulaAgreements, arg.VersionID, arg.UserID)
|
||||
var total int64
|
||||
err := row.Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
const countEulaAgreementsByVersion = `-- name: CountEulaAgreementsByVersion :one
|
||||
SELECT COUNT(*) as count
|
||||
FROM eulaAgreements
|
||||
WHERE eulaVersionId = $1
|
||||
`
|
||||
|
||||
// Counts how many users have agreed to a specific EULA version
|
||||
//
|
||||
// SELECT COUNT(*) as count
|
||||
// FROM eulaAgreements
|
||||
// WHERE eulaVersionId = $1
|
||||
func (q *Queries) CountEulaAgreementsByVersion(ctx context.Context, eulaversionid uuid.UUID) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countEulaAgreementsByVersion, eulaversionid)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const countEulaVersions = `-- name: CountEulaVersions :one
|
||||
SELECT COUNT(*) as total
|
||||
FROM eulaVersions
|
||||
`
|
||||
|
||||
// Counts total number of EULA versions
|
||||
//
|
||||
// SELECT COUNT(*) as total
|
||||
// FROM eulaVersions
|
||||
func (q *Queries) CountEulaVersions(ctx context.Context) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countEulaVersions)
|
||||
var total int64
|
||||
err := row.Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
const createEulaAgreement = `-- name: CreateEulaAgreement :one
|
||||
INSERT INTO eulaAgreements (cognitoSubjectId, userEmail, eulaVersionId, agreedFromIp)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
`
|
||||
|
||||
type CreateEulaAgreementParams struct {
|
||||
Cognitosubjectid string `db:"cognitosubjectid"`
|
||||
Useremail string `db:"useremail"`
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
Agreedfromip string `db:"agreedfromip"`
|
||||
}
|
||||
|
||||
// Records a user's agreement to an EULA version
|
||||
//
|
||||
// INSERT INTO eulaAgreements (cognitoSubjectId, userEmail, eulaVersionId, agreedFromIp)
|
||||
// VALUES ($1, $2, $3, $4)
|
||||
// RETURNING id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
func (q *Queries) CreateEulaAgreement(ctx context.Context, arg *CreateEulaAgreementParams) (*Eulaagreement, error) {
|
||||
row := q.db.QueryRow(ctx, createEulaAgreement,
|
||||
arg.Cognitosubjectid,
|
||||
arg.Useremail,
|
||||
arg.Eulaversionid,
|
||||
arg.Agreedfromip,
|
||||
)
|
||||
var i Eulaagreement
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const createEulaVersion = `-- name: CreateEulaVersion :one
|
||||
INSERT INTO eulaVersions (version, title, content, effectiveDate, createdBy)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
`
|
||||
|
||||
type CreateEulaVersionParams struct {
|
||||
Version string `db:"version"`
|
||||
Title string `db:"title"`
|
||||
Content string `db:"content"`
|
||||
Effectivedate pgtype.Timestamptz `db:"effectivedate"`
|
||||
Createdby string `db:"createdby"`
|
||||
}
|
||||
|
||||
// Creates a new EULA version
|
||||
//
|
||||
// INSERT INTO eulaVersions (version, title, content, effectiveDate, createdBy)
|
||||
// VALUES ($1, $2, $3, $4, $5)
|
||||
// RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
func (q *Queries) CreateEulaVersion(ctx context.Context, arg *CreateEulaVersionParams) (*Eulaversion, error) {
|
||||
row := q.db.QueryRow(ctx, createEulaVersion,
|
||||
arg.Version,
|
||||
arg.Title,
|
||||
arg.Content,
|
||||
arg.Effectivedate,
|
||||
arg.Createdby,
|
||||
)
|
||||
var i Eulaversion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getCurrentEulaVersion = `-- name: GetCurrentEulaVersion :one
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
WHERE isCurrent = TRUE
|
||||
`
|
||||
|
||||
// Retrieves the current active EULA version
|
||||
//
|
||||
// SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
// FROM eulaVersions
|
||||
// WHERE isCurrent = TRUE
|
||||
func (q *Queries) GetCurrentEulaVersion(ctx context.Context) (*Eulaversion, error) {
|
||||
row := q.db.QueryRow(ctx, getCurrentEulaVersion)
|
||||
var i Eulaversion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getEulaVersionById = `-- name: GetEulaVersionById :one
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
// Retrieves a specific EULA version by its ID
|
||||
//
|
||||
// SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
// FROM eulaVersions
|
||||
// WHERE id = $1
|
||||
func (q *Queries) GetEulaVersionById(ctx context.Context, id uuid.UUID) (*Eulaversion, error) {
|
||||
row := q.db.QueryRow(ctx, getEulaVersionById, id)
|
||||
var i Eulaversion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getUserCurrentEulaAgreement = `-- name: GetUserCurrentEulaAgreement :one
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE a.cognitoSubjectId = $1 AND v.isCurrent = TRUE
|
||||
`
|
||||
|
||||
// Checks if a user has agreed to the current EULA version
|
||||
//
|
||||
// SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp
|
||||
// FROM eulaAgreements a
|
||||
// JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
// WHERE a.cognitoSubjectId = $1 AND v.isCurrent = TRUE
|
||||
func (q *Queries) GetUserCurrentEulaAgreement(ctx context.Context, cognitosubjectid string) (*Eulaagreement, error) {
|
||||
row := q.db.QueryRow(ctx, getUserCurrentEulaAgreement, cognitosubjectid)
|
||||
var i Eulaagreement
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getUserEulaAgreement = `-- name: GetUserEulaAgreement :one
|
||||
SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
FROM eulaAgreements
|
||||
WHERE cognitoSubjectId = $1 AND eulaVersionId = $2
|
||||
`
|
||||
|
||||
type GetUserEulaAgreementParams struct {
|
||||
Cognitosubjectid string `db:"cognitosubjectid"`
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
}
|
||||
|
||||
// Checks if a user has agreed to a specific EULA version
|
||||
//
|
||||
// SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
// FROM eulaAgreements
|
||||
// WHERE cognitoSubjectId = $1 AND eulaVersionId = $2
|
||||
func (q *Queries) GetUserEulaAgreement(ctx context.Context, arg *GetUserEulaAgreementParams) (*Eulaagreement, error) {
|
||||
row := q.db.QueryRow(ctx, getUserEulaAgreement, arg.Cognitosubjectid, arg.Eulaversionid)
|
||||
var i Eulaagreement
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const listAgreedUsersForVersion = `-- name: ListAgreedUsersForVersion :many
|
||||
SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
FROM eulaAgreements
|
||||
WHERE eulaVersionId = $1
|
||||
ORDER BY agreedAt DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListAgreedUsersForVersionParams struct {
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
Limit int64 `db:"limit"`
|
||||
Offset int64 `db:"offset"`
|
||||
}
|
||||
|
||||
// Lists all users who have agreed to a specific EULA version
|
||||
//
|
||||
// SELECT id, cognitoSubjectId, userEmail, eulaVersionId, agreedAt, agreedFromIp
|
||||
// FROM eulaAgreements
|
||||
// WHERE eulaVersionId = $1
|
||||
// ORDER BY agreedAt DESC
|
||||
// LIMIT $2 OFFSET $3
|
||||
func (q *Queries) ListAgreedUsersForVersion(ctx context.Context, arg *ListAgreedUsersForVersionParams) ([]*Eulaagreement, error) {
|
||||
rows, err := q.db.Query(ctx, listAgreedUsersForVersion, arg.Eulaversionid, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Eulaagreement{}
|
||||
for rows.Next() {
|
||||
var i Eulaagreement
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listEulaAgreements = `-- name: ListEulaAgreements :many
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
v.version as eulaVersionString
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE ($3::uuid IS NULL OR a.eulaVersionId = $3::uuid)
|
||||
AND ($4::text IS NULL OR a.cognitoSubjectId = $4::text)
|
||||
ORDER BY a.agreedAt DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
type ListEulaAgreementsParams struct {
|
||||
Limit int64 `db:"limit"`
|
||||
Offset int64 `db:"offset"`
|
||||
VersionID *uuid.UUID `db:"version_id"`
|
||||
UserID *string `db:"user_id"`
|
||||
}
|
||||
|
||||
type ListEulaAgreementsRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Cognitosubjectid string `db:"cognitosubjectid"`
|
||||
Useremail string `db:"useremail"`
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
Agreedat pgtype.Timestamptz `db:"agreedat"`
|
||||
Agreedfromip string `db:"agreedfromip"`
|
||||
Eulaversionstring string `db:"eulaversionstring"`
|
||||
}
|
||||
|
||||
// Lists agreements with optional filtering by version_id or user_id
|
||||
// Pass NULL for filters to skip them
|
||||
//
|
||||
// SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
// v.version as eulaVersionString
|
||||
// FROM eulaAgreements a
|
||||
// JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
// WHERE ($3::uuid IS NULL OR a.eulaVersionId = $3::uuid)
|
||||
// AND ($4::text IS NULL OR a.cognitoSubjectId = $4::text)
|
||||
// ORDER BY a.agreedAt DESC
|
||||
// LIMIT $1 OFFSET $2
|
||||
func (q *Queries) ListEulaAgreements(ctx context.Context, arg *ListEulaAgreementsParams) ([]*ListEulaAgreementsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listEulaAgreements,
|
||||
arg.Limit,
|
||||
arg.Offset,
|
||||
arg.VersionID,
|
||||
arg.UserID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*ListEulaAgreementsRow{}
|
||||
for rows.Next() {
|
||||
var i ListEulaAgreementsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
&i.Eulaversionstring,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listEulaAgreementsByUser = `-- name: ListEulaAgreementsByUser :many
|
||||
SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
v.version as eulaVersionString, v.title as eulaVersionTitle
|
||||
FROM eulaAgreements a
|
||||
JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
WHERE a.cognitoSubjectId = $1
|
||||
ORDER BY a.agreedAt DESC
|
||||
`
|
||||
|
||||
type ListEulaAgreementsByUserRow struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Cognitosubjectid string `db:"cognitosubjectid"`
|
||||
Useremail string `db:"useremail"`
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
Agreedat pgtype.Timestamptz `db:"agreedat"`
|
||||
Agreedfromip string `db:"agreedfromip"`
|
||||
Eulaversionstring string `db:"eulaversionstring"`
|
||||
Eulaversiontitle string `db:"eulaversiontitle"`
|
||||
}
|
||||
|
||||
// Gets all EULA versions a specific user has agreed to (user's complete history)
|
||||
//
|
||||
// SELECT a.id, a.cognitoSubjectId, a.userEmail, a.eulaVersionId, a.agreedAt, a.agreedFromIp,
|
||||
// v.version as eulaVersionString, v.title as eulaVersionTitle
|
||||
// FROM eulaAgreements a
|
||||
// JOIN eulaVersions v ON a.eulaVersionId = v.id
|
||||
// WHERE a.cognitoSubjectId = $1
|
||||
// ORDER BY a.agreedAt DESC
|
||||
func (q *Queries) ListEulaAgreementsByUser(ctx context.Context, cognitosubjectid string) ([]*ListEulaAgreementsByUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listEulaAgreementsByUser, cognitosubjectid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*ListEulaAgreementsByUserRow{}
|
||||
for rows.Next() {
|
||||
var i ListEulaAgreementsByUserRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Cognitosubjectid,
|
||||
&i.Useremail,
|
||||
&i.Eulaversionid,
|
||||
&i.Agreedat,
|
||||
&i.Agreedfromip,
|
||||
&i.Eulaversionstring,
|
||||
&i.Eulaversiontitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listEulaVersions = `-- name: ListEulaVersions :many
|
||||
SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
FROM eulaVersions
|
||||
ORDER BY createdAt DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
type ListEulaVersionsParams struct {
|
||||
Limit int64 `db:"limit"`
|
||||
Offset int64 `db:"offset"`
|
||||
}
|
||||
|
||||
// Lists all EULA versions with pagination, ordered by createdAt descending
|
||||
//
|
||||
// SELECT id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
// FROM eulaVersions
|
||||
// ORDER BY createdAt DESC
|
||||
// LIMIT $1 OFFSET $2
|
||||
func (q *Queries) ListEulaVersions(ctx context.Context, arg *ListEulaVersionsParams) ([]*Eulaversion, error) {
|
||||
rows, err := q.db.Query(ctx, listEulaVersions, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []*Eulaversion{}
|
||||
for rows.Next() {
|
||||
var i Eulaversion
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const setEulaVersionCurrent = `-- name: SetEulaVersionCurrent :one
|
||||
UPDATE eulaVersions
|
||||
SET isCurrent = TRUE, activatedAt = NOW(), activatedBy = $2
|
||||
WHERE id = $1
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
`
|
||||
|
||||
type SetEulaVersionCurrentParams struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Activatedby *string `db:"activatedby"`
|
||||
}
|
||||
|
||||
// Sets a specific version as the current active version with audit fields
|
||||
//
|
||||
// UPDATE eulaVersions
|
||||
// SET isCurrent = TRUE, activatedAt = NOW(), activatedBy = $2
|
||||
// WHERE id = $1
|
||||
// RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
func (q *Queries) SetEulaVersionCurrent(ctx context.Context, arg *SetEulaVersionCurrentParams) (*Eulaversion, error) {
|
||||
row := q.db.QueryRow(ctx, setEulaVersionCurrent, arg.ID, arg.Activatedby)
|
||||
var i Eulaversion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const updateEulaVersionMetadata = `-- name: UpdateEulaVersionMetadata :one
|
||||
UPDATE eulaVersions
|
||||
SET title = $2, effectiveDate = $3
|
||||
WHERE id = $1
|
||||
RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
`
|
||||
|
||||
type UpdateEulaVersionMetadataParams struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Title string `db:"title"`
|
||||
Effectivedate pgtype.Timestamptz `db:"effectivedate"`
|
||||
}
|
||||
|
||||
// Updates only the metadata (title and effectiveDate) of an EULA version
|
||||
//
|
||||
// UPDATE eulaVersions
|
||||
// SET title = $2, effectiveDate = $3
|
||||
// WHERE id = $1
|
||||
// RETURNING id, version, title, content, effectiveDate, createdAt, createdBy, isCurrent, activatedAt, activatedBy
|
||||
func (q *Queries) UpdateEulaVersionMetadata(ctx context.Context, arg *UpdateEulaVersionMetadataParams) (*Eulaversion, error) {
|
||||
row := q.db.QueryRow(ctx, updateEulaVersionMetadata, arg.ID, arg.Title, arg.Effectivedate)
|
||||
var i Eulaversion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Version,
|
||||
&i.Title,
|
||||
&i.Content,
|
||||
&i.Effectivedate,
|
||||
&i.Createdat,
|
||||
&i.Createdby,
|
||||
&i.Iscurrent,
|
||||
&i.Activatedat,
|
||||
&i.Activatedby,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
@@ -524,6 +524,28 @@ type Documentupload struct {
|
||||
FolderID *uuid.UUID `db:"folder_id"`
|
||||
}
|
||||
|
||||
type Eulaagreement struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Cognitosubjectid string `db:"cognitosubjectid"`
|
||||
Useremail string `db:"useremail"`
|
||||
Eulaversionid uuid.UUID `db:"eulaversionid"`
|
||||
Agreedat pgtype.Timestamptz `db:"agreedat"`
|
||||
Agreedfromip string `db:"agreedfromip"`
|
||||
}
|
||||
|
||||
type Eulaversion struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
Version string `db:"version"`
|
||||
Title string `db:"title"`
|
||||
Content string `db:"content"`
|
||||
Effectivedate pgtype.Timestamptz `db:"effectivedate"`
|
||||
Createdat pgtype.Timestamptz `db:"createdat"`
|
||||
Createdby string `db:"createdby"`
|
||||
Iscurrent bool `db:"iscurrent"`
|
||||
Activatedat pgtype.Timestamptz `db:"activatedat"`
|
||||
Activatedby *string `db:"activatedby"`
|
||||
}
|
||||
|
||||
// Virtual folder hierarchy for document organization. Database-only - has no direct relationship to S3 storage locations.
|
||||
type Folder struct {
|
||||
ID uuid.UUID `db:"id"`
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Package eula provides EULA (End User License Agreement) version management and compliance tracking.
|
||||
// This file contains functions that require Cognito integration and cannot be tested with localstack.
|
||||
package eula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"queryorchestration/internal/usermanagement"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
|
||||
)
|
||||
|
||||
// GetComplianceReport generates a compliance report showing which users have agreed to an EULA version.
|
||||
// This function requires Cognito access to list users and compare against recorded agreements.
|
||||
//
|
||||
// The report includes:
|
||||
// - Version information for the target EULA
|
||||
// - Summary statistics (total users, agreed count, not agreed count, compliance percentage)
|
||||
// - Paginated list of users with their agreement status
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for cancellation and timeout
|
||||
// - cognitoClient: AWS Cognito Identity Provider client
|
||||
// - userPoolID: The Cognito User Pool ID to query for users
|
||||
// - input: Report configuration including optional version_id, pagination, and filters
|
||||
//
|
||||
// Returns:
|
||||
// - ComplianceReportResult with version info, summary stats, and paginated user list
|
||||
// - ErrNoCurrentVersion if no version_id provided and no current version exists
|
||||
// - ErrVersionNotFound if the specified version_id does not exist
|
||||
// - An error if Cognito or database queries fail
|
||||
func (s *Service) GetComplianceReport(
|
||||
ctx context.Context,
|
||||
cognitoClient *cognitoidentityprovider.Client,
|
||||
userPoolID string,
|
||||
input *ComplianceReportInput,
|
||||
) (*ComplianceReportResult, error) {
|
||||
// Apply default pagination values
|
||||
s.ApplyCompliancePaginationDefaults(input)
|
||||
|
||||
// Step 1: Resolve target EULA version
|
||||
version, err := s.ResolveComplianceVersion(ctx, input.VersionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Step 2: Fetch all enabled users from Cognito
|
||||
cognitoUsers, err := s.fetchAllEnabledCognitoUsers(ctx, cognitoClient, userPoolID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch Cognito users: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Fetch all agreements for this version from DB
|
||||
agreements, err := s.FetchAllAgreementsForVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch agreements: %w", err)
|
||||
}
|
||||
|
||||
// Step 4: Build user list with agreement status and compute summary
|
||||
allUsers, summary := s.BuildComplianceUserList(cognitoUsers, agreements)
|
||||
|
||||
// Step 5: Apply filters and sort
|
||||
filteredUsers := s.FilterAndSortComplianceUsers(allUsers, input.Agreed)
|
||||
|
||||
// Step 6: Apply pagination
|
||||
paginatedUsers, totalItems, totalPages := s.PaginateComplianceUsers(filteredUsers, input.Page, input.PageSize)
|
||||
|
||||
return &ComplianceReportResult{
|
||||
Version: version,
|
||||
Summary: summary,
|
||||
Users: paginatedUsers,
|
||||
Page: input.Page,
|
||||
PageSize: input.PageSize,
|
||||
TotalPages: totalPages,
|
||||
TotalItems: totalItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fetchAllEnabledCognitoUsers fetches all enabled users from Cognito using pagination.
|
||||
// Cognito has a max limit of 60 users per request, so this loops until all users are fetched.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for cancellation and timeout
|
||||
// - client: AWS Cognito Identity Provider client
|
||||
// - userPoolID: The Cognito User Pool ID
|
||||
//
|
||||
// Returns:
|
||||
// - A slice of all enabled Cognito users
|
||||
// - An error if any Cognito API call fails
|
||||
func (s *Service) fetchAllEnabledCognitoUsers(
|
||||
ctx context.Context,
|
||||
client *cognitoidentityprovider.Client,
|
||||
userPoolID string,
|
||||
) ([]usermanagement.CognitoUserResponse, error) {
|
||||
const maxPerRequest = 60
|
||||
var allUsers []usermanagement.CognitoUserResponse
|
||||
var paginationToken *string
|
||||
|
||||
for {
|
||||
result, err := usermanagement.ListCognitoUsers(ctx, client, userPoolID, maxPerRequest, paginationToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter to only enabled users
|
||||
for _, user := range result.Users {
|
||||
if user.Enabled {
|
||||
allUsers = append(allUsers, user)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
if result.PaginationKey == nil || *result.PaginationKey == "" {
|
||||
break
|
||||
}
|
||||
paginationToken = result.PaginationKey
|
||||
}
|
||||
|
||||
return allUsers, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -320,6 +320,10 @@ func New(ctx context.Context, cfg Config) (*Server, error) {
|
||||
cognitoauth.RegisterRoutes(cfg.GetRouter(), cfg)
|
||||
// auth end
|
||||
|
||||
// Test user injection middleware - only active when DISABLE_AUTH=true
|
||||
// This allows testing user-authenticated endpoints by passing X-Test-User-Subject header
|
||||
e.Use(cognitoauth.TestUserMiddleware())
|
||||
|
||||
opnapi, err := cfg.RegisterHandlers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user