# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Invitation model for self-service onboarding. Token-basierte Einladungen für neue User zu Mandanten/Features. """ import time import uuid import secrets from typing import Optional, List from pydantic import BaseModel, Field, computed_field from modules.datamodels.datamodelBase import PowerOnModel from modules.shared.i18nRegistry import i18nModel @i18nModel("Einladung") class Invitation(PowerOnModel): """ Einladungs-Token für neue User. Ermöglicht Self-Service Onboarding zu Mandanten und Feature-Instanzen. """ id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the invitation", json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False} ) token: str = Field( default_factory=lambda: secrets.token_urlsafe(32), description="Secure invitation token", json_schema_extra={"label": "Token", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False} ) mandateId: str = Field( description="FK → Mandate.id - Target mandate for the invitation", json_schema_extra={ "label": "Mandant", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "fk_target": {"db": "poweron_app", "table": "Mandate", "labelField": "label"}, }, ) featureInstanceId: Optional[str] = Field( default=None, description="Optional FK → FeatureInstance.id - Direct access to specific feature", json_schema_extra={ "label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "fk_target": {"db": "poweron_app", "table": "FeatureInstance", "labelField": "label"}, }, ) roleIds: List[str] = Field( default_factory=list, description="List of Role IDs to assign to the invited user", json_schema_extra={"label": "Rollen", "frontend_type": "multiselect", "frontend_readonly": False, "frontend_required": True} ) targetUsername: Optional[str] = Field( default=None, description="Username of the invited user (must match on acceptance)", json_schema_extra={"label": "Ziel-Benutzername", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False} ) email: Optional[str] = Field( default=None, description="Email address to send invitation link (optional)", json_schema_extra={"label": "E-Mail (optional)", "frontend_type": "email", "frontend_readonly": False, "frontend_required": False} ) expiresAt: float = Field( description="When the invitation expires (UTC timestamp)", json_schema_extra={"label": "Gueltig bis", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": True} ) usedBy: Optional[str] = Field( default=None, description="User ID of the person who used the invitation", json_schema_extra={ "label": "Verwendet von", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "fk_target": {"db": "poweron_app", "table": "UserInDB", "labelField": "username"}, }, ) usedAt: Optional[float] = Field( default=None, description="When the invitation was used (UTC timestamp)", json_schema_extra={"label": "Verwendet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False} ) revokedAt: Optional[float] = Field( default=None, description="When the invitation was revoked (UTC timestamp)", json_schema_extra={"label": "Widerrufen am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False} ) emailSentFlag: Optional[bool] = Field( default=False, description="Whether the invitation email was successfully sent", json_schema_extra={ "label": "E-Mail gesendet", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False, "frontend_format_labels": ["Ja", "-", "Nein"], }, ) emailSentAt: Optional[float] = Field( default=None, description="Timestamp when the invitation email was sent (UTC, seconds)", json_schema_extra={ "label": "E-Mail gesendet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, }, ) maxUses: int = Field( default=1, ge=1, le=100, description="Maximum number of times this invitation can be used", json_schema_extra={"label": "Max. Verwendungen", "frontend_type": "number", "frontend_readonly": False, "frontend_required": False} ) currentUses: int = Field( default=0, ge=0, description="Current number of times this invitation has been used", json_schema_extra={"label": "Aktuelle Verwendungen", "frontend_type": "number", "frontend_readonly": True, "frontend_required": False} ) @computed_field( # type: ignore[prop-decorator] json_schema_extra={ "label": "Abgelaufen", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False, "frontend_format_labels": ["Ja", "-", "Nein"], }, ) @property def expiredFlag(self) -> bool: """True iff `expiresAt` lies in the past (UTC).""" if self.expiresAt is None: return False return float(self.expiresAt) < time.time() @computed_field( # type: ignore[prop-decorator] json_schema_extra={ "label": "Verbraucht", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False, "frontend_format_labels": ["Ja", "-", "Nein"], }, ) @property def usedUpFlag(self) -> bool: """True iff `currentUses >= maxUses`.""" return (self.currentUses or 0) >= (self.maxUses or 1)