Web service initiated

This commit is contained in:
2025-04-05 14:59:10 +03:00
parent b1c8203a33
commit fa4df11323
76 changed files with 5385 additions and 171 deletions

View File

@@ -17,9 +17,8 @@ RUN poetry config virtualenvs.create false \
# Copy application code
COPY /ApiServices/AuthService /ApiServices/AuthService
COPY /Controllers /Controllers
COPY /Schemas/building /Schemas/building
COPY /Schemas/company /Schemas/company
COPY /Schemas/identity /Schemas/identity
COPY /Modules /Modules
COPY /Schemas /Schemas
# Set Python path to include app directory
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1

View File

@@ -8,10 +8,7 @@ class RouteRegisterController:
self.router_list = router_list
self.app = app
def register_routes(self):
for router in self.router_list:
self.app.include_router(router)
self.add_router_to_database(router)
return self.app

View File

@@ -2,4 +2,4 @@ from .auth.route import auth_route
__all__ = [
"auth_route",
]
]

View File

@@ -13,13 +13,12 @@ from ApiServices.AuthService.validations.request.authentication.login_post impor
RequestChangePassword,
RequestForgotPasswordPhone,
RequestForgotPasswordEmail,
RequestVerifyOTP,
)
from ApiServices.AuthService.events.auth.auth import AuthHandlers
auth_route = APIRouter(
prefix="/authentication",
tags=["Authentication Cluster"],
)
auth_route = APIRouter(prefix="/authentication", tags=["Authentication Cluster"])
@auth_route.post(
@@ -49,8 +48,12 @@ def authentication_login_post(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
headers=headers,
)
result = AuthHandlers.LoginHandler.authentication_login_with_domain_and_creds(
request=request,
data=data,
)
return JSONResponse(
content={**data.model_dump()},
content=result,
status_code=status.HTTP_202_ACCEPTED,
headers=headers,
)
@@ -84,9 +87,12 @@ def authentication_select_post(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
headers=headers,
)
result = AuthHandlers.LoginHandler.authentication_select_company_or_occupant_type(
request=request,
data=data,
)
return JSONResponse(
content=data.model_dump(),
content=result,
status_code=status.HTTP_202_ACCEPTED,
headers=headers,
)
@@ -107,13 +113,15 @@ def authentication_password_create_post(
"""
Authentication create password Route with Post Method
"""
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
headers = {
"language": language or "",
"domain": domain or "",
"eys-ext": f"{str(uuid.uuid4())}",
"token": token,
}
result = AuthHandlers.PasswordHandler.create_password(
password=data.password,
password_token=data.password_token,
)
if not domain or not language:
return JSONResponse(
content={"error": "EYS_0001"},
@@ -121,7 +129,7 @@ def authentication_password_create_post(
headers=headers,
)
return JSONResponse(
content={**data.model_dump()},
content={},
status_code=status.HTTP_202_ACCEPTED,
headers=headers,
)
@@ -333,3 +341,41 @@ def authentication_token_refresh_post(
status_code=status.HTTP_202_ACCEPTED,
headers=headers,
)
@auth_route.get(
path="/password/verify-otp",
summary="Verify OTP for password reset",
description="Verify OTP for password reset",
)
def authentication_password_verify_otp(
request: Request,
data: RequestVerifyOTP,
language: str = Header(None, alias="language"),
domain: str = Header(None, alias="domain"),
tz: str = Header(None, alias="timezone"),
):
"""
Verify OTP for password reset
"""
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
headers = {
"language": language or "",
"domain": domain or "",
"eys-ext": f"{str(uuid.uuid4())}",
"tz": tz or "GMT+3",
"token": token,
}
print('Token&OTP : ', data.otp, data.token)
if not domain or not language:
return JSONResponse(
content={"error": "EYS_0003"},
status_code=status.HTTP_406_NOT_ACCEPTABLE,
headers=headers,
)
return JSONResponse(
content={},
status_code=status.HTTP_202_ACCEPTED,
headers=headers,
)

View File

@@ -16,4 +16,7 @@ def get_safe_endpoint_urls() -> list[tuple[str, str]]:
("/auth/login", "POST"),
("/metrics", "GET"),
("/authentication/login", "POST"),
("/authentication/password/reset", "POST"),
("/authentication/password/create", "POST"),
("/authentication/password/verify-otp", "POST"),
]

View File

@@ -1,4 +1,7 @@
from typing import Any, List, Dict, Optional, Union
import arrow
from typing import Any, Dict, Optional, Union
from ApiServices.AuthService.events.auth.model import PasswordHistoryViaUser
from ApiServices.AuthService.validations.custom.token import (
EmployeeTokenObject,
OccupantTokenObject,
@@ -6,7 +9,7 @@ from ApiServices.AuthService.validations.custom.token import (
OccupantToken,
UserType,
)
from ApiServices.TemplateService.config import api_config
from ApiServices.AuthService.config import api_config
from Schemas import (
Users,
People,
@@ -25,12 +28,14 @@ from Schemas import (
Event2Employee,
)
from Modules.Token.password_module import PasswordModule
from Controllers.Redis.database import RedisActions
from Schemas.building.build import RelationshipEmployee2Build
from Schemas.event.event import Event2Occupant
from Controllers.Redis.database import RedisActions
from Controllers.Mongo.database import mongo_handler
TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject]
class RedisHandlers:
AUTH_TOKEN: str = "AUTH_TOKEN"
@@ -63,7 +68,7 @@ class RedisHandlers:
result_delete = RedisActions.delete(
list_keys=[RedisHandlers.AUTH_TOKEN, "*", str(user.uu_id)]
)
print('result_delete', result_delete)
print("result_delete", result_delete)
generated_access_token = PasswordModule.generate_access_token()
keys = [RedisHandlers.AUTH_TOKEN, generated_access_token, str(user.uu_id)]
RedisActions.set_json(
@@ -74,20 +79,27 @@ class RedisHandlers:
return generated_access_token
@classmethod
def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]):
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:
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:
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)],
list_keys=[
RedisHandlers.AUTH_TOKEN,
token,
str(already_token.user_uu_id),
],
value=already_token.model_dump(),
expires={"hours": 1, "minutes": 30},
)
print("result.first", result.first)
return result.first
raise ValueError("Something went wrong")
@@ -118,6 +130,14 @@ class UserHandlers:
"""
Check if the password is valid.
"""
print(
dict(
domain=domain,
id_=id_,
password=password,
password_hashed=password_hashed,
)
)
if PasswordModule.check_password(
domain=domain, id_=id_, password=password, password_hashed=password_hashed
):
@@ -135,10 +155,9 @@ class LoginHandler:
def is_employee(email: str):
return str(email).split("@")[1] == api_config.ACCESS_EMAIL_EXT
@classmethod
def do_employee_login(
cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None
cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None
):
"""
Handle employee login.
@@ -154,10 +173,10 @@ class LoginHandler:
)
if not user_handler.check_password_valid(
domain=data.domain,
domain=domain or "",
id_=str(found_user.uu_id),
password=data.password,
password_hashed=found_user.hash_password
password_hashed=found_user.hash_password,
):
raise ValueError("EYS_0005")
@@ -230,13 +249,27 @@ class LoginHandler:
return {
"access_token": access_token,
"user_type": UserType.employee.name,
"user": found_user.get_dict(
exclude_list=[
Users.hash_password,
Users.cryp_uu_id,
Users.password_token,
Users.created_credentials_token,
Users.updated_credentials_token,
Users.confirmed_credentials_token,
Users.is_confirmed,
Users.is_notification_send,
Users.is_email_send,
Users.remember_me,
]
),
"selection_list": companies_list,
}
raise ValueError("Something went wrong")
@classmethod
def do_employee_occupant(
cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None
cls, request: Any, data: Any, extra_dict: Optional[Dict[str, Any]] = None
):
"""
Handle occupant login.
@@ -254,7 +287,7 @@ class LoginHandler:
domain=data.domain,
id_=str(found_user.uu_id),
password=data.password,
password_hashed=found_user.hash_password
password_hashed=found_user.hash_password,
):
raise ValueError("EYS_0005")
@@ -300,7 +333,9 @@ class LoginHandler:
"occupants": [occupant_data],
}
else:
occupants_selection_dict[build_key]["occupants"].append(occupant_data)
occupants_selection_dict[build_key]["occupants"].append(
occupant_data
)
person = found_user.person
model_value = OccupantTokenObject(
@@ -344,9 +379,9 @@ class LoginHandler:
Returns:
SuccessResponse containing authentication token and user info
"""
language = request.headers("language", "tr")
domain = request.headers("domain", None)
timezone = request.headers("tz", None) or "GMT+3"
language = request.headers.get("language", "tr")
domain = request.headers.get("domain", None)
timezone = request.headers.get("tz", None) or "GMT+3"
if cls.is_employee(data.access_key):
return cls.do_employee_login(
@@ -386,7 +421,9 @@ class LoginHandler:
return request.headers.get(api_config.ACCESS_TOKEN_TAG)
@classmethod
def handle_employee_selection(cls, access_token: str, data: Any, token_dict: TokenDictType):
def handle_employee_selection(
cls, access_token: str, data: Any, token_dict: TokenDictType
):
with Users.new_session() as db:
if data.company_uu_id not in token_dict.companies_uu_id_list:
ValueError("EYS_0011")
@@ -407,7 +444,9 @@ class LoginHandler:
# Get staff IDs
staff_ids = [
staff.id
for staff in Staff.filter_all(Staff.duties_id.in_(duties_ids), db=db).data
for staff in Staff.filter_all(
Staff.duties_id.in_(duties_ids), db=db
).data
]
# Get employee
@@ -456,13 +495,12 @@ class LoginHandler:
reachable_event_codes=reachable_event_codes,
)
redis_handler = RedisHandlers()
try: # Update Redis
return redis_handler.update_token_at_redis(
token=access_token, add_payload=company_token
)
except Exception as e:
err = e
ValueError("EYS_0008")
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(
@@ -526,12 +564,12 @@ class LoginHandler:
reachable_event_codes=reachable_event_codes,
)
redis_handler = RedisHandlers()
try: # Update Redis
return redis_handler.update_token_at_redis(
token=access_token, add_payload=occupant_token
)
except Exception as e:
raise ValueError("EYS_0008")
redis_handler.update_token_at_redis(
token=access_token, add_payload=occupant_token
)
return {
"selected_uu_id": data.company_uu_id,
}
@classmethod # Requires auth context
def authentication_select_company_or_occupant_type(cls, request: Any, data: Any):
@@ -549,16 +587,119 @@ class LoginHandler:
if not access_token:
raise ValueError("EYS_0001")
token_object = RedisHandlers().get_object_from_redis(access_token=access_token)
if token_object.is_employee and isinstance(data, CompanyToken):
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,
access_token=access_token,
data=data,
token_dict=token_object,
)
elif token_object.is_occupant and isinstance(data, OccupantToken):
elif token_object.is_occupant:
return cls.handle_occupant_selection(
access_token=access_token, data=data, token_dict=token_object,
access_token=access_token,
data=data,
token_dict=token_object,
)
class PasswordHandler:
@staticmethod
def create_password(password, password_token=None):
with Users.new_session() as db_session:
found_user = Users.filter_one(
Users.password_token == password_token, db=db_session
).data
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 password_token == found_user.password_token and token_is_expired:
raise ValueError("")
collection_name = f"{found_user.related_company}*Domain"
print("collection_name", collection_name)
with mongo_handler.collection(collection_name) as mongo_engine:
print({"user_uu_id": str(found_user.uu_id)})
domain_via_user = mongo_engine.find_one(
{"user_uu_id": str(found_user.uu_id)}
)
print("domain_via_user", domain_via_user)
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:
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 AuthHandlers:
LoginHandler: LoginHandler = LoginHandler()
PasswordHandler: PasswordHandler = PasswordHandler()

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

@@ -5,8 +5,7 @@ from ..config import api_config
async def token_middleware(request: Request, call_next):
base_url = "/".join(request.url.path.split("/")[:3])
base_url = request.url.path
safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()]
if base_url in safe_endpoints:
return await call_next(request)

View File

@@ -9,6 +9,11 @@ class RequestLogin(BaseModel):
remember_me: Optional[bool]
class RequestVerifyOTP(BaseModel):
token: str
otp: str
class RequestSelectOccupant(BaseModel):
company_uu_id: str