58912a66d4
handle missing email in jwt * handle missing email
217 lines
7.1 KiB
Go
217 lines
7.1 KiB
Go
// eulaPublicHandlers.go implements the public EULA API handlers.
|
|
// These endpoints handle EULA retrieval and user agreement recording.
|
|
package queryapi
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"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",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("GetCurrentEula: failed to get current EULA version",
|
|
"error", err)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to get current EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
// 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",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("GetEulaStatus: failed to get EULA status",
|
|
"error", err,
|
|
"user_subject", userSubject)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to get EULA status: %v", err),
|
|
})
|
|
}
|
|
|
|
// 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 {
|
|
s.cfg.GetAuthLogger().Error("AgreeToEula: authentication info missing from context",
|
|
"subject_ok", subjectOK,
|
|
"info_ok", infoOK)
|
|
return ctx.JSON(http.StatusUnauthorized, ErrorMessage{
|
|
Message: "Authentication required",
|
|
})
|
|
}
|
|
|
|
// Resolve the user identifier for the agreement record.
|
|
// Cognito access tokens do not include the email claim by default (only ID tokens do).
|
|
// Fall back to username, then to subject ID (always present in any Cognito token).
|
|
userIdentifier := userInfo.Email
|
|
if userIdentifier == "" {
|
|
userIdentifier = userInfo.Username
|
|
s.cfg.GetAuthLogger().Info("AgreeToEula: email missing from token, using username fallback",
|
|
"user_subject", userSubject,
|
|
"username", userInfo.Username)
|
|
}
|
|
if userIdentifier == "" {
|
|
userIdentifier = userSubject
|
|
s.cfg.GetAuthLogger().Info("AgreeToEula: email and username missing from token, using subject ID fallback",
|
|
"user_subject", userSubject)
|
|
}
|
|
|
|
// 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",
|
|
})
|
|
}
|
|
s.cfg.GetAuthLogger().Error("AgreeToEula: failed to get current EULA version",
|
|
"error", err,
|
|
"user_subject", userSubject)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to get current EULA version: %v", err),
|
|
})
|
|
}
|
|
|
|
// 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: userIdentifier,
|
|
EulaVersionID: currentVersion.ID,
|
|
IPAddress: ipAddress,
|
|
})
|
|
if err != nil {
|
|
s.cfg.GetAuthLogger().Error("AgreeToEula: failed to record EULA agreement",
|
|
"error", err,
|
|
"user_subject", userSubject,
|
|
"user_identifier", userIdentifier,
|
|
"eula_version_id", currentVersion.ID)
|
|
return ctx.JSON(http.StatusInternalServerError, ErrorMessage{
|
|
Message: fmt.Sprintf("Failed to record EULA agreement: %v", err),
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|