Files
query-orchestration/api/queryAPI/eulaPublicHandlers.go
T
Jay Brown 63c12a2f44 Merged in feature/eula.part1 (pull request #206)
eula support

* eula support

* docs
2026-01-22 18:17:27 +00:00

184 lines
5.6 KiB
Go

// eulaPublicHandlers.go implements the public EULA API handlers.
// These endpoints handle EULA retrieval and user agreement recording.
package queryapi
import (
"errors"
"net/http"
"time"
"queryorchestration/internal/cognitoauth"
"queryorchestration/internal/eula"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/oapi-codegen/nullable"
openapi_types "github.com/oapi-codegen/runtime/types"
)
// GetCurrentEula returns the current active EULA version.
// This endpoint is public and does not require authentication.
// GET /eula
//
// Returns:
// - 200 OK with EulaPublicResponse on success
// - 404 Not Found if no EULA is currently active
// - 500 Internal Server Error on database errors
func (s *Controllers) GetCurrentEula(ctx echo.Context) error {
version, err := s.svc.Eula.GetCurrentVersion(ctx.Request().Context())
if err != nil {
if errors.Is(err, eula.ErrNoCurrentVersion) {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: "No current EULA version configured",
})
}
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to get current EULA version",
})
}
// Convert to public response
response := EulaPublicResponse{
Id: openapi_types.UUID(version.ID),
Version: version.Version,
Title: version.Title,
Content: version.Content,
}
// Handle nullable effectiveDate
if version.Effectivedate.Valid {
response.EffectiveDate = nullable.NewNullableWithValue(version.Effectivedate.Time)
}
return ctx.JSON(http.StatusOK, response)
}
// GetEulaStatus returns the current user's agreement status for the current EULA.
// Used by the frontend to determine if the user needs to be prompted to agree.
// GET /eula/status
//
// Requires authentication via JWT.
//
// Returns:
// - 200 OK with EulaStatusResponse on success
// - 401 Unauthorized if not authenticated
// - 404 Not Found if no EULA is currently active
// - 500 Internal Server Error on database errors
func (s *Controllers) GetEulaStatus(ctx echo.Context) error {
// Get user subject from JWT
userSubject, ok := cognitoauth.GetUserSubject(ctx)
if !ok {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Authentication required",
})
}
status, err := s.svc.Eula.GetUserEulaStatus(ctx.Request().Context(), userSubject)
if err != nil {
if errors.Is(err, eula.ErrNoCurrentVersion) {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: "No current EULA version configured",
})
}
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to get EULA status",
})
}
// Convert to API response
response := EulaStatusResponse{
HasAgreed: status.HasAgreed,
CurrentVersion: status.CurrentVersion,
CurrentVersionId: openapi_types.UUID(status.CurrentVersionID),
}
if status.AgreedAt != nil {
response.AgreedAt = nullable.NewNullableWithValue(*status.AgreedAt)
}
if status.AgreedVersion != nil {
response.AgreedVersion = nullable.NewNullableWithValue(*status.AgreedVersion)
}
return ctx.JSON(http.StatusOK, response)
}
// AgreeToEula records the current user's agreement to the current EULA version.
// This operation is idempotent - calling it multiple times will not create duplicate records.
// POST /eula/agree
//
// Requires authentication via JWT.
// IP address is automatically captured from the request.
//
// Returns:
// - 201 Created with EulaAgreementResponse for new agreements
// - 200 OK with EulaAgreementResponse if user already agreed (idempotent)
// - 400 Bad Request if no current EULA is configured
// - 401 Unauthorized if not authenticated
// - 500 Internal Server Error on database errors
func (s *Controllers) AgreeToEula(ctx echo.Context) error {
// Get user info from JWT
userSubject, subjectOK := cognitoauth.GetUserSubject(ctx)
userInfo, infoOK := cognitoauth.GetUserInfo(ctx)
if !subjectOK || !infoOK {
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
Message: "Authentication required",
})
}
// Get current EULA version
currentVersion, err := s.svc.Eula.GetCurrentVersion(ctx.Request().Context())
if err != nil {
if errors.Is(err, eula.ErrNoCurrentVersion) {
return ctx.JSON(http.StatusBadRequest, ErrorMessage{
Message: "No current EULA version configured",
})
}
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to get current EULA version",
})
}
// Get client IP address
ipAddress := ctx.RealIP()
if ipAddress == "" {
ipAddress = ctx.Request().RemoteAddr
}
// Record the agreement
result, err := s.svc.Eula.RecordAgreement(ctx.Request().Context(), &eula.RecordAgreementInput{
CognitoSubjectID: userSubject,
UserEmail: userInfo.Email,
EulaVersionID: currentVersion.ID,
IPAddress: ipAddress,
})
if err != nil {
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
Message: "Failed to record EULA agreement",
})
}
// Build response
var agreedAt time.Time
if result.Agreement.Agreedat.Valid {
agreedAt = result.Agreement.Agreedat.Time
}
response := EulaAgreementResponse{
Id: openapi_types.UUID(result.Agreement.ID),
EulaVersionId: openapi_types.UUID(result.Agreement.Eulaversionid),
EulaVersion: currentVersion.Version,
AgreedAt: agreedAt,
}
// Return 200 if already agreed (idempotent), 201 for new agreement
if result.AlreadyAgreed {
return ctx.JSON(http.StatusOK, response)
}
return ctx.JSON(http.StatusCreated, response)
}
// convertVersionIDParam converts the path parameter to uuid.UUID.
// Returns error if the ID format is invalid.
func convertVersionIDParam(versionID EulaVersionID) (uuid.UUID, error) {
return uuid.UUID(versionID), nil
}