feat: Mandant/Reseller ohne E-Mail anlegbar (Temp-Passwort statt Einladung)

- admin_email bzw. reseller email optional; ohne E-Mail wird interne Login-
  Kennung (<name>@<slug>.local) + einmaliges Temp-Passwort erzeugt, Account
  sofort aktiv, kein Mailversand
- TenantOut/ResellerOut: initial_password (einmalig) ergänzt
- LoginRequest.email: str statt EmailStr (Kennung muss kein zustellbares
  Postfach sein; .local-Domains sind sonst nicht einloggbar)
- Frontend: E-Mail-Felder optional, CredDialog zeigt Login + Temp-Passwort einmalig
- Test: Anlage ohne E-Mail + Login mit generierten Zugangsdaten; 173/173 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:34:23 +02:00
co-authored by Claude Opus 4.8
parent fcda813ba6
commit 9e1ba2bd91
9 changed files with 215 additions and 56 deletions
+10 -3
View File
@@ -25,17 +25,22 @@ class TenantOut(BaseModel):
active_user_count: int = 0
admin_email: str | None = None
created_at: datetime | None = None
# Nur bei Anlage ohne E-Mail befüllt: einmalig anzuzeigende Zugangsdaten
# (Login-Kennung = admin_email, Passwort hier). Sonst None.
initial_password: str | None = None
class TenantCreate(BaseModel):
"""Neue Firma anlegen (durch Reseller oder SUPER_ADMIN).
Es wird automatisch ein erster COMPANY_ADMIN per Einladung angelegt.
Es wird automatisch ein erster COMPANY_ADMIN angelegt. Mit admin_email wird
eine Einladung verschickt; ohne admin_email wird eine interne Login-Kennung
plus Temporär-Passwort erzeugt und einmalig zurückgegeben (kein Mailversand).
"""
name: str = Field(min_length=2, max_length=255)
country: str = Field(default="DE", max_length=10)
plan: str = Field(default="trial", max_length=50)
admin_email: EmailStr
admin_email: EmailStr | None = None
admin_first_name: str = Field(min_length=1, max_length=100)
admin_last_name: str = Field(min_length=1, max_length=100)
@@ -61,9 +66,11 @@ class ResellerOut(BaseModel):
is_active: bool
created_at: datetime | None = None
company_count: int = 0
# Nur bei Anlage ohne E-Mail befüllt: einmalig anzuzeigendes Temporär-Passwort.
initial_password: str | None = None
class ResellerCreate(BaseModel):
email: EmailStr
email: EmailStr | None = None
first_name: str = Field(min_length=1, max_length=100)
last_name: str = Field(min_length=1, max_length=100)