62b5de5722
fix eula issue when email not present in jwt * bug fix
550 lines
17 KiB
Go
550 lines
17 KiB
Go
// eulaAdminHandlers.go implements the admin EULA API handlers.
|
|
// These endpoints handle EULA version management, agreement listing, and compliance reporting.
|
|
package queryapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"queryorchestration/internal/database/repository"
|
|
"queryorchestration/internal/eula"
|
|
"queryorchestration/internal/serviceconfig/aws"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
|
|
"github.com/google/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/oapi-codegen/nullable"
|
|
openapi_types "github.com/oapi-codegen/runtime/types"
|
|
)
|
|
|
|
// ListEulaVersions returns a paginated list of all EULA versions.
|
|
// GET /admin/eula
|
|
//
|
|
// Requires AdminService permission.
|
|
//
|
|
// Parameters:
|
|
// - page: Page number (1-based, default: 1)
|
|
// - page_size: Items per page (1-100, default: 20)
|
|
//
|
|
// Returns:
|
|
// - 200 OK with EulaVersionList on success
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) ListEulaVersions(ctx echo.Context, params ListEulaVersionsParams) error {
|
|
// Apply defaults
|
|
page := int32(1)
|
|
pageSize := int32(20)
|
|
if params.Page != nil {
|
|
page = *params.Page
|
|
}
|
|
if params.PageSize != nil {
|
|
pageSize = *params.PageSize
|
|
}
|
|
|
|
result, err := s.svc.Eula.ListVersions(ctx.Request().Context(), &eula.ListVersionsInput{
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
s.cfg.GetAuthLogger().Error("ListEulaVersions: failed to list EULA versions",
|
|
"error", err,
|
|
"page", page,
|
|
"page_size", pageSize)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("ListEulaVersions: failed to list EULA versions: %v", err),
|
|
})
|
|
}
|
|
|
|
// Convert to API response
|
|
versions := make([]EulaVersion, len(result.Versions))
|
|
for i, v := range result.Versions {
|
|
versions[i] = convertVersionToAPI(v)
|
|
}
|
|
|
|
response := EulaVersionList{
|
|
Versions: versions,
|
|
Total: int32(result.Total), // #nosec G115 -- Total is bounded by practical EULA version counts (< 1000)
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
HasMore: &result.HasMore,
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// CreateEulaVersion creates a new EULA version.
|
|
// POST /admin/eula
|
|
//
|
|
// Requires AdminService permission.
|
|
//
|
|
// Request body: EulaVersionCreate
|
|
//
|
|
// Returns:
|
|
// - 201 Created with EulaVersion on success
|
|
// - 400 Bad Request on validation errors
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 409 Conflict if version string already exists
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) CreateEulaVersion(ctx echo.Context) error {
|
|
var req EulaVersionCreate
|
|
if err := ctx.Bind(&req); err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
|
Message: "Invalid request body",
|
|
})
|
|
}
|
|
|
|
// Get admin user for audit
|
|
adminUser, err := getAdminUserForAudit(ctx)
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
|
Message: "Could not identify admin user",
|
|
})
|
|
}
|
|
|
|
// Extract effectiveDate if specified (nullable field)
|
|
var effectiveDate *time.Time
|
|
if req.EffectiveDate.IsSpecified() && !req.EffectiveDate.IsNull() {
|
|
val := req.EffectiveDate.MustGet()
|
|
effectiveDate = &val
|
|
}
|
|
|
|
version, err := s.svc.Eula.CreateVersion(ctx.Request().Context(), &eula.CreateVersionInput{
|
|
Version: req.Version,
|
|
Title: req.Title,
|
|
Content: req.Content,
|
|
EffectiveDate: effectiveDate,
|
|
CreatedBy: adminUser.Email,
|
|
})
|
|
if err != nil {
|
|
// Check for duplicate version
|
|
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
|
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
|
Message: "EULA version already exists",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("CreateEulaVersion: failed to create EULA version",
|
|
"error", err,
|
|
"version", req.Version,
|
|
"created_by", adminUser.Email)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to create EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
return ctx.JSON(http.StatusCreated, convertVersionToAPI(version))
|
|
}
|
|
|
|
// GetEulaVersion retrieves a specific EULA version by ID.
|
|
// GET /admin/eula/{version_id}
|
|
//
|
|
// Requires AdminService permission.
|
|
//
|
|
// Returns:
|
|
// - 200 OK with EulaVersion on success
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 404 Not Found if version doesn't exist
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) GetEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
|
id, err := convertVersionIDParam(versionID)
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
|
Message: "Invalid version ID format",
|
|
})
|
|
}
|
|
|
|
version, err := s.svc.Eula.GetVersionByID(ctx.Request().Context(), id)
|
|
if err != nil {
|
|
if errors.Is(err, eula.ErrVersionNotFound) {
|
|
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
|
Message: "EULA version not found",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("GetEulaVersion: failed to get EULA version",
|
|
"error", err,
|
|
"version_id", id)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to get EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
|
}
|
|
|
|
// UpdateEulaVersion updates the metadata of an EULA version.
|
|
// Only title and effective date can be updated; content is immutable.
|
|
// PATCH /admin/eula/{version_id}
|
|
//
|
|
// Requires AdminService permission.
|
|
//
|
|
// Request body: EulaVersionUpdate
|
|
//
|
|
// Returns:
|
|
// - 200 OK with updated EulaVersion on success
|
|
// - 400 Bad Request on validation errors
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 404 Not Found if version doesn't exist
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) UpdateEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
|
id, err := convertVersionIDParam(versionID)
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
|
Message: "Invalid version ID format",
|
|
})
|
|
}
|
|
|
|
var req EulaVersionUpdate
|
|
if err := ctx.Bind(&req); err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
|
Message: "Invalid request body",
|
|
})
|
|
}
|
|
|
|
input := &eula.UpdateVersionInput{}
|
|
if req.Title != nil {
|
|
input.Title = req.Title
|
|
}
|
|
if req.EffectiveDate != nil {
|
|
input.EffectiveDate = req.EffectiveDate
|
|
}
|
|
|
|
version, err := s.svc.Eula.UpdateVersion(ctx.Request().Context(), id, input)
|
|
if err != nil {
|
|
if errors.Is(err, eula.ErrVersionNotFound) {
|
|
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
|
Message: "EULA version not found",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("UpdateEulaVersion: failed to update EULA version",
|
|
"error", err,
|
|
"version_id", id)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to update EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
|
}
|
|
|
|
// ActivateEulaVersion sets the specified version as the current active EULA.
|
|
// POST /admin/eula/{version_id}/activate
|
|
//
|
|
// Requires AdminService permission.
|
|
// Uses a transaction to atomically clear the previous current flag and set the new one.
|
|
//
|
|
// Returns:
|
|
// - 200 OK with activated EulaVersion on success
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 404 Not Found if version doesn't exist
|
|
// - 409 Conflict if another admin activated a different version concurrently
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) ActivateEulaVersion(ctx echo.Context, versionID EulaVersionID) error {
|
|
id, err := convertVersionIDParam(versionID)
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
|
|
Message: "Invalid version ID format",
|
|
})
|
|
}
|
|
|
|
// Get admin user for audit
|
|
adminUser, err := getAdminUserForAudit(ctx)
|
|
if err != nil {
|
|
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
|
Message: "Could not identify admin user",
|
|
})
|
|
}
|
|
|
|
version, err := s.svc.Eula.ActivateVersion(ctx.Request().Context(), id, adminUser.Email)
|
|
if err != nil {
|
|
if errors.Is(err, eula.ErrVersionNotFound) {
|
|
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
|
Message: "EULA version not found",
|
|
})
|
|
}
|
|
if errors.Is(err, eula.ErrConcurrentActivation) {
|
|
return ctx.JSON(http.StatusConflict, ErrorMessage{
|
|
Message: "Concurrent activation conflict - another version was activated. Please refresh and try again.",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("ActivateEulaVersion: failed to activate EULA version",
|
|
"error", err,
|
|
"version_id", id,
|
|
"activated_by", adminUser.Email)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to activate EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, convertVersionToAPI(version))
|
|
}
|
|
|
|
// ListEulaAgreements returns a paginated list of EULA agreements.
|
|
// GET /admin/eula/agreements
|
|
//
|
|
// Requires AdminService permission.
|
|
//
|
|
// Parameters:
|
|
// - page: Page number (1-based, default: 1)
|
|
// - page_size: Items per page (1-100, default: 20)
|
|
// - version_id: Optional filter by EULA version
|
|
// - user_id: Optional filter by Cognito subject ID
|
|
//
|
|
// Returns:
|
|
// - 200 OK with EulaAgreementList on success
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) ListEulaAgreements(ctx echo.Context, params ListEulaAgreementsParams) error {
|
|
// Apply defaults
|
|
page := int32(1)
|
|
pageSize := int32(20)
|
|
if params.Page != nil {
|
|
page = *params.Page
|
|
}
|
|
if params.PageSize != nil {
|
|
pageSize = *params.PageSize
|
|
}
|
|
|
|
input := &eula.ListAgreementsInput{
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
|
|
// Convert optional UUID filter
|
|
if params.VersionId != nil {
|
|
versionID := uuid.UUID(*params.VersionId)
|
|
input.VersionID = &versionID
|
|
}
|
|
if params.UserId != nil {
|
|
input.UserID = params.UserId
|
|
}
|
|
|
|
result, err := s.svc.Eula.ListAgreements(ctx.Request().Context(), input)
|
|
if err != nil {
|
|
s.cfg.GetAuthLogger().Error("ListEulaAgreements: failed to list EULA agreements",
|
|
"error", err,
|
|
"page", page,
|
|
"page_size", pageSize)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("ListEulaAgreements: failed to list EULA agreements: %v", err),
|
|
})
|
|
}
|
|
|
|
// Convert to API response
|
|
agreements := make([]EulaAgreement, len(result.Agreements))
|
|
for i, a := range result.Agreements {
|
|
agreements[i] = convertAgreementToAPI(a)
|
|
}
|
|
|
|
response := EulaAgreementList{
|
|
Agreements: agreements,
|
|
Total: int32(result.Total), // #nosec G115 -- Total is bounded by pageSize pagination
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
HasMore: &result.HasMore,
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// GetEulaCompliance returns a comprehensive compliance report.
|
|
// GET /admin/eula/compliance
|
|
//
|
|
// Requires AdminService permission.
|
|
// This endpoint queries Cognito for all users and compares with EULA agreements.
|
|
//
|
|
// Parameters:
|
|
// - page: Page number (1-based, default: 1)
|
|
// - page_size: Items per page (1-100, default: 50)
|
|
// - version_id: Optional specific version to report on (defaults to current)
|
|
// - agreed: Optional filter by agreement status
|
|
//
|
|
// Returns:
|
|
// - 200 OK with EulaComplianceReport on success
|
|
// - 401 Unauthorized if not authenticated
|
|
// - 403 Forbidden if not authorized
|
|
// - 404 Not Found if specified version doesn't exist or no current EULA
|
|
// - 502 Bad Gateway on Cognito errors
|
|
// - 500 Internal Server Error on database errors
|
|
func (s *Controllers) GetEulaCompliance(ctx echo.Context, params GetEulaComplianceParams) error {
|
|
// Initialize AWS config for Cognito
|
|
awsCfg, err := aws.GetAWSConfig(context.Background())
|
|
if err != nil {
|
|
s.cfg.GetAuthLogger().Error("GetEulaCompliance: failed to initialize AWS configuration",
|
|
"error", err)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("GetEulaCompliance: failed to initialize AWS configuration: %v", err),
|
|
})
|
|
}
|
|
|
|
cognitoClient := cognitoidentityprovider.NewFromConfig(awsCfg)
|
|
userPoolID := s.cfg.GetAuthUserPoolID()
|
|
|
|
// Build service input from params
|
|
input := &eula.ComplianceReportInput{
|
|
Page: 1,
|
|
PageSize: 50,
|
|
}
|
|
|
|
if params.Page != nil {
|
|
input.Page = *params.Page
|
|
}
|
|
if params.PageSize != nil {
|
|
input.PageSize = *params.PageSize
|
|
}
|
|
if params.VersionId != nil {
|
|
// Convert openapi_types.UUID to uuid.UUID
|
|
versionID := uuid.UUID(*params.VersionId)
|
|
input.VersionID = &versionID
|
|
}
|
|
if params.Agreed != nil {
|
|
input.Agreed = params.Agreed
|
|
}
|
|
|
|
// Call service method
|
|
result, err := s.svc.Eula.GetComplianceReport(ctx.Request().Context(), cognitoClient, userPoolID, input)
|
|
if err != nil {
|
|
if errors.Is(err, eula.ErrNoCurrentVersion) {
|
|
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
|
Message: "GetEulaCompliance: no current EULA version configured",
|
|
})
|
|
}
|
|
if errors.Is(err, eula.ErrVersionNotFound) {
|
|
return ctx.JSON(http.StatusNotFound, ErrorMessage{
|
|
Message: "GetEulaCompliance: specified EULA version not found",
|
|
})
|
|
}
|
|
// Check if it's a Cognito error
|
|
if strings.Contains(err.Error(), "Cognito") || strings.Contains(err.Error(), "cognito") {
|
|
s.cfg.GetAuthLogger().Error("GetEulaCompliance: Cognito error during compliance report",
|
|
"error", err)
|
|
return ctx.JSON(http.StatusBadGateway, ErrorMessage{
|
|
Message: fmt.Sprintf("GetEulaCompliance: failed to fetch users from Cognito: %v", err),
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("GetEulaCompliance: failed to generate compliance report",
|
|
"error", err)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("GetEulaCompliance: failed to generate compliance report: %v", err),
|
|
})
|
|
}
|
|
|
|
// Convert result to API response
|
|
response := convertComplianceReportToAPI(result)
|
|
|
|
return ctx.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// convertComplianceReportToAPI converts the service result to the API response type.
|
|
func convertComplianceReportToAPI(result *eula.ComplianceReportResult) EulaComplianceReport {
|
|
// Convert version to summary format
|
|
versionSummary := EulaVersionSummary{
|
|
Id: openapi_types.UUID(result.Version.ID),
|
|
Version: result.Version.Version,
|
|
Title: result.Version.Title,
|
|
IsCurrent: result.Version.Iscurrent,
|
|
}
|
|
// Handle nullable effectiveDate
|
|
if result.Version.Effectivedate.Valid {
|
|
versionSummary.EffectiveDate = nullable.NewNullableWithValue(result.Version.Effectivedate.Time)
|
|
}
|
|
|
|
// Convert summary
|
|
summary := EulaComplianceSummary{
|
|
TotalUsers: result.Summary.TotalUsers,
|
|
AgreedCount: result.Summary.AgreedCount,
|
|
NotAgreedCount: result.Summary.NotAgreedCount,
|
|
CompliancePercentage: result.Summary.CompliancePercentage,
|
|
}
|
|
|
|
// Convert users
|
|
users := make([]EulaComplianceUser, 0, len(result.Users))
|
|
for _, u := range result.Users {
|
|
user := EulaComplianceUser{
|
|
CognitoSubjectId: u.CognitoSubjectID,
|
|
CurrentEmail: u.CurrentEmail,
|
|
Agreed: u.Agreed,
|
|
}
|
|
|
|
if u.Email != nil {
|
|
user.Email = nullable.NewNullableWithValue(*u.Email)
|
|
}
|
|
if u.AgreedAt != nil {
|
|
user.AgreedAt = nullable.NewNullableWithValue(*u.AgreedAt)
|
|
}
|
|
if u.AgreedFromIP != nil {
|
|
user.AgreedFromIp = nullable.NewNullableWithValue(*u.AgreedFromIP)
|
|
}
|
|
|
|
users = append(users, user)
|
|
}
|
|
|
|
// Build pagination
|
|
pagination := EulaCompliancePagination{
|
|
Page: result.Page,
|
|
PageSize: result.PageSize,
|
|
TotalPages: result.TotalPages,
|
|
TotalItems: result.TotalItems,
|
|
}
|
|
|
|
return EulaComplianceReport{
|
|
EulaVersion: versionSummary,
|
|
Summary: summary,
|
|
Users: users,
|
|
Pagination: pagination,
|
|
}
|
|
}
|
|
|
|
// convertVersionToAPI converts a repository Eulaversion to the API EulaVersion type.
|
|
func convertVersionToAPI(v *repository.Eulaversion) EulaVersion {
|
|
version := EulaVersion{
|
|
Id: openapi_types.UUID(v.ID),
|
|
Version: v.Version,
|
|
Title: v.Title,
|
|
Content: v.Content,
|
|
CreatedAt: v.Createdat.Time,
|
|
CreatedBy: v.Createdby,
|
|
IsCurrent: v.Iscurrent,
|
|
}
|
|
|
|
// Handle nullable effectiveDate
|
|
if v.Effectivedate.Valid {
|
|
version.EffectiveDate = nullable.NewNullableWithValue(v.Effectivedate.Time)
|
|
}
|
|
|
|
if v.Activatedat.Valid {
|
|
version.ActivatedAt = nullable.NewNullableWithValue(v.Activatedat.Time)
|
|
}
|
|
if v.Activatedby != nil {
|
|
version.ActivatedBy = nullable.NewNullableWithValue(*v.Activatedby)
|
|
}
|
|
|
|
return version
|
|
}
|
|
|
|
// convertAgreementToAPI converts a repository ListEulaAgreementsRow to the API EulaAgreement type.
|
|
func convertAgreementToAPI(a *repository.ListEulaAgreementsRow) EulaAgreement {
|
|
var agreedAt time.Time
|
|
if a.Agreedat.Valid {
|
|
agreedAt = a.Agreedat.Time
|
|
}
|
|
|
|
return EulaAgreement{
|
|
Id: openapi_types.UUID(a.ID),
|
|
CognitoSubjectId: a.Cognitosubjectid,
|
|
UserEmail: a.Useremail,
|
|
EulaVersionId: openapi_types.UUID(a.Eulaversionid),
|
|
EulaVersion: a.Eulaversionstring,
|
|
AgreedAt: agreedAt,
|
|
AgreedFromIp: a.Agreedfromip,
|
|
}
|
|
}
|