feat(auth): invite links for several people, invite by email, registration with approval or email domains
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s
- Invites: link /?invite=CODE, copy link, send by email, max uses with counter - Registration modes closed | approval | domain | domain+approval | open, waiting list with approve/reject - REGISTRATION_MODE / REGISTRATION_DOMAINS lock the admin setting; ALLOW_REGISTRATION=true is honoured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db5c5cde9b
commit
af77b9e809
24 files changed
+1077
-142
No files matched your search
@@ -29,21 +29,24 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
count, _ := s.store.CountUsers()
|
||||
sys, _ := s.store.GetSystemSettings()
|
||||
allowReg := false
|
||||
policy := s.registrationPolicy()
|
||||
instanceName := "MindSift"
|
||||
if sys != nil {
|
||||
allowReg = sys.AllowRegistration
|
||||
if sys.InstanceName != "" {
|
||||
instanceName = sys.InstanceName
|
||||
}
|
||||
}
|
||||
status := map[string]interface{}{
|
||||
"needsSetup": count == 0,
|
||||
"allowRegistration": allowReg,
|
||||
"instanceName": instanceName,
|
||||
"demoMode": s.cfg.DemoMode,
|
||||
"sharingMode": s.store.SharingMode(),
|
||||
"passwordReset": s.passwordResetAvailable(),
|
||||
"needsSetup": count == 0,
|
||||
// Registering without an invite: allowed at all, how (closed | approval | domain | domain+approval |
|
||||
// open) and for which e-mail domains (shown as a hint on the sign-up form)
|
||||
"allowRegistration": policy.Mode != storage.RegistrationClosed,
|
||||
"registrationMode": policy.Mode,
|
||||
"registrationDomains": policy.Domains,
|
||||
"instanceName": instanceName,
|
||||
"demoMode": s.cfg.DemoMode,
|
||||
"sharingMode": s.store.SharingMode(),
|
||||
"passwordReset": s.passwordResetAvailable(),
|
||||
// How voice input works: "server" (own Whisper), "browser" or "off"; set by STT_* only
|
||||
"voice": map[string]interface{}{"mode": s.voiceMode(), "maxSeconds": s.cfg.Speech.MaxSeconds},
|
||||
// Public so the login screen can link Impressum and Datenschutz
|
||||
@@ -131,6 +134,10 @@ func (s *Server) handleFirstBootSetup(w http.ResponseWriter, r *http.Request) {
|
||||
sys.InstanceName = strings.TrimSpace(req.InstanceName)
|
||||
}
|
||||
sys.AllowRegistration = req.AllowRegistration
|
||||
sys.RegistrationMode = storage.RegistrationClosed
|
||||
if req.AllowRegistration {
|
||||
sys.RegistrationMode = storage.RegistrationOpen
|
||||
}
|
||||
sys.SharingMode = storage.NormalizeSharingMode(req.SharingMode)
|
||||
if err := s.store.UpdateSystemSettings(sys); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Einstellungen konnten nicht gespeichert werden: "+err.Error())
|
||||
@@ -191,6 +198,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
count, _ := s.store.CountUsers()
|
||||
role := "member"
|
||||
var validInvite *domain.UserInvite
|
||||
pending := false // registered without invite while approval is needed
|
||||
|
||||
if strings.TrimSpace(req.InviteCode) != "" {
|
||||
inv, err := s.store.GetUserInviteByCode(req.InviteCode)
|
||||
@@ -207,9 +215,14 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
} else if count == 0 {
|
||||
role = "admin"
|
||||
} else {
|
||||
sysSettings, _ := s.store.GetSystemSettings()
|
||||
if sysSettings == nil || !sysSettings.AllowRegistration {
|
||||
writeError(w, http.StatusForbidden, "Öffentliche Registrierung ist deaktiviert – bitte nutze einen Einladungscode vom Administrator")
|
||||
var err error
|
||||
pending, err = s.registrationPolicy().registrationOutcome(req.Email)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
if pending && len(s.store.PendingRegistrationIDs()) >= storage.MaxPendingRegistrations {
|
||||
writeError(w, http.StatusTooManyRequests, "Gerade warten zu viele Registrierungen auf Freigabe – bitte später erneut versuchen")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -248,6 +261,26 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Waiting for approval: the account exists but stays locked, no session yet
|
||||
if pending {
|
||||
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
|
||||
}
|
||||
slog.Info("Registrierung wartet auf Freigabe", "user_id", user.ID, "email", user.Email)
|
||||
go s.notifyAdminsOfRegistration(user)
|
||||
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.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := s.startSession(r, user)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Token konnte nicht erstellt werden")
|
||||
@@ -310,6 +343,10 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
s.loginGuard.recordSuccess(account, ip)
|
||||
if !user.IsActive && s.store.IsPendingRegistration(user.ID) {
|
||||
writeError(w, http.StatusForbidden, "Dein Konto wartet noch auf die Freigabe durch einen Administrator")
|
||||
return
|
||||
}
|
||||
if !user.IsActive {
|
||||
slog.Warn("Login verweigert: Benutzerkonto ist deaktiviert", "login", identifier, "user_id", user.ID, "display_name", user.DisplayName, "remote_addr", r.RemoteAddr)
|
||||
writeError(w, http.StatusForbidden, "Dieses Benutzerkonto ist deaktiviert")
|
||||
|
||||
Reference in new issue
Block a user