From 2dc6165cee60f5709c6eedc3b485fd9ab3e9e483 Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sat, 5 Sep 2026 10:37:33 -0400 Subject: [PATCH 1/2] sec(cmd/web): added functionality to enable or disable email and password signup. This is particularly useful for selfhoster to prevent unwanted signups after first email password signup or disable it for OIDC use completely #32 --- cmd/web/handlers:login.go | 39 +++++++++++++--------- cmd/web/options.go | 7 ++++ cmd/web/routes.go | 6 ++-- internal/templates/pages/login/signin.tmpl | 2 ++ 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/cmd/web/handlers:login.go b/cmd/web/handlers:login.go index e49ee5a..ed4d009 100644 --- a/cmd/web/handlers:login.go +++ b/cmd/web/handlers:login.go @@ -45,10 +45,23 @@ func (app *application) redirectAfterLogin(ctx context.Context, w http.ResponseW } } +type signInData struct { + OIDCProviders []string + EnableEmailPasswordSignup bool +} + func (app *application) getSignIn(w http.ResponseWriter, r *http.Request) *httperr.Error { + names := make([]string, 0, len(app.oidcProviders)) + for name := range app.oidcProviders { + names = append(names, name) + } + tmplData := app.html.TemplateData(r) tmplData.Form = accounts.NewSignInForm() - tmplData.Data = app.loginData() + tmplData.Data = signInData{ + EnableEmailPasswordSignup: app.options.EnableEmailPasswordSignup, + OIDCProviders: names, + } return app.html.Render(w, r, http.StatusOK, pages.SignIn, tmplData) } @@ -56,9 +69,17 @@ func (app *application) postSignIn(w http.ResponseWriter, r *http.Request) *http ctx := r.Context() fail := func(formWithErrors *accounts.SignInForm) *httperr.Error { + names := make([]string, 0, len(app.oidcProviders)) + for name := range app.oidcProviders { + names = append(names, name) + } + data := app.html.TemplateData(r) data.Form = formWithErrors - data.Data = app.loginData() + data.Data = signInData{ + EnableEmailPasswordSignup: app.options.EnableEmailPasswordSignup, + OIDCProviders: names, + } return app.html.Render(w, r, http.StatusUnprocessableEntity, pages.SignIn, data) } @@ -263,20 +284,6 @@ func (app *application) postSignOut(w http.ResponseWriter, r *http.Request) *htt return nil } -// LoginData carries the set of enabled OIDC provider names to the sign-in and -// sign-up templates so they can render the appropriate provider buttons. -type LoginData struct { - OIDCProviders []string -} - -func (app *application) loginData() LoginData { - names := make([]string, 0, len(app.oidcProviders)) - for name := range app.oidcProviders { - names = append(names, name) - } - return LoginData{OIDCProviders: names} -} - // getAuthOIDC initiates the OIDC login flow for the named provider. It generates // a random state token, stores it in the session for CSRF verification, then // redirects the browser to the provider's authorization endpoint. diff --git a/cmd/web/options.go b/cmd/web/options.go index aacd308..a43df2f 100644 --- a/cmd/web/options.go +++ b/cmd/web/options.go @@ -34,6 +34,11 @@ type options struct { SMTP SMTP // has SECRET OIDCProviders OIDCProviderMap // has SECRET Limits Limits + + // EnableEmailPasswordSignup controls whether users can sign up with email and password. + // Selfhosters can signup once to create an account and then disable email password signup + // to prevent further unwanted signups. + EnableEmailPasswordSignup bool } func parseOptions(args []string) (*options, error) { @@ -60,6 +65,8 @@ func parseOptions(args []string) (*options, error) { fs.StringVar(&cfg.SMTP.Password, "smtp-password", envOr("SMTP_PASSWORD", ""), "SMTP password (prefer SMTP_PASSWORD env var over flag)") fs.StringVar(&cfg.SMTP.From, "smtp-from", "", "from address for outgoing mail") + fs.BoolVar(&cfg.EnableEmailPasswordSignup, "enable-email-password-signup", true, "enable email and password signup (default true)") + if err := fs.Parse(args); err != nil { return nil, fmt.Errorf("parseServeOptions: %w", err) } diff --git a/cmd/web/routes.go b/cmd/web/routes.go index cc52a72..f3a0591 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -50,8 +50,10 @@ func (app *application) routes() (http.Handler, error) { // Login related routes that are only accessible if the user is not logged in. mux.Handle("GET /signin", app.withGuest(app.html.Handle(app.getSignIn))) mux.Handle("POST /signin", app.withGuest(app.html.Handle(app.postSignIn))) - mux.Handle("GET /signup", app.withGuest(app.html.Handle(app.getSignUp))) - mux.Handle("POST /signup", app.withGuest(app.html.Handle(app.postSignUp))) + if app.options.EnableEmailPasswordSignup { + mux.Handle("GET /signup", app.withGuest(app.html.Handle(app.getSignUp))) + mux.Handle("POST /signup", app.withGuest(app.html.Handle(app.postSignUp))) + } mux.Handle("GET /forgot-password", app.withGuest(app.html.Handle(app.getForgotPassword))) mux.Handle("POST /forgot-password", app.withGuest(app.html.Handle(app.postForgotPassword))) mux.Handle("GET /forgot-password/success", app.withGuest(app.html.Handle(app.getForgotPasswordSuccess))) diff --git a/internal/templates/pages/login/signin.tmpl b/internal/templates/pages/login/signin.tmpl index 6f0ce05..90da719 100644 --- a/internal/templates/pages/login/signin.tmpl +++ b/internal/templates/pages/login/signin.tmpl @@ -80,10 +80,12 @@ {{ end }} {{ end }} + {{ if .Data.EnableEmailPasswordSignup }}

Don't have an account? Sign up.

+ {{ end }} From 7c413d00ad03350ff0e6dfc6e8dbf30d97f150cf Mon Sep 17 00:00:00 2001 From: Tobias Gleiter Date: Sat, 5 Sep 2026 10:44:56 -0400 Subject: [PATCH 2/2] docs(cmd/docs): add -enable-email-password-signup to documentation #32 --- internal/templates/pages/docs.tmpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/templates/pages/docs.tmpl b/internal/templates/pages/docs.tmpl index add9fea..772d0c9 100644 --- a/internal/templates/pages/docs.tmpl +++ b/internal/templates/pages/docs.tmpl @@ -289,6 +289,12 @@ error debug, info, warn, error + + -enable-email-password-signup + — + true + enable email and password signup (default true) +