diff --git a/api_services/api_builds/auth-service/Dockerfile b/api_services/api_builds/auth-service/Dockerfile index c89f53b..df7abca 100644 --- a/api_services/api_builds/auth-service/Dockerfile +++ b/api_services/api_builds/auth-service/Dockerfile @@ -14,12 +14,14 @@ RUN poetry config virtualenvs.create false && poetry install --no-interaction -- # Copy application code COPY /api_services/api_initializer /api_initializer COPY /api_services/api_controllers /api_controllers +COPY /api_services/api_validations /api_validations COPY /api_services/schemas /schemas -COPY /api_services/api_middlewares /middlewares +COPY /api_services/api_middlewares /api_middlewares COPY /api_services/api_builds/auth-service/endpoints /api_initializer/endpoints COPY /api_services/api_builds/auth-service/events /api_initializer/events COPY /api_services/api_builds/auth-service/validations /api_initializer/validations +COPY /api_services/api_modules /api_modules # Set Python path to include app directory ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 diff --git a/api_services/api_builds/auth-service/endpoints/auth/router.py b/api_services/api_builds/auth-service/endpoints/auth/router.py new file mode 100644 index 0000000..a550443 --- /dev/null +++ b/api_services/api_builds/auth-service/endpoints/auth/router.py @@ -0,0 +1,155 @@ +from typing import Union + +from fastapi import APIRouter, Request, status, Header, Depends +from fastapi.responses import JSONResponse + +from config import api_config +from validations.request.auth.validations import ( + RequestLogin, + RequestResetPassword, + RequestSelectLiving, + RequestSelectEmployee, + RequestCreatePassword, + RequestChangePassword, + RequestForgotPasswordPhone, + RequestForgotPasswordEmail, + RequestVerifyOTP, + RequestApplication, +) +from events.auth.events import AuthHandlers +from endpoints.index import endpoints_index + +from api_validations.defaults.validations import CommonHeaders +from api_middlewares.token_provider import TokenProvider + + +auth_route = APIRouter(prefix="/authentication", tags=["Authentication Cluster"]) + + +auth_route_login = "AuthLoginViaDomainAndCreds" +@auth_route.post( + path="/login", + summary="Login via domain and access key : [email] | [phone]", + description="Login Route", + operation_id=endpoints_index[auth_route_login] +) +def login(data: RequestLogin, headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Login via domain and access key : [email] | [phone]""" + return AuthHandlers.LoginHandler.authentication_login_with_domain_and_creds(headers=headers, data=data) + + +auth_route_select_living = "AuthSelectLiving" +@auth_route.post( + path="/select", + summary="Select token object company or occupant type", + description="Selection of users company or occupant type", + operation_id=endpoints_index[auth_route_select_living] +) +def select_living(data: Union[RequestSelectLiving, RequestSelectEmployee], headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Select token object company or occupant type""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return AuthHandlers.LoginHandler.authentication_select_company_or_occupant_type(request=headers.request, data=data) + + +auth_route_create_password = "AuthCreatePassword" +@auth_route.post( + path="/password/create", + summary="Create password with access token", + description="Create password", + operation_id=endpoints_index[auth_route_create_password] +) +def create_password(data: RequestCreatePassword, headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Create password with access token""" + # token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return AuthHandlers.PasswordHandler.create_password(password=data.password, password_token=data.password_token) + + +auth_route_change_password = "AuthChangePassword" +@auth_route.post( + path="/password/change", + summary="Change password with access token", + description="Change password", + operation_id=endpoints_index[auth_route_change_password] +) +def change_password(data: RequestChangePassword, headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Change password with access token""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_reset_password = "AuthResetPassword" +@auth_route.post( + path="/password/reset", + summary="Reset password with access token", + description="Reset password", + operation_id=endpoints_index[auth_route_reset_password] +) +def reset_password(data: RequestResetPassword, headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Reset password with access token""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_logout = "AuthLogout" +@auth_route.get( + path="/logout", + summary="Logout user", + description="Logout only single session of user which domain is provided", + operation_id=endpoints_index[auth_route_logout] +) +def logout(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Logout user""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_disconnect = "AuthDisconnect" +@auth_route.get( + path="/disconnect", + summary="Disconnect all sessions", + description="Disconnect all sessions of user in access token", + operation_id=endpoints_index[auth_route_disconnect] +) +def disconnect(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Disconnect all sessions""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_check_token = "AuthCheckToken" +@auth_route.get( + path="/token/check", + summary="Check if token is valid", + description="Check if access token is valid for user", + operation_id=endpoints_index[auth_route_check_token] +) +def check_token(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Check if token is valid""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_refresh_token = "AuthRefreshToken" +@auth_route.get( + path="/token/refresh", + summary="Refresh if token is valid", + description="Refresh if access token is valid for user", + operation_id=endpoints_index[auth_route_refresh_token] +) +def refresh_token(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Refresh if token is valid""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None + + +auth_route_verify_otp = "AuthVerifyOTP" +@auth_route.get( + path="/password/verify-otp", + summary="Verify OTP for password reset", + description="Verify OTP for password reset", + operation_id=endpoints_index[auth_route_verify_otp] +) +def verify_otp(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): + """Verify OTP for password reset""" + token_object = TokenProvider.get_dict_from_redis(token=headers.token) + return None diff --git a/api_services/api_builds/auth-service/endpoints/index.py b/api_services/api_builds/auth-service/endpoints/index.py index 3775b02..fcf0468 100644 --- a/api_services/api_builds/auth-service/endpoints/index.py +++ b/api_services/api_builds/auth-service/endpoints/index.py @@ -1,9 +1,13 @@ endpoints_index: dict = { - "Name": "d538deb4-38f4-4913-a1af-bbef14cf6873", - "Slot1": "c0f5ccb1-1e56-4653-af13-ec0bf5e6aa51", - "Slot2": "034a7eb7-0186-4f48-bb8c-165c429ad5c1", - "Slot3": "ec1f3ec3-3f28-4eaf-b89a-c463632c0b90", - "Slot4": "2cf99f10-72f0-4c2b-98be-3082d67b950d", - "Slot5": "15c24c6c-651b-4c5d-9c2b-5c6c6c6c6c6c", + "AuthLoginViaDomainAndCreds": "1b94a704-7768-436d-bc20-655d92b34d83", + "AuthSelectLiving": "585d578e-2b72-4f71-b996-530fc0613568", + "AuthCreatePassword": "a4252148-2bac-42df-aa3a-1784f4cbd599", + "AuthChangePassword": "d55834fa-6d7f-4007-9591-a50d3266b3aa", + "AuthResetPassword": "29f14043-2a79-4230-bf66-a709ae954dc5", + "AuthLogout": "616a992a-2a73-4709-a394-f043caa75937", + "AuthDisconnect": "55dd1df1-4a00-41f9-92a9-fb776aee1cd3", + "AuthCheckToken": "040e7a48-1ce0-432c-9bd9-5b05c2c7aef3", + "AuthRefreshToken": "0ca54d41-d9ca-4143-b974-1050d65769b7", + "AuthVerifyOTP": "4192e7a5-cf52-4d09-8b51-2088d77271d0", } diff --git a/api_services/api_builds/auth-service/endpoints/routes.py b/api_services/api_builds/auth-service/endpoints/routes.py index b82ac19..d82dbf4 100644 --- a/api_services/api_builds/auth-service/endpoints/routes.py +++ b/api_services/api_builds/auth-service/endpoints/routes.py @@ -1,15 +1,22 @@ from fastapi import APIRouter +from .auth.router import auth_route def get_routes() -> list[APIRouter]: - return [] + """Get all routes""" + return [auth_route] def get_safe_endpoint_urls() -> list[tuple[str, str]]: + """Get all safe endpoint urls""" return [ ("/", "GET"), ("/docs", "GET"), ("/redoc", "GET"), ("/openapi.json", "GET"), ("/metrics", "GET"), + ("/authentication/login", "POST"), + ("/authentication/password/reset", "POST"), + ("/authentication/password/create", "POST"), + ("/authentication/password/verify-otp", "POST"), ] diff --git a/api_services/api_builds/auth-service/events/auth/events.py b/api_services/api_builds/auth-service/events/auth/events.py new file mode 100644 index 0000000..f30900b --- /dev/null +++ b/api_services/api_builds/auth-service/events/auth/events.py @@ -0,0 +1,599 @@ +import arrow + +from typing import Any, Dict, Optional, Union +from config import api_config +from schemas import ( + Users, + People, + BuildLivingSpace, + BuildParts, + OccupantTypes, + Employees, + Addresses, + Companies, + Staff, + Duty, + Duties, + Departments, + Event2Employee, + Application2Occupant, + Event2Occupant, + Application2Employee, + RelationshipEmployee2Build, +) +from api_modules.token.password_module import PasswordModule +from api_controllers.redis.database import RedisActions +from api_controllers.mongo.database import mongo_handler +from api_validations.token.validations import EmployeeTokenObject, OccupantTokenObject, CompanyToken, OccupantToken, UserType +from api_validations.defaults.validations import CommonHeaders +from validations.password.validations import PasswordHistoryViaUser + + +TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject] + + +class RedisHandlers: + + AUTH_TOKEN: str = "AUTH_TOKEN" + + @classmethod + def process_redis_object(cls, redis_object: Dict[str, Any]) -> TokenDictType: + """Process Redis object and return appropriate token object.""" + if not redis_object.get("selected_company"): + redis_object["selected_company"] = None + if not redis_object.get("selected_occupant"): + redis_object["selected_occupant"] = None + if redis_object.get("user_type") == UserType.employee.value: + return EmployeeTokenObject(**redis_object) + elif redis_object.get("user_type") == UserType.occupant.value: + return OccupantTokenObject(**redis_object) + raise ValueError("Invalid user type") + + @classmethod + def get_object_from_redis(cls, access_token: str) -> TokenDictType: + redis_response = RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, access_token, "*"]) + if not redis_response.status: + raise ValueError("EYS_0001") + if redis_object := redis_response.first: + return cls.process_redis_object(redis_object) + raise ValueError("EYS_0002") + + @classmethod + def set_object_to_redis(cls, user: Users, token, header_info): + result_delete = RedisActions.delete(list_keys=[RedisHandlers.AUTH_TOKEN, "*", str(user.uu_id)]) + generated_access_token = PasswordModule.generate_access_token() + keys = [RedisHandlers.AUTH_TOKEN, generated_access_token, str(user.uu_id)] + RedisActions.set_json(list_keys=keys, value={**token, **header_info}, expires={"hours": 1, "minutes": 30}) + return generated_access_token + + @classmethod + def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]): + if already_token_data := RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, token, "*"]).first: + already_token = cls.process_redis_object(already_token_data) + if already_token.is_employee and isinstance(add_payload, CompanyToken): + already_token.selected_company = add_payload + elif already_token.is_occupant and isinstance(add_payload, OccupantToken): + already_token.selected_occupant = add_payload + result = RedisActions.set_json( + list_keys=[RedisHandlers.AUTH_TOKEN, token, str(already_token.user_uu_id)], value=already_token.model_dump(), expires={"hours": 1, "minutes": 30} + ) + return result.first + raise ValueError("Something went wrong") + + +class UserHandlers: + + @staticmethod + def check_user_exists(access_key: str, db_session) -> Users: + """Check if the user exists in the database.""" + Users.set_session(db_session) + if "@" in access_key: + found_user: Users = Users.query.filter(Users.email == access_key.lower()).first() + else: + found_user: Users = Users.query.filter(Users.phone_number == access_key.replace(" ", "")).first() + if not found_user: + raise ValueError("EYS_0003") + return found_user + + @staticmethod + def check_password_valid(domain: str, id_: str, password: str, password_hashed: str) -> bool: + """Check if the password is valid.""" + if PasswordModule.check_password(domain=domain, id_=id_, password=password, password_hashed=password_hashed): + return True + raise ValueError("EYS_0004") + + @staticmethod + def update_password(): + return + + +class LoginHandler: + + @staticmethod + def is_occupant(email: str): + return not str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT + + @staticmethod + def is_employee(email: str): + return str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT + + @classmethod + def do_employee_login(cls, headers: CommonHeaders, data: Any, db_session): + """Handle employee login.""" + user_handler, other_domains_list, main_domain = UserHandlers(), [], "" + found_user = user_handler.check_user_exists(access_key=data.access_key, db_session=db_session) + with mongo_handler.collection(f"{str(found_user.related_company)}*Domain") as collection: + result = collection.find_one({"user_uu_id": str(found_user.uu_id)}) + if not result: + raise ValueError("EYS_00087") + other_domains_list = result.get("other_domains_list", []) + main_domain = result.get("main_domain", None) + if headers.domain not in other_domains_list or not main_domain: + raise ValueError("EYS_00088") + + if not user_handler.check_password_valid(domain=main_domain, id_=str(found_user.uu_id), password=data.password, password_hashed=found_user.hash_password): + raise ValueError("EYS_0005") + list_of_returns = ( + Employees.id, Employees.uu_id, People.id, People.uu_id, Users.id, Users.uu_id, Companies.id, Companies.uu_id, + Departments.id, Departments.uu_id, Duty.id, Duty.uu_id, Companies.public_name, Companies.company_type, Duty.duty_name, + Addresses.letter_address + ) + + list_employee_query = db_session.query(*list_of_returns + ).join(Staff, Staff.id == Employees.staff_id + ).join(People, People.id == Employees.people_id + ).join(Duties, Duties.id == Staff.duties_id + ).join(Duty, Duty.id == Duties.duties_id + ).join(Departments, Departments.id == Duties.department_id + ).join(Companies, Companies.id == Departments.company_id + ).join(Users, Users.person_id == People.id + ).outerjoin(Addresses, Addresses.id == Companies.official_address_id + ).filter(Employees.people_id == found_user.person_id) + list_employees, list_employees_query_all = [], list_employee_query.all() + if not list_employees_query_all: + ValueError("No Employee found for this user") + + for employee in list_employees_query_all: + single_employee = {} + for ix, returns in enumerate(list_of_returns): + single_employee[str(returns)] = employee[ix] + list_employees.append(single_employee) + companies_uu_id_list, companies_id_list, companies_list, duty_uu_id_list, duty_id_list = [], [], [], [], [] + for list_employee in list_employees: + companies_id_list.append(int(list_employee["Companies.id"])) + companies_uu_id_list.append(str(list_employee["Companies.uu_id"])) + duty_uu_id_list.append(str(list_employee["Duty.uu_id"])) + duty_id_list.append(int(list_employee["Duty.id"])) + companies_list.append({ + "uu_id": str(list_employee["Companies.uu_id"]), "public_name": list_employee["Companies.public_name"], + "company_type": list_employee["Companies.company_type"], "company_address": list_employee["Addresses.letter_address"], + "duty": list_employee["Duty.duty_name"] + }) + model_value = EmployeeTokenObject( + user_type=UserType.employee.value, + user_uu_id=str(found_user.uu_id), + user_id=found_user.id, + person_id=found_user.person_id, + person_uu_id=str(list_employees[0]["People.uu_id"]), + request=dict(headers.request.headers), + domain_list=other_domains_list, + 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, + ).model_dump() + set_to_redis_dict = dict( + user=found_user, + token=model_value, + header_info=dict(language=headers.language, domain=headers.domain, timezone=headers.timezone), + ) + redis_handler = RedisHandlers() + user_dict = found_user.get_dict() + person_dict = found_user.person.get_dict() + if access_token := redis_handler.set_object_to_redis(**set_to_redis_dict): + return { + "access_token": access_token, + "user_type": UserType.employee.name, + "user": { + "uuid": user_dict["uu_id"], + "avatar": user_dict["avatar"], + "email": user_dict["email"], + "phone_number": user_dict["phone_number"], + "user_tag": user_dict["user_tag"], + "password_expiry_begins": str(arrow.get(user_dict["password_expiry_begins"]).shift(days=int(user_dict["password_expires_day"]))), + "person": { + "uuid": person_dict["uu_id"], + "firstname": person_dict["firstname"], + "surname": person_dict["surname"], + "middle_name": person_dict["middle_name"], + "sex_code": person_dict["sex_code"], + "person_tag": person_dict["person_tag"], + "country_code": person_dict["country_code"], + "birth_date": person_dict["birth_date"], + }, + }, + "selection_list": companies_list, + } + raise ValueError("Something went wrong") + + @classmethod + def do_occupant_login(cls, request: Any, data: Any, db_session, extra_dict: Optional[Dict[str, Any]] = None): + """ + Handle occupant login. + """ + language = extra_dict.get("language", "tr") + domain = extra_dict.get("domain", None) + timezone = extra_dict.get("tz", None) or "GMT+3" + + user_handler = UserHandlers() + found_user = user_handler.check_user_exists( + access_key=data.access_key, db_session=db_session + ) + other_domains_list, main_domain = [], "" + with mongo_handler.collection( + f"{str(found_user.related_company)}*Domain" + ) as collection: + result = collection.find_one({"user_uu_id": str(found_user.uu_id)}) + if not result: + raise ValueError("EYS_00087") + other_domains_list = result.get("other_domains_list", []) + main_domain = result.get("main_domain", None) + if domain not in other_domains_list or not main_domain: + raise ValueError("EYS_00088") + + if not user_handler.check_password_valid( + domain=main_domain, + id_=str(found_user.uu_id), + password=data.password, + password_hashed=found_user.hash_password, + ): + raise ValueError("EYS_0005") + + occupants_selection_dict: Dict[str, Any] = {} + living_spaces: list[BuildLivingSpace] = BuildLivingSpace.filter_all( + BuildLivingSpace.person_id == found_user.person_id, db=db_session + ).data + + if not living_spaces: + raise ValueError("EYS_0006") + for living_space in living_spaces: + build_part = BuildParts.filter_one( + BuildParts.id == living_space.build_parts_id, + db=db_session, + ).data + if not build_part: + raise ValueError("EYS_0007") + + build = build_part.buildings + occupant_type = OccupantTypes.filter_by_one( + id=living_space.occupant_type_id, + db=db_session, + system=True, + ).data + occupant_data = { + "build_living_space_uu_id": str(living_space.uu_id), + "part_uu_id": str(build_part.uu_id), + "part_name": build_part.part_name(db=db_session), + "part_level": build_part.part_level, + "occupant_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) + + person = found_user.person + model_value = OccupantTokenObject( + user_type=UserType.occupant.value, + user_uu_id=str(found_user.uu_id), + user_id=found_user.id, + person_id=person.id, + person_uu_id=str(person.uu_id), + domain_list=other_domains_list, + request=dict(request.headers), + available_occupants=occupants_selection_dict, + ).model_dump() + redis_handler = RedisHandlers() + if access_token := redis_handler.set_object_to_redis( + user=found_user, + token=model_value, + header_info=dict(language=language, domain=domain, timezone=timezone), + ): + return { + "access_token": access_token, + "user_type": UserType.occupant.name, + "selection_list": occupants_selection_dict, + } + raise ValueError("Something went wrong") + + @classmethod + def authentication_login_with_domain_and_creds(cls, headers: CommonHeaders, data: Any): + """ + Authenticate user with domain and credentials. + + Args: + headers: CommonHeaders object + data: Request body containing login credentials + { + "access_key": "karatay.berkay.sup@evyos.com.tr", + "password": "string", + "remember_me": false + } + Returns: + SuccessResponse containing authentication token and user info + """ + + with Users.new_session() as db_session: + if cls.is_employee(data.access_key): + return cls.do_employee_login(headers=headers, data=data, db_session=db_session) + elif cls.is_occupant(data.access_key): + return cls.do_occupant_login(headers=headers, data=data, db_session=db_session) + else: + raise ValueError("Invalid email format") + + @classmethod + def raise_error_if_request_has_no_token(cls, request: Any) -> None: + """Validate request has required token headers.""" + if not hasattr(request, "headers"): + raise ValueError("Request has no headers") + if not request.headers.get(api_config.ACCESS_TOKEN_TAG): + raise ValueError("Request has no access token") + + @classmethod + def get_access_token_from_request(cls, request: Any) -> str: + """Extract access token from request headers.""" + cls.raise_error_if_request_has_no_token(request=request) + return request.headers.get(api_config.ACCESS_TOKEN_TAG) + + @classmethod + def handle_employee_selection(cls, access_token: str, data: Any, token_dict: TokenDictType): + with Users.new_session() as db_session: + if data.company_uu_id not in token_dict.companies_uu_id_list: + ValueError("EYS_0011") + list_of_returns = ( + Employees.id, Employees.uu_id, People.id, People.uu_id, Users.id, Users.uu_id, Companies.id, Companies.uu_id, + Departments.id, Departments.uu_id, Duty.id, Duty.uu_id, Addresses.id, Addresses.letter_address, Staff.id, Staff.uu_id, + Duties.id, Duties.uu_id, + ) + + selected_company_query = db_session.query(*list_of_returns + ).join(Staff, Staff.id == Employees.staff_id + ).join(People, People.id == Employees.people_id + ).join(Duties, Duties.id == Staff.duties_id + ).join(Duty, Duty.id == Duties.duties_id + ).join(Departments, Departments.id == Duties.department_id + ).join(Companies, Companies.id == Departments.company_id + ).join(Users, Users.person_id == People.id + ).outerjoin(Addresses, Addresses.id == Companies.official_address_id + ).filter(Companies.uu_id == data.company_uu_id, Users.id == token_dict.user_id) + + selected_company_first = selected_company_query.first() + if not selected_company_first: + ValueError("Selected company not found") + + result_with_keys_dict = {} + for ix, selected_company_item in enumerate(selected_company_first): + result_with_keys_dict[str(list_of_returns[ix])] = selected_company_item + + if not selected_company_first: + ValueError("EYS_0010") + + # Get reachable events + reachable_event_codes = Event2Employee.get_event_codes(employee_id=int(result_with_keys_dict['Employees.id']), db=db_session) + # Get reachable applications + reachable_app_codes = Application2Employee.get_application_codes(employee_id=int(result_with_keys_dict['Employees.id']), db=db_session) + + company_token = CompanyToken( + company_uu_id=str(result_with_keys_dict['Companies.uu_id']), + company_id=int(result_with_keys_dict['Companies.id']), + department_id=int(result_with_keys_dict['Departments.id']), + department_uu_id=str(result_with_keys_dict['Departments.uu_id']), + duty_id=int(result_with_keys_dict['Duty.id']), + duty_uu_id=str(result_with_keys_dict['Duty.uu_id']), + bulk_duties_id=int(result_with_keys_dict['Duties.id']), + staff_id=int(result_with_keys_dict['Staff.id']), + staff_uu_id=str(result_with_keys_dict['Staff.uu_id']), + employee_id=int(result_with_keys_dict['Employees.id']), + employee_uu_id=str(result_with_keys_dict['Employees.uu_id']), + reachable_event_codes=reachable_event_codes, + reachable_app_codes=reachable_app_codes, + ) + redis_handler = RedisHandlers() + redis_result = redis_handler.update_token_at_redis(token=access_token, add_payload=company_token) + return {"selected_uu_id": data.company_uu_id} + + @classmethod + def handle_occupant_selection(cls, access_token: str, data: Any, token_dict: TokenDictType): + """Handle occupant type selection""" + with BuildLivingSpace.new_session() as db: + # Get selected occupant type + selected_build_living_space: BuildLivingSpace = BuildLivingSpace.filter_one(BuildLivingSpace.uu_id == data.build_living_space_uu_id, db=db).data + if not selected_build_living_space: + raise ValueError("EYS_0012") + + # Get reachable events + reachable_event_codes = Event2Occupant.get_event_codes(build_living_space_id=selected_build_living_space.id, db=db) + occupant_type = OccupantTypes.filter_one_system(OccupantTypes.id == selected_build_living_space.occupant_type_id, db=db).data + build_part = BuildParts.filter_one(BuildParts.id == selected_build_living_space.build_parts_id, db=db) + build = build_part.buildings + reachable_app_codes = Application2Occupant.get_application_codes(build_living_space_id=selected_build_living_space.id, db=db) + # responsible_employee = Employees.filter_one( + # Employees.id == build_part.responsible_employee_id, + # db=db, + # ).data + # related_company = RelationshipEmployee2Build.filter_one( + # RelationshipEmployee2Build.member_id == build.id, + # db=db, + # ).data + # Get company + # company_related = Companies.filter_one( + # Companies.id == related_company.company_id, + # db=db, + # ).data + + # Create occupant token + occupant_token = OccupantToken( + living_space_id=selected_build_living_space.id, + living_space_uu_id=selected_build_living_space.uu_id.__str__(), + occupant_type_id=occupant_type.id, + occupant_type_uu_id=occupant_type.uu_id.__str__(), + occupant_type=occupant_type.occupant_type, + build_id=build.id, + build_uuid=build.uu_id.__str__(), + build_part_id=build_part.id, + build_part_uuid=build_part.uu_id.__str__(), + # responsible_employee_id=responsible_employee.id, + # responsible_employee_uuid=responsible_employee.uu_id.__str__(), + # responsible_company_id=company_related.id, + # responsible_company_uuid=company_related.uu_id.__str__(), + reachable_event_codes=reachable_event_codes, + reachable_app_codes=reachable_app_codes, + ) + redis_handler = RedisHandlers() + redis_handler.update_token_at_redis(token=access_token, add_payload=occupant_token) + return {"selected_uu_id": occupant_token.living_space_uu_id} + + @classmethod # Requires auth context + def authentication_select_company_or_occupant_type(cls, request: Any, data: Any): + """ + Handle selection of company or occupant type + {"data": {"build_living_space_uu_id": ""}} | {"data": {"company_uu_id": ""}} + { + "data": {"company_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"} + } + { + "data": {"build_living_space_uu_id": "e9869a25-ba4d-49dc-bb0d-8286343b184b"} + } + """ + access_token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None) + if not access_token: + raise ValueError("EYS_0001") + + token_object = RedisHandlers.get_object_from_redis(access_token=access_token) + if token_object.is_employee: + return cls.handle_employee_selection(access_token=access_token, data=data, token_dict=token_object) + elif token_object.is_occupant: + return cls.handle_occupant_selection(access_token=access_token, data=data, token_dict=token_object) + + @classmethod + def authentication_check_token_valid(cls, domain, access_token: str) -> bool: + redis_handler = RedisHandlers() + if auth_token := redis_handler.get_object_from_redis(access_token=access_token): + if auth_token.is_employee: + if domain not in auth_token.domain_list: + raise ValueError("EYS_00112") + return True + elif auth_token.is_occupant: + if domain not in auth_token.domain_list: + raise ValueError("EYS_00113") + return True + return False + + +class PasswordHandler: + + @staticmethod + def create_password(password, password_token=None): + with Users.new_session() as db_session: + Users.set_session(db_session) + found_user = Users.query.filter(Users.password_token == password_token).first() + if not found_user: + raise ValueError("EYS_0031") + + if found_user.password_token: + replace_day = 0 + try: + replace_day = int(str(found_user.password_expires_day or 0).split(",")[0].replace(" days", "")) + except Exception as e: + err = e + token_is_expired = arrow.now() >= arrow.get(str(found_user.password_expiry_begins)).shift(days=replace_day) + if not str(password_token) == str(found_user.password_token) or token_is_expired: + raise ValueError("EYS_0032") + + collection_name = f"{found_user.related_company}*Domain" + with mongo_handler.collection(collection_name) as mongo_engine: + domain_via_user = mongo_engine.find_one({"user_uu_id": str(found_user.uu_id)}) + if not domain_via_user: + raise ValueError("EYS_0024") + domain_via_user = domain_via_user.get("main_domain", None) + new_password_dict = { + "password": PasswordModule.create_hashed_password(domain=domain_via_user, id_=str(found_user.uu_id), password=password), + "date": str(arrow.now().date()), + } + history_dict = PasswordHistoryViaUser(user_uu_id=str(found_user.uu_id), password_add=new_password_dict, access_history_detail={"request": "", "ip": ""}) + found_user.password_expiry_begins = str(arrow.now()) + found_user.hash_password = new_password_dict.get("password") + found_user.password_token = "" if found_user.password_token else "" + + collection_name = f"{found_user.related_company}*PasswordHistory" + with mongo_handler.collection(collection_name) as mongo_engine_sc: + password_history_item = mongo_engine_sc.find_one({"user_uu_id": str(found_user.uu_id)}) + if not password_history_item: + mongo_engine_sc.insert_one(document={"user_uu_id": str(found_user.uu_id), "password_history": []}) + password_history_item = mongo_engine_sc.find_one({"user_uu_id": str(found_user.uu_id)}) + password_history_list = password_history_item.get("password_history", []) + hashed_password = history_dict.password_add.get("password") + for password_in_history in password_history_list: + print('password_history_list', password_history_list, password_in_history) + if str(password_in_history.get("password")) == str(hashed_password): + raise ValueError("EYS_0032") + if len(password_history_list) > 3: + password_history_list.pop(0) + password_history_list.append(history_dict.password_add) + return mongo_engine_sc.update_one( + filter={"user_uu_id": str(found_user.uu_id)}, + update={"$set": { + "password_history": password_history_list, "modified_at": arrow.now().timestamp(), "access_history_detail": history_dict.access_history_detail + }}, upsert=True, + ) + found_user.save(db=db_session) + return found_user + + +class PageHandlers: + + @classmethod + def retrieve_valid_page_via_token(cls, access_token: str, page_url: str) -> str: + """ + Retrieve valid page via token. {access_token: "string", page_url: "string"} | Results: str(application) + """ + if result := RedisHandlers.get_object_from_redis(access_token=access_token): + if result.is_employee: + if result.selected_company and result.selected_company.reachable_app_codes: + if application := result.selected_company.reachable_app_codes.get(page_url, None): + return application + elif result.is_occupant: + if result.selected_occupant and result.selected_occupant.reachable_app_codes: + if application := result.selected_occupant.reachable_app_codes.get(page_url, None): + return application + raise ValueError("EYS_0013") + + @classmethod + def retrieve_valid_sites_via_token(cls, access_token: str) -> list: + """ + Retrieve valid pages via token. {"access_token": "string"} | Results: list(sites) + """ + if result := RedisHandlers.get_object_from_redis(access_token=access_token): + if result.is_employee: + if result.selected_company and result.selected_company.reachable_app_codes: + return result.selected_company.reachable_app_codes.keys() + elif result.is_occupant: + if result.selected_occupant and result.selected_occupant.reachable_app_codes: + return result.selected_occupant.reachable_app_codes.keys() + raise ValueError("EYS_0013") + + +class AuthHandlers: + + LoginHandler: LoginHandler = LoginHandler() + PasswordHandler: PasswordHandler = PasswordHandler() + PageHandlers: PageHandlers = PageHandlers() diff --git a/api_services/api_builds/auth-service/validations/a.txt b/api_services/api_builds/auth-service/validations/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/api_services/api_builds/auth-service/validations/password/validations.py b/api_services/api_builds/auth-service/validations/password/validations.py new file mode 100644 index 0000000..fbbdab2 --- /dev/null +++ b/api_services/api_builds/auth-service/validations/password/validations.py @@ -0,0 +1,19 @@ +from typing import Optional +from pydantic import BaseModel + + +class DomainViaUser(BaseModel): + user_uu_id: str + main_domain: str + other_domains_list: Optional[list] = None + + +class PasswordHistoryViaUser(BaseModel): + user_uu_id: str + password_add: dict + access_history_detail: Optional[dict] + + +class AccessHistoryViaUser(BaseModel): + user_uu_id: str + access_history: dict diff --git a/api_services/api_builds/auth-service/validations/request/auth/validations.py b/api_services/api_builds/auth-service/validations/request/auth/validations.py new file mode 100644 index 0000000..6227179 --- /dev/null +++ b/api_services/api_builds/auth-service/validations/request/auth/validations.py @@ -0,0 +1,78 @@ +from typing import Optional +from pydantic import BaseModel + + + +class RequestLogin(BaseModel): + access_key: str + password: str + remember_me: Optional[bool] + + +class RequestVerifyOTP(BaseModel): + token: str + otp: str + + +class RequestApplication(BaseModel): + page_url: str # /building/create + + +class RequestSelectEmployee(BaseModel): + + company_uu_id: str + + @property + def is_employee(self): + return True + + @property + def is_occupant(self): + return False + + +class RequestResetPassword(BaseModel): + password_token: str + password: str + re_password: str + + +class RequestSelectLiving(BaseModel): + + build_living_space_uu_id: str + + @property + def is_employee(self): + return False + + @property + def is_occupant(self): + return True + + +class RequestCreatePassword(BaseModel): + password_token: str + password: str + re_password: str + + @property + def is_valid(self): + return self.password == self.re_password + + +class RequestChangePassword(BaseModel): + old_password: str + password: str + re_password: str + + @property + def is_valid(self): + return self.password == self.re_password + + +class RequestForgotPasswordEmail(BaseModel): + email: str + + +class RequestForgotPasswordPhone(BaseModel): + phone_number: str diff --git a/api_services/api_builds/initial-service/Dockerfile b/api_services/api_builds/initial-service/Dockerfile index 1628d3b..9a930d3 100644 --- a/api_services/api_builds/initial-service/Dockerfile +++ b/api_services/api_builds/initial-service/Dockerfile @@ -14,6 +14,7 @@ RUN poetry config virtualenvs.create false && poetry install --no-interaction -- # Copy application code COPY /api_services/api_controllers /api_controllers COPY /api_services/schemas /schemas +COPY /api_services/api_modules /api_modules COPY /api_services/api_builds/initial-service /initial-service COPY /api_services/api_builds/initial-service / diff --git a/api_services/api_builds/initial-service/init_app_defaults.py b/api_services/api_builds/initial-service/init_app_defaults.py index 3e3765d..e102943 100644 --- a/api_services/api_builds/initial-service/init_app_defaults.py +++ b/api_services/api_builds/initial-service/init_app_defaults.py @@ -1,6 +1,6 @@ import arrow -from modules.Token.password_module import PasswordModule +from api_modules.token.password_module import PasswordModule from api_controllers.mongo.database import mongo_handler from schemas import ( Companies, diff --git a/api_services/api_builds/initial-service/init_occ_defaults.py b/api_services/api_builds/initial-service/init_occ_defaults.py index ca3387c..7a75cce 100644 --- a/api_services/api_builds/initial-service/init_occ_defaults.py +++ b/api_services/api_builds/initial-service/init_occ_defaults.py @@ -1,5 +1,5 @@ import arrow -from modules.Token.password_module import PasswordModule +from api_modules.token.password_module import PasswordModule from api_controllers.mongo.database import mongo_handler from schemas import ( Addresses, diff --git a/api_services/api_builds/auth-service/endpoints/__init__.py b/api_services/api_builds/management-service/endpoints/__init__.py similarity index 100% rename from api_services/api_builds/auth-service/endpoints/__init__.py rename to api_services/api_builds/management-service/endpoints/__init__.py diff --git a/api_services/api_builds/management-service/endpoints/index.py b/api_services/api_builds/management-service/endpoints/index.py new file mode 100644 index 0000000..5a5b01f --- /dev/null +++ b/api_services/api_builds/management-service/endpoints/index.py @@ -0,0 +1,19 @@ +endpoints_index: dict = { + "AccountRecordsAll": "d538deb4-38f4-4913-a1af-bbef14cf6873", + "AccountRecordsMonthly": "c0f5ccb1-1e56-4653-af13-ec0bf5e6aa51", + "EventsListAvailable": "034a7eb7-0186-4f48-bb8c-165c429ad5c1", + "EventsListAppended": "ec1f3ec3-3f28-4eaf-b89a-c463632c0b90", + "EventServiceRegister": "2cf99f10-72f0-4c2b-98be-3082d67b950d", + "EventServiceUnRegister": "15c24c6c-651b-4c5d-9c2b-5c6c6c6c6c6c", + "EventBindExtraEmployee": "74cafa62-674e-41da-959d-1238ad4a443c", + "EventBindExtraOccupant": "480bee12-8dfd-4242-b481-f6807eb9adf7", + "ApplicationListAll": "a61169be-a009-47ec-8658-3dd388af5c3e", + "ApplicationListAvailable": "bf8d7986-2db7-4ff8-80c2-1935977730a6", + "ApplicationListAppended": "ff7bde16-2631-4465-a4c5-349b357dd334", + "ApplicationRegisterService": "c77a9f36-c007-4079-83fa-1c995b585a6f", + "ApplicationUnRegisterService": "48460f25-fb1e-477f-b641-d5eeacce5e7a", + "ApplicationCreate": "a3ec9f67-12a2-4e8a-b977-1acfa0069c12", + "ApplicationUpdate": "83281757-696a-41ed-9706-e145ac54c3a9", + "ApplicationBindEmployee": "80427237-5ab6-4d17-8084-cdb87bda22a3", + "ApplicationBindOccupant": "ae0fb101-cb13-47ab-86bd-233a5dbef269", +} diff --git a/api_services/api_builds/management-service/endpoints/routes.py b/api_services/api_builds/management-service/endpoints/routes.py new file mode 100644 index 0000000..b82ac19 --- /dev/null +++ b/api_services/api_builds/management-service/endpoints/routes.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter + + +def get_routes() -> list[APIRouter]: + return [] + + +def get_safe_endpoint_urls() -> list[tuple[str, str]]: + return [ + ("/", "GET"), + ("/docs", "GET"), + ("/redoc", "GET"), + ("/openapi.json", "GET"), + ("/metrics", "GET"), + ] diff --git a/api_services/api_builds/management-service/events/__init__.py b/api_services/api_builds/management-service/events/__init__.py new file mode 100644 index 0000000..9015d41 --- /dev/null +++ b/api_services/api_builds/management-service/events/__init__.py @@ -0,0 +1,3 @@ + + +__all__ = [] \ No newline at end of file diff --git a/api_services/api_builds/management-service/events/index.py b/api_services/api_builds/management-service/events/index.py new file mode 100644 index 0000000..14b0856 --- /dev/null +++ b/api_services/api_builds/management-service/events/index.py @@ -0,0 +1,10 @@ + + +events_index: dict = { + "Slot1": "", + "Slot2": "", + "Slot3": "", + "Slot4": "", + "Slot5": "", +} + diff --git a/api_services/api_controllers/postgres/mixin.py b/api_services/api_controllers/postgres/mixin.py index 9cce20a..cd608ea 100644 --- a/api_services/api_controllers/postgres/mixin.py +++ b/api_services/api_controllers/postgres/mixin.py @@ -1,12 +1,18 @@ import arrow +import datetime -from sqlalchemy import Column, Integer, String, Float, ForeignKey, UUID, TIMESTAMP, Boolean, SmallInteger, Numeric, func, text +from decimal import Decimal +from typing import Any, Optional + +from sqlalchemy import Column, Integer, String, Float, ForeignKey, UUID, TIMESTAMP, Boolean, SmallInteger, Numeric, func, text, NUMERIC from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy_mixins.serialize import SerializeMixin from sqlalchemy_mixins.repr import ReprMixin from sqlalchemy_mixins.smartquery import SmartQueryMixin from sqlalchemy_mixins.activerecord import ActiveRecordMixin +from sqlalchemy.orm import InstrumentedAttribute, Mapped + from api_controllers.postgres.engine import get_db, Base @@ -26,6 +32,100 @@ class BasicMixin( """Get database session.""" return get_db() + @classmethod + def iterate_over_variables(cls, val: Any, key: str) -> tuple[bool, Optional[Any]]: + """ + Process a field value based on its type and convert it to the appropriate format. + + Args: + val: Field value + key: Field name + + Returns: + Tuple of (should_include, processed_value) + """ + try: + key_ = cls.__annotations__.get(key, None) + is_primary = key in getattr(cls, "primary_keys", []) + row_attr = bool(getattr(getattr(cls, key), "foreign_keys", None)) + + # Skip primary keys and foreign keys + if is_primary or row_attr: + return False, None + + if val is None: # Handle None values + return True, None + + if str(key[-5:]).lower() == "uu_id": # Special handling for UUID fields + return True, str(val) + + if key_: # Handle typed fields + if key_ == Mapped[int]: + return True, int(val) + elif key_ == Mapped[bool]: + return True, bool(val) + elif key_ == Mapped[float] or key_ == Mapped[NUMERIC]: + return True, round(float(val), 3) + elif key_ == Mapped[TIMESTAMP]: + return True, str(arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss")) + elif key_ == Mapped[str]: + return True, str(val) + else: # Handle based on Python types + if isinstance(val, datetime.datetime): + return True, str(arrow.get(str(val)).format("YYYY-MM-DD HH:mm:ss")) + elif isinstance(val, bool): + return True, bool(val) + elif isinstance(val, (float, Decimal)): + return True, round(float(val), 3) + elif isinstance(val, int): + return True, int(val) + elif isinstance(val, str): + return True, str(val) + elif val is None: + return True, None + return False, None + + except Exception as e: + err = e + return False, None + + def get_dict(self, exclude_list: Optional[list[InstrumentedAttribute]] = None) -> dict[str, Any]: + """ + Convert model instance to dictionary with customizable fields. + + Args: + exclude_list: List of fields to exclude from the dictionary + + Returns: + Dictionary representation of the model + """ + try: + return_dict: Dict[str, Any] = {} + exclude_list = exclude_list or [] + exclude_list = [exclude_arg.key for exclude_arg in exclude_list] + + # Get all column names from the model + columns = [col.name for col in self.__table__.columns] + columns_set = set(columns) + + # Filter columns + columns_list = set([col for col in columns_set if str(col)[-2:] != "id"]) + columns_extend = set(col for col in columns_set if str(col)[-5:].lower() == "uu_id") + columns_list = set(columns_list) | set(columns_extend) + columns_list = list(set(columns_list) - set(exclude_list)) + + for key in columns_list: + val = getattr(self, key) + correct, value_of_database = self.iterate_over_variables(val, key) + if correct: + return_dict[key] = value_of_database + + return return_dict + + except Exception as e: + err = e + return {} + class CrudMixin(BasicMixin): """ diff --git a/api_services/api_controllers/redis/base.py b/api_services/api_controllers/redis/base.py index aa2a0b7..c792e76 100644 --- a/api_services/api_controllers/redis/base.py +++ b/api_services/api_controllers/redis/base.py @@ -10,9 +10,9 @@ This module provides a class for managing Redis key-value operations with suppor import arrow import json -from typing import Union, Dict, List, Optional, Any, TypeVar -from Controllers.Redis.connection import redis_cli +from typing import Union, Dict, List, Optional, Any, TypeVar +from .connection import redis_cli T = TypeVar("T", Dict[str, Any], List[Any]) diff --git a/api_services/api_controllers/redis/connection.py b/api_services/api_controllers/redis/connection.py index 7cb48f8..5c1422c 100644 --- a/api_services/api_controllers/redis/connection.py +++ b/api_services/api_controllers/redis/connection.py @@ -2,7 +2,7 @@ import time from typing import Dict, Any from redis import Redis, ConnectionError, TimeoutError, ConnectionPool -from Controllers.Redis.config import redis_configs +from .config import redis_configs class RedisConn: diff --git a/api_services/api_controllers/redis/database.py b/api_services/api_controllers/redis/database.py index d4eafeb..062fe4c 100644 --- a/api_services/api_controllers/redis/database.py +++ b/api_services/api_controllers/redis/database.py @@ -2,9 +2,9 @@ import arrow from typing import Optional, List, Dict, Union, Iterator -from Controllers.Redis.response import RedisResponse -from Controllers.Redis.connection import redis_cli -from Controllers.Redis.base import RedisRow +from .response import RedisResponse +from .connection import redis_cli +from .base import RedisRow class MainConfig: @@ -87,9 +87,7 @@ class RedisActions: return bool(redis_cli.exists(key)) @classmethod - def refresh_ttl( - cls, key: Union[str, bytes], expires: Dict[str, int] - ) -> RedisResponse: + def refresh_ttl(cls, key: Union[str, bytes], expires: Dict[str, int]) -> RedisResponse: """ Refresh TTL for an existing key. @@ -160,9 +158,7 @@ class RedisActions: ) @classmethod - def delete( - cls, list_keys: List[Union[Optional[str], Optional[bytes]]] - ) -> RedisResponse: + def delete(cls, list_keys: List[Union[Optional[str], Optional[bytes]]]) -> RedisResponse: """ Delete multiple keys matching a pattern. @@ -199,12 +195,7 @@ class RedisActions: ) @classmethod - def set_json( - cls, - list_keys: List[Union[str, bytes]], - value: Optional[Union[Dict, List]], - expires: Optional[Dict[str, int]] = None, - ) -> RedisResponse: + def set_json(cls, list_keys: List[Union[str, bytes]], value: Optional[Union[Dict, List]], expires: Optional[Dict[str, int]] = None) -> RedisResponse: """ Set JSON value in Redis with optional expiry. @@ -252,11 +243,7 @@ class RedisActions: ) @classmethod - def get_json( - cls, - list_keys: List[Union[Optional[str], Optional[bytes]]], - limit: Optional[int] = None, - ) -> RedisResponse: + def get_json(cls, list_keys: List[Union[Optional[str], Optional[bytes]]], limit: Optional[int] = None) -> RedisResponse: """ Get JSON values from Redis using pattern matching. @@ -313,9 +300,7 @@ class RedisActions: ) @classmethod - def get_json_iterator( - cls, list_keys: List[Union[Optional[str], Optional[bytes]]] - ) -> Iterator[RedisRow]: + def get_json_iterator(cls, list_keys: List[Union[Optional[str], Optional[bytes]]]) -> Iterator[RedisRow]: """ Get JSON values from Redis as an iterator for memory-efficient processing of large datasets. diff --git a/api_services/api_controllers/redis/implementations.py b/api_services/api_controllers/redis/implementations.py index 05c30b7..c7b5500 100644 --- a/api_services/api_controllers/redis/implementations.py +++ b/api_services/api_controllers/redis/implementations.py @@ -1,10 +1,11 @@ -from Controllers.Redis.database import RedisActions import threading import time import random import uuid import concurrent.futures +from .database import RedisActions + def example_set_json() -> None: """Example of setting JSON data in Redis with and without expiry.""" diff --git a/api_services/api_controllers/redis/response.py b/api_services/api_controllers/redis/response.py index ea4aa7c..8af3f1a 100644 --- a/api_services/api_controllers/redis/response.py +++ b/api_services/api_controllers/redis/response.py @@ -1,5 +1,5 @@ from typing import Union, Dict, Optional, Any -from Controllers.Redis.base import RedisRow +from .base import RedisRow class RedisResponse: @@ -10,13 +10,7 @@ class RedisResponse: with tools to convert between different data representations. """ - def __init__( - self, - status: bool, - message: str, - data: Any = None, - error: Optional[str] = None, - ): + def __init__(self, status: bool, message: str, data: Any = None, error: Optional[str] = None): """ Initialize a Redis response. diff --git a/api_services/api_initializer/app.py b/api_services/api_initializer/app.py index 5935a1c..5555a91 100644 --- a/api_services/api_initializer/app.py +++ b/api_services/api_initializer/app.py @@ -5,12 +5,10 @@ from api_initializer.create_app import create_app # from prometheus_fastapi_instrumentator import Instrumentator - app = create_app() # Create FastAPI application # Instrumentator().instrument(app=app).expose(app=app) # Setup Prometheus metrics if __name__ == "__main__": - # Run the application with Uvicorn Server - uvicorn_config = uvicorn.Config(**api_config.app_as_dict) + uvicorn_config = uvicorn.Config(**api_config.app_as_dict, workers=1) # Run the application with Uvicorn Server uvicorn.Server(uvicorn_config).run() diff --git a/api_services/api_initializer/create_app.py b/api_services/api_initializer/create_app.py index bba3eb6..87ba23d 100644 --- a/api_services/api_initializer/create_app.py +++ b/api_services/api_initializer/create_app.py @@ -3,34 +3,35 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse from event_clusters import RouterCluster, EventCluster from config import api_config +from open_api_creator import create_openapi_schema +from create_route import RouteRegisterController + +from api_middlewares.token_middleware import token_middleware +from endpoints.routes import get_routes +import events + cluster_is_set = False -def create_events_if_any_cluster_set(): - import events - - global cluster_is_set - if not events.__all__ or cluster_is_set: - return - - router_cluster_stack: list[RouterCluster] = [getattr(events, e, None) for e in events.__all__] - for router_cluster in router_cluster_stack: - event_cluster_stack: list[EventCluster] = list(router_cluster.event_clusters.values()) - for event_cluster in event_cluster_stack: - try: - event_cluster.set_events_to_database() - except Exception as e: - print(f"Error creating event cluster: {e}") - - cluster_is_set = True - - def create_app(): - from open_api_creator import create_openapi_schema - from middlewares.token_middleware import token_middleware - from create_route import RouteRegisterController - from endpoints.routes import get_routes + + def create_events_if_any_cluster_set(): + + global cluster_is_set + if not events.__all__ or cluster_is_set: + return + + router_cluster_stack: list[RouterCluster] = [getattr(events, e, None) for e in events.__all__] + for router_cluster in router_cluster_stack: + event_cluster_stack: list[EventCluster] = list(router_cluster.event_clusters.values()) + for event_cluster in event_cluster_stack: + try: + event_cluster.set_events_to_database() + except Exception as e: + print(f"Error creating event cluster: {e}") + + cluster_is_set = True application = FastAPI(**api_config.api_info) application.add_middleware( diff --git a/api_services/api_initializer/create_route.py b/api_services/api_initializer/create_route.py index e4e4237..438a77e 100644 --- a/api_services/api_initializer/create_route.py +++ b/api_services/api_initializer/create_route.py @@ -12,44 +12,27 @@ class RouteRegisterController: def add_router_with_event_to_database(router: APIRouter): from schemas import EndpointRestriction - # Endpoint operation_id is static now if record exits update() record else create() with EndpointRestriction.new_session() as db_session: + EndpointRestriction.set_session(db_session) for route in router.routes: route_path = str(getattr(route, "path")) route_summary = str(getattr(route, "name")) operation_id = getattr(route, "operation_id", None) if not operation_id: raise ValueError(f"Route {route_path} operation_id is not found") + if not getattr(route, "methods") and isinstance(getattr(route, "methods")): + raise ValueError(f"Route {route_path} methods is not found") - for route_method in [ - method.lower() for method in getattr(route, "methods") - ]: - methods = [method.lower() for method in getattr(route, "methods")] - print('methods count : ', len(methods)) - print(dict( - route_method=route_method, - operation_uu_id=operation_id, - route_path=route_path, - route_summary=route_summary, - )) - # add_or_update_dict = dict( - # endpoint_method=route_method, - # endpoint_name=route_path, - # endpoint_desc=route_summary.replace("_", " "), - # endpoint_function=route_summary, - # operation_uu_id=operation_id, - # is_confirmed=True, - # ) - # endpoint_restriction_found = EndpointRestriction.filter_one_system( - # EndpointRestriction.operation_uu_id == operation_id, db=db_session, - # ).data - # if endpoint_restriction_found: - # endpoint_restriction_found.update(**add_or_update_dict, db=db_session) - # endpoint_restriction_found.save(db=db_session) - # else: - # restriction = EndpointRestriction.find_or_create(**add_or_update_dict, db=db_session) - # if restriction.meta_data.created: - # restriction.save(db=db_session) + route_method = [method.lower() for method in getattr(route, "methods")][0] + add_or_update_dict = dict( + endpoint_method=route_method, endpoint_name=route_path, endpoint_desc=route_summary.replace("_", " "), endpoint_function=route_summary, is_confirmed=True + ) + if to_save_endpoint := EndpointRestriction.query.filter(EndpointRestriction.operation_uu_id == operation_id).first(): + to_save_endpoint.update(**add_or_update_dict) + to_save_endpoint.save() + else: + created_endpoint = EndpointRestriction.create(**add_or_update_dict, operation_uu_id=operation_id) + created_endpoint.save() def register_routes(self): for router in self.router_list: diff --git a/api_services/api_initializer/event_clusters.py b/api_services/api_initializer/event_clusters.py index c1903e0..f40c464 100644 --- a/api_services/api_initializer/event_clusters.py +++ b/api_services/api_initializer/event_clusters.py @@ -39,18 +39,23 @@ class EventCluster: # EndpointRestriction.operation_uu_id == self.endpoint_uu_id, # db=db_session, # ).data: - for event in self.events: - event_dict_to_save = dict( - function_code=event.key, - function_class=event.name, - description=event.description, - endpoint_code=self.endpoint_uu_id, - endpoint_id=to_save_endpoint.id, - endpoint_uu_id=str(to_save_endpoint.uu_id), - is_confirmed=True, - db=db_session, - ) - print('event_dict_to_save', event_dict_to_save) + Events.set_session(db_session) + EndpointRestriction.set_session(db_session) + + if to_save_endpoint := EndpointRestriction.query.filter(EndpointRestriction.operation_uu_id == self.endpoint_uu_id).first(): + print('to_save_endpoint', to_save_endpoint) + for event in self.events: + event_dict_to_save = dict( + function_code=event.key, + function_class=event.name, + description=event.description, + endpoint_code=self.endpoint_uu_id, + endpoint_id=to_save_endpoint.id, + endpoint_uu_id=str(to_save_endpoint.uu_id), + is_confirmed=True, + ) + print('set_events_to_database event_dict_to_save', event_dict_to_save) + # event_found = Events.filter_one( # Events.function_code == event_dict_to_save["function_code"], # db=db_session, diff --git a/api_services/api_middlewares/token_provider.py b/api_services/api_middlewares/token_provider.py new file mode 100644 index 0000000..4293164 --- /dev/null +++ b/api_services/api_middlewares/token_provider.py @@ -0,0 +1,78 @@ +import enum + +from typing import Optional, Union, Dict, Any, List +from pydantic import BaseModel + +from api_controllers.redis.database import RedisActions +from api_validations.token.validations import ( + TokenDictType, + OccupantTokenObject, + EmployeeTokenObject, + UserType, +) + +class TokenProvider: + + AUTH_TOKEN: str = "AUTH_TOKEN" + + @classmethod + def convert_redis_object_to_token(cls, redis_object: Dict[str, Any]) -> TokenDictType: + """ + Process Redis object and return appropriate token object. + """ + if redis_object.get("user_type") == UserType.employee.value: + return EmployeeTokenObject(**redis_object) + elif redis_object.get("user_type") == UserType.occupant.value: + return OccupantTokenObject(**redis_object) + raise ValueError("Invalid user type") + + @classmethod + def get_dict_from_redis(cls, token: Optional[str] = None, user_uu_id: Optional[str] = None) -> Union[TokenDictType, List[TokenDictType]]: + """ + Retrieve token object from Redis using token and user_uu_id + """ + token_to_use, user_uu_id_to_use = token or "*", user_uu_id or "*" + list_of_token_dict, auth_key_list = [], [ + cls.AUTH_TOKEN, + token_to_use, + user_uu_id_to_use, + ] + if token: + result = RedisActions.get_json(list_keys=auth_key_list, limit=1) + if first_record := result.first: + return cls.convert_redis_object_to_token(first_record) + elif user_uu_id: + result = RedisActions.get_json(list_keys=auth_key_list) + if all_records := result.all: + for all_record in all_records: + list_of_token_dict.append(cls.convert_redis_object_to_token(all_record)) + return list_of_token_dict + raise ValueError( + "Token not found in Redis. Please check the token or user_uu_id." + ) + + @classmethod + def retrieve_application_codes(cls, page_url: str, token: TokenDictType): + """ + Retrieve application code from the token object or list of token objects. + """ + if isinstance(token, EmployeeTokenObject): + if application_codes := token.selected_company.reachable_app_codes.get(page_url, None): + return application_codes + elif isinstance(token, OccupantTokenObject): + if application_codes := token.selected_occupant.reachable_app_codes.get(page_url, None): + return application_codes + raise ValueError("Invalid token type or no application code found.") + + @classmethod + def retrieve_event_codes(cls, endpoint_code: str, token: TokenDictType) -> str: + """ + Retrieve event code from the token object or list of token objects. + """ + if isinstance(token, EmployeeTokenObject): + if event_codes := token.selected_company.reachable_event_codes.get(endpoint_code, None): + return event_codes + elif isinstance(token, OccupantTokenObject): + if event_codes := token.selected_occupant.reachable_event_codes.get(endpoint_code, None): + return event_codes + raise ValueError("Invalid token type or no event code found.") diff --git a/api_services/api_builds/initial-service/modules/Token/config.py b/api_services/api_modules/token/config.py similarity index 100% rename from api_services/api_builds/initial-service/modules/Token/config.py rename to api_services/api_modules/token/config.py diff --git a/api_services/api_builds/initial-service/modules/Token/password_module.py b/api_services/api_modules/token/password_module.py similarity index 100% rename from api_services/api_builds/initial-service/modules/Token/password_module.py rename to api_services/api_modules/token/password_module.py diff --git a/api_services/api_validations/a.txt b/api_services/api_validations/a.txt index e69de29..30d769c 100644 --- a/api_services/api_validations/a.txt +++ b/api_services/api_validations/a.txt @@ -0,0 +1,2 @@ + +result_with_keys_dict {'Employees.id': 3, 'Employees.uu_id': UUID('2b757a5c-01bb-4213-9cf1-402480e73edc'), 'People.id': 2, 'People.uu_id': UUID('d945d320-2a4e-48db-a18c-6fd024beb517'), 'Users.id': 3, 'Users.uu_id': UUID('bdfa84d9-0e05-418c-9406-d6d1d41ae2a1'), 'Companies.id': 1, 'Companies.uu_id': UUID('da1de172-2f89-42d2-87f3-656b36a79d5b'), 'Departments.id': 3, 'Departments.uu_id': UUID('4edcec87-e072-408d-a780-3a62151b3971'), 'Duty.id': 9, 'Duty.uu_id': UUID('00d29292-c29e-4435-be41-9704ccf4b24d'), 'Addresses.id': None, 'Addresses.letter_address': None} diff --git a/api_services/api_validations/defaults/validations.py b/api_services/api_validations/defaults/validations.py index 2090466..b184cb8 100644 --- a/api_services/api_validations/defaults/validations.py +++ b/api_services/api_validations/defaults/validations.py @@ -1,7 +1,7 @@ from fastapi import Header, Request, Response from pydantic import BaseModel -from api_services.api_initializer.config import api_config +from config import api_config class CommonHeaders(BaseModel): diff --git a/api_services/api_validations/token/validations.py b/api_services/api_validations/token/validations.py new file mode 100644 index 0000000..26ff579 --- /dev/null +++ b/api_services/api_validations/token/validations.py @@ -0,0 +1,123 @@ +from enum import Enum +from pydantic import BaseModel +from typing import Optional, Union + + +class UserType(Enum): + + employee = 1 + occupant = 2 + + +class Credentials(BaseModel): + + person_id: int + person_name: str + + +class ApplicationToken(BaseModel): + # Application Token Object -> is the main object for the user + + user_type: int = UserType.occupant.value + credential_token: str = "" + + user_uu_id: str + user_id: int + + person_id: int + person_uu_id: str + + request: Optional[dict] = None # Request Info of Client + expires_at: Optional[float] = None # Expiry timestamp + + +class OccupantToken(BaseModel): + + # Selection of the occupant type for a build part is made by the user + + living_space_id: int # Internal use + living_space_uu_id: str # Outer use + + occupant_type_id: int + occupant_type_uu_id: str + occupant_type: str + + build_id: int + build_uuid: str + build_part_id: int + build_part_uuid: str + + responsible_company_id: Optional[int] = None + responsible_company_uuid: Optional[str] = None + responsible_employee_id: Optional[int] = None + responsible_employee_uuid: Optional[str] = None + + # ID list of reachable event codes as "endpoint_code": ["UUID", "UUID"] + reachable_event_codes: Optional[dict[str, str]] = None + + # ID list of reachable applications as "page_url": ["UUID", "UUID"] + reachable_app_codes: Optional[dict[str, str]] = None + + +class CompanyToken(BaseModel): + + # Selection of the company for an employee is made by the user + company_id: int + company_uu_id: str + + department_id: int # ID list of departments + department_uu_id: str # ID list of departments + + duty_id: int + duty_uu_id: str + + staff_id: int + staff_uu_id: str + + employee_id: int + employee_uu_id: str + bulk_duties_id: int + + # ID list of reachable event codes as "endpoint_code": ["UUID", "UUID"] + reachable_event_codes: Optional[dict[str, str]] = None + + # ID list of reachable applications as "page_url": ["UUID", "UUID"] + reachable_app_codes: Optional[dict[str, str]] = None + + +class OccupantTokenObject(ApplicationToken): + # Occupant Token Object -> Requires selection of the occupant type for a specific build part + + available_occupants: dict = None + selected_occupant: Optional[OccupantToken] = None # Selected Occupant Type + + @property + def is_employee(self) -> bool: + return False + + @property + def is_occupant(self) -> bool: + return True + + +class EmployeeTokenObject(ApplicationToken): + # Full hierarchy Employee[staff_id] -> Staff -> Duty -> Department -> Company + + companies_id_list: list[int] # List of company objects + companies_uu_id_list: list[str] # List of company objects + + duty_id_list: list[int] # List of duty objects + duty_uu_id_list: list[str] # List of duty objects + + selected_company: Optional[CompanyToken] = None # Selected Company Object + + @property + def is_employee(self) -> bool: + return True + + @property + def is_occupant(self) -> bool: + return False + + +TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject] diff --git a/api_services/schemas/event/event.py b/api_services/schemas/event/event.py index 0a62d9b..932622c 100644 --- a/api_services/schemas/event/event.py +++ b/api_services/schemas/event/event.py @@ -316,27 +316,16 @@ class Event2Employee(CrudCollection): @classmethod def get_event_codes(cls, employee_id: int, db) -> dict[str:str]: - employee_events = cls.filter_all( - cls.employee_id == employee_id, - db=db, - ).data + cls.set_session(db) + Service2Events.set_session(db) + Events.set_session(db) + Event2EmployeeExtra.set_session(db) + employee_events = cls.query.filter(cls.employee_id == employee_id).all() service_ids = list(set([event.event_service_id for event in employee_events])) - active_event_ids = Service2Events.filter_all( - Service2Events.service_id.in_(service_ids), - db=db, - ).data - active_events = Events.filter_all( - Events.id.in_([event.event_id for event in active_event_ids]), - db=db, - ).data - if extra_events := Event2EmployeeExtra.filter_all( - Event2EmployeeExtra.employee_id == employee_id, - db=db, - ).data: - events_extra = Events.filter_all( - Events.id.in_([event.event_id for event in extra_events]), - db=db, - ).data + active_event_ids = Service2Events.query.filter(Service2Events.service_id.in_(service_ids)).all() + active_events = Events.query.filter(Events.id.in_([event.event_id for event in active_event_ids])).all() + if extra_events := Event2EmployeeExtra.query.filter(Event2EmployeeExtra.employee_id == employee_id).all(): + events_extra = Events.query.filter(Events.id.in_([event.event_id for event in extra_events]),).all() active_events.extend(events_extra) events_dict = {} for event in active_events: @@ -382,27 +371,15 @@ class Event2Occupant(CrudCollection): @classmethod def get_event_codes(cls, build_living_space_id: int, db) -> dict[str:str]: - occupant_events = cls.filter_all( - cls.build_living_space_id == build_living_space_id, - db=db, - ).data + cls.set_session(db) + Service2Events.set_session(db) + Events.set_session(db) + occupant_events = cls.query.filter(cls.build_living_space_id == build_living_space_id).all() service_ids = list(set([event.event_service_id for event in occupant_events])) - active_event_ids = Service2Events.filter_all_system( - Service2Events.service_id.in_(service_ids), - db=db, - ).data - active_events = Events.filter_all( - Events.id.in_([event.event_id for event in active_event_ids]), - db=db, - ).data - if extra_events := Event2OccupantExtra.filter_all( - Event2OccupantExtra.build_living_space_id == build_living_space_id, - db=db, - ).data: - events_extra = Events.filter_all( - Events.id.in_([event.event_id for event in extra_events]), - db=db, - ).data + active_event_ids = Service2Events.query.filter(Service2Events.service_id.in_(service_ids)).all() + active_events = Events.query.filter(Events.id.in_([event.event_id for event in active_event_ids])).all() + if extra_events := Event2OccupantExtra.query.filter(Event2OccupantExtra.build_living_space_id == build_living_space_id).all(): + events_extra = Events.query.filter(Events.id.in_([event.event_id for event in extra_events])).all() active_events.extend(events_extra) events_dict = {} for event in active_events: @@ -428,31 +405,16 @@ class Application2Employee(CrudCollection): @classmethod def get_application_codes(cls, employee_id: int, db) -> list[int]: - employee_services = cls.filter_all( - cls.employee_id == employee_id, - db=db, - ).data + cls.set_session(db) + Service2Application.set_session(db) + Applications.set_session(db) + Application2EmployeeExtra.set_session(db) + employee_services = cls.query.filter(cls.employee_id == employee_id).all() service_ids = [service.service_id for service in employee_services] - active_applications = Service2Application.filter_all( - Service2Application.service_id.in_(service_ids), - db=db, - ).data - applications = Applications.filter_all( - Applications.id.in_( - [application.application_id for application in active_applications] - ), - db=db, - ).data - if extra_applications := Application2EmployeeExtra.filter_all( - Application2EmployeeExtra.employee_id == employee_id, - db=db, - ).data: - applications_extra = Applications.filter_all( - Applications.id.in_( - [application.application_id for application in extra_applications] - ), - db=db, - ).data + active_applications = Service2Application.query.filter(Service2Application.service_id.in_(service_ids)).all() + applications = Applications.query.filter(Applications.id.in_([application.application_id for application in active_applications])).all() + if extra_applications := Application2EmployeeExtra.query.filter(Application2EmployeeExtra.employee_id == employee_id).all(): + applications_extra = Applications.query.filter(Applications.id.in_([application.application_id for application in extra_applications])).all() applications.extend(applications_extra) applications_dict = {} for application in applications: @@ -492,33 +454,25 @@ class Application2Occupant(CrudCollection): @classmethod def get_application_codes(cls, build_living_space_id: int, db) -> list[int]: - occupant_services = cls.filter_all( - cls.build_living_space_id == build_living_space_id, - db=db, - ).data + cls.set_session(db) + Service2Application.set_session(db) + Applications.set_session(db) + Application2OccupantExtra.set_session(db) + occupant_services = cls.query.filter(cls.build_living_space_id == build_living_space_id).all() service_ids = [service.service_id for service in occupant_services] - active_applications = Service2Application.filter_all( - Service2Application.service_id.in_(service_ids), - db=db, - ).data - applications = Applications.filter_all( - Applications.id.in_( - [application.application_id for application in active_applications] - ), - db=db, - ).data - if extra_applications := Application2OccupantExtra.filter_all( - Application2OccupantExtra.build_living_space_id == build_living_space_id, - db=db, - ).data: - applications_extra = Applications.filter_all( - Applications.id.in_( - [application.application_id for application in extra_applications] - ), - db=db, - ).data + active_applications = Service2Application.query.filter(Service2Application.service_id.in_(service_ids)).all() + applications = Applications.query.filter(Applications.id.in_([application.application_id for application in active_applications])).all() + if extra_applications := Application2OccupantExtra.query.filter(Application2OccupantExtra.build_living_space_id == build_living_space_id).all(): + applications_extra = Applications.query.filter(Applications.id.in_([application.application_id for application in extra_applications])).all() applications.extend(applications_extra) applications_dict = {} + for application in applications: + if not application.site_url in applications_dict: + applications_dict[str(application.site_url)] = str(application.application_code) + else: + ValueError("Duplicate application code found for single endpoint") + applications.extend(applications_extra) + applications_dict = {} for application in applications: if not application.site_url in applications_dict: applications_dict[str(application.site_url)] = str(application.application_code) diff --git a/docker-compose.yml b/docker-compose.yml index a81c58d..2a89f1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,25 @@ services: - # auth_service: - # container_name: auth_service - # build: - # context: . - # dockerfile: api_services/api_builds/auth-service/Dockerfile - # env_file: - # - api_env.env - # environment: - # - API_PATH=app:app - # - API_HOST=0.0.0.0 - # - API_PORT=8001 - # - API_LOG_LEVEL=info - # - API_RELOAD=1 - # - API_APP_NAME=evyos-auth-api-gateway - # - API_TITLE=WAG API Auth Api Gateway - # - API_FORGOT_LINK=https://auth_service/forgot-password - # - API_DESCRIPTION=This api is serves as web auth api gateway only to evyos web services. - # - API_APP_URL=https://auth_service - # ports: - # - "8000:8000" - # restart: unless-stopped - # logging: - # driver: "json-file" - # options: - # max-size: "10m" - # max-file: "3" + auth_service: + container_name: auth_service + build: + context: . + dockerfile: api_services/api_builds/auth-service/Dockerfile + env_file: + - api_env.env + environment: + - API_PATH=app:app + - API_HOST=0.0.0.0 + - API_PORT=8001 + - API_LOG_LEVEL=info + - API_RELOAD=1 + - API_APP_NAME=evyos-auth-api-gateway + - API_TITLE=WAG API Auth Api Gateway + - API_FORGOT_LINK=https://auth_service/forgot-password + - API_DESCRIPTION=This api is serves as web auth api gateway only to evyos web services. + - API_APP_URL=https://auth_service + ports: + - "8001:8001" + # restart: unless-stopped initializer_service: container_name: initializer_service @@ -40,8 +35,6 @@ services: mem_limit: 512m cpus: 0.5 - networks: wag-services: driver: bridge -