fixes
This commit is contained in:
parent
4de962d7d6
commit
df4c60fc99
30 changed files with 705 additions and 381 deletions
10
app.py
10
app.py
|
|
@ -335,6 +335,15 @@ async def lifespan(app: FastAPI):
|
|||
logger.info("Application has been shut down")
|
||||
|
||||
|
||||
# Custom function to generate readable operation IDs for Swagger UI
|
||||
# Uses snake_case function names directly instead of auto-generated IDs
|
||||
def _generateOperationId(route) -> str:
|
||||
"""Generate operation ID from route function name (snake_case)."""
|
||||
if hasattr(route, "endpoint") and hasattr(route.endpoint, "__name__"):
|
||||
return route.endpoint.__name__
|
||||
return route.name if route.name else "unknown"
|
||||
|
||||
|
||||
# START APP
|
||||
app = FastAPI(
|
||||
title="PowerOn | Data Platform API",
|
||||
|
|
@ -343,6 +352,7 @@ app = FastAPI(
|
|||
swagger_ui_init_oauth={
|
||||
"usePkceWithAuthorizationCodeGrant": True,
|
||||
},
|
||||
generate_unique_id_function=_generateOperationId,
|
||||
)
|
||||
|
||||
# Configure OpenAPI security scheme for Swagger UI
|
||||
|
|
|
|||
|
|
@ -166,15 +166,38 @@ class User(BaseModel):
|
|||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
||||
)
|
||||
language: str = Field(
|
||||
default="en",
|
||||
description="Preferred language of the user",
|
||||
default="de",
|
||||
description="Preferred language of the user (ISO 639-1 code: de, en, fr, it)",
|
||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [
|
||||
{"value": "de", "label": {"en": "Deutsch", "fr": "Allemand"}},
|
||||
{"value": "en", "label": {"en": "English", "fr": "Anglais"}},
|
||||
{"value": "fr", "label": {"en": "Français", "fr": "Français"}},
|
||||
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}},
|
||||
{"value": "de", "label": {"en": "Deutsch", "de": "Deutsch", "fr": "Allemand"}},
|
||||
{"value": "en", "label": {"en": "English", "de": "Englisch", "fr": "Anglais"}},
|
||||
{"value": "fr", "label": {"en": "Français", "de": "Französisch", "fr": "Français"}},
|
||||
{"value": "it", "label": {"en": "Italiano", "de": "Italienisch", "fr": "Italien"}},
|
||||
]}
|
||||
)
|
||||
|
||||
@field_validator('language', mode='before')
|
||||
@classmethod
|
||||
def _normalizeLanguage(cls, v):
|
||||
"""Normalize language to valid ISO 639-1 code."""
|
||||
if v is None:
|
||||
return "de"
|
||||
# Map common variations to standard codes
|
||||
langMap = {
|
||||
'english': 'en', 'englisch': 'en',
|
||||
'german': 'de', 'deutsch': 'de',
|
||||
'french': 'fr', 'französisch': 'fr', 'francais': 'fr',
|
||||
'italian': 'it', 'italienisch': 'it', 'italiano': 'it',
|
||||
}
|
||||
normalized = str(v).lower().strip()
|
||||
if normalized in langMap:
|
||||
return langMap[normalized]
|
||||
# If already a valid code, return as-is
|
||||
if normalized in ['de', 'en', 'fr', 'it']:
|
||||
return normalized
|
||||
# Default fallback
|
||||
return "de"
|
||||
|
||||
enabled: bool = Field(
|
||||
default=True,
|
||||
description="Indicates whether the user is enabled",
|
||||
|
|
|
|||
|
|
@ -282,9 +282,10 @@ class ChatObjects:
|
|||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
||||
# mandateId can be None for sysadmins performing cross-mandate operations
|
||||
if not self.mandateId and not getattr(currentUser, 'isSysAdmin', False):
|
||||
raise ValueError("Invalid user context: mandateId is required for non-sysadmin users")
|
||||
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
|
||||
# Users are NOT assigned to mandates by design - they get mandate context from the request.
|
||||
# sysAdmin users can additionally perform cross-mandate operations.
|
||||
# Without mandateId, operations will be filtered to accessible mandates via RBAC.
|
||||
|
||||
# Add language settings
|
||||
self.userLanguage = currentUser.language # Default user language
|
||||
|
|
|
|||
|
|
@ -126,9 +126,10 @@ class RealEstateObjects:
|
|||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
||||
# mandateId can be None for sysadmins performing cross-mandate operations
|
||||
if not self.mandateId and not getattr(currentUser, 'isSysAdmin', False):
|
||||
raise ValueError("Invalid user context: mandateId is required for non-sysadmin users")
|
||||
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
|
||||
# Users are NOT assigned to mandates by design - they get mandate context from the request.
|
||||
# sysAdmin users can additionally perform cross-mandate operations.
|
||||
# Without mandateId, operations will be filtered to accessible mandates via RBAC.
|
||||
|
||||
# Initialize RBAC interface
|
||||
if not self.currentUser:
|
||||
|
|
|
|||
|
|
@ -279,7 +279,12 @@ registerModelLabels(
|
|||
|
||||
|
||||
class TrusteeDocument(BaseModel):
|
||||
"""Contains document references and receipts for bookings."""
|
||||
"""Contains document references and receipts for bookings.
|
||||
|
||||
Note: organisationId and contractId removed as per architecture decision:
|
||||
- The feature instance IS the organisation
|
||||
- Contracts are eliminated from the model
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique document ID",
|
||||
|
|
@ -289,25 +294,6 @@ class TrusteeDocument(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/organisations/options"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/contracts/options",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
documentData: Optional[bytes] = Field(
|
||||
default=None,
|
||||
description="The file content (binary)",
|
||||
|
|
@ -332,30 +318,27 @@ class TrusteeDocument(BaseModel):
|
|||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": [
|
||||
{"value": "application/pdf", "label": {"en": "PDF", "fr": "PDF", "de": "PDF"}},
|
||||
{"value": "image/jpeg", "label": {"en": "JPEG", "fr": "JPEG", "de": "JPEG"}},
|
||||
{"value": "image/png", "label": {"en": "PNG", "fr": "PNG", "de": "PNG"}},
|
||||
{"value": "application/octet-stream", "label": {"en": "Other", "fr": "Autre", "de": "Andere"}},
|
||||
]
|
||||
"frontend_options": "/api/trustee/mime-types/options"
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
description="Mandate ID (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
|
@ -366,8 +349,6 @@ registerModelLabels(
|
|||
{"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"documentData": {"en": "Document Data", "fr": "Données du document", "de": "Dokumentdaten"},
|
||||
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
||||
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
||||
|
|
@ -378,7 +359,12 @@ registerModelLabels(
|
|||
|
||||
|
||||
class TrusteePosition(BaseModel):
|
||||
"""Contains booking positions (expense entries)."""
|
||||
"""Contains booking positions (expense entries).
|
||||
|
||||
Note: organisationId and contractId removed as per architecture decision:
|
||||
- The feature instance IS the organisation
|
||||
- Contracts are eliminated from the model
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique position ID",
|
||||
|
|
@ -388,25 +374,6 @@ class TrusteePosition(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/organisations/options"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/contracts/options",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
valuta: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Value date (ISO format: YYYY-MM-DD)",
|
||||
|
|
@ -520,20 +487,22 @@ class TrusteePosition(BaseModel):
|
|||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
description="Mandate ID (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
|
@ -544,8 +513,6 @@ registerModelLabels(
|
|||
{"en": "Position", "fr": "Position", "de": "Position"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
||||
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
||||
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
||||
|
|
@ -564,7 +531,12 @@ registerModelLabels(
|
|||
|
||||
|
||||
class TrusteePositionDocument(BaseModel):
|
||||
"""Cross-reference table linking positions to documents (many-to-many)."""
|
||||
"""Cross-reference table linking positions to documents (many-to-many).
|
||||
|
||||
Note: organisationId and contractId removed as per architecture decision:
|
||||
- The feature instance IS the organisation
|
||||
- Contracts are eliminated from the model
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique link ID",
|
||||
|
|
@ -574,33 +546,13 @@ class TrusteePositionDocument(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
organisationId: str = Field(
|
||||
description="Reference to TrusteeOrganisation.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/organisations/options"
|
||||
}
|
||||
)
|
||||
contractId: str = Field(
|
||||
description="Reference to TrusteeContract.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/contracts/options",
|
||||
"frontend_depends_on": "organisationId"
|
||||
}
|
||||
)
|
||||
documentId: str = Field(
|
||||
description="Reference to TrusteeDocument.id",
|
||||
json_schema_extra={
|
||||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/documents/options",
|
||||
"frontend_depends_on": "contractId"
|
||||
"frontend_options": "/api/trustee/{instanceId}/documents/options"
|
||||
}
|
||||
)
|
||||
positionId: str = Field(
|
||||
|
|
@ -609,26 +561,27 @@ class TrusteePositionDocument(BaseModel):
|
|||
"frontend_type": "select",
|
||||
"frontend_readonly": False,
|
||||
"frontend_required": True,
|
||||
"frontend_options": "/api/trustee/{instanceId}/positions/options",
|
||||
"frontend_depends_on": "contractId"
|
||||
"frontend_options": "/api/trustee/{instanceId}/positions/options"
|
||||
}
|
||||
)
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate ID",
|
||||
description="Mandate ID (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
"frontend_required": False,
|
||||
"frontend_hidden": True
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
|
@ -639,8 +592,6 @@ registerModelLabels(
|
|||
{"en": "Position-Document Link", "fr": "Lien Position-Document", "de": "Position-Dokument-Verknüpfung"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
||||
"contractId": {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
|
|
|
|||
|
|
@ -110,9 +110,10 @@ class TrusteeObjects:
|
|||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
||||
# mandateId can be None for sysadmins performing cross-mandate operations
|
||||
if not self.mandateId and not getattr(currentUser, 'isSysAdmin', False):
|
||||
raise ValueError("Invalid user context: mandateId is required for non-sysadmin users")
|
||||
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
|
||||
# Users are NOT assigned to mandates by design - they get mandate context from the request.
|
||||
# sysAdmin users can additionally perform cross-mandate operations.
|
||||
# Without mandateId, operations will be filtered to accessible mandates via RBAC.
|
||||
|
||||
self.userLanguage = currentUser.language
|
||||
|
||||
|
|
@ -734,18 +735,19 @@ class TrusteeObjects:
|
|||
# ===== Document CRUD =====
|
||||
|
||||
def createDocument(self, data: Dict[str, Any]) -> Optional[TrusteeDocument]:
|
||||
"""Create a new document."""
|
||||
organisationId = data.get("organisationId")
|
||||
contractId = data.get("contractId")
|
||||
"""Create a new document.
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "create", organisationId, contractId):
|
||||
logger.warning(f"User {self.userId} lacks permission to create document in org {organisationId}")
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
Permission is checked via system RBAC (feature-level access).
|
||||
"""
|
||||
# Check system RBAC permission
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create document")
|
||||
return None
|
||||
|
||||
# Auto-set context fields
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
documentId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -790,20 +792,22 @@ class TrusteeObjects:
|
|||
# This applies userreport filtering (only own records)
|
||||
records = self.filterRecordsByTrusteeAccess(records, TrusteeDocument)
|
||||
|
||||
# Remove binary data from responses
|
||||
# Convert dicts to Pydantic objects (remove binary data and internal fields)
|
||||
pydanticItems = []
|
||||
for record in records:
|
||||
record.pop("documentData", None)
|
||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_") and k != "documentData"}
|
||||
pydanticItems.append(TrusteeDocument(**cleanedRecord))
|
||||
|
||||
totalItems = len(records)
|
||||
totalItems = len(pydanticItems)
|
||||
if params:
|
||||
pageSize = params.pageSize or 20
|
||||
page = params.page or 1
|
||||
startIdx = (page - 1) * pageSize
|
||||
endIdx = startIdx + pageSize
|
||||
items = records[startIdx:endIdx]
|
||||
items = pydanticItems[startIdx:endIdx]
|
||||
totalPages = math.ceil(totalItems / pageSize) if pageSize > 0 else 1
|
||||
else:
|
||||
items = records
|
||||
items = pydanticItems
|
||||
totalPages = 1
|
||||
page = 1
|
||||
pageSize = totalItems
|
||||
|
|
@ -835,8 +839,11 @@ class TrusteeObjects:
|
|||
return result
|
||||
|
||||
def updateDocument(self, documentId: str, data: Dict[str, Any]) -> Optional[TrusteeDocument]:
|
||||
"""Update a document."""
|
||||
# Get existing document to check organisation and creator
|
||||
"""Update a document.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing document to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -844,14 +851,11 @@ class TrusteeObjects:
|
|||
logger.warning(f"Document {documentId} not found")
|
||||
return None
|
||||
|
||||
organisationId = existing.get("organisationId")
|
||||
contractId = existing.get("contractId")
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
# For userreport, this checks if they created the record
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "update", organisationId, contractId, createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to update document in org {organisationId}")
|
||||
# Check system RBAC permission (userreport can only edit their own records)
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "update", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to update document")
|
||||
return None
|
||||
|
||||
data["id"] = documentId
|
||||
|
|
@ -862,8 +866,11 @@ class TrusteeObjects:
|
|||
return TrusteeDocument(**cleanedRecord)
|
||||
|
||||
def deleteDocument(self, documentId: str) -> bool:
|
||||
"""Delete a document."""
|
||||
# Get existing document to check organisation and creator
|
||||
"""Delete a document.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing document to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -871,14 +878,11 @@ class TrusteeObjects:
|
|||
logger.warning(f"Document {documentId} not found")
|
||||
return False
|
||||
|
||||
organisationId = existing.get("organisationId")
|
||||
contractId = existing.get("contractId")
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
# For userreport, this checks if they created the record
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "delete", organisationId, contractId, createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete document in org {organisationId}")
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteeDocument, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete document")
|
||||
return False
|
||||
|
||||
return self.db.recordDelete(TrusteeDocument, documentId)
|
||||
|
|
@ -886,18 +890,19 @@ class TrusteeObjects:
|
|||
# ===== Position CRUD =====
|
||||
|
||||
def createPosition(self, data: Dict[str, Any]) -> Optional[TrusteePosition]:
|
||||
"""Create a new position."""
|
||||
organisationId = data.get("organisationId")
|
||||
contractId = data.get("contractId")
|
||||
"""Create a new position.
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
if not self.checkCombinedPermission(TrusteePosition, "create", organisationId, contractId):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position in org {organisationId}")
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
Permission is checked via system RBAC (feature-level access).
|
||||
"""
|
||||
# Check system RBAC permission
|
||||
if not self.checkCombinedPermission(TrusteePosition, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position")
|
||||
return None
|
||||
|
||||
# Auto-set context fields
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Calculate VAT amount if not provided
|
||||
if "vatAmount" not in data or data.get("vatAmount") == 0:
|
||||
|
|
@ -936,16 +941,22 @@ class TrusteeObjects:
|
|||
# This applies userreport filtering (only own records)
|
||||
records = self.filterRecordsByTrusteeAccess(records, TrusteePosition)
|
||||
|
||||
totalItems = len(records)
|
||||
# Convert dicts to Pydantic objects (remove internal fields)
|
||||
pydanticItems = []
|
||||
for record in records:
|
||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
|
||||
pydanticItems.append(TrusteePosition(**cleanedRecord))
|
||||
|
||||
totalItems = len(pydanticItems)
|
||||
if params:
|
||||
pageSize = params.pageSize or 20
|
||||
page = params.page or 1
|
||||
startIdx = (page - 1) * pageSize
|
||||
endIdx = startIdx + pageSize
|
||||
items = records[startIdx:endIdx]
|
||||
items = pydanticItems[startIdx:endIdx]
|
||||
totalPages = math.ceil(totalItems / pageSize) if pageSize > 0 else 1
|
||||
else:
|
||||
items = records
|
||||
items = pydanticItems
|
||||
totalPages = 1
|
||||
page = 1
|
||||
pageSize = totalItems
|
||||
|
|
@ -987,8 +998,11 @@ class TrusteeObjects:
|
|||
return [TrusteePosition(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in filtered]
|
||||
|
||||
def updatePosition(self, positionId: str, data: Dict[str, Any]) -> Optional[TrusteePosition]:
|
||||
"""Update a position."""
|
||||
# Get existing position to check organisation and creator
|
||||
"""Update a position.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing position to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -996,14 +1010,11 @@ class TrusteeObjects:
|
|||
logger.warning(f"Position {positionId} not found")
|
||||
return None
|
||||
|
||||
organisationId = existing.get("organisationId")
|
||||
contractId = existing.get("contractId")
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
# For userreport, this checks if they created the record
|
||||
if not self.checkCombinedPermission(TrusteePosition, "update", organisationId, contractId, createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to update position in org {organisationId}")
|
||||
# Check system RBAC permission (userreport can only edit their own records)
|
||||
if not self.checkCombinedPermission(TrusteePosition, "update", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to update position")
|
||||
return None
|
||||
|
||||
data["id"] = positionId
|
||||
|
|
@ -1013,8 +1024,11 @@ class TrusteeObjects:
|
|||
return TrusteePosition(**{k: v for k, v in updatedRecord.items() if not k.startswith("_")})
|
||||
|
||||
def deletePosition(self, positionId: str) -> bool:
|
||||
"""Delete a position."""
|
||||
# Get existing position to check organisation and creator
|
||||
"""Delete a position.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing position to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -1022,14 +1036,11 @@ class TrusteeObjects:
|
|||
logger.warning(f"Position {positionId} not found")
|
||||
return False
|
||||
|
||||
organisationId = existing.get("organisationId")
|
||||
contractId = existing.get("contractId")
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
# For userreport, this checks if they created the record
|
||||
if not self.checkCombinedPermission(TrusteePosition, "delete", organisationId, contractId, createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position in org {organisationId}")
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteePosition, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position")
|
||||
return False
|
||||
|
||||
return self.db.recordDelete(TrusteePosition, positionId)
|
||||
|
|
@ -1037,18 +1048,19 @@ class TrusteeObjects:
|
|||
# ===== Position-Document Link CRUD =====
|
||||
|
||||
def createPositionDocument(self, data: Dict[str, Any]) -> Optional[TrusteePositionDocument]:
|
||||
"""Create a new position-document link."""
|
||||
organisationId = data.get("organisationId")
|
||||
contractId = data.get("contractId")
|
||||
"""Create a new position-document link.
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "create", organisationId, contractId):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position-document link in org {organisationId}")
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
Permission is checked via system RBAC (feature-level access).
|
||||
"""
|
||||
# Check system RBAC permission
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "create"):
|
||||
logger.warning(f"User {self.userId} lacks permission to create position-document link")
|
||||
return None
|
||||
|
||||
# Auto-set context fields
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
linkId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -1132,8 +1144,11 @@ class TrusteeObjects:
|
|||
return [TrusteePositionDocument(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in filtered]
|
||||
|
||||
def deletePositionDocument(self, linkId: str) -> bool:
|
||||
"""Delete a position-document link."""
|
||||
# Get existing link to check organisation and creator
|
||||
"""Delete a position-document link.
|
||||
|
||||
Note: organisationId and contractId removed - feature instance IS the organisation.
|
||||
"""
|
||||
# Get existing link to check creator
|
||||
existingRecords = self.db.getRecordset(TrusteePositionDocument, recordFilter={"id": linkId})
|
||||
existing = existingRecords[0] if existingRecords else None
|
||||
|
||||
|
|
@ -1141,14 +1156,11 @@ class TrusteeObjects:
|
|||
logger.warning(f"Position-document link {linkId} not found")
|
||||
return False
|
||||
|
||||
organisationId = existing.get("organisationId")
|
||||
contractId = existing.get("contractId")
|
||||
createdBy = existing.get("_createdBy")
|
||||
|
||||
# Check combined permission (system RBAC + feature-level)
|
||||
# For userreport, this checks if they created the record
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "delete", organisationId, contractId, createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position-document link in org {organisationId}")
|
||||
# Check system RBAC permission (userreport can only delete their own records)
|
||||
if not self.checkCombinedPermission(TrusteePositionDocument, "delete", recordCreatedBy=createdBy):
|
||||
logger.warning(f"User {self.userId} lacks permission to delete position-document link")
|
||||
return False
|
||||
|
||||
return self.db.recordDelete(TrusteePositionDocument, linkId)
|
||||
|
|
@ -1317,6 +1329,15 @@ class TrusteeObjects:
|
|||
if accessLevel == AccessLevel.ALL:
|
||||
return records
|
||||
|
||||
# NEW: Feature-instance based access (new system)
|
||||
# If featureInstanceId is set, user has access via FeatureAccess system.
|
||||
# Data is already filtered by featureInstanceId in getRecordsetWithRBAC.
|
||||
# The old TrusteeAccess system (organisation-based) is not used for
|
||||
# feature-instance scoped data.
|
||||
if self.featureInstanceId:
|
||||
return records # User already has access to this feature instance
|
||||
|
||||
# LEGACY: TrusteeAccess based filtering (for backwards compatibility)
|
||||
# Get all user's access records
|
||||
userAccess = self.getAllUserAccess(self.userId)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ URL Structure: /api/trustee/{instanceId}/{entity}
|
|||
- This ensures proper multi-tenant isolation at the URL level
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response, UploadFile, File, Form
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import status
|
||||
import logging
|
||||
import json
|
||||
import io
|
||||
import base64
|
||||
|
||||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
from .interfaceFeatureTrustee import getInterface
|
||||
|
|
@ -133,7 +134,7 @@ _TRUSTEE_ENTITY_MODELS = {
|
|||
|
||||
@router.get("/{instanceId}/attributes/{entityType}")
|
||||
@limiter.limit("30/minute")
|
||||
async def getEntityAttributes(
|
||||
async def get_entity_attributes(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
entityType: str = Path(..., description="Entity type (e.g., TrusteeDocument)"),
|
||||
|
|
@ -179,9 +180,44 @@ async def getEntityAttributes(
|
|||
# OPTIONS ENDPOINTS (for dropdowns)
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/mime-types/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def get_mime_type_options(
|
||||
request: Request,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get supported MIME types from the document extraction service.
|
||||
Returns: [{ value: "mime/type", label: "Description" }]
|
||||
"""
|
||||
from modules.services.serviceExtraction.subRegistry import ExtractorRegistry
|
||||
|
||||
registry = ExtractorRegistry()
|
||||
formats = registry.getSupportedFormats()
|
||||
|
||||
# Collect all unique MIME types
|
||||
allMimeTypes = set()
|
||||
for mimeList in formats.get("mime_types", {}).values():
|
||||
allMimeTypes.update(mimeList)
|
||||
|
||||
# Sort and create options with labels
|
||||
result = []
|
||||
for mimeType in sorted(allMimeTypes):
|
||||
# Create readable label from mime type
|
||||
parts = mimeType.split("/")
|
||||
if len(parts) == 2:
|
||||
mainType, subType = parts
|
||||
# Clean up subtype for label
|
||||
label = subType.replace("vnd.", "").replace("x-", "").replace("-", " ").title()
|
||||
result.append({"value": mimeType, "label": f"{label} ({mimeType})"})
|
||||
else:
|
||||
result.append({"value": mimeType, "label": mimeType})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{instanceId}/organisations/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getOrganisationOptions(
|
||||
async def get_organisation_options(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -196,7 +232,7 @@ async def getOrganisationOptions(
|
|||
|
||||
@router.get("/{instanceId}/roles/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getRoleOptions(
|
||||
async def get_role_options(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -211,7 +247,7 @@ async def getRoleOptions(
|
|||
|
||||
@router.get("/{instanceId}/contracts/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getContractOptions(
|
||||
async def get_contract_options(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
organisationId: Optional[str] = Query(None, description="Optional: Filter by organisation ID"),
|
||||
|
|
@ -241,7 +277,7 @@ async def getContractOptions(
|
|||
|
||||
@router.get("/{instanceId}/documents/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getDocumentOptions(
|
||||
async def get_document_options(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -256,7 +292,7 @@ async def getDocumentOptions(
|
|||
|
||||
@router.get("/{instanceId}/positions/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getPositionOptions(
|
||||
async def get_position_options(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -266,8 +302,8 @@ async def getPositionOptions(
|
|||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllPositions(None)
|
||||
items = result.items if hasattr(result, 'items') else result
|
||||
# Erstelle Label aus Datum, Firma und Beschreibung
|
||||
def _makePositionLabel(p):
|
||||
|
||||
def _makePositionLabel(p: TrusteePosition) -> str:
|
||||
parts = []
|
||||
if p.valuta:
|
||||
parts.append(str(p.valuta)[:10]) # Datum ohne Zeit
|
||||
|
|
@ -276,6 +312,7 @@ async def getPositionOptions(
|
|||
if p.desc:
|
||||
parts.append(p.desc[:30])
|
||||
return " - ".join(parts) if parts else p.id
|
||||
|
||||
return [{"value": p.id, "label": _makePositionLabel(p)} for p in items]
|
||||
|
||||
|
||||
|
|
@ -287,7 +324,7 @@ async def getPositionOptions(
|
|||
|
||||
@router.get("/{instanceId}/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisations(
|
||||
async def get_organisations(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||
|
|
@ -317,7 +354,7 @@ async def getOrganisations(
|
|||
|
||||
@router.get("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisation(
|
||||
async def get_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
|
|
@ -335,7 +372,7 @@ async def getOrganisation(
|
|||
|
||||
@router.post("/{instanceId}/organisations", response_model=TrusteeOrganisation, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createOrganisation(
|
||||
async def create_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeOrganisation = Body(...),
|
||||
|
|
@ -353,7 +390,7 @@ async def createOrganisation(
|
|||
|
||||
@router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateOrganisation(
|
||||
async def update_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
|
|
@ -376,7 +413,7 @@ async def updateOrganisation(
|
|||
|
||||
@router.delete("/{instanceId}/organisations/{orgId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteOrganisation(
|
||||
async def delete_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
|
|
@ -400,7 +437,7 @@ async def deleteOrganisation(
|
|||
|
||||
@router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole])
|
||||
@limiter.limit("30/minute")
|
||||
async def getRoles(
|
||||
async def get_roles(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -430,7 +467,7 @@ async def getRoles(
|
|||
|
||||
@router.get("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("30/minute")
|
||||
async def getRole(
|
||||
async def get_role(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
@ -448,7 +485,7 @@ async def getRole(
|
|||
|
||||
@router.post("/{instanceId}/roles", response_model=TrusteeRole, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createRole(
|
||||
async def create_role(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeRole = Body(...),
|
||||
|
|
@ -466,7 +503,7 @@ async def createRole(
|
|||
|
||||
@router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateRole(
|
||||
async def update_role(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(...),
|
||||
|
|
@ -489,7 +526,7 @@ async def updateRole(
|
|||
|
||||
@router.delete("/{instanceId}/roles/{roleId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteRole(
|
||||
async def delete_role(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(...),
|
||||
|
|
@ -513,7 +550,7 @@ async def deleteRole(
|
|||
|
||||
@router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAllAccess(
|
||||
async def get_all_access(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -543,7 +580,7 @@ async def getAllAccess(
|
|||
|
||||
@router.get("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccess(
|
||||
async def get_access(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
|
|
@ -561,7 +598,7 @@ async def getAccess(
|
|||
|
||||
@router.get("/{instanceId}/access/organisation/{orgId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByOrganisation(
|
||||
async def get_access_by_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
|
|
@ -576,7 +613,7 @@ async def getAccessByOrganisation(
|
|||
|
||||
@router.get("/{instanceId}/access/user/{userId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByUser(
|
||||
async def get_access_by_user(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
userId: str = Path(...),
|
||||
|
|
@ -591,7 +628,7 @@ async def getAccessByUser(
|
|||
|
||||
@router.post("/{instanceId}/access", response_model=TrusteeAccess, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createAccess(
|
||||
async def create_access(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeAccess = Body(...),
|
||||
|
|
@ -609,7 +646,7 @@ async def createAccess(
|
|||
|
||||
@router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateAccess(
|
||||
async def update_access(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
|
|
@ -632,7 +669,7 @@ async def updateAccess(
|
|||
|
||||
@router.delete("/{instanceId}/access/{accessId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteAccess(
|
||||
async def delete_access(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
|
|
@ -656,7 +693,7 @@ async def deleteAccess(
|
|||
|
||||
@router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContracts(
|
||||
async def get_contracts(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -686,7 +723,7 @@ async def getContracts(
|
|||
|
||||
@router.get("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("30/minute")
|
||||
async def getContract(
|
||||
async def get_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
|
|
@ -704,7 +741,7 @@ async def getContract(
|
|||
|
||||
@router.get("/{instanceId}/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContractsByOrganisation(
|
||||
async def get_contracts_by_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
|
|
@ -719,7 +756,7 @@ async def getContractsByOrganisation(
|
|||
|
||||
@router.post("/{instanceId}/contracts", response_model=TrusteeContract, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createContract(
|
||||
async def create_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeContract = Body(...),
|
||||
|
|
@ -737,7 +774,7 @@ async def createContract(
|
|||
|
||||
@router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateContract(
|
||||
async def update_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
|
|
@ -760,7 +797,7 @@ async def updateContract(
|
|||
|
||||
@router.delete("/{instanceId}/contracts/{contractId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteContract(
|
||||
async def delete_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
|
|
@ -784,7 +821,7 @@ async def deleteContract(
|
|||
|
||||
@router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocuments(
|
||||
async def get_documents(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -814,7 +851,7 @@ async def getDocuments(
|
|||
|
||||
@router.get("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocument(
|
||||
async def get_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
|
|
@ -832,7 +869,7 @@ async def getDocument(
|
|||
|
||||
@router.get("/{instanceId}/documents/{documentId}/data")
|
||||
@limiter.limit("10/minute")
|
||||
async def getDocumentData(
|
||||
async def get_document_data(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
|
|
@ -859,7 +896,7 @@ async def getDocumentData(
|
|||
|
||||
@router.get("/{instanceId}/documents/contract/{contractId}", response_model=List[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsByContract(
|
||||
async def get_documents_by_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
|
|
@ -874,17 +911,66 @@ async def getDocumentsByContract(
|
|||
|
||||
@router.post("/{instanceId}/documents", response_model=TrusteeDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createDocument(
|
||||
async def create_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeDocument = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeDocument:
|
||||
"""Create a new document."""
|
||||
"""Create a new document. Accepts JSON body with optional base64-encoded documentData."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
# Parse JSON body
|
||||
body = await request.json()
|
||||
|
||||
# Handle documentData: convert base64 string to bytes if present
|
||||
if "documentData" in body and body["documentData"]:
|
||||
dataValue = body["documentData"]
|
||||
if isinstance(dataValue, str):
|
||||
# Base64-encoded data from frontend
|
||||
try:
|
||||
body["documentData"] = base64.b64decode(dataValue)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to decode base64 documentData: {e}")
|
||||
body["documentData"] = None
|
||||
elif isinstance(dataValue, bytes):
|
||||
# Already bytes
|
||||
pass
|
||||
else:
|
||||
# Unknown format (e.g., File object serialized wrong)
|
||||
body["documentData"] = None
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createDocument(data.model_dump())
|
||||
result = interface.createDocument(body)
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{instanceId}/documents/upload", response_model=TrusteeDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def upload_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
file: UploadFile = File(..., description="Document file"),
|
||||
documentName: str = Form(..., description="Document name"),
|
||||
documentMimeType: str = Form(default="application/octet-stream", description="MIME type"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeDocument:
|
||||
"""Upload a document with multipart/form-data."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
# Read file content
|
||||
fileContent = await file.read()
|
||||
|
||||
# Build document data
|
||||
docData = {
|
||||
"documentName": documentName,
|
||||
"documentMimeType": documentMimeType or file.content_type or "application/octet-stream",
|
||||
"documentData": fileContent
|
||||
}
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createDocument(docData)
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
||||
return result
|
||||
|
|
@ -892,7 +978,7 @@ async def createDocument(
|
|||
|
||||
@router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateDocument(
|
||||
async def update_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
|
|
@ -915,7 +1001,7 @@ async def updateDocument(
|
|||
|
||||
@router.delete("/{instanceId}/documents/{documentId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteDocument(
|
||||
async def delete_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
|
|
@ -939,7 +1025,7 @@ async def deleteDocument(
|
|||
|
||||
@router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositions(
|
||||
async def get_positions(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -969,7 +1055,7 @@ async def getPositions(
|
|||
|
||||
@router.get("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPosition(
|
||||
async def get_position(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
|
|
@ -987,7 +1073,7 @@ async def getPosition(
|
|||
|
||||
@router.get("/{instanceId}/positions/contract/{contractId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByContract(
|
||||
async def get_positions_by_contract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
|
|
@ -1002,7 +1088,7 @@ async def getPositionsByContract(
|
|||
|
||||
@router.get("/{instanceId}/positions/organisation/{orgId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByOrganisation(
|
||||
async def get_positions_by_organisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
|
|
@ -1017,7 +1103,7 @@ async def getPositionsByOrganisation(
|
|||
|
||||
@router.post("/{instanceId}/positions", response_model=TrusteePosition, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPosition(
|
||||
async def create_position(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteePosition = Body(...),
|
||||
|
|
@ -1035,7 +1121,7 @@ async def createPosition(
|
|||
|
||||
@router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("10/minute")
|
||||
async def updatePosition(
|
||||
async def update_position(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
|
|
@ -1058,7 +1144,7 @@ async def updatePosition(
|
|||
|
||||
@router.delete("/{instanceId}/positions/{positionId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePosition(
|
||||
async def delete_position(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
|
|
@ -1082,7 +1168,7 @@ async def deletePosition(
|
|||
|
||||
@router.get("/{instanceId}/position-documents", response_model=PaginatedResponse[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocuments(
|
||||
async def get_position_documents(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
|
|
@ -1112,7 +1198,7 @@ async def getPositionDocuments(
|
|||
|
||||
@router.get("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocument(
|
||||
async def get_position_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
linkId: str = Path(...),
|
||||
|
|
@ -1130,7 +1216,7 @@ async def getPositionDocument(
|
|||
|
||||
@router.get("/{instanceId}/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsForPosition(
|
||||
async def get_documents_for_position(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
|
|
@ -1145,7 +1231,7 @@ async def getDocumentsForPosition(
|
|||
|
||||
@router.get("/{instanceId}/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsForDocument(
|
||||
async def get_positions_for_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
|
|
@ -1160,7 +1246,7 @@ async def getPositionsForDocument(
|
|||
|
||||
@router.post("/{instanceId}/position-documents", response_model=TrusteePositionDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPositionDocument(
|
||||
async def create_position_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteePositionDocument = Body(...),
|
||||
|
|
@ -1178,7 +1264,7 @@ async def createPositionDocument(
|
|||
|
||||
@router.delete("/{instanceId}/position-documents/{linkId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePositionDocument(
|
||||
async def delete_position_document(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
linkId: str = Path(...),
|
||||
|
|
@ -1240,7 +1326,7 @@ async def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> st
|
|||
|
||||
@router.get("/{instanceId}/instance-roles", response_model=PaginatedResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def getInstanceRoles(
|
||||
async def get_instance_roles(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -1270,7 +1356,7 @@ async def getInstanceRoles(
|
|||
|
||||
@router.get("/{instanceId}/instance-roles/{roleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def getInstanceRole(
|
||||
async def get_instance_role(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
@ -1296,7 +1382,7 @@ async def getInstanceRole(
|
|||
|
||||
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def getInstanceRoleRules(
|
||||
async def get_instance_role_rules(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
@ -1329,7 +1415,7 @@ async def getInstanceRoleRules(
|
|||
|
||||
@router.post("/{instanceId}/instance-roles/{roleId}/rules", response_model=Dict[str, Any], status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createInstanceRoleRule(
|
||||
async def create_instance_role_rule(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
@ -1378,7 +1464,7 @@ async def createInstanceRoleRule(
|
|||
|
||||
@router.put("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def updateInstanceRoleRule(
|
||||
async def update_instance_role_rule(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
@ -1431,7 +1517,7 @@ async def updateInstanceRoleRule(
|
|||
|
||||
@router.delete("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteInstanceRoleRule(
|
||||
async def delete_instance_role_rule(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
|
|
|
|||
|
|
@ -107,11 +107,9 @@ class AppObjects:
|
|||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
||||
# mandateId is optional for isSysAdmin users doing system-level operations
|
||||
isSysAdmin = getattr(currentUser, 'isSysAdmin', False)
|
||||
if not self.mandateId and not isSysAdmin:
|
||||
# Non-sysadmin users MUST have a mandateId for tenant-scoped operations
|
||||
logger.warning(f"User {self.userId} has no mandateId context")
|
||||
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
|
||||
# Users are NOT assigned to mandates by design - they get mandate context from the request.
|
||||
# sysAdmin users can additionally perform cross-mandate operations.
|
||||
|
||||
# Add language settings
|
||||
self.userLanguage = currentUser.language # Default user language
|
||||
|
|
@ -599,27 +597,52 @@ class AppObjects:
|
|||
logger.error(f"Error getting user by ID: {str(e)}")
|
||||
return None
|
||||
|
||||
def _getUserForAuthentication(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get user record by username for authentication purposes.
|
||||
|
||||
SECURITY NOTE: This method bypasses RBAC intentionally because:
|
||||
1. Users are NOT mandate-bound (Multi-Tenant Design)
|
||||
2. Authentication must work regardless of mandate context
|
||||
3. RBAC filtering for User table requires mandate context which doesn't exist at login time
|
||||
|
||||
This method should ONLY be used for authentication flows.
|
||||
For all other user queries, use getUserByUsername() which applies RBAC.
|
||||
|
||||
Returns:
|
||||
Full UserInDB record as dict, or None if not found
|
||||
"""
|
||||
try:
|
||||
users = self.db.getRecordset(UserInDB, recordFilter={"username": username})
|
||||
if not users:
|
||||
return None
|
||||
return users[0]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user for authentication: {str(e)}")
|
||||
return None
|
||||
|
||||
def authenticateLocalUser(self, username: str, password: str) -> Optional[User]:
|
||||
"""Authenticates a user by username and password using local authentication."""
|
||||
# Clear the users table from cache and reload it
|
||||
"""
|
||||
Authenticates a user by username and password using local authentication.
|
||||
|
||||
# Get user by username
|
||||
user = self.getUserByUsername(username)
|
||||
SECURITY NOTE: Uses _getUserForAuthentication() which bypasses RBAC.
|
||||
This is intentional because users are mandate-independent.
|
||||
"""
|
||||
# Get full user record directly (bypasses RBAC - see _getUserForAuthentication docstring)
|
||||
userRecord = self._getUserForAuthentication(username)
|
||||
|
||||
if not user:
|
||||
if not userRecord:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Check if the user is enabled
|
||||
if not user.enabled:
|
||||
if not userRecord.get("enabled", True):
|
||||
raise ValueError("User is disabled")
|
||||
|
||||
# Verify that the user has local authentication enabled
|
||||
if user.authenticationAuthority != AuthAuthority.LOCAL:
|
||||
authAuthority = userRecord.get("authenticationAuthority", AuthAuthority.LOCAL)
|
||||
if authAuthority != AuthAuthority.LOCAL and authAuthority != AuthAuthority.LOCAL.value:
|
||||
raise ValueError("User does not have local authentication enabled")
|
||||
|
||||
# Get the full user record with password hash for verification
|
||||
userRecord = self.db.getRecordset(UserInDB, recordFilter={"id": user.id})[0]
|
||||
|
||||
# Check if user has a reset token set (password reset required)
|
||||
if userRecord.get("resetToken"):
|
||||
raise ValueError("Passwort-Zurücksetzung erforderlich. Bitte prüfen Sie Ihre E-Mail.")
|
||||
|
|
@ -630,7 +653,12 @@ class AppObjects:
|
|||
if not self._verifyPassword(password, userRecord["hashedPassword"]):
|
||||
raise ValueError("Invalid password")
|
||||
|
||||
return user
|
||||
# Return clean User object (without password hash and internal fields)
|
||||
cleanedUser = {k: v for k, v in userRecord.items() if not k.startswith("_") and k != "hashedPassword" and k != "resetToken" and k != "resetTokenExpires"}
|
||||
# Ensure roleLabels is always a list
|
||||
if cleanedUser.get("roleLabels") is None:
|
||||
cleanedUser["roleLabels"] = []
|
||||
return User(**cleanedUser)
|
||||
|
||||
def createUser(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -282,9 +282,10 @@ class ChatObjects:
|
|||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
||||
# mandateId can be None for sysadmins performing cross-mandate operations
|
||||
if not self.mandateId and not getattr(currentUser, 'isSysAdmin', False):
|
||||
raise ValueError("Invalid user context: mandateId is required for non-sysadmin users")
|
||||
# Note: mandateId is ALWAYS optional here - it comes from Request-Context, not from User.
|
||||
# Users are NOT assigned to mandates by design - they get mandate context from the request.
|
||||
# sysAdmin users can additionally perform cross-mandate operations.
|
||||
# Without mandateId, operations will be filtered to accessible mandates via RBAC.
|
||||
|
||||
# Add language settings
|
||||
self.userLanguage = currentUser.language # Default user language
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ class FeatureInterface:
|
|||
featureCode: str,
|
||||
mandateId: str,
|
||||
label: str,
|
||||
enabled: bool = True,
|
||||
copyTemplateRoles: bool = True
|
||||
) -> FeatureInstance:
|
||||
"""
|
||||
|
|
@ -171,6 +172,7 @@ class FeatureInterface:
|
|||
featureCode: Feature code (e.g., "trustee")
|
||||
mandateId: Mandate ID
|
||||
label: Instance label (e.g., "Buchhaltung 2025")
|
||||
enabled: Whether the instance is enabled
|
||||
copyTemplateRoles: Whether to copy template roles
|
||||
|
||||
Returns:
|
||||
|
|
@ -182,7 +184,7 @@ class FeatureInterface:
|
|||
featureCode=featureCode,
|
||||
mandateId=mandateId,
|
||||
label=label,
|
||||
enabled=True
|
||||
enabled=enabled
|
||||
)
|
||||
createdInstance = self.db.recordCreate(FeatureInstance, instance.model_dump())
|
||||
|
||||
|
|
@ -382,6 +384,41 @@ class FeatureInterface:
|
|||
logger.error(f"Error syncing roles from template: {e}")
|
||||
raise ValueError(f"Failed to sync roles: {e}")
|
||||
|
||||
def updateFeatureInstance(self, instanceId: str, updateData: Dict[str, Any]) -> Optional[FeatureInstance]:
|
||||
"""
|
||||
Update a feature instance.
|
||||
|
||||
Only label and enabled fields can be updated.
|
||||
featureCode and mandateId are immutable.
|
||||
|
||||
Args:
|
||||
instanceId: FeatureInstance ID
|
||||
updateData: Dictionary with fields to update (label, enabled)
|
||||
|
||||
Returns:
|
||||
Updated FeatureInstance object or None if not found
|
||||
"""
|
||||
try:
|
||||
instance = self.getFeatureInstance(instanceId)
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
# Only allow updating specific fields
|
||||
allowedFields = {"label", "enabled"}
|
||||
filteredData = {k: v for k, v in updateData.items() if k in allowedFields}
|
||||
|
||||
if not filteredData:
|
||||
return instance
|
||||
|
||||
updated = self.db.recordModify(FeatureInstance, instanceId, filteredData)
|
||||
if updated:
|
||||
cleanedRecord = {k: v for k, v in updated.items() if not k.startswith("_")}
|
||||
return FeatureInstance(**cleanedRecord)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating feature instance {instanceId}: {e}")
|
||||
raise ValueError(f"Failed to update feature instance: {e}")
|
||||
|
||||
def deleteFeatureInstance(self, instanceId: str) -> bool:
|
||||
"""
|
||||
Delete a feature instance.
|
||||
|
|
|
|||
|
|
@ -239,23 +239,40 @@ def buildRbacWhereClause(
|
|||
logger.warning(f"User {currentUser.id} has no mandateId for GROUP access")
|
||||
return {"condition": "1 = 0", "values": []}
|
||||
|
||||
# For UserInDB, filter by mandateId directly
|
||||
# For UserInDB: Filter via UserMandate junction table
|
||||
# Multi-Tenant Design: Users do NOT have mandateId - they are linked via UserMandate
|
||||
if table == "UserInDB":
|
||||
return {
|
||||
"condition": '"mandateId" = %s',
|
||||
"values": [effectiveMandateId]
|
||||
}
|
||||
# For UserConnection, need to join with UserInDB or filter by mandateId in user
|
||||
elif table == "UserConnection":
|
||||
# Get all user IDs in the same mandate using direct SQL query
|
||||
try:
|
||||
with connector.connection.cursor() as cursor:
|
||||
# Get all user IDs that are members of the current mandate
|
||||
cursor.execute(
|
||||
'SELECT "id" FROM "UserInDB" WHERE "mandateId" = %s',
|
||||
'SELECT "userId" FROM "UserMandate" WHERE "mandateId" = %s AND "enabled" = true',
|
||||
(effectiveMandateId,)
|
||||
)
|
||||
users = cursor.fetchall()
|
||||
userIds = [u["id"] for u in users]
|
||||
userMandates = cursor.fetchall()
|
||||
userIds = [um["userId"] for um in userMandates]
|
||||
if not userIds:
|
||||
return {"condition": "1 = 0", "values": []}
|
||||
placeholders = ",".join(["%s"] * len(userIds))
|
||||
return {
|
||||
"condition": f'"id" IN ({placeholders})',
|
||||
"values": userIds
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error building GROUP filter for UserInDB via UserMandate: {e}")
|
||||
return {"condition": "1 = 0", "values": []}
|
||||
|
||||
# For UserConnection: Filter via UserMandate junction table
|
||||
elif table == "UserConnection":
|
||||
try:
|
||||
with connector.connection.cursor() as cursor:
|
||||
# Get all user IDs that are members of the current mandate
|
||||
cursor.execute(
|
||||
'SELECT "userId" FROM "UserMandate" WHERE "mandateId" = %s AND "enabled" = true',
|
||||
(effectiveMandateId,)
|
||||
)
|
||||
userMandates = cursor.fetchall()
|
||||
userIds = [um["userId"] for um in userMandates]
|
||||
if not userIds:
|
||||
return {"condition": "1 = 0", "values": []}
|
||||
placeholders = ",".join(["%s"] * len(userIds))
|
||||
|
|
@ -266,7 +283,8 @@ def buildRbacWhereClause(
|
|||
except Exception as e:
|
||||
logger.error(f"Error building GROUP filter for UserConnection: {e}")
|
||||
return {"condition": "1 = 0", "values": []}
|
||||
# For other tables, filter by mandateId
|
||||
|
||||
# For other tables, filter by mandateId field
|
||||
else:
|
||||
return {
|
||||
"condition": '"mandateId" = %s',
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class FeatureInstanceCreate(BaseModel):
|
|||
"""Request model for creating a feature instance"""
|
||||
featureCode: str = Field(..., description="Feature code (e.g., 'trustee', 'chatbot')")
|
||||
label: str = Field(..., description="Instance label (e.g., 'Buchhaltung 2025')")
|
||||
enabled: bool = Field(True, description="Whether this feature instance is enabled")
|
||||
copyTemplateRoles: bool = Field(True, description="Whether to copy template roles on creation")
|
||||
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ class SyncRolesResult(BaseModel):
|
|||
|
||||
@router.get("/", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listFeatures(
|
||||
async def list_features(
|
||||
request: Request,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -101,7 +102,7 @@ class FeaturesMyResponse(BaseModel):
|
|||
|
||||
@router.get("/my", response_model=FeaturesMyResponse)
|
||||
@limiter.limit("60/minute")
|
||||
async def getMyFeatureInstances(
|
||||
async def get_my_feature_instances(
|
||||
request: Request,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> FeaturesMyResponse:
|
||||
|
|
@ -239,11 +240,12 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
permissions = {
|
||||
"tables": {},
|
||||
"views": {},
|
||||
"fields": {}
|
||||
"fields": {},
|
||||
"isAdmin": False # Flag if user has admin role
|
||||
}
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
|
||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
||||
|
||||
# Get FeatureAccess for this user and instance
|
||||
|
|
@ -272,6 +274,15 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
|
||||
return permissions
|
||||
|
||||
# Check if user has admin role
|
||||
for roleId in roleIds:
|
||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
||||
if roles:
|
||||
roleLabel = roles[0].get("roleLabel", "").lower()
|
||||
if "admin" in roleLabel:
|
||||
permissions["isAdmin"] = True
|
||||
break
|
||||
|
||||
# Get permissions (AccessRules) for all roles
|
||||
for roleId in roleIds:
|
||||
accessRules = rootInterface.db.getRecordset(
|
||||
|
|
@ -323,6 +334,10 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
# item=None means all views - set a wildcard flag
|
||||
permissions["views"]["_all"] = True
|
||||
|
||||
# Derive view permissions from table permissions
|
||||
# This allows UI navigation to be controlled by data access rights
|
||||
_deriveViewPermissions(permissions)
|
||||
|
||||
return permissions
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -330,6 +345,51 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
|||
return permissions
|
||||
|
||||
|
||||
def _deriveViewPermissions(permissions: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Derive UI view permissions from table/data permissions.
|
||||
|
||||
Mapping:
|
||||
- trustee-dashboard: always visible (basic access)
|
||||
- trustee-positions: visible if READ on TrusteePosition
|
||||
- trustee-documents: visible if READ on TrusteeDocument
|
||||
- trustee-position-documents: visible if READ on TrusteePositionDocument
|
||||
- trustee-instance-roles: visible only for admin roles
|
||||
|
||||
This function modifies permissions["views"] in place.
|
||||
"""
|
||||
tables = permissions.get("tables", {})
|
||||
views = permissions.get("views", {})
|
||||
isAdmin = permissions.get("isAdmin", False)
|
||||
|
||||
# If user has _all views permission, skip derivation
|
||||
if views.get("_all"):
|
||||
return
|
||||
|
||||
# Dashboard is always visible for users with any access
|
||||
if "trustee-dashboard" not in views:
|
||||
views["trustee-dashboard"] = True
|
||||
|
||||
# Positions view: requires READ on TrusteePosition
|
||||
if "trustee-positions" not in views:
|
||||
positionPerms = tables.get("TrusteePosition", {})
|
||||
views["trustee-positions"] = positionPerms.get("read", "n") != "n"
|
||||
|
||||
# Documents view: requires READ on TrusteeDocument
|
||||
if "trustee-documents" not in views:
|
||||
documentPerms = tables.get("TrusteeDocument", {})
|
||||
views["trustee-documents"] = documentPerms.get("read", "n") != "n"
|
||||
|
||||
# Position-Documents view: requires READ on TrusteePositionDocument
|
||||
if "trustee-position-documents" not in views:
|
||||
linkPerms = tables.get("TrusteePositionDocument", {})
|
||||
views["trustee-position-documents"] = linkPerms.get("read", "n") != "n"
|
||||
|
||||
# Instance-roles (admin) view: requires admin role
|
||||
if "trustee-instance-roles" not in views:
|
||||
views["trustee-instance-roles"] = isAdmin
|
||||
|
||||
|
||||
def _mergeAccessLevel(current: str, new: str) -> str:
|
||||
"""Merge two access levels, returning the highest."""
|
||||
levels = {"n": 0, "m": 1, "g": 2, "a": 3}
|
||||
|
|
@ -343,7 +403,7 @@ def _mergeAccessLevel(current: str, new: str) -> str:
|
|||
|
||||
@router.post("/", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def createFeature(
|
||||
async def create_feature(
|
||||
request: Request,
|
||||
code: str = Query(..., description="Unique feature code"),
|
||||
label: Dict[str, str] = None,
|
||||
|
|
@ -397,7 +457,7 @@ async def createFeature(
|
|||
|
||||
@router.get("/instances", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listFeatureInstances(
|
||||
async def list_feature_instances(
|
||||
request: Request,
|
||||
featureCode: Optional[str] = Query(None, description="Filter by feature code"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -439,7 +499,7 @@ async def listFeatureInstances(
|
|||
|
||||
@router.get("/instances/{instanceId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def getFeatureInstance(
|
||||
async def get_feature_instance(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -483,7 +543,7 @@ async def getFeatureInstance(
|
|||
|
||||
@router.post("/instances", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def createFeatureInstance(
|
||||
async def create_feature_instance(
|
||||
request: Request,
|
||||
data: FeatureInstanceCreate,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -525,6 +585,7 @@ async def createFeatureInstance(
|
|||
featureCode=data.featureCode,
|
||||
mandateId=str(context.mandateId),
|
||||
label=data.label,
|
||||
enabled=data.enabled,
|
||||
copyTemplateRoles=data.copyTemplateRoles
|
||||
)
|
||||
|
||||
|
|
@ -547,7 +608,7 @@ async def createFeatureInstance(
|
|||
|
||||
@router.delete("/instances/{instanceId}", response_model=Dict[str, str])
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteFeatureInstance(
|
||||
async def delete_feature_instance(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -603,9 +664,90 @@ async def deleteFeatureInstance(
|
|||
)
|
||||
|
||||
|
||||
class FeatureInstanceUpdate(BaseModel):
|
||||
"""Request model for updating a feature instance."""
|
||||
label: Optional[str] = Field(None, description="New label for the instance")
|
||||
enabled: Optional[bool] = Field(None, description="Enable/disable the instance")
|
||||
|
||||
|
||||
@router.put("/instances/{instanceId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def updateFeatureInstance(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
data: FeatureInstanceUpdate,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update a feature instance (label, enabled).
|
||||
|
||||
Requires Mandate-Admin role.
|
||||
|
||||
Args:
|
||||
instanceId: FeatureInstance ID
|
||||
data: Fields to update (label, enabled)
|
||||
"""
|
||||
try:
|
||||
rootInterface = getRootInterface()
|
||||
featureInterface = getFeatureInterface(rootInterface.db)
|
||||
|
||||
# Verify instance exists
|
||||
instance = featureInterface.getFeatureInstance(instanceId)
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Feature instance '{instanceId}' not found"
|
||||
)
|
||||
|
||||
# Verify mandate access
|
||||
if context.mandateId and str(instance.mandateId) != str(context.mandateId):
|
||||
if not context.isSysAdmin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this feature instance"
|
||||
)
|
||||
|
||||
# Check mandate admin permission
|
||||
if not _hasMandateAdminRole(context) and not context.isSysAdmin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Mandate-Admin role required to update feature instances"
|
||||
)
|
||||
|
||||
# Build update data (only non-None values)
|
||||
updateData = {}
|
||||
if data.label is not None:
|
||||
updateData["label"] = data.label
|
||||
if data.enabled is not None:
|
||||
updateData["enabled"] = data.enabled
|
||||
|
||||
if not updateData:
|
||||
return instance.model_dump()
|
||||
|
||||
updated = featureInterface.updateFeatureInstance(instanceId, updateData)
|
||||
if not updated:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update feature instance"
|
||||
)
|
||||
|
||||
logger.info(f"User {context.user.id} updated feature instance {instanceId}: {updateData}")
|
||||
|
||||
return updated.model_dump()
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating feature instance {instanceId}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update feature instance: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/instances/{instanceId}/sync-roles", response_model=SyncRolesResult)
|
||||
@limiter.limit("10/minute")
|
||||
async def syncInstanceRoles(
|
||||
async def sync_instance_roles(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
addOnly: bool = Query(True, description="Only add missing roles, don't remove extras"),
|
||||
|
|
@ -672,7 +814,7 @@ async def syncInstanceRoles(
|
|||
|
||||
@router.get("/templates/roles", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listTemplateRoles(
|
||||
async def list_template_roles(
|
||||
request: Request,
|
||||
featureCode: Optional[str] = Query(None, description="Filter by feature code"),
|
||||
sysAdmin: User = Depends(requireSysAdmin)
|
||||
|
|
@ -702,7 +844,7 @@ async def listTemplateRoles(
|
|||
|
||||
@router.post("/templates/roles", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def createTemplateRole(
|
||||
async def create_template_role(
|
||||
request: Request,
|
||||
roleLabel: str = Query(..., description="Role label (e.g., 'admin', 'viewer')"),
|
||||
featureCode: str = Query(..., description="Feature code this role belongs to"),
|
||||
|
|
@ -780,7 +922,7 @@ class FeatureInstanceUserResponse(BaseModel):
|
|||
|
||||
@router.get("/instances/{instanceId}/users", response_model=List[FeatureInstanceUserResponse])
|
||||
@limiter.limit("60/minute")
|
||||
async def listFeatureInstanceUsers(
|
||||
async def list_feature_instance_users(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -872,7 +1014,7 @@ async def listFeatureInstanceUsers(
|
|||
|
||||
@router.post("/instances/{instanceId}/users", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def addUserToFeatureInstance(
|
||||
async def add_user_to_feature_instance(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
data: FeatureInstanceUserCreate,
|
||||
|
|
@ -976,7 +1118,7 @@ async def addUserToFeatureInstance(
|
|||
|
||||
@router.delete("/instances/{instanceId}/users/{userId}", response_model=Dict[str, str])
|
||||
@limiter.limit("30/minute")
|
||||
async def removeUserFromFeatureInstance(
|
||||
async def remove_user_from_feature_instance(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
userId: str,
|
||||
|
|
@ -1057,7 +1199,7 @@ async def removeUserFromFeatureInstance(
|
|||
|
||||
@router.put("/instances/{instanceId}/users/{userId}/roles", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def updateFeatureInstanceUserRoles(
|
||||
async def update_feature_instance_user_roles(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
userId: str,
|
||||
|
|
@ -1154,7 +1296,7 @@ async def updateFeatureInstanceUserRoles(
|
|||
|
||||
@router.get("/instances/{instanceId}/available-roles", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getFeatureInstanceAvailableRoles(
|
||||
async def get_feature_instance_available_roles(
|
||||
request: Request,
|
||||
instanceId: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -1222,7 +1364,7 @@ async def getFeatureInstanceAvailableRoles(
|
|||
|
||||
@router.get("/{featureCode}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def getFeature(
|
||||
async def get_feature(
|
||||
request: Request,
|
||||
featureCode: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class RbacImportResult(BaseModel):
|
|||
|
||||
@router.get("/export/global", response_model=RbacExportData)
|
||||
@limiter.limit("10/minute")
|
||||
async def exportGlobalRbac(
|
||||
async def export_global_rbac(
|
||||
request: Request,
|
||||
sysAdmin: User = Depends(requireSysAdmin)
|
||||
) -> RbacExportData:
|
||||
|
|
@ -138,7 +138,7 @@ async def exportGlobalRbac(
|
|||
|
||||
@router.post("/import/global", response_model=RbacImportResult)
|
||||
@limiter.limit("5/minute")
|
||||
async def importGlobalRbac(
|
||||
async def import_global_rbac(
|
||||
request: Request,
|
||||
file: UploadFile = File(..., description="JSON file with RBAC export data"),
|
||||
updateExisting: bool = False,
|
||||
|
|
@ -285,7 +285,7 @@ async def importGlobalRbac(
|
|||
|
||||
@router.get("/export/mandate", response_model=RbacExportData)
|
||||
@limiter.limit("10/minute")
|
||||
async def exportMandateRbac(
|
||||
async def export_mandate_rbac(
|
||||
request: Request,
|
||||
includeFeatureInstances: bool = True,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -380,7 +380,7 @@ async def exportMandateRbac(
|
|||
|
||||
@router.post("/import/mandate", response_model=RbacImportResult)
|
||||
@limiter.limit("5/minute")
|
||||
async def importMandateRbac(
|
||||
async def import_mandate_rbac(
|
||||
request: Request,
|
||||
file: UploadFile = File(..., description="JSON file with RBAC export data"),
|
||||
updateExisting: bool = False,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ router = APIRouter(
|
|||
|
||||
@router.get("/", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listRoles(
|
||||
async def list_roles(
|
||||
request: Request,
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -121,7 +121,7 @@ async def listRoles(
|
|||
|
||||
@router.get("/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getRoleOptions(
|
||||
async def get_role_options(
|
||||
request: Request,
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -162,7 +162,7 @@ async def getRoleOptions(
|
|||
|
||||
@router.post("/", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def createRole(
|
||||
async def create_role(
|
||||
request: Request,
|
||||
role: Role = Body(...),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -206,7 +206,7 @@ async def createRole(
|
|||
|
||||
@router.get("/{roleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def getRole(
|
||||
async def get_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -250,7 +250,7 @@ async def getRole(
|
|||
|
||||
@router.put("/{roleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def updateRole(
|
||||
async def update_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
role: Role = Body(...),
|
||||
|
|
@ -298,7 +298,7 @@ async def updateRole(
|
|||
|
||||
@router.delete("/{roleId}", response_model=Dict[str, str])
|
||||
@limiter.limit("30/minute")
|
||||
async def deleteRole(
|
||||
async def delete_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -342,7 +342,7 @@ async def deleteRole(
|
|||
|
||||
@router.get("/users", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listUsersWithRoles(
|
||||
async def list_users_with_roles(
|
||||
request: Request,
|
||||
roleLabel: Optional[str] = Query(None, description="Filter by role label"),
|
||||
mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"),
|
||||
|
|
@ -412,7 +412,7 @@ async def listUsersWithRoles(
|
|||
|
||||
@router.get("/users/{userId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def getUserRoles(
|
||||
async def get_user_roles(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="User ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -462,7 +462,7 @@ async def getUserRoles(
|
|||
|
||||
@router.put("/users/{userId}/roles", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def updateUserRoles(
|
||||
async def update_user_roles(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="User ID"),
|
||||
newRoleLabels: List[str] = Body(..., description="List of role labels to assign"),
|
||||
|
|
@ -559,7 +559,7 @@ async def updateUserRoles(
|
|||
|
||||
@router.post("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def addUserRole(
|
||||
async def add_user_role(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="User ID"),
|
||||
roleLabel: str = Path(..., description="Role label to add"),
|
||||
|
|
@ -641,7 +641,7 @@ async def addUserRole(
|
|||
|
||||
@router.delete("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def removeUserRole(
|
||||
async def remove_user_role(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="User ID"),
|
||||
roleLabel: str = Path(..., description="Role label to remove"),
|
||||
|
|
@ -721,7 +721,7 @@ async def removeUserRole(
|
|||
|
||||
@router.get("/roles/{roleLabel}/users", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getUsersWithRole(
|
||||
async def get_users_with_role(
|
||||
request: Request,
|
||||
roleLabel: str = Path(..., description="Role label"),
|
||||
mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ router = APIRouter(
|
|||
|
||||
@router.get("/permissions", response_model=UserPermissions)
|
||||
@limiter.limit("300/minute") # Raised from 60 - sidebar checks many pages individually
|
||||
async def getPermissions(
|
||||
async def get_permissions(
|
||||
request: Request,
|
||||
context: str = Query(..., description="Context type: DATA, UI, or RESOURCE"),
|
||||
item: Optional[str] = Query(None, description="Item identifier (table name, UI path, or resource path)"),
|
||||
|
|
@ -98,7 +98,7 @@ async def getPermissions(
|
|||
|
||||
@router.get("/permissions/all", response_model=Dict[str, Any])
|
||||
@limiter.limit("120/minute") # Raised from 30 - optimized endpoint for bulk permission fetch
|
||||
async def getAllPermissions(
|
||||
async def get_all_permissions(
|
||||
request: Request,
|
||||
context: Optional[str] = Query(None, description="Context type: UI or RESOURCE (if not provided, returns both)"),
|
||||
reqContext: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -224,7 +224,7 @@ async def getAllPermissions(
|
|||
|
||||
@router.get("/rules", response_model=PaginatedResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessRules(
|
||||
async def get_access_rules(
|
||||
request: Request,
|
||||
roleLabel: Optional[str] = Query(None, description="Filter by role label"),
|
||||
context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
|
||||
|
|
@ -313,7 +313,7 @@ async def getAccessRules(
|
|||
|
||||
@router.get("/rules/by-role/{roleId}", response_model=PaginatedResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessRulesByRole(
|
||||
async def get_access_rules_by_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID to get rules for"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -357,7 +357,7 @@ async def getAccessRulesByRole(
|
|||
|
||||
@router.get("/rules/{ruleId}", response_model=dict)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessRule(
|
||||
async def get_access_rule(
|
||||
request: Request,
|
||||
ruleId: str = Path(..., description="Access rule ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -399,7 +399,7 @@ async def getAccessRule(
|
|||
|
||||
@router.post("/rules", response_model=dict)
|
||||
@limiter.limit("30/minute")
|
||||
async def createAccessRule(
|
||||
async def create_access_rule(
|
||||
request: Request,
|
||||
accessRuleData: dict = Body(..., description="Access rule data"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -465,7 +465,7 @@ async def createAccessRule(
|
|||
|
||||
@router.put("/rules/{ruleId}", response_model=dict)
|
||||
@limiter.limit("30/minute")
|
||||
async def updateAccessRule(
|
||||
async def update_access_rule(
|
||||
request: Request,
|
||||
ruleId: str = Path(..., description="Access rule ID"),
|
||||
accessRuleData: dict = Body(..., description="Updated access rule data"),
|
||||
|
|
@ -548,7 +548,7 @@ async def updateAccessRule(
|
|||
|
||||
@router.delete("/rules/{ruleId}")
|
||||
@limiter.limit("30/minute")
|
||||
async def deleteAccessRule(
|
||||
async def delete_access_rule(
|
||||
request: Request,
|
||||
ruleId: str = Path(..., description="Access rule ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -606,7 +606,7 @@ async def deleteAccessRule(
|
|||
|
||||
@router.get("/roles", response_model=PaginatedResponse)
|
||||
@limiter.limit("60/minute")
|
||||
async def listRoles(
|
||||
async def list_roles(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
includeTemplates: bool = Query(False, description="Include feature template roles"),
|
||||
|
|
@ -775,7 +775,7 @@ async def listRoles(
|
|||
|
||||
@router.get("/roles/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getRoleOptions(
|
||||
async def get_role_options(
|
||||
request: Request,
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -816,7 +816,7 @@ async def getRoleOptions(
|
|||
|
||||
@router.post("/roles", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def createRole(
|
||||
async def create_role(
|
||||
request: Request,
|
||||
role: Role = Body(...),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -865,7 +865,7 @@ async def createRole(
|
|||
|
||||
@router.get("/roles/{roleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def getRole(
|
||||
async def get_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
@ -912,7 +912,7 @@ async def getRole(
|
|||
|
||||
@router.put("/roles/{roleId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def updateRole(
|
||||
async def update_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
role: Role = Body(...),
|
||||
|
|
@ -965,7 +965,7 @@ async def updateRole(
|
|||
|
||||
@router.delete("/roles/{roleId}", response_model=Dict[str, str])
|
||||
@limiter.limit("30/minute")
|
||||
async def deleteRole(
|
||||
async def delete_role(
|
||||
request: Request,
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ router = APIRouter(
|
|||
|
||||
@router.get("/statuses/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getConnectionStatusOptions(
|
||||
async def get_connection_status_options(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -116,7 +116,7 @@ async def getConnectionStatusOptions(
|
|||
|
||||
@router.get("/authorities/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getAuthAuthorityOptions(
|
||||
async def get_auth_authority_options(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ async def delete_mandate(
|
|||
|
||||
@router.get("/{targetMandateId}/users")
|
||||
@limiter.limit("60/minute")
|
||||
async def listMandateUsers(
|
||||
async def list_mandate_users(
|
||||
request: Request,
|
||||
targetMandateId: str = Path(..., description="ID of the mandate"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
|
|
@ -486,7 +486,7 @@ async def listMandateUsers(
|
|||
|
||||
@router.post("/{targetMandateId}/users", response_model=UserMandateResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def addUserToMandate(
|
||||
async def add_user_to_mandate(
|
||||
request: Request,
|
||||
targetMandateId: str = Path(..., description="ID of the mandate"),
|
||||
data: UserMandateCreate = Body(...),
|
||||
|
|
@ -602,7 +602,7 @@ async def addUserToMandate(
|
|||
|
||||
@router.delete("/{targetMandateId}/users/{targetUserId}", response_model=Dict[str, str])
|
||||
@limiter.limit("30/minute")
|
||||
async def removeUserFromMandate(
|
||||
async def remove_user_from_mandate(
|
||||
request: Request,
|
||||
targetMandateId: str = Path(..., description="ID of the mandate"),
|
||||
targetUserId: str = Path(..., description="ID of the user to remove"),
|
||||
|
|
@ -680,7 +680,7 @@ async def removeUserFromMandate(
|
|||
|
||||
@router.put("/{targetMandateId}/users/{targetUserId}/roles", response_model=UserMandateResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def updateUserRolesInMandate(
|
||||
async def update_user_roles_in_mandate(
|
||||
request: Request,
|
||||
targetMandateId: str = Path(..., description="ID of the mandate"),
|
||||
targetUserId: str = Path(..., description="ID of the user"),
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ router = APIRouter(
|
|||
|
||||
@router.get("/options", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def getUserOptions(
|
||||
async def get_user_options(
|
||||
request: Request,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
|
@ -631,7 +631,7 @@ async def change_password(
|
|||
|
||||
@router.post("/{userId}/send-password-link")
|
||||
@limiter.limit("10/minute")
|
||||
async def sendPasswordLink(
|
||||
async def send_password_link(
|
||||
request: Request,
|
||||
userId: str = Path(..., description="ID of the user to send password setup link"),
|
||||
frontendUrl: str = Body(..., embed=True),
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class DeletionResult(BaseModel):
|
|||
|
||||
@router.get("/data-export", response_model=DataExportResponse)
|
||||
@limiter.limit("5/minute")
|
||||
async def exportUserData(
|
||||
async def export_user_data(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> DataExportResponse:
|
||||
|
|
@ -238,7 +238,7 @@ async def exportUserData(
|
|||
|
||||
@router.get("/data-portability")
|
||||
@limiter.limit("5/minute")
|
||||
async def exportPortableData(
|
||||
async def export_portable_data(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> JSONResponse:
|
||||
|
|
@ -333,7 +333,7 @@ async def exportPortableData(
|
|||
|
||||
@router.delete("/", response_model=DeletionResult)
|
||||
@limiter.limit("1/hour")
|
||||
async def deleteAccount(
|
||||
async def delete_account(
|
||||
request: Request,
|
||||
confirmDeletion: bool = False,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -465,7 +465,7 @@ async def deleteAccount(
|
|||
|
||||
@router.get("/consent-info", response_model=Dict[str, Any])
|
||||
@limiter.limit("30/minute")
|
||||
async def getConsentInfo(
|
||||
async def get_consent_info(
|
||||
request: Request,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class RegisterAndAcceptResponse(BaseModel):
|
|||
|
||||
@router.post("/", response_model=InvitationResponse)
|
||||
@limiter.limit("30/minute")
|
||||
async def createInvitation(
|
||||
async def create_invitation(
|
||||
request: Request,
|
||||
data: InvitationCreate,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -234,7 +234,7 @@ async def createInvitation(
|
|||
|
||||
@router.get("/", response_model=List[Dict[str, Any]])
|
||||
@limiter.limit("60/minute")
|
||||
async def listInvitations(
|
||||
async def list_invitations(
|
||||
request: Request,
|
||||
includeUsed: bool = Query(False, description="Include already used invitations"),
|
||||
includeExpired: bool = Query(False, description="Include expired invitations"),
|
||||
|
|
@ -313,7 +313,7 @@ async def listInvitations(
|
|||
|
||||
@router.delete("/{invitationId}", response_model=Dict[str, str])
|
||||
@limiter.limit("30/minute")
|
||||
async def revokeInvitation(
|
||||
async def revoke_invitation(
|
||||
request: Request,
|
||||
invitationId: str,
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
|
|
@ -397,7 +397,7 @@ async def revokeInvitation(
|
|||
|
||||
@router.get("/validate/{token}", response_model=InvitationValidation)
|
||||
@limiter.limit("30/minute")
|
||||
async def validateInvitation(
|
||||
async def validate_invitation(
|
||||
request: Request,
|
||||
token: str
|
||||
) -> InvitationValidation:
|
||||
|
|
@ -482,7 +482,7 @@ async def validateInvitation(
|
|||
|
||||
@router.post("/accept/{token}", response_model=Dict[str, Any])
|
||||
@limiter.limit("10/minute")
|
||||
async def acceptInvitation(
|
||||
async def accept_invitation(
|
||||
request: Request,
|
||||
token: str,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -614,7 +614,7 @@ async def acceptInvitation(
|
|||
|
||||
@router.post("/register-and-accept", response_model=RegisterAndAcceptResponse)
|
||||
@limiter.limit("10/minute") # Stricter rate limit for registration
|
||||
async def registerAndAcceptInvitation(
|
||||
async def register_and_accept_invitation(
|
||||
request: Request,
|
||||
data: RegisterAndAcceptRequest
|
||||
) -> RegisterAndAcceptResponse:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ router = APIRouter(
|
|||
|
||||
@router.get("/subscriptions", response_model=PaginatedResponse[MessagingSubscription])
|
||||
@limiter.limit("60/minute")
|
||||
async def getSubscriptions(
|
||||
async def get_subscriptions(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -79,7 +79,7 @@ async def getSubscriptions(
|
|||
|
||||
@router.post("/subscriptions", response_model=MessagingSubscription)
|
||||
@limiter.limit("60/minute")
|
||||
async def createSubscription(
|
||||
async def create_subscription(
|
||||
request: Request,
|
||||
subscription: MessagingSubscription,
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -95,7 +95,7 @@ async def createSubscription(
|
|||
|
||||
@router.get("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
|
||||
@limiter.limit("60/minute")
|
||||
async def getSubscription(
|
||||
async def get_subscription(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -115,7 +115,7 @@ async def getSubscription(
|
|||
|
||||
@router.put("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
|
||||
@limiter.limit("60/minute")
|
||||
async def updateSubscription(
|
||||
async def update_subscription(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription to update"),
|
||||
subscriptionData: MessagingSubscription = Body(...),
|
||||
|
|
@ -145,7 +145,7 @@ async def updateSubscription(
|
|||
|
||||
@router.delete("/subscriptions/{subscriptionId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def deleteSubscription(
|
||||
async def delete_subscription(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription to delete"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -174,7 +174,7 @@ async def deleteSubscription(
|
|||
|
||||
@router.get("/subscriptions/{subscriptionId}/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
|
||||
@limiter.limit("60/minute")
|
||||
async def getSubscriptionRegistrations(
|
||||
async def get_subscription_registrations(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
|
|
@ -219,7 +219,7 @@ async def getSubscriptionRegistrations(
|
|||
|
||||
@router.post("/subscriptions/{subscriptionId}/subscribe", response_model=MessagingSubscriptionRegistration)
|
||||
@limiter.limit("60/minute")
|
||||
async def subscribeUser(
|
||||
async def subscribe_user(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription"),
|
||||
channel: MessagingChannel = Body(..., embed=True),
|
||||
|
|
@ -241,7 +241,7 @@ async def subscribeUser(
|
|||
|
||||
@router.delete("/subscriptions/{subscriptionId}/unsubscribe", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def unsubscribeUser(
|
||||
async def unsubscribe_user(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription"),
|
||||
channel: MessagingChannel = Body(..., embed=True),
|
||||
|
|
@ -267,7 +267,7 @@ async def unsubscribeUser(
|
|||
|
||||
@router.get("/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
|
||||
@limiter.limit("60/minute")
|
||||
async def getMyRegistrations(
|
||||
async def get_my_registrations(
|
||||
request: Request,
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -311,7 +311,7 @@ async def getMyRegistrations(
|
|||
|
||||
@router.put("/registrations/{registrationId}", response_model=MessagingSubscriptionRegistration)
|
||||
@limiter.limit("60/minute")
|
||||
async def updateRegistration(
|
||||
async def update_registration(
|
||||
request: Request,
|
||||
registrationId: str = Path(..., description="ID of the registration to update"),
|
||||
registrationData: MessagingSubscriptionRegistration = Body(...),
|
||||
|
|
@ -341,7 +341,7 @@ async def updateRegistration(
|
|||
|
||||
@router.delete("/registrations/{registrationId}", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def deleteRegistration(
|
||||
async def delete_registration(
|
||||
request: Request,
|
||||
registrationId: str = Path(..., description="ID of the registration to delete"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
@ -376,7 +376,7 @@ def _getTriggerKey(request: Request) -> str:
|
|||
|
||||
@router.post("/trigger/{subscriptionId}", response_model=MessagingSubscriptionExecutionResult)
|
||||
@limiter.limit("60/minute", key_func=_getTriggerKey)
|
||||
async def triggerSubscription(
|
||||
async def trigger_subscription(
|
||||
request: Request,
|
||||
subscriptionId: str = Path(..., description="ID of the subscription to trigger"),
|
||||
eventParameters: Dict[str, Any] = Body(...),
|
||||
|
|
@ -440,7 +440,7 @@ def _hasTriggerPermission(context: RequestContext) -> bool:
|
|||
|
||||
@router.get("/deliveries", response_model=PaginatedResponse[MessagingDelivery])
|
||||
@limiter.limit("60/minute")
|
||||
async def getDeliveries(
|
||||
async def get_deliveries(
|
||||
request: Request,
|
||||
subscriptionId: Optional[str] = Query(None, description="Filter by subscription ID"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
||||
|
|
@ -486,7 +486,7 @@ async def getDeliveries(
|
|||
|
||||
@router.get("/deliveries/{deliveryId}", response_model=MessagingDelivery)
|
||||
@limiter.limit("60/minute")
|
||||
async def getDelivery(
|
||||
async def get_delivery(
|
||||
request: Request,
|
||||
deliveryId: str = Path(..., description="ID of the delivery"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
|
|
|
|||
|
|
@ -106,18 +106,9 @@ async def login(
|
|||
# Get gateway interface with root privileges for authentication
|
||||
rootInterface = getRootInterface()
|
||||
|
||||
# Get default mandate ID
|
||||
defaultMandateId = rootInterface.getInitialId(Mandate)
|
||||
if not defaultMandateId:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="No default mandate found"
|
||||
)
|
||||
|
||||
# Set the mandate ID on the interface
|
||||
rootInterface.mandateId = defaultMandateId
|
||||
|
||||
# Authenticate user
|
||||
# Note: authenticateLocalUser uses _getUserForAuthentication which bypasses RBAC
|
||||
# This is correct because users are mandate-independent (Multi-Tenant Design)
|
||||
user = rootInterface.authenticateLocalUser(
|
||||
username=formData.username,
|
||||
password=formData.password
|
||||
|
|
@ -265,16 +256,9 @@ async def register_user(
|
|||
# Get gateway interface with root privileges since this is a public endpoint
|
||||
appInterface = getRootInterface()
|
||||
|
||||
# Get default mandate ID
|
||||
defaultMandateId = appInterface.getInitialId(Mandate)
|
||||
if not defaultMandateId:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="No default mandate found"
|
||||
)
|
||||
|
||||
# Set the mandate ID on the interface
|
||||
appInterface.mandateId = defaultMandateId
|
||||
# Note: User registration does NOT require mandateId context
|
||||
# Users are mandate-independent (Multi-Tenant Design)
|
||||
# Mandate assignment happens via createUserMandate() after registration
|
||||
|
||||
# Frontend URL is required - no fallback
|
||||
baseUrl = frontendUrl.rstrip("/")
|
||||
|
|
@ -548,7 +532,7 @@ async def check_username_availability(
|
|||
|
||||
@router.post("/password-reset-request")
|
||||
@limiter.limit("5/minute")
|
||||
async def passwordResetRequest(
|
||||
async def password_reset_request(
|
||||
request: Request,
|
||||
username: str = Body(..., embed=True),
|
||||
frontendUrl: str = Body(..., embed=True)
|
||||
|
|
@ -628,7 +612,7 @@ Falls Sie diese Anforderung nicht gestellt haben, können Sie diese E-Mail ignor
|
|||
|
||||
@router.post("/password-reset")
|
||||
@limiter.limit("10/minute")
|
||||
async def passwordReset(
|
||||
async def password_reset(
|
||||
request: Request,
|
||||
token: str = Body(..., embed=True),
|
||||
password: str = Body(..., embed=True)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class ConnectionManager:
|
|||
del activeConnections[connectionId]
|
||||
logger.info(f"WebSocket disconnected: {connectionId}")
|
||||
|
||||
async def sendPersonalMessage(self, message: dict, websocket: WebSocket):
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_text(json.dumps(message))
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ class MethodAiOperationsTester:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
self.services = None
|
||||
self.methodAi = None
|
||||
|
|
@ -119,7 +122,7 @@ class MethodAiOperationsTester:
|
|||
currentAction=0,
|
||||
totalTasks=0,
|
||||
totalActions=0,
|
||||
mandateId=self.testUser.mandateId,
|
||||
mandateId=self.testMandateId,
|
||||
messageIds=[],
|
||||
workflowMode=WorkflowModeEnum.WORKFLOW_DYNAMIC,
|
||||
maxSteps=5
|
||||
|
|
@ -277,7 +280,7 @@ class MethodAiOperationsTester:
|
|||
currentAction=0,
|
||||
totalTasks=0,
|
||||
totalActions=0,
|
||||
mandateId=self.testUser.mandateId,
|
||||
mandateId=self.testMandateId,
|
||||
messageIds=[],
|
||||
workflowMode=WorkflowModeEnum.WORKFLOW_DYNAMIC,
|
||||
maxSteps=5
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ class AIBehaviorTester:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -60,7 +63,7 @@ class AIBehaviorTester:
|
|||
currentAction=0,
|
||||
totalTasks=0,
|
||||
totalActions=0,
|
||||
mandateId=self.testUser.mandateId,
|
||||
mandateId=self.testMandateId,
|
||||
messageIds=[],
|
||||
workflowMode=WorkflowModeEnum.WORKFLOW_DYNAMIC,
|
||||
maxSteps=5
|
||||
|
|
|
|||
|
|
@ -30,8 +30,11 @@ class WorkflowWithDocumentsTester:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -45,7 +48,7 @@ class WorkflowWithDocumentsTester:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
print(f"Initialized test with user: {self.testUser.id}")
|
||||
print(f"Mandate ID: {self.testUser.mandateId}")
|
||||
print(f"Test Mandate ID: {self.testMandateId}")
|
||||
|
||||
def createCsvTemplate(self) -> str:
|
||||
"""Create a CSV template file for prime numbers."""
|
||||
|
|
|
|||
|
|
@ -32,8 +32,11 @@ class WorkflowPromptVariationsTester:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -46,7 +49,7 @@ class WorkflowPromptVariationsTester:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
print(f"Initialized test with user: {self.testUser.id}")
|
||||
print(f"Mandate ID: {self.testUser.mandateId}")
|
||||
print(f"Test Mandate ID: {self.testMandateId}")
|
||||
|
||||
def _createFile(self, fileName: str, mimeType: str, content: str) -> str:
|
||||
"""Helper method to create a file and return its ID."""
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ class DocumentGenerationFormatsTester:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -52,7 +55,7 @@ class DocumentGenerationFormatsTester:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
print(f"Initialized test with user: {self.testUser.id}")
|
||||
print(f"Mandate ID: {self.testUser.mandateId}")
|
||||
print(f"Test Mandate ID: {self.testMandateId}")
|
||||
print(f"Debug logging enabled: {APP_CONFIG.get('APP_DEBUG_CHAT_WORKFLOW_ENABLED', False)}")
|
||||
|
||||
# Upload PDF file for testing
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ class DocumentGenerationFormatsTester10:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -52,7 +55,7 @@ class DocumentGenerationFormatsTester10:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
print(f"Initialized test with user: {self.testUser.id}")
|
||||
print(f"Mandate ID: {self.testUser.mandateId}")
|
||||
print(f"Test Mandate ID: {self.testMandateId}")
|
||||
print(f"Debug logging enabled: {APP_CONFIG.get('APP_DEBUG_CHAT_WORKFLOW_ENABLED', False)}")
|
||||
|
||||
# Upload PDF file for testing
|
||||
|
|
|
|||
|
|
@ -33,8 +33,11 @@ class CodeGenerationFormatsTester11:
|
|||
def __init__(self):
|
||||
# Use root user for testing (has full access to everything)
|
||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||
from modules.datamodels.datamodelUam import Mandate
|
||||
rootInterface = getRootInterface()
|
||||
self.testUser = rootInterface.currentUser
|
||||
# Get initial mandate ID for testing (User has no mandateId - use initial mandate)
|
||||
self.testMandateId = rootInterface.getInitialId(Mandate)
|
||||
|
||||
# Initialize services using the existing system
|
||||
self.services = getServices(self.testUser, None) # Test user, no workflow
|
||||
|
|
@ -53,7 +56,7 @@ class CodeGenerationFormatsTester11:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
print(f"Initialized test with user: {self.testUser.id}")
|
||||
print(f"Mandate ID: {self.testUser.mandateId}")
|
||||
print(f"Test Mandate ID: {self.testMandateId}")
|
||||
print(f"Debug logging enabled: {APP_CONFIG.get('APP_DEBUG_CHAT_WORKFLOW_ENABLED', False)}")
|
||||
|
||||
def createTestPrompt(self, format: str) -> str:
|
||||
|
|
|
|||
Loading…
Reference in a new issue