From 61229cb761feaf66e8563b769b0ed5ba63cebbba Mon Sep 17 00:00:00 2001 From: berkay Date: Thu, 16 Jan 2025 22:35:49 +0300 Subject: [PATCH] events auth repair --- ApiEvents/AuthServiceApi/auth/__init__.py | 9 + ApiEvents/AuthServiceApi/auth/auth.py | 530 +++--------------- ApiEvents/AuthServiceApi/auth/endpoints.py | 371 ++++++++++++ ApiEvents/AuthServiceApi/auth/models.py | 138 +++++ ApiEvents/AuthServiceApi/route_configs.py | 2 +- ApiEvents/EventServiceApi/account/__init__.py | 9 + .../account/account_records.py | 199 +------ .../EventServiceApi/account/endpoints.py | 129 +++++ ApiEvents/EventServiceApi/account/models.py | 49 ++ ApiEvents/EventServiceApi/route_configs.py | 2 +- ApiEvents/base_request_model.py | 71 +-- ApiLibrary/common/line_number.py | 15 +- ApiServices/Login/user_login_handler.py | 4 + ApiServices/Token/token_handler.py | 40 +- DockerApiServices/AllApiNeeds/app.py | 10 +- .../AllApiNeeds/middleware/auth_middleware.py | 5 +- .../AllApiNeeds/open_api_creator.py | 96 +++- ErrorHandlers/Exceptions/api_exc.py | 3 +- Services/MongoDb/Models/exception_handlers.py | 1 + Services/MongoDb/Models/exceptions.py | 5 +- Services/MongoDb/Models/mixins.py | 4 + .../PostgresDb/Models/alchemy_response.py | 3 + .../PostgresDb/Models/filter_functions.py | 4 + 23 files changed, 945 insertions(+), 754 deletions(-) create mode 100644 ApiEvents/AuthServiceApi/auth/__init__.py create mode 100644 ApiEvents/AuthServiceApi/auth/endpoints.py create mode 100644 ApiEvents/AuthServiceApi/auth/models.py create mode 100644 ApiEvents/EventServiceApi/account/__init__.py create mode 100644 ApiEvents/EventServiceApi/account/endpoints.py create mode 100644 ApiEvents/EventServiceApi/account/models.py diff --git a/ApiEvents/AuthServiceApi/auth/__init__.py b/ApiEvents/AuthServiceApi/auth/__init__.py new file mode 100644 index 0000000..318fff4 --- /dev/null +++ b/ApiEvents/AuthServiceApi/auth/__init__.py @@ -0,0 +1,9 @@ +""" +Authentication package initialization. +""" + +from .endpoints import AUTH_CONFIG + +__all__ = [ + "AUTH_CONFIG", +] diff --git a/ApiEvents/AuthServiceApi/auth/auth.py b/ApiEvents/AuthServiceApi/auth/auth.py index f0b3f20..c60a16f 100644 --- a/ApiEvents/AuthServiceApi/auth/auth.py +++ b/ApiEvents/AuthServiceApi/auth/auth.py @@ -2,103 +2,37 @@ Authentication related API endpoints. """ -from typing import TYPE_CHECKING, Union, Optional, Dict, Any +from typing import TYPE_CHECKING, Union, Dict, Any # Regular imports (non-TYPE_CHECKING) -from ApiEvents.abstract_class import ( - MethodToEvent, - RouteFactoryConfig, - EndpointFactoryConfig, -) -from ApiEvents.base_request_model import ( - BaseRequestModel, - DictRequestModel, - SuccessResponse, +from ApiEvents.abstract_class import MethodToEvent +from ApiEvents.base_request_model import DictRequestModel, SuccessResponse +from ApiLibrary.common.line_number import get_line_number_for_error +from ApiServices.Login.user_login_handler import UserLoginModule +from ApiServices.Token.token_handler import AccessToken, TokenService +from ApiValidations.Request.authentication import EmployeeSelectionValidation, Login, OccupantSelectionValidation +from ErrorHandlers import HTTPExceptionApi +from .models import ( + LoginData, + LoginRequestModel, + LogoutRequestModel, + RememberRequestModel, + ForgotRequestModel, + ChangePasswordRequestModel, + CreatePasswordRequestModel, + SelectionDataEmployee, + SelectionDataOccupant, ) + if TYPE_CHECKING: from fastapi import Request from ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject - from Schemas import ( - Login, - ChangePassword, - CreatePassword, - Forgot, - Logout, - Remember, - EmployeeSelection, - OccupantSelection, - ) - -from ApiLibrary.common.line_number import get_line_number_for_error -from ApiServices.Login.user_login_handler import UserLoginModule -from ApiValidations.Request import ( - Login, - Logout, - Remember, - Forgot, - ChangePassword, - CreatePassword, - EmployeeSelection, - OccupantSelection, -) -from Services.PostgresDb.Models.alchemy_response import AlchemyJsonResponse -from ApiValidations.Response import AccountRecordResponse -from ApiServices.Token.token_handler import AccessToken, TokenService -from ErrorHandlers import HTTPExceptionApi # Type aliases for common types TokenDictType = Union["EmployeeTokenObject", "OccupantTokenObject"] -class LoginRequestModel(BaseRequestModel[Login]): - """Request model for login endpoint.""" - - pass - - -class LogoutRequestModel(BaseRequestModel[Logout]): - """Request model for logout endpoint.""" - - pass - - -class RememberRequestModel(BaseRequestModel[Remember]): - """Request model for remember token endpoint.""" - - pass - - -class ForgotRequestModel(BaseRequestModel[Forgot]): - """Request model for forgot password endpoint.""" - - pass - - -class ChangePasswordRequestModel(BaseRequestModel[ChangePassword]): - """Request model for change password endpoint.""" - - pass - - -class CreatePasswordRequestModel(BaseRequestModel[CreatePassword]): - """Request model for create password endpoint.""" - - pass - - -class EmployeeSelectionRequestModel(BaseRequestModel[EmployeeSelection]): - """Request model for employee selection endpoint.""" - - pass - - -class OccupantSelectionRequestModel(BaseRequestModel[OccupantSelection]): - """Request model for occupant selection endpoint.""" - - pass - - class AuthenticationLoginEventMethods(MethodToEvent): event_type = "LOGIN" event_description = "Login via domain and access key : [email] | [phone]" @@ -112,7 +46,7 @@ class AuthenticationLoginEventMethods(MethodToEvent): @classmethod async def authentication_login_with_domain_and_creds( - cls, request: "Request", data: LoginRequestModel + cls, request: "Request", data: Login ): """ Authenticate user with domain and credentials. @@ -129,11 +63,9 @@ class AuthenticationLoginEventMethods(MethodToEvent): Returns: SuccessResponse containing authentication token and user info """ - # Create login module instance - login_module = UserLoginModule(request=request) - # Get token from login module - token = await login_module.login_user_via_credentials(access_data=data) + user_login_module = UserLoginModule(request=request) + token = await user_login_module.login_user_via_credentials(access_data=data) # Return response with token and headers return { @@ -157,135 +89,53 @@ class AuthenticationSelectEventMethods(MethodToEvent): @classmethod def _handle_employee_selection( cls, - data: EmployeeSelectionRequestModel, + data: SelectionDataEmployee, token_dict: TokenDictType, request: "Request", ): - return - # """Handle employee company selection""" - # Users.client_arrow = DateTimeLocal(is_client=True, timezone=token_dict.timezone) - # if data.company_uu_id not in token_dict.companies_uu_id_list: - # return ResponseHandler.unauthorized( - # "Company not found in user's company list" - # ) - - # selected_company = Companies.filter_one( - # Companies.uu_id == data.company_uu_id - # ).data - # if not selected_company: - # return ResponseHandler.not_found("Company not found") - - # # Get department IDs for the company - # department_ids = [ - # dept.id - # for dept in Departments.filter_all( - # Departments.company_id == selected_company.id - # ).data - # ] - - # # Get duties IDs for the company - # duties_ids = [ - # duty.id - # for duty in Duties.filter_all(Duties.company_id == selected_company.id).data - # ] - - # # Get staff IDs - # staff_ids = [ - # staff.id for staff in Staff.filter_all(Staff.duties_id.in_(duties_ids)).data - # ] - - # # Get employee - # employee = Employees.filter_one( - # Employees.people_id == token_dict.person_id, - # Employees.staff_id.in_(staff_ids), - # ).data - - # if not employee: - # return ResponseHandler.not_found("Employee not found") - - # # Get reachable events - # reachable_event_list_id = Event2Employee.get_event_id_by_employee_id( - # employee_id=employee.id - # ) - - # # Get staff and duties - # staff = Staff.filter_one(Staff.id == employee.staff_id).data - # duties = Duties.filter_one(Duties.id == staff.duties_id).data - # department = Departments.filter_one(Departments.id == duties.department_id).data - - # # Get bulk duty - # bulk_id = Duty.filter_by_one(system=True, duty_code="BULK").data - # bulk_duty_id = Duties.filter_by_one( - # company_id=selected_company.id, - # duties_id=bulk_id.id, - # **Duties.valid_record_dict, - # ).data - - # # Create company token - # company_token = CompanyToken( - # company_uu_id=selected_company.uu_id.__str__(), - # company_id=selected_company.id, - # department_id=department.id, - # department_uu_id=department.uu_id.__str__(), - # duty_id=duties.id, - # duty_uu_id=duties.uu_id.__str__(), - # bulk_duties_id=bulk_duty_id.id, - # staff_id=staff.id, - # staff_uu_id=staff.uu_id.__str__(), - # employee_id=employee.id, - # employee_uu_id=employee.uu_id.__str__(), - # reachable_event_list_id=reachable_event_list_id, - # ) - - # # Update Redis - # AuthActions.update_selected_to_redis(request=request, add_payload=company_token) - # return ResponseHandler.success("Company selected successfully") + raise HTTPExceptionApi( + error_code="", lang="en", loc=get_line_number_for_error() + ) @classmethod def _handle_occupant_selection( cls, - data: OccupantSelectionRequestModel, + data: SelectionDataOccupant, token_dict: TokenDictType, request: "Request", + ): + """Handle occupant type selection""" + raise HTTPExceptionApi( + error_code="HTTP_400_BAD_REQUEST", + lang=token_dict.lang, + loc=get_line_number_for_error(), + sys_msg="Occupant selection not implemented", + ) + + @classmethod + async def authentication_select_company_or_occupant_type( + cls, + request: "Request", + data: Union[EmployeeSelectionValidation, OccupantSelectionValidation], + token_dict: TokenDictType, ): """Handle selection of company or occupant type""" try: - if isinstance(token_dict, EmployeeTokenObject): - return cls._handle_employee_selection(data, token_dict, request) - elif isinstance(token_dict, OccupantTokenObject): - return cls._handle_occupant_selection(data, token_dict, request) - raise HTTPExceptionApi( - error_code="HTTP_400_BAD_REQUEST", - lang=token_dict.lang, - loc=get_line_number_for_error(), + print( + dict( + data=data, + token_dict=token_dict.model_dump(), + request=dict(request.headers) + ) ) except Exception as e: - return ResponseHandler.error( - str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + raise HTTPExceptionApi( + error_code="HTTP_500_INTERNAL_SERVER_ERROR", + lang="en", + loc=get_line_number_for_error(), + sys_msg=str(e), ) - @classmethod - def authentication_select_company_or_occupant_type( - cls, - request: "Request", - data: Union[EmployeeSelectionRequestModel, OccupantSelectionRequestModel], - token_dict: TokenDictType, - ): - """Handle selection of company or occupant type""" - # try: - # if isinstance(token_dict, EmployeeTokenObject): - # return cls._handle_employee_selection(data, token_dict, request) - # elif isinstance(token_dict, OccupantTokenObject): - # return cls._handle_occupant_selection(data, token_dict, request) - # return ResponseHandler.error( - # "Invalid token type", status_code=status.HTTP_400_BAD_REQUEST - # ) - # except Exception as e: - # return ResponseHandler.error( - # str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR - # ) - return - class AuthenticationCheckTokenEventMethods(MethodToEvent): event_type = "LOGIN" @@ -300,7 +150,7 @@ class AuthenticationCheckTokenEventMethods(MethodToEvent): # } @classmethod - def authentication_check_token_is_valid( + async def authentication_check_token_is_valid( cls, request: "Request", data: DictRequestModel ): # try: @@ -324,7 +174,7 @@ class AuthenticationRefreshEventMethods(MethodToEvent): # } @classmethod - def authentication_refresh_user_info( + async def authentication_refresh_user_info( cls, request: "Request", token_dict: TokenDictType, @@ -371,7 +221,7 @@ class AuthenticationChangePasswordEventMethods(MethodToEvent): # } @classmethod - def authentication_change_password( + async def authentication_change_password( cls, request: "Request", data: ChangePasswordRequestModel, @@ -429,7 +279,7 @@ class AuthenticationCreatePasswordEventMethods(MethodToEvent): # } @classmethod - def authentication_create_password( + async def authentication_create_password( cls, request: "Request", data: CreatePasswordRequestModel ): @@ -475,7 +325,7 @@ class AuthenticationDisconnectUserEventMethods(MethodToEvent): # } @classmethod - def authentication_disconnect_user( + async def authentication_disconnect_user( cls, request: "Request", data: LogoutRequestModel, token_dict: TokenDictType ): # found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data @@ -514,11 +364,11 @@ class AuthenticationLogoutEventMethods(MethodToEvent): # } @classmethod - def authentication_logout_user( + async def authentication_logout_user( cls, request: "Request", data: LogoutRequestModel, - token_dict: TokenDictType = None, + token_dict: TokenDictType, ): # token_user = None # if already_tokens := RedisActions.get_object_via_access_key(request=request): @@ -553,7 +403,7 @@ class AuthenticationRefreshTokenEventMethods(MethodToEvent): # } @classmethod - def authentication_refresher_token( + async def authentication_refresher_token( cls, request: "Request", data: RememberRequestModel, token_dict: TokenDictType ): # token_refresher = UsersTokens.filter_by_one( @@ -602,7 +452,7 @@ class AuthenticationForgotPasswordEventMethods(MethodToEvent): # } @classmethod - def authentication_forgot_password( + async def authentication_forgot_password( cls, request: "Request", data: ForgotRequestModel, @@ -644,7 +494,7 @@ class AuthenticationResetPasswordEventMethods(MethodToEvent): } @classmethod - def authentication_reset_password( + async def authentication_reset_password( cls, request: "Request", data: ForgotRequestModel ): # from sqlalchemy import or_ @@ -695,11 +545,11 @@ class AuthenticationDownloadAvatarEventMethods(MethodToEvent): # } @classmethod - def authentication_download_avatar( + async def authentication_download_avatar( cls, - token_dict: TokenDictType, request: "Request", data: DictRequestModel, + token_dict: TokenDictType, ): # if found_user := Users.filter_one(Users.id == token_dict.user_id).data: # expired_starts = str( @@ -725,253 +575,3 @@ class AuthenticationDownloadAvatarEventMethods(MethodToEvent): # return ResponseHandler.not_found("Invalid data") return - - -async def authentication_select_company_or_occupant_type( - request: "Request", - data: Union[EmployeeSelectionRequestModel, OccupantSelectionRequestModel], -) -> Dict[str, Any]: - return await AuthenticationSelectEventMethods.authentication_select_company_or_occupant_type( - request=request, data=data - ) - - -async def authentication_login_with_domain_and_creds( - request: "Request", data: LoginRequestModel -) -> SuccessResponse: - """ - Authenticate user with domain and credentials. - - Args: - request: FastAPI request object - data: Request body containing login credentials - { - "domain": str, - "username": str, - "password": str - } - - Returns: - SuccessResponse containing authentication token and user info - """ - return await AuthenticationLoginEventMethods.authentication_login_with_domain_and_creds( - request=request, data=data - ) - - -async def authentication_check_token_is_valid( - request: "Request", data: DictRequestModel -) -> Dict[str, Any]: - return ( - await AuthenticationCheckTokenEventMethods.authentication_check_token_is_valid( - request=request, data=data - ) - ) - - -def authentication_refresh_user_info( - request: "Request", data: DictRequestModel -) -> Dict[str, Any]: - return - - -def authentication_change_password( - request: "Request", data: ChangePasswordRequestModel -) -> Dict[str, Any]: - return - - -def authentication_create_password( - request: "Request", data: CreatePasswordRequestModel -) -> Dict[str, Any]: - return - - -def authentication_forgot_password( - request: "Request", data: ForgotRequestModel -) -> Dict[str, Any]: - return - - -def authentication_reset_password( - request: "Request", data: ForgotRequestModel -) -> Dict[str, Any]: - return - - -def authentication_disconnect_user( - request: "Request", data: LogoutRequestModel -) -> Dict[str, Any]: - return - - -def authentication_logout_user( - request: "Request", data: LogoutRequestModel -) -> Dict[str, Any]: - return - - -def authentication_refresher_token( - request: "Request", data: RememberRequestModel -) -> Dict[str, Any]: - return - - -def authentication_download_avatar( - request: "Request", data: DictRequestModel -) -> Dict[str, Any]: - return - - -AUTH_CONFIG = RouteFactoryConfig( - name="authentication", - prefix="/authentication", - tags=["Authentication"], - include_in_schema=True, - endpoints=[ - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/select", - url_of_endpoint="/authentication/select", - endpoint="/select", - method="POST", - summary="Select company or occupant type", - description="Select company or occupant type", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_select_company_or_occupant_type, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/login", - url_of_endpoint="/authentication/login", - endpoint="/login", - method="POST", - summary="Login user with domain and password", - description="Login user with domain and password", - is_auth_required=False, # Public endpoint - is_event_required=False, - endpoint_function=authentication_login_with_domain_and_creds, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/valid", - url_of_endpoint="/authentication/valid", - endpoint="/valid", - method="GET", - summary="Check access token is valid", - description="Check access token is valid", - is_auth_required=True, # Needs token validation - is_event_required=False, - endpoint_function=authentication_check_token_is_valid, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/refresh", - url_of_endpoint="/authentication/refresh", - endpoint="/refresh", - method="GET", - summary="Refresh credentials with access token", - description="Refresh credentials with access token", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_refresh_user_info, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/change_password", - url_of_endpoint="/authentication/change_password", - endpoint="/change_password", - method="POST", - summary="Change password with access token", - description="Change password with access token", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_change_password, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/create_password", - url_of_endpoint="/authentication/create_password", - endpoint="/create_password", - method="POST", - summary="Create password with password token", - description="Create password with password token", - is_auth_required=False, # Public endpoint - is_event_required=False, - endpoint_function=authentication_create_password, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/reset_password", - url_of_endpoint="/authentication/reset_password", - endpoint="/reset_password", - method="POST", - summary="Create password with password token", - description="Create password with password token", - is_auth_required=False, # Public endpoint - is_event_required=False, - endpoint_function=authentication_reset_password, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/disconnect", - url_of_endpoint="/authentication/disconnect", - endpoint="/disconnect", - method="POST", - summary="Disconnect user with access token", - description="Disconnect user with access token", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_disconnect_user, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/logout", - url_of_endpoint="/authentication/logout", - endpoint="/logout", - method="POST", - summary="Logout user with access token", - description="Logout user with access token", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_logout_user, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/refresher", - url_of_endpoint="/authentication/refresher", - endpoint="/refresher", - method="POST", - summary="Refresh token with refresh token", - description="Refresh token with refresh token", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_refresher_token, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/forgot", - url_of_endpoint="/authentication/forgot", - endpoint="/forgot", - method="POST", - summary="Forgot password with email or phone number", - description="Forgot password with email or phone number", - is_auth_required=False, # Public endpoint - is_event_required=False, - endpoint_function=authentication_forgot_password, - ), - EndpointFactoryConfig( - url_prefix="/authentication", - url_endpoint="/avatar", - url_of_endpoint="/authentication/avatar", - endpoint="/avatar", - method="POST", - summary="Get link of avatar with credentials", - description="Get link of avatar with credentials", - is_auth_required=True, # Needs token_dict - is_event_required=False, - endpoint_function=authentication_download_avatar, - ), - ], -).as_dict() diff --git a/ApiEvents/AuthServiceApi/auth/endpoints.py b/ApiEvents/AuthServiceApi/auth/endpoints.py new file mode 100644 index 0000000..ef89bcf --- /dev/null +++ b/ApiEvents/AuthServiceApi/auth/endpoints.py @@ -0,0 +1,371 @@ +""" +Authentication endpoint configurations. +""" + +from typing import TYPE_CHECKING, Dict, Any, Union, Annotated +from fastapi import HTTPException, status, Body + +from ApiValidations.Request.authentication import Login + +from .auth import ( + AuthenticationChangePasswordEventMethods, + AuthenticationCheckTokenEventMethods, + AuthenticationCreatePasswordEventMethods, + AuthenticationDisconnectUserEventMethods, + AuthenticationDownloadAvatarEventMethods, + AuthenticationForgotPasswordEventMethods, + AuthenticationLoginEventMethods, + AuthenticationLogoutEventMethods, + AuthenticationRefreshEventMethods, + AuthenticationRefreshTokenEventMethods, + AuthenticationResetPasswordEventMethods, + AuthenticationSelectEventMethods, +) +from .models import ( + ChangePasswordRequestModel, + CreatePasswordRequestModel, + ForgotRequestModel, + LoginData, + LoginRequestModel, + LogoutRequestModel, + SelectionDataEmployee, + SelectionDataOccupant, + RememberRequestModel, +) +from ApiEvents.base_request_model import DictRequestModel +from ApiEvents.abstract_class import RouteFactoryConfig, EndpointFactoryConfig, endpoint_wrapper + +if TYPE_CHECKING: + from fastapi import Request +from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject + + +# Type aliases for common types +TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject] + + +@endpoint_wrapper("/authentication/select") +async def authentication_select_company_or_occupant_type( + request: "Request", + data: Union[SelectionDataEmployee, SelectionDataOccupant], + token_dict: TokenDictType = None +) -> Dict[str, Any]: + """ + Handle selection of company or occupant type. + + Args: + request: The FastAPI request object + data: Selection request data + + Returns: + Dict containing the response data + """ + return { + "headers": dict(request.headers), + "data": data, + "token": token_dict + } + + +@endpoint_wrapper("/authentication/login") +async def authentication_login_with_domain_and_creds( + request: "Request", + data: Login, +) -> Dict[str, Any]: + """ + Authenticate user with domain and credentials. + """ + return AuthenticationLoginEventMethods.authentication_login_with_domain_and_creds( + request=request, data=data + ) + + +@endpoint_wrapper("/authentication/check") +async def authentication_check_token_is_valid( + request: "Request", + data: DictRequestModel, +) -> Dict[str, Any]: + """ + Check if a token is valid. + """ + return { + "status": "OK", + } + + + +@endpoint_wrapper("/authentication/refresh") +async def authentication_refresh_user_info( + request: "Request", + data: DictRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Refresh user information. + """ + return { + "status": "OK", + } + + +@endpoint_wrapper("/authentication/change-password") +async def authentication_change_password( + request: "Request", + data: ChangePasswordRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Change user password. + """ + return { + "status": "OK", + } + + +@endpoint_wrapper("/authentication/create-password") +async def authentication_create_password( + request: "Request", + data: CreatePasswordRequestModel, +) -> Dict[str, Any]: + """ + Create new password. + """ + return { + "status": "OK", + } + +@endpoint_wrapper("/authentication/forgot-password") +async def authentication_forgot_password( + request: "Request", + data: ForgotRequestModel, +) -> Dict[str, Any]: + """ + Handle forgot password request. + """ + return { + "status": "OK", + } + +@endpoint_wrapper("/authentication/reset-password") +async def authentication_reset_password( + request: "Request", + data: ForgotRequestModel, +) -> Dict[str, Any]: + """ + Reset password. + """ + return { + "status": "OK", + } + +@endpoint_wrapper("/authentication/disconnect") +async def authentication_disconnect_user( + request: "Request", + data: LogoutRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Disconnect user. + """ + return { + "status": "OK", + } + + +@endpoint_wrapper("/authentication/logout") +async def authentication_logout_user( + request: "Request", + data: LogoutRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Logout user. + """ + return { + "status": "OK", + } + + +@endpoint_wrapper("/authentication/remember") +async def authentication_refresher_token( + request: "Request", + data: RememberRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Refresh remember token. + """ + return { + "status": "OK", + } + + +@endpoint_wrapper("/authentication/avatar") +async def authentication_download_avatar( + request: "Request", + data: DictRequestModel, + token_dict: TokenDictType = None, +) -> Dict[str, Any]: + """ + Download user avatar. + """ + return { + "status": "OK", + } + + +prefix = "/authentication" + + +AUTH_CONFIG = RouteFactoryConfig( + name="authentication", + prefix=prefix, + tags=["Authentication"], + include_in_schema=True, + endpoints=[ + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/select", + url_of_endpoint="/authentication/select", + endpoint="/select", + method="POST", + summary="Select company or occupant type", + description="Select company or occupant type", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_select_company_or_occupant_type, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/login", + url_of_endpoint="/authentication/login", + endpoint="/login", + method="POST", + summary="Login user with domain and password", + description="Login user with domain and password", + is_auth_required=False, # Public endpoint + is_event_required=False, + endpoint_function=authentication_login_with_domain_and_creds, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/check", + url_of_endpoint="/authentication/check", + endpoint="/check", + method="GET", + summary="Check access token is valid", + description="Check access token is valid", + is_auth_required=True, # Needs token validation + is_event_required=False, + endpoint_function=authentication_check_token_is_valid, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/refresh", + url_of_endpoint="/authentication/refresh", + endpoint="/refresh", + method="GET", + summary="Refresh credentials with access token", + description="Refresh credentials with access token", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_refresh_user_info, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/change-password", + url_of_endpoint="/authentication/change-password", + endpoint="/change-password", + method="POST", + summary="Change password with access token", + description="Change password with access token", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_change_password, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/create-password", + url_of_endpoint="/authentication/create-password", + endpoint="/create-password", + method="POST", + summary="Create password with password token", + description="Create password with password token", + is_auth_required=False, # Public endpoint + is_event_required=False, + endpoint_function=authentication_create_password, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/reset-password", + url_of_endpoint="/authentication/reset-password", + endpoint="/reset-password", + method="POST", + summary="Reset password with token", + description="Reset password with token", + is_auth_required=False, # Public endpoint + is_event_required=False, + endpoint_function=authentication_reset_password, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/disconnect", + url_of_endpoint="/authentication/disconnect", + endpoint="/disconnect", + method="POST", + summary="Disconnect user with access token", + description="Disconnect user with access token", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_disconnect_user, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/logout", + url_of_endpoint="/authentication/logout", + endpoint="/logout", + method="POST", + summary="Logout user with access token", + description="Logout user with access token", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_logout_user, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/remember", + url_of_endpoint="/authentication/remember", + endpoint="/remember", + method="POST", + summary="Refresh token with refresh token", + description="Refresh token with refresh token", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_refresher_token, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/forgot-password", + url_of_endpoint="/authentication/forgot-password", + endpoint="/forgot-password", + method="POST", + summary="Request password reset via email", + description="Request password reset via email", + is_auth_required=False, # Public endpoint + is_event_required=False, + endpoint_function=authentication_forgot_password, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/avatar", + url_of_endpoint="/authentication/avatar", + endpoint="/avatar", + method="POST", + summary="Get user avatar with credentials", + description="Get user avatar with credentials", + is_auth_required=True, # Needs token_dict + is_event_required=False, + endpoint_function=authentication_download_avatar, + ), + ], +).as_dict() diff --git a/ApiEvents/AuthServiceApi/auth/models.py b/ApiEvents/AuthServiceApi/auth/models.py new file mode 100644 index 0000000..c130d1a --- /dev/null +++ b/ApiEvents/AuthServiceApi/auth/models.py @@ -0,0 +1,138 @@ +""" +Authentication request models. +""" + +from typing import TYPE_CHECKING, Dict, Any, Literal, Optional, TypedDict +from pydantic import BaseModel, Field, model_validator, RootModel, ConfigDict +from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel + +if TYPE_CHECKING: + from fastapi import Request + + +class TokenObjectBase(BaseModel): + """Base model for token objects.""" + user_type: str = Field(..., description="Type of user") + user_id: str = Field(..., description="User ID") + token: str = Field(..., description="Authentication token") + permissions: Dict[str, Any] = Field(default_factory=dict, description="User permissions") + + + +class LoginData(TypedDict): + """Type for login data.""" + domain: str + access_key: str + password: str + remember_me: bool + + +class LoginRequestModel(BaseRequestModel[LoginData]): + """Request model for login endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "domain": "example.com", + "access_key": "user@example", + "password": "password", + "remember_me": False + } + } + ) + + +class LogoutData(TypedDict): + """Type for logout data.""" + token: str + + +class LogoutRequestModel(BaseRequestModel[LogoutData]): + """Request model for logout endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "token": "your-token-here" + } + } + ) + + +class RememberData(TypedDict): + """Type for remember token data.""" + remember_token: str + + +class RememberRequestModel(BaseRequestModel[RememberData]): + """Request model for remember token endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "remember_token": "your-remember-token-here" + } + } + ) + + +class ForgotData(TypedDict): + """Type for forgot password data.""" + email: str + domain: str + + +class ForgotRequestModel(BaseRequestModel[ForgotData]): + """Request model for forgot password endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "email": "user@example.com", + "domain": "example.com" + } + } + ) + + +class ChangePasswordData(TypedDict): + """Type for change password data.""" + old_password: str + new_password: str + + +class ChangePasswordRequestModel(BaseRequestModel[ChangePasswordData]): + """Request model for change password endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "old_password": "old-pass", + "new_password": "new-pass" + } + } + ) + + +class CreatePasswordData(TypedDict): + """Type for create password data.""" + token: str + password: str + + +class CreatePasswordRequestModel(BaseRequestModel[CreatePasswordData]): + """Request model for create password endpoint.""" + model_config = ConfigDict( + json_schema_extra={ + "example": { + "token": "password-creation-token", + "password": "new-password" + } + } + ) + + +class SelectionDataOccupant(TypedDict): + """Type for selection data.""" + build_living_space_uu_id: Optional[str] + + +class SelectionDataEmployee(TypedDict): + """Type for selection data.""" + company_uu_id: Optional[str] + diff --git a/ApiEvents/AuthServiceApi/route_configs.py b/ApiEvents/AuthServiceApi/route_configs.py index fb603f0..1b096bb 100644 --- a/ApiEvents/AuthServiceApi/route_configs.py +++ b/ApiEvents/AuthServiceApi/route_configs.py @@ -6,7 +6,7 @@ to be used by the dynamic route creation system. """ from typing import Dict, List, Any -from .auth.auth import AUTH_CONFIG +from .auth.endpoints import AUTH_CONFIG # Registry of all route configurations diff --git a/ApiEvents/EventServiceApi/account/__init__.py b/ApiEvents/EventServiceApi/account/__init__.py new file mode 100644 index 0000000..3c5f278 --- /dev/null +++ b/ApiEvents/EventServiceApi/account/__init__.py @@ -0,0 +1,9 @@ +""" +Account records package initialization. +""" + +from .endpoints import ACCOUNT_RECORDS_CONFIG + +__all__ = [ + "ACCOUNT_RECORDS_CONFIG", +] diff --git a/ApiEvents/EventServiceApi/account/account_records.py b/ApiEvents/EventServiceApi/account/account_records.py index 3e3413f..cf9e097 100644 --- a/ApiEvents/EventServiceApi/account/account_records.py +++ b/ApiEvents/EventServiceApi/account/account_records.py @@ -1,24 +1,22 @@ -import typing +""" +Account records service implementation. +""" -from ApiEvents.abstract_class import ( - MethodToEvent, - RouteFactoryConfig, - EndpointFactoryConfig, - endpoint_wrapper, -) -from ApiEvents.base_request_model import BaseRequestModel, DictRequestModel - -from typing import TYPE_CHECKING, Dict, Any -from fastapi import Request, Path, Body, Depends, APIRouter -from pydantic import BaseModel, Field - -if TYPE_CHECKING: - from fastapi import Request +from typing import Dict, Any, Union +from pydantic import Field +from ApiEvents.abstract_class import MethodToEvent, endpoint_wrapper +from ApiEvents.base_request_model import DictRequestModel from ApiValidations.Custom.token_objects import ( OccupantTokenObject, EmployeeTokenObject, ) +from ApiLibrary import system_arrow +from ApiValidations.Request.account_records import ( + InsertAccountRecord, + UpdateAccountRecord, +) +from ApiValidations.Request.base_validations import ListOptions from Schemas import ( BuildLivingSpace, AccountRecords, @@ -26,49 +24,16 @@ from Schemas import ( BuildDecisionBookPayments, ApiEnumDropdown, ) -from ApiLibrary import system_arrow -from ApiValidations.Request import ( - InsertAccountRecord, - UpdateAccountRecord, - ListOptions, -) from Services.PostgresDb.Models.alchemy_response import ( AlchemyJsonResponse, DictJsonResponse, ) from ApiValidations.Response import AccountRecordResponse - - -class AddressUpdateRequest(BaseModel): - """Request model for address update.""" - - data: Dict[str, Any] = Field(..., description="Updated address data") - - -class AddressUpdateResponse(BaseModel): - """Response model for address update.""" - - address_uu_id: str = Field(..., description="UUID of the updated address") - data: Dict[str, Any] = Field(..., description="Updated address data") - function_code: str = Field(..., description="Function code for the endpoint") - - -class InsertAccountRecordRequestModel(BaseRequestModel[InsertAccountRecord]): - """Request model for inserting account records.""" - - pass - - -class UpdateAccountRecordRequestModel(BaseRequestModel[UpdateAccountRecord]): - """Request model for updating account records.""" - - pass - - -class ListOptionsRequestModel(BaseRequestModel[ListOptions]): - """Request model for list options.""" - - pass +from .models import ( + InsertAccountRecordRequestModel, + UpdateAccountRecordRequestModel, + ListOptionsRequestModel, +) class AccountRecordsListEventMethods(MethodToEvent): @@ -90,7 +55,7 @@ class AccountRecordsListEventMethods(MethodToEvent): def account_records_list( cls, list_options: ListOptionsRequestModel, - token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], ): db_session = AccountRecords.new_session() if isinstance(token_dict, OccupantTokenObject): @@ -119,7 +84,7 @@ class AccountRecordsListEventMethods(MethodToEvent): def account_records_list_flt_res( cls, list_options: ListOptionsRequestModel, - token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], ): db_session = AccountRecords.new_session() if not isinstance(token_dict, OccupantTokenObject): @@ -269,7 +234,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent): def account_records_create( cls, data: InsertAccountRecordRequestModel, - token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], ): data_dict = data.excluded_dump() if isinstance(token_dict, OccupantTokenObject): @@ -358,7 +323,7 @@ class AccountRecordsUpdateEventMethods(MethodToEvent): cls, build_uu_id: str, data: UpdateAccountRecordRequestModel, - token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], + token_dict: Union[EmployeeTokenObject, OccupantTokenObject], ): if isinstance(token_dict, OccupantTokenObject): pass @@ -373,123 +338,3 @@ class AccountRecordsUpdateEventMethods(MethodToEvent): cls_object=AccountRecords, response_model=UpdateAccountRecord, ) - - -@endpoint_wrapper("/account/records/address/list") -async def address_list(request: "Request", data: ListOptionsRequestModel): - """Handle address list endpoint.""" - return { - "data": data, - "request": str(request.headers), - "request_url": str(request.url), - "request_base_url": str(request.base_url), - } - - -@endpoint_wrapper("/account/records/address/create") -async def address_create(request: "Request", data: DictRequestModel): - """Handle address creation endpoint.""" - return { - "data": data, - "request": str(request.headers), - "request_url": str(request.url), - "request_base_url": str(request.base_url), - } - - -@endpoint_wrapper("/account/records/address/search") -async def address_search(request: "Request", data: DictRequestModel): - """Handle address search endpoint.""" - return {"data": data} - - -router = APIRouter() - - -@endpoint_wrapper("/account/records/address/{address_uu_id}") -async def address_update( - request: Request, - address_uu_id: str = Path(..., description="UUID of the address to update"), - request_data: DictRequestModel = Body(..., description="Request body"), -): - """ - Handle address update endpoint. - - Args: - request: FastAPI request object - address_uu_id: UUID of the address to update - request_data: Request body containing updated address data - - Returns: - DictJsonResponse: Response containing updated address info - """ - return DictJsonResponse( - data={ - "address_uu_id": address_uu_id, - "data": request_data.root, - "request": str(request.headers), - "request_url": str(request.url), - "request_base_url": str(request.base_url), - } - ) - - -prefix = "/account/records" - -# Account Records Router Configuration -ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig( - name="account_records", - prefix=prefix, - tags=["Account Records"], - include_in_schema=True, - endpoints=[ - EndpointFactoryConfig( - url_prefix=prefix, - url_endpoint="/address/list", - url_of_endpoint="/account/records/address/list", - endpoint="/address/list", - method="POST", - summary="List Active/Delete/Confirm Address", - description="List Active/Delete/Confirm Address", - is_auth_required=True, - is_event_required=True, - endpoint_function=address_list, - ), - EndpointFactoryConfig( - url_prefix=prefix, - url_endpoint="/address/create", - url_of_endpoint="/account/records/address/create", - endpoint="/address/create", - method="POST", - summary="Create Address with given auth levels", - description="Create Address with given auth levels", - is_auth_required=False, - is_event_required=False, - endpoint_function=address_create, - ), - EndpointFactoryConfig( - url_prefix=prefix, - url_endpoint="/address/search", - url_of_endpoint="/account/records/address/search", - endpoint="/address/search", - method="POST", - summary="Search Address with given auth levels", - description="Search Address with given auth levels", - is_auth_required=True, - is_event_required=True, - endpoint_function=address_search, - ), - EndpointFactoryConfig( - url_prefix=prefix, - url_endpoint="/address/{address_uu_id}", - url_of_endpoint="/account/records/address/{address_uu_id}", - endpoint="/address/{address_uu_id}", - method="PUT", - summary="Update Address with given auth levels", - description="Update Address with given auth levels", - is_auth_required=True, - is_event_required=True, - endpoint_function=address_update, - ), - ], -).as_dict() diff --git a/ApiEvents/EventServiceApi/account/endpoints.py b/ApiEvents/EventServiceApi/account/endpoints.py new file mode 100644 index 0000000..8df4a67 --- /dev/null +++ b/ApiEvents/EventServiceApi/account/endpoints.py @@ -0,0 +1,129 @@ +""" +Account records endpoint configurations. + +""" + +from ApiEvents.abstract_class import RouteFactoryConfig, EndpointFactoryConfig, endpoint_wrapper +from ApiEvents.base_request_model import DictRequestModel + + +from Services.PostgresDb.Models.alchemy_response import DictJsonResponse +from fastapi import Request, Path, Body + +from .models import ListOptionsRequestModel + + +@endpoint_wrapper("/account/records/address/list") +async def address_list(request: "Request", data: ListOptionsRequestModel): + """Handle address list endpoint.""" + return { + "data": data, + "request": str(request.headers), + "request_url": str(request.url), + "request_base_url": str(request.base_url), + } + + +@endpoint_wrapper("/account/records/address/create") +async def address_create(request: "Request", data: DictRequestModel): + """Handle address creation endpoint.""" + return { + "data": data, + "request": str(request.headers), + "request_url": str(request.url), + "request_base_url": str(request.base_url), + } + + +@endpoint_wrapper("/account/records/address/search") +async def address_search(request: "Request", data: DictRequestModel): + """Handle address search endpoint.""" + return {"data": data} + + +@endpoint_wrapper("/account/records/address/{address_uu_id}") +async def address_update( + request: Request, + address_uu_id: str = Path(..., description="UUID of the address to update"), + request_data: DictRequestModel = Body(..., description="Request body"), +): + """ + Handle address update endpoint. + + Args: + request: FastAPI request object + address_uu_id: UUID of the address to update + request_data: Request body containing updated address data + + Returns: + DictJsonResponse: Response containing updated address info + """ + return DictJsonResponse( + data={ + "address_uu_id": address_uu_id, + "data": request_data.root, + "request": str(request.headers), + "request_url": str(request.url), + "request_base_url": str(request.base_url), + } + ) + +prefix = "/account/records" + +# Account Records Router Configuration +ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig( + name="account_records", + prefix=prefix, + tags=["Account Records"], + include_in_schema=True, + endpoints=[ + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/address/list", + url_of_endpoint="/account/records/address/list", + endpoint="/address/list", + method="POST", + summary="List Active/Delete/Confirm Address", + description="List Active/Delete/Confirm Address", + is_auth_required=True, + is_event_required=True, + endpoint_function=address_list, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/address/create", + url_of_endpoint="/account/records/address/create", + endpoint="/address/create", + method="POST", + summary="Create Address with given auth levels", + description="Create Address with given auth levels", + is_auth_required=False, + is_event_required=False, + endpoint_function=address_create, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/address/search", + url_of_endpoint="/account/records/address/search", + endpoint="/address/search", + method="POST", + summary="Search Address with given auth levels", + description="Search Address with given auth levels", + is_auth_required=True, + is_event_required=True, + endpoint_function=address_search, + ), + EndpointFactoryConfig( + url_prefix=prefix, + url_endpoint="/address/{address_uu_id}", + url_of_endpoint="/account/records/address/{address_uu_id}", + endpoint="/address/{address_uu_id}", + method="PUT", + summary="Update Address with given auth levels", + description="Update Address with given auth levels", + is_auth_required=True, + is_event_required=True, + endpoint_function=address_update, + ), + ], +).as_dict() diff --git a/ApiEvents/EventServiceApi/account/models.py b/ApiEvents/EventServiceApi/account/models.py new file mode 100644 index 0000000..efa4138 --- /dev/null +++ b/ApiEvents/EventServiceApi/account/models.py @@ -0,0 +1,49 @@ +""" +Account records request and response models. +""" + +from typing import TYPE_CHECKING, Dict, Any +from pydantic import BaseModel, Field, RootModel +from ApiEvents.base_request_model import BaseRequestModel + +if TYPE_CHECKING: + from ApiValidations.Request import ( + InsertAccountRecord, + UpdateAccountRecord, + ListOptions, + ) + + +class AddressUpdateRequest(RootModel[Dict[str, Any]]): + """Request model for address update.""" + model_config = { + "json_schema_extra": { + "example": { + "street": "123 Main St", + "city": "Example City", + "country": "Example Country" + } + } + } + + +class AddressUpdateResponse(BaseModel): + """Response model for address update.""" + address_uu_id: str = Field(..., description="UUID of the updated address") + data: Dict[str, Any] = Field(..., description="Updated address data") + function_code: str = Field(..., description="Function code for the endpoint") + + +class InsertAccountRecordRequestModel(BaseRequestModel["InsertAccountRecord"]): + """Request model for inserting account records.""" + pass + + +class UpdateAccountRecordRequestModel(BaseRequestModel["UpdateAccountRecord"]): + """Request model for updating account records.""" + pass + + +class ListOptionsRequestModel(BaseRequestModel["ListOptions"]): + """Request model for list options.""" + pass diff --git a/ApiEvents/EventServiceApi/route_configs.py b/ApiEvents/EventServiceApi/route_configs.py index 0206e22..721b8f7 100644 --- a/ApiEvents/EventServiceApi/route_configs.py +++ b/ApiEvents/EventServiceApi/route_configs.py @@ -6,7 +6,7 @@ to be used by the dynamic route creation system. """ from typing import Dict, List, Any -from .account.account_records import ACCOUNT_RECORDS_CONFIG +from .account.endpoints import ACCOUNT_RECORDS_CONFIG # Registry of all route configurations diff --git a/ApiEvents/base_request_model.py b/ApiEvents/base_request_model.py index ab57c4a..dbd6817 100644 --- a/ApiEvents/base_request_model.py +++ b/ApiEvents/base_request_model.py @@ -5,54 +5,22 @@ This module provides base request models that can be used across different endpo to ensure consistent request handling and validation. """ -from typing import Dict, Any, Generic, TypeVar, Optional, get_args, get_origin -from pydantic import RootModel, BaseModel, Field, ConfigDict +from typing import Dict, Any, Generic, TypeVar, Optional, Union, get_args +from pydantic import BaseModel, Field, ConfigDict, RootModel T = TypeVar("T") -class BaseRequestModel(RootModel[T]): - """Base model for all API requests. - - This model can be extended to create specific request models for different endpoints. - """ - +class BaseRequestModel(RootModel[T], Generic[T]): + """Base model for all API requests.""" model_config = ConfigDict( json_schema_extra={"example": {}} # Will be populated by subclasses ) - @classmethod - def model_json_schema(cls, *args, **kwargs): - schema = super().model_json_schema(*args, **kwargs) - if hasattr(cls, "__orig_bases__"): - generic_type = get_args(cls.__orig_bases__[0])[0] - if generic_type and hasattr(generic_type, "model_json_schema"): - type_schema = generic_type.model_json_schema() - if "properties" in type_schema: - schema["properties"] = type_schema["properties"] - if "required" in type_schema: - schema["required"] = type_schema["required"] - if "title" in type_schema: - schema["title"] = type_schema["title"] - if "example" in type_schema: - schema["example"] = type_schema["example"] - elif "properties" in type_schema: - schema["example"] = { - key: prop.get("example", "string") - for key, prop in type_schema["properties"].items() - } - schema["type"] = "object" - return schema - - @property - def data(self) -> T: - return self.root - class DictRequestModel(RootModel[Dict[str, Any]]): """Request model for endpoints that accept dictionary data.""" - model_config = ConfigDict( json_schema_extra={ "example": { @@ -63,40 +31,9 @@ class DictRequestModel(RootModel[Dict[str, Any]]): } ) - @classmethod - def model_json_schema(cls, *args, **kwargs): - schema = super().model_json_schema(*args, **kwargs) - schema.update( - { - "title": "Dictionary Request Model", - "type": "object", - "properties": { - "key1": {"type": "string", "example": "value1"}, - "key2": {"type": "string", "example": "value2"}, - "nested": { - "type": "object", - "properties": { - "inner_key": {"type": "string", "example": "inner_value"} - }, - }, - }, - "example": { - "key1": "value1", - "key2": "value2", - "nested": {"inner_key": "inner_value"}, - }, - } - ) - return schema - - @property - def data(self) -> Dict[str, Any]: - return self.root - class SuccessResponse(BaseModel): """Standard success response model.""" - token: str = Field(..., example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") user_info: Dict[str, Any] = Field( ..., diff --git a/ApiLibrary/common/line_number.py b/ApiLibrary/common/line_number.py index 3638ae6..383b830 100644 --- a/ApiLibrary/common/line_number.py +++ b/ApiLibrary/common/line_number.py @@ -1,5 +1,14 @@ -def get_line_number_for_error(): - from inspect import currentframe, getframeinfo +"""Utility functions for getting line numbers and file locations.""" - frameinfo = getframeinfo(currentframe()) +from inspect import currentframe, getframeinfo, stack + + +def get_line_number_for_error() -> str: + """Get the file name and line number of where an error occurred. + + Returns: + str: A string in the format 'filename | line_number' showing where the error occurred + """ + caller = stack()[1] # Get the caller's frame + frameinfo = getframeinfo(caller[0]) return f"{frameinfo.filename} | {frameinfo.lineno}" diff --git a/ApiServices/Login/user_login_handler.py b/ApiServices/Login/user_login_handler.py index a97ca48..52c3539 100644 --- a/ApiServices/Login/user_login_handler.py +++ b/ApiServices/Login/user_login_handler.py @@ -30,12 +30,14 @@ class UserLoginModule: error_code="HTTP_400_BAD_REQUEST", lang="en", loc=get_line_number_for_error(), + sys_msg="User not found", ) return found_user async def login_user_via_credentials(self, access_data: "Login") -> Dict[str, Any]: """Login user via credentials.""" # Get the actual data from the BaseRequestModel if needed + print("access_data", access_data) if hasattr(access_data, "data"): access_data = access_data.data @@ -47,6 +49,7 @@ class UserLoginModule: error_code="HTTP_400_BAD_REQUEST", lang=found_user.lang, loc=get_line_number_for_error(), + sys_msg="Invalid password create a password to user first", ) if PasswordModule.check_password( @@ -65,4 +68,5 @@ class UserLoginModule: error_code="HTTP_400_BAD_REQUEST", lang=found_user.lang, loc=get_line_number_for_error(), + sys_msg="login_user_via_credentials raised error", ) diff --git a/ApiServices/Token/token_handler.py b/ApiServices/Token/token_handler.py index e7bf97e..2e8ec75 100644 --- a/ApiServices/Token/token_handler.py +++ b/ApiServices/Token/token_handler.py @@ -67,7 +67,10 @@ class TokenService: ).data if not living_spaces: raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="User does not have any living space", ) occupants_selection_dict: Dict[str, Any] = {} @@ -79,7 +82,10 @@ class TokenService: ).data if not build_parts_selection: raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="User does not have any living space", ) build_part = build_parts_selection.get(1) @@ -211,7 +217,10 @@ class TokenService: "companies_list": companies_list, } raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Creating Token failed...", ) @classmethod @@ -287,11 +296,17 @@ class TokenService: """Validate request has required token headers.""" if not hasattr(request, "headers"): raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Request has no headers", ) if not request.headers.get(Auth.ACCESS_TOKEN_TAG): raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Request has no access token presented", ) @classmethod @@ -322,7 +337,10 @@ class TokenService: return OccupantTokenObject(**redis_object) raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Unknown user type", ) @classmethod @@ -336,7 +354,10 @@ class TokenService: return cls._process_redis_object(redis_object) raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Invalid access token", ) @classmethod @@ -350,5 +371,8 @@ class TokenService: return cls._process_redis_object(redis_object) raise HTTPExceptionApi( - error_code="", lang="en", loc=get_line_number_for_error() + error_code="", + lang="en", + loc=get_line_number_for_error(), + sys_msg="Invalid access token", ) diff --git a/DockerApiServices/AllApiNeeds/app.py b/DockerApiServices/AllApiNeeds/app.py index aff539d..cc7033b 100644 --- a/DockerApiServices/AllApiNeeds/app.py +++ b/DockerApiServices/AllApiNeeds/app.py @@ -15,7 +15,7 @@ from create_routes import get_all_routers from prometheus_fastapi_instrumentator import Instrumentator from app_handler import setup_middleware, get_uvicorn_config from create_file import setup_security_schema, configure_route_security -from fastapi.openapi.utils import get_openapi +from open_api_creator import OpenAPISchemaCreator, create_openapi_schema def create_app() -> FastAPI: @@ -63,12 +63,8 @@ def create_app() -> FastAPI: if app.openapi_schema: return app.openapi_schema - openapi_schema = get_openapi( - title="WAG Management API", - version="4.0.0", - description="WAG Management API Service", - routes=app.routes, - ) + # Create OpenAPI schema using our custom creator + openapi_schema = create_openapi_schema(app) # Add security scheme openapi_schema.update(setup_security_schema()) diff --git a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py index 6e7850a..45c3ba3 100644 --- a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py +++ b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py @@ -79,7 +79,10 @@ class MiddlewareModule: token_context = TokenService.get_object_via_access_key(access_token=redis_token) if not token_context: raise HTTPExceptionApi( - error_code="USER_NOT_FOUND", lang="tr", loc=get_line_number_for_error() + error_code="USER_NOT_FOUND", + lang="tr", + loc=get_line_number_for_error(), + sys_msg="TokenService: Token Context couldnt retrieved from redis", ) return AuthContext(token_context=token_context) diff --git a/DockerApiServices/AllApiNeeds/open_api_creator.py b/DockerApiServices/AllApiNeeds/open_api_creator.py index cec5eca..afb1fbb 100644 --- a/DockerApiServices/AllApiNeeds/open_api_creator.py +++ b/DockerApiServices/AllApiNeeds/open_api_creator.py @@ -11,8 +11,10 @@ This module provides functionality to create and customize OpenAPI documentation from typing import Any, Dict, List, Optional, Set from fastapi import FastAPI, APIRouter +from fastapi.routing import APIRoute from fastapi.openapi.utils import get_openapi from AllConfigs.main import MainConfig as Config +from create_routes import get_all_routers class OpenAPISchemaCreator: @@ -28,7 +30,7 @@ class OpenAPISchemaCreator: app: FastAPI application instance """ self.app = app - self.protected_paths: Set[str] = set() + _, self.protected_routes = get_all_routers() self.tags_metadata = self._create_tags_metadata() @staticmethod @@ -60,16 +62,16 @@ class OpenAPISchemaCreator: """ return { "Bearer Auth": { - "type": "apiKey", - "in": "header", - "name": "evyos-session-key", - "description": "Enter: **'Bearer '**, where JWT is the access token", + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Enter the token with the `Bearer: ` prefix", }, "API Key": { "type": "apiKey", "in": "header", "name": "X-API-Key", - "description": "API key for service authentication", + "description": "Optional API key for service authentication", }, } @@ -82,20 +84,10 @@ class OpenAPISchemaCreator: """ return { "401": { - "description": "Unauthorized - Authentication failed or not provided", + "description": "Unauthorized - Invalid or missing credentials", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": {"type": "string"}, - "error_code": {"type": "string"}, - }, - }, - "example": { - "detail": "Invalid authentication credentials", - "error_code": "INVALID_CREDENTIALS", - }, + "schema": {"$ref": "#/components/schemas/HTTPValidationError"} } }, }, @@ -135,6 +127,62 @@ class OpenAPISchemaCreator: }, } + def _process_request_body(self, path: str, method: str, schema: Dict[str, Any]) -> None: + """ + Process request body to include examples from model config. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + """ + try: + route_schema = schema["paths"][path][method] + if "requestBody" in route_schema: + request_body = route_schema["requestBody"] + if "content" in request_body: + content = request_body["content"] + if "application/json" in content: + json_content = content["application/json"] + if "schema" in json_content and "$ref" in json_content["schema"]: + ref = json_content["schema"]["$ref"] + model_name = ref.split("/")[-1] + if model_name in schema["components"]["schemas"]: + model_schema = schema["components"]["schemas"][model_name] + if "example" in model_schema: + json_content["example"] = model_schema["example"] + except KeyError: + pass + + def _process_response_examples(self, path: str, method: str, schema: Dict[str, Any]) -> None: + """ + Process response body to include examples from model config. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + """ + try: + route_schema = schema["paths"][path][method] + if "responses" in route_schema: + responses = route_schema["responses"] + if "200" in responses: + response = responses["200"] + if "content" in response: + content = response["content"] + if "application/json" in content: + json_content = content["application/json"] + if "schema" in json_content and "$ref" in json_content["schema"]: + ref = json_content["schema"]["$ref"] + model_name = ref.split("/")[-1] + if model_name in schema["components"]["schemas"]: + model_schema = schema["components"]["schemas"][model_name] + if "example" in model_schema: + json_content["example"] = model_schema["example"] + except KeyError: + pass + def configure_route_security( self, path: str, method: str, schema: Dict[str, Any] ) -> None: @@ -146,7 +194,8 @@ class OpenAPISchemaCreator: method: HTTP method schema: OpenAPI schema to modify """ - if path not in Config.INSECURE_PATHS: + # Check if route is protected based on dynamic routing info + if path in self.protected_routes and method in self.protected_routes[path]: schema["paths"][path][method]["security"] = [ {"Bearer Auth": []}, {"API Key": []}, @@ -154,6 +203,11 @@ class OpenAPISchemaCreator: schema["paths"][path][method]["responses"].update( self._create_common_responses() ) + + # Process request body examples + self._process_request_body(path, method, schema) + # Process response examples + self._process_response_examples(path, method, schema) def create_schema(self) -> Dict[str, Any]: """ @@ -174,9 +228,7 @@ class OpenAPISchemaCreator: if "components" not in openapi_schema: openapi_schema["components"] = {} - openapi_schema["components"][ - "securitySchemes" - ] = self._create_security_schemes() + openapi_schema["components"]["securitySchemes"] = self._create_security_schemes() # Configure route security and responses for route in self.app.routes: diff --git a/ErrorHandlers/Exceptions/api_exc.py b/ErrorHandlers/Exceptions/api_exc.py index 2fcf713..c9503ff 100644 --- a/ErrorHandlers/Exceptions/api_exc.py +++ b/ErrorHandlers/Exceptions/api_exc.py @@ -1,6 +1,7 @@ class HTTPExceptionApi(Exception): - def __init__(self, error_code: str, lang: str, loc: str = ""): + def __init__(self, error_code: str, lang: str, loc: str = "", sys_msg: str = ""): self.error_code = error_code self.lang = lang self.loc = loc + self.sys_msg = sys_msg diff --git a/Services/MongoDb/Models/exception_handlers.py b/Services/MongoDb/Models/exception_handlers.py index bc7d80a..d3754ed 100644 --- a/Services/MongoDb/Models/exception_handlers.py +++ b/Services/MongoDb/Models/exception_handlers.py @@ -56,6 +56,7 @@ def handle_mongo_errors(func: Callable) -> Callable: lang="en", error_code=status.HTTP_500_INTERNAL_SERVER_ERROR, loc=get_line_number_for_error(), + sys_msg=str(e), ) return wrapper diff --git a/Services/MongoDb/Models/exceptions.py b/Services/MongoDb/Models/exceptions.py index 94e3720..8197d4a 100644 --- a/Services/MongoDb/Models/exceptions.py +++ b/Services/MongoDb/Models/exceptions.py @@ -7,6 +7,7 @@ operations and password-related functionality. from typing import Any, Dict, Optional from fastapi import HTTPException, status +from ApiLibrary.common.line_number import get_line_number_for_error from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi @@ -26,9 +27,11 @@ class MongoBaseException(Exception): def to_http_exception(self) -> HTTPException: """Convert to FastAPI HTTPException.""" - return HTTPExceptionApi( + raise HTTPExceptionApi( lang="en", error_code=self.status_code, + loc=get_line_number_for_error(), + sys_msg=self.message, ) diff --git a/Services/MongoDb/Models/mixins.py b/Services/MongoDb/Models/mixins.py index da28ce8..16fd056 100644 --- a/Services/MongoDb/Models/mixins.py +++ b/Services/MongoDb/Models/mixins.py @@ -39,24 +39,28 @@ def handle_mongo_errors(func): error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en", loc=get_line_number_for_error(), + sys_msg="MongoDB connection failed", ) except ServerSelectionTimeoutError: raise HTTPExceptionApi( error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en", loc=get_line_number_for_error(), + sys_msg="MongoDB connection timed out", ) except OperationFailure as e: raise HTTPExceptionApi( error_code="HTTP_400_BAD_REQUEST", lang="en", loc=get_line_number_for_error(), + sys_msg=str(e), ) except PyMongoError as e: raise HTTPExceptionApi( error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en", loc=get_line_number_for_error(), + sys_msg=str(e), ) return wrapper diff --git a/Services/PostgresDb/Models/alchemy_response.py b/Services/PostgresDb/Models/alchemy_response.py index 0636337..ca4314c 100644 --- a/Services/PostgresDb/Models/alchemy_response.py +++ b/Services/PostgresDb/Models/alchemy_response.py @@ -161,6 +161,7 @@ class BaseJsonResponse(Generic[T]): lang=cls_object.lang, error_code="HTTP_400_BAD_REQUEST", loc=get_line_number_for_error(), + sys_msg=f"Invalid data type: {type(data)}", ) @@ -201,6 +202,7 @@ class SinglePostgresResponse(BaseJsonResponse[T]): lang=cls_object.lang, error_code="HTTP_400_BAD_REQUEST", loc=get_line_number_for_error(), + sys_msg="No data found", ) instance = super().__new__(cls) @@ -257,6 +259,7 @@ class AlchemyJsonResponse(BaseJsonResponse[T]): lang=cls_object.lang, error_code="HTTP_400_BAD_REQUEST", loc=get_line_number_for_error(), + sys_msg="No data found", ) instance = super().__new__(cls) diff --git a/Services/PostgresDb/Models/filter_functions.py b/Services/PostgresDb/Models/filter_functions.py index 58655aa..efc62bc 100644 --- a/Services/PostgresDb/Models/filter_functions.py +++ b/Services/PostgresDb/Models/filter_functions.py @@ -139,6 +139,7 @@ class FilterAttributes: error_code="HTTP_304_NOT_MODIFIED", lang=cls.lang or "tr", loc=get_line_number_for_error(), + sys_msg=str(e), ) @classmethod @@ -173,6 +174,7 @@ class FilterAttributes: error_code="HTTP_304_NOT_MODIFIED", lang=cls.lang or "tr", loc=get_line_number_for_error(), + sys_msg=str(e), ) @classmethod @@ -193,6 +195,7 @@ class FilterAttributes: error_code="HTTP_304_NOT_MODIFIED", lang=cls.lang or "tr", loc=get_line_number_for_error(), + sys_msg=str(e), ) @classmethod @@ -225,6 +228,7 @@ class FilterAttributes: error_code="HTTP_304_NOT_MODIFIED", lang=cls.lang or "tr", loc=get_line_number_for_error(), + sys_msg=str(e), ) @classmethod