prod-wag-backend-automate-s.../api_services/api_builds/auth-service/events/auth/events.py

600 lines
28 KiB
Python

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()