feat: disable registration

This commit is contained in:
juancwu 2026-05-17 14:30:59 +00:00
commit 39330ce821
17 changed files with 179 additions and 132 deletions

View file

@ -12,6 +12,9 @@ JWT_SECRET=
# Go duration format # Go duration format
JWT_EXPIRY=168h JWT_EXPIRY=168h
# Set to true to block new account creation via magic link. Existing users can still log in.
DISABLE_REGISTRATION=false
MAILER_SMTP_HOST= MAILER_SMTP_HOST=
MAILER_SMTP_PORT= MAILER_SMTP_PORT=
MAILER_IMAP_HOST= MAILER_IMAP_HOST=

View file

@ -85,6 +85,7 @@ func New(cfg *config.Config) (*App, error) {
cfg.JWTExpiry, cfg.JWTExpiry,
cfg.TokenMagicLinkExpiry, cfg.TokenMagicLinkExpiry,
cfg.IsProduction(), cfg.IsProduction(),
cfg.DisableRegistration,
) )
inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService, auditLogService) inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService, auditLogService)
recurringEventService := service.NewRecurringEventService(recurringEventRepository, transactionService, accountService) recurringEventService := service.NewRecurringEventService(recurringEventRepository, transactionService, accountService)

View file

@ -23,6 +23,8 @@ type Config struct {
JWTExpiry time.Duration JWTExpiry time.Duration
TokenMagicLinkExpiry time.Duration TokenMagicLinkExpiry time.Duration
DisableRegistration bool
MailerSMTPHost string MailerSMTPHost string
MailerSMTPPort int MailerSMTPPort int
MailerIMAPHost string MailerIMAPHost string
@ -58,6 +60,8 @@ func Load(version string) *Config {
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
TokenMagicLinkExpiry: envDuration("TOKEN_MAGIC_LINK_EXPIRY", 10*time.Minute), TokenMagicLinkExpiry: envDuration("TOKEN_MAGIC_LINK_EXPIRY", 10*time.Minute),
DisableRegistration: envBool("DISABLE_REGISTRATION", false),
MailerSMTPHost: envString("MAILER_SMTP_HOST", ""), MailerSMTPHost: envString("MAILER_SMTP_HOST", ""),
MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587), MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587),
MailerIMAPHost: envString("MAILER_IMAP_HOST", ""), MailerIMAPHost: envString("MAILER_IMAP_HOST", ""),
@ -120,6 +124,19 @@ func envInt(key string, def int) int {
return int(i) return int(i)
} }
func envBool(key string, def bool) bool {
value, ok := os.LookupEnv(key)
if !ok || value == "" {
return def
}
b, err := strconv.ParseBool(value)
if err != nil {
slog.Warn("config invalid bool, using default", "key", key, "value", value, "default", def)
return def
}
return b
}
func envDuration(key string, def time.Duration) time.Duration { func envDuration(key string, def time.Duration) time.Duration {
value, ok := os.LookupEnv(key) value, ok := os.LookupEnv(key)
if !ok || value == "" { if !ok || value == "" {

View file

@ -88,6 +88,23 @@ func (h *authHandler) SendMagicLink(w http.ResponseWriter, r *http.Request) {
err = h.authService.SendMagicLink(email) err = h.authService.SendMagicLink(email)
if err != nil { if err != nil {
slog.Warn("magic link send failed", "error", err, "email", email) slog.Warn("magic link send failed", "error", err, "email", email)
if errors.Is(err, service.ErrRegistrationDisabled) {
msg := "Registration is disabled. Please contact an administrator if you need an account."
if r.URL.Query().Get("resend") == "true" {
ui.RenderToast(w, r, toast.Toast(toast.Props{
Title: "Magic link not sent",
Description: msg,
Variant: toast.VariantError,
Icon: true,
Dismissible: true,
Duration: 5000,
}))
return
}
ui.Render(w, r, pages.Auth(msg))
return
}
} }
if r.URL.Query().Get("resend") == "true" { if r.URL.Query().Get("resend") == "true" {

View file

@ -24,7 +24,7 @@ func newTestAuthHandler(dbi testutil.DBInfo) *authHandler {
spaceSvc := service.NewSpaceService(spaceRepo) spaceSvc := service.NewSpaceService(spaceRepo)
accountSvc := service.NewAccountService(accountRepo) accountSvc := service.NewAccountService(accountRepo)
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false) emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false) authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil) inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)
return NewAuthHandler(authSvc, inviteSvc, spaceSvc) return NewAuthHandler(authSvc, inviteSvc, spaceSvc)
} }

View file

@ -21,7 +21,7 @@ func newTestSettingsHandler(dbi testutil.DBInfo) (*settingsHandler, *service.Aut
spaceSvc := service.NewSpaceService(spaceRepo) spaceSvc := service.NewSpaceService(spaceRepo)
accountSvc := service.NewAccountService(accountRepo) accountSvc := service.NewAccountService(accountRepo)
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false) emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false) authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
userSvc := service.NewUserService(userRepo) userSvc := service.NewUserService(userRepo)
return NewSettingsHandler(authSvc, userSvc), authSvc return NewSettingsHandler(authSvc, userSvc), authSvc
} }

View file

@ -26,7 +26,7 @@ func newTestApp(dbi testutil.DBInfo) *app.App {
spaceSvc := service.NewSpaceService(spaceRepo) spaceSvc := service.NewSpaceService(spaceRepo)
accountSvc := service.NewAccountService(accountRepo) accountSvc := service.NewAccountService(accountRepo)
emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false) emailSvc := service.NewEmailService(nil, "test@example.com", "http://localhost:9999", "Budgit Test", false)
authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false) authSvc := service.NewAuthService(emailSvc, userRepo, tokenRepo, spaceSvc, accountSvc, cfg.JWTSecret, cfg.JWTExpiry, cfg.TokenMagicLinkExpiry, false, false)
userSvc := service.NewUserService(userRepo) userSvc := service.NewUserService(userRepo)
inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil) inviteSvc := service.NewInviteService(inviteRepo, spaceRepo, userRepo, emailSvc, nil)

View file

@ -29,6 +29,7 @@ var (
ErrEmailNotVerified = errors.New("email not verified") ErrEmailNotVerified = errors.New("email not verified")
ErrInvalidEmail = errors.New("invalid email address") ErrInvalidEmail = errors.New("invalid email address")
ErrNameRequired = errors.New("name is required") ErrNameRequired = errors.New("name is required")
ErrRegistrationDisabled = errors.New("registration is disabled")
) )
type AuthService struct { type AuthService struct {
@ -41,6 +42,7 @@ type AuthService struct {
jwtExpiry time.Duration jwtExpiry time.Duration
tokenMagicLinkExpiry time.Duration tokenMagicLinkExpiry time.Duration
isProduction bool isProduction bool
disableRegistration bool
} }
func NewAuthService( func NewAuthService(
@ -53,6 +55,7 @@ func NewAuthService(
jwtExpiry time.Duration, jwtExpiry time.Duration,
tokenMagicLinkExpiry time.Duration, tokenMagicLinkExpiry time.Duration,
isProduction bool, isProduction bool,
disableRegistration bool,
) *AuthService { ) *AuthService {
return &AuthService{ return &AuthService{
emailService: emailService, emailService: emailService,
@ -64,6 +67,7 @@ func NewAuthService(
jwtExpiry: jwtExpiry, jwtExpiry: jwtExpiry,
tokenMagicLinkExpiry: tokenMagicLinkExpiry, tokenMagicLinkExpiry: tokenMagicLinkExpiry,
isProduction: isProduction, isProduction: isProduction,
disableRegistration: disableRegistration,
} }
} }
@ -235,6 +239,10 @@ func (s *AuthService) SendMagicLink(email string) error {
if err != nil { if err != nil {
// User doesn't exist - create a new passwordless account // User doesn't exist - create a new passwordless account
if errors.Is(err, repository.ErrUserNotFound) { if errors.Is(err, repository.ErrUserNotFound) {
if s.disableRegistration {
slog.Info("registration disabled, refusing to create new user", "email", email)
return ErrRegistrationDisabled
}
now := time.Now() now := time.Now()
user = &model.User{ user = &model.User{
ID: uuid.NewString(), ID: uuid.NewString(),

View file

@ -30,6 +30,7 @@ func newTestAuthService(dbi testutil.DBInfo) *AuthService {
cfg.JWTExpiry, cfg.JWTExpiry,
cfg.TokenMagicLinkExpiry, cfg.TokenMagicLinkExpiry,
false, false,
false,
) )
} }