diff --git a/gwserver/_database_gateway/mandates.json b/gwserver/_database_gateway/mandates.json index d251ec2c..f25ed973 100644 --- a/gwserver/_database_gateway/mandates.json +++ b/gwserver/_database_gateway/mandates.json @@ -5,5 +5,12 @@ "language": "de", "mandate_id": 1, "user_id": 1 + }, + { + "id": 2, + "name": "Mandant von test", + "language": "de", + "mandate_id": 1, + "user_id": 1 } ] \ No newline at end of file diff --git a/gwserver/_database_gateway/users.json b/gwserver/_database_gateway/users.json index 9d8e710a..1efd867d 100644 --- a/gwserver/_database_gateway/users.json +++ b/gwserver/_database_gateway/users.json @@ -7,7 +7,20 @@ "full_name": "Administrator", "disabled": false, "language": "de", + "privilege": "sysadmin", "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$tJYSAmDs/T+HEALg3Nsbww$2HnCW69CkCkk82fgRhzjYtdNu3b9G7NmSJZmbUQVDOs", "user_id": 1 + }, + { + "id": 2, + "mandate_id": 2, + "username": "test", + "email": "web@pamocreate.com", + "full_name": "Test User", + "disabled": false, + "language": "de", + "privilege": "user", + "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$3fufM+bc+19r7V3rvVfKmQ$CrJCc6r44jO4DxMHs8E61ayupBeUi6a+65VH0Q1cGo8", + "user_id": 2 } ] \ No newline at end of file diff --git a/gwserver/_database_lucydom/agents.json b/gwserver/_database_lucydom/agents.json index f3ed4c02..d9a8ab59 100644 --- a/gwserver/_database_lucydom/agents.json +++ b/gwserver/_database_lucydom/agents.json @@ -3,9 +3,9 @@ "id": 2, "mandate_id": 1, "user_id": 1, - "name": "Hugo", + "name": "Hugo C", "type": "Transformation", - "workspace_id": 1, + "workspace_id": "1", "capabilities": "ccc", "description": "ddd" } diff --git a/gwserver/app.py b/gwserver/app.py index a283e127..830fc052 100644 --- a/gwserver/app.py +++ b/gwserver/app.py @@ -9,6 +9,7 @@ from typing import Dict, Any import os import logging from datetime import timedelta +import pathlib # Import interfaces from modules.gateway_interface import get_gateway_interface @@ -26,15 +27,6 @@ from auth import ( # Import models - generisch importieren import modules.gateway_model as gateway_model -# Import router modules -from routes.attributes import router as attributes_router -from routes.workspace import router as workspace_router -from routes.agent import router as agent_router -from routes.file import router as file_router -from routes.prompt import router as prompt_router -from routes.workflow import router as workflow_router -from routes.mandate import router as mandate_router - # Konfiguration des Loggers logging.basicConfig( level=logging.INFO, @@ -56,14 +48,17 @@ app.add_middleware( max_age=86400 # Erhöhung des Caching für Preflight-Anfragen ) -# Statischer Folder für Frontend -os.makedirs(os.path.join(os.getcwd(), "static"), exist_ok=True) -app.mount("/static", StaticFiles(directory="static"), name="static") +# Statischer Folder für Frontend - mit absolutem Pfad arbeiten +base_dir = pathlib.Path(__file__).parent +static_folder = base_dir / "static" +os.makedirs(static_folder, exist_ok=True) +app.mount("/static", StaticFiles(directory=str(static_folder)), name="static") # Add a specific route for favicon.ico @app.get("/favicon.ico", include_in_schema=False) async def favicon(): - return FileResponse("static/favicon.ico") + favicon_path = static_folder / "favicon.ico" + return FileResponse(str(favicon_path)) # Generelle Elements @app.get("/", tags=["General"]) @@ -75,7 +70,7 @@ async def root(): async def get_test(): return "OK 1.1" -@app.options("/{full_path:path}") +@app.options("/{full_path:path}", tags=["General"]) async def options_route(full_path: str): return Response(status_code=200) @@ -107,48 +102,36 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( return {"access_token": access_token, "token_type": "bearer"} - -# Benutzerregistrierung -@app.post("/api/users/register", response_model=gateway_model.User, tags=["General"]) -async def register_user(user_data: dict = Body(...), current_user: Dict[str, Any] = Depends(get_current_active_user)): - """Neuen Benutzer registrieren""" - # Nur Benutzer des gleichen Mandanten dürfen erstellt werden - mandate_id, user_id = await get_user_context(current_user) - - # Gateway-Interface mit Benutzerkontext initialisieren - gateway = get_gateway_interface(mandate_id, user_id) - - if "username" not in user_data or "password" not in user_data: - raise HTTPException(status_code=400, detail="Benutzername und Passwort erforderlich") - - try: - new_user = gateway.create_user( - username=user_data["username"], - password=user_data["password"], - email=user_data.get("email"), - full_name=user_data.get("full_name"), - language=user_data.get("language", "de") - ) - return new_user - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - - # Benutzerinfo abrufen @app.get("/api/users/me", response_model=Dict[str, Any], tags=["General"]) async def read_users_me(current_user: Dict[str, Any] = Depends(get_current_active_user)): return current_user - # Alle Router einbinden -app.include_router(attributes_router) -app.include_router(workspace_router) -app.include_router(agent_router) -app.include_router(file_router) -app.include_router(prompt_router) -app.include_router(workflow_router) +from routes.mandate import router as mandate_router app.include_router(mandate_router) +from routes.users import router as users_router +app.include_router(users_router) + +from routes.attributes import router as attributes_router +app.include_router(attributes_router) + +from routes.workspace import router as workspace_router +app.include_router(workspace_router) + +from routes.agent import router as agent_router +app.include_router(agent_router) + +from routes.file import router as file_router +app.include_router(file_router) + +from routes.prompt import router as prompt_router +app.include_router(prompt_router) + +from routes.workflow import router as workflow_router +app.include_router(workflow_router) + # Event handler beim Herunterfahren @app.on_event("shutdown") diff --git a/gwserver/modules/gateway_interface.py b/gwserver/modules/gateway_interface.py index d6978d44..6cc1509b 100644 --- a/gwserver/modules/gateway_interface.py +++ b/gwserver/modules/gateway_interface.py @@ -91,6 +91,7 @@ class GatewayInterface: "full_name": "Administrator", "disabled": False, "language": "de", + "privilege": "sysadmin", # SysAdmin-Berechtigung "hashed_password": self._get_password_hash("admin") # In der Produktion ein sicheres Passwort verwenden! } @@ -139,11 +140,94 @@ class GatewayInterface: return self.db.record_create("mandates", mandate_data) + def update_mandate(self, mandate_id: int, mandate_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Aktualisiert einen bestehenden Mandanten + + Args: + mandate_id: Die ID des zu aktualisierenden Mandanten + mandate_data: Die zu aktualisierenden Mandantendaten + + Returns: + Dict[str, Any]: Die aktualisierten Mandantendaten + + Raises: + ValueError: Wenn der Mandant nicht gefunden wurde + """ + # Prüfe, ob der Mandant existiert + mandate = self.get_mandate(mandate_id) + if not mandate: + raise ValueError(f"Mandant mit ID {mandate_id} nicht gefunden") + + # Aktualisiere den Mandanten + updated_mandate = self.db.record_modify("mandates", mandate_id, mandate_data) + + return updated_mandate + + def delete_mandate(self, mandate_id: int) -> bool: + """ + Löscht einen Mandanten und alle damit verbundenen Benutzer und Daten + + Args: + mandate_id: Die ID des zu löschenden Mandanten + + Returns: + bool: True, wenn der Mandant erfolgreich gelöscht wurde, sonst False + """ + # Prüfe, ob der Mandant existiert + mandate = self.get_mandate(mandate_id) + if not mandate: + return False + + # Root-Mandant darf nicht gelöscht werden + if mandate_id == 1: + logger.warning("Versuch, den Root-Mandanten zu löschen, wurde verhindert") + return False + + # Finde alle Benutzer des Mandanten + users = self.get_users_by_mandate(mandate_id) + + # Lösche alle Benutzer des Mandanten und ihre zugehörigen Daten + for user in users: + self.delete_user(user["id"]) + + # Lösche den Mandanten + success = self.db.record_delete("mandates", mandate_id) + + if success: + logger.info(f"Mandant mit ID {mandate_id} wurde erfolgreich gelöscht") + else: + logger.error(f"Fehler beim Löschen des Mandanten mit ID {mandate_id}") + + return success + # Benutzer-Methoden def get_all_users(self) -> List[Dict[str, Any]]: - """Gibt alle Benutzer des aktuellen Mandanten zurück""" - return self.db.get_recordset("users") + """Gibt alle Benutzer zurück""" + users = self.db.get_recordset("users") + # Entferne die Passwort-Hashes aus der Rückgabe + for user in users: + if "hashed_password" in user: + del user["hashed_password"] + return users + + def get_users_by_mandate(self, mandate_id: int) -> List[Dict[str, Any]]: + """ + Gibt alle Benutzer eines bestimmten Mandanten zurück + + Args: + mandate_id: Die ID des Mandanten + + Returns: + List[Dict[str, Any]]: Liste der Benutzer des Mandanten + """ + users = self.db.get_recordset("users", record_filter={"mandate_id": mandate_id}) + # Entferne die Passwort-Hashes aus der Rückgabe + for user in users: + if "hashed_password" in user: + del user["hashed_password"] + return users def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: """Gibt einen Benutzer anhand seines Benutzernamens zurück""" @@ -157,12 +241,37 @@ class GatewayInterface: """Gibt einen Benutzer anhand seiner ID zurück""" users = self.db.get_recordset("users", record_filter={"id": user_id}) if users: - return users[0] + user = users[0] + # Entferne das Passwort-Hash aus der Rückgabe für die API + if "hashed_password" in user: + user_copy = user.copy() + del user_copy["hashed_password"] + return user_copy + return user return None def create_user(self, username: str, password: str, email: str = None, - full_name: str = None, language: str = "de") -> Dict[str, Any]: - """Erstellt einen neuen Benutzer""" + full_name: str = None, language: str = "de", mandate_id: int = None, + disabled: bool = False, privilege: str = "user") -> Dict[str, Any]: + """ + Erstellt einen neuen Benutzer + + Args: + username: Der Benutzername + password: Das Passwort + email: Die E-Mail-Adresse (optional) + full_name: Der vollständige Name (optional) + language: Die bevorzugte Sprache (Standard: "de") + mandate_id: Die ID des Mandanten (optional) + disabled: Ob der Benutzer deaktiviert ist (Standard: False) + privilege: Die Berechtigungsstufe (Standard: "user") + + Returns: + Dict[str, Any]: Die erstellten Benutzerdaten + + Raises: + ValueError: Wenn der Benutzername bereits existiert + """ # Prüfe, ob der Benutzername bereits existiert existing_user = self.get_user_by_username(username) if existing_user: @@ -174,14 +283,18 @@ class GatewayInterface: if users: next_id = max(user["id"] for user in users) + 1 + # Verwende den übergebenen mandate_id oder den aktuellen Kontext + user_mandate_id = mandate_id if mandate_id is not None else self.mandate_id + user_data = { "id": next_id, - "mandate_id": self.mandate_id, + "mandate_id": user_mandate_id, "username": username, "email": email, "full_name": full_name, - "disabled": False, + "disabled": disabled, "language": language, + "privilege": privilege, # Neue Eigenschaft "hashed_password": self._get_password_hash(password) } @@ -194,7 +307,16 @@ class GatewayInterface: return created_user def authenticate_user(self, username: str, password: str) -> Optional[Dict[str, Any]]: - """Authentifiziert einen Benutzer anhand von Benutzername und Passwort""" + """ + Authentifiziert einen Benutzer anhand von Benutzername und Passwort + + Args: + username: Der Benutzername + password: Das Passwort + + Returns: + Optional[Dict[str, Any]]: Die Benutzerdaten oder None, wenn die Authentifizierung fehlschlägt + """ user = self.get_user_by_username(username) if not user: @@ -203,6 +325,10 @@ class GatewayInterface: if not self._verify_password(password, user.get("hashed_password", "")): return None + # Prüfe, ob der Benutzer deaktiviert ist + if user.get("disabled", False): + return None + # Erstelle eine Kopie ohne Passwort-Hash authenticated_user = {**user} if "hashed_password" in authenticated_user: @@ -211,12 +337,26 @@ class GatewayInterface: return authenticated_user def update_user(self, user_id: int, user_data: Dict[str, Any]) -> Dict[str, Any]: - """Aktualisiert einen Benutzer""" - # Hole den aktuellen Benutzer - user = self.get_user(user_id) - if not user: + """ + Aktualisiert einen Benutzer + + Args: + user_id: Die ID des zu aktualisierenden Benutzers + user_data: Die zu aktualisierenden Benutzerdaten + + Returns: + Dict[str, Any]: Die aktualisierten Benutzerdaten + + Raises: + ValueError: Wenn der Benutzer nicht gefunden wurde + """ + # Hole den aktuellen Benutzer mit Hash-Passwort (direkt aus der DB) + users = self.db.get_recordset("users", record_filter={"id": user_id}) + if not users: raise ValueError(f"Benutzer mit ID {user_id} nicht gefunden") + user = users[0] + # Wenn das Passwort geändert werden soll, hashe es if "password" in user_data: user_data["hashed_password"] = self._get_password_hash(user_data["password"]) @@ -238,6 +378,92 @@ class GatewayInterface: def enable_user(self, user_id: int) -> Dict[str, Any]: """Aktiviert einen Benutzer""" return self.update_user(user_id, {"disabled": False}) + + def _delete_user_referenced_data(self, user_id: int) -> None: + """ + Löscht alle Daten, die mit einem Benutzer verbunden sind + + Args: + user_id: Die ID des Benutzers + """ + # Hier werden alle Tabellen durchsucht und alle Einträge gelöscht, + # die auf diesen Benutzer verweisen + + # Workspaces des Benutzers finden und löschen + try: + workspaces = self.db.get_recordset("workspaces", record_filter={"user_id": user_id}) + for workspace in workspaces: + # Agenten in diesem Workspace löschen + agents = self.db.get_recordset("agents", record_filter={"workspace_id": workspace["id"]}) + for agent in agents: + # Hier könnten weitere abhängige Objekte gelöscht werden + self.db.record_delete("agents", agent["id"]) + + # Dateien in diesem Workspace löschen + files = self.db.get_recordset("files", record_filter={"workspace_id": workspace["id"]}) + for file in files: + self.db.record_delete("files", file["id"]) + + # Prompts in diesem Workspace löschen + prompts = self.db.get_recordset("prompts", record_filter={"workspace_id": workspace["id"]}) + for prompt in prompts: + self.db.record_delete("prompts", prompt["id"]) + + # Workflows in diesem Workspace löschen + workflows = self.db.get_recordset("workflows", record_filter={"workspace_id": workspace["id"]}) + for workflow in workflows: + self.db.record_delete("workflows", workflow["id"]) + + # Den Workspace selbst löschen + self.db.record_delete("workspaces", workspace["id"]) + except Exception as e: + logger.error(f"Fehler beim Löschen der Workspaces für Benutzer {user_id}: {e}") + + # Attribute des Benutzers löschen + try: + attributes = self.db.get_recordset("attributes", record_filter={"user_id": user_id}) + for attribute in attributes: + self.db.record_delete("attributes", attribute["id"]) + except Exception as e: + logger.error(f"Fehler beim Löschen der Attribute für Benutzer {user_id}: {e}") + + # Weitere Tabellen, die auf den Benutzer verweisen könnten + # (Je nach Datenbankstruktur der Anwendung) + + logger.info(f"Alle referenzierten Daten für Benutzer {user_id} wurden gelöscht") + + def delete_user(self, user_id: int) -> bool: + """ + Löscht einen Benutzer und alle damit verbundenen Daten + + Args: + user_id: Die ID des zu löschenden Benutzers + + Returns: + bool: True, wenn der Benutzer erfolgreich gelöscht wurde, sonst False + """ + # Prüfe, ob der Benutzer existiert + users = self.db.get_recordset("users", record_filter={"id": user_id}) + if not users: + return False + + # Root-Admin (ID 1) darf nicht gelöscht werden + if user_id == 1: + logger.warning("Versuch, den Root-Admin zu löschen, wurde verhindert") + return False + + # Lösche alle mit dem Benutzer verbundenen Daten + self._delete_user_referenced_data(user_id) + + # Lösche den Benutzer + success = self.db.record_delete("users", user_id) + + if success: + logger.info(f"Benutzer mit ID {user_id} wurde erfolgreich gelöscht") + else: + logger.error(f"Fehler beim Löschen des Benutzers mit ID {user_id}") + + return success # Singleton-Factory für GatewayInterface-Instanzen pro Kontext @@ -253,6 +479,5 @@ def get_gateway_interface(mandate_id: int = None, user_id: int = None) -> Gatewa _gateway_interfaces[context_key] = GatewayInterface(mandate_id, user_id) return _gateway_interfaces[context_key] -#Init -get_gateway_interface() - +# Init +get_gateway_interface() \ No newline at end of file diff --git a/gwserver/modules/gateway_model.py b/gwserver/modules/gateway_model.py index 4ab16aa8..5a476fcb 100644 --- a/gwserver/modules/gateway_model.py +++ b/gwserver/modules/gateway_model.py @@ -40,8 +40,9 @@ class User(BaseModel): username: str = Field(description="Benutzername für die Anmeldung") email: Optional[str] = Field(None, description="E-Mail-Adresse des Benutzers") full_name: Optional[str] = Field(None, description="Vollständiger Name des Benutzers") - disabled: Optional[bool] = Field(False, description="Gibt an, ob der Benutzer deaktiviert ist") language: str = Field(description="Bevorzugte Sprache des Benutzers") + disabled: Optional[bool] = Field(False, description="Gibt an, ob der Benutzer deaktiviert ist") + privilege: str = Field(description="Berechtigungsstufe") #sysadmin,admin,user label: Label = Field( default=Label(default="Benutzer", translations={"en": "User", "fr": "Utilisateur"}), @@ -55,8 +56,9 @@ class User(BaseModel): "username": Label(default="Benutzername", translations={"en": "Username", "fr": "Nom d'utilisateur"}), "email": Label(default="E-Mail", translations={"en": "Email", "fr": "E-mail"}), "full_name": Label(default="Vollständiger Name", translations={"en": "Full name", "fr": "Nom complet"}), + "language": Label(default="Sprache", translations={"en": "Language", "fr": "Langue"}), "disabled": Label(default="Deaktiviert", translations={"en": "Disabled", "fr": "Désactivé"}), - "language": Label(default="Sprache", translations={"en": "Language", "fr": "Langue"}) + "privilege": Label(default="Berechtigungsstufe", translations={"en": "Access level", "fr": "Niveau d'accès"}), } diff --git a/gwserver/modules/lucydom_model.py b/gwserver/modules/lucydom_model.py index 945421d4..e93297e9 100644 --- a/gwserver/modules/lucydom_model.py +++ b/gwserver/modules/lucydom_model.py @@ -83,7 +83,6 @@ class Prompt(BaseModel): mandate_id: int = Field(description="ID des zugehörigen Mandanten") user_id: int = Field(description="ID des Erstellers") content: str = Field(description="Inhalt des Prompts") - created_at: str = Field(description="Erstellungszeitpunkt") label: Label = Field( default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}), @@ -96,7 +95,6 @@ class Prompt(BaseModel): "mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}), "user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}), "content": Label(default="Inhalt", translations={"en": "Content", "fr": "Contenu"}), - "created_at": Label(default="Erstellt am", translations={"en": "Created at", "fr": "Créé le"}) } @@ -106,7 +104,6 @@ class Workspace(BaseModel): mandate_id: int = Field(description="ID des zugehörigen Mandanten") user_id: int = Field(description="ID des Erstellers") name: str = Field(description="Name des Workspaces") - created_at: str = Field(description="Erstellungszeitpunkt") prompts: List[Prompt] = Field(default=[], description="Liste der Prompts im Workspace") agents: List[Agent] = Field(default=[], description="Liste der Agenten im Workspace") dataObjectReferences: List[int] = Field(default=[], description="Referenzen zu Datenobjekten") @@ -122,7 +119,6 @@ class Workspace(BaseModel): "mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}), "user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}), "name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}), - "created_at": Label(default="Erstellt am", translations={"en": "Created at", "fr": "Créé le"}), "prompts": Label(default="Prompts", translations={"en": "Prompts", "fr": "Invites"}), "agents": Label(default="Agenten", translations={"en": "Agents", "fr": "Agents"}), "dataObjectReferences": Label(default="Datenobjekte", translations={"en": "Data Objects", "fr": "Objets de données"}) diff --git a/gwserver/routes/attributes.py b/gwserver/routes/attributes.py index 2544161d..8f07aace 100644 --- a/gwserver/routes/attributes.py +++ b/gwserver/routes/attributes.py @@ -33,16 +33,6 @@ router = APIRouter( responses={404: {"description": "Not found"}} ) -@router.get("/test", response_model=str) -async def get_entity_test(s:str): - return s+s - -@router.options("/{entity_type}") -async def options_entity_attributes( - entity_type: str = Path(..., description="Typ der Entität (z.B. workspace, agent, prompt)") -): - return Response(status_code=200) - @router.get("/{entity_type}", response_model=List[AttributeDefinition]) async def get_entity_attributes( entity_type: str = Path(..., description="Typ der Entität (z.B. workspace, agent, prompt)"), @@ -108,4 +98,12 @@ async def get_entity_attributes( for agent in agents ] - return visible_attributes \ No newline at end of file + return visible_attributes + + +@router.options("/{entity_type}") +async def options_entity_attributes( + entity_type: str = Path(..., description="Typ der Entität (z.B. workspace, agent, prompt)") +): + return Response(status_code=200) + diff --git a/gwserver/routes/mandate.py b/gwserver/routes/mandate.py index 9c5f7b7c..06bae432 100644 --- a/gwserver/routes/mandate.py +++ b/gwserver/routes/mandate.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends, Body +from fastapi import APIRouter, HTTPException, Depends, Body, Path from typing import List, Dict, Any from fastapi import status @@ -17,13 +17,18 @@ router = APIRouter( @router.get("", response_model=List[Dict[str, Any]]) async def get_mandates(current_user: Dict[str, Any] = Depends(get_current_active_user)): - """Alle verfügbaren Mandanten abrufen (nur für Admin-Benutzer)""" + """Alle verfügbaren Mandanten abrufen (nur für SysAdmin-Benutzer)""" mandate_id, user_id = await get_user_context(current_user) # Gateway-Interface mit Benutzerkontext initialisieren gateway = get_gateway_interface(mandate_id, user_id) - # TODO: Hier sollte eine Berechtigungsprüfung erfolgen + # Berechtigungsprüfung + if current_user.get("privilege") != "sysadmin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Nur System-Administratoren können alle Mandanten abrufen" + ) return gateway.get_all_mandates() @@ -33,13 +38,18 @@ async def create_mandate( mandate: Dict[str, Any] = Body(...), current_user: Dict[str, Any] = Depends(get_current_active_user) ): - """Einen neuen Mandanten erstellen (nur für Admin-Benutzer)""" + """Einen neuen Mandanten erstellen (nur für SysAdmin-Benutzer)""" mandate_id, user_id = await get_user_context(current_user) # Gateway-Interface mit Benutzerkontext initialisieren gateway = get_gateway_interface(mandate_id, user_id) - # TODO: Hier sollte eine Berechtigungsprüfung erfolgen + # Berechtigungsprüfung + if current_user.get("privilege") != "sysadmin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Nur System-Administratoren können Mandanten erstellen" + ) new_mandate = gateway.create_mandate( name=mandate.get("name", "Neuer Mandant"), @@ -60,8 +70,17 @@ async def get_mandate( # Gateway-Interface mit Benutzerkontext initialisieren gateway = get_gateway_interface(user_mandate_id, user_id) - # TODO: Hier sollte eine Berechtigungsprüfung erfolgen - # Nur Admins oder Benutzer des gleichen Mandanten dürfen den Mandanten sehen + # Berechtigungsprüfung + # Admin darf nur seinen eigenen Mandanten sehen, SysAdmin alle + is_admin = current_user.get("privilege") == "admin" + is_sysadmin = current_user.get("privilege") == "sysadmin" + is_own_mandate = user_mandate_id == mandate_id + + if (is_admin and not is_own_mandate) and not is_sysadmin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Abrufen dieses Mandanten" + ) mandate = gateway.get_mandate(mandate_id) if not mandate: @@ -70,4 +89,84 @@ async def get_mandate( detail=f"Mandant mit ID {mandate_id} nicht gefunden" ) - return mandate \ No newline at end of file + return mandate + +@router.post("/{mandate_id}", response_model=Dict[str, Any]) +async def update_mandate( + mandate_id: int = Path(..., description="ID des zu aktualisierenden Mandanten"), + mandate_data: Dict[str, Any] = Body(..., description="Aktualisierte Mandantendaten"), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen bestehenden Mandanten aktualisieren""" + user_mandate_id, user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(user_mandate_id, user_id) + + # Mandant existiert? + mandate = gateway.get_mandate(mandate_id) + if not mandate: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Mandant mit ID {mandate_id} nicht gefunden" + ) + + # Berechtigungsprüfung + is_admin = current_user.get("privilege") == "admin" + is_sysadmin = current_user.get("privilege") == "sysadmin" + is_own_mandate = user_mandate_id == mandate_id + + if (is_admin and not is_own_mandate) and not is_sysadmin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Aktualisieren dieses Mandanten" + ) + + # Mandant aktualisieren + updated_mandate = gateway.update_mandate( + mandate_id=mandate_id, + mandate_data=mandate_data + ) + + return updated_mandate + +@router.delete("/{mandate_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_mandate( + mandate_id: int = Path(..., description="ID des zu löschenden Mandanten"), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen Mandanten löschen, inklusive aller zugehörigen Benutzer und referenzierten Objekte""" + user_mandate_id, user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(user_mandate_id, user_id) + + # Mandant existiert? + mandate = gateway.get_mandate(mandate_id) + if not mandate: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Mandant mit ID {mandate_id} nicht gefunden" + ) + + # Berechtigungsprüfung + is_admin = current_user.get("privilege") == "admin" + is_sysadmin = current_user.get("privilege") == "sysadmin" + is_own_mandate = user_mandate_id == mandate_id + + if (is_admin and not is_own_mandate) and not is_sysadmin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Löschen dieses Mandanten" + ) + + # Mandant löschen + success = gateway.delete_mandate(mandate_id) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Fehler beim Löschen des Mandanten mit ID {mandate_id}" + ) + + # Kein Inhalt zurückgeben bei erfolgreichem Löschen + return None \ No newline at end of file diff --git a/gwserver/routes/users.py b/gwserver/routes/users.py new file mode 100644 index 00000000..30422963 --- /dev/null +++ b/gwserver/routes/users.py @@ -0,0 +1,214 @@ +from fastapi import APIRouter, HTTPException, Depends, Body, Path +from typing import List, Dict, Any, Optional +from fastapi import status + +# Import auth module +from auth import get_current_active_user, get_user_context + +# Import interfaces +from modules.gateway_interface import get_gateway_interface + +# Router für Benutzer-Endpunkte erstellen +router = APIRouter( + prefix="/api/users", + tags=["Users"], + responses={404: {"description": "Not found"}} +) + +@router.get("", response_model=List[Dict[str, Any]]) +async def get_users(current_user: Dict[str, Any] = Depends(get_current_active_user)): + """Alle verfügbaren Benutzer abrufen (nur für Admin/SysAdmin-Benutzer)""" + mandate_id, user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(mandate_id, user_id) + + # Berechtigungsprüfung + if current_user.get("privilege") not in ["admin", "sysadmin"]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Abrufen der Benutzerliste" + ) + + # Admin sieht nur Benutzer des eigenen Mandanten, SysAdmin sieht alle + if current_user.get("privilege") == "admin": + return gateway.get_users_by_mandate(mandate_id) + else: # sysadmin + return gateway.get_all_users() + +@router.post("/register", response_model=Dict[str, Any]) +async def register_user(user_data: dict = Body(...)): + """Neuen Benutzer registrieren""" + # Bei der Registrierung keinen Benutzerkontext verwenden + gateway = get_gateway_interface() + + if "username" not in user_data or "password" not in user_data: + raise HTTPException(status_code=400, detail="Benutzername und Passwort erforderlich") + + try: + new_mandate = gateway.create_mandate( + name=f"Mandant von {user_data['username']}", + language=user_data.get("language", "de") + ) + + new_user = gateway.create_user( + username=user_data["username"], + password=user_data["password"], + email=user_data.get("email"), + full_name=user_data.get("full_name"), + language=user_data.get("language", "de"), + mandate_id=new_mandate["id"], + disabled=False, + privilege="user" + ) + return new_user + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/{user_id}", response_model=Dict[str, Any]) +async def get_user( + user_id: int, + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen bestimmten Benutzer abrufen""" + mandate_id, current_user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(mandate_id, current_user_id) + + # Berechtigungsprüfung + # Benutzer darf nur sich selbst abrufen, Admin nur Benutzer des eigenen Mandanten, SysAdmin alle + user_to_get = gateway.get_user(user_id) + if not user_to_get: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benutzer mit ID {user_id} nicht gefunden" + ) + + if user_id == current_user_id: + # Benutzer darf sich selbst abrufen + pass + elif current_user.get("privilege") == "admin" and user_to_get.get("mandate_id") == mandate_id: + # Admin darf Benutzer des eigenen Mandanten abrufen + pass + elif current_user.get("privilege") == "sysadmin": + # SysAdmin darf alle Benutzer abrufen + pass + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Abrufen dieses Benutzers" + ) + + return user_to_get + +@router.post("/{user_id}", response_model=Dict[str, Any]) +async def update_user( + user_id: int = Path(..., description="ID des zu aktualisierenden Benutzers"), + user_data: Dict[str, Any] = Body(..., description="Aktualisierte Benutzerdaten"), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen bestehenden Benutzer aktualisieren""" + mandate_id, current_user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(mandate_id, current_user_id) + + # Benutzer existiert? + user_to_update = gateway.get_user(user_id) + if not user_to_update: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benutzer mit ID {user_id} nicht gefunden" + ) + + # Berechtigungsprüfung + is_self_update = user_id == current_user_id + is_admin = current_user.get("privilege") == "admin" + is_sysadmin = current_user.get("privilege") == "sysadmin" + same_mandate = user_to_update.get("mandate_id") == mandate_id + + # Filtere erlaubte Felder je nach Berechtigungsstufe + allowed_fields = {"username", "email", "full_name", "language"} + sensitive_fields = {"mandate_id", "disabled", "privilege"} + + # Prüfe, ob sensitive Felder geändert werden sollen + sensitive_update = any(field in user_data for field in sensitive_fields) + + if is_self_update and sensitive_update: + # Normale Benutzer dürfen ihre sensitiven Daten nicht ändern + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Ändern sensitiver Benutzerdaten" + ) + elif is_admin and sensitive_update and not same_mandate: + # Admins dürfen sensitive Daten nur für Benutzer des eigenen Mandanten ändern + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Ändern sensitiver Daten für Benutzer anderer Mandanten" + ) + elif not (is_self_update or (is_admin and same_mandate) or is_sysadmin): + # Keine Berechtigung für andere Fälle + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Aktualisieren dieses Benutzers" + ) + + # Entferne nicht erlaubte Felder für normale Benutzer + if not (is_admin or is_sysadmin): + user_data = {k: v for k, v in user_data.items() if k in allowed_fields} + + # User-Daten aktualisieren + updated_user = gateway.update_user(user_id, user_data) + return updated_user + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_user( + user_id: int = Path(..., description="ID des zu löschenden Benutzers"), + current_user: Dict[str, Any] = Depends(get_current_active_user) +): + """Einen Benutzer löschen""" + mandate_id, current_user_id = await get_user_context(current_user) + + # Gateway-Interface mit Benutzerkontext initialisieren + gateway = get_gateway_interface(mandate_id, current_user_id) + + # Benutzer existiert? + user_to_delete = gateway.get_user(user_id) + if not user_to_delete: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Benutzer mit ID {user_id} nicht gefunden" + ) + + # Berechtigungsprüfung + is_self_delete = user_id == current_user_id + is_admin = current_user.get("privilege") == "admin" + is_sysadmin = current_user.get("privilege") == "sysadmin" + same_mandate = user_to_delete.get("mandate_id") == mandate_id + + if is_self_delete: + # Benutzer darf sich selbst löschen + pass + elif is_admin and same_mandate: + # Admin darf Benutzer des eigenen Mandanten löschen + pass + elif is_sysadmin: + # SysAdmin darf alle Benutzer löschen + pass + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Keine Berechtigung zum Löschen dieses Benutzers" + ) + + # Benutzer und alle referenzierten Objekte löschen + success = gateway.delete_user(user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Fehler beim Löschen des Benutzers mit ID {user_id}" + ) + + # Kein Inhalt zurückgeben bei erfolgreichem Löschen + return None \ No newline at end of file