diff --git a/src/server/operandi_server/models/__init__.py b/src/server/operandi_server/models/__init__.py index 0446ac98..50c33379 100644 --- a/src/server/operandi_server/models/__init__.py +++ b/src/server/operandi_server/models/__init__.py @@ -1,6 +1,7 @@ __all__ = [ "PYDiscovery", "PYUserAction", + "PYUserInfo", "Resource", "SbatchArguments", "WorkflowArguments", @@ -11,6 +12,7 @@ from .base import Resource, SbatchArguments, WorkflowArguments from .discovery import PYDiscovery -from .user import PYUserAction +from .user import PYUserAction, PYUserInfo from .workflow import WorkflowRsrc, WorkflowJobRsrc from .workspace import WorkspaceRsrc + diff --git a/src/server/operandi_server/models/user.py b/src/server/operandi_server/models/user.py index e7ad35dc..1bbf83f7 100644 --- a/src/server/operandi_server/models/user.py +++ b/src/server/operandi_server/models/user.py @@ -25,3 +25,26 @@ def from_db_user_account(action: str, db_user_account: DBUserAccount): details=db_user_account.details, action=action ) + + +class PYUserInfo(BaseModel): + institution_id: str = Field(..., description="Institution id of the user") + user_id: str = Field(..., description="Unique id of the user") + email: str = Field(..., description="Email linked to this User") + account_type: AccountType = Field(AccountType.UNSET, description="The type of this user") + approved_user: bool = Field(False, description="Whether the account was admin approved and fully functional") + details: str = Field(..., description="More details about the account") + + class Config: + allow_population_by_field_name = True + + @staticmethod + def from_db_user_account(db_user_account: DBUserAccount): + return PYUserInfo( + institution_id=db_user_account.institution_id, + user_id=db_user_account.user_id, + email=db_user_account.email, + account_type=db_user_account.account_type, + approved_user=db_user_account.approved_user, + details=db_user_account.details + ) diff --git a/src/server/operandi_server/routers/admin_panel.py b/src/server/operandi_server/routers/admin_panel.py index 6425347a..65fc8313 100644 --- a/src/server/operandi_server/routers/admin_panel.py +++ b/src/server/operandi_server/routers/admin_panel.py @@ -2,7 +2,9 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials +from operandi_server.models import PYUserInfo from operandi_utils.constants import AccountType, ServerApiTag +from operandi_utils.database import db_get_all_user_accounts, db_get_processing_stats from operandi_utils.utils import send_bag_to_ola_hd from .user import RouterUser from .workspace_utils import create_workspace_bag, get_db_workspace_with_handling, validate_bag_with_handling @@ -18,6 +20,16 @@ def __init__(self): endpoint=self.push_to_ola_hd, methods=["POST"], status_code=status.HTTP_201_CREATED, summary="Push a workspace to Ola-HD service" ) + self.router.add_api_route( + path="/admin/users", + endpoint=self.get_users, methods=["GET"], status_code=status.HTTP_200_OK, + summary="Get all registered users" + ) + self.router.add_api_route( + path="/admin/processing_stats/{user_id}", + endpoint=self.get_processing_stats_for_user, methods=["GET"], status_code=status.HTTP_200_OK, + summary="Get processing stats for a specific user by user_id" + ) async def push_to_ola_hd(self, workspace_id: str, auth: HTTPBasicCredentials = Depends(HTTPBasic())): py_user_action = await self.user_authenticator.user_login(auth) @@ -46,3 +58,37 @@ async def push_to_ola_hd(self, workspace_id: str, auth: HTTPBasicCredentials = D "pid": pid } return response_message + + async def get_users(self, auth: HTTPBasicCredentials = Depends(HTTPBasic())): + # Authenticate the user and ensure they have admin privileges + py_user_action = await self.user_authenticator.user_login(auth) + if py_user_action.account_type != AccountType.ADMIN: + message = "Admin privileges required for the endpoint" + self.logger.error(message) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message) + + users = await db_get_all_user_accounts() + return [PYUserInfo.from_db_user_account(user) for user in users] + + async def get_processing_stats_for_user(self, user_id: str, auth: HTTPBasicCredentials = Depends(HTTPBasic())): + # Authenticate the admin user + py_user_action = await self.user_authenticator.user_login(auth) + if py_user_action.account_type != AccountType.ADMIN: + message = f"Admin privileges required for the endpoint" + self.logger.error(f"{message}") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message) + + # Retrieve the processing stats for the specified user + try: + db_processing_stats = await db_get_processing_stats(user_id) + if not db_processing_stats: + message = f"Processing stats not found for the user_id: {user_id}" + self.logger.error(message) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message) + except Exception as error: + message = f"Failed to fetch processing stats for user_id: {user_id}, error: {error}" + self.logger.error(message) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=message) + + # Return the processing stats in the response model + return db_processing_stats \ No newline at end of file diff --git a/src/server/operandi_server/routers/discovery.py b/src/server/operandi_server/routers/discovery.py index 911129db..e510361d 100644 --- a/src/server/operandi_server/routers/discovery.py +++ b/src/server/operandi_server/routers/discovery.py @@ -1,13 +1,16 @@ """ module for implementing the discovery section of the api """ +from json import JSONDecodeError from logging import getLogger from os import cpu_count from psutil import virtual_memory -from fastapi import APIRouter, Depends, status +from typing import List, Dict +from fastapi import APIRouter, Depends, status, HTTPException from fastapi.security import HTTPBasic, HTTPBasicCredentials from operandi_utils.constants import ServerApiTag +from operandi_utils.oton.constants import OCRD_ALL_JSON from operandi_server.models import PYDiscovery from .user import RouterUser @@ -24,6 +27,16 @@ def __init__(self): summary="List Operandi Server properties", response_model=PYDiscovery, response_model_exclude_unset=True, response_model_exclude_none=True ) + self.router.add_api_route( + path="/discovery/processors", + endpoint=self.get_processor_names, methods=["GET"], status_code=status.HTTP_200_OK, + summary="List OCR-D processor names" + ) + self.router.add_api_route( + path="/discovery/processor/{processor_name}", + endpoint=self.get_processor_info, methods=["GET"], status_code=status.HTTP_200_OK, + summary="Get information about a specific OCR-D processor" + ) async def discovery(self, auth: HTTPBasicCredentials = Depends(HTTPBasic())) -> PYDiscovery: await self.user_authenticator.user_login(auth) @@ -37,3 +50,50 @@ async def discovery(self, auth: HTTPBasicCredentials = Depends(HTTPBasic())) -> has_docker=False ) return response + + async def get_processor_names(self, auth: HTTPBasicCredentials = Depends(HTTPBasic())) -> List[str]: + # Authenticate the user + await self.user_authenticator.user_login(auth) + + try: + # Load JSON and extract processor names + processor_names = list(OCRD_ALL_JSON.keys()) + return processor_names + + + except JSONDecodeError as e: + # Raise a 500 error if the JSON is invalid or cannot be parsed + message = f"Error decoding processor data file: {str(e)}" + # Log the detailed message + self.logger.error(message) + # Raise the HTTPException with the same message + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=message + ) + + except Exception as e: + # Raise a generic 500 error for any other exceptions + self.logger.error(f"Unexpected error while loading processors: {e}") + raise HTTPException( + status_code=500, + detail="An unexpected error occurred while loading processor names." + ) + + async def get_processor_info(self, processor_name: str, auth: HTTPBasicCredentials = Depends(HTTPBasic())) -> Dict: + await self.user_authenticator.user_login(auth) + + try: + # Check if the processor name exists in the JSON + if processor_name not in OCRD_ALL_JSON: + raise HTTPException(status_code=404, detail=f"Processor '{processor_name}' not found.") + + # Retrieve processor information as a dictionary + processor_info = OCRD_ALL_JSON[processor_name] + return processor_info + + except JSONDecodeError: + raise HTTPException(status_code=500, detail="Error decoding processor data file.") + except Exception as e: + self.logger.error(f"Error retrieving processor info for {processor_name}: {e}") + raise HTTPException(status_code=500, detail="An unexpected error occurred.") diff --git a/src/utils/operandi_utils/database/__init__.py b/src/utils/operandi_utils/database/__init__.py index d3fbef59..c2639de0 100644 --- a/src/utils/operandi_utils/database/__init__.py +++ b/src/utils/operandi_utils/database/__init__.py @@ -12,6 +12,7 @@ "db_create_workspace", "db_get_hpc_slurm_job", "db_get_processing_stats", + "db_get_all_user_accounts", "db_get_user_account", "db_get_user_account_with_email", "db_get_workflow", @@ -33,6 +34,7 @@ "sync_db_create_workspace", "sync_db_get_hpc_slurm_job", "sync_db_get_processing_stats", + "sync_db_get_all_user_accounts", "sync_db_get_user_account", "sync_db_get_user_account_with_email", "sync_db_get_workflow", @@ -59,9 +61,11 @@ ) from .db_user_account import ( db_create_user_account, + db_get_all_user_accounts, db_get_user_account, db_get_user_account_with_email, db_update_user_account, + sync_db_get_all_user_accounts, sync_db_create_user_account, sync_db_get_user_account, sync_db_get_user_account_with_email, diff --git a/src/utils/operandi_utils/database/db_user_account.py b/src/utils/operandi_utils/database/db_user_account.py index d520d637..efe43b0e 100644 --- a/src/utils/operandi_utils/database/db_user_account.py +++ b/src/utils/operandi_utils/database/db_user_account.py @@ -1,5 +1,6 @@ from datetime import datetime from operandi_utils import call_sync, generate_id +from typing import List from ..constants import AccountType from .models import DBUserAccount @@ -32,6 +33,15 @@ async def sync_db_create_user_account( institution_id, email, encrypted_pass, salt, account_type, approved_user, details) +async def db_get_all_user_accounts() -> List[DBUserAccount]: + all_user_accounts = await DBUserAccount.find().to_list() + return all_user_accounts + +@call_sync +async def sync_db_get_all_user_accounts() -> List[DBUserAccount]: + return await db_get_all_user_accounts() + + async def db_get_user_account(user_id: str) -> DBUserAccount: db_user_account = await DBUserAccount.find_one(DBUserAccount.user_id == user_id) if not db_user_account: @@ -87,3 +97,4 @@ async def db_update_user_account(user_id: str, **kwargs) -> DBUserAccount: @call_sync async def sync_db_update_user_account(user_id: str, **kwargs) -> DBUserAccount: return await db_update_user_account(user_id=user_id, **kwargs) +