feat(auth): confirm the e-mail address of new accounts by link
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m21s

- Setting / REGISTRATION_VERIFY_EMAIL; only with SMTP and BASE_URL; invites skip it
- Hashed one-time tokens (48 h), resend with cooldown, unconfirmed accounts purged
- Approval modes notify admins only after the address is confirmed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 14:01:41 +01:00
1 parent 1406650b7c
commit ab97638361
25 files changed
+621 -56

No files matched your search

+30 -9
View File
@@ -43,6 +43,7 @@ func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
"allowRegistration": policy.Mode != storage.RegistrationClosed,
"registrationMode": policy.Mode,
"registrationDomains": policy.Domains,
"emailVerification": policy.VerifyEmail,
"instanceName": instanceName,
"demoMode": s.cfg.DemoMode,
"sharingMode": s.store.SharingMode(),
@@ -199,6 +200,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
role := "member"
var validInvite *domain.UserInvite
pending := false // registered without invite while approval is needed
verify := false // registered without invite while the address must be confirmed
if strings.TrimSpace(req.InviteCode) != "" {
inv, err := s.store.GetUserInviteByCode(req.InviteCode)
@@ -215,8 +217,10 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
} else if count == 0 {
role = "admin"
} else {
policy := s.registrationPolicy()
var err error
pending, err = s.registrationPolicy().registrationOutcome(req.Email)
pending, err = policy.registrationOutcome(req.Email)
verify = policy.VerifyEmail
if err != nil {
writeError(w, http.StatusForbidden, err.Error())
return
@@ -261,22 +265,35 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
}
}
// Waiting for approval: the account exists but stays locked, no session yet
if pending {
// Waiting for the e-mail confirmation and/or approval: the account exists but stays locked, no session
if pending || verify {
inactive := false
if _, err := s.store.UpdateUser(user.ID, nil, nil, nil, &inactive, nil, nil); err != nil {
writeError(w, http.StatusInternalServerError, "Konto konnte nicht angelegt werden")
return
}
if err := s.store.AddPendingRegistration(user.ID); err != nil {
writeError(w, http.StatusInternalServerError, "Konto konnte nicht angelegt werden")
return
if pending {
if err := s.store.AddPendingRegistration(user.ID); err != nil {
writeError(w, http.StatusInternalServerError, "Konto konnte nicht angelegt werden")
return
}
}
slog.Info("Registrierung wartet auf Freigabe", "user_id", user.ID, "email", user.Email)
go s.notifyAdminsOfRegistration(user)
message := "Dein Konto wurde angelegt und wartet auf die Freigabe durch einen Administrator. Du bekommst eine E-Mail, sobald du dich anmelden kannst."
if verify {
// Admins hear about it only once the address is confirmed
go s.sendVerificationMail(user)
message = "Fast geschafft: Wir haben dir eine E-Mail an " + user.Email + " geschickt. Bestätige deine Adresse über den Link darin."
if pending {
message += " Danach schaltet ein Administrator dein Konto frei."
}
} else {
go s.notifyAdminsOfRegistration(user)
}
slog.Info("Registrierung wartet", "user_id", user.ID, "email", user.Email, "approval", pending, "verify", verify)
writeJSON(w, http.StatusAccepted, map[string]interface{}{
"pending": true,
"message": "Dein Konto wurde angelegt und wartet auf die Freigabe durch einen Administrator. Du bekommst eine E-Mail, sobald du dich anmelden kannst.",
"verify": verify,
"message": message,
})
return
}
@@ -343,6 +360,10 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
s.loginGuard.recordSuccess(account, ip)
if !user.IsActive && s.store.IsEmailUnverified(user.ID) {
writeError(w, http.StatusForbidden, "Bitte bestätige zuerst deine E-Mail-Adresse über den Link, den wir dir geschickt haben")
return
}
if !user.IsActive && s.store.IsPendingRegistration(user.ID) {
writeError(w, http.StatusForbidden, "Dein Konto wartet noch auf die Freigabe durch einen Administrator")
return