"""Token service for handling authentication tokens and user sessions.""" from typing import List, Union, TypeVar, Dict, Any, Optional, TYPE_CHECKING 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 Schemas.identity.identity import UsersTokens, People from Services.Redis import RedisActions, AccessToken from ApiValidations.Custom.token_objects import ( EmployeeTokenObject, OccupantTokenObject, UserType, CompanyToken, OccupantToken, ) from Schemas import ( Users, BuildLivingSpace, BuildParts, Employees, Addresses, Companies, Staff, Duty, Duties, Departments, OccupantTypes, ) from Services.Redis.Models.response import RedisResponse if TYPE_CHECKING: from fastapi import Request T = TypeVar("T", EmployeeTokenObject, OccupantTokenObject) class TokenService: """Service class for handling authentication tokens and user sessions.""" @classmethod def _create_access_token(cls, access: bool = True) -> str: """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(), sys_msg="User does not have any living space", ) 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(), sys_msg="User does not have any living space", ) 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() if access_token := cls.set_object_to_redis(user, model_value): return { "access_token": access_token, "user_type": UserType.occupant.name, "available_occupants": occupants_selection_dict, } raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Creating Token failed...", ) @classmethod def set_object_to_redis(cls, user, model: Dict): access_object = AccessToken( userUUID=user.uu_id, accessToken=cls._create_access_token(), ) redis_action = RedisActions.set_json( list_keys=access_object.to_list(), value=model, expires={"seconds": int(Auth.TOKEN_EXPIRE_MINUTES_30.seconds)}, ) if redis_action.status: return access_object.accessToken raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Saving Token failed...", ) @classmethod def update_object_to_redis(cls, access_token: str, user_uu_id: str, model: Dict): access_object = AccessToken( userUUID=user_uu_id, accessToken=access_token, ) redis_action = RedisActions.set_json( list_keys=access_object.to_list(), value=model, expires={"seconds": int(Auth.TOKEN_EXPIRE_MINUTES_30.seconds)}, ) if redis_action.status: return access_object.accessToken raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Saving Token failed...", ) @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, } ) person = People.filter_one(People.id == user.person_id, db=db_session).data 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=person.id, person_uu_id=str(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 access_token := cls.set_object_to_redis(user, model_value): return { "access_token": access_token, "user_type": UserType.employee.name, "companies_list": companies_list, } raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Creating Token failed...", ) @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.data.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 if user.is_occupant: login_dict = cls.do_occupant_login( request=request, user=user, domain=domain ) elif user.is_employee: login_dict = cls.do_employee_login( request=request, user=user, domain=domain ) # 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 update_token_at_redis( cls, request: "Request", add_payload: Union[CompanyToken, OccupantToken] ) -> Dict[str, Any]: """Update token at Redis.""" access_token = cls.get_access_token_from_request(request=request) token_object = cls.get_object_via_access_key(access_token=access_token) if isinstance(token_object, EmployeeTokenObject) and isinstance( add_payload, CompanyToken ): token_object.selected_company = add_payload cls.update_object_to_redis( access_token=access_token, user_uu_id=token_object.user_uu_id, model=token_object.model_dump(), ) return token_object.selected_company.model_dump() elif isinstance(token_object, OccupantTokenObject) and isinstance( add_payload, OccupantToken ): token_object.selected_occupant = add_payload cls.update_object_to_redis( access_token=access_token, user_uu_id=token_object.user_uu_id, model=token_object.model_dump(), ) return token_object.selected_occupant.model_dump() raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Token not found", ) @classmethod def raise_error_if_request_has_no_token(cls, request: "Request") -> None: """Validate request has required token headers.""" if not hasattr(request, "headers"): raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Request has no headers", ) if not request.headers.get(Auth.ACCESS_TOKEN_TAG): raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Request has no access token presented", ) @classmethod def access_token_is_valid(cls, request: "Request") -> bool: """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) return request.headers.get(Auth.ACCESS_TOKEN_TAG) @classmethod def _process_redis_object(cls, redis_object: Dict[str, Any]) -> T: """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 HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Unknown user type", ) @classmethod def get_object_via_access_key(cls, access_token: str) -> T: """Get token object using access key.""" access_token_obj = AccessToken(accessToken=access_token) redis_response = RedisActions.get_json(list_keys=access_token_obj.to_list()) if not redis_response.status: raise HTTPExceptionApi( error_code="", lang="en", loc=get_line_number_for_error(), sys_msg="Access token token is not found or unable to retrieve", ) if redis_object := redis_response.first: redis_object_dict = redis_object.data access_token_obj.userUUID = redis_object_dict.get("user_uu_id") return cls._process_redis_object(redis_object_dict) @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(), sys_msg="Invalid access token", )