Documentation
¶
Overview ¶
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for analytics endpoints.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Package api provides the HTTP API handlers for the CUDly dashboard.
Index ¶
- Constants
- func IsClientError(err error) (*clientError, bool)
- func IsNotFoundError(err error) bool
- func IsOIDCIssuerPath(path string) bool
- func NewClientError(code int, message string) error
- func NewClientErrorWithDetails(code int, message string, details map[string]any) error
- type APIKeyInfo
- type AccountServiceOverrideRequest
- type AccountSummary
- type AccountTestResult
- type AdminExistsResponse
- type AnalyticsClientInterface
- type AnalyticsCollectorInterface
- type AnalyticsResponse
- type AnalyticsSnapshotStoreInterface
- type AuthLevel
- type AuthServiceInterface
- type AzureCompatibleOfferingsRequestBody
- type AzureCompatibleOfferingsResponse
- type AzureExchangeSourceBody
- type AzureExchangeTargetBody
- type AzureExecuteExchangeRequestBody
- type AzureExecuteExchangeResponse
- type BreakdownResponse
- type BreakdownValue
- type ChangePasswordRequest
- type CloudAccountRequest
- type CommitmentOptsInterface
- type ConfigResponse
- type ConvertibleRIsResponse
- type CoverageBreakdownResponse
- type CoverageServiceRow
- type CreateAPIKeyRequest
- type CreateAPIKeyResponse
- type CreateGroupRequest
- type CreatePlannedPurchasesRequest
- type CreatePlannedPurchasesResponse
- type CreateUserRequest
- type CredentialsRequest
- type CurrentUserResponse
- type DBRateLimiter
- func (rl *DBRateLimiter) Allow(ctx context.Context, key, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithEmail(ctx context.Context, email, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithIP(ctx context.Context, ip, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithUser(ctx context.Context, userID, endpoint string) (bool, error)
- func (rl *DBRateLimiter) SetLimit(endpoint string, config RateLimitConfig)
- func (rl *DBRateLimiter) StartCleanupWorker(ctx context.Context)
- type DashboardSummaryResponse
- type DeploymentInfoResponse
- type DiscoverOrgRequest
- type DiscoverOrgResult
- type EmptyServiceConfigResponse
- type ExchangeExecuteRequestBody
- type ExchangeExecuteResponse
- type ExchangeQuoteRequestBody
- type ExchangeTargetBody
- type ExchangeableAzureRIsResponse
- type ExecutePurchaseRequest
- type FederationIaCResponse
- type Group
- type Handler
- func (h *Handler) GetHealth(ctx context.Context) (*HealthResponse, error)
- func (h *Handler) HandleOIDC(ctx context.Context, req *events.LambdaFunctionURLRequest) (*events.LambdaFunctionURLResponse, bool)
- func (h *Handler) HandleRequest(ctx context.Context, req *events.LambdaFunctionURLRequest) (*events.LambdaFunctionURLResponse, error)
- type HandlerConfig
- type HealthCheck
- type HealthResponse
- type HistoryDataPoint
- type HistoryResponse
- type HistorySummary
- type HistorySummaryAnalytics
- type InMemoryRateLimiter
- func (rl *InMemoryRateLimiter) Allow(ctx context.Context, key, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithEmail(ctx context.Context, email, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithIP(ctx context.Context, ip, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithUser(ctx context.Context, userID, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) SetLimit(endpoint string, config RateLimitConfig)
- type InventoryCommitment
- type InventoryCommitmentsResponse
- type LambdaInvokerInterface
- type LoginRequest
- type LoginResponse
- type MFADisableRequest
- type MFAEnableRequest
- type MFAEnableResponse
- type MFARegenerateRequest
- type MFARegenerateResponse
- type MFASetupRequest
- type MFASetupResponse
- type MarketplaceListRequest
- type MarketplaceListResponse
- type MarketplacePriceTier
- type PasswordResetConfirm
- type PasswordResetRequest
- type PatchPlanRequest
- type PaymentAdjustment
- type Permission
- type PermissionConstraint
- type PermissionEntry
- type PlanHealthFactor
- type PlanHealthFactorCode
- type PlanRequest
- type PlanWithHealth
- type PlannedPurchase
- type PlannedPurchasesResponse
- type PlansResponse
- type PostgresAnalyticsClient
- type Principal
- type PrincipalKind
- type ProfileUpdateRequest
- type ProviderCoverageSection
- type PublicInfoResponse
- type PurchaseManagerInterface
- type RIExchangeConfigResponse
- type RIExchangeConfigUpdateRequest
- type RIExchangeHistoryResponse
- type RIUtilizationResponse
- type RateLimitConfig
- type RateLimiterInterface
- type RecommendationDetailResponse
- type RecommendationsResponse
- type RecommendationsSummary
- type RefreshResponse
- type RegistrationRequest
- type RegistrationStatusResponse
- type RejectRequest
- type ReshapeRecommendationsResponse
- type Route
- type RouteHandler
- type Router
- type SchedulerInterface
- type ServiceSavings
- type Session
- type SetupAdminRequest
- type StatusResponse
- type TargetOfferingsResponse
- type TrendsResponse
- type UpcomingPurchase
- type UpcomingPurchaseResponse
- type UpdateGroupRequest
- type UpdateUserRequest
- type UsagePoint
- type User
- type UserInfo
- type UserPermissionsResponse
- type VersionResponse
Constants ¶
const AzureRevocationWindowDays = config.AzureRevocationWindowDays
AzureRevocationWindowDays is the number of days after purchase within which Azure reservations are eligible for a return (refund). Per Azure docs: https://learn.microsoft.com/azure/cost-management-billing/reservations/exchange-and-refund-azure-reservations Aliases config.AzureRevocationWindowDays so the purchase-write path and this endpoint share a single source of truth for the window length.
const MaxAccountIDsPerRequest = 200
MaxAccountIDsPerRequest caps the number of account IDs accepted in a single comma-separated `account_ids` query parameter. Each accepted ID fans out into per-account DB queries / cloud API calls downstream, so an unbounded list is an amplification vector — a single request with thousands of IDs can exhaust connection pools or hit cloud-API rate limits. 200 is generous for legitimate usage (typical operators have far fewer onboarded accounts) while keeping the worst-case work bounded.
const MaxHistoryDateRangeDays = 366
MaxHistoryDateRangeDays caps the inclusive start/end window the History handler accepts on a single request. Mirrors the analytics cap (issue #414 / PR #529): an unbounded range turns the WHERE-on-timestamp into a full-table scan over purchase_history, which the dashboard frontend neither needs nor renders coherently past a year. 366 admits a full leap year and rejects anything larger with 400.
const (
// MaxRequestBodySize is the maximum allowed request body size (1MB).
MaxRequestBodySize = 1 * 1024 * 1024
)
Security constants.
const ( // OIDCBasePath is the URL prefix CUDly publishes its OIDC issuer // under. Federated credentials registered with target clouds // (Azure AD, GCP STS) must use issuer = <base URL> + OIDCBasePath. OIDCBasePath = "/oidc" )
OIDC discovery paths served by this handler. Scoped under /oidc/ so the paths read descriptively in code and in CUDly's URL space rather than sitting at the root under the opaque .well-known prefix. The RFC 8414 discovery suffix still applies (Azure AD, GCP STS, etc. fetch ${issuer}/.well-known/openid-configuration), but because we register the issuer URL as <host>/oidc the full external paths become <host>/oidc/.well-known/openid-configuration — the issuer prefix alone appears in routing lists, the well-known suffix is only an implementation detail inside handler_oidc.go.
Variables ¶
This section is empty.
Functions ¶
func IsClientError ¶
IsClientError checks if the error is a client error and returns it.
func IsNotFoundError ¶
IsNotFoundError checks if the error is a not found error.
func IsOIDCIssuerPath ¶
IsOIDCIssuerPath returns true if path belongs to CUDly's OIDC issuer surface (the discovery document or the JWKS). The server transport layer uses this to route requests directly to HandleOIDC before the main API router so the issuer endpoints never touch the auth middleware or the static-file fallback.
func NewClientError ¶
NewClientError creates a new client-facing error with the given HTTP status code and message.
func NewClientErrorWithDetails ¶
NewClientErrorWithDetails creates a client-facing error that also carries structured detail fields. The response writer flattens those into the JSON body so consumers can branch on machine-readable hints rather than substring-matching the message. Used by the retry handler (issue #47) for ops_hint + retry_attempt_n / threshold callouts. The details map is copied at construction so caller mutations after the error is created don't leak into the response body.
Types ¶
type APIKeyInfo ¶
type APIKeyInfo struct {
ID string `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"key_prefix"`
ExpiresAt string `json:"expires_at,omitempty"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at,omitempty"`
Permissions []Permission `json:"permissions,omitempty"`
IsActive bool `json:"is_active"`
}
APIKeyInfo represents public information about an API key.
type AccountServiceOverrideRequest ¶
type AccountServiceOverrideRequest struct {
Enabled *bool `json:"enabled,omitempty"`
Term *int `json:"term,omitempty"`
Payment *string `json:"payment,omitempty"`
Coverage *float64 `json:"coverage,omitempty"`
RampSchedule *string `json:"ramp_schedule,omitempty"`
IncludeEngines []string `json:"include_engines,omitempty"`
ExcludeEngines []string `json:"exclude_engines,omitempty"`
IncludeRegions []string `json:"include_regions,omitempty"`
ExcludeRegions []string `json:"exclude_regions,omitempty"`
IncludeTypes []string `json:"include_types,omitempty"`
ExcludeTypes []string `json:"exclude_types,omitempty"`
}
AccountServiceOverrideRequest is the request body for service override endpoints.
type AccountSummary ¶
type AccountSummary struct {
ID string `json:"id"`
Name string `json:"name"`
ExternalID string `json:"external_id"`
Provider string `json:"provider"`
}
AccountSummary is the minimal-disclosure projection of a cloud account used by the global topbar filter and the create-plan-from-commitment target prefill (issues #949, #951). It deliberately omits every credential/config field (role ARNs, subscription IDs, client emails, auth modes, bastion IDs, the self-account marker) so the endpoint can be gated on a low read verb that Standard / Read-Only users already hold, without leaking sensitive account configuration. The full CloudAccount object stays behind GET /api/accounts (view:accounts, admin-grade).
type AccountTestResult ¶
AccountTestResult is the response for the test-credentials endpoint.
type AdminExistsResponse ¶
type AdminExistsResponse struct {
AdminExists bool `json:"admin_exists"`
}
AdminExistsResponse holds the admin exists check response.
type AnalyticsClientInterface ¶
type AnalyticsClientInterface interface {
QueryHistory(ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, provider string, start, end time.Time, interval string) ([]HistoryDataPoint, *HistorySummary, error)
QueryBreakdown(ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, start, end time.Time, dimension string) (map[string]BreakdownValue, error)
}
AnalyticsClientInterface defines the interface for analytics queries.
accountUUIDs / accountExternalIDsByProvider are the dual-column account filter: rows match when cloud_account_id = ANY(accountUUIDs) OR (provider = p AND account_id = ANY(accountExternalIDsByProvider[p])). Both nil/empty means "all accounts accessible to the caller" (scoping is enforced upstream in the handler). The handler resolves the requested account (a top-bar chip UUID) into both representations via resolveSingleAccountFilterIDs so rows that carry only the external account_id (cloud_account_id NULL) are still aggregated, and the external ids stay grouped by provider so a reused external number across providers cannot leak the wrong rows (issue #701/#498/#866).
type AnalyticsCollectorInterface ¶
AnalyticsCollectorInterface defines the interface for analytics collection.
type AnalyticsResponse ¶
type AnalyticsResponse struct {
Start string `json:"start"`
End string `json:"end"`
Interval string `json:"interval"`
Summary *HistorySummary `json:"summary"`
DataPoints []HistoryDataPoint `json:"data_points"`
}
AnalyticsResponse represents the response for the analytics endpoint.
type AnalyticsSnapshotStoreInterface ¶
type AnalyticsSnapshotStoreInterface interface {
QuerySavings(ctx context.Context, req analytics.QueryRequest) ([]analytics.SavingsSnapshot, error)
QueryMonthlyTotals(ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, months int) ([]analytics.MonthlySummary, error)
QueryByProvider(ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, startDate, endDate time.Time) ([]analytics.ProviderBreakdown, error)
QueryByService(ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, provider string, startDate, endDate time.Time) ([]analytics.ServiceBreakdown, error)
}
AnalyticsSnapshotStoreInterface exposes the savings-snapshot time-series for the /api/analytics/trends endpoint. It is the read side of the now-wired internal/analytics collector. Scoping is the dual-column model: the handler resolves the requested account into accountUUIDs + accountExternalIDsByProvider (see resolveSingleAccountFilterIDs) and the store ORs both columns so rows carrying only one identifier are still matched. Both empty means "all" — the handler MUST enforce allowed_accounts scope before passing empty filters.
type AuthLevel ¶
type AuthLevel int
AuthLevel controls how Router.Route() enforces authentication. The Auth field on Route is MANDATORY — NewRouter panics at startup if any registered route leaves it at the zero value. See the const block below for the AuthAdmin / AuthUser / AuthPublic options.
Router.Route enforces these levels itself as a defense-in-depth check, in addition to the validateSecurity → authenticate middleware that runs earlier in the request pipeline. If middleware ordering ever changes or a new route bypasses validateSecurity, the router-level enforcement still rejects unauthorized requests.
const ( // AuthAdmin requires admin role (admin API key or admin bearer-token // session). Enforced by Router.Route via h.requireAdmin. AuthAdmin AuthLevel // AuthUser requires any authenticated user (admin API key, user API // key, or any valid bearer-token session). Use for read-only views // and self-service endpoints (logout, profile, API key management). // Enforced by Router.Route via h.requireAuth. AuthUser // AuthPublic requires no authentication. Must also be listed in // isPublicEndpoint() so the middleware skips its auth/CSRF checks. AuthPublic )
type AuthServiceInterface ¶
type AuthServiceInterface interface {
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
Logout(ctx context.Context, token string) error
ValidateSession(ctx context.Context, token string) (*Session, error)
ValidateCSRFToken(ctx context.Context, sessionToken, csrfToken string) error
SetupAdmin(ctx context.Context, req SetupAdminRequest) (*LoginResponse, error)
CheckAdminExists(ctx context.Context) (bool, error)
RequestPasswordReset(ctx context.Context, email string) error
ConfirmPasswordReset(ctx context.Context, req PasswordResetConfirm) error
// ResetTokenStatus returns the runtime state of a reset token
// without consuming it. Used by the GET /api/auth/reset-password/
// status endpoint so the frontend can branch on expired / used
// tokens before rendering the reset-password form (issues #460,
// #461). state is one of "valid" | "expired" | "used"; flow is
// "reset" | "invite".
ResetTokenStatus(ctx context.Context, token string) (state string, flow string, err error)
GetUser(ctx context.Context, userID string) (*User, error)
UpdateUserProfile(ctx context.Context, userID string, email string, currentPassword string, newPassword string) error
// User management - uses auth.API* types
CreateUserAPI(ctx context.Context, req any) (any, error)
UpdateUserAPI(ctx context.Context, actorUserID, userID string, req any) (any, error)
DeleteUser(ctx context.Context, userID string) error
ListUsersAPI(ctx context.Context) (any, error)
ChangePasswordAPI(ctx context.Context, userID, currentPassword, newPassword string) error
// MFA lifecycle (issue #497). All four require the user to be
// already authenticated; setup + disable additionally require a
// fresh password re-verify carried in the request body.
MFASetupAPI(ctx context.Context, userID, password string) (secret, provisioningURI string, err error)
MFAEnableAPI(ctx context.Context, userID, code string) (recoveryCodes []string, err error)
MFADisableAPI(ctx context.Context, userID, password, codeOrRecovery string) error
MFARegenerateRecoveryCodesAPI(ctx context.Context, userID, code string) (recoveryCodes []string, err error)
// Group management - uses auth.API* types
// CreateGroupAPI / UpdateGroupAPI take the acting principal so the
// service can enforce the grant ceiling: a caller may not write a
// permission onto a group that their own effective permissions do not
// hold, and the money verbs carved out of admin:* are not grantable at
// all (issues #1550, #1629).
CreateGroupAPI(ctx context.Context, actorUserID string, req any) (any, error)
UpdateGroupAPI(ctx context.Context, actorUserID, groupID string, req any) (any, error)
DeleteGroup(ctx context.Context, groupID string) error
GetGroupAPI(ctx context.Context, groupID string) (any, error)
ListGroupsAPI(ctx context.Context) (any, error)
// Permission checking
HasPermissionAPI(ctx context.Context, userID, action, resource string) (bool, error)
// HasPermissionForConstraintsAPI checks action on resource against
// request-derived constraint sets so per-permission Constraints
// (MaxPurchaseAmount, Providers, Services, Regions, AccountIDs) are
// enforced at execution time. Every constraint set must be granted by
// at least one of the user's permissions (SEC-01, issue #1141).
HasPermissionForConstraintsAPI(ctx context.Context, userID, action, resource string, constraintSets []auth.PermissionConstraints) (bool, error)
// GetUserPermissionsAPI returns the effective permission set for a user
// (union of all group permissions). Used by GET /api/auth/me/permissions.
// Returns []auth.APIPermission converted to []PermissionEntry by the handler.
GetUserPermissionsAPI(ctx context.Context, userID string) (any, error)
// Account access - returns the union of allowed_accounts from all user groups (empty = all access)
GetAllowedAccountsAPI(ctx context.Context, userID string) ([]string, error)
// API Key management
CreateAPIKeyAPI(ctx context.Context, userID string, req any) (any, error)
ListUserAPIKeysAPI(ctx context.Context, userID string) (any, error)
// GetAPIKeysUsageStatsAPI returns the section-level usage summary
// (active count, fixed-window/lifetime request totals, top-3 most
// active keys) for the calling user's own API keys.
GetAPIKeysUsageStatsAPI(ctx context.Context, userID string) (any, error)
DeleteAPIKeyAPI(ctx context.Context, userID, keyID string) error
RevokeAPIKeyAPI(ctx context.Context, userID, keyID string) error
ValidateUserAPIKeyAPI(ctx context.Context, apiKey string) (any, any, error)
// RecordAPIKeyUsageAsync books exactly one request against the given
// API key ID (the last_used_at timestamp plus the lifetime and
// fixed-window request counters). Fire-and-forget: the store write
// happens off the request path, so it returns nothing and never delays
// or fails the caller's request.
//
// Must be called once per inbound request and only from
// Handler.validateSecurityContext. Calling it from a credential
// validation path instead would count validations, which a single
// request performs several times over.
RecordAPIKeyUsageAsync(keyID string)
// HasAPIKeyPermissionAPI validates a user API key and checks the
// requested action/resource against the key's effective permissions
// (the intersection of the key's scoped permissions with the owning
// user's group-derived permissions). Returns the owning user's ID, the
// key's database ID, and whether the permission is held; a non-nil error
// means the key did not validate or the lookup failed, and callers must
// deny (fail closed). Wired into requirePermission so per-key scoping is
// enforced at request time (issue #1142).
HasAPIKeyPermissionAPI(ctx context.Context, apiKey, action, resource string) (userID, keyID string, allowed bool, err error)
// HasAPIKeyPermissionForConstraintsAPI checks request-derived permission
// constraint sets against a user API key's effective permissions (the
// intersection of the key's own constraints and the owning user's
// group-derived permissions). Must be called after HasAPIKeyPermissionAPI
// confirms action/resource access. Enforces per-key caps (MaxPurchaseAmount,
// Providers, Services, Regions, AccountIDs) that the key's owner cannot
// override via their broader group permissions (adversarial-review F2).
HasAPIKeyPermissionForConstraintsAPI(ctx context.Context, keyID, userID, action, resource string, constraintSets []auth.PermissionConstraints) (bool, error)
}
AuthServiceInterface defines auth service methods used by handler Note: This interface uses API-specific types that are converted from auth package types.
type AzureCompatibleOfferingsRequestBody ¶
type AzureCompatibleOfferingsRequestBody struct {
SubscriptionID string `json:"subscription_id"`
Sources []AzureExchangeSourceBody `json:"sources"`
Targets []AzureExchangeTargetBody `json:"targets"`
}
AzureCompatibleOfferingsRequestBody is the request body for the compatible-offerings endpoint.
type AzureCompatibleOfferingsResponse ¶
type AzureCompatibleOfferingsResponse struct {
Offerings []azurecompute.CompatibleOffering `json:"offerings"`
Preview *azurecompute.ExchangePreview `json:"preview"`
}
AzureCompatibleOfferingsResponse is the response for the compatible-offerings endpoint: the priced candidate offerings plus the preview (including the SessionID a subsequent execute call would need, though execute never trusts a client-supplied session -- see executeAzureExchange).
type AzureExchangeSourceBody ¶
type AzureExchangeSourceBody struct {
ReservationID string `json:"reservation_id"`
Quantity int32 `json:"quantity"`
}
AzureExchangeSourceBody is one source reservation entry in an Azure compatible-offerings or execute request body.
type AzureExchangeTargetBody ¶
type AzureExchangeTargetBody struct {
SKU string `json:"sku"`
Location string `json:"location"`
Term string `json:"term"`
Quantity int32 `json:"quantity"`
// BillingScopeID is optional and is NOT the scope that gets charged:
// the handler always derives that from the request's authorized
// subscription_id (azureBillingScopeID), matching every other Azure
// reservation purchase path in this repo. When supplied it must match
// the derived scope, so a caller cannot direct the charge at a
// different subscription than the one their permission constraints
// were evaluated against.
BillingScopeID string `json:"billing_scope_id,omitempty"`
}
AzureExchangeTargetBody is one target entry. Term is the ISO 8601 reservation term string ("P1Y", "P3Y", ...); azureReservationTermFromString validates it against the SDK's typed enum rather than accepting anything the caller sends.
type AzureExecuteExchangeRequestBody ¶
type AzureExecuteExchangeRequestBody struct {
SubscriptionID string `json:"subscription_id"`
Sources []AzureExchangeSourceBody `json:"sources"`
Targets []AzureExchangeTargetBody `json:"targets"`
MaxPaymentDue string `json:"max_payment_due"`
Currency string `json:"currency"`
}
AzureExecuteExchangeRequestBody is the request body for the execute endpoint. MaxPaymentDue + Currency are mandatory safety guardrails: the handler refuses to execute an exchange whose fresh quote exceeds the cap or is denominated in a different currency.
type AzureExecuteExchangeResponse ¶
type AzureExecuteExchangeResponse struct {
SessionID string `json:"session_id"`
Status string `json:"status"`
NetPayable *float64 `json:"net_payable"`
NetPayableCurrency string `json:"net_payable_currency,omitempty"`
RefundsTotal *float64 `json:"refunds_total"`
PurchasesTotal *float64 `json:"purchases_total"`
}
AzureExecuteExchangeResponse is the response from a successfully executed Azure exchange.
type BreakdownResponse ¶
type BreakdownResponse struct {
Data map[string]BreakdownValue `json:"data"`
Dimension string `json:"dimension"`
Start string `json:"start"`
End string `json:"end"`
}
BreakdownResponse represents the response for the breakdown endpoint.
type BreakdownValue ¶
type BreakdownValue struct {
TotalSavings float64 `json:"total_savings"`
TotalUpfront float64 `json:"total_upfront"`
PurchaseCount int `json:"purchase_count"`
Percentage float64 `json:"percentage"`
}
BreakdownValue represents savings breakdown by dimension.
type ChangePasswordRequest ¶
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
ChangePasswordRequest represents a request to change password.
type CloudAccountRequest ¶
type CloudAccountRequest struct {
Enabled *bool `json:"enabled"`
AWSWebIdentityTokenFile string `json:"aws_web_identity_token_file"`
AzureClientID string `json:"azure_client_id"`
Provider string `json:"provider"`
Name string `json:"name"`
Description string `json:"description"`
AWSAuthMode string `json:"aws_auth_mode"`
AWSRoleARN string `json:"aws_role_arn"`
AWSExternalID string `json:"aws_external_id"`
ContactEmail string `json:"contact_email"`
GCPWIFAudience string `json:"gcp_wif_audience"`
ExternalID string `json:"external_id"`
AzureSubscriptionID string `json:"azure_subscription_id"`
AzureTenantID string `json:"azure_tenant_id"`
AWSBastionID string `json:"aws_bastion_id"`
AzureAuthMode string `json:"azure_auth_mode"`
GCPProjectID string `json:"gcp_project_id"`
GCPClientEmail string `json:"gcp_client_email"`
GCPAuthMode string `json:"gcp_auth_mode"`
AWSIsOrgRoot bool `json:"aws_is_org_root"`
}
CloudAccountRequest is the request body for create/update account endpoints.
type CommitmentOptsInterface ¶
type CommitmentOptsInterface interface {
Get(ctx context.Context) (commitmentopts.Options, error)
Validate(ctx context.Context, provider, service string, term int, payment string) (bool, error)
}
CommitmentOptsInterface lets us swap the real *commitmentopts.Service for a stub in handler tests without pulling in the probe+store machinery.
type ConfigResponse ¶
type ConfigResponse struct {
Global *config.GlobalConfig `json:"global"`
SourceIdentity *sourceIdentity `json:"source_identity,omitempty"`
SourceCloud string `json:"source_cloud,omitempty"`
Services []config.ServiceConfig `json:"services"`
}
ConfigResponse holds the configuration response.
type ConvertibleRIsResponse ¶
type ConvertibleRIsResponse struct {
Instances []ec2svc.ConvertibleRI `json:"instances"`
}
ConvertibleRIsResponse holds the list of convertible RIs.
type CoverageBreakdownResponse ¶
type CoverageBreakdownResponse struct {
Providers []ProviderCoverageSection `json:"providers"`
}
CoverageBreakdownResponse is the envelope returned by GET /api/inventory/coverage.
type CoverageServiceRow ¶
type CoverageServiceRow struct {
CoveragePct *float64 `json:"coverage_pct"`
Service string `json:"service"`
CoveredMonthly float64 `json:"covered_monthly"`
OnDemandMonthly float64 `json:"on_demand_monthly"`
}
CoverageServiceRow is one service row within a provider's coverage section. CoveredMonthly is the sum of active-commitment MonthlyCost values for the (provider, service) pair. OnDemandMonthly is the sum of recommendation Savings values — i.e. the portion of on-demand spend that is NOT yet committed. CoveragePct is nil when both sums are zero (no usage detected), not 0, to preserve the "absent" semantic per feedback_nullable_not_zero.
type CreateAPIKeyRequest ¶
type CreateAPIKeyRequest struct {
ExpiresAt *time.Time `json:"expires_at,omitempty"`
Name string `json:"name"`
Permissions []Permission `json:"permissions,omitempty"`
}
CreateAPIKeyRequest represents a request to create a new API key.
type CreateAPIKeyResponse ¶
type CreateAPIKeyResponse struct {
Info *APIKeyInfo `json:"info"`
APIKey string `json:"api_key"` //nolint:gosec // G117: intentional one-time serialization -- this response returns the newly created key to its owner; the key is never re-stored or re-serialized
KeyID string `json:"key_id"`
}
CreateAPIKeyResponse returns the newly created API key (only shown once).
type CreateGroupRequest ¶
type CreateGroupRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Permissions []Permission `json:"permissions"`
AllowedAccounts []string `json:"allowed_accounts,omitempty"`
}
CreateGroupRequest represents a request to create a new group.
type CreatePlannedPurchasesRequest ¶
type CreatePlannedPurchasesRequest struct {
StartDate string `json:"start_date"`
Count int `json:"count"`
}
CreatePlannedPurchasesRequest represents a request to create planned purchases.
type CreatePlannedPurchasesResponse ¶
type CreatePlannedPurchasesResponse struct {
Created int `json:"created"`
}
CreatePlannedPurchasesResponse represents the response after creating planned purchases.
type CreateUserRequest ¶
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
Groups []string `json:"groups,omitempty"`
}
CreateUserRequest represents a request to create a new user. Groups must be non-empty: authorization is group-membership-only (issue #907).
type CredentialsRequest ¶
type CredentialsRequest struct {
Payload map[string]interface{} `json:"payload"`
CredentialType string `json:"credential_type"`
}
CredentialsRequest is the request body for the save-credentials endpoint.
type CurrentUserResponse ¶
type CurrentUserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Groups []string `json:"groups,omitempty"`
MFAEnabled bool `json:"mfa_enabled"`
}
CurrentUserResponse holds the current user response.
type DBRateLimiter ¶
type DBRateLimiter struct {
// contains filtered or unexported fields
}
DBRateLimiter provides distributed rate limiting using the database This implementation uses a sliding window algorithm with the database as the backend, making it suitable for Lambda functions and distributed systems.
func NewDBRateLimiter ¶
func NewDBRateLimiter(pool *pgxpool.Pool) *DBRateLimiter
NewDBRateLimiter creates a new database-backed rate limiter. Call StartCleanupWorker on the returned limiter to ensure periodic cleanup of expired rows independent of traffic (addresses 02-M2).
func (*DBRateLimiter) Allow ¶
Allow checks if a request should be allowed based on rate limits. The key should be formatted as "IP#{ip}" or "EMAIL#{email}". The endpoint identifies which rate limit configuration to use.
Implementation: a single atomic INSERT ... ON CONFLICT DO UPDATE statement performs the read-modify-write in one round trip, so two concurrent first-requests for the same id can never collide on the PK (the older check-then-insert flow hit SQLSTATE 23505 in production — see commit 9fa4170a1's sibling note in known_issues/05_config_store_postgres.md).
Behavior: each call increments `count` (or resets to 1 if the window has expired). The returned `count` is then compared to `config.MaxAttempts` to decide allow/deny. `count` may temporarily drift past MaxAttempts under sustained over-limit traffic — the rate limiter still denies correctly, and `cleanup()` evicts expired rows on its 24-hour cycle. This is a small accounting trade for atomicity and is acceptable for rate-limit semantics.
func (*DBRateLimiter) AllowWithEmail ¶
AllowWithEmail is a convenience method that formats the key as an email-based key.
func (*DBRateLimiter) AllowWithIP ¶
AllowWithIP is a convenience method that formats the key as an IP-based key.
func (*DBRateLimiter) AllowWithUser ¶
AllowWithUser is a convenience method that formats the key as a user-based key.
func (*DBRateLimiter) SetLimit ¶
func (rl *DBRateLimiter) SetLimit(endpoint string, config RateLimitConfig)
SetLimit allows customizing rate limits for specific endpoints.
func (*DBRateLimiter) StartCleanupWorker ¶
func (rl *DBRateLimiter) StartCleanupWorker(ctx context.Context)
StartCleanupWorker launches a background goroutine that calls cleanup() on a fixed schedule, independent of the count==1 opportunistic trigger.
This ensures that perpetually-denied keys (whose count never resets to 1) are also evicted, preventing unbounded row growth on the rate_limits table under sustained abuse (02-M2). The goroutine stops when ctx is canceled.
Call this once at server startup after creating the DBRateLimiter. The goroutine is lightweight (one blocked timer channel) and safe to call from tests with a short-lived context.
type DashboardSummaryResponse ¶
type DashboardSummaryResponse struct {
ByService map[string]ServiceSavings `json:"by_service"`
PotentialMonthlySavings float64 `json:"potential_monthly_savings"`
TotalRecommendations int `json:"total_recommendations"`
ActiveCommitments int `json:"active_commitments"`
CommittedMonthly float64 `json:"committed_monthly"`
CurrentCoverage float64 `json:"current_coverage"`
TargetCoverage float64 `json:"target_coverage"`
YTDSavings float64 `json:"ytd_savings"`
}
DashboardSummaryResponse holds the dashboard summary data.
type DeploymentInfoResponse ¶
type DeploymentInfoResponse struct {
// APIKeySecretURL is the AWS Console deep-link to the Secrets Manager
// secret holding the CUDly API key.
APIKeySecretURL string `json:"api_key_secret_url,omitempty"`
// DeploymentAWSAccountID is the AWS account ID of the Lambda host,
// resolved via STS GetCallerIdentity. Empty on non-AWS deployments
// or when STS is unreachable. Used by the frontend to distinguish
// legitimate ambient-credential executions from orphan rows (#608).
DeploymentAWSAccountID string `json:"deployment_aws_account_id,omitempty"`
}
DeploymentInfoResponse holds deployment-scoped identifiers that must not be exposed to unauthenticated callers. Served by GET /api/info/deployment (AuthUser).
type DiscoverOrgRequest ¶
type DiscoverOrgRequest struct {
AccountID string `json:"account_id"`
}
DiscoverOrgRequest is the request body for POST /api/accounts/discover-org. AccountID is the UUID of the org-root cloud account whose stored credentials will be used to call AWS Organizations.
type DiscoverOrgResult ¶
type DiscoverOrgResult struct {
Discovered int `json:"discovered"`
Created int `json:"created"`
Skipped int `json:"skipped"`
}
DiscoverOrgResult is the response shape for POST /api/accounts/discover-org. Discovered is the total number of member accounts the AWS Organizations API returned; Created is the number of new cloud_accounts rows persisted; Skipped is the number that already existed (matched by provider+external_id).
type EmptyServiceConfigResponse ¶
type EmptyServiceConfigResponse struct{}
EmptyServiceConfigResponse represents an empty service config.
type ExchangeExecuteRequestBody ¶
type ExchangeExecuteRequestBody struct {
TargetOfferingID string `json:"target_offering_id,omitempty"`
MaxPaymentDueUSD string `json:"max_payment_due_usd"`
Region string `json:"region,omitempty"`
RIIDs []string `json:"ri_ids"`
Targets []ExchangeTargetBody `json:"targets,omitempty"`
TargetCount int32 `json:"target_count,omitempty"`
}
ExchangeExecuteRequestBody is the request body for the execute endpoint. Same `targets` / legacy-alias semantics as ExchangeQuoteRequestBody. `max_payment_due_usd` is a TOTAL cap across all targets in the exchange — AWS returns a single aggregated PaymentDue so spend-cap checking naturally becomes a total when `targets[]` has multiple entries.
type ExchangeExecuteResponse ¶
type ExchangeExecuteResponse struct {
Quote *exchange.ExchangeQuoteSummary `json:"quote"`
ExchangeID string `json:"exchange_id"`
}
ExchangeExecuteResponse is the response from a successful exchange execution.
type ExchangeQuoteRequestBody ¶
type ExchangeQuoteRequestBody struct {
TargetOfferingID string `json:"target_offering_id,omitempty"`
Region string `json:"region,omitempty"`
RIIDs []string `json:"ri_ids"`
Targets []ExchangeTargetBody `json:"targets,omitempty"`
TargetCount int32 `json:"target_count,omitempty"`
}
ExchangeQuoteRequestBody is the request body for the quote endpoint. Callers may supply either the new `targets` array (preferred) or the legacy `target_offering_id` + `target_count` singleton fields. When both are present, `targets` wins. Exactly one of them must be provided (or the handler returns 400).
type ExchangeTargetBody ¶
type ExchangeTargetBody struct {
OfferingID string `json:"offering_id"`
Count int32 `json:"count"`
}
ExchangeTargetBody is one entry in an ExchangeQuote/Execute request's `targets` array. Mirrors pkg/exchange.TargetConfig but with JSON tags shaped for the HTTP surface.
type ExchangeableAzureRIsResponse ¶
type ExchangeableAzureRIsResponse struct {
Reservations []azurecompute.ExchangeableReservation `json:"reservations"`
}
ExchangeableAzureRIsResponse holds the list of Azure VM reservations that are eligible for the cross-SKU/cross-region exchange flow.
type ExecutePurchaseRequest ¶
type ExecutePurchaseRequest struct {
// Recommendations are matched against the stored recommendation set on
// (provider, cloud_account_id, service, region, resource_type, engine,
// term, payment). Only count, recommended_count and selected are taken
// from the request; id, details and every cost field are replaced from
// the stored row scaled by count (priceRecommendationsFromStore, #1905).
Recommendations []config.RecommendationRecord `json:"recommendations"`
// CapacityPercent is what fraction (1..100) of the originally-
// recommended counts the user chose in the bulk Purchase flow.
// Audit-only: the Recommendations slice already carries scaled
// counts, so backend math ignores this field for purchase work.
// 0 / absent defaults to 100 ("full capacity").
CapacityPercent int `json:"capacity_percent,omitempty"`
// ExecuteMode controls whether this request bypasses the approval
// email and executes the purchase immediately. The only accepted
// non-empty value is "direct"; any other value is treated as the
// default approval-required flow. The handler re-checks the
// execute-any/execute-own RBAC gate before honoring "direct",
// even if the session already passed the execute:purchases gate in
// validateExecutePurchaseRequest, so a client that sets this field
// without the privilege receives a 403 rather than silent fallback.
ExecuteMode string `json:"execute_mode,omitempty"`
}
ExecutePurchaseRequest represents the request to execute purchases.
type FederationIaCResponse ¶
type FederationIaCResponse struct {
Filename string `json:"filename"`
Content string `json:"content"`
ContentType string `json:"content_type"`
ContentEncoding string `json:"content_encoding,omitempty"` // "base64" for binary (zip)
}
FederationIaCResponse is returned by the /api/federation/iac endpoint.
type Group ¶
type Group struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Permissions []Permission `json:"permissions"`
AllowedAccounts []string `json:"allowed_accounts,omitempty"`
}
Group represents a user group with permissions.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler processes HTTP requests.
func (*Handler) GetHealth ¶
func (h *Handler) GetHealth(ctx context.Context) (*HealthResponse, error)
GetHealth performs comprehensive health checks.
func (*Handler) HandleOIDC ¶
func (h *Handler) HandleOIDC(ctx context.Context, req *events.LambdaFunctionURLRequest) (*events.LambdaFunctionURLResponse, bool)
HandleOIDC serves the two OIDC issuer endpoints directly, without going through the API router. Both are always public (no auth, no CSRF). Returns nil if path is not an OIDC discovery path so the caller can fall through to the main router.
The Azure federated credential path also reads the resolved issuer URL via oidc.IssuerURL(), so calling this endpoint once populates the shared cache — which is how the purchase manager (no HTTP context) learns what iss claim to put in its client_assertion JWTs.
func (*Handler) HandleRequest ¶
func (h *Handler) HandleRequest(ctx context.Context, req *events.LambdaFunctionURLRequest) (*events.LambdaFunctionURLResponse, error)
HandleRequest processes a Lambda Function URL request.
type HandlerConfig ¶
type HandlerConfig struct {
AnalyticsClient AnalyticsClientInterface
AnalyticsCollector AnalyticsCollectorInterface
PurchaseManager PurchaseManagerInterface
RateLimiter RateLimiterInterface
AuthService AuthServiceInterface
CommitmentOpts CommitmentOptsInterface
OIDCSigner oidc.Signer
AnalyticsSnapshots AnalyticsSnapshotStoreInterface
CredentialStore credentials.CredentialStore
EmailNotifier email.SenderInterface
Scheduler SchedulerInterface
ConfigStore config.StoreInterface
DashboardURL string
CORSAllowedOrigin string
DashboardBucket string
OIDCIssuerURL string
APIKeySecretARN string
EncryptionKeySource string
EnableDashboard bool
}
HandlerConfig holds configuration for the API handler.
type HealthCheck ¶
HealthCheck represents a single health check result.
type HealthResponse ¶
type HealthResponse struct {
Timestamp time.Time `json:"timestamp"`
Checks map[string]HealthCheck `json:"checks"`
Status string `json:"status"`
}
HealthResponse represents the health check response.
type HistoryDataPoint ¶
type HistoryDataPoint struct {
Timestamp time.Time `json:"timestamp"`
ByService map[string]float64 `json:"by_service,omitempty"`
ByProvider map[string]float64 `json:"by_provider,omitempty"`
TotalSavings float64 `json:"total_savings"`
TotalUpfront float64 `json:"total_upfront"`
PurchaseCount int `json:"purchase_count"`
CumulativeSavings float64 `json:"cumulative_savings"`
}
HistoryDataPoint represents aggregated historical data.
type HistoryResponse ¶
type HistoryResponse struct {
Purchases []config.PurchaseHistoryRecord `json:"purchases"`
Summary HistorySummary `json:"summary"`
}
HistoryResponse represents the response from the history API.
type HistorySummary ¶
type HistorySummary struct {
TotalPurchases int `json:"total_purchases"`
TotalCompleted int `json:"total_completed"`
TotalPending int `json:"total_pending"`
// TotalInProgress counts executions that have been approved but whose
// synchronous purchase has not finalized (status approved/running/paused).
// Tracked separately from pending and excluded from the dollar totals so an
// interrupted approval (issue #621) stays visible without inflating
// committed spend/savings.
TotalInProgress int `json:"total_in_progress"`
TotalFailed int `json:"total_failed"`
TotalExpired int `json:"total_expired"`
// TotalRevoked counts commitments that have been revoked/refunded via
// MarkPurchaseRevoked. They are excluded from TotalCompleted and all dollar
// totals because the provider has canceled the commitment.
TotalRevoked int `json:"total_revoked"`
TotalUpfront float64 `json:"total_upfront"`
TotalMonthlySavings float64 `json:"total_monthly_savings"`
TotalAnnualSavings float64 `json:"total_annual_savings"`
}
HistorySummary provides aggregate statistics for purchase history. TotalPurchases is the total count of rows (completed + all non-completed states); the per-state counters break it down so the UI can render meaningful totals. Dollar totals count completed, non-revoked rows only: pending, in-progress, failed, expired, canceled, and revoked rows are all excluded because no money is being actively committed for those states.
type HistorySummaryAnalytics ¶
type HistorySummaryAnalytics struct {
TotalPeriodSavings float64 `json:"total_period_savings"`
TotalUpfrontSpent float64 `json:"total_upfront_spent"`
PurchaseCount int `json:"purchase_count"`
AverageSavingsPerPeriod float64 `json:"average_savings_per_period"`
PeakSavings float64 `json:"peak_savings"`
}
HistorySummaryAnalytics contains aggregated statistics for analytics.
type InMemoryRateLimiter ¶
type InMemoryRateLimiter struct {
// contains filtered or unexported fields
}
InMemoryRateLimiter provides in-memory rate limiting for single-instance deployments (Fargate, ECS) This implementation should NOT be used for Lambda (multi-instance) - use DBRateLimiter instead.
func NewInMemoryRateLimiter ¶
func NewInMemoryRateLimiter() *InMemoryRateLimiter
NewInMemoryRateLimiter creates a new in-memory rate limiter for single-instance deployments.
func (*InMemoryRateLimiter) Allow ¶
Allow checks if a request should be allowed based on rate limits. The key should be formatted as "IP#{ip}" or "EMAIL#{email}". The endpoint identifies which rate limit configuration to use.
func (*InMemoryRateLimiter) AllowWithEmail ¶
func (rl *InMemoryRateLimiter) AllowWithEmail(ctx context.Context, email, endpoint string) (bool, error)
AllowWithEmail is a convenience method that formats the key as an email-based key.
func (*InMemoryRateLimiter) AllowWithIP ¶
AllowWithIP is a convenience method that formats the key as an IP-based key.
func (*InMemoryRateLimiter) AllowWithUser ¶
func (rl *InMemoryRateLimiter) AllowWithUser(ctx context.Context, userID, endpoint string) (bool, error)
AllowWithUser is a convenience method that formats the key as a user-based key.
func (*InMemoryRateLimiter) SetLimit ¶
func (rl *InMemoryRateLimiter) SetLimit(endpoint string, config RateLimitConfig)
SetLimit allows customizing rate limits for specific endpoints.
type InventoryCommitment ¶
type InventoryCommitment struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
// MonthlyCost is nil when the source purchase_history row has a NULL
// monthly_cost (provider did not return a monthly breakdown). The
// frontend renders "--" for nil and "$X.XX" when non-nil.
MonthlyCost *float64 `json:"monthly_cost"`
Provider string `json:"provider"`
AccountID string `json:"account_id"`
AccountName string `json:"account_name,omitempty"`
Service string `json:"service"`
ResourceType string `json:"resource_type,omitempty"`
Region string `json:"region"`
Status string `json:"status"`
ID string `json:"id"`
PaymentOption string `json:"payment_option,omitempty"`
TermYears int `json:"term_years"`
UpfrontCost float64 `json:"upfront_cost"`
EstimatedSavings float64 `json:"estimated_savings"`
Count int `json:"count"`
}
InventoryCommitment is one row in the per-commitment Inventory & Coverage view (issue #340 deferred sub-task — "Active commitments"). Aggregated from PurchaseHistoryRecord rows that are still within their term; the inventory endpoint filters out expired commitments before responding.
ID is `{account_id}:{purchase_id}` so the row is uniquely identifiable in the JSON payload without a DB schema change — purchase_id alone is unique within an account but not globally across the table.
Status is always `"active"` today (the handler drops expired rows). The field stays in the response shape so a future "expiring soon" sub-state has a slot without a breaking API change.
type InventoryCommitmentsResponse ¶
type InventoryCommitmentsResponse struct {
Commitments []InventoryCommitment `json:"commitments"`
}
InventoryCommitmentsResponse is the envelope returned by GET /api/inventory/commitments. Commitments is always a slice — never nil — so the frontend can rely on `resp.commitments.length` without a null check.
type LambdaInvokerInterface ¶
type LambdaInvokerInterface interface {
Invoke(ctx context.Context, params *lambda.InvokeInput, optFns ...func(*lambda.Options)) (*lambda.InvokeOutput, error)
}
LambdaInvokerInterface is the narrow subset of lambda.Client used by the async refresh handler. Extracted so tests can inject a stub without standing up a real Lambda client.
type LoginRequest ¶
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
MFACode string `json:"mfa_code,omitempty"`
}
Auth request/response types (to avoid import cycle with auth package).
type LoginResponse ¶
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
User *UserInfo `json:"user"`
CSRFToken string `json:"csrf_token,omitempty"`
}
LoginResponse carries the JWT token and user summary returned after a successful login.
type MFADisableRequest ¶
type MFADisableRequest struct {
Password string `json:"password"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
Code string `json:"code"`
}
MFADisableRequest turns off MFA. Requires the current password AND a fresh proof-of-possession (TOTP code or unused recovery code).
type MFAEnableRequest ¶
type MFAEnableRequest struct {
Code string `json:"code"`
}
MFAEnableRequest finalizes an enrollment by proving the user loaded the secret into their authenticator (the supplied code is validated against the pending secret).
type MFAEnableResponse ¶
type MFAEnableResponse struct {
RecoveryCodes []string `json:"recovery_codes"`
}
MFAEnableResponse returns the plaintext recovery codes exactly once. Backend stores only bcrypt hashes.
type MFARegenerateRequest ¶
type MFARegenerateRequest struct {
Code string `json:"code"`
}
MFARegenerateRequest replaces all stored recovery codes. Requires a fresh TOTP code (NOT a recovery code — see service for the rationale).
type MFARegenerateResponse ¶
type MFARegenerateResponse struct {
RecoveryCodes []string `json:"recovery_codes"`
}
MFARegenerateResponse mirrors MFAEnableResponse — plaintext codes returned exactly once.
type MFASetupRequest ¶
type MFASetupRequest struct {
Password string `json:"password"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
}
MFASetupRequest begins an MFA enrollment. Current password is required as defense-in-depth — a stolen session alone shouldn't be enough to swap a user's MFA secret.
type MFASetupResponse ¶
type MFASetupResponse struct {
Secret string `json:"secret"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
ProvisioningURI string `json:"provisioning_uri"`
}
MFASetupResponse returns the freshly-generated secret + the otpauth:// URI the frontend renders as a QR code. The secret is already persisted server-side as the pending secret; clients do not need to round-trip it back on enable.
type MarketplaceListRequest ¶
type MarketplaceListRequest struct {
PriceSchedule []MarketplacePriceTier `json:"price_schedule,omitempty"`
}
MarketplaceListRequest is the request body for POST .../marketplace-list. PriceSchedule is optional: when absent the handler computes a default from the row's upfront_cost, monthly_cost, term, and a 5% discount to attract buyers (documented in the response body so the caller can see what was used).
type MarketplaceListResponse ¶
type MarketplaceListResponse struct {
ListingID string `json:"listing_id"`
ListingState string `json:"listing_state"`
PriceSchedule []MarketplacePriceTier `json:"price_schedule"`
AWSFeePercent float64 `json:"aws_fee_percent"`
Note string `json:"note,omitempty"`
}
MarketplaceListResponse is the JSON response body for a successful listing.
type MarketplacePriceTier ¶
type MarketplacePriceTier struct {
// TermMonths is the remaining-months count this tier covers.
TermMonths int64 `json:"term_months"`
// Price is the USD list price per unit for this tier.
Price float64 `json:"price"`
}
MarketplacePriceTier is the JSON-decodable shape accepted in the request body.
type PasswordResetConfirm ¶
type PasswordResetConfirm struct {
Token string `json:"token"`
NewPassword string `json:"new_password"`
}
PasswordResetConfirm completes a password-reset flow by pairing the one-time token (delivered by email) with the user's chosen new password.
type PasswordResetRequest ¶
type PasswordResetRequest struct {
Email string `json:"email"`
}
PasswordResetRequest initiates a password-reset flow by sending a reset link to the provided email address if it matches an existing account.
type PatchPlanRequest ¶
type PatchPlanRequest struct {
Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
AutoPurchase *bool `json:"auto_purchase,omitempty"`
NotificationDaysBefore *int `json:"notification_days_before,omitempty"`
}
PatchPlanRequest represents a partial update request for plans.
type PaymentAdjustment ¶
type PaymentAdjustment struct {
// RecIndex is the rec's position in the request's recommendations slice.
RecIndex int `json:"rec_index"`
Provider string `json:"provider"`
Service string `json:"service"`
// RequestedPaymentOption is the caller's token after trim/lowercase only
// (case-only differences are not surfaced as adjustments, matching the
// operator WARN-log semantics).
RequestedPaymentOption string `json:"requested_payment_option"`
// AppliedPaymentOption is the provider-canonical token the purchase will
// actually carry.
AppliedPaymentOption string `json:"applied_payment_option"`
Reason string `json:"reason"`
}
PaymentAdjustment surfaces a payment-option coercion to the API caller (follow-up to #1503): when the web execute path normalizes a caller-supplied payment option onto a provider-canonical token that bills on a DIFFERENT schedule than what was requested (e.g. Azure "partial-upfront" -> "monthly"), the response carries one of these per adjusted rec so the caller sees what they requested, what was actually applied, and why, not just the operator WARN log. Purely additive visibility: the coercion policy itself lives in config.NormalizePaymentOption and is unchanged.
Cross-provider renames of the SAME schedule (Azure "all-upfront" -> "upfront", "no-upfront" -> "monthly") are not adjustments: nothing about the customer's cash flow changed, so there is nothing to disclose. See config.PaymentCoercionChangesSchedule.
type Permission ¶
type Permission struct {
Constraints *PermissionConstraint `json:"constraints,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
}
Permission represents an action that can be performed on a resource.
type PermissionConstraint ¶
type PermissionConstraint struct {
Accounts []string `json:"accounts,omitempty"`
Providers []string `json:"providers,omitempty"`
Services []string `json:"services,omitempty"`
Regions []string `json:"regions,omitempty"`
MaxAmount float64 `json:"max_amount,omitempty"`
}
PermissionConstraint limits where a permission applies.
type PermissionEntry ¶
PermissionEntry is a single {action, resource} pair in the permissions response. Constraints are omitted from the wire shape for now; the frontend uses the pair for UX gating only.
type PlanHealthFactor ¶
type PlanHealthFactor struct {
Code PlanHealthFactorCode `json:"code"`
Penalty int `json:"penalty"`
Note string `json:"note"`
}
PlanHealthFactor is one penalty applied when computing a plan's health score. Returned alongside the score so a bad score is actionable instead of opaque: the frontend tooltip enumerates every factor and its penalty.
type PlanHealthFactorCode ¶
type PlanHealthFactorCode string
PlanHealthFactorCode identifies a single scoring factor in a plan's health-score breakdown (issue #340 follow-up). Typed so the frontend tooltip renders deterministic wording per factor instead of parsing free text out of Note.
const ( HealthFactorOverdue PlanHealthFactorCode = "overdue" HealthFactorFailedExecutions PlanHealthFactorCode = "failed_executions" HealthFactorCanceledExecutions PlanHealthFactorCode = "canceled_executions" HealthFactorRampBlocked PlanHealthFactorCode = "ramp_blocked" HealthFactorStalled PlanHealthFactorCode = "stalled" HealthFactorBehindSchedule PlanHealthFactorCode = "behind_schedule" HealthFactorDisabledMidway PlanHealthFactorCode = "disabled_midway" )
Health-factor codes, in the same order computePlanHealth evaluates them.
type PlanRequest ¶
type PlanRequest struct {
Payment string `json:"payment,omitempty"`
Description string `json:"description,omitempty"`
RampSchedule string `json:"ramp_schedule,omitempty"`
Name string `json:"name"`
Provider string `json:"provider,omitempty"`
Service string `json:"service,omitempty"`
TargetAccounts []string `json:"target_accounts,omitempty"`
TargetCoverage int `json:"target_coverage,omitempty"`
Term int `json:"term,omitempty"`
NotificationDaysBefore int `json:"notification_days_before"`
CustomStepPercent int `json:"custom_step_percent,omitempty"`
CustomIntervalDays int `json:"custom_interval_days,omitempty"`
AutoPurchase bool `json:"auto_purchase"`
Enabled bool `json:"enabled"`
}
PlanRequest represents the API request format for creating/updating plans The frontend sends ramp_schedule as a string, which we convert to the proper struct.
type PlanWithHealth ¶
type PlanWithHealth struct {
config.PurchasePlan
HealthScore *int `json:"health_score"`
HealthFactors []PlanHealthFactor `json:"health_factors,omitempty"`
}
PlanWithHealth wraps a config.PurchasePlan with its computed health-score badge data for the Plans list view (issue #340 follow-up, PR #376). The score/factors are derived at read time from the plan's own attributes and its recent executions -- see computePlanHealth in plan_health.go -- and are never persisted, so they can't drift into the config.PurchasePlan struct (and by extension the DynamoDB/Postgres columns backing it).
HealthScore is a pointer so "not computable" stays distinguishable from a real score: when the execution counts the score depends on can't be read, the field serializes as null and the UI renders an explicit "unknown" badge. Any concrete default would be a fabricated number an operator could not tell apart from a measured one.
type PlannedPurchase ¶
type PlannedPurchase struct {
// CreatedByUserID is the UUID of the user who created the scheduled
// purchase, mirroring PurchaseHistoryRecord.CreatedByUserID. The
// frontend gates the row action buttons on creator-scope ownership
// (issue #950); omitted for legacy rows with a NULL creator.
CreatedByUserID *string `json:"created_by_user_id,omitempty"`
Status string `json:"status"`
PlanID string `json:"plan_id"`
PlanName string `json:"plan_name"`
ScheduledDate string `json:"scheduled_date"`
Provider string `json:"provider"`
Service string `json:"service"`
ResourceType string `json:"resource_type"`
Region string `json:"region"`
ID string `json:"id"`
Payment string `json:"payment"`
Count int `json:"count"`
UpfrontCost float64 `json:"upfront_cost"`
EstimatedSavings float64 `json:"estimated_savings"`
StepNumber int `json:"step_number"`
TotalSteps int `json:"total_steps"`
Term int `json:"term"`
}
PlannedPurchase represents a scheduled purchase from a plan.
type PlannedPurchasesResponse ¶
type PlannedPurchasesResponse struct {
Purchases []PlannedPurchase `json:"purchases"`
}
PlannedPurchasesResponse holds the list of planned purchases.
type PlansResponse ¶
type PlansResponse struct {
Plans []PlanWithHealth `json:"plans"`
}
PlansResponse holds the purchase plans response.
type PostgresAnalyticsClient ¶
type PostgresAnalyticsClient struct {
// contains filtered or unexported fields
}
PostgresAnalyticsClient implements AnalyticsClientInterface by aggregating the purchase_history table. It replaces the legacy S3/Athena-backed client — all purchase history is now written to Postgres and we want the analytics endpoints to serve the same shape without a second storage layer.
func NewPostgresAnalyticsClient ¶
func NewPostgresAnalyticsClient(db *database.Connection) *PostgresAnalyticsClient
NewPostgresAnalyticsClient creates a new Postgres-backed analytics client.
func (*PostgresAnalyticsClient) QueryBreakdown ¶
func (c *PostgresAnalyticsClient) QueryBreakdown( ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, start, end time.Time, dimension string, ) (map[string]BreakdownValue, error)
QueryBreakdown groups purchase_history by dimension (service, provider, region, account) and returns totals + percentage-of-total-savings per bucket.
func (*PostgresAnalyticsClient) QueryHistory ¶
func (c *PostgresAnalyticsClient) QueryHistory( ctx context.Context, accountUUIDs []string, accountExternalIDsByProvider map[string][]string, provider string, start, end time.Time, interval string, ) ([]HistoryDataPoint, *HistorySummary, error)
QueryHistory aggregates purchase_history rows bucketed by interval. Empty accountUUIDs AND accountExternalIDsByProvider means "all accounts accessible to the caller"; scoping is enforced upstream in the handler. A non-empty provider ("aws"/"azure"/"gcp") restricts to rows of that provider; "" means all providers (the global-filter "all" sentinel is normalised to "" in the handler). Returns data points in ascending order and a summary covering the full window.
type Principal ¶
type Principal struct {
// Field order groups the pointer-bearing Session ahead of the trailing
// string so the struct satisfies govet fieldalignment.
Kind PrincipalKind
Session *Session // non-nil when a valid bearer session was presented
UserID string // empty for PrincipalAdminAPIKey
Email string // empty for PrincipalAdminAPIKey; populated for session/user-api-key
// APIKeyID is the database ID of the user API key the request
// authenticated with. Non-empty exactly when Kind is
// PrincipalUserAPIKey; empty for every other kind. It lets
// validateSecurityContext book the key's usage once per request without
// re-validating the credential.
APIKeyID string
}
Principal carries the resolved caller identity returned by authenticatePrincipal and requireAuth. Handlers that need the caller's identity read it from here rather than re-resolving it through a second ValidateSession / ValidateUserAPIKeyAPI call.
type PrincipalKind ¶
type PrincipalKind string
PrincipalKind identifies which credential type was used to authenticate.
const ( // PrincipalAdminAPIKey is set when the request authenticated with the // shared admin API key (X-API-Key header matching h.apiKey). PrincipalAdminAPIKey PrincipalKind = "admin-api-key" // PrincipalUserAPIKey is set when the request authenticated with a // per-user API key issued via /api/api-keys. PrincipalUserAPIKey PrincipalKind = "user-api-key" // #nosec G101 -- credential-type label, not a credential value // PrincipalSession is set when the request authenticated with a // bearer-token session (X-Authorization / Authorization header). PrincipalSession PrincipalKind = "session" )
type ProfileUpdateRequest ¶
type ProfileUpdateRequest struct {
Email string `json:"email"`
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password,omitempty"`
}
ProfileUpdateRequest represents a profile update request.
type ProviderCoverageSection ¶
type ProviderCoverageSection struct {
OverallCoveragePct *float64 `json:"overall_coverage_pct"`
Provider string `json:"provider"`
Services []CoverageServiceRow `json:"services"`
}
ProviderCoverageSection is the per-provider block returned by GET /api/inventory/coverage. Services is nil (not []) when the provider has no usage data, which the frontend uses to distinguish "no usage detected" from "usage exists but all services are 0%". OverallCoveragePct follows the same null-vs-zero contract as CoverageServiceRow.CoveragePct.
type PublicInfoResponse ¶
type PublicInfoResponse struct {
Version string `json:"version"`
AdminExists bool `json:"admin_exists"`
}
PublicInfoResponse holds public information about the CUDly instance. Only fields safe for unauthenticated callers — sensitive identifiers (API key secret URL, deployment AWS account ID) live on the authenticated /api/info/deployment endpoint instead.
type PurchaseManagerInterface ¶
type PurchaseManagerInterface interface {
ApproveExecution(ctx context.Context, execID, token, actor string) error
ApproveAndExecute(ctx context.Context, execID, actor string, transitionedBy *string) error
CancelExecution(ctx context.Context, execID, token, actor string) error
}
PurchaseManagerInterface defines purchase manager methods used by handler. `actor` is the session-authenticated user's email for per-user attribution via the auth-gated deep-link flow; pass "" for token-only paths (message workers, legacy callers) where attribution falls back to the notification email at render time.
`transitionedBy` (ApproveAndExecute) is the session user's UUID stamped onto purchase_executions.transitioned_by for human-initiated approvals; pass nil for token/SQS/system flows so transitioned_by = NULL (issue #1009).
type RIExchangeConfigResponse ¶
type RIExchangeConfigResponse struct {
Mode string `json:"mode"`
UtilizationThreshold float64 `json:"utilization_threshold"`
MaxPaymentPerExchangeUSD float64 `json:"max_payment_per_exchange_usd"`
MaxPaymentDailyUSD float64 `json:"max_payment_daily_usd"`
LookbackDays int `json:"lookback_days"`
AutoExchangeEnabled bool `json:"auto_exchange_enabled"`
}
RIExchangeConfigResponse is the response for GET /api/ri-exchange/config.
type RIExchangeConfigUpdateRequest ¶
type RIExchangeConfigUpdateRequest struct {
Mode string `json:"mode"`
UtilizationThreshold float64 `json:"utilization_threshold"`
MaxPaymentPerExchangeUSD float64 `json:"max_payment_per_exchange_usd"`
MaxPaymentDailyUSD float64 `json:"max_payment_daily_usd"`
LookbackDays int `json:"lookback_days"`
AutoExchangeEnabled bool `json:"auto_exchange_enabled"`
}
RIExchangeConfigUpdateRequest is the request body for PUT /api/ri-exchange/config.
type RIExchangeHistoryResponse ¶
type RIExchangeHistoryResponse struct {
Records []config.RIExchangeRecord `json:"records"`
}
RIExchangeHistoryResponse is the response for GET /api/ri-exchange/history.
type RIUtilizationResponse ¶
type RIUtilizationResponse struct {
Utilization []recommendations.RIUtilization `json:"utilization"`
}
RIUtilizationResponse holds per-RI utilization data.
type RateLimitConfig ¶
type RateLimitConfig struct {
MaxAttempts int // Maximum number of attempts allowed
WindowSecs int // Time window in seconds
Window time.Duration // Computed time window (for convenience)
}
RateLimitConfig defines the rate limiting parameters for a specific endpoint/operation.
func NewRateLimitConfig ¶
func NewRateLimitConfig(maxAttempts, windowSecs int) RateLimitConfig
NewRateLimitConfig creates a new RateLimitConfig.
type RateLimiterInterface ¶
type RateLimiterInterface interface {
// Allow checks if a request should be allowed based on rate limits
// Returns (allowed bool, error)
Allow(ctx context.Context, key string, endpoint string) (bool, error)
// AllowWithIP is a convenience method for IP-based rate limiting
AllowWithIP(ctx context.Context, ip string, endpoint string) (bool, error)
// AllowWithEmail is a convenience method for email-based rate limiting
AllowWithEmail(ctx context.Context, email string, endpoint string) (bool, error)
// AllowWithUser is a convenience method for user-based rate limiting
AllowWithUser(ctx context.Context, userID string, endpoint string) (bool, error)
}
RateLimiterInterface defines the interface for rate limiting implementations This allows for both in-memory and database-backed rate limiters.
type RecommendationDetailResponse ¶
type RecommendationDetailResponse struct {
ID string `json:"id"`
UsageHistory []UsagePoint `json:"usage_history"`
ConfidenceBucket string `json:"confidence_bucket"`
ProvenanceNote string `json:"provenance_note"`
// HiddenBy is non-nil when the rec is filtered out by an account-service
// override (issue #214). Each element names one failing dimension:
// "enabled=false", "engine", "region", or "resource_type". The frontend
// renders a "hidden by your override" banner when this field is present.
// Absent (null) means the rec is fully visible.
HiddenBy []string `json:"hidden_by,omitempty"`
}
RecommendationDetailResponse is the per-id payload backing the Recommendations row-click drawer. Contract documented in issue #44.
ConfidenceBucket is "low" | "medium" | "high" — server-side mirror of the client-side heuristic that previously lived in frontend/src/recommendations.ts:confidenceBucketFor. Centralizing it server-side lets future provider-specific tuning happen in one place.
ProvenanceNote is a short human-readable string naming the collector + the freshness window. Rendered verbatim in the drawer.
type RecommendationsResponse ¶
type RecommendationsResponse struct {
Recommendations []config.RecommendationRecord `json:"recommendations"`
Regions []string `json:"regions"`
Summary RecommendationsSummary `json:"summary"`
}
RecommendationsResponse holds the recommendations response.
type RecommendationsSummary ¶
type RecommendationsSummary struct {
TotalCount int `json:"total_count"`
TotalMonthlySavings float64 `json:"total_monthly_savings"`
TotalUpfrontCost float64 `json:"total_upfront_cost"`
AvgPaybackMonths float64 `json:"avg_payback_months"`
}
RecommendationsSummary holds aggregate statistics for recommendations.
type RefreshResponse ¶
type RefreshResponse struct {
StartedAt time.Time `json:"started_at"`
LastCollectedAt *time.Time `json:"last_collected_at"`
}
RefreshResponse is the 202 body returned by POST /api/recommendations/refresh. started_at is the timestamp recorded by MarkCollectionStarted. last_collected_at is the previous successful collection timestamp (may be nil on first-ever collection). The frontend polls GET /api/recommendations/freshness every 5 s and clears the refreshing banner once last_collected_at advances past started_at.
type RegistrationRequest ¶
type RegistrationRequest struct {
Provider string `json:"provider"`
ExternalID string `json:"external_id"`
AccountName string `json:"account_name"`
ContactEmail string `json:"contact_email"`
Description string `json:"description"`
SourceProvider string `json:"source_provider"`
AWSRoleARN string `json:"aws_role_arn"`
AWSAuthMode string `json:"aws_auth_mode"`
AWSExternalID string `json:"aws_external_id"`
AzureSubscriptionID string `json:"azure_subscription_id"`
AzureTenantID string `json:"azure_tenant_id"`
AzureClientID string `json:"azure_client_id"`
AzureAuthMode string `json:"azure_auth_mode"`
GCPProjectID string `json:"gcp_project_id"`
GCPClientEmail string `json:"gcp_client_email"`
GCPAuthMode string `json:"gcp_auth_mode"`
GCPWIFAudience string `json:"gcp_wif_audience"`
CredentialType string `json:"credential_type"`
CredentialPayload string `json:"credential_payload"`
}
RegistrationRequest is the JSON body for POST /api/register.
type RegistrationStatusResponse ¶
type RegistrationStatusResponse struct {
Status string `json:"status"`
CreatedAt string `json:"created_at"`
RejectionReason string `json:"rejection_reason,omitempty"`
}
RegistrationStatusResponse is the limited public response for GET /api/register/:token.
type RejectRequest ¶
type RejectRequest struct {
Reason string `json:"reason"`
}
RejectRequest is the JSON body for POST /api/registrations/:id/reject.
type ReshapeRecommendationsResponse ¶
type ReshapeRecommendationsResponse struct {
RecsCollectedAt *time.Time `json:"recs_collected_at,omitempty"`
RecsStaleness string `json:"recs_staleness,omitempty"`
Recommendations []exchange.ReshapeRecommendation `json:"recommendations"`
}
ReshapeRecommendationsResponse holds reshape recommendations.
RecsStaleness is empty when the underlying Cost Explorer cache is fresh, "soft" when it is older than reshapeSoftStaleThreshold (12 h), and "hard" when it is older than reshapeHardStaleThreshold (24 h). RecsCollectedAt carries the raw timestamp so the frontend can build its own relative-time label ("last collected 23h ago").
type Route ¶
type Route struct {
Handler RouteHandler
ExactPath string
PathPrefix string
PathSuffix string
Method string
Auth AuthLevel
}
Route defines a routing rule.
type RouteHandler ¶
type RouteHandler func(ctx context.Context, req *events.LambdaFunctionURLRequest, params map[string]string) (any, error)
RouteHandler is a function that handles a matched route.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router manages request routing.
func NewRouter ¶
NewRouter creates a new router with all routes configured. Panics on startup if any route was registered with an unset Auth field — see the AuthLevel doc for the rationale (forces every route author to declare a level explicitly so a missed field can't silently inherit an over- or under-permissive default).
func (*Router) Route ¶
func (r *Router) Route(ctx context.Context, method, path string, req *events.LambdaFunctionURLRequest) (any, error)
Route finds and executes the matching route handler.
Authentication enforcement is defense-in-depth: validateSecurity → authenticate already runs in the middleware pipeline before dispatch, but Router.Route also enforces the per-route Auth level so routes stay protected even if middleware ordering changes or a new code path bypasses validateSecurity. AuthAdmin routes require admin access; AuthUser routes require any authenticated user; AuthPublic routes are unauthenticated. There is no implicit default — every route declares its level at registration time and NewRouter rejects authUnset.
type SchedulerInterface ¶
type SchedulerInterface interface {
CollectRecommendations(ctx context.Context, ownerToken string) (*scheduler.CollectResult, error)
ListRecommendations(ctx context.Context, filter config.RecommendationFilter) ([]config.RecommendationRecord, error)
// GetRecommendationByID fetches a single rec by its application-level id,
// bypassing account-override filtering so deep-linked URLs to override-
// hidden recs resolve. hiddenBy is non-nil when the rec would be dropped by
// the override filter; callers render a "hidden" banner. Returns nil, nil,
// nil when the rec is absent or fully suppressed.
GetRecommendationByID(ctx context.Context, id string) (rec *config.RecommendationRecord, hiddenBy []string, err error)
}
SchedulerInterface defines scheduler methods used by handler.
type ServiceSavings ¶
type ServiceSavings struct {
PotentialSavings float64 `json:"potential_savings"`
CurrentSavings float64 `json:"current_savings"`
}
ServiceSavings holds savings data for a service.
type Session ¶
type Session struct {
UserID string `json:"user_id"`
Email string `json:"email"`
// UserAPIKeyID is the database key ID when the request was authenticated via
// a user API key (not the admin infra key). Empty for bearer-token and
// admin-API-key sessions. Used by requirePermissionConstraints to evaluate
// constraints against the key's effective permissions rather than the owning
// user's full group permissions (adversarial-review F2).
UserAPIKeyID string `json:"-"`
}
Session holds the minimal identity claims extracted from a validated JWT and attached to the request context by the authentication middleware.
type SetupAdminRequest ¶
type SetupAdminRequest struct {
Email string `json:"email"`
Password string `json:"password"` //nolint:gosec // G117: intentional credential field in request/response struct -- value is supplied by the authenticated caller or returned once at creation; not re-stored downstream
}
SetupAdminRequest is the one-time bootstrap payload used to create the first administrator account when the system has no users yet.
type StatusResponse ¶
type StatusResponse struct {
Status string `json:"status"`
}
StatusResponse holds a simple status response.
type TargetOfferingsResponse ¶
type TargetOfferingsResponse struct {
Offerings []ec2svc.TargetOffering `json:"offerings"`
}
TargetOfferingsResponse is the response for GET /api/ri-exchange/target-offerings.
type TrendsResponse ¶
type TrendsResponse struct {
Start string `json:"start"`
End string `json:"end"`
Monthly []analytics.MonthlySummary `json:"monthly"`
Provider []analytics.ProviderBreakdown `json:"by_provider"`
Service []analytics.ServiceBreakdown `json:"by_service"`
Months int `json:"months"`
}
TrendsResponse is the savings-snapshot time-series for the Trends view: a monthly series (coverage %, committed spend, usage, realized savings) plus by-provider and by-service breakdowns over the requested window. Backed by the savings_snapshots store / materialized views (issues #1023 / #1033), distinct from the purchase_history-backed /history/analytics path.
type UpcomingPurchase ¶
type UpcomingPurchase struct {
// CreatedByUserID propagates the underlying execution's
// created_by_user_id so the dashboard widget can apply the same
// creator-scope ownership gate the Plans page uses (issue #950).
// Without it the widget renders a "Cancel" button on every row
// while the backend now 403s for non-owners -- a UX hole that
// surfaces as a confusing toast on click. Mirrors the field on
// PlannedPurchase / PurchaseHistoryEntry. omitempty so legacy
// NULL-creator rows keep the JSON shape they had pre-fix.
CreatedByUserID *string `json:"created_by_user_id,omitempty"`
ExecutionID string `json:"execution_id"`
PlanID string `json:"plan_id"`
PlanName string `json:"plan_name"`
ScheduledDate string `json:"scheduled_date"`
Provider string `json:"provider"`
Service string `json:"service"`
StepNumber int `json:"step_number"`
TotalSteps int `json:"total_steps"`
EstimatedSavings float64 `json:"estimated_savings"`
}
UpcomingPurchase represents one upcoming planned purchase — a pending purchase_executions row whose scheduled_date hasn't fired yet, joined to its parent PurchasePlan for display.
The dashboard's Cancel button targets ExecutionID via DELETE /api/purchases/planned/{id} (api.deletePlannedPurchase) so the operator removes just THIS scheduled instance and leaves the plan template intact — the next scheduler tick re-creates the next instance for the plan. PlanID is exposed as context (e.g. for linking to the plan's settings) and is NOT what destructive action endpoints should target. PR #207 + #213 history: an earlier iteration routed Cancel to api.deletePlan(planID) which deleted the entire plan; that was too aggressive — operators usually want "skip this scheduled run", not "nuke the recurring template".
type UpcomingPurchaseResponse ¶
type UpcomingPurchaseResponse struct {
Purchases []UpcomingPurchase `json:"purchases"`
}
UpcomingPurchaseResponse holds upcoming purchase data.
type UpdateGroupRequest ¶
type UpdateGroupRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Permissions []Permission `json:"permissions,omitempty"`
AllowedAccounts []string `json:"allowed_accounts,omitempty"`
}
UpdateGroupRequest represents a request to update a group.
type UpdateUserRequest ¶
type UpdateUserRequest struct {
Email string `json:"email,omitempty"`
Groups []string `json:"groups,omitempty"`
}
UpdateUserRequest represents a request to update a user.
type UsagePoint ¶
type UsagePoint struct {
Timestamp string `json:"timestamp"`
CPUPct float64 `json:"cpu_pct"`
MemPct float64 `json:"mem_pct"`
}
UsagePoint is a single sample in the per-recommendation usage time series surfaced by GET /api/recommendations/:id/detail. The series is always ordered by Timestamp ascending. CPUPct/MemPct are 0..100.
Empty in the current implementation: the collector pipeline does not yet persist time-series utilization per recommendation. The endpoint returns the empty slice with a non-error status so the frontend can render a "Usage history not yet available" placeholder rather than a broken empty chart. See known_issues/28_recommendations_detail_endpoint.md for the full collector wiring follow-up.
type User ¶
type User struct {
ID string `json:"id"`
Email string `json:"email"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Groups []string `json:"groups,omitempty"`
MFAEnabled bool `json:"mfa_enabled"`
}
User is the full user record returned by the admin user-management endpoints. It mirrors UserInfo's identity fields and adds CreatedAt/UpdatedAt audit timestamps; it is never embedded in the public-facing login or session responses, which use the slimmer UserInfo / CurrentUserResponse types instead.
type UserInfo ¶
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Groups []string `json:"groups,omitempty"`
MFAEnabled bool `json:"mfa_enabled"`
}
UserInfo is a lightweight projection of the authenticated user included in login and session responses; it carries only the fields needed for client-side routing and display, without exposing credential hashes or internal audit fields.
type UserPermissionsResponse ¶
type UserPermissionsResponse struct {
Permissions []PermissionEntry `json:"permissions"`
IsAdmin bool `json:"is_admin"`
}
UserPermissionsResponse is the response shape for GET /api/auth/me/permissions. Permissions is the effective set derived from the union of the user's groups. IsAdmin mirrors whether the effective set contains the {admin, *} wildcard.
type VersionResponse ¶
type VersionResponse struct {
Version string `json:"version"`
GitSHA string `json:"git_sha"`
BuildTime string `json:"build_time"`
}
VersionResponse holds build-identity metadata for the public /version endpoint. It carries no sensitive data (no account IDs, ARNs, or secrets) so it is safe to expose unauthenticated. The fields let an operator curl a running environment and compare git_sha against the branch HEAD to diagnose deploy-lag (an environment still serving a stale build).
Source Files
¶
- analytics_postgres.go
- db_rate_limiter.go
- exchange_lookup.go
- handler.go
- handler_accounts.go
- handler_analytics.go
- handler_apikeys.go
- handler_apikeys_usage.go
- handler_auth.go
- handler_commitment_options.go
- handler_config.go
- handler_dashboard.go
- handler_docs.go
- handler_federation.go
- handler_groups.go
- handler_history.go
- handler_inventory.go
- handler_ladder.go
- handler_marketplace.go
- handler_notifications.go
- handler_oidc.go
- handler_plans.go
- handler_purchases.go
- handler_purchases_revoke.go
- handler_recommendations.go
- handler_recommendations_refresh.go
- handler_registrations.go
- handler_ri_exchange.go
- handler_router.go
- handler_users.go
- handler_version.go
- health.go
- inmemory_rate_limiter.go
- middleware.go
- plan_health.go
- purchase_pricing.go
- rate_limiter.go
- ri_exchange_idempotency.go
- ri_utilization_cache.go
- router.go
- scoping.go
- types.go
- types_apikeys.go
- validation.go