from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import Session, select from typing import List from app.database import get_session from app.models import User, Department, UserDepartmentLink, Channel, Snippet, SnippetDepartmentLink, Language, Translation from app.schemas import ( DepartmentCreate, DepartmentResponse, ChannelCreate, ChannelResponse, UserResponse, LanguageCreate, LanguageResponse, TranslationGroupResponse, TranslationUpdateRequest ) from app.auth import get_current_user from pydantic import BaseModel from app.services.translations import ( ensure_default_languages, ensure_translation_entries, ensure_translations_for_language, get_translation_blueprint, update_translation_timestamp, ) router = APIRouter(prefix="/admin", tags=["Admin"]) class UserDepartmentAssignment(BaseModel): user_id: int department_id: int class UserAdminUpdate(BaseModel): user_id: int is_admin: bool class SnippetDepartmentAccess(BaseModel): snippet_id: int department_id: int enabled: bool def require_admin(current_user: User = Depends(get_current_user)) -> User: """Verify that the current user is an admin""" if not current_user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required" ) return current_user # ========== User Management ========== @router.get("/users", response_model=List[UserResponse]) def get_all_users( session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Get all users (Admin only)""" statement = select(User) users = session.exec(statement).all() return users @router.patch("/users/{user_id}/admin") def toggle_admin_status( user_id: int, is_admin: bool, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Make a user admin or remove admin privileges""" user = session.get(User, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) user.is_admin = is_admin session.add(user) session.commit() session.refresh(user) return {"message": f"User {user.username} admin status updated", "is_admin": is_admin} # ========== Department Management ========== @router.post("/departments", response_model=DepartmentResponse, status_code=status.HTTP_201_CREATED) def create_department( department_data: DepartmentCreate, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Create a new department (Admin only)""" department = Department(**department_data.model_dump()) session.add(department) session.commit() session.refresh(department) return department @router.get("/departments", response_model=List[DepartmentResponse]) def get_all_departments( session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Get all departments (Admin only)""" statement = select(Department) departments = session.exec(statement).all() return departments @router.put("/departments/{department_id}", response_model=DepartmentResponse) def update_department( department_id: int, department_data: DepartmentCreate, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Update a department (Admin only)""" department = session.get(Department, department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) # Update fields department.name = department_data.name department.description = department_data.description session.add(department) session.commit() session.refresh(department) return department @router.patch("/departments/{department_id}/snippets") def toggle_department_snippets( department_id: int, enabled: bool, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Enable or disable snippet access for entire department (master switch)""" department = session.get(Department, department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) department.snippets_enabled = enabled session.add(department) session.commit() session.refresh(department) return { "message": f"Snippet access {'enabled' if enabled else 'disabled'} for department {department.name}", "department_id": department_id, "snippets_enabled": enabled } @router.delete("/departments/{department_id}") def delete_department( department_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Delete a department (Admin only)""" department = session.get(Department, department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) # Check if department has channels channels_statement = select(Channel).where(Channel.department_id == department_id) channels = session.exec(channels_statement).all() if channels: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot delete department. It has {len(channels)} channel(s). Please delete channels first." ) # Delete user-department associations links_statement = select(UserDepartmentLink).where(UserDepartmentLink.department_id == department_id) links = session.exec(links_statement).all() for link in links: session.delete(link) # Now delete the department session.delete(department) session.commit() return {"message": f"Department '{department.name}' deleted successfully"} # ========== User-Department Assignment ========== @router.post("/departments/{department_id}/members") def assign_user_to_department( department_id: int, user_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Assign a user to a department (Admin only)""" # Check if department exists department = session.get(Department, department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) # Check if user exists user = session.get(User, user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) # Check if already assigned statement = select(UserDepartmentLink).where( UserDepartmentLink.user_id == user_id, UserDepartmentLink.department_id == department_id ) existing = session.exec(statement).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User already assigned to this department" ) # Create assignment link = UserDepartmentLink(user_id=user_id, department_id=department_id) session.add(link) session.commit() return {"message": f"User {user.username} assigned to {department.name}"} @router.delete("/departments/{department_id}/members/{user_id}") def remove_user_from_department( department_id: int, user_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Remove a user from a department (Admin only)""" statement = select(UserDepartmentLink).where( UserDepartmentLink.user_id == user_id, UserDepartmentLink.department_id == department_id ) link = session.exec(statement).first() if not link: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not assigned to this department" ) session.delete(link) session.commit() return {"message": "User removed from department"} @router.get("/departments/{department_id}/members", response_model=List[UserResponse]) def get_department_members( department_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Get all members of a department (Admin only)""" department = session.get(Department, department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) return department.users # ========== Channel Management ========== @router.get("/channels", response_model=List[ChannelResponse]) def get_all_channels( session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Get all channels (Admin only)""" statement = select(Channel) channels = session.exec(statement).all() return channels @router.post("/channels", response_model=ChannelResponse, status_code=status.HTTP_201_CREATED) def create_channel( channel_data: ChannelCreate, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Create a new channel (Admin only)""" # Verify department exists department = session.get(Department, channel_data.department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) channel = Channel(**channel_data.model_dump()) session.add(channel) session.commit() session.refresh(channel) return channel @router.delete("/channels/{channel_id}") def delete_channel( channel_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Delete a channel (Admin only)""" channel = session.get(Channel, channel_id) if not channel: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Channel not found" ) session.delete(channel) session.commit() return {"message": f"Channel '{channel.name}' deleted"} # ========== Language & Translation Management ========== @router.get("/languages", response_model=List[LanguageResponse]) def get_languages( session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """List all available UI languages.""" ensure_default_languages(session) statement = select(Language).order_by(Language.name) return session.exec(statement).all() @router.post("/languages", response_model=LanguageResponse, status_code=status.HTTP_201_CREATED) def create_language( language_data: LanguageCreate, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Create a new UI language.""" code = language_data.code.strip().lower() name = language_data.name.strip() if not code or not name: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Code und Name sind erforderlich" ) if " " in code: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Der Sprachcode darf keine Leerzeichen enthalten" ) ensure_default_languages(session) existing_code = session.exec( select(Language).where(Language.code == code) ).first() if existing_code: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Es existiert bereits eine Sprache mit diesem Code" ) language = Language(code=code, name=name) session.add(language) session.commit() session.refresh(language) ensure_translation_entries(session) ensure_translations_for_language(session, language) return language @router.delete("/languages/{language_id}") def delete_language( language_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Remove a UI language.""" language = session.get(Language, language_id) if not language: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Sprache nicht gefunden" ) if language.is_default: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Die Standardsprache kann nicht gelöscht werden" ) translations = session.exec( select(Translation).where(Translation.language_id == language.id) ).all() for translation in translations: session.delete(translation) session.delete(language) session.commit() return {"message": "Sprache gelöscht"} @router.get("/translations", response_model=List[TranslationGroupResponse]) def get_translations( session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Retrieve translation values grouped by attribute.""" ensure_default_languages(session) ensure_translation_entries(session) languages = session.exec(select(Language).order_by(Language.name)).all() translations = session.exec(select(Translation)).all() translations_by_key = {} for translation in translations: translations_by_key.setdefault(translation.key, []).append(translation) blueprint = get_translation_blueprint() response: List[TranslationGroupResponse] = [] created_entries = False for blueprint_entry in blueprint: key = blueprint_entry["key"] entries = [] existing = {t.language_id: t for t in translations_by_key.get(key, [])} for language in languages: translation = existing.get(language.id) if not translation: translation = Translation(key=key, language_id=language.id, value="") session.add(translation) session.flush() existing[language.id] = translation created_entries = True entries.append( { "translation_id": translation.id, "language_id": language.id, "language_code": language.code, "language_name": language.name, "value": translation.value, } ) response.append( TranslationGroupResponse( key=key, label=blueprint_entry.get("label", key), description=blueprint_entry.get("description"), entries=entries, ) ) if created_entries: session.commit() return response @router.put("/translations") def update_translation( payload: TranslationUpdateRequest, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Update a single translation entry.""" translation = session.get(Translation, payload.translation_id) if not translation: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Übersetzung nicht gefunden" ) translation.value = payload.value update_translation_timestamp(translation) session.add(translation) session.commit() session.refresh(translation) return { "translation_id": translation.id, "value": translation.value, "updated_at": translation.updated_at, } # ========== Snippet Department Access Management ========== @router.get("/snippets/{snippet_id}/departments") def get_snippet_departments( snippet_id: int, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Get all departments and their access status for a snippet. By default, snippets are disabled for all departments. Admins must explicitly enable access via department editing.""" snippet = session.get(Snippet, snippet_id) if not snippet: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Snippet not found" ) # Get all departments all_departments = session.exec(select(Department)).all() # Get snippet-department links (only enabled ones exist in DB by default) links_statement = select(SnippetDepartmentLink).where( SnippetDepartmentLink.snippet_id == snippet_id ) links = {link.department_id: link.enabled for link in session.exec(links_statement).all()} result = [] for dept in all_departments: result.append({ "department_id": dept.id, "department_name": dept.name, "enabled": links.get(dept.id, False) # Default: False (disabled) }) return result @router.post("/snippets/departments/toggle") def toggle_snippet_department_access( access_data: SnippetDepartmentAccess, session: Session = Depends(get_session), admin: User = Depends(require_admin) ): """Enable or disable a snippet for a specific department. By default, all snippets are disabled for all departments. This endpoint allows admins to explicitly grant or revoke access.""" # Verify snippet exists snippet = session.get(Snippet, access_data.snippet_id) if not snippet: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Snippet not found" ) # Verify department exists department = session.get(Department, access_data.department_id) if not department: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Department not found" ) # Check if link exists statement = select(SnippetDepartmentLink).where( SnippetDepartmentLink.snippet_id == access_data.snippet_id, SnippetDepartmentLink.department_id == access_data.department_id ) link = session.exec(statement).first() if link: if access_data.enabled: # Update to enabled link.enabled = access_data.enabled session.add(link) else: # If disabling, remove the link entirely (cleaner approach) session.delete(link) else: # Only create new link if enabling if access_data.enabled: new_link = SnippetDepartmentLink( snippet_id=access_data.snippet_id, department_id=access_data.department_id, enabled=True ) session.add(new_link) # If disabling and no link exists, nothing to do (already disabled by default) session.commit() return { "message": f"Snippet access {'enabled' if access_data.enabled else 'disabled'} for department", "snippet_id": access_data.snippet_id, "department_id": access_data.department_id, "enabled": access_data.enabled }