feat(PROJ-28): Self-Service Onboarding — Signup, Verify, Password Reset, Invites

- internal/mailer: SMTP-Out via net/smtp (TLS + STARTTLS), HTML+Text-Templates
- internal/tokenstore: auth_tokens Tabelle, SHA-256-Hash, TTL, einmalig verwendbar
- userstore: CreateInactive(), Activate(), GetByEmail(), SetPassword()
- API: POST /signup, GET /verify, POST /forgot-password, POST /reset-password
- API: POST /admin/invite (domain_admin+), GET /auth/invite?token (check)
- Login-Seite: Links zu "Passwort vergessen" und "Registrieren"
- Frontend: /signup, /verify, /forgot-password, /reset-password Seiten
- server.fqdn nicht konfiguriert → Startup-Warnung, Self-Service deaktiviert
- LDAP-Nutzer: Passwort-Reset abgewiesen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 21:54:11 +02:00
parent 7930b85cde
commit 4583262ea4
13 changed files with 1232 additions and 0 deletions
+28
View File
@@ -18,10 +18,12 @@ import (
"github.com/archivmail/internal/index"
"github.com/archivmail/internal/labelstore"
ldapcfg "github.com/archivmail/internal/ldapconfig"
"github.com/archivmail/internal/mailer"
pop3store "github.com/archivmail/internal/pop3"
"github.com/archivmail/internal/smtpd"
"github.com/archivmail/internal/storage"
"github.com/archivmail/internal/tenantstore"
"github.com/archivmail/internal/tokenstore"
"github.com/archivmail/internal/userstore"
)
@@ -81,6 +83,9 @@ type Server struct {
appVersion string
moduleVersions map[string]string
globalRetentionDays int // from storage config (PROJ-34)
mailer *mailer.Mailer
tokenStore *tokenstore.Store
fqdn string // from server.fqdn config (PROJ-28)
}
// SetSMTPDaemon wires the SMTP daemon into the API server after construction.
@@ -117,6 +122,21 @@ func (s *Server) SetGlobalRetentionDays(days int) {
s.globalRetentionDays = days
}
// SetMailer wires the SMTP-Out mailer into the API server (PROJ-28).
func (s *Server) SetMailer(m *mailer.Mailer) {
s.mailer = m
}
// SetTokenStore wires the token store into the API server (PROJ-28).
func (s *Server) SetTokenStore(ts *tokenstore.Store) {
s.tokenStore = ts
}
// SetFQDN wires the server FQDN for link generation (PROJ-28).
func (s *Server) SetFQDN(fqdn string) {
s.fqdn = fqdn
}
// New creates and wires up a new API server.
func New(
cfg config.APIConfig,
@@ -157,6 +177,14 @@ func (s *Server) routes() {
s.mux.HandleFunc("POST /api/auth/login", s.handleLogin)
s.mux.HandleFunc("GET /api/auth/me", s.auth(s.handleMe))
s.mux.HandleFunc("POST /api/auth/logout", s.auth(s.handleLogout))
// PROJ-28: Self-Service Onboarding
s.mux.HandleFunc("POST /api/auth/signup", s.handleSignup)
s.mux.HandleFunc("GET /api/auth/verify", s.handleVerifyEmail)
s.mux.HandleFunc("POST /api/auth/forgot-password", s.handleForgotPassword)
s.mux.HandleFunc("POST /api/auth/reset-password", s.handleResetPassword)
s.mux.HandleFunc("GET /api/auth/invite", s.handleCheckInvite)
s.mux.HandleFunc("POST /api/admin/invite", s.authAdmin(s.handleCreateInvite))
s.mux.HandleFunc("GET /api/users", s.authAdmin(s.handleListUsers))
s.mux.HandleFunc("POST /api/users", s.authAdmin(s.handleCreateUser))
s.mux.HandleFunc("PATCH /api/users/{id}", s.authAdmin(s.handleUpdateUser))