From f284d4c61b4926897238b6dad301b06c89c99dfa Mon Sep 17 00:00:00 2001 From: berkay Date: Thu, 3 Apr 2025 15:20:39 +0300 Subject: [PATCH] updated timezone params header --- ApiServices/AuthService/create_app.py | 6 - .../AuthService/endpoints/auth/route.py | 214 +++++++++--------- ApiServices/AuthService/events/auth/auth.py | 2 + .../AuthService/validations/custom/token.py | 120 ++++++++++ .../request/authentication/login_post.py | 27 ++- Controllers/Postgres/mixin.py | 24 +- Schemas/identity/identity.py | 49 ++++ 7 files changed, 322 insertions(+), 120 deletions(-) create mode 100644 ApiServices/AuthService/events/auth/auth.py create mode 100644 ApiServices/AuthService/validations/custom/token.py diff --git a/ApiServices/AuthService/create_app.py b/ApiServices/AuthService/create_app.py index ac18dfb..66cd6cc 100644 --- a/ApiServices/AuthService/create_app.py +++ b/ApiServices/AuthService/create_app.py @@ -1,7 +1,6 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse -from fastapi.staticfiles import StaticFiles from ApiServices.AuthService.create_route import RouteRegisterController from ApiServices.AuthService.endpoints.routes import get_routes @@ -13,11 +12,6 @@ from ApiServices.AuthService.config import api_config def create_app(): application = FastAPI(**api_config.api_info) - # application.mount( - # "/application/static", - # StaticFiles(directory="application/static"), - # name="static", - # ) application.add_middleware( CORSMiddleware, allow_origins=api_config.ALLOW_ORIGINS, diff --git a/ApiServices/AuthService/endpoints/auth/route.py b/ApiServices/AuthService/endpoints/auth/route.py index 1f62642..3776228 100644 --- a/ApiServices/AuthService/endpoints/auth/route.py +++ b/ApiServices/AuthService/endpoints/auth/route.py @@ -8,7 +8,10 @@ from ApiServices.AuthService.config import api_config from ApiServices.AuthService.validations.request.authentication.login_post import ( RequestLogin, RequestSelectLiving, - RequestSelectOccupant, RequestCreatePassword, RequestChangePassword, RequestForgotPasswordPhone, + RequestSelectOccupant, + RequestCreatePassword, + RequestChangePassword, + RequestForgotPasswordPhone, RequestForgotPasswordEmail, ) @@ -29,6 +32,7 @@ def authentication_login_post( data: RequestLogin, language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Authentication Login Route with Post Method @@ -61,6 +65,7 @@ def authentication_select_post( data: Union[RequestSelectOccupant, RequestSelectLiving], language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Authentication Select Route with Post Method @@ -86,6 +91,109 @@ def authentication_select_post( ) +@auth_route.post( + path="/password/create", + summary="Create password with access token", + description="Create password", +) +def authentication_password_create_post( + request: Request, + data: RequestCreatePassword, + language: str = Header(None, alias="language"), + domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), +): + """ + 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, + } + if not domain or not language: + return JSONResponse( + content={"error": "EYS_0001"}, + status_code=status.HTTP_406_NOT_ACCEPTABLE, + headers=headers, + ) + return JSONResponse( + content={**data.model_dump()}, + status_code=status.HTTP_202_ACCEPTED, + headers=headers, + ) + + +@auth_route.post( + path="/password/change", + summary="Change password with access token", + description="Change password", +) +def authentication_password_change_post( + request: Request, + data: RequestChangePassword, + language: str = Header(None, alias="language"), + domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), +): + """ + Authentication change 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, + } + if not domain or not language: + return JSONResponse( + content={"error": "EYS_0001"}, + status_code=status.HTTP_406_NOT_ACCEPTABLE, + headers=headers, + ) + return JSONResponse( + content={**data.model_dump()}, + status_code=status.HTTP_202_ACCEPTED, + headers=headers, + ) + + +@auth_route.post( + path="/password/reset", + summary="Reset password with access token", + description="Reset password", +) +def authentication_password_reset_post( + request: Request, + data: Union[RequestForgotPasswordEmail, RequestForgotPasswordPhone], + language: str = Header(None, alias="language"), + domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), +): + """ + Authentication reset password Route with Post Method + """ + headers = { + "language": language or "", + "domain": domain or "", + "eys-ext": f"{str(uuid.uuid4())}", + } + if not domain or not language: + return JSONResponse( + content={"error": "EYS_0001"}, + status_code=status.HTTP_406_NOT_ACCEPTABLE, + headers=headers, + ) + return JSONResponse( + content={**data.model_dump()}, + status_code=status.HTTP_202_ACCEPTED, + headers=headers, + ) + + @auth_route.get( path="/logout", summary="Logout user", @@ -95,6 +203,7 @@ def authentication_logout_post( request: Request, language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Logout user from the system @@ -129,6 +238,7 @@ def authentication_disconnect_post( request: Request, language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Disconnect all sessions of user in access token @@ -164,6 +274,7 @@ def authentication_token_check_post( request: Request, language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Check if access token is valid for user @@ -199,6 +310,7 @@ def authentication_token_refresh_post( request: Request, language: str = Header(None, alias="language"), domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), ): """ Refresh if access token is valid for user @@ -220,103 +332,3 @@ def authentication_token_refresh_post( status_code=status.HTTP_202_ACCEPTED, headers=headers, ) - - -@auth_route.post( - path="/password/create", - summary="Create password with access token", - description="Create password", -) -def authentication_password_create_post( - request: Request, - data: RequestCreatePassword, - language: str = Header(None, alias="language"), - domain: str = Header(None, alias="domain"), -): - """ - 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, - } - if not domain or not language: - return JSONResponse( - content={"error": "EYS_0001"}, - status_code=status.HTTP_406_NOT_ACCEPTABLE, - headers=headers, - ) - return JSONResponse( - content={**data.model_dump()}, - status_code=status.HTTP_202_ACCEPTED, - headers=headers, - ) - - -@auth_route.post( - path="/password/change", - summary="Change password with access token", - description="Change password", -) -def authentication_password_change_post( - request: Request, - data: RequestChangePassword, - language: str = Header(None, alias="language"), - domain: str = Header(None, alias="domain"), -): - """ - Authentication change 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, - } - if not domain or not language: - return JSONResponse( - content={"error": "EYS_0001"}, - status_code=status.HTTP_406_NOT_ACCEPTABLE, - headers=headers, - ) - return JSONResponse( - content={**data.model_dump()}, - status_code=status.HTTP_202_ACCEPTED, - headers=headers, - ) - - -@auth_route.post( - path="/password/reset", - summary="Reset password with access token", - description="Reset password", -) -def authentication_password_reset_post( - request: Request, - data: Union[RequestForgotPasswordEmail, RequestForgotPasswordPhone], - language: str = Header(None, alias="language"), - domain: str = Header(None, alias="domain"), -): - """ - Authentication reset password Route with Post Method - """ - headers = { - "language": language or "", - "domain": domain or "", - "eys-ext": f"{str(uuid.uuid4())}", - } - if not domain or not language: - return JSONResponse( - content={"error": "EYS_0001"}, - status_code=status.HTTP_406_NOT_ACCEPTABLE, - headers=headers, - ) - return JSONResponse( - content={**data.model_dump()}, - status_code=status.HTTP_202_ACCEPTED, - headers=headers, - ) diff --git a/ApiServices/AuthService/events/auth/auth.py b/ApiServices/AuthService/events/auth/auth.py new file mode 100644 index 0000000..596f912 --- /dev/null +++ b/ApiServices/AuthService/events/auth/auth.py @@ -0,0 +1,2 @@ +class AuthHandlers: + pass diff --git a/ApiServices/AuthService/validations/custom/token.py b/ApiServices/AuthService/validations/custom/token.py new file mode 100644 index 0000000..3152533 --- /dev/null +++ b/ApiServices/AuthService/validations/custom/token.py @@ -0,0 +1,120 @@ +import enum +from typing import Optional, List +from pydantic import BaseModel + + +# Company / Priority / Department / Duty / Employee / Occupant / Module / Endpoint are changeable dynamics +# domain: Optional[str] = "app.evyos.com.tr" +# lang: Optional[str] = "TR" +# timezone: Optional[str] = "GMT+3" +# full_name: Optional[str] = None + + +class UserType(enum.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 + + reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules + + +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 + + reachable_event_codes: Optional[list[str]] = None # ID list of reachable modules + + +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 diff --git a/ApiServices/AuthService/validations/request/authentication/login_post.py b/ApiServices/AuthService/validations/request/authentication/login_post.py index 069ea06..54cd5f7 100644 --- a/ApiServices/AuthService/validations/request/authentication/login_post.py +++ b/ApiServices/AuthService/validations/request/authentication/login_post.py @@ -10,24 +10,50 @@ class RequestLogin(BaseModel): class RequestSelectOccupant(BaseModel): + company_uu_id: str + @property + def is_employee(self): + return True + + @property + def is_occupant(self): + return False + 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 @@ -35,4 +61,3 @@ class RequestForgotPasswordEmail(BaseModel): class RequestForgotPasswordPhone(BaseModel): phone_number: str - diff --git a/Controllers/Postgres/mixin.py b/Controllers/Postgres/mixin.py index 5f60605..014ed32 100644 --- a/Controllers/Postgres/mixin.py +++ b/Controllers/Postgres/mixin.py @@ -114,18 +114,18 @@ class CrudCollection(CrudMixin): cryp_uu_id: Mapped[str] = mapped_column( String, nullable=True, index=True, comment="Cryptographic UUID" ) - created_by: Mapped[str] = mapped_column( - String, nullable=True, comment="Creator name" - ) - created_by_id: Mapped[int] = mapped_column( - Integer, nullable=True, comment="Creator ID" - ) - updated_by: Mapped[str] = mapped_column( - String, nullable=True, comment="Last modifier name" - ) - updated_by_id: Mapped[int] = mapped_column( - Integer, nullable=True, comment="Last modifier ID" - ) + # created_by: Mapped[str] = mapped_column( + # String, nullable=True, comment="Creator name" + # ) + # created_by_id: Mapped[int] = mapped_column( + # Integer, nullable=True, comment="Creator ID" + # ) + # updated_by: Mapped[str] = mapped_column( + # String, nullable=True, comment="Last modifier name" + # ) + # updated_by_id: Mapped[int] = mapped_column( + # Integer, nullable=True, comment="Last modifier ID" + # ) confirmed_by: Mapped[str] = mapped_column( String, nullable=True, comment="Confirmer name" ) diff --git a/Schemas/identity/identity.py b/Schemas/identity/identity.py index d3fd430..36067fd 100644 --- a/Schemas/identity/identity.py +++ b/Schemas/identity/identity.py @@ -35,6 +35,55 @@ class UsersTokens(CrudCollection): # users = relationship("Users", back_populates="tokens", foreign_keys=[user_id]) +class Credentials(CrudCollection): + """ + Credentials class to store user credentials + """ + + __tablename__ = "credentials" + __exclude__fields__ = [] + + credential_token: Mapped[str] = mapped_column( + String, server_default="", comment="Credential token for authentication" + ) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, comment="Foreign key to users table" + ) + user_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="User UUID", index=True + ) + person_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=False, comment="Foreign key to person table" + ) + person_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Person UUID", index=True + ) + name: Mapped[str] = mapped_column( + String, server_default="", comment="Name of the user", index=True + ) + surname: Mapped[str] = mapped_column( + String, server_default="", comment="Surname of the user", index=True + ) + email: Mapped[str] = mapped_column( + String, server_default="", comment="Email address of the user", index=True + ) + phone: Mapped[str] = mapped_column( + String, server_default="", comment="Phone number of the user", index=True + ) + is_verified: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Flag to check if user is verified" + ) + + def generate_token(self) -> str: + """ + Generate a unique token for the user + """ + name_token, rest_of_token = "", "" + if self.name and self.surname: + name_token = f"{self.name[0].upper()}{self.surname[0].upper()}" + return "" + + class Users(CrudCollection): """ Application User frame to connect to API with assigned token-based HTTP connection