join tested auth service login/select completed

This commit is contained in:
Berkay 2025-05-13 13:19:01 +03:00
parent 1d4f00e8b2
commit cd62d96158
34 changed files with 1360 additions and 231 deletions

View File

@ -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

View File

@ -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

View File

@ -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",
}

View File

@ -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"),
]

View File

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

View File

@ -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

View File

@ -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

View File

@ -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 /

View File

@ -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,

View File

@ -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,

View File

@ -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",
}

View File

@ -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"),
]

View File

@ -0,0 +1,3 @@
__all__ = []

View File

@ -0,0 +1,10 @@
events_index: dict = {
"Slot1": "",
"Slot2": "",
"Slot3": "",
"Slot4": "",
"Slot5": "",
}

View File

@ -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):
"""

View File

@ -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])

View File

@ -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:

View File

@ -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.

View File

@ -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."""

View File

@ -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.

View File

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

View File

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

View File

@ -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:

View File

@ -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,

View File

@ -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.")

View File

@ -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}

View File

@ -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):

View File

@ -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]

View File

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

View File

@ -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