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.
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 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 string, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithEmail(ctx context.Context, email string, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithIP(ctx context.Context, ip string, endpoint string) (bool, error)
- func (rl *DBRateLimiter) AllowWithUser(ctx context.Context, userID string, 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 string, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithEmail(ctx context.Context, email string, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithIP(ctx context.Context, ip string, endpoint string) (bool, error)
- func (rl *InMemoryRateLimiter) AllowWithUser(ctx context.Context, userID string, 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 PasswordResetConfirm
- type PasswordResetRequest
- type PatchPlanRequest
- type Permission
- type PermissionConstraint
- type PermissionEntry
- type PlanRequest
- type PlannedPurchase
- type PlannedPurchasesResponse
- type PlansResponse
- type PostgresAnalyticsClient
- 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"` // First 8 chars for display
Permissions []Permission `json:"permissions,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at,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(ctx context.Context, req any) (any, error)
UpdateGroupAPI(ctx context.Context, 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)
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)
}
AuthServiceInterface defines auth service methods used by handler Note: This interface uses API-specific types that are converted from auth package types.
type BreakdownResponse ¶
type BreakdownResponse struct {
Dimension string `json:"dimension"`
Start string `json:"start"`
End string `json:"end"`
Data map[string]BreakdownValue `json:"data"`
}
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 {
Name string `json:"name"`
Description string `json:"description"`
ContactEmail string `json:"contact_email"`
Provider string `json:"provider"`
ExternalID string `json:"external_id"`
Enabled *bool `json:"enabled"`
// AWS
AWSAuthMode string `json:"aws_auth_mode"`
AWSRoleARN string `json:"aws_role_arn"`
AWSExternalID string `json:"aws_external_id"`
AWSBastionID string `json:"aws_bastion_id"`
AWSWebIdentityTokenFile string `json:"aws_web_identity_token_file"`
AWSIsOrgRoot bool `json:"aws_is_org_root"`
// Azure
AzureSubscriptionID string `json:"azure_subscription_id"`
AzureTenantID string `json:"azure_tenant_id"`
AzureClientID string `json:"azure_client_id"`
AzureAuthMode string `json:"azure_auth_mode"`
// GCP
GCPProjectID string `json:"gcp_project_id"`
GCPClientEmail string `json:"gcp_client_email"`
GCPAuthMode string `json:"gcp_auth_mode"`
GCPWIFAudience string `json:"gcp_wif_audience"` // Full WIF provider resource, secret-free path only.
}
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"`
Services []config.ServiceConfig `json:"services"`
SourceCloud string `json:"source_cloud,omitempty"`
SourceIdentity *sourceIdentity `json:"source_identity,omitempty"`
}
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 {
Service string `json:"service"`
CoveredMonthly float64 `json:"covered_monthly"`
OnDemandMonthly float64 `json:"on_demand_monthly"`
CoveragePct *float64 `json:"coverage_pct"`
}
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 {
Name string `json:"name"`
Permissions []Permission `json:"permissions,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
CreateAPIKeyRequest represents a request to create a new API key.
type CreateAPIKeyResponse ¶
type CreateAPIKeyResponse struct {
APIKey string `json:"api_key"` // Full key - only returned on creation
KeyID string `json:"key_id"`
Info *APIKeyInfo `json:"info"`
}
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 {
Count int `json:"count"`
StartDate string `json:"start_date"`
}
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"`
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 {
CredentialType string `json:"credential_type"`
Payload map[string]interface{} `json:"payload"`
}
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 ¶
func (rl *DBRateLimiter) AllowWithEmail(ctx context.Context, email string, endpoint string) (bool, error)
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 ¶
func (rl *DBRateLimiter) AllowWithUser(ctx context.Context, userID string, endpoint string) (bool, error)
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 {
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"`
ByService map[string]ServiceSavings `json:"by_service"`
}
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 {
RIIDs []string `json:"ri_ids"`
Targets []ExchangeTargetBody `json:"targets,omitempty"`
TargetOfferingID string `json:"target_offering_id,omitempty"`
TargetCount int32 `json:"target_count,omitempty"`
MaxPaymentDueUSD string `json:"max_payment_due_usd"`
Region string `json:"region,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 {
ExchangeID string `json:"exchange_id"`
Quote *exchange.ExchangeQuoteSummary `json:"quote"`
}
ExchangeExecuteResponse is the response from a successful exchange execution.
type ExchangeQuoteRequestBody ¶
type ExchangeQuoteRequestBody struct {
RIIDs []string `json:"ri_ids"`
Targets []ExchangeTargetBody `json:"targets,omitempty"`
TargetOfferingID string `json:"target_offering_id,omitempty"`
TargetCount int32 `json:"target_count,omitempty"`
Region string `json:"region,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 []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 honouring "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"`
Permissions []Permission `json:"permissions"`
AllowedAccounts []string `json:"allowed_accounts,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,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 {
ConfigStore config.StoreInterface
CredentialStore credentials.CredentialStore
PurchaseManager PurchaseManagerInterface
Scheduler SchedulerInterface
AuthService AuthServiceInterface
APIKeySecretARN string
EnableDashboard bool
DashboardBucket string
CORSAllowedOrigin string // CORS allowed origin (default "*")
RateLimiter RateLimiterInterface
EmailNotifier email.SenderInterface // Optional: used to send purchase approval emails
DashboardURL string // Base URL for approval/cancel links in emails
// Analytics configuration (optional)
AnalyticsClient AnalyticsClientInterface
AnalyticsCollector AnalyticsCollectorInterface
// AnalyticsSnapshots serves the savings-snapshot time-series (coverage %,
// utilization, committed spend, realized savings over time) backed by the
// savings_snapshots store. Optional; nil disables /api/analytics/trends.
AnalyticsSnapshots AnalyticsSnapshotStoreInterface
// OIDCSigner is the cloud-agnostic signer that backs
// /.well-known/openid-configuration and /.well-known/jwks.json.
// Nil disables the OIDC issuer endpoints (they return 404).
OIDCSigner oidc.Signer
// OIDCIssuerURL is the canonical issuer URL the OIDC handlers
// publish in the Discovery document. Must match what Azure AD
// federated credentials are registered with.
OIDCIssuerURL string
// CommitmentOpts discovers which (term, payment) combinations each
// AWS service actually sells and validates saves against that data.
// Nil disables both the /api/commitment-options endpoint (returns
// unavailable) and save-side validation in updateServiceConfig.
CommitmentOpts CommitmentOptsInterface
// EncryptionKeySource is the env var name that resolved the credential
// encryption key (e.g. "CREDENTIAL_ENCRYPTION_KEY_SECRET_NAME"). Empty
// when no credStore is configured. Used by the /health endpoint to
// surface which key source is in use and detect dev-key state.
EncryptionKeySource string
}
HandlerConfig holds configuration for the API handler.
type HealthCheck ¶
HealthCheck represents a single health check result.
type HealthResponse ¶
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Checks map[string]HealthCheck `json:"checks"`
}
HealthResponse represents the health check response.
type HistoryDataPoint ¶
type HistoryDataPoint struct {
Timestamp time.Time `json:"timestamp"`
TotalSavings float64 `json:"total_savings"`
TotalUpfront float64 `json:"total_upfront"`
PurchaseCount int `json:"purchase_count"`
CumulativeSavings float64 `json:"cumulative_savings"`
ByService map[string]float64 `json:"by_service,omitempty"`
ByProvider map[string]float64 `json:"by_provider,omitempty"`
}
HistoryDataPoint represents aggregated historical data.
type HistoryResponse ¶
type HistoryResponse struct {
Summary HistorySummary `json:"summary"`
Purchases []config.PurchaseHistoryRecord `json:"purchases"`
}
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"`
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 rows only: pending, in-progress, failed, expired, and canceled rows are all excluded because no money was committed for any of 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 ¶
func (rl *InMemoryRateLimiter) Allow(ctx context.Context, key string, endpoint string) (bool, error)
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 string, endpoint string) (bool, error)
AllowWithEmail is a convenience method that formats the key as an email-based key.
func (*InMemoryRateLimiter) AllowWithIP ¶
func (rl *InMemoryRateLimiter) AllowWithIP(ctx context.Context, ip string, endpoint string) (bool, error)
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 string, 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 {
ID string `json:"id"`
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"`
Count int `json:"count"`
TermYears int `json:"term_years"`
PaymentOption string `json:"payment_option,omitempty"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
UpfrontCost float64 `json:"upfront_cost"`
// 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"`
EstimatedSavings float64 `json:"estimated_savings"`
Status string `json:"status"`
}
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"`
MFACode string `json:"mfa_code,omitempty"`
}
Auth request/response types (to avoid import cycle with auth package).
type LoginResponse ¶
type MFADisableRequest ¶
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"`
}
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"`
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 PasswordResetConfirm ¶
type PasswordResetRequest ¶
type PasswordResetRequest struct {
Email string `json:"email"`
}
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 Permission ¶
type Permission struct {
Action string `json:"action"`
Resource string `json:"resource"`
Constraints *PermissionConstraint `json:"constraints,omitempty"`
}
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 PlanRequest ¶
type PlanRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
AutoPurchase bool `json:"auto_purchase"`
NotificationDaysBefore int `json:"notification_days_before"`
// Frontend sends these as top-level fields
Provider string `json:"provider,omitempty"`
Service string `json:"service,omitempty"`
Term int `json:"term,omitempty"`
Payment string `json:"payment,omitempty"`
TargetCoverage int `json:"target_coverage,omitempty"`
// Ramp schedule as string from frontend (immediate, weekly-25pct, monthly-10pct, custom)
RampSchedule string `json:"ramp_schedule,omitempty"`
CustomStepPercent int `json:"custom_step_percent,omitempty"`
CustomIntervalDays int `json:"custom_interval_days,omitempty"`
// TargetAccounts is the list of cloud_account UUIDs the plan will purchase
// for. Required (non-empty) on POST /plans -- a plan with no rows in
// plan_accounts is a "universal plan", which the design no longer allows:
// every plan must be tied to at least one explicit account. The handler
// inserts the plan_accounts rows immediately after CreatePurchasePlan so
// the two writes are observed together by downstream consumers.
TargetAccounts []string `json:"target_accounts,omitempty"`
}
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 PlannedPurchase ¶
type PlannedPurchase struct {
ID string `json:"id"`
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"`
Count int `json:"count"`
Term int `json:"term"`
Payment string `json:"payment"`
EstimatedSavings float64 `json:"estimated_savings"`
UpfrontCost float64 `json:"upfront_cost"`
Status string `json:"status"`
StepNumber int `json:"step_number"`
TotalSteps int `json:"total_steps"`
// 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"`
}
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 []config.PurchasePlan `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 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 {
Provider string `json:"provider"`
Services []CoverageServiceRow `json:"services"`
OverallCoveragePct *float64 `json:"overall_coverage_pct"`
}
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 {
AutoExchangeEnabled bool `json:"auto_exchange_enabled"`
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"`
}
RIExchangeConfigResponse is the response for GET /api/ri-exchange/config.
type RIExchangeConfigUpdateRequest ¶
type RIExchangeConfigUpdateRequest struct {
AutoExchangeEnabled bool `json:"auto_exchange_enabled"`
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"`
}
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 int, 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"`
Summary RecommendationsSummary `json:"summary"`
Regions []string `json:"regions"`
}
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 {
Recommendations []exchange.ReshapeRecommendation `json:"recommendations"`
RecsStaleness string `json:"recs_staleness,omitempty"`
RecsCollectedAt *time.Time `json:"recs_collected_at,omitempty"`
}
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 {
// Pattern matching fields
ExactPath string // Exact path match (e.g., "/api/health")
PathPrefix string // Path must start with this (e.g., "/api/users/")
PathSuffix string // Path must end with this (e.g., "/revoke")
Method string // HTTP method (e.g., "GET", "POST")
// Handler function
Handler RouteHandler
// Auth controls authentication level. REQUIRED — leaving this unset
// (zero value) causes NewRouter to panic at startup so every route
// author makes an explicit AuthAdmin / AuthUser / AuthPublic choice.
// See AuthLevel doc for the history behind the mandatory-field rule.
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) (*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 SetupAdminRequest ¶
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"`
Months int `json:"months"`
Monthly []analytics.MonthlySummary `json:"monthly"`
Provider []analytics.ProviderBreakdown `json:"by_provider"`
Service []analytics.ServiceBreakdown `json:"by_service"`
}
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 {
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"`
// 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"`
}
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 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_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_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
- rate_limiter.go
- ri_utilization_cache.go
- router.go
- scoping.go
- types.go
- types_apikeys.go
- validation.go