fixed mandate routing
This commit is contained in:
parent
82badd9e4d
commit
b16420db41
8 changed files with 133 additions and 30 deletions
|
|
@ -46,9 +46,10 @@ class Invitation(BaseModel):
|
|||
)
|
||||
|
||||
# Einladungs-Details
|
||||
targetUsername: str = Field(
|
||||
targetUsername: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Username of the invited user (must match on acceptance)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
||||
)
|
||||
email: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ def initBootstrap(db: DatabaseConnector) -> None:
|
|||
# Migrate existing mandate records: description -> label
|
||||
_migrateMandateDescriptionToLabel(db)
|
||||
|
||||
# Clean up duplicate roles and fix corrupted templates FIRST
|
||||
_deduplicateRoles(db)
|
||||
|
||||
# Initialize system role TEMPLATES (mandateId=None, isSystemRole=True)
|
||||
initRoles(db)
|
||||
|
||||
|
|
@ -429,23 +432,89 @@ def initRoles(db: DatabaseConnector) -> None:
|
|||
),
|
||||
]
|
||||
|
||||
existingRoles = db.getRecordset(Role)
|
||||
existingRoleLabels = {role.get("roleLabel"): role.get("id") for role in existingRoles}
|
||||
# Check specifically for system template roles:
|
||||
# mandateId=NULL, isSystemRole=True, featureCode=NULL
|
||||
# Feature templates (e.g. chatplayground admin) share the same labels but have featureCode set!
|
||||
allTemplates = db.getRecordset(
|
||||
Role,
|
||||
recordFilter={"mandateId": None, "isSystemRole": True}
|
||||
)
|
||||
# Filter for SYSTEM templates only (featureCode=None), not feature templates
|
||||
systemTemplates = [r for r in allTemplates if r.get("featureCode") is None]
|
||||
existingTemplateLabels = {role.get("roleLabel"): role.get("id") for role in systemTemplates}
|
||||
|
||||
for role in standardRoles:
|
||||
if role.roleLabel not in existingRoleLabels:
|
||||
if role.roleLabel not in existingTemplateLabels:
|
||||
try:
|
||||
createdRole = db.recordCreate(Role, role)
|
||||
_roleIdCache[role.roleLabel] = createdRole.get("id")
|
||||
logger.info(f"Created role: {role.roleLabel} with ID {createdRole.get('id')}")
|
||||
logger.info(f"Created template role: {role.roleLabel} with ID {createdRole.get('id')}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating role {role.roleLabel}: {e}")
|
||||
else:
|
||||
_roleIdCache[role.roleLabel] = existingRoleLabels[role.roleLabel]
|
||||
_roleIdCache[role.roleLabel] = existingTemplateLabels[role.roleLabel]
|
||||
|
||||
logger.info("Roles initialization completed")
|
||||
|
||||
|
||||
def _deduplicateRoles(db: DatabaseConnector) -> None:
|
||||
"""
|
||||
Remove duplicate roles (same roleLabel + mandateId + featureInstanceId).
|
||||
Keeps the oldest role (smallest ID) and deletes newer duplicates.
|
||||
"""
|
||||
allRoles = db.getRecordset(Role)
|
||||
|
||||
# Group by (roleLabel, mandateId, featureInstanceId, featureCode)
|
||||
# featureCode is essential: system template ('admin', None, None, None)
|
||||
# must NOT be grouped with feature template ('admin', None, None, 'chatplayground')
|
||||
groups: dict = {}
|
||||
for role in allRoles:
|
||||
key = (role.get("roleLabel"), role.get("mandateId"), role.get("featureInstanceId"), role.get("featureCode"))
|
||||
if key not in groups:
|
||||
groups[key] = []
|
||||
groups[key].append(role)
|
||||
|
||||
deletedCount = 0
|
||||
for key, roles in groups.items():
|
||||
if len(roles) > 1:
|
||||
# Sort by id to keep the first (oldest), delete the rest
|
||||
roles.sort(key=lambda r: r.get("id", ""))
|
||||
for duplicate in roles[1:]:
|
||||
try:
|
||||
db.recordDelete(Role, duplicate.get("id"))
|
||||
deletedCount += 1
|
||||
logger.info(f"Deleted duplicate role: label='{key[0]}', mandateId={key[1]}, id={duplicate.get('id')}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete duplicate role {duplicate.get('id')}: {e}")
|
||||
|
||||
if deletedCount > 0:
|
||||
logger.info(f"Deduplicated roles: removed {deletedCount} duplicates")
|
||||
|
||||
# Migration: Fix isSystemRole flags
|
||||
fixedMandateCount = 0
|
||||
fixedTemplateCount = 0
|
||||
for role in allRoles:
|
||||
# Mandate-level roles should NOT be isSystemRole=True
|
||||
if role.get("mandateId") is not None and role.get("isSystemRole") is True:
|
||||
try:
|
||||
db.recordModify(Role, role.get("id"), {"isSystemRole": False})
|
||||
fixedMandateCount += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fix mandate role {role.get('id')}: {e}")
|
||||
# Template roles (mandateId=None, standard labels) MUST be isSystemRole=True
|
||||
if role.get("mandateId") is None and role.get("isSystemRole") is not True:
|
||||
if role.get("roleLabel") in ("admin", "user", "viewer"):
|
||||
try:
|
||||
db.recordModify(Role, role.get("id"), {"isSystemRole": True})
|
||||
fixedTemplateCount += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fix template role {role.get('id')}: {e}")
|
||||
if fixedMandateCount > 0:
|
||||
logger.info(f"Fixed {fixedMandateCount} mandate-level roles: isSystemRole → False")
|
||||
if fixedTemplateCount > 0:
|
||||
logger.info(f"Fixed {fixedTemplateCount} template roles: isSystemRole → True")
|
||||
|
||||
|
||||
def _ensureAllMandatesHaveSystemRoles(db: DatabaseConnector) -> None:
|
||||
"""
|
||||
Ensure all existing mandates have system-instance roles.
|
||||
|
|
@ -453,11 +522,15 @@ def _ensureAllMandatesHaveSystemRoles(db: DatabaseConnector) -> None:
|
|||
"""
|
||||
allMandates = db.getRecordset(Mandate)
|
||||
if not allMandates:
|
||||
logger.info("No mandates found, skipping system role copy")
|
||||
return
|
||||
|
||||
logger.info(f"Ensuring system roles for {len(allMandates)} mandates...")
|
||||
for mandate in allMandates:
|
||||
mandateId = mandate.get("id")
|
||||
copySystemRolesToMandate(db, mandateId)
|
||||
copiedCount = copySystemRolesToMandate(db, mandateId)
|
||||
if copiedCount > 0:
|
||||
logger.info(f"Copied {copiedCount} system roles to mandate {mandateId}")
|
||||
|
||||
|
||||
def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
|
||||
|
|
@ -477,22 +550,23 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
|
|||
"""
|
||||
import uuid as _uuid
|
||||
|
||||
# Find system template roles (global, no mandateId)
|
||||
# Find system template roles (global: mandateId=NULL, isSystemRole=True)
|
||||
templateRoles = db.getRecordset(
|
||||
Role,
|
||||
recordFilter={"isSystemRole": True, "mandateId": None}
|
||||
)
|
||||
|
||||
if not templateRoles:
|
||||
logger.debug("No system template roles found to copy")
|
||||
logger.warning(f"No system template roles found (mandateId IS NULL, isSystemRole=True)")
|
||||
return 0
|
||||
|
||||
# Check which roles already exist for this mandate
|
||||
# Check which mandate-level roles already exist for this mandate
|
||||
existingMandateRoles = db.getRecordset(
|
||||
Role,
|
||||
recordFilter={"mandateId": mandateId, "featureInstanceId": None}
|
||||
)
|
||||
existingLabels = {r.get("roleLabel") for r in existingMandateRoles}
|
||||
logger.info(f"copySystemRolesToMandate: mandate={mandateId}, templates={len(templateRoles)}, existing={len(existingMandateRoles)}, labels={existingLabels}")
|
||||
|
||||
# Load all AccessRules for template roles
|
||||
templateRoleIds = [r.get("id") for r in templateRoles]
|
||||
|
|
@ -520,7 +594,7 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
|
|||
mandateId=mandateId,
|
||||
featureInstanceId=None,
|
||||
featureCode=None,
|
||||
isSystemRole=True # Still a system role, but bound to this mandate
|
||||
isSystemRole=False # Mandate-level role, not a system template
|
||||
)
|
||||
db.recordCreate(Role, newRole.model_dump())
|
||||
|
||||
|
|
|
|||
|
|
@ -3142,6 +3142,21 @@ class AppObjects:
|
|||
logger.error(f"Error getting role by label and scope {roleLabel}: {str(e)}")
|
||||
return None
|
||||
|
||||
def getRolesForMandate(self, mandateId: str) -> List[Role]:
|
||||
"""
|
||||
Get mandate-level roles for a specific mandate (featureInstanceId=NULL).
|
||||
These are the roles created by copySystemRolesToMandate during bootstrap.
|
||||
"""
|
||||
try:
|
||||
roles = self.db.getRecordset(
|
||||
Role,
|
||||
recordFilter={"mandateId": mandateId, "featureInstanceId": None}
|
||||
)
|
||||
return [Role(**{k: v for k, v in r.items() if not k.startswith("_")}) for r in roles]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting roles for mandate {mandateId}: {e}")
|
||||
return []
|
||||
|
||||
def getAllRoles(self, pagination: Optional[PaginationParams] = None) -> Union[List[Role], PaginatedResult]:
|
||||
"""
|
||||
Get all roles with optional pagination, sorting, and filtering.
|
||||
|
|
|
|||
|
|
@ -983,7 +983,7 @@ class BillingObjects:
|
|||
if not mandate:
|
||||
continue
|
||||
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
|
||||
settings = self.getSettings(mandateId)
|
||||
if not settings:
|
||||
|
|
@ -1066,7 +1066,7 @@ class BillingObjects:
|
|||
mandate = appInterface.getMandate(mandateId)
|
||||
mandateName = ""
|
||||
if mandate:
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
|
||||
for t in transactions:
|
||||
t["mandateId"] = mandateId
|
||||
|
|
@ -1118,7 +1118,7 @@ class BillingObjects:
|
|||
mandate = appInterface.getMandate(mandateId)
|
||||
mandateName = ""
|
||||
if mandate:
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
|
||||
# Get user accounts count (always exist now for audit trail)
|
||||
userAccounts = self.db.getRecordset(
|
||||
|
|
@ -1186,7 +1186,7 @@ class BillingObjects:
|
|||
mandate = appInterface.getMandate(mandateId)
|
||||
mandateName = ""
|
||||
if mandate:
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
|
||||
for t in transactions:
|
||||
t["mandateId"] = mandateId
|
||||
|
|
@ -1251,7 +1251,7 @@ class BillingObjects:
|
|||
for mandateId in mandateIdList:
|
||||
mandate = appInterface.getMandate(mandateId)
|
||||
if mandate:
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateMap[mandateId] = mandateName
|
||||
|
||||
for account in allAccounts:
|
||||
|
|
@ -1335,7 +1335,7 @@ class BillingObjects:
|
|||
for mandateId in mandateIdList:
|
||||
mandate = appInterface.getMandate(mandateId)
|
||||
if mandate:
|
||||
mandateName = getattr(mandate, 'name', None) or (mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateName = getattr(mandate, 'label', None) or getattr(mandate, 'name', None) or (mandate.get("label") or mandate.get("name", "") if isinstance(mandate, dict) else "")
|
||||
mandateMap[mandateId] = mandateName
|
||||
|
||||
# Get transactions for all accounts and collect createdByUserIds
|
||||
|
|
|
|||
|
|
@ -70,20 +70,28 @@ router = APIRouter(
|
|||
@limiter.limit("60/minute")
|
||||
def list_roles(
|
||||
request: Request,
|
||||
mandateId: Optional[str] = Query(None, description="Filter roles by mandate ID"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get list of all available roles with metadata.
|
||||
MULTI-TENANT: SysAdmin-only (roles are system resources).
|
||||
Get list of roles with metadata.
|
||||
|
||||
Returns:
|
||||
- List of role dictionaries with role label, description, and user count
|
||||
Without mandateId: returns system template roles (mandateId=NULL).
|
||||
With mandateId: returns mandate-level roles for that mandate (featureInstanceId=NULL).
|
||||
"""
|
||||
try:
|
||||
interface = getRootInterface()
|
||||
|
||||
# Get all roles from database
|
||||
dbRoles = interface.getAllRoles()
|
||||
# Get roles filtered by scope
|
||||
print(f"[DEBUG list_roles] mandateId={mandateId}")
|
||||
if mandateId:
|
||||
# Mandate-specific roles (mandate-level only, no feature-instance roles)
|
||||
dbRoles = interface.getRolesForMandate(mandateId)
|
||||
print(f"[DEBUG list_roles] getRolesForMandate returned {len(dbRoles)} roles")
|
||||
else:
|
||||
# System template roles only
|
||||
dbRoles = interface.getAllRoles()
|
||||
print(f"[DEBUG list_roles] getAllRoles returned {len(dbRoles)} roles")
|
||||
|
||||
# Count role assignments from UserMandateRole table
|
||||
roleCounts = interface.countRoleAssignments()
|
||||
|
|
@ -95,6 +103,8 @@ def list_roles(
|
|||
"id": role.id,
|
||||
"roleLabel": role.roleLabel,
|
||||
"description": role.description,
|
||||
"mandateId": role.mandateId,
|
||||
"featureInstanceId": role.featureInstanceId,
|
||||
"userCount": roleCounts.get(str(role.id), 0),
|
||||
"isSystemRole": role.isSystemRole
|
||||
})
|
||||
|
|
|
|||
|
|
@ -720,15 +720,18 @@ def list_roles(
|
|||
# Get all roles from database
|
||||
dbRoles = interface.getAllRoles(pagination=None)
|
||||
|
||||
|
||||
# Count role assignments from UserMandateRole table
|
||||
roleCounts = interface.countRoleAssignments()
|
||||
|
||||
# Helper function to compute scopeType
|
||||
# Note: mandateId takes precedence — a role with mandateId is always "mandate" scope,
|
||||
# even if isSystemRole=True (which just means it was copied from a system template)
|
||||
def _computeScopeType(role) -> str:
|
||||
if role.isSystemRole:
|
||||
return "system"
|
||||
if role.mandateId:
|
||||
return "mandate"
|
||||
if role.isSystemRole:
|
||||
return "system"
|
||||
return "global"
|
||||
|
||||
# Convert Role objects to dictionaries and add user counts
|
||||
|
|
|
|||
|
|
@ -666,16 +666,16 @@ def accept_invitation(
|
|||
# Update invitation usage
|
||||
rootInterface.db.recordModify(
|
||||
Invitation,
|
||||
invitation.get("id"),
|
||||
invitation.id,
|
||||
{
|
||||
"currentUses": invitation.get("currentUses", 0) + 1,
|
||||
"currentUses": (invitation.currentUses or 0) + 1,
|
||||
"usedBy": str(currentUser.id),
|
||||
"usedAt": currentTime
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"User {currentUser.id} accepted invitation {invitation.get('id')} "
|
||||
f"User {currentUser.id} accepted invitation {invitation.id} "
|
||||
f"for mandate {mandateId}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ Falls Sie sich nicht registriert haben, können Sie diese E-Mail ignorieren."""
|
|||
|
||||
# Get inviter name
|
||||
inviterId = invitation.createdBy
|
||||
inviter = appInterface.getUserById(inviterId) if inviterId else None
|
||||
inviter = appInterface.getUser(inviterId) if inviterId else None
|
||||
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
|
||||
|
||||
createInvitationNotification(
|
||||
|
|
|
|||
Loading…
Reference in a new issue