package api import ( "encoding/json" "net/http" "strconv" "time" "github.com/archivmail/internal/audit" "github.com/archivmail/internal/mailer" "github.com/archivmail/internal/tokenstore" "github.com/archivmail/internal/userstore" ) // handleCreateInvite generates an invite token for a tenant and optionally emails it. // POST /api/admin/invite — domain_admin+ (PROJ-28) func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) { var body struct { TenantID int64 `json:"tenant_id"` // superadmin: arbitrary; domain_admin: own tenant Email string `json:"email"` // optional: send invite by email } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } sess := sessionFromCtx(r.Context()) // domain_admin may only invite to their own tenant if sess.Role != userstore.RoleSuperAdmin { tenantID := tenantFromCtx(r.Context()) if tenantID == nil || *tenantID != body.TenantID { writeError(w, http.StatusForbidden, "forbidden") return } } tid := body.TenantID token, err := s.tokenStore.Create(r.Context(), tokenstore.TypeInvite, nil, &tid, 72*time.Hour) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } inviteURL := "" if s.fqdn != "" { inviteURL = "https://" + s.fqdn + "/signup?invite=" + token } // Optionally send invite email if body.Email != "" && s.mailer.IsConfigured() { tenantName := strconv.FormatInt(body.TenantID, 10) if s.tenantStore != nil { if t, err := s.tenantStore.Get(r.Context(), body.TenantID); err == nil { tenantName = t.Name } } go func() { html := mailer.InviteHTML(s.fqdn, token, tenantName) txt := mailer.InviteText(s.fqdn, token, tenantName) _ = s.mailer.Send(body.Email, "Einladung zu archivmail", html, txt) }() } if s.audlog != nil { s.audlog.Log(audit.Entry{ EventType: "invite_created", Username: sess.Username, IPAddress: s.remoteIP(r), Success: true, }) } writeJSON(w, http.StatusOK, map[string]interface{}{ "token": token, "invite_url": inviteURL, }) } // handleCheckInvite validates an invite token and returns the tenant name. // GET /api/auth/invite?token=... (PROJ-28) func (s *Server) handleCheckInvite(w http.ResponseWriter, r *http.Request) { plain := r.URL.Query().Get("token") if plain == "" { writeError(w, http.StatusBadRequest, "token required") return } tok, err := s.tokenStore.Peek(r.Context(), tokenstore.TypeInvite, plain) if err != nil { writeError(w, http.StatusBadRequest, "ungültiger oder abgelaufener Einladungslink") return } tenantName := "" if tok.TenantID != nil && s.tenantStore != nil { if t, err := s.tenantStore.Get(r.Context(), *tok.TenantID); err == nil { tenantName = t.Name } } writeJSON(w, http.StatusOK, map[string]interface{}{ "valid": true, "tenant_name": tenantName, }) }