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

- 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:
sandrodachandClaude Opus 5.5 committed 2026-10-31 13:43:54 +01:00
1 parent db5c5cde9b
commit af77b9e809
24 files changed
+1077 -142

No files matched your search

+32 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
"github.com/sandrodach/mindsift/internal/domain"
"github.com/sandrodach/mindsift/internal/storage"
)
// --- ORGANIZATIONS & TEAMS ---
@@ -93,6 +94,10 @@ func (s *Server) handleAdminGetUsers(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
pending := s.store.PendingRegistrationIDs()
for i := range users {
users[i].PendingApproval = pending[users[i].ID]
}
writeJSON(w, http.StatusOK, users)
}
@@ -191,6 +196,9 @@ func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
if req.IsActive != nil && !*req.IsActive {
s.wsHub.DisconnectUser(targetUserID)
}
if req.IsActive != nil && *req.IsActive {
s.store.ClearPendingRegistration(targetUserID) // activating by hand counts as approval
}
writeJSON(w, http.StatusOK, u)
}
@@ -247,6 +255,7 @@ func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
return
}
s.removeUserBackups(targetUserID)
s.store.ClearPendingRegistration(targetUserID)
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
@@ -270,6 +279,7 @@ func (s *Server) handleAdminCreateInvite(w http.ResponseWriter, r *http.Request)
Role string `json:"role"`
OrganizationID string `json:"organizationId"`
ValidDays int `json:"validDays"`
MaxUses int `json:"maxUses"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
@@ -278,7 +288,7 @@ func (s *Server) handleAdminCreateInvite(w http.ResponseWriter, r *http.Request)
if req.Role == "admin" && (caller == nil || caller.Role != "admin") {
req.Role = "member"
}
inv, err := s.store.CreateUserInvite(callerID, req.Email, req.Role, req.OrganizationID, req.ValidDays)
inv, err := s.store.CreateUserInvite(callerID, req.Email, req.Role, req.OrganizationID, req.ValidDays, req.MaxUses)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -312,6 +322,16 @@ func (s *Server) settingsForBrowser(settings *domain.SystemSettings) *domain.Sys
out.SMTPPassSet = out.SMTPPass != ""
out.SMTPPass = ""
out.SMTPConfiguredViaEnv = strings.TrimSpace(s.cfg.SMTPHost) != ""
// What the environment fixes is shown as in effect and locked in the form
if s.cfg.RegistrationMode != "" {
out.RegistrationMode = s.cfg.RegistrationMode
out.AllowRegistration = out.RegistrationMode == storage.RegistrationOpen
out.RegistrationConfiguredViaEnv = true
}
if s.cfg.RegistrationDomains != "" {
out.RegistrationDomains = strings.Join(storage.ParseEmailDomains(s.cfg.RegistrationDomains), ", ")
out.RegistrationConfiguredViaEnv = true
}
return &out
}
@@ -327,6 +347,17 @@ func (s *Server) handleAdminUpdateSettings(w http.ResponseWriter, r *http.Reques
settings.SMTPPass = cur.SMTPPass
}
}
// Registration fixed by the environment cannot be changed here (the stored value stays as it was)
if s.cfg.RegistrationMode != "" || s.cfg.RegistrationDomains != "" {
if cur, err := s.store.GetSystemSettings(); err == nil {
if s.cfg.RegistrationMode != "" {
settings.RegistrationMode, settings.AllowRegistration = cur.RegistrationMode, cur.AllowRegistration
}
if s.cfg.RegistrationDomains != "" {
settings.RegistrationDomains = cur.RegistrationDomains
}
}
}
previousMode := s.store.SharingMode()
if err := s.store.UpdateSystemSettings(&settings); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())