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:
@@ -38,15 +38,17 @@ async def create_company(
|
||||
current_user: User = require_role(*_reseller_roles),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
company = await tenant_service.create_tenant(
|
||||
company, admin, initial_password = await tenant_service.create_tenant(
|
||||
data, actor=current_user, db=db, reseller_id=current_user.id,
|
||||
)
|
||||
# Antwort aus der frisch erstellten Firma (Kennzahlen 1 Admin, 0 aktiv)
|
||||
# Antwort aus der frisch erstellten Firma; bei Einladung Account inaktiv,
|
||||
# bei Temp-Passwort sofort aktiv.
|
||||
return TenantOut(
|
||||
id=company.id, name=company.name, slug=company.slug, plan=company.plan,
|
||||
is_active=company.is_active, country=company.country, reseller_id=company.reseller_id,
|
||||
reseller_name=f"{current_user.first_name} {current_user.last_name}".strip(),
|
||||
user_count=1, active_user_count=0, admin_email=data.admin_email,
|
||||
user_count=1, active_user_count=1 if initial_password else 0,
|
||||
admin_email=admin.email, initial_password=initial_password,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
SUPER_ADMIN läuft mit aktivem RLS-Bypass und sieht daher alle Firmen.
|
||||
Nur SUPER_ADMIN darf Reseller anlegen und Firmen einem Reseller zuordnen.
|
||||
"""
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID
|
||||
|
||||
@@ -51,11 +52,13 @@ async def create_tenant(
|
||||
reseller = await db.get(User, reseller_id)
|
||||
if reseller is None or reseller.role != UserRole.RESELLER:
|
||||
raise HTTPException(status_code=400, detail="Ungültiger Reseller.")
|
||||
company = await tenant_service.create_tenant(
|
||||
company, admin, initial_password = await tenant_service.create_tenant(
|
||||
data, actor=current_user, db=db, reseller_id=reseller_id,
|
||||
)
|
||||
tenants = {t.id: t for t in await tenant_service.list_tenants(db)}
|
||||
return tenants[company.id]
|
||||
result = tenants[company.id]
|
||||
result.initial_password = initial_password
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/tenants/{company_id}", response_model=TenantOut)
|
||||
@@ -142,37 +145,52 @@ async def create_reseller(
|
||||
current_user: User = require_role(*_sa),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if await db.scalar(select(User).where(User.email == data.email)):
|
||||
if data.email and await db.scalar(select(User).where(User.email == data.email)):
|
||||
raise HTTPException(status_code=400, detail="E-Mail ist bereits vergeben.")
|
||||
|
||||
raw_token, token_hash = generate_invite_token()
|
||||
reseller = User(
|
||||
company_id=None,
|
||||
email=data.email,
|
||||
first_name=data.first_name,
|
||||
last_name=data.last_name,
|
||||
role=UserRole.RESELLER,
|
||||
password_hash=hash_password(raw_token), # temporär, beim Accept überschrieben
|
||||
invite_token_hash=token_hash,
|
||||
invite_expires=datetime.now(timezone.utc) + timedelta(days=7),
|
||||
is_active=False,
|
||||
)
|
||||
initial_password: str | None = None
|
||||
if data.email:
|
||||
raw_token, token_hash = generate_invite_token()
|
||||
reseller = User(
|
||||
company_id=None, email=data.email,
|
||||
first_name=data.first_name, last_name=data.last_name,
|
||||
role=UserRole.RESELLER,
|
||||
password_hash=hash_password(raw_token), # temporär, beim Accept überschrieben
|
||||
invite_token_hash=token_hash,
|
||||
invite_expires=datetime.now(timezone.utc) + timedelta(days=7),
|
||||
is_active=False,
|
||||
)
|
||||
else:
|
||||
# Kein Mailversand: interne Kennung + sofort nutzbares Temp-Passwort
|
||||
login = await tenant_service._unique_email(
|
||||
f"{data.first_name}.{data.last_name}", "reseller", db
|
||||
)
|
||||
initial_password = secrets.token_urlsafe(9)
|
||||
reseller = User(
|
||||
company_id=None, email=login,
|
||||
first_name=data.first_name, last_name=data.last_name,
|
||||
role=UserRole.RESELLER,
|
||||
password_hash=hash_password(initial_password),
|
||||
is_active=True,
|
||||
)
|
||||
db.add(reseller)
|
||||
await db.flush()
|
||||
db.add(AuditLog(
|
||||
company_id=None, user_id=current_user.id,
|
||||
action="reseller_created", entity_type="user", entity_id=reseller.id,
|
||||
new_value={"email": reseller.email},
|
||||
new_value={"email": reseller.email, "invited": bool(data.email)},
|
||||
))
|
||||
await db.flush()
|
||||
|
||||
from app.services.email_service import email_service
|
||||
await email_service.send_invite(reseller, current_user, raw_token, db)
|
||||
if data.email:
|
||||
from app.services.email_service import email_service
|
||||
await email_service.send_invite(reseller, current_user, raw_token, db)
|
||||
|
||||
return ResellerOut(
|
||||
id=reseller.id, email=reseller.email, first_name=reseller.first_name,
|
||||
last_name=reseller.last_name, is_active=reseller.is_active,
|
||||
created_at=reseller.created_at, company_count=0,
|
||||
initial_password=initial_password,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user