update events via wrapper routers

This commit is contained in:
berkay 2025-01-16 19:32:59 +03:00
parent 049a7c1e11
commit 426b69b33c
42 changed files with 2344 additions and 460 deletions

View File

@ -0,0 +1,5 @@
"""Auth Service API initialization"""
from .route_configs import get_route_configs
__all__ = ["get_route_configs"]

View File

@ -0,0 +1,977 @@
"""
Authentication related API endpoints.
"""
from typing import TYPE_CHECKING, Union, Optional, Dict, Any
# Regular imports (non-TYPE_CHECKING)
from ApiEvents.abstract_class import (
MethodToEvent,
RouteFactoryConfig,
EndpointFactoryConfig,
)
from ApiEvents.base_request_model import (
BaseRequestModel,
DictRequestModel,
SuccessResponse,
)
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]"
event_category = "AUTHENTICATION"
__event_keys__ = {
"e672846d-cc45-4d97-85d5-6f96747fac67": "authentication_login_with_domain_and_creds",
}
__event_validation__ = {
"e672846d-cc45-4d97-85d5-6f96747fac67": SuccessResponse,
}
@classmethod
async def authentication_login_with_domain_and_creds(
cls, request: "Request", data: LoginRequestModel
):
"""
Authenticate user with domain and credentials.
Args:
request: FastAPI request object
data: Request body containing login credentials
{
"domain": "evyos.com.tr",
"access_key": "karatay.berkay.sup@evyos.com.tr",
"password": "string",
"remember_me": false
}
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)
# Return response with token and headers
return {
"token": token,
"headers": dict(request.headers),
}
class AuthenticationSelectEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Select Employee Duty or Occupant Type"
event_category = "AUTHENTICATION"
__event_keys__ = {
"cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type",
}
# __event_validation__ = {
# "cee96b9b-8487-4e9f-aaed-2e8c79687bf9": "authentication_select_company_or_occupant_type",
# }
@classmethod
def _handle_employee_selection(
cls,
data: EmployeeSelectionRequestModel,
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")
@classmethod
def _handle_occupant_selection(
cls,
data: OccupantSelectionRequestModel,
token_dict: TokenDictType,
request: "Request",
):
"""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(),
)
except Exception as e:
return ResponseHandler.error(
str(e), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@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"
event_description = "Check Token is valid for user"
event_category = "AUTHENTICATION"
__event_keys__ = {
"73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid",
}
# __event_validation__ = {
# "73d77e45-a33f-4f12-909e-3b56f00d8a12": "authentication_check_token_is_valid",
# }
@classmethod
def authentication_check_token_is_valid(
cls, request: "Request", data: DictRequestModel
):
# try:
# if RedisActions.get_object_via_access_key(request=request):
# return ResponseHandler.success("Access Token is valid")
# except HTTPException:
# return ResponseHandler.unauthorized("Access Token is NOT valid")
return
class AuthenticationRefreshEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Refresh user info using access token"
event_category = "AUTHENTICATION"
__event_keys__ = {
"48379bb2-ba81-4d8e-a9dd-58837cfcbf67": "authentication_refresh_user_info",
}
# __event_validation__ = {
# "48379bb2-ba81-4d8e-a9dd-58837cfcbf67": AuthenticationRefreshResponse,
# }
@classmethod
def authentication_refresh_user_info(
cls,
request: "Request",
token_dict: TokenDictType,
data: DictRequestModel,
):
# try:
# access_token = request.headers.get(Auth.ACCESS_TOKEN_TAG)
# if not access_token:
# return ResponseHandler.unauthorized()
# # Get user and token info
# found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
# if not found_user:
# return ResponseHandler.not_found("User not found")
# user_token = UsersTokens.filter_one(
# UsersTokens.domain == found_user.domain_name,
# UsersTokens.user_id == found_user.id,
# UsersTokens.token_type == "RememberMe",
# ).data
# response_data = {
# "access_token": access_token,
# "refresh_token": getattr(user_token, "token", None),
# "user": found_user.get_dict(),
# }
# return ResponseHandler.success(
# "User info refreshed successfully",
# data=response_data,
# )
# except Exception as e:
# return ResponseHandler.error(str(e))
return
class AuthenticationChangePasswordEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Change password with access token"
event_category = "AUTHENTICATION"
__event_keys__ = {
"f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password",
}
# __event_validation__ = {
# "f09f7c1a-bee6-4e32-8444-962ec8f39091": "authentication_change_password",
# }
@classmethod
def authentication_change_password(
cls,
request: "Request",
data: ChangePasswordRequestModel,
token_dict: TokenDictType,
):
# try:
# if not isinstance(token_dict, EmployeeTokenObject):
# return ResponseHandler.unauthorized(
# "Only employees can change password"
# )
# found_user = Users.filter_one(Users.uu_id == token_dict.user_uu_id).data
# if not found_user:
# return ResponseHandler.not_found("User not found")
# if not found_user.check_password(data.old_password):
# # UserLogger.log_password_change(
# # request,
# # found_user.id,
# # "change",
# # success=False,
# # error="Invalid old password",
# # )
# return ResponseHandler.unauthorized("Old password is incorrect")
# found_user.set_password(data.new_password)
# # UserLogger.log_password_change(
# # request, found_user.id, "change", success=True
# # )
# return ResponseHandler.success("Password changed successfully")
# except Exception as e:
# # UserLogger.log_password_change(
# # request,
# # found_user.id if found_user else None,
# # "change",
# # success=False,
# # error=str(e),
# # )
# return ResponseHandler.error(str(e))
return
class AuthenticationCreatePasswordEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Create password with password reset token requested via email"
event_category = "AUTHENTICATION"
__event_keys__ = {
"c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password",
}
# __event_validation__ = {
# "c519f9af-92e1-47b2-abf7-5a3316d075f7": "authentication_create_password",
# }
@classmethod
def authentication_create_password(
cls, request: "Request", data: CreatePasswordRequestModel
):
# if not data.re_password == data.password:
# raise HTTPException(
# status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Password must match"
# )
# if found_user := Users.filter_one(
# Users.password_token == data.password_token
# ).data:
# found_user: Users = found_user
# found_user.create_password(found_user=found_user, password=data.password)
# found_user.password_token = ""
# found_user.save()
# # send_email_completed = send_email(
# # subject=f"Dear {found_user.user_tag}, your password has been changed.",
# # receivers=[str(found_user.email)],
# # html=password_is_changed_template(user_name=found_user.user_tag),
# # )
# # if not send_email_completed:
# # raise HTTPException(
# # status_code=400, detail="Email can not be sent. Try again later"
# # )
# return ResponseHandler.success(
# "Password is created successfully",
# data=found_user.get_dict(),
# )
# return ResponseHandler.not_found("Record not found")
return
class AuthenticationDisconnectUserEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Disconnect all sessions of user in access token"
event_category = "AUTHENTICATION"
__event_keys__ = {
"8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user",
}
# __event_validation__ = {
# "8b586848-2fb3-4161-abbe-642157eec7ce": "authentication_disconnect_user",
# }
@classmethod
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
# if not found_user:
# return ResponseHandler.not_found("User not found")
# if already_tokens := RedisActions.get_object_via_user_uu_id(
# user_id=str(found_user.uu_id)
# ):
# for key, token_user in already_tokens.items():
# RedisActions.delete(key)
# selected_user = Users.filter_one(
# Users.uu_id == token_user.get("uu_id"),
# ).data
# selected_user.remove_refresher_token(
# domain=data.domain, disconnect=True
# )
# return ResponseHandler.success(
# "All sessions are disconnected",
# data=selected_user.get_dict(),
# )
# return ResponseHandler.not_found("Invalid data")
return
class AuthenticationLogoutEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Logout only single session of user which domain is provided"
event_category = "AUTHENTICATION"
__event_keys__ = {
"5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user",
}
# __event_validation__ = {
# "5cc22e4e-a0f7-4077-be41-1871feb3dfd1": "authentication_logout_user",
# }
@classmethod
def authentication_logout_user(
cls,
request: "Request",
data: LogoutRequestModel,
token_dict: TokenDictType = None,
):
# token_user = None
# if already_tokens := RedisActions.get_object_via_access_key(request=request):
# for key in already_tokens:
# token_user = RedisActions.get_json(key)
# if token_user.get("domain") == data.domain:
# RedisActions.delete(key)
# selected_user = Users.filter_one(
# Users.uu_id == token_user.get("uu_id"),
# ).data
# selected_user.remove_refresher_token(domain=data.domain)
# return ResponseHandler.success(
# "Session is logged out",
# data=token_user,
# )
# return ResponseHandler.not_found("Logout is not successfully completed")
return
class AuthenticationRefreshTokenEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Refresh access token with refresher token"
event_category = "AUTHENTICATION"
__event_keys__ = {
"c90f3334-10c9-4181-b5ff-90d98a0287b2": "authentication_refresher_token",
}
# __event_validation__ = {
# "c90f3334-10c9-4181-b5ff-90d98a0287b2": AuthenticationRefreshResponse,
# }
@classmethod
def authentication_refresher_token(
cls, request: "Request", data: RememberRequestModel, token_dict: TokenDictType
):
# token_refresher = UsersTokens.filter_by_one(
# token=data.refresh_token,
# domain=data.domain,
# **UsersTokens.valid_record_dict,
# ).data
# if not token_refresher:
# return ResponseHandler.not_found("Invalid data")
# if found_user := Users.filter_one(
# Users.id == token_refresher.user_id,
# ).data:
# found_user: Users = found_user
# access_key = AuthActions.save_access_token_to_redis(
# request=request, found_user=found_user, domain=data.domain
# )
# found_user.last_agent = request.headers.get("User-Agent", None)
# found_user.last_platform = request.headers.get("Origin", None)
# found_user.last_remote_addr = getattr(
# request, "remote_addr", None
# ) or request.headers.get("X-Forwarded-For", None)
# found_user.last_seen = str(system_arrow.now())
# response_data = {
# "access_token": access_key,
# "refresh_token": data.refresh_token,
# }
# return ResponseHandler.success(
# "User is logged in successfully via refresher token",
# data=response_data,
# )
# return ResponseHandler.not_found("Invalid data")
return
class AuthenticationForgotPasswordEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Send an email to user for a valid password reset token"
event_category = "AUTHENTICATION"
__event_keys__ = {
"e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password",
}
# __event_validation__ = {
# "e3ca6e24-b9f8-4127-949c-3bfa364e3513": "authentication_forgot_password",
# }
@classmethod
def authentication_forgot_password(
cls,
request: "Request",
data: ForgotRequestModel,
):
# found_user: Users = Users.check_user_exits(
# access_key=data.access_key, domain=data.domain
# )
# forgot_key = AuthActions.save_access_token_to_redis(
# request=request, found_user=found_user, domain=data.domain
# )
# forgot_link = ApiStatic.forgot_link(forgot_key=forgot_key)
# send_email_completed = send_email(
# subject=f"Dear {found_user.user_tag}, your forgot password link has been sent.",
# receivers=[str(found_user.email)],
# html=change_your_password_template(
# user_name=found_user.user_tag, forgot_link=forgot_link
# ),
# )
# if not send_email_completed:
# raise HTTPException(
# status_code=400, detail="Email can not be sent. Try again later"
# )
# found_user.password_token = forgot_key
# found_user.password_token_is_valid = str(system_arrow.shift(days=1))
# found_user.save()
# return ResponseHandler.success(
# "Password is change link is sent to your email or phone",
# data={},
# )
return
class AuthenticationResetPasswordEventMethods(MethodToEvent):
event_type = "UPDATE"
__event_keys__ = {
"af9e121e-24bb-44ac-a616-471d5754360e": "authentication_reset_password",
}
@classmethod
def authentication_reset_password(
cls, request: "Request", data: ForgotRequestModel
):
# from sqlalchemy import or_
# found_user = Users.query.filter(
# or_(
# Users.email == str(data.access_key).lower(),
# Users.phone_number == str(data.access_key).replace(" ", ""),
# ),
# ).first()
# if not found_user:
# raise HTTPException(
# status_code=status.HTTP_400_BAD_REQUEST,
# detail="Given access key or domain is not matching with the any user record.",
# )
# reset_password_token = found_user.reset_password_token(found_user=found_user)
# send_email_completed = send_email(
# subject=f"Dear {found_user.user_tag}, a password reset request has been received.",
# receivers=[str(found_user.email)],
# html=change_your_password_template(
# user_name=found_user.user_tag,
# forgot_link=ApiStatic.forgot_link(forgot_key=reset_password_token),
# ),
# )
# if not send_email_completed:
# raise found_user.raise_http_exception(
# status_code=400, message="Email can not be sent. Try again later"
# )
# return ResponseHandler.success(
# "Password change link is sent to your email or phone",
# data=found_user.get_dict(),
# )
return
class AuthenticationDownloadAvatarEventMethods(MethodToEvent):
event_type = "LOGIN"
event_description = "Download avatar icon and profile info of user"
event_category = "AUTHENTICATION"
__event_keys__ = {
"c140cd5f-307f-4046-a93e-3ade032a57a7": "authentication_download_avatar",
}
# __event_validation__ = {
# "c140cd5f-307f-4046-a93e-3ade032a57a7": AuthenticationUserInfoResponse,
# }
@classmethod
def authentication_download_avatar(
cls,
token_dict: TokenDictType,
request: "Request",
data: DictRequestModel,
):
# if found_user := Users.filter_one(Users.id == token_dict.user_id).data:
# expired_starts = str(
# system_arrow.now() - system_arrow.get(str(found_user.expiry_ends))
# )
# expired_int = (
# system_arrow.now() - system_arrow.get(str(found_user.expiry_ends))
# ).days
# user_info = {
# "lang": token_dict.lang,
# "full_name": found_user.person.full_name,
# "avatar": found_user.avatar,
# "remember_me": found_user.remember_me,
# "expiry_ends": str(found_user.expiry_ends),
# "expired_str": expired_starts,
# "expired_int": int(expired_int),
# }
# return ResponseHandler.success(
# "Avatar and profile is shared via user credentials",
# data=user_info,
# )
# 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()

View File

@ -0,0 +1,20 @@
"""
Route configuration registry.
This module collects and registers all route configurations from different modules
to be used by the dynamic route creation system.
"""
from typing import Dict, List, Any
from .auth.auth import AUTH_CONFIG
# Registry of all route configurations
ROUTE_CONFIGS = [
AUTH_CONFIG,
]
def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations."""
return [AUTH_CONFIG]

View File

@ -0,0 +1,5 @@
"""Event Service API initialization"""
from .route_configs import get_route_configs
__all__ = ["get_route_configs"]

View File

@ -1,8 +1,19 @@
import typing import typing
from collections.abc import Callable
from fastapi import Request from ApiEvents.abstract_class import (
from typing import Dict, Any 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 ApiValidations.Custom.token_objects import ( from ApiValidations.Custom.token_objects import (
OccupantTokenObject, OccupantTokenObject,
@ -21,15 +32,43 @@ from ApiValidations.Request import (
UpdateAccountRecord, UpdateAccountRecord,
ListOptions, ListOptions,
) )
from Services.PostgresDb.Models.alchemy_response import AlchemyJsonResponse from Services.PostgresDb.Models.alchemy_response import (
from ApiValidations.Response import AccountRecordResponse AlchemyJsonResponse,
from events.abstract_class import ( DictJsonResponse,
MethodToEvent,
RouteFactoryConfig,
EndpointFactoryConfig,
) )
from ApiValidations.Response import AccountRecordResponse
# from events.utils import with_token_event
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
class AccountRecordsListEventMethods(MethodToEvent): class AccountRecordsListEventMethods(MethodToEvent):
@ -50,7 +89,7 @@ class AccountRecordsListEventMethods(MethodToEvent):
@classmethod @classmethod
def account_records_list( def account_records_list(
cls, cls,
list_options: ListOptions, list_options: ListOptionsRequestModel,
token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject],
): ):
db_session = AccountRecords.new_session() db_session = AccountRecords.new_session()
@ -79,7 +118,7 @@ class AccountRecordsListEventMethods(MethodToEvent):
@classmethod @classmethod
def account_records_list_flt_res( def account_records_list_flt_res(
cls, cls,
list_options: ListOptions, list_options: ListOptionsRequestModel,
token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject],
): ):
db_session = AccountRecords.new_session() db_session = AccountRecords.new_session()
@ -229,7 +268,7 @@ class AccountRecordsCreateEventMethods(MethodToEvent):
@classmethod @classmethod
def account_records_create( def account_records_create(
cls, cls,
data: InsertAccountRecord, data: InsertAccountRecordRequestModel,
token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject],
): ):
data_dict = data.excluded_dump() data_dict = data.excluded_dump()
@ -318,7 +357,7 @@ class AccountRecordsUpdateEventMethods(MethodToEvent):
def build_area_update( def build_area_update(
cls, cls,
build_uu_id: str, build_uu_id: str,
data: UpdateAccountRecord, data: UpdateAccountRecordRequestModel,
token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject], token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject],
): ):
if isinstance(token_dict, OccupantTokenObject): if isinstance(token_dict, OccupantTokenObject):
@ -336,52 +375,19 @@ class AccountRecordsUpdateEventMethods(MethodToEvent):
) )
class AccountRecordsPatchEventMethods(MethodToEvent): @endpoint_wrapper("/account/records/address/list")
async def address_list(request: "Request", data: ListOptionsRequestModel):
event_type = "PATCH"
event_description = ""
event_category = ""
__event_keys__ = {
"34c38937-42a2-45f1-b2ef-a23978650aee": "account_records_patch",
}
__event_validation__ = {
"34c38937-42a2-45f1-b2ef-a23978650aee": None,
}
@classmethod
def build_area_patch(
cls,
build_uu_id: str,
data,
token_dict: typing.Union[EmployeeTokenObject, OccupantTokenObject],
):
account_record = AccountRecords.patch_one(build_uu_id, data).data
return AlchemyJsonResponse(
completed=True,
message="Account record patched successfully",
result=account_record,
)
def address_list(request: Request, data: dict) -> Dict[str, Any]:
"""Handle address list endpoint.""" """Handle address list endpoint."""
# Access context through the handler
handler = address_list.handler
handler_context = address_list.handler.context
function_name = AccountRecordsListEventMethods.__event_keys__.get(handler.function_code)
original_function = getattr(AccountRecordsListEventMethods, function_name)
# original_function(data, request)
return { return {
"data": data, "data": data,
"function_code": handler.function_code, # This will be the URL
"token_dict": handler_context.get('token_dict'),
"url_of_endpoint": handler_context.get('url_of_endpoint'),
"request": str(request.headers), "request": str(request.headers),
"request_url": str(request.url),
"request_base_url": str(request.base_url),
} }
def address_create(request: Request, data: dict):
@endpoint_wrapper("/account/records/address/create")
async def address_create(request: "Request", data: DictRequestModel):
"""Handle address creation endpoint.""" """Handle address creation endpoint."""
return { return {
"data": data, "data": data,
@ -390,80 +396,100 @@ def address_create(request: Request, data: dict):
"request_base_url": str(request.base_url), "request_base_url": str(request.base_url),
} }
def address_search(request: Request, data: dict):
@endpoint_wrapper("/account/records/address/search")
async def address_search(request: "Request", data: DictRequestModel):
"""Handle address search endpoint.""" """Handle address search endpoint."""
# Get function_code from the wrapper's closure return {"data": data}
function_code = address_search.function_code
return {
"data": data,
"function_code": function_code
}
def address_update(request: Request, address_uu_id: str, data: dict):
"""Handle address update endpoint."""
# Get function_code from the wrapper's closure
function_code = address_update.function_code
return {
"address_uu_id": address_uu_id,
"data": data,
"function_code": function_code
}
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 Router Configuration
ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig( ACCOUNT_RECORDS_CONFIG = RouteFactoryConfig(
name='account_records', name="account_records",
prefix='/account/records', prefix=prefix,
tags=['Account Records'], tags=["Account Records"],
include_in_schema=True, include_in_schema=True,
endpoints=[ endpoints=[
EndpointFactoryConfig( EndpointFactoryConfig(
url_prefix = "/account/records", url_prefix=prefix,
url_endpoint="/address/list", url_endpoint="/address/list",
url_of_endpoint = "/account/records/address/list", url_of_endpoint="/account/records/address/list",
endpoint="/address/list", endpoint="/address/list",
method="POST", method="POST",
summary="List Active/Delete/Confirm Address", summary="List Active/Delete/Confirm Address",
description="List Active/Delete/Confirm Address", description="List Active/Delete/Confirm Address",
is_auth_required=True, is_auth_required=True,
is_event_required=True, is_event_required=True,
endpoint_function=address_list endpoint_function=address_list,
), ),
EndpointFactoryConfig( EndpointFactoryConfig(
url_prefix = "/account/records", url_prefix=prefix,
url_endpoint="/address/create", url_endpoint="/address/create",
url_of_endpoint = "/account/records/address/create", url_of_endpoint="/account/records/address/create",
endpoint="/address/create", endpoint="/address/create",
method="POST", method="POST",
summary="Create Address with given auth levels", summary="Create Address with given auth levels",
description="Create Address with given auth levels", description="Create Address with given auth levels",
is_auth_required=False, is_auth_required=False,
is_event_required=False, is_event_required=False,
endpoint_function=address_create endpoint_function=address_create,
), ),
EndpointFactoryConfig( EndpointFactoryConfig(
url_prefix = "/account/records", url_prefix=prefix,
url_endpoint="/address/search", url_endpoint="/address/search",
url_of_endpoint = "/account/records/address/search", url_of_endpoint="/account/records/address/search",
endpoint="/address/search", endpoint="/address/search",
method="POST", method="POST",
summary="Search Address with given auth levels", summary="Search Address with given auth levels",
description="Search Address with given auth levels", description="Search Address with given auth levels",
is_auth_required=True, is_auth_required=True,
is_event_required=True, is_event_required=True,
endpoint_function=address_search endpoint_function=address_search,
), ),
EndpointFactoryConfig( EndpointFactoryConfig(
url_prefix = "/account/records", url_prefix=prefix,
url_endpoint="/address/update/{address_uu_id}", url_endpoint="/address/{address_uu_id}",
url_of_endpoint="/account/records/address/update/{address_uu_id}", url_of_endpoint="/account/records/address/{address_uu_id}",
endpoint="/address/update/{address_uu_id}", endpoint="/address/{address_uu_id}",
method="PUT", method="PUT",
summary="Update Address with given auth levels", summary="Update Address with given auth levels",
description="Update Address with given auth levels", description="Update Address with given auth levels",
is_auth_required=True, is_auth_required=True,
is_event_required=True, is_event_required=True,
endpoint_function=address_update endpoint_function=address_update,
) ),
] ],
).as_dict() ).as_dict()

View File

@ -1,12 +1,12 @@
""" """
Route configuration registry. Route configuration registry for Event Service.
This module collects and registers all route configurations from different modules This module collects and registers all route configurations from different modules
to be used by the dynamic route creation system. to be used by the dynamic route creation system.
""" """
from typing import Dict, List, Any from typing import Dict, List, Any
from events.account.account_records import ACCOUNT_RECORDS_CONFIG from .account.account_records import ACCOUNT_RECORDS_CONFIG
# Registry of all route configurations # Registry of all route configurations
@ -14,6 +14,7 @@ ROUTE_CONFIGS = [
ACCOUNT_RECORDS_CONFIG, ACCOUNT_RECORDS_CONFIG,
] ]
def get_route_configs() -> List[Dict[str, Any]]: def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations.""" """Get all registered route configurations."""
return ROUTE_CONFIGS return [ACCOUNT_RECORDS_CONFIG]

View File

@ -0,0 +1,5 @@
"""Event Service API initialization"""
from .route_configs import get_route_configs
__all__ = ["get_route_configs"]

View File

@ -0,0 +1,17 @@
"""
Route configuration registry for Event Service.
This module collects and registers all route configurations from different modules
to be used by the dynamic route creation system.
"""
from typing import Dict, List, Any
# Registry of all route configurations
ROUTE_CONFIGS = []
def get_route_configs() -> List[Dict[str, Any]]:
"""Get all registered route configurations."""
return []

View File

@ -0,0 +1,21 @@
"""ApiEvents package initialization.
This module serves as the main entry point for the ApiEvents package,
making common utilities and base classes available for all API services.
"""
from .abstract_class import (
MethodToEvent,
RouteFactoryConfig,
EndpointFactoryConfig,
)
from .base_request_model import BaseRequestModel, DictRequestModel
# Re-export commonly used classes
__all__ = [
"MethodToEvent",
"RouteFactoryConfig",
"EndpointFactoryConfig",
"BaseRequestModel",
"DictRequestModel",
]

View File

@ -5,12 +5,66 @@ This module provides core abstractions for route configuration and factory,
with support for authentication and event handling. with support for authentication and event handling.
""" """
from typing import Optional, Dict, Any, List, Type, Union, ClassVar, Tuple, TypeVar, Callable from typing import (
Tuple,
TypeVar,
Optional,
Callable,
Dict,
Any,
List,
Type,
ClassVar,
Union,
Awaitable,
)
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import Request, Depends, APIRouter
from functools import wraps
import inspect
ResponseModel = TypeVar('ResponseModel', bound=BaseModel) ResponseModel = TypeVar("ResponseModel", bound=BaseModel)
def endpoint_wrapper(url_of_endpoint: Optional[str] = None):
"""Create a wrapper for endpoints that stores url_of_endpoint in closure.
Args:
url_of_endpoint: Optional URL path for the endpoint
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(
*args: Any, **kwargs: Any
) -> Union[Dict[str, Any], BaseModel]:
# Handle both async and sync functions
if inspect.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
# If result is a coroutine, await it
if inspect.iscoroutine(result):
result = await result
# Add function_code to the result
if isinstance(result, dict):
result["function_code"] = url_of_endpoint
return result
elif isinstance(result, BaseModel):
# Convert Pydantic model to dict and add function_code
result_dict = result.model_dump()
result_dict["function_code"] = url_of_endpoint
return result_dict
return result
wrapper.url_of_endpoint = url_of_endpoint
return wrapper
return decorator
@dataclass @dataclass
@ -25,40 +79,53 @@ class EndpointFactoryConfig:
description: Detailed description for API documentation description: Detailed description for API documentation
endpoint_function: Function to handle the endpoint endpoint_function: Function to handle the endpoint
is_auth_required: Whether authentication is required is_auth_required: Whether authentication is required
response_model: Optional response model for OpenAPI schema
request_model: Optional request model for OpenAPI schema
is_event_required: Whether event handling is required is_event_required: Whether event handling is required
extra_options: Additional endpoint options extra_options: Additional endpoint options
""" """
url_prefix :str
url_prefix: str
url_endpoint: str url_endpoint: str
url_of_endpoint: str url_of_endpoint: str
endpoint: str endpoint: str
method: str method: str
summary: str summary: str
description: str description: str
endpoint_function: Callable endpoint_function: Callable[..., Any] # Now accepts any parameters and return type
response_model: Optional[type] = None
request_model: Optional[type] = None
is_auth_required: bool = True is_auth_required: bool = True
is_event_required: bool = False is_event_required: bool = False
extra_options: Dict[str, Any] = field(default_factory=dict) extra_options: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self): def __post_init__(self):
"""Post-initialization processing. """Post initialization hook.
Apply appropriate wrappers based on auth and event requirements: Wraps endpoint function with appropriate middleware based on configuration:
- If both auth and event required -> wrap with with_token_event - If auth and event required -> wrap with TokenEventMiddleware
- If only event required -> wrap with EventMiddleware
- If only auth required -> wrap with MiddlewareModule.auth_required - If only auth required -> wrap with MiddlewareModule.auth_required
""" """
# Store url_of_endpoint for the handler # Wrap the endpoint function to store url_of_endpoint
self.endpoint_function.url_of_endpoint = self.url_of_endpoint self.endpoint_function = endpoint_wrapper(self.url_of_endpoint)(
self.endpoint_function
)
if self.is_auth_required and self.is_event_required: if self.is_auth_required and self.is_event_required:
from events.utils import with_token_event from middleware import TokenEventMiddleware
self.endpoint_function = with_token_event(self.endpoint_function)
self.endpoint_function = TokenEventMiddleware.event_required(
self.endpoint_function
)
elif self.is_auth_required: elif self.is_auth_required:
from DockerApiServices.AllApiNeeds.middleware.auth_middleware import MiddlewareModule from middleware import MiddlewareModule
self.endpoint_function = MiddlewareModule.auth_required(self.endpoint_function)
self.endpoint_function = MiddlewareModule.auth_required(
self.endpoint_function
)
@dataclass
class RouteFactoryConfig: class RouteFactoryConfig:
"""Configuration class for API route factories. """Configuration class for API route factories.
@ -70,17 +137,29 @@ class RouteFactoryConfig:
endpoints: List of endpoint configurations endpoints: List of endpoint configurations
extra_options: Additional route options extra_options: Additional route options
""" """
name: str
tags: Union[str, List[str]] def __init__(
prefix: str self,
include_in_schema: bool = True name: str,
endpoints: List[EndpointFactoryConfig] = field(default_factory=list) tags: List[str],
extra_options: Dict[str, Any] = field(default_factory=dict) prefix: str,
include_in_schema: bool = True,
endpoints: List[EndpointFactoryConfig] = None,
extra_options: Dict[str, Any] = None,
):
self.name = name
self.tags = tags
self.prefix = prefix
self.include_in_schema = include_in_schema
self.endpoints = endpoints or []
self.extra_options = extra_options or {}
def __post_init__(self): def __post_init__(self):
"""Validate and normalize configuration after initialization.""" """Validate and normalize configuration after initialization."""
if isinstance(self.tags, str): if self.endpoints is None:
self.tags = [self.tags] self.endpoints = []
if self.extra_options is None:
self.extra_options = {}
def as_dict(self) -> Dict[str, Any]: def as_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary format.""" """Convert configuration to dictionary format."""
@ -89,20 +168,8 @@ class RouteFactoryConfig:
"tags": self.tags, "tags": self.tags,
"prefix": self.prefix, "prefix": self.prefix,
"include_in_schema": self.include_in_schema, "include_in_schema": self.include_in_schema,
"endpoints": [ "endpoints": [endpoint.__dict__ for endpoint in self.endpoints],
{ "extra_options": self.extra_options,
"endpoint": ep.endpoint,
"method": ep.method,
"summary": ep.summary,
"description": ep.description,
"endpoint_function": ep.endpoint_function,
"is_auth_required": ep.is_auth_required,
"is_event_required": ep.is_event_required,
"extra_options": ep.extra_options
}
for ep in self.endpoints
],
"extra_options": self.extra_options
} }
@ -112,6 +179,7 @@ class ActionsSchema:
This class handles endpoint registration and validation in the database. This class handles endpoint registration and validation in the database.
Subclasses should implement specific validation logic. Subclasses should implement specific validation logic.
""" """
def __init__(self, endpoint: str): def __init__(self, endpoint: str):
"""Initialize with an API endpoint path. """Initialize with an API endpoint path.
@ -129,7 +197,9 @@ class ActionsSchema:
Raises: Raises:
HTTPException: If endpoint is not found in database HTTPException: If endpoint is not found in database
""" """
raise NotImplementedError("Subclasses must implement retrieve_action_from_endpoint") raise NotImplementedError(
"Subclasses must implement retrieve_action_from_endpoint"
)
class ActionsSchemaFactory: class ActionsSchemaFactory:
@ -138,6 +208,7 @@ class ActionsSchemaFactory:
This class ensures proper initialization and validation of API endpoints This class ensures proper initialization and validation of API endpoints
through their action schemas. through their action schemas.
""" """
def __init__(self, action: ActionsSchema): def __init__(self, action: ActionsSchema):
"""Initialize with an action schema. """Initialize with an action schema.
@ -161,12 +232,15 @@ class MethodToEvent:
TokenType: Type of authentication token TokenType: Type of authentication token
ResponseModel: Type of response model ResponseModel: Type of response model
""" """
action_key: ClassVar[Optional[str]] = None action_key: ClassVar[Optional[str]] = None
event_type: ClassVar[Optional[str]] = None event_type: ClassVar[Optional[str]] = None
event_description: ClassVar[str] = "" event_description: ClassVar[str] = ""
event_category: ClassVar[str] = "" event_category: ClassVar[str] = ""
__event_keys__: ClassVar[Dict[str, str]] = {} __event_keys__: ClassVar[Dict[str, str]] = {}
__event_validation__: ClassVar[List[Tuple[Type[ResponseModel], List[Dict[str, Any]]]]] = [] __event_validation__: ClassVar[Dict[str, Tuple[Type[ResponseModel], List[Any]]]] = (
{}
)
@classmethod @classmethod
def retrieve_language_parameters( def retrieve_language_parameters(
@ -184,9 +258,9 @@ class MethodToEvent:
validation_dict = dict(cls.__event_validation__) validation_dict = dict(cls.__event_validation__)
if function_code not in validation_dict: if function_code not in validation_dict:
return {} return {}
event_response_model, event_language_models = validation_dict[function_code] event_response_model, event_language_models = validation_dict[function_code]
# Collect language-specific field mappings # Collect language-specific field mappings
language_models = {} language_models = {}
for model in event_language_models: for model in event_language_models:

View File

@ -0,0 +1,123 @@
"""
Base request models for API endpoints.
This module provides base request models that can be used across different endpoints
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
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.
"""
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": {
"key1": "value1",
"key2": "value2",
"nested": {"inner_key": "inner_value"},
}
}
)
@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(
...,
example={
"id": "123",
"username": "john.doe",
"email": "john@example.com",
"role": "user",
},
)
model_config = ConfigDict(
json_schema_extra={
"example": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user_info": {
"id": "123",
"username": "john.doe",
"email": "john@example.com",
"role": "user",
},
}
}
)

View File

@ -1,104 +0,0 @@
"""
Utility functions for API event handling.
"""
from typing import TypeVar, Callable, Dict, Any
from functools import wraps
from fastapi import Request
R = TypeVar('R')
class BaseEndpointHandler:
"""Base class for handling endpoint execution with context."""
def __init__(self, func: Callable, url_of_endpoint: str):
self.func = func
self.url_of_endpoint = url_of_endpoint
self.function_code = url_of_endpoint # Set initial function_code
self._context = {
'url_of_endpoint': url_of_endpoint,
'function_code': url_of_endpoint, # Initialize with URL
}
@property
def context(self) -> dict:
"""Get the endpoint context."""
return self._context
def update_context(self, **kwargs):
"""Update the endpoint context with new values."""
self._context.update(kwargs)
# Update function_code property if it's in the context
if 'function_code' in kwargs:
self.function_code = kwargs['function_code']
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class TokenEventHandler(BaseEndpointHandler):
"""Handler for endpoints that require token and event tracking."""
def __init__(self, func: Callable, url_of_endpoint: str):
super().__init__(func, url_of_endpoint)
self.update_context(
token_dict={
'user_id': '1234567890',
'username': 'test_user',
'email': 'asda@email.com',
}
)
class AuthHandler(BaseEndpointHandler):
"""Handler for endpoints that require only authentication."""
def __init__(self, func: Callable, url_of_endpoint: str):
super().__init__(func, url_of_endpoint)
self.update_context(
auth_level="user",
permissions=["read", "write"]
)
def with_token_event(func: Callable[..., Dict[str, Any]]) -> Callable[..., Dict[str, Any]]:
"""Decorator for endpoints with token and event requirements."""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = TokenEventHandler(
func=func,
url_of_endpoint=func.url_of_endpoint
)
# Update event-specific context
handler.update_context(
function_code=f"7192c2aa-5352-4e36-98b3-dafb7d036a3d" # Keep function_code as URL
)
# Make context available to the function
func.handler = handler
# Call the original function
return func(*args, **kwargs)
return wrapper
def auth_required(func: Callable[..., Dict[str, Any]]) -> Callable[..., Dict[str, Any]]:
"""Decorator for endpoints with only auth requirements."""
@wraps(func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = AuthHandler(
func=func,
url_of_endpoint=func.url_of_endpoint
)
# Make context available to the function
func.handler = handler
# Call the original function
return func(*args, **kwargs)
return wrapper

View File

@ -0,0 +1,5 @@
def get_line_number_for_error():
from inspect import currentframe, getframeinfo
frameinfo = getframeinfo(currentframe())
return f"{frameinfo.filename} | {frameinfo.lineno}"

View File

@ -0,0 +1,68 @@
from typing import Any, ClassVar, Dict
from sqlalchemy import or_
from ApiLibrary.common.line_number import get_line_number_for_error
from Schemas import Users
from ErrorHandlers import HTTPExceptionApi
from ApiValidations.Request.authentication import Login
from ApiLibrary.token.password_module import PasswordModule
from ApiServices.Token.token_handler import TokenService
class UserLoginModule:
def __init__(self, request: "Request"):
self.request = request
@staticmethod
async def check_user_exists(access_key) -> ClassVar[Users]:
"""Check if user exists."""
db_session = Users.new_session()
if "@" in access_key:
found_user = Users.filter_one(
Users.email == access_key.lower(), db=db_session
).data
else:
found_user = Users.filter_one(
Users.phone_number == access_key.replace(" ", ""), db=db_session
).data
if not found_user:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang="en",
loc=get_line_number_for_error(),
)
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
if hasattr(access_data, "data"):
access_data = access_data.data
found_user: Users = await self.check_user_exists(
access_key=access_data.access_key
)
if len(found_user.hash_password) < 5:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=found_user.lang,
loc=get_line_number_for_error(),
)
if PasswordModule.check_password(
domain=access_data.domain,
id_=found_user.uu_id,
password=access_data.password,
password_hashed=found_user.hash_password,
):
return TokenService.set_access_token_to_redis(
request=self.request,
user=found_user,
domain=access_data.domain,
remember=access_data.remember_me,
)
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang=found_user.lang,
loc=get_line_number_for_error(),
)

View File

@ -1,78 +1,354 @@
from typing import Union """Token service for handling authentication tokens and user sessions."""
import json
from typing import List, Union, TypeVar, Dict, Any, Optional, TYPE_CHECKING
from AllConfigs.Token.config import Auth from AllConfigs.Token.config import Auth
from ApiLibrary.common.line_number import get_line_number_for_error
from ApiLibrary.date_time_actions.date_functions import DateTimeLocal
from ApiLibrary.token.password_module import PasswordModule
from ErrorHandlers import HTTPExceptionApi from ErrorHandlers import HTTPExceptionApi
from Schemas.identity.identity import UsersTokens
from Services.Redis import RedisActions, AccessToken from Services.Redis import RedisActions, AccessToken
from fastapi import Request from ApiValidations.Custom.token_objects import (
from ApiValidations.Custom.token_objects import EmployeeTokenObject, OccupantTokenObject EmployeeTokenObject,
OccupantTokenObject,
UserType,
)
from Schemas import (
Users,
BuildLivingSpace,
BuildParts,
Employees,
Addresses,
Companies,
Staff,
Duty,
Duties,
Departments,
OccupantTypes,
)
from Services.Redis.Models.base import RedisRow
from Services.Redis.Models.response import RedisResponse
if TYPE_CHECKING:
from fastapi import Request
T = TypeVar("T", EmployeeTokenObject, OccupantTokenObject)
class TokenService: class TokenService:
"""Service class for handling authentication tokens and user sessions."""
@classmethod @classmethod
def raise_error_if_request_has_no_token(cls, request: Request) -> None: def _create_access_token(cls, access: bool = True) -> str:
"""Get access token from request headers.""" """Generate a new access token."""
if not access:
return PasswordModule.generate_token(Auth.REFRESHER_TOKEN_LENGTH)
return PasswordModule.generate_token(Auth.ACCESS_TOKEN_LENGTH)
@classmethod
def _get_user_tokens(cls, user: Users) -> RedisResponse:
"""Get all tokens for a user from Redis."""
return RedisActions.get_json(
list_keys=AccessToken(
userUUID=user.uu_id,
).to_list()
)
@classmethod
def do_occupant_login(
cls, request: "Request", user: Users, domain: str
) -> Dict[str, Any]:
"""Handle occupant login process and return login information."""
db_session = BuildLivingSpace.new_session()
living_spaces: list[BuildLivingSpace] = BuildLivingSpace.filter_all(
BuildLivingSpace.person_id == user.person_id, db=db_session
).data
if not living_spaces:
raise HTTPExceptionApi(
error_code="", lang="en", loc=get_line_number_for_error()
)
occupants_selection_dict: Dict[str, Any] = {}
for living_space in living_spaces:
build_parts_selection = BuildParts.filter_all(
BuildParts.id == living_space.build_parts_id,
db=db_session,
).data
if not build_parts_selection:
raise HTTPExceptionApi(
error_code="", lang="en", loc=get_line_number_for_error()
)
build_part = build_parts_selection.get(1)
build = build_part.buildings
occupant_type = OccupantTypes.filter_by_one(
id=living_space.occupant_type,
db=db_session,
system=True,
).data
occupant_data = {
"part_uu_id": str(build_part.uu_id),
"part_name": build_part.part_name,
"part_level": build_part.part_level,
"uu_id": str(occupant_type.uu_id),
"description": occupant_type.occupant_description,
"code": occupant_type.occupant_code,
}
build_key = str(build.uu_id)
if build_key not in occupants_selection_dict:
occupants_selection_dict[build_key] = {
"build_uu_id": build_key,
"build_name": build.build_name,
"build_no": build.build_no,
"occupants": [occupant_data],
}
else:
occupants_selection_dict[build_key]["occupants"].append(occupant_data)
model_value = OccupantTokenObject(
domain=domain,
user_type=UserType.occupant.value,
user_uu_id=str(user.uu_id),
credentials=user.credentials(),
user_id=user.id,
person_id=user.person_id,
person_uu_id=str(user.person.uu_id),
request=dict(request.headers),
available_occupants=occupants_selection_dict,
timezone=user.local_timezone or "GMT+0",
lang=user.lang or "tr",
).model_dump()
cls.set_object_to_redis(user, model_value)
return {
"user_type": UserType.occupant.name,
"available_occupants": occupants_selection_dict,
}
@classmethod
def set_object_to_redis(cls, user, model: Dict):
accessObject = AccessToken(
userUUID=user.uu_id,
accessToken=cls._create_access_token(),
)
return RedisActions.set_json(
list_keys=accessObject.to_list(),
value=json.dumps(model),
expires=Auth.TOKEN_EXPIRE_MINUTES_30.seconds,
)
@classmethod
def do_employee_login(
cls, request: "Request", user: Users, domain: str
) -> Dict[str, Any]:
"""Handle employee login process and return login information."""
db_session = Employees.new_session()
list_employee = Employees.filter_all(
Employees.people_id == user.person_id, db=db_session
).data
companies_uu_id_list: List[str] = []
companies_id_list: List[int] = []
companies_list: List[Dict[str, Any]] = []
duty_uu_id_list: List[str] = []
duty_id_list: List[int] = []
for employee in list_employee:
staff = Staff.filter_one(Staff.id == employee.staff_id, db=db_session).data
if duties := Duties.filter_one(
Duties.id == staff.duties_id, db=db_session
).data:
if duty_found := Duty.filter_by_one(
id=duties.duties_id, db=db_session
).data:
duty_uu_id_list.append(str(duty_found.uu_id))
duty_id_list.append(duty_found.id)
department = Departments.filter_one(
Departments.id == duties.department_id, db=db_session
).data
if company := Companies.filter_one(
Companies.id == department.company_id, db=db_session
).data:
companies_uu_id_list.append(str(company.uu_id))
companies_id_list.append(company.id)
company_address = Addresses.filter_by_one(
id=company.official_address_id, db=db_session
).data
companies_list.append(
{
"uu_id": str(company.uu_id),
"public_name": company.public_name,
"company_type": company.company_type,
"company_address": company_address,
}
)
model_value = EmployeeTokenObject(
domain=domain,
user_type=UserType.employee.value,
user_uu_id=str(user.uu_id),
credentials=user.credentials(),
user_id=user.id,
person_id=user.person_id,
person_uu_id=str(user.person.uu_id),
request=dict(request.headers),
companies_uu_id_list=companies_uu_id_list,
companies_id_list=companies_id_list,
duty_uu_id_list=duty_uu_id_list,
duty_id_list=duty_id_list,
timezone=user.local_timezone or "GMT+0",
lang=user.lang or "tr",
).model_dump()
if cls.set_object_to_redis(user, model_value):
return {
"user_type": UserType.employee.name,
"companies_list": companies_list,
}
raise HTTPExceptionApi(
error_code="", lang="en", loc=get_line_number_for_error()
)
@classmethod
def remove_token_with_domain(cls, user: Users, domain: str) -> None:
"""Remove all tokens for a user with specific domain."""
redis_rows = cls._get_user_tokens(user)
for redis_row in redis_rows.all:
if redis_row.get("domain") == domain:
RedisActions.delete_key(redis_row.key)
@classmethod
def remove_all_token(cls, user: Users) -> None:
"""Remove all tokens for a user."""
redis_rows = cls._get_user_tokens(user)
RedisActions.delete([redis_row.key for redis_row in redis_rows.all])
@classmethod
def set_access_token_to_redis(
cls,
request: "Request",
user: Users,
domain: str,
remember: bool,
) -> Dict[str, Any]:
"""Set access token to redis and handle user session."""
cls.remove_token_with_domain(user=user, domain=domain)
Users.client_arrow = DateTimeLocal(is_client=True, timezone=user.local_timezone)
db_session = UsersTokens.new_session()
# Handle login based on user type
login_dict = (
cls.do_occupant_login(request=request, user=user, domain=domain)
if user.is_occupant
else (
cls.do_employee_login(request=request, user=user, domain=domain)
if user.is_employee
else {}
)
)
# Handle remember me functionality
if remember:
users_token = UsersTokens.find_or_create(
db=db_session,
user_id=user.id,
token_type="RememberMe",
token=cls._create_access_token(access=False),
domain=domain,
)
if users_token.meta_data.get("created"):
user.remember_me = True
else:
if UsersTokens.filter_all(
UsersTokens.user_id == user.id,
UsersTokens.token_type == "RememberMe",
UsersTokens.domain == domain,
db=db_session,
).data:
UsersTokens.filter_all(
UsersTokens.user_id == user.id,
UsersTokens.token_type == "RememberMe",
UsersTokens.domain == domain,
db=db_session,
).query.delete(synchronize_session=False)
user.remember_me = False
user.save(db=db_session)
return {
**login_dict,
"user": user.get_dict(),
}
@classmethod
def raise_error_if_request_has_no_token(cls, request: "Request") -> None:
"""Validate request has required token headers."""
if not hasattr(request, "headers"): if not hasattr(request, "headers"):
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="", error_code="", lang="en", loc=get_line_number_for_error()
lang="en",
) )
if not request.headers.get(Auth.ACCESS_TOKEN_TAG): if not request.headers.get(Auth.ACCESS_TOKEN_TAG):
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="", error_code="", lang="en", loc=get_line_number_for_error()
lang="en",
) )
@classmethod @classmethod
def get_access_token_from_request(cls, request: Request) -> str: def access_token_is_valid(cls, request: "Request") -> bool:
"""Get access token from request headers.""" """Check if access token in request is valid."""
access_token = cls.get_access_token_from_request(request=request)
return RedisActions.get_json(
list_keys=AccessToken(accessToken=access_token).to_list()
).status
@classmethod
def get_access_token_from_request(cls, request: "Request") -> str:
"""Extract access token from request headers."""
cls.raise_error_if_request_has_no_token(request=request) cls.raise_error_if_request_has_no_token(request=request)
return request.headers.get(Auth.ACCESS_TOKEN_TAG) return request.headers.get(Auth.ACCESS_TOKEN_TAG)
@classmethod @classmethod
def get_object_via_access_key(cls, access_token: str) -> Union[EmployeeTokenObject, OccupantTokenObject]: def _process_redis_object(cls, redis_object: Dict[str, Any]) -> T:
"""Get access token from request headers.""" """Process Redis object and return appropriate token object."""
access_token = AccessToken( if not redis_object.get("selected_company"):
accessToken=access_token, redis_object["selected_company"] = None
userUUID="", if not redis_object.get("selected_occupant"):
) redis_object["selected_occupant"] = None
if redis_object := RedisActions.get_json(
list_keys=access_token.to_list() if redis_object.get("user_type") == UserType.employee.value:
).first.data: return EmployeeTokenObject(**redis_object)
access_token.userUUID = redis_object.get("user_uu_id") elif redis_object.get("user_type") == UserType.occupant.value:
if redis_object.get("user_type") == 1: return OccupantTokenObject(**redis_object)
if not redis_object.get("selected_company", None):
redis_object["selected_company"] = None
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == 2:
if not redis_object.get("selected_occupant", None):
redis_object["selected_occupant"] = None
return OccupantTokenObject(**redis_object)
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="", error_code="", lang="en", loc=get_line_number_for_error()
lang="en",
) )
@classmethod @classmethod
def get_object_via_user_uu_id(cls, user_id: str) -> Union[EmployeeTokenObject, OccupantTokenObject]: def get_object_via_access_key(cls, access_token: str) -> T:
"""Get access token from user uuid.""" """Get token object using access key."""
access_token = AccessToken( access_token_obj = AccessToken(accessToken=access_token)
accessToken="", redis_response = RedisActions.get_json(list_keys=access_token_obj.to_list())
userUUID=user_id,
) if redis_object := redis_response.first.data:
if redis_object := RedisActions.get_json( access_token_obj.userUUID = redis_object.get("user_uu_id")
list_keys=access_token.to_list() return cls._process_redis_object(redis_object)
).first.data:
access_token.userUUID = redis_object.get("user_uu_id")
if redis_object.get("user_type") == 1:
if not redis_object.get("selected_company", None):
redis_object["selected_company"] = None
return EmployeeTokenObject(**redis_object)
elif redis_object.get("user_type") == 2:
if not redis_object.get("selected_occupant", None):
redis_object["selected_occupant"] = None
return OccupantTokenObject(**redis_object)
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="", error_code="", lang="en", loc=get_line_number_for_error()
lang="en", )
@classmethod
def get_object_via_user_uu_id(cls, user_id: str) -> T:
"""Get token object using user UUID."""
access_token = AccessToken(userUUID=user_id)
redis_response = RedisActions.get_json(list_keys=access_token.to_list())
if redis_object := redis_response.first.data:
access_token.userUUID = redis_object.get("user_uu_id")
return cls._process_redis_object(redis_object)
raise HTTPExceptionApi(
error_code="", lang="en", loc=get_line_number_for_error()
) )

View File

@ -1,5 +1,5 @@
from ApiServices.Token.token_handler import TokenService from ApiServices.Token.token_handler import TokenService
__all__ = [ __all__ = [
'TokenService', "TokenService",
] ]

View File

@ -1,7 +1,7 @@
from ApiValidations.Request import BaseModelRegular from ApiValidations.Request import BaseModelRegular
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict, Field
class ChangePasswordValidation: class ChangePasswordValidation:
@ -10,8 +10,14 @@ class ChangePasswordValidation:
class ChangePassword(BaseModelRegular, ChangePasswordValidation): class ChangePassword(BaseModelRegular, ChangePasswordValidation):
old_password: str old_password: str = Field(..., example="current123")
new_password: str new_password: str = Field(..., example="newpass456")
model_config = ConfigDict(
json_schema_extra={
"example": {"old_password": "current123", "new_password": "newpass456"}
}
)
class CreatePasswordValidation: class CreatePasswordValidation:
@ -28,30 +34,53 @@ class CreatePasswordValidation:
class CreatePassword(BaseModelRegular, CreatePasswordValidation): class CreatePassword(BaseModelRegular, CreatePasswordValidation):
password_token: str password_token: str = Field(..., example="abc123token")
password: str password: str = Field(..., example="newpass123")
re_password: str re_password: str = Field(..., example="newpass123")
model_config = ConfigDict(
json_schema_extra={
"example": {
"password_token": "abc123token",
"password": "newpass123",
"re_password": "newpass123",
}
}
)
class OccupantSelectionValidation: class OccupantSelectionValidation:
tr = {"occupant_uu_id": "Kiracı UU ID", "build_part_uu_id": "Bölüm UU ID"} tr = {"occupant_uu_id": "Kiracı UU ID", "build_part_uu_id": "Bölüm UU ID"}
en = {"occupant_uu_id": "Occupant UU ID", "build_part_uu_id": "Build Part UU ID"} en = {"occupant_uu_id": "Occupant UU ID", "build_part_uu_id": "Build Part UU ID"}
class OccupantSelection(BaseModel, OccupantSelectionValidation): class OccupantSelection(BaseModel, OccupantSelectionValidation):
occupant_uu_id: str occupant_uu_id: str = Field(..., example="123e4567-e89b-12d3-a456-426614174000")
build_part_uu_id: str build_part_uu_id: str = Field(..., example="987fcdeb-51a2-43e7-9876-543210987654")
model_config = ConfigDict(
json_schema_extra={
"example": {
"occupant_uu_id": "123e4567-e89b-12d3-a456-426614174000",
"build_part_uu_id": "987fcdeb-51a2-43e7-9876-543210987654",
}
}
)
class EmployeeSelectionValidation: class EmployeeSelectionValidation:
tr = {"company_uu_id": "Şirket UU ID"} tr = {"company_uu_id": "Şirket UU ID"}
en = {"company_uu_id": "Company UU ID"} en = {"company_uu_id": "Company UU ID"}
class EmployeeSelection(BaseModel, EmployeeSelectionValidation): class EmployeeSelection(BaseModel, EmployeeSelectionValidation):
company_uu_id: str company_uu_id: str = Field(..., example="abcdef12-3456-7890-abcd-ef1234567890")
model_config = ConfigDict(
json_schema_extra={
"example": {"company_uu_id": "abcdef12-3456-7890-abcd-ef1234567890"}
}
)
class LoginValidation: class LoginValidation:
@ -70,10 +99,21 @@ class LoginValidation:
class Login(BaseModelRegular, LoginValidation): class Login(BaseModelRegular, LoginValidation):
domain: str domain: str = Field(..., example="example.com")
access_key: str access_key: str = Field(..., example="user@example.com")
password: str password: str = Field(..., example="password123")
remember_me: Optional[bool] = False remember_me: Optional[bool] = Field(False, example=True)
model_config = ConfigDict(
json_schema_extra={
"example": {
"domain": "example.com",
"access_key": "user@example.com",
"password": "password123",
"remember_me": True,
}
}
)
class LogoutValidation: class LogoutValidation:
@ -82,7 +122,9 @@ class LogoutValidation:
class Logout(BaseModelRegular, LogoutValidation): class Logout(BaseModelRegular, LogoutValidation):
domain: str domain: str = Field(..., example="example.com")
model_config = ConfigDict(json_schema_extra={"example": {"domain": "example.com"}})
class RememberValidation: class RememberValidation:
@ -91,8 +133,17 @@ class RememberValidation:
class Remember(BaseModelRegular, RememberValidation): class Remember(BaseModelRegular, RememberValidation):
domain: str domain: str = Field(..., example="example.com")
refresh_token: str refresh_token: str = Field(..., example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
model_config = ConfigDict(
json_schema_extra={
"example": {
"domain": "example.com",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
}
}
)
class ForgotValidation: class ForgotValidation:
@ -101,5 +152,11 @@ class ForgotValidation:
class Forgot(BaseModelRegular, ForgotValidation): class Forgot(BaseModelRegular, ForgotValidation):
domain: str domain: str = Field(..., example="example.com")
access_key: str access_key: str = Field(..., example="user@example.com")
model_config = ConfigDict(
json_schema_extra={
"example": {"domain": "example.com", "access_key": "user@example.com"}
}
)

View File

@ -62,7 +62,6 @@ class UpdateCompanyEmployeesSalaries(PydanticBaseModel):
people_id: Optional[int] = None people_id: Optional[int] = None
class InsertCompanyEmployees(BaseModelRegular): class InsertCompanyEmployees(BaseModelRegular):
employee_description: Optional[str] = None employee_description: Optional[str] = None
@ -73,7 +72,6 @@ class InsertCompanyEmployees(BaseModelRegular):
stop_date: Optional[str] = None stop_date: Optional[str] = None
class UpdateCompanyEmployees(PydanticBaseModel): class UpdateCompanyEmployees(PydanticBaseModel):
stop_date: Optional[str] = None stop_date: Optional[str] = None
employee_description: Optional[str] = None employee_description: Optional[str] = None

View File

@ -38,9 +38,7 @@ class UpdateBuildDecisionBookProjectPerson(PydanticBaseModel):
project_team_type_uu_id: Optional[str] = None project_team_type_uu_id: Optional[str] = None
class InsertBuildDecisionBookProjects( class InsertBuildDecisionBookProjects(BaseModelRegular):
BaseModelRegular
):
build_decision_book_item_uu_id: str build_decision_book_item_uu_id: str
project_responsible_person_uu_id: str project_responsible_person_uu_id: str
project_name: str project_name: str
@ -60,9 +58,7 @@ class InsertBuildDecisionBookProjects(
resp_company_uu_id: Optional[str] = None resp_company_uu_id: Optional[str] = None
class UpdateBuildDecisionBookProjects( class UpdateBuildDecisionBookProjects(PydanticBaseModel):
PydanticBaseModel
):
build_decision_book_project_uu_id: str build_decision_book_project_uu_id: str
is_out_sourced: Optional[bool] = False is_out_sourced: Optional[bool] = False
project_note: Optional[str] = None project_note: Optional[str] = None
@ -76,10 +72,7 @@ class UpdateBuildDecisionBookProjects(
approved_price: Optional[float] = None approved_price: Optional[float] = None
class ApprovalsBuildDecisionBookProjects(PydanticBaseModel):
class ApprovalsBuildDecisionBookProjects(
PydanticBaseModel
):
build_decision_book_project_uu_id: str build_decision_book_project_uu_id: str
project_stop_date: str project_stop_date: str
status_code: Optional[int] = None status_code: Optional[int] = None
@ -88,9 +81,7 @@ class ApprovalsBuildDecisionBookProjects(
) )
class InsertBuildDecisionBookProjectItemDebits( class InsertBuildDecisionBookProjectItemDebits(PydanticBaseModel):
PydanticBaseModel
):
build_decision_book_project_item_uu_id: str build_decision_book_project_item_uu_id: str
payment_date: str payment_date: str
dues_values: dict dues_values: dict
@ -100,9 +91,7 @@ class InsertBuildDecisionBookProjectItemDebits(
decision_taken: Optional[bool] = None decision_taken: Optional[bool] = None
class UpdateBuildDecisionBookProjectItemDebits( class UpdateBuildDecisionBookProjectItemDebits(PydanticBaseModel):
PydanticBaseModel
):
dues_values: Optional[str] = None dues_values: Optional[str] = None
discount_value: Optional[float] = None discount_value: Optional[float] = None
discount_fix: Optional[float] = None discount_fix: Optional[float] = None

View File

@ -5,6 +5,7 @@ from decimal import Decimal
from uuid import UUID from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
class AccountBooksResponse(BaseModel): class AccountBooksResponse(BaseModel):
"""Response model for account books""" """Response model for account books"""

View File

@ -1,8 +1,12 @@
from pydantic import BaseModel """
Base validation models and utilities.
"""
from pydantic import BaseModel, ConfigDict
def rewrite_input_data(data): def rewrite_input_data(data):
"""Remove empty and None values from input data."""
return { return {
item[0]: item[1] item[0]: item[1]
for item in data.items() for item in data.items()
@ -11,6 +15,11 @@ def rewrite_input_data(data):
class BaseModelRegular(BaseModel): class BaseModelRegular(BaseModel):
"""Base model for all validation models with proper schema handling."""
model_config = ConfigDict(
json_schema_extra={"example": {}} # Will be populated by subclasses
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**rewrite_input_data(kwargs)) super().__init__(**rewrite_input_data(kwargs))
@ -20,3 +29,30 @@ class BaseModelRegular(BaseModel):
def dump(self): def dump(self):
return self.model_dump() return self.model_dump()
@classmethod
def model_json_schema(cls, *args, **kwargs):
"""Generate JSON schema with proper examples."""
schema = super().model_json_schema(*args, **kwargs)
# Add examples based on field types
if "properties" in schema:
example = {}
for field_name, field_schema in schema["properties"].items():
field_type = field_schema.get("type")
if field_type == "string":
example[field_name] = f"example_{field_name}"
elif field_type == "integer":
example[field_name] = 0
elif field_type == "number":
example[field_name] = 0.0
elif field_type == "boolean":
example[field_name] = False
elif field_type == "array":
example[field_name] = []
elif field_type == "object":
example[field_name] = {}
schema["example"] = example
return schema

View File

@ -20,15 +20,44 @@ from fastapi.openapi.utils import get_openapi
def create_app() -> FastAPI: def create_app() -> FastAPI:
"""Create and configure the FastAPI application.""" """Create and configure the FastAPI application."""
app = FastAPI() app = FastAPI(
responses={
422: {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"detail": {
"type": "array",
"items": {
"type": "object",
"properties": {
"loc": {
"type": "array",
"items": {"type": "string"},
},
"msg": {"type": "string"},
"type": {"type": "string"},
},
},
}
},
}
}
},
}
}
)
# Get all routers and protected routes from the new configuration # Get all routers and protected routes from the new configuration
routers, protected_routes = get_all_routers() routers, protected_routes = get_all_routers()
# Include all routers # Include all routers
for router in routers: for router in routers:
app.include_router(router) app.include_router(router)
# Configure OpenAPI schema with security # Configure OpenAPI schema with security
def custom_openapi(): def custom_openapi():
if app.openapi_schema: if app.openapi_schema:
@ -47,7 +76,9 @@ def create_app() -> FastAPI:
# Configure security for protected routes # Configure security for protected routes
for path, methods in protected_routes.items(): for path, methods in protected_routes.items():
for method in methods: for method in methods:
configure_route_security(path, method, openapi_schema, list(protected_routes.keys())) configure_route_security(
path, method, openapi_schema, list(protected_routes.keys())
)
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema

View File

@ -18,6 +18,7 @@ from AllConfigs.main import MainConfig as Config
from create_routes import get_all_routers from create_routes import get_all_routers
def setup_security_schema() -> Dict[str, Any]: def setup_security_schema() -> Dict[str, Any]:
""" """
Configure security schema for the OpenAPI documentation. Configure security schema for the OpenAPI documentation.
@ -32,12 +33,13 @@ def setup_security_schema() -> Dict[str, Any]:
"type": "http", "type": "http",
"scheme": "bearer", "scheme": "bearer",
"bearerFormat": "JWT", "bearerFormat": "JWT",
"description": "Enter the token" "description": "Enter the token",
} }
} }
} }
} }
def configure_route_security( def configure_route_security(
path: str, method: str, schema: Dict[str, Any], protected_paths: List[str] path: str, method: str, schema: Dict[str, Any], protected_paths: List[str]
) -> None: ) -> None:
@ -55,6 +57,7 @@ def configure_route_security(
if method.lower() in schema["paths"][path]: if method.lower() in schema["paths"][path]:
schema["paths"][path][method.lower()]["security"] = [{"Bearer": []}] schema["paths"][path][method.lower()]["security"] = [{"Bearer": []}]
def create_app() -> FastAPI: def create_app() -> FastAPI:
""" """
Create and configure a FastAPI application with dynamic route creation. Create and configure a FastAPI application with dynamic route creation.
@ -68,7 +71,7 @@ def create_app() -> FastAPI:
description=Config.DESCRIPTION, description=Config.DESCRIPTION,
default_response_class=JSONResponse, default_response_class=JSONResponse,
) )
@app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION)) @app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION))
async def home() -> RedirectResponse: async def home() -> RedirectResponse:
"""Redirect root path to API documentation.""" """Redirect root path to API documentation."""
@ -76,11 +79,11 @@ def create_app() -> FastAPI:
# Get all routers and protected routes using the dynamic route creation # Get all routers and protected routes using the dynamic route creation
routers, protected_routes = get_all_routers() routers, protected_routes = get_all_routers()
# Include all routers # Include all routers
for router in routers: for router in routers:
app.include_router(router) app.include_router(router)
# Configure OpenAPI schema with security # Configure OpenAPI schema with security
def custom_openapi(): def custom_openapi():
if app.openapi_schema: if app.openapi_schema:
@ -100,7 +103,9 @@ def create_app() -> FastAPI:
# Configure security for protected routes # Configure security for protected routes
for path, methods in protected_routes.items(): for path, methods in protected_routes.items():
for method in methods: for method in methods:
configure_route_security(path, method, openapi_schema, list(protected_routes.keys())) configure_route_security(
path, method, openapi_schema, list(protected_routes.keys())
)
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema

View File

@ -5,8 +5,8 @@ Handles dynamic route creation based on configurations.
from typing import Optional, Dict, Any, List, Callable, TypeVar, ParamSpec from typing import Optional, Dict, Any, List, Callable, TypeVar, ParamSpec
P = ParamSpec('P') # For function parameters P = ParamSpec("P") # For function parameters
R = TypeVar('R') # For return type R = TypeVar("R") # For return type
from dataclasses import dataclass from dataclasses import dataclass
from functools import wraps from functools import wraps
@ -17,7 +17,6 @@ from pydantic import BaseModel
from AllConfigs.main import MainConfig as Config from AllConfigs.main import MainConfig as Config
@dataclass @dataclass
class EndpointFactoryConfig: class EndpointFactoryConfig:
endpoint: str endpoint: str
@ -39,17 +38,17 @@ class EndpointFactoryConfig:
class EnhancedEndpointFactory: class EnhancedEndpointFactory:
def __init__(self, router_config: dict): def __init__(self, router_config: dict):
self.router = APIRouter( self.router = APIRouter(
prefix=router_config['prefix'], prefix=router_config["prefix"],
tags=router_config['tags'], tags=router_config["tags"],
include_in_schema=router_config.get('include_in_schema', True) include_in_schema=router_config.get("include_in_schema", True),
) )
self.endpoints = router_config['endpoints'] self.endpoints = router_config["endpoints"]
self.protected_routes: Dict[str, List[str]] = {} self.protected_routes: Dict[str, List[str]] = {}
def create_endpoint(self, config: EndpointFactoryConfig): def create_endpoint(self, config: EndpointFactoryConfig):
""" """
Create an endpoint directly from the configuration. Create an endpoint directly from the configuration.
Args: Args:
config: EndpointFactoryConfig instance containing endpoint configuration config: EndpointFactoryConfig instance containing endpoint configuration
""" """
@ -70,7 +69,7 @@ class EnhancedEndpointFactory:
response_model=config.response_model, response_model=config.response_model,
summary=config.summary, summary=config.summary,
description=config.description, description=config.description,
**config.extra_options **config.extra_options,
)(endpoint_function) )(endpoint_function)
def get_router(self) -> APIRouter: def get_router(self) -> APIRouter:
@ -94,13 +93,13 @@ async def ping_test(request: Request, service_name: str = "base-router"):
def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]: def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]:
""" """
Get all configured routers and their protected routes. Get all routers and protected routes from route configurations.
Returns: Returns:
tuple: (routers, protected_routes) tuple: (routers, protected_routes)
""" """
from events.route_configs import get_route_configs from ApiEvents.route_configs import get_route_configs
routers = [] routers = []
all_protected_routes = {} all_protected_routes = {}
@ -109,25 +108,23 @@ def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]:
factory_all = [] factory_all = []
for config in route_configs: for config in route_configs:
factory = EnhancedEndpointFactory(config) factory = EnhancedEndpointFactory(config)
# Create endpoints from configuration # Create endpoints from configuration
for endpoint_dict in config['endpoints']: for endpoint_dict in config["endpoints"]:
endpoint_config = EndpointFactoryConfig( endpoint_config = EndpointFactoryConfig(
endpoint=endpoint_dict['endpoint'], endpoint=endpoint_dict["endpoint"],
method=endpoint_dict['method'], method=endpoint_dict["method"],
summary=endpoint_dict['summary'], summary=endpoint_dict["summary"],
description=endpoint_dict['description'], description=endpoint_dict["description"],
endpoint_function=endpoint_dict['endpoint_function'], endpoint_function=endpoint_dict["endpoint_function"],
is_auth_required=endpoint_dict['is_auth_required'], is_auth_required=endpoint_dict["is_auth_required"],
is_event_required=endpoint_dict['is_event_required'], is_event_required=endpoint_dict["is_event_required"],
extra_options=endpoint_dict.get('extra_options', {}) extra_options=endpoint_dict.get("extra_options", {}),
) )
factory.create_endpoint(endpoint_config) factory.create_endpoint(endpoint_config)
factory_all.append( factory_all.append(endpoint_config.__dict__)
endpoint_config.__dict__
)
# Add router and protected routes # Add router and protected routes
routers.append(factory.get_router()) routers.append(factory.get_router())
all_protected_routes.update(factory.get_protected_routes()) all_protected_routes.update(factory.get_protected_routes())
return routers, all_protected_routes return routers, all_protected_routes

View File

@ -1 +1,5 @@
from .token_event_middleware import TokenEventMiddleware
from .auth_middleware import RequestTimingMiddleware, MiddlewareModule
__all__ = ["TokenEventMiddleware", "RequestTimingMiddleware", "MiddlewareModule"]

View File

@ -6,70 +6,83 @@ and a middleware for request timing measurements.
""" """
from time import perf_counter from time import perf_counter
from typing import Callable, Optional, Dict, Any, Tuple from typing import Callable, Optional, Dict, Any, Tuple, Union
from functools import wraps from functools import wraps
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request, Response
from AllConfigs.Token.config import Auth from AllConfigs.Token.config import Auth
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
from .base_context import BaseContext
from ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
class AuthContext(BaseContext):
"""
Context class for authentication middleware.
Extends BaseContext to provide authentication-specific functionality.
"""
def __init__(
self, token_context: Union[OccupantTokenObject, EmployeeTokenObject]
) -> None:
super().__init__()
self.token_context = token_context
@property
def is_employee(self) -> bool:
"""Check if authenticated token is for an employee."""
return isinstance(self.token_context, EmployeeTokenObject)
@property
def is_occupant(self) -> bool:
"""Check if authenticated token is for an occupant."""
return isinstance(self.token_context, OccupantTokenObject)
@property
def user_id(self) -> str:
"""Get the user's UUID from token context."""
return self.token_context.user_uu_id if self.token_context else ""
def __repr__(self) -> str:
user_type = "Employee" if self.is_employee else "Occupant"
return f"AuthContext({user_type}Token: {self.user_id})"
class MiddlewareModule: class MiddlewareModule:
""" """
Module containing authentication and middleware functionality. Middleware module for handling authentication and request timing.
This class provides:
- Token extraction and validation
- Authentication decorator for endpoints
""" """
@staticmethod @staticmethod
def get_access_token(request: Request) -> Tuple[str, str]: def get_user_from_request(
request: Request,
) -> AuthContext:
""" """
Extract access token from request headers. Get authenticated token context from request.
Args: Args:
request: FastAPI request object request: FastAPI request object
Returns: Returns:
Tuple[str, str]: A tuple containing (scheme, token) AuthContext: Context containing the authenticated token data
Raises: Raises:
HTTPExceptionApi: If token is missing or malformed HTTPExceptionApi: If token is missing, invalid, or user not found
""" """
auth_header = request.headers.get(Auth.ACCESS_TOKEN_TAG) from ApiServices.Token.token_handler import TokenService
if not auth_header:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
try: # Get token and validate - will raise HTTPExceptionApi if invalid
scheme, token = auth_header.split() redis_token = TokenService.get_access_token_from_request(request=request)
if scheme.lower() != "bearer":
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
return scheme, token
except ValueError:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
@staticmethod # Get token context - will validate token and raise appropriate errors
async def validate_token(token: str) -> Dict[str, Any]: token_context = TokenService.get_object_via_access_key(access_token=redis_token)
""" if not token_context:
Validate the authentication token. raise HTTPExceptionApi(
error_code="USER_NOT_FOUND", lang="tr", loc=get_line_number_for_error()
)
Args: return AuthContext(token_context=token_context)
token: JWT token to validate
Returns:
Dict[str, Any]: User data extracted from token
Raises:
HTTPExceptionApi: If token is invalid
"""
try:
# TODO: Implement your token validation logic
# Example:
# return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
return {"user_id": "test", "role": "user"} # Placeholder
except Exception as e:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
@classmethod @classmethod
def auth_required(cls, func: Callable) -> Callable: def auth_required(cls, func: Callable) -> Callable:
@ -80,30 +93,36 @@ class MiddlewareModule:
@router.get("/protected") @router.get("/protected")
@MiddlewareModule.auth_required @MiddlewareModule.auth_required
async def protected_endpoint(request: Request): async def protected_endpoint(request: Request):
user = request.state.user # Access authenticated user data auth = protected_endpoint.auth # Access auth context
return {"message": "Protected content"} if auth.is_employee:
# Handle employee logic
@router.get("/public") # No decorator = public endpoint employee_id = auth.token_context.employee_id
async def public_endpoint(): else:
return {"message": "Public content"} # Handle occupant logic
occupant_id = auth.token_context.occupant_id
return {"user_id": auth.user_id}
Args: Args:
func: The FastAPI route handler function to protect func: The FastAPI route handler function to protect
Returns: Returns:
Callable: Wrapped function that checks authentication before execution Callable: Wrapped function that checks authentication before execution
Raises:
HTTPExceptionApi: If authentication fails
""" """
@wraps(func) @wraps(func)
def wrapper(request: Request, *args, **kwargs): def wrapper(request: Request, *args, **kwargs):
from ApiServices import TokenService # Get and validate token context from request
# Get token from header auth_context = cls.get_user_from_request(request)
# token = TokenService.get_access_token_from_request(request=request)
# print(token) # Attach auth context to function
# if not token: func.auth = auth_context
# raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr")
# Call the original endpoint function # Call the original endpoint function
return func(request, *args, **kwargs) return func(request, *args, **kwargs)
return wrapper return wrapper

View File

@ -0,0 +1,47 @@
"""Base context for middleware."""
from typing import Optional, Dict, Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
class BaseContext:
"""Base context class for middleware."""
def __init__(self) -> None:
self._token_context: Optional[
Union["OccupantTokenObject", "EmployeeTokenObject"]
] = None
self._function_code: Optional[str] = None
@property
def token_context(
self,
) -> Optional[Union["OccupantTokenObject", "EmployeeTokenObject"]]:
"""Get token context if available."""
return self._token_context
@token_context.setter
def token_context(
self, value: Union["OccupantTokenObject", "EmployeeTokenObject"]
) -> None:
"""Set token context."""
self._token_context = value
@property
def function_code(self) -> Optional[str]:
"""Get function code if available."""
return self._function_code
@function_code.setter
def function_code(self, value: str) -> None:
"""Set function code."""
self._function_code = value
def to_dict(self) -> Dict[str, Any]:
"""Convert context to dictionary."""
return {
"token_context": self._token_context,
"function_code": self._function_code,
}

View File

@ -0,0 +1,76 @@
"""
Token event middleware for handling authentication and event tracking.
"""
from functools import wraps
from typing import Callable, Dict, Any
from .auth_middleware import MiddlewareModule
from .base_context import BaseContext
class TokenEventHandler(BaseContext):
"""Handler for token events with authentication context."""
def __init__(self, func: Callable, url_of_endpoint: str):
"""Initialize the handler with function and URL."""
super().__init__()
self.func = func
self.url_of_endpoint = url_of_endpoint
def update_context(self, function_code: str):
"""Update the event context with function code."""
self.function_code = function_code
class TokenEventMiddleware:
"""
Module containing token and event handling functionality.
This class provides:
- Token and event context management
- Event validation decorator for endpoints
"""
@staticmethod
def event_required(
func: Callable[..., Dict[str, Any]]
) -> Callable[..., Dict[str, Any]]:
"""
Decorator for endpoints with token and event requirements.
This decorator:
1. First validates authentication using MiddlewareModule.auth_required
2. Then adds event tracking context
Args:
func: The function to be decorated
Returns:
Callable: The wrapped function with both auth and event handling
"""
# First apply authentication
authenticated_func = MiddlewareModule.auth_required(func)
@wraps(authenticated_func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = TokenEventHandler(
func=authenticated_func,
url_of_endpoint=authenticated_func.url_of_endpoint,
)
# Update event-specific context
handler.update_context(
function_code="7192c2aa-5352-4e36-98b3-dafb7d036a3d" # Keep function_code as URL
)
# Copy auth context from authenticated function
if hasattr(authenticated_func, "auth"):
handler.token_context = authenticated_func.auth.token_context
# Make handler available to the function
authenticated_func.handler = handler
# Call the authenticated function
return authenticated_func(*args, **kwargs)
return wrapper

View File

@ -19,7 +19,7 @@ RUN poetry config virtualenvs.create false \
&& rm -rf ~/.cache/pypoetry && rm -rf ~/.cache/pypoetry
# Copy application code # Copy application code
COPY DockerApiServices/AllApiNeeds /app/ COPY DockerApiServices/AllApiNeeds /app
COPY ErrorHandlers /app/ErrorHandlers COPY ErrorHandlers /app/ErrorHandlers
COPY LanguageModels /app/LanguageModels COPY LanguageModels /app/LanguageModels
COPY ApiLibrary /app/ApiLibrary COPY ApiLibrary /app/ApiLibrary
@ -28,6 +28,15 @@ COPY AllConfigs /app/AllConfigs
COPY ErrorHandlers /app/ErrorHandlers COPY ErrorHandlers /app/ErrorHandlers
COPY Schemas /app/Schemas COPY Schemas /app/Schemas
COPY Services /app/Services COPY Services /app/Services
COPY ApiServices /app/ApiServices
# Copy Events structure with consistent naming
COPY ApiEvents/AuthServiceApi /app/ApiEvents
COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py
COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py
# Create empty __init__.py files to make directories into Python packages
RUN touch /app/ApiEvents/__init__.py
# Set Python path to include app directory # Set Python path to include app directory
ENV PYTHONPATH=/app \ ENV PYTHONPATH=/app \

View File

@ -19,7 +19,7 @@ RUN poetry config virtualenvs.create false \
&& rm -rf ~/.cache/pypoetry && rm -rf ~/.cache/pypoetry
# Copy application code # Copy application code
COPY DockerApiServices/AllApiNeeds /app/ COPY DockerApiServices/AllApiNeeds /app
COPY ErrorHandlers /app/ErrorHandlers COPY ErrorHandlers /app/ErrorHandlers
COPY LanguageModels /app/LanguageModels COPY LanguageModels /app/LanguageModels
COPY ApiLibrary /app/ApiLibrary COPY ApiLibrary /app/ApiLibrary
@ -31,9 +31,12 @@ COPY Services /app/Services
COPY ApiServices /app/ApiServices COPY ApiServices /app/ApiServices
# Copy Events structure with consistent naming # Copy Events structure with consistent naming
COPY ApiEvents/EventServiceApi /app/events COPY ApiEvents/EventServiceApi /app/ApiEvents
COPY ApiEvents/utils.py /app/events/utils.py COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py
COPY ApiEvents/abstract_class.py /app/events/abstract_class.py COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py
# Create empty __init__.py files to make directories into Python packages
RUN touch /app/ApiEvents/__init__.py
# Set Python path to include app directory # Set Python path to include app directory
ENV PYTHONPATH=/app \ ENV PYTHONPATH=/app \

View File

@ -35,11 +35,9 @@ docker compose -f docker-compose-services.yml build --no-cache && docker compose
``` ```
## Service Ports ## Service Ports
- Auth Service: `http://localhost:8000` - Auth Service: http://localhost:41575
- `/test/health` - Protected health check endpoint (requires authentication) - Event Service: http://localhost:41576
- `/test/ping` - Public ping endpoint - Validation Service: http://localhost:41577
- Event Service: `http://localhost:8001`
- Validation Service: `http://localhost:8002`
## Development Notes ## Development Notes
- Use clean build (--no-cache) when: - Use clean build (--no-cache) when:

View File

@ -19,13 +19,21 @@ RUN poetry config virtualenvs.create false \
&& rm -rf ~/.cache/pypoetry && rm -rf ~/.cache/pypoetry
# Copy application code # Copy application code
COPY DockerApiServices/AllApiNeeds /app/ COPY DockerApiServices/AllApiNeeds /app
COPY ErrorHandlers /app/ErrorHandlers
COPY LanguageModels /app/LanguageModels
COPY ApiLibrary /app/ApiLibrary COPY ApiLibrary /app/ApiLibrary
COPY ApiValidations /app/ApiValidations COPY ApiValidations /app/ApiValidations
COPY AllConfigs /app/AllConfigs COPY AllConfigs /app/AllConfigs
COPY ErrorHandlers /app/ErrorHandlers COPY ErrorHandlers /app/ErrorHandlers
COPY Schemas /app/Schemas COPY Schemas /app/Schemas
COPY Services /app/Services COPY Services /app/Services
COPY ApiServices /app/ApiServices
# Copy Events structure with consistent naming
COPY ApiEvents/EventServiceApi /app/ApiEvents
COPY ApiEvents/abstract_class.py /app/ApiEvents/abstract_class.py
COPY ApiEvents/base_request_model.py /app/ApiEvents/base_request_model.py
# Set Python path to include app directory # Set Python path to include app directory
ENV PYTHONPATH=/app \ ENV PYTHONPATH=/app \

View File

@ -18,14 +18,13 @@ class HTTPExceptionApiHandler:
@staticmethod @staticmethod
def retrieve_error_status_code(exc: HTTPExceptionApi) -> int: def retrieve_error_status_code(exc: HTTPExceptionApi) -> int:
error_by_codes = BaseErrorModelClass.retrieve_error_by_codes() error_by_codes = BaseErrorModelClass.retrieve_error_by_codes()
grab_status_code = error_by_codes.get( grab_status_code = error_by_codes.get(str(exc.error_code).upper(), 500)
str(exc.error_code).upper(), 500
)
return int(grab_status_code) return int(grab_status_code)
@staticmethod @staticmethod
def retrieve_error_message(exc: HTTPExceptionApi, error_languages) -> str: def retrieve_error_message(exc: HTTPExceptionApi, error_languages) -> str:
from ErrorHandlers import DEFAULT_ERROR from ErrorHandlers import DEFAULT_ERROR
return error_languages.get(str(exc.error_code).upper(), DEFAULT_ERROR) return error_languages.get(str(exc.error_code).upper(), DEFAULT_ERROR)
async def handle_exception( async def handle_exception(

View File

@ -1,5 +1,5 @@
# SQL Models # SQL Models
from Schemas.account.account import ( from .account.account import (
AccountBooks, AccountBooks,
AccountCodeParser, AccountCodeParser,
AccountRecords, AccountRecords,
@ -8,18 +8,18 @@ from Schemas.account.account import (
AccountMaster, AccountMaster,
AccountRecordExchanges, AccountRecordExchanges,
) )
from Schemas.building.budget import ( from .building.budget import (
DecisionBookBudgetBooks, DecisionBookBudgetBooks,
DecisionBookBudgetCodes, DecisionBookBudgetCodes,
DecisionBookBudgetMaster, DecisionBookBudgetMaster,
DecisionBookBudgets, DecisionBookBudgets,
) )
from Schemas.account.iban import ( from .account.iban import (
BuildIbans, BuildIbans,
BuildIbanDescription, BuildIbanDescription,
) )
from Schemas.api.encrypter import CrypterEngine from .api.encrypter import CrypterEngine
from Schemas.building.build import ( from .building.build import (
Build, Build,
BuildTypes, BuildTypes,
BuildParts, BuildParts,
@ -30,7 +30,7 @@ from Schemas.building.build import (
BuildCompaniesProviding, BuildCompaniesProviding,
RelationshipEmployee2Build, RelationshipEmployee2Build,
) )
from Schemas.building.decision_book import ( from .building.decision_book import (
BuildDecisionBook, BuildDecisionBook,
BuildDecisionBookItems, BuildDecisionBookItems,
BuildDecisionBookPerson, BuildDecisionBookPerson,
@ -43,22 +43,22 @@ from Schemas.building.decision_book import (
BuildDecisionBookPersonOccupants, BuildDecisionBookPersonOccupants,
BuildDecisionBookProjectItems, BuildDecisionBookProjectItems,
) )
from Schemas.company.company import ( from .company.company import (
Companies, Companies,
RelationshipDutyCompany, RelationshipDutyCompany,
) )
from Schemas.company.employee import ( from .company.employee import (
Employees, Employees,
EmployeesSalaries, EmployeesSalaries,
EmployeeHistory, EmployeeHistory,
Staff, Staff,
) )
from Schemas.company.department import ( from .company.department import (
Duty, Duty,
Duties, Duties,
Departments, Departments,
) )
from Schemas.event.event import ( from .event.event import (
Modules, Modules,
Services, Services,
Service2Events, Service2Events,
@ -68,7 +68,7 @@ from Schemas.event.event import (
Event2OccupantExtra, Event2OccupantExtra,
Event2EmployeeExtra, Event2EmployeeExtra,
) )
from Schemas.identity.identity import ( from .identity.identity import (
Addresses, Addresses,
AddressCity, AddressCity,
AddressStreet, AddressStreet,
@ -87,10 +87,10 @@ from Schemas.identity.identity import (
RelationshipEmployee2PostCode, RelationshipEmployee2PostCode,
Contracts, Contracts,
) )
from Schemas.others.enums import ( from .others.enums import (
ApiEnumDropdown, ApiEnumDropdown,
) )
from Schemas.rules.rules import ( from .rules.rules import (
EndpointRestriction, EndpointRestriction,
) )

View File

@ -138,6 +138,10 @@ class Users(CrudCollection, UserLoginModule, SelectAction):
def is_occupant(self): def is_occupant(self):
return not str(self.email).split("@")[1] == Auth.ACCESS_EMAIL_EXT return not str(self.email).split("@")[1] == Auth.ACCESS_EMAIL_EXT
@property
def is_employee(self):
return str(self.email).split("@")[1] == Auth.ACCESS_EMAIL_EXT
@property @property
def password_expiry_ends(self): def password_expiry_ends(self):
"""Calculates the expiry end date based on expiry begins and expires day""" """Calculates the expiry end date based on expiry begins and expires day"""
@ -165,8 +169,10 @@ class Users(CrudCollection, UserLoginModule, SelectAction):
@classmethod @classmethod
def create_action(cls, create_user: InsertUsers, token_dict): def create_action(cls, create_user: InsertUsers, token_dict):
db_session = cls.new_session()
found_person = People.filter_one( found_person = People.filter_one(
People.uu_id == create_user.people_uu_id, People.uu_id == create_user.people_uu_id,
db=db_session,
).data ).data
if not found_person: if not found_person:
@ -192,7 +198,10 @@ class Users(CrudCollection, UserLoginModule, SelectAction):
@classmethod @classmethod
def credentials(cls): def credentials(cls):
person_object = People.filter_by_one(system=True, id=cls.person_id).data db_session = cls.new_session()
person_object = People.filter_by_one(
db=db_session, system=True, id=cls.person_id
).data
if person_object: if person_object:
return { return {
"person_id": person_object.id, "person_id": person_object.id,
@ -206,17 +215,20 @@ class Users(CrudCollection, UserLoginModule, SelectAction):
def get_employee_and_duty_details(self): def get_employee_and_duty_details(self):
from Schemas import Employees, Duties from Schemas import Employees, Duties
db_session = self.new_session()
found_person = People.filter_one( found_person = People.filter_one(
People.id == self.person_id, People.id == self.person_id,
db=db_session,
) )
found_employees = Employees.filter_by_active( found_employees = Employees.filter_by_active(
people_id=found_person.id, is_confirmed=True people_id=found_person.id, is_confirmed=True, db=db_session
) )
found_duties = Duties.filter_all( found_duties = Duties.filter_all(
Duties.is_confirmed == True, Duties.is_confirmed == True,
Duties.id.in_( Duties.id.in_(
list(found_employee.duty_id for found_employee in found_employees.data) list(found_employee.duty_id for found_employee in found_employees.data)
), ),
db=db_session,
) )
if not found_employees.count: if not found_employees.count:
raise HTTPException( raise HTTPException(

View File

@ -10,6 +10,7 @@ from fastapi import Request, status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pymongo.errors import PyMongoError, DuplicateKeyError, ConnectionFailure from pymongo.errors import PyMongoError, DuplicateKeyError, ConnectionFailure
from ApiLibrary.common.line_number import get_line_number_for_error
from Services.MongoDb.Models.exceptions import ( from Services.MongoDb.Models.exceptions import (
MongoBaseException, MongoBaseException,
MongoConnectionError, MongoConnectionError,
@ -54,6 +55,7 @@ def handle_mongo_errors(func: Callable) -> Callable:
raise HTTPExceptionApi( raise HTTPExceptionApi(
lang="en", lang="en",
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR, error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
loc=get_line_number_for_error(),
) )
return wrapper return wrapper

View File

@ -20,6 +20,7 @@ from pymongo.errors import (
PyMongoError, PyMongoError,
) )
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
@ -34,14 +35,28 @@ def handle_mongo_errors(func):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except ConnectionFailure: except ConnectionFailure:
raise HTTPExceptionApi(error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en") raise HTTPExceptionApi(
error_code="HTTP_503_SERVICE_UNAVAILABLE",
lang="en",
loc=get_line_number_for_error(),
)
except ServerSelectionTimeoutError: except ServerSelectionTimeoutError:
raise HTTPExceptionApi(error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en") raise HTTPExceptionApi(
error_code="HTTP_504_GATEWAY_TIMEOUT",
lang="en",
loc=get_line_number_for_error(),
)
except OperationFailure as e: except OperationFailure as e:
raise HTTPExceptionApi(error_code="HTTP_400_BAD_REQUEST", lang="en") raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang="en",
loc=get_line_number_for_error(),
)
except PyMongoError as e: except PyMongoError as e:
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en" error_code="HTTP_500_INTERNAL_SERVER_ERROR",
lang="en",
loc=get_line_number_for_error(),
) )
return wrapper return wrapper

View File

@ -18,6 +18,7 @@ from dataclasses import dataclass
from fastapi import status from fastapi import status
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from ApiLibrary.common.line_number import get_line_number_for_error
from Services.PostgresDb.Models.response import PostgresResponse from Services.PostgresDb.Models.response import PostgresResponse
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
from Services.pagination import Pagination, PaginationConfig from Services.pagination import Pagination, PaginationConfig
@ -159,6 +160,7 @@ class BaseJsonResponse(Generic[T]):
raise HTTPExceptionApi( raise HTTPExceptionApi(
lang=cls_object.lang, lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST", error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
) )
@ -198,6 +200,7 @@ class SinglePostgresResponse(BaseJsonResponse[T]):
raise HTTPExceptionApi( raise HTTPExceptionApi(
lang=cls_object.lang, lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST", error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
) )
instance = super().__new__(cls) instance = super().__new__(cls)
@ -253,6 +256,7 @@ class AlchemyJsonResponse(BaseJsonResponse[T]):
raise HTTPExceptionApi( raise HTTPExceptionApi(
lang=cls_object.lang, lang=cls_object.lang,
error_code="HTTP_400_BAD_REQUEST", error_code="HTTP_400_BAD_REQUEST",
loc=get_line_number_for_error(),
) )
instance = super().__new__(cls) instance = super().__new__(cls)

View File

@ -16,6 +16,7 @@ from sqlalchemy.orm import Query, Session
from sqlalchemy.sql.elements import BinaryExpression from sqlalchemy.sql.elements import BinaryExpression
from ApiLibrary import system_arrow from ApiLibrary import system_arrow
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi from ErrorHandlers.Exceptions.api_exc import HTTPExceptionApi
from Services.PostgresDb.Models.response import PostgresResponse from Services.PostgresDb.Models.response import PostgresResponse
@ -137,6 +138,7 @@ class FilterAttributes:
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED", error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr", lang=cls.lang or "tr",
loc=get_line_number_for_error(),
) )
@classmethod @classmethod
@ -170,6 +172,7 @@ class FilterAttributes:
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED", error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr", lang=cls.lang or "tr",
loc=get_line_number_for_error(),
) )
@classmethod @classmethod
@ -189,6 +192,7 @@ class FilterAttributes:
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED", error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr", lang=cls.lang or "tr",
loc=get_line_number_for_error(),
) )
@classmethod @classmethod
@ -220,6 +224,7 @@ class FilterAttributes:
raise HTTPExceptionApi( raise HTTPExceptionApi(
error_code="HTTP_304_NOT_MODIFIED", error_code="HTTP_304_NOT_MODIFIED",
lang=cls.lang or "tr", lang=cls.lang or "tr",
loc=get_line_number_for_error(),
) )
@classmethod @classmethod
@ -522,5 +527,5 @@ class FilterAttributes:
# """ # """
# raise HTTPExceptionApi( # raise HTTPExceptionApi(
# error_code="HTTP_304_NOT_MODIFIED", # error_code="HTTP_304_NOT_MODIFIED",
# lang=cls.lang or "tr", # lang=cls.lang or "tr", loc=get_line_number_for_error()
# ) # )

View File

@ -1,3 +1,4 @@
import json
import arrow import arrow
from typing import Optional, List, Dict, Union from typing import Optional, List, Dict, Union
@ -20,6 +21,43 @@ class RedisActions:
for unit, multiplier in time_multipliers.items() for unit, multiplier in time_multipliers.items()
) )
@classmethod
def delete_key(cls, key: Union[Optional[str], Optional[bytes]]):
try:
redis_cli.delete(key)
return RedisResponse(
status=True,
message="Value is deleted successfully.",
)
except Exception as e:
return RedisResponse(
status=False,
message="Value is not deleted successfully.",
error=str(e),
)
@classmethod
def delete(
cls, list_keys: List[Union[Optional[str], Optional[bytes]]]
) -> RedisResponse:
try:
regex = RedisRow.regex(list_keys=list_keys)
json_get = redis_cli.scan_iter(match=regex)
for row in list(json_get):
redis_cli.delete(row)
return RedisResponse(
status=True,
message="Values are deleted successfully.",
)
except Exception as e:
return RedisResponse(
status=False,
message="Values are not deleted successfully.",
error=str(e),
)
@classmethod @classmethod
def set_json( def set_json(
cls, cls,

View File

@ -131,7 +131,7 @@ class RedisRow:
return key_str.split(cls.delimiter) return key_str.split(cls.delimiter)
@classmethod @classmethod
def feed(cls, value: Union[bytes, Dict, List]) -> None: def feed(cls, value: Union[bytes, Dict, List, str]) -> None:
""" """
Convert and store value in JSON format. Convert and store value in JSON format.
@ -151,6 +151,8 @@ class RedisRow:
cls.value = json.dumps(value) cls.value = json.dumps(value)
elif isinstance(value, bytes): elif isinstance(value, bytes):
cls.value = json.dumps(json.loads(value.decode())) cls.value = json.dumps(json.loads(value.decode()))
elif isinstance(value, str):
cls.value = value
else: else:
raise RedisValueError(f"Unsupported value type: {type(value)}") raise RedisValueError(f"Unsupported value type: {type(value)}")
except json.JSONDecodeError as e: except json.JSONDecodeError as e:

View File

@ -1,12 +1,22 @@
from pydantic import BaseModel from typing import Optional
from uuid import UUID
from pydantic import BaseModel, validator
class AccessToken(BaseModel): class AccessToken(BaseModel):
accessToken: str accessToken: Optional[str] = None
userUUID: str userUUID: Optional[str] = None
@validator("userUUID", pre=True)
def validate_uuid(cls, v):
"""Convert UUID to string during validation."""
if isinstance(v, UUID):
return str(v)
return v
def to_list(self): def to_list(self):
"""Convert to list for Redis storage."""
return [self.accessToken, self.userUUID] return [self.accessToken, self.userUUID]
@property @property