- 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>
85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
from pydantic import BaseModel, EmailStr, Field, model_validator
|
||
|
||
|
||
class RegisterRequest(BaseModel):
|
||
company_name: str = Field(min_length=2, max_length=255)
|
||
first_name: str = Field(min_length=1, max_length=100)
|
||
last_name: str = Field(min_length=1, max_length=100)
|
||
email: EmailStr
|
||
password: str = Field(min_length=8, max_length=128)
|
||
|
||
@model_validator(mode="after")
|
||
def password_strength(self):
|
||
pw = self.password
|
||
if not any(c.isupper() for c in pw):
|
||
raise ValueError("Password must contain at least one uppercase letter")
|
||
if not any(c.isdigit() for c in pw):
|
||
raise ValueError("Password must contain at least one digit")
|
||
return self
|
||
|
||
|
||
class LoginRequest(BaseModel):
|
||
# Login-Kennung: i.d.R. eine E-Mail, kann aber auch eine intern erzeugte
|
||
# Kennung sein (z.B. Mandanten-Admin ohne E-Mail → '<name>@<slug>.local').
|
||
# Daher kein EmailStr-Zwang – das Passwort ist das eigentliche Gate.
|
||
email: str = Field(min_length=1, max_length=255)
|
||
password: str
|
||
|
||
|
||
class RefreshRequest(BaseModel):
|
||
refresh_token: str | None = None
|
||
|
||
|
||
class PasswordResetRequest(BaseModel):
|
||
email: EmailStr
|
||
|
||
|
||
class PasswordResetConfirm(BaseModel):
|
||
token: str
|
||
new_password: str = Field(min_length=8, max_length=128)
|
||
|
||
@model_validator(mode="after")
|
||
def password_strength(self):
|
||
pw = self.new_password
|
||
if not any(c.isupper() for c in pw):
|
||
raise ValueError("Password must contain at least one uppercase letter")
|
||
if not any(c.isdigit() for c in pw):
|
||
raise ValueError("Password must contain at least one digit")
|
||
return self
|
||
|
||
|
||
class TokenResponse(BaseModel):
|
||
access_token: str
|
||
refresh_token: str | None = None # Nur für API-Clients; Browser nutzt HttpOnly-Cookie
|
||
token_type: str = "bearer"
|
||
totp_required: bool = False
|
||
partial_token: str | None = None
|
||
|
||
|
||
class TotpSetupResponse(BaseModel):
|
||
secret: str # base32 secret for manual entry
|
||
otpauth_uri: str # otpauth://totp/... für QR-Code
|
||
|
||
|
||
class TotpConfirmRequest(BaseModel):
|
||
code: str = Field(min_length=6, max_length=6)
|
||
|
||
|
||
class TotpLoginRequest(BaseModel):
|
||
partial_token: str
|
||
code: str = Field(min_length=6, max_length=6)
|
||
|
||
|
||
class TotpDisableRequest(BaseModel):
|
||
password: str
|
||
code: str = Field(min_length=6, max_length=6)
|
||
|
||
|
||
class AccessTokenResponse(BaseModel):
|
||
access_token: str
|
||
token_type: str = "bearer"
|
||
|
||
|
||
class MessageResponse(BaseModel):
|
||
message: str
|