Files

109 lines
4.3 KiB
Go

package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/warmbly/warmbly/internal/config"
)
// DeploymentAuthConfig is what the login screen needs to render truthfully.
//
// Without it the frontends guess: the Turnstile widget mounted even when
// captcha was off server-side, social buttons rendered with no client
// configured, and nothing told a self-hoster their login code was going to a
// log file. Everything here is public, non-secret configuration.
type DeploymentAuthConfig struct {
// Captcha reports whether a Turnstile token is actually verified. When
// false the client must not mount the widget: a self-hosted or air-gapped
// install cannot reach challenges.cloudflare.com.
Captcha bool `json:"captcha"`
// PasswordLogin is false when the deployment authenticates only through
// OIDC or passkeys.
PasswordLogin bool `json:"password_login"`
// LoginCode is always, new_device or off. The client uses it to decide
// whether to expect a code step, and to explain the flow up front.
LoginCode string `json:"login_code"`
// Registration is false, invite_only or true, already resolved through the
// first-launch exemption, so a brand new instance reports open signups.
Registration string `json:"registration"`
// EmailVerification reports whether a signup must confirm an emailed code.
EmailVerification bool `json:"email_verification"`
// MailDelivers is false when the platform mail transport does not put mail
// on the wire (MAIL_TRANSPORT=log). The login screen tells the operator
// where to find codes instead of leaving them waiting for an email.
MailDelivers bool `json:"mail_delivers"`
// Passkeys reports whether WebAuthn can work here at all. It needs a
// secure context, so a plain-http LAN origin disables it rather than
// failing in the browser with an opaque error.
Passkeys bool `json:"passkeys"`
Providers []string `json:"providers"`
// SelfHosted lets the UI drop hosted-only affordances (billing prompts,
// referral fields) that make no sense on someone's own server.
SelfHosted bool `json:"self_hosted"`
// BillingEnabled mirrors the backend feature gate exactly: false when
// BILLING_PROVIDER=none, in which case every feature is unlocked and the
// dashboard must not present the org as being on a trial or free tier.
BillingEnabled bool `json:"billing_enabled"`
// SetupRequired is true while the instance has no accounts at all. The
// login screen redirects to the setup page rather than showing a form
// nobody can yet use.
SetupRequired bool `json:"setup_required"`
// InvitesRequired mirrors registration == invite_only, precomputed so the
// client does not reimplement the meaning of a tri-state string.
InvitesRequired bool `json:"invites_required"`
// DocsURL is where to send someone whose signup was refused by deployment
// policy rather than by anything they did wrong.
DocsURL string `json:"docs_url"`
}
// accountsDocsURL is the page every registration refusal points at.
const accountsDocsURL = "https://docs.warmbly.com/development/accounts-and-access/"
// AuthConfig serves GET /v1/auth/config. Public and unauthenticated by design:
// it is the first request the login screen makes.
func (h *Handler) AuthConfig(c *gin.Context) {
policy := h.AuthService.Policy()
providers := []string{}
if h.ExternalAuthProviders.GoogleIOSClientID != "" || h.GoogleWebSignIn {
providers = append(providers, "google")
}
if h.ExternalAuthProviders.AppleBundleID != "" || h.AppleWebSignIn {
providers = append(providers, "apple")
}
if h.OIDCEnabled {
providers = append(providers, "oidc")
}
registration := h.AuthService.RegistrationMode(c.Request.Context())
c.JSON(http.StatusOK, DeploymentAuthConfig{
Captcha: config.CaptchaProvider() != "none",
PasswordLogin: !policy.DisablePasswordLogin,
LoginCode: policy.LoginCode,
Registration: registration,
EmailVerification: policy.RequireEmailVerification,
MailDelivers: h.MailDelivers,
Passkeys: h.PasskeysUsable,
Providers: providers,
SelfHosted: config.SelfHosted(),
BillingEnabled: config.BillingProvider() != "none",
SetupRequired: h.BootstrapService != nil && h.BootstrapService.Required(c.Request.Context()),
InvitesRequired: registration == config.RegistrationInviteOnly,
DocsURL: accountsDocsURL,
})
}