Merged in feature/eula.part1 (pull request #206)
eula support * eula support * docs
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user