Files
Matthew Meszaros 734cb5fe08 feat: make self-hosted onboarding survivable by fixing invite_only, which could not onboard anyone (the accept route is JWT-only, so redeeming the invitation that would create your account required already having one, making the self-host default silently identical to fully closed), threading the invitation token through registration so an invited person lands in the inviting organization instead of a stray workspace, gating SSO just-in-time provisioning behind DISABLE_REGISTRATION (it bypassed the gate entirely, so an instance set to true was still open to anyone the IdP would assert) with SSO_AUTO_PROVISION as the opt-out, correcting the OIDC redirect URL that pointed at /api/v1 against a route at /v1 and 404'd every SSO login, scoping the first-launch exemption so it no longer overrides an explicit lockdown, preserving the remaining TTL when restoring a losing setup token so a public endpoint cannot hold the claim window open forever, replacing a generic 403 with typed registration_invite_only, registration_closed, invitation_invalid, setup_token_invalid and setup_already_complete codes that name the next step, logging why no claim link was issued on an already-claimed instance instead of staying silent, adding a warmblyctl operator CLI (status with health checks and a non-zero exit, reissuable setup-link, user create/list/reset-password/grant-admin/revoke-admin/disable-2fa, hash-password) so a locked-out operator no longer needs hand-written psql, adding read-only instance configuration over 104 environment variables with structural secret redaction and fingerprints, 35 health checks, a database-backed settings tier for the three keys no environment variable owns, hiding the signup form when the config already says invite_only rather than failing the whole form with a toast, and documenting first run, accounts and access, configuration, instance health and troubleshooting alongside the root .env.example the README told operators to write but never shipped (#114)
2026-08-16 05:58:11 +02:00

49 lines
1.7 KiB
Go

package main
import (
"context"
"fmt"
"github.com/warmbly/warmbly/internal/app/bootstrap"
)
func runSetupLink(ctx context.Context, args []string) error {
fs := newFlagSet("setup-link")
if err := fs.Parse(args); err != nil {
return err
}
if err := noExtraArgs(fs); err != nil {
return err
}
c, err := connect(ctx)
if err != nil {
return err
}
defer c.close()
accounts, cerr := c.users.CountUsers(ctx)
if cerr != nil {
return fmt.Errorf("counting accounts: %w", cerr)
}
if accounts > 0 {
return fmt.Errorf("this instance already has %d account(s), so no setup link is issued: a second owner must never be mintable without an existing account.\nCreate the account you need instead:\n warmblyctl user create --email you@example.com --admin\nOr recover the one you had:\n warmblyctl user reset-password --email you@example.com", accounts)
}
// The token lives in Redis, so this is the one command that cannot degrade.
if err := c.openCache(ctx, true, ""); err != nil {
return fmt.Errorf("%w\nA setup link is stored in Redis, so it cannot be issued while Redis is down. Create the owner directly instead:\n warmblyctl user create --email you@example.com --admin", err)
}
svc := bootstrap.NewService(c.users, c.userService(), c.orgService(), c.trialService(), c.admins, c.cache)
link, lerr := svc.IssueSetupLink(ctx)
if lerr != nil {
return lerr
}
fmt.Printf("Issued a setup link. It replaces any earlier one, is single use, and expires in %s.\n\n %s\n\n", bootstrap.SetupTokenTTL, link)
fmt.Println("Only its hash is stored, so this is the only time it is printed.")
fmt.Println("If that host is wrong, set APP_URL to the URL the dashboard is served from and run this again.")
return nil
}