Documentation
¶
Overview ¶
Package cloud is the self-serve cloud/SaaS PREPARATION skeleton (v0.7 "Kimlik" E3 — docs/CLOUD.md).
Everything here is master-gated by FILEX_CLOUD (config.CloudConfig.Enabled, default OFF). While the flag is off the package is never constructed: api.BuildRouter skips the /api/cloud route block entirely, capabilities carry no cloud field, and the migration-00021 provider columns (plan / limits_json / billing_ref) stay NULL. Turning the flag on only makes sense together with FILEX_MULTI_TENANT=1 — a signed-up "tenant" IS a provider row from the v0.1.61 native multi-tenancy foundation, provisioned through the exact same db.Store.CreateProvider primitive the /api/admin/providers lifecycle API uses (no parallel provisioning path exists).
Index ¶
- Variables
- func ReturnURLs(origin string) (success, cancel string)
- func VerifyWebhookSignature(payload []byte, sigHeader, signingSecret string, now time.Time) error
- type CheckoutSession
- type Plan
- type PlanLimits
- type Service
- func (s *Service) ApplyStripeEvent(ctx context.Context, payload []byte) (bool, error)
- func (s *Service) Checkout(ctx context.Context, sc *StripeClient, slug, planID string, ...) (string, error)
- func (s *Service) PlanByID(id string) *Plan
- func (s *Service) Plans() []Plan
- func (s *Service) PlansErr() string
- func (s *Service) Signup(ctx context.Context, req SignupRequest) (*SignupResult, error)
- func (s *Service) SignupPlan() *Plan
- func (s *Service) Verify(ctx context.Context, token string) (*model.Provider, error)
- type SignupRequest
- type SignupResult
- type StripeClient
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalid — malformed signup input (→ 400). ErrInvalid = errors.New("cloud: invalid input") // ErrConflict — tenant slug already taken (→ 409). ErrConflict = errors.New("cloud: conflict") // ErrTokenUnknown — verification token unknown or expired (→ 404). ErrTokenUnknown = errors.New("cloud: unknown or expired verification token") )
Sentinel error kinds the handler maps to HTTP statuses.
var ( // ErrWebhookNotConfigured — no webhook signing secret // (STRIPE_WEBHOOK_SECRET); the webhook answers 503. ErrWebhookNotConfigured = errors.New("cloud: stripe webhook signing secret not configured") // ErrWebhookSignature — the Stripe-Signature header does not verify, or // its timestamp is outside the tolerance. ErrWebhookSignature = errors.New("cloud: stripe webhook signature does not verify") )
Webhook signature errors.
var ErrStripeNotConfigured = errors.New("cloud: stripe not configured")
ErrStripeNotConfigured — no STRIPE_SECRET; the billing endpoints answer 503.
Functions ¶
func ReturnURLs ¶ added in v0.54.0
ReturnURLs are the two addresses Stripe sends the payer back to, built by the server from the tenant's origin. A caller never names them: an address taken from the request would let anyone send a payer, through Stripe's own page, wherever they liked.
func VerifyWebhookSignature ¶
VerifyWebhookSignature checks a Stripe-Signature header (`t=<unix>,v1=<hex>[,v1=…]`): HMAC-SHA256 over "<t>.<payload>" with the endpoint's signing secret (whsec_…), compared in constant time, with the timestamp within webhookTolerance of now. A plan changes only after this says yes.
Types ¶
type CheckoutSession ¶ added in v0.54.0
type CheckoutSession struct {
PriceID string
SuccessURL string
CancelURL string
// ClientReferenceID is the tenant's provider id.
ClientReferenceID string
// Plan is the catalogue plan id, carried as metadata[plan].
Plan string
}
CheckoutSession is what a checkout session is opened with. Every field is the server's: the price is the catalogue plan's, the return addresses are built from the tenant's own origin (ReturnURLs), and the tenant and plan travel in the session so the webhook knows what was paid for.
type Plan ¶
type Plan struct {
// ID is the stable plan key stored in providers.plan (e.g. "free", "pro").
ID string `json:"id"`
// Name is the display name.
Name string `json:"name"`
// PriceMonthly is a display string (e.g. "9.90 EUR"); "" = free.
PriceMonthly string `json:"price_monthly,omitempty"`
// StripePriceID links the plan to a Stripe Price for checkout sessions.
// Empty on free plans / while billing is not wired.
StripePriceID string `json:"stripe_price_id,omitempty"`
// Limits are the plan's resource ceilings.
Limits PlanLimits `json:"limits"`
}
Plan is one entry of the config-driven catalog (FILEX_CLOUD_PLANS).
func DefaultPlans ¶
func DefaultPlans() []Plan
DefaultPlans is the built-in catalog used when FILEX_CLOUD_PLANS is empty: a single free skeleton plan so the signup flow is exercisable without any configuration.
func ParsePlans ¶
ParsePlans decodes the FILEX_CLOUD_PLANS JSON array. An empty input returns DefaultPlans(). Errors (bad JSON, missing/duplicate ids, no plan a signup can start on) are returned so the caller can surface them; callers that must keep booting fall back to DefaultPlans() and report the error via /api/cloud/status.
⚠ A catalogue needs at least one plan with no stripe_price_id: that is the plan every signup starts on (SignupPlan). A paid plan is reached only by a payment Stripe confirms (ApplyStripeEvent), never by asking for it.
type PlanLimits ¶
type PlanLimits struct {
// StorageBytes caps the tenant's total storage. 0 = unlimited.
StorageBytes int64 `json:"storage_bytes,omitempty"`
// MaxUsers caps the tenant's user count. 0 = unlimited.
MaxUsers int `json:"max_users,omitempty"`
}
PlanLimits are the resource ceilings a plan grants a tenant. The zero value means "unlimited" for that dimension. Stamped as a JSON snapshot into providers.limits_json at signup so later catalog edits don't silently change an existing tenant's entitlement.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service implements the self-signup skeleton. Constructed ONLY when FILEX_CLOUD=1 (api.BuildRouter); nothing else references it.
func New ¶
New builds the Service from the runtime config pieces. An invalid FILEX_CLOUD_PLANS does not abort boot: the service falls back to DefaultPlans() and reports the parse error through Status().
func (*Service) ApplyStripeEvent ¶ added in v0.54.0
ApplyStripeEvent acts on an event VerifyWebhookSignature has accepted, and reports whether a tenant's plan changed.
checkout.session.completed with payment_status "paid" moves the tenant the session names (client_reference_id, set by Checkout) to the paid plan in metadata.plan, stamping the plan snapshot and the subscription (else the customer) as billing_ref. Every other event is acknowledged and ignored.
TODO(cloud-launch): customer.subscription.deleted → back to SignupPlan (needs a lookup of the tenant by billing_ref).
func (*Service) Checkout ¶ added in v0.54.0
func (s *Service) Checkout(ctx context.Context, sc *StripeClient, slug, planID string, origin func(ctx context.Context, providerID int64) string) (string, error)
Checkout opens a Stripe checkout session for the tenant named by slug to move to planID, and returns the hosted page's address. The price is the catalogue plan's, the return addresses are the server's (ReturnURLs over origin(the tenant)), and the tenant and plan travel in the session for the webhook. A plan with no stripe_price_id cannot be bought (ErrInvalid).
func (*Service) PlansErr ¶
PlansErr returns the remembered FILEX_CLOUD_PLANS parse error ("" when the catalog loaded cleanly).
func (*Service) Signup ¶
func (s *Service) Signup(ctx context.Context, req SignupRequest) (*SignupResult, error)
Signup validates the request and provisions the tenant DISABLED, pending e-mail verification. Provisioning reuses the exact primitive the /api/admin/providers lifecycle API uses (db.Store.CreateProvider) — no parallel provisioning path — then stamps the plan snapshot into the migration-00021 columns: SignupPlan's, never one the request names. Storage linking is deliberately NOT part of the skeleton (see docs/CLOUD.md runbook).
func (*Service) SignupPlan ¶ added in v0.54.0
SignupPlan is the plan every new tenant starts on: the catalogue's first plan nobody pays for. It is never the plan a signup asks for: a paid plan is reached only through a payment Stripe confirms (ApplyStripeEvent).
type SignupRequest ¶
type SignupRequest struct {
// Email of the prospective tenant owner (receives the verification mail).
Email string `json:"email"`
// Slug is the requested tenant identifier (DNS-label rules).
Slug string `json:"slug"`
// Name is the display name; defaults to the slug.
Name string `json:"name,omitempty"`
// (No plan: a signup starts on SignupPlan whatever its body says. A
// `plan` field a client still sends is ignored.)
// Locale is the language the verification mail is written in — the
// signing-up visitor's (the handler fills it from the request when the
// body does not say). Any language the server speaks; English otherwise.
Locale string `json:"locale,omitempty"`
}
SignupRequest is the POST /api/cloud/signup payload.
type SignupResult ¶
type SignupResult struct {
ProviderID int64 `json:"tenant_id"`
Slug string `json:"slug"`
Host string `json:"host,omitempty"`
Plan string `json:"plan"`
// MailSent — whether the verification mail went out through SMTP.
MailSent bool `json:"mail_sent"`
// VerifyToken is returned ONLY when the mail could not be sent (no/unverified
// SMTP) so a local/dev flow can complete verification — the same on-screen
// fallback pattern the share/invite mailer uses. Empty when mailed.
VerifyToken string `json:"verify_token,omitempty"`
}
SignupResult reports a provisioned (but not yet verified) tenant.
type StripeClient ¶
type StripeClient struct {
// Secret is the API secret key (sk_test_… / sk_live_…). Empty = not
// configured; every call returns ErrStripeNotConfigured.
Secret string
// HTTP is the transport; nil → a 15s-timeout default client.
HTTP *http.Client
}
StripeClient is a minimal stdlib-http Stripe caller.
func NewStripe ¶
func NewStripe(secret string) *StripeClient
NewStripe constructs the client (secret may be empty = not configured).
func (*StripeClient) Configured ¶
func (c *StripeClient) Configured() bool
Configured reports whether a secret key is present.
func (*StripeClient) CreateCheckoutSession ¶
func (c *StripeClient) CreateCheckoutSession(ctx context.Context, cs CheckoutSession) (string, error)
CreateCheckoutSession opens POST /v1/checkout/sessions and returns the hosted checkout URL.
TODO(cloud-launch): idempotency key header (Idempotency-Key) TODO(cloud-launch): typed error decoding (Stripe error JSON envelope)