445 lines
17 KiB
Python
445 lines
17 KiB
Python
"""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",
|
|
)
|