api

package
v0.0.0-...-79642c4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: OSL-3.0 Imports: 67 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
const (
	// MaxRequestBodySize is the maximum allowed request body size (1MB).
	MaxRequestBodySize = 1 * 1024 * 1024
)

Security constants.

View Source
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

func IsClientError(err error) (*clientError, bool)

IsClientError checks if the error is a client error and returns it.

func IsNotFoundError

func IsNotFoundError(err error) bool

IsNotFoundError checks if the error is a not found error.

func IsOIDCIssuerPath

func IsOIDCIssuerPath(path string) bool

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

func NewClientError(code int, message string) error

NewClientError creates a new client-facing error with the given HTTP status code and message.

func NewClientErrorWithDetails

func NewClientErrorWithDetails(code int, message string, details map[string]any) error

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

type AccountTestResult struct {
	OK      bool   `json:"ok"`
	Message string `json:"message"`
}

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

type AnalyticsCollectorInterface interface {
	Collect(ctx context.Context) error
}

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

func (rl *DBRateLimiter) 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.

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

func (rl *DBRateLimiter) 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 (*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 NewHandler

func NewHandler(cfg HandlerConfig) *Handler

NewHandler creates a new API handler

func (*Handler) GetHealth

func (h *Handler) GetHealth(ctx context.Context) (*HealthResponse, error)

GetHealth performs comprehensive health checks.

func (*Handler) HandleOIDC

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

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

type HealthCheck struct {
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
}

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 LoginResponse struct {
	Token     string    `json:"token"`
	ExpiresAt string    `json:"expires_at"`
	User      *UserInfo `json:"user"`
	CSRFToken string    `json:"csrf_token,omitempty"`
}

type MFADisableRequest

type MFADisableRequest struct {
	Password string `json:"password"`
	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"`
}

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 PasswordResetConfirm struct {
	Token       string `json:"token"`
	NewPassword string `json:"new_password"`
}

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

type PermissionEntry struct {
	Action   string `json:"action"`
	Resource string `json:"resource"`
}

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

func NewRouter(h *Handler) *Router

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 Session

type Session struct {
	UserID string `json:"user_id"`
	Email  string `json:"email"`
}

type SetupAdminRequest

type SetupAdminRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

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 User

type User struct {
	ID         string   `json:"id"`
	Email      string   `json:"email"`
	Groups     []string `json:"groups,omitempty"`
	MFAEnabled bool     `json:"mfa_enabled"`
	CreatedAt  string   `json:"created_at,omitempty"`
	UpdatedAt  string   `json:"updated_at,omitempty"`
}

type UserInfo

type UserInfo struct {
	ID         string   `json:"id"`
	Email      string   `json:"email"`
	Groups     []string `json:"groups,omitempty"`
	MFAEnabled bool     `json:"mfa_enabled"`
}

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL