From 26eedb1b894d229c9ae045e8a851a8c7da8a0443 Mon Sep 17 00:00:00 2001 From: Jet Li Date: Sun, 20 Oct 2024 19:04:04 +0000 Subject: [PATCH] Add all sign-in and token management APIs, localhost up, need clean-up and tuning Gitea APIs, Notification center --- app/authentication/Dockerfile | 19 + .../backend/application/signin_hub.py | 148 ++------ .../backend/business/signin_manager.py | 309 +++++++++++++--- .../backend/infra/auth/user_auth_handler.py | 62 ++++ .../infra/code_management/depot_handler.py | 346 +----------------- .../user_profile/user_profile_handler.py | 121 +++++- .../backend/models/user/constants.py | 13 + .../backend/models/user/models.py | 5 +- .../backend/models/user_profile/models.py | 103 ++++++ .../services/auth/user_auth_service.py | 54 ++- .../services/code_depot/code_depot_service.py | 53 +++ .../services/user/user_management_service.py | 99 +++-- app/authentication/requirements.txt | 13 + .../webapi/bootstrap/application.py | 18 +- app/authentication/webapi/main.py | 15 +- app/authentication/webapi/providers/common.py | 2 +- app/authentication/webapi/providers/logger.py | 13 +- app/authentication/webapi/providers/router.py | 2 +- app/authentication/webapi/routes/__init__.py | 5 +- .../webapi/routes/signin/__init__.py | 9 +- .../signin/reset_password_through_email.py | 4 +- .../webapi/routes/signin/sign_out.py | 40 ++ .../signin/signin_with_email_and_code.py | 16 +- .../signin/signin_with_email_and_password.py | 44 +-- .../routes/signin/try_signin_with_email.py | 53 +-- .../routes/signin/update_new_user_flid.py | 50 +++ .../routes/signin/update_user_password.py | 23 +- app/central_storage/Dockerfile | 2 +- .../common/config/log_settings.py | 17 - .../webapi/providers/logger.py | 2 +- docker-compose.yaml | 14 + infra/config/app_settings.py | 3 + infra/log/application_logger.py | 2 +- infra/log/base_logger.py | 20 +- infra/token/token_manager.py | 17 + sites/authentication/.env | 1 + 36 files changed, 1039 insertions(+), 678 deletions(-) create mode 100644 app/authentication/Dockerfile create mode 100644 app/authentication/backend/models/user_profile/models.py create mode 100644 app/authentication/backend/services/code_depot/code_depot_service.py create mode 100644 app/authentication/requirements.txt create mode 100644 app/authentication/webapi/routes/signin/sign_out.py create mode 100644 app/authentication/webapi/routes/signin/update_new_user_flid.py delete mode 100644 app/central_storage/common/config/log_settings.py create mode 100644 sites/authentication/.env diff --git a/app/authentication/Dockerfile b/app/authentication/Dockerfile new file mode 100644 index 0000000..f1cbf8c --- /dev/null +++ b/app/authentication/Dockerfile @@ -0,0 +1,19 @@ +# Dockerfile for Python Service +FROM python:3.10-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the requirements.txt to the working directory and install dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the application code to the working directory +COPY . /app + +# Expose the port used by the FastAPI app +EXPOSE 8004 + + +# Run the application using the start script +CMD ["uvicorn", "app.authentication.webapi.main:app", "--reload", "--port=8004", "--host=0.0.0.0"] diff --git a/app/authentication/backend/application/signin_hub.py b/app/authentication/backend/application/signin_hub.py index d1b0d76..37b2c69 100644 --- a/app/authentication/backend/application/signin_hub.py +++ b/app/authentication/backend/application/signin_hub.py @@ -1,29 +1,17 @@ -from app.authentication.backend.models.constants import ( - NewUserMethod, - UserAccountProperty, - UserLoginAction, -) from typing import Optional, Tuple -from infra.i18n.region_handler import RegionHandler -from infra.models.constants import UserRegion from infra.log.log_utils import log_entry_exit_async -from app.authentication.backend.infra.user_management.user_auth_handler import ( - UserAuthManager, -) from app.authentication.backend.business.signin_manager import SignInManager +from app.authentication.backend.models.user.constants import UserLoginAction class SignInHub: def __init__(self) -> None: - self.user_auth_manager = UserAuthManager() self.signin_manager = SignInManager() - self.basic_profile_store = BasicProfileStore() - self.provider_profile_store = ProviderProfileStore() - self.code_depot_manager = CodeDepotManager() - self.notification_center = NotificationCenter(sender_id=settings.SYSTEM_USER_ID) - self.event_dispatcher = UserEventDispatcher(owner_id=settings.SYSTEM_USER_ID) - self.module_logger = ModuleLogger(sender_id=UserManager) + # TODO: Dax - Event dispatch and notification center + # self.notification_center = NotificationCenter(sender_id=settings.SYSTEM_USER_ID) + # self.event_dispatcher = UserEventDispatcher(owner_id=settings.SYSTEM_USER_ID) + @log_entry_exit_async async def signin_with_email_and_code( self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC" ) -> Tuple[int, Optional[int], Optional[str], Optional[str]]: @@ -35,16 +23,14 @@ class SignInHub: ) @log_entry_exit_async - async def signin_with_email_and_code( - self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC" + async def signin_with_email_and_password( + self, email: str, password: str ) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]: - """Try to signin with email and code. - create a new user account, if the email address has never been used before. + """Try to signin with email and password. Args: email (str): email address - code (str): auth code to be verified - host (str): the host address by which the client access the frontend service + password (str): password to be verified Returns: [int, Optional[int], Optional[str], Optional[str]]: @@ -53,99 +39,35 @@ class SignInHub: - Optional[str]: user_id - Optional[str]: flid """ - - # Step 1: Verify the email and code - try: - user_id, is_new_user, preferred_region = ( - await self.signin_manager.verify_email_with_code(email, code, host) - ) - except InvalidAuthCodeException: - await self.logger.log_info( - info="The auth code is invalid.", - properties={"email": email, "code": code}, - ) - return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None] - - # Step 2: Handle new user creation if necessary - # This will be moved to the Freeleaps - if is_new_user: - user_id = await self.user_service.create_new_user( - NewUserMethod.EMAIL, preferred_region, email, time_zone - ) - await self.event_service.log_signup_event( - user_id, email, preferred_region, time_zone - ) - - # Step 3: Fetch user account and handle login actions - user_account = await self.user_service.get_user_account(user_id) - await self.event_service.log_user_login_event(user_id) - - # Step 4: Handle special actions (FLID reset, password reset, etc.) - if await self.user_service.is_flid_reset_required(user_id): - return [ - UserLoginAction.REVIEW_AND_REVISE_FLID, - user_account.user_role, - user_id, - email.split("@")[0], - preferred_region, - ] - - user_flid = await self.user_service.get_flid(user_id) - - if await self.auth_service.is_password_reset_required(user_id): - return [ - UserLoginAction.NEW_USER_SET_PASSWORD, - user_account.user_role, - user_id, - user_flid, - user_account.preferred_region, - ] - - return [ - UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, - user_account.user_role, - user_id, - user_flid, - user_account.preferred_region, - ] + return await self.signin_manager.signin_with_email_and_password( + email=email, password=password + ) @log_entry_exit_async - async def __create_new_user_account( - self, method: NewUserMethod, region: UserRegion - ) -> str: - """create a new user account document in DB + async def update_new_user_flid( + self, user_id: str, user_flid: str + ) -> Tuple[UserLoginAction, Optional[str]]: + return await self.signin_manager.update_new_user_flid( + user_id=user_id, user_flid=user_flid + ) - Args: - method (NewUserMethod): the method the new user came from - region : preferred user region detected via the user log-in website + @log_entry_exit_async + async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction: + return await self.signin_manager.try_signin_with_email(email=email, host=host) - Returns: - str: id of user account - """ - if NewUserMethod.EMAIL == method: - user_account = UserAccountDoc( - profile_id=None, - account_id=None, - service_plan_id=None, - properties=int(UserAccountProperty.EMAIL_VERIFIED), - capabilities=int(Capability.VISITOR), - user_role=int(AdministrativeRole.PERSONAL), - region=region, - ) - user_account = await user_account.create() + @log_entry_exit_async + async def reset_password_through_email(self, email: str, host: str) -> int: + return await self.signin_manager.reset_password_through_email( + email=email, host=host + ) - elif NewUserMethod.MOBILE == method: - user_account = UserAccountDoc( - profile_id=None, - account_id=None, - service_plan_id=None, - properties=int(UserAccountProperty.MOBILE_VERIFIED), - capabilities=int(Capability.VISITOR), - user_role=int(AdministrativeRole.PERSONAL), - region=region, - ) - user_account = await user_account.create() + @log_entry_exit_async + async def update_user_password(self, user_id: str, password: str) -> dict[str, any]: + return await self.signin_manager.update_user_password( + user_id=user_id, password=password + ) - # Create other doc in collections for the new user - await UserAchievement(str(user_account.id)).create_activeness_achievement() - return str(user_account.id) + @log_entry_exit_async + async def sign_out(self, identity: str) -> bool: + # TODO: to be implemented + return True diff --git a/app/authentication/backend/business/signin_manager.py b/app/authentication/backend/business/signin_manager.py index 1eede32..11f8339 100644 --- a/app/authentication/backend/business/signin_manager.py +++ b/app/authentication/backend/business/signin_manager.py @@ -1,17 +1,22 @@ -# business/auth/signin_business.py +import random from typing import Tuple, Optional from app.authentication.backend.services.auth.user_auth_service import UserAuthService from infra.i18n.region_handler import RegionHandler -from services.auth.achievement_service import AchievementService -from services.auth.event_service import EventService -from services.auth.profile_service import ProfileService -from utils.enums.auth import UserLoginAction, NewUserMethod -from infra.config import settings +from app.authentication.backend.models.user.constants import ( + UserLoginAction, + NewUserMethod, +) from infra.exception.exceptions import InvalidAuthCodeException from app.authentication.backend.models.constants import UserLoginAction from app.authentication.backend.services.user.user_management_service import ( UserManagementService, ) +from app.authentication.backend.services.code_depot.code_depot_service import ( + CodeDepotService, +) +from infra.log.module_logger import ModuleLogger +from infra.utils.string import check_password_complexity +from infra.exception.exceptions import InvalidDataError class SignInManager: @@ -19,77 +24,299 @@ class SignInManager: self.user_auth_service = UserAuthService() self.region_handler = RegionHandler() self.user_management_service = UserManagementService() - self.achievement_service = AchievementService() - self.event_service = EventService() - self.profile_service = ProfileService() + self.module_logger = ModuleLogger(sender_id=SignInManager) + self.code_depot_service = CodeDepotService() + # TODO: Dax: notification service + # self.event_service = EventService() async def signin_with_email_and_code( self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC" ) -> Tuple[int, Optional[int], Optional[str], Optional[str]]: + """Try to signin with email and code. + create a new user account, if the email address has never been used before. + + Args: + email (str): email address + code (str): auth code to be verified + host (str): the host address by which the client access the frontend service + + Returns: + [int, Optional[int], Optional[str], Optional[str]]: + - int: UserLoginAction + - Optional[int]: user role + - Optional[str]: user_id + - Optional[str]: flid """ - Handles the business logic for signing in with email and code. - """ + # check if the user account exist user_id = await self.user_auth_service.get_user_id_by_email(email) + + # if it cannot find user account according to the email address, new user is_new_user = user_id is None preferred_region = self.region_handler.detect_from_host(host) + # verify the email through auth code if await self.user_auth_service.verify_email_with_code(email, code): if is_new_user: - user_id = await self.user_management_service.create_new_user_account( - method=NewUserMethod.EMAIL, region=preferred_region - ) - await self.user_service.initialize_new_user_data( - user_id=user_id, - email=email, - region=preferred_region, - time_zone=time_zone, - ) - await self.event_service.log_signup_event( - user_id=user_id, - email=email, - region=preferred_region, - time_zone=time_zone, + user_account = ( + await self.user_management_service.create_new_user_account( + method=NewUserMethod.EMAIL, region=preferred_region + ) ) - user_account = await self.user_service.get_user_account(user_id) + user_id = str(user_account.id) + await self.user_management_service.initialize_new_user_data( + user_id=str(user_account.id), + method=NewUserMethod.EMAIL, + email_address=email, + region=preferred_region, + time_zone=time_zone, + ) + # TODO: Dax - Add notification for log_signup_event + # await self.event_service.log_signup_event( + # user_id=user_id, + # email=email, + # region=preferred_region, + # time_zone=time_zone, + # ) + + # TODO: Dax - Add notification for log_signup_event # This will be done by sending the notification back to Freeleaps App # await self.achievement_service.record_login_event(user_id) - if await self.profile_service.is_flid_reset_required(user_id): + if await self.user_auth_service.is_flid_reset_required(user_id): return ( UserLoginAction.REVIEW_AND_REVISE_FLID, - user_account.role, + user_account.user_role, user_id, email.split("@")[0], preferred_region, ) - flid = await self.profile_service.get_user_flid(user_id) + user_flid = await self.user_auth_service.get_user_flid(user_id) - if await self.user_service.is_password_reset_required(user_id): + if await self.user_auth_service.is_password_reset_required(user_id): return ( UserLoginAction.NEW_USER_SET_PASSWORD, - user_account.role, + user_account.user_role, user_id, - flid, + user_flid, preferred_region, ) return ( - UserLoginAction.USER_SIGNED_IN, - user_account.role, + UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, + user_account.user_role, user_id, - flid, + user_flid, preferred_region, ) else: + await self.module_logger.log_warning( + info="The auth code is invalid.", + properties={"email": email, "code": code}, + ) return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None - async def verify_email_with_code(self, email: str, code: str, host: str): - user_id = await self.user_auth_service.get_user_id_by_email(email) - if not await self.user_auth_service.verify_email_with_code(email, code): - raise InvalidAuthCodeException() + async def signin_with_email_and_password( + self, email: str, password: str + ) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]: + # check if the user account exist + user_id = await self.user_auth_service.get_user_id_by_email(email) + + # if it cannot find user account according to the email address, new user is_new_user = user_id is None - preferred_region = self.region_handler.detect_from_host(host) - return user_id, is_new_user, preferred_region + + if is_new_user: + # cannot find the email address + return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None] + else: + if await self.user_auth_service.is_password_reset_required(user_id): + # password hasn't been set before, save password for the user + return [ + UserLoginAction.NEW_USER_SET_PASSWORD, + None, + None, + None, + ] + else: + if await self.user_auth_service.verify_user_with_password( + user_id, password + ): + # TODO: Add user achievement logic in Freeleaps + # TODO: Add notification system + # user logins count + 1 + # await UserAchievement(user_id).record_user_login_event() + # for new users, log the sign up event + # await self.event_dispatcher.dispatch_event( + # receiver_id=settings.SYSTEM_USER_ID, + # subject="login", + # event="signin", + # properties={ + # "user_id": user_id, + # }, + # ) + + user_account = await self.user_management_service.get_account_by_id( + user_id=user_id + ) + if await self.user_auth_service.is_flid_reset_required(user_id): + return [ + UserLoginAction.REVIEW_AND_REVISE_FLID, + user_account.user_role, + user_id, + email.split("@")[0], + ] + + user_flid = await self.user_auth_service.get_user_flid(user_id) + + # password verification passed + return [ + UserLoginAction.USER_SIGNED_IN, + user_account.user_role, + user_id, + user_flid, + ] + else: + # ask user to input password again. + # TODO: we need to limit times of user to input the wrong password + return [ + UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, + None, + None, + None, + ] + + async def update_new_user_flid( + self, user_id: str, user_flid: str + ) -> Tuple[UserLoginAction, Optional[str]]: + if await self.user_auth_service.is_flid_available(user_flid): + + code_depot_email = "{}@freeleaps.com".format(user_flid) + result = await self.code_depot_service.create_depot_user( + user_flid, user_id, code_depot_email + ) + + if not result: + await self.module_logger.log_error( + error="Failed to create depot user for {} with flid {} and email {}".format( + user_id, user_flid, code_depot_email + ), + properties={ + "user_id": user_id, + "user_flid": user_flid, + "code_depot_email": code_depot_email, + }, + ) + return [ + UserLoginAction.REVIEW_AND_REVISE_FLID, + "{}{}".format(user_flid, random.randint(100, 999)), + ] + + await self.user_auth_service.update_flid(user_id, user_flid) + + if await self.user_auth_service.is_password_reset_required(user_id): + return [ + UserLoginAction.NEW_USER_SET_PASSWORD, + user_flid, + ] + else: + return [ + UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED, + user_flid, + ] + else: + return [ + UserLoginAction.REVIEW_AND_REVISE_FLID, + "{}{}".format(user_flid, random.randint(100, 999)), + ] + + async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction: + """try signin through email, generate auth code and send to the email address + + Args: + email (str): email address + host (str): host url that user tried to sign in + + Returns: + int: UserLoginAction + """ + user_id = await self.user_auth_service.get_user_id_by_email(email) + + is_password_reset_required = False + if user_id: + is_password_reset_required = ( + await self.user_auth_service.is_password_reset_required(user_id) + ) + + if user_id is None or is_password_reset_required: + # send auth code through email if the email address + # hasn't been associated with any account. + # Or if the user's password is empty, which means the user's pasword hasn't been set. + + mail_code = await self.user_auth_service.generate_auth_code_for_email(email) + region = RegionHandler().detect_from_host(host) + # TODO: Add notification + # async with self.notification_center: + # await self.notification_center.send_email_notification( + # receiver_id=email, + # subject="email", + # event="authentication", + # properties={"auth_code": mail_code}, + # region=region, + # ) + return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE + else: + return UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED + + async def reset_password_through_email(self, email: str, host: str) -> int: + """verify the email is exisitng, clear the existing password, + generate auth code and send to the email address + so in the following steps, the user can reset their password. + + Args: + email (str): email address + host (str): host that user will perform the reset on + + Returns: + int: UserLoginAction + """ + + user_id = await self.user_auth_service.get_user_id_by_email(email) + if user_id is not None: + # send auth code through email if the email address + # hasn been associated with any account. + mail_code = await self.user_auth_service.generate_auth_code_for_email(email) + region = RegionHandler().detect_from_host(host) + # TODO: Add notification support + # async with self.notification_center: + # await self.notification_center.send_email_notification( + # receiver_id=email, + # subject="email", + # event="authentication", + # properties={"auth_code": mail_code}, + # region=region, + # ) + + await self.user_auth_service.reset_password(user_id) + + return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE + else: + return UserLoginAction.EMAIL_NOT_ASSOCIATED_WITH_USER + + async def update_user_password(self, user_id: str, password: str) -> dict[str, any]: + error_message = """ + Password does not pass complexity requirements: + - At least one lowercase character + - At least one uppercase character + - At least one digit + - At least one special character (punctuation, brackets, quotes, etc.) + """ + if not check_password_complexity(password): + raise InvalidDataError(error_message) + + user_flid = await self.user_auth_service.get_user_flid(user_id) + await self.user_auth_service.save_password_auth_method( + user_id, user_flid, password + ) + return {"succeeded": True} diff --git a/app/authentication/backend/infra/auth/user_auth_handler.py b/app/authentication/backend/infra/auth/user_auth_handler.py index 638a74c..a6d268d 100644 --- a/app/authentication/backend/infra/auth/user_auth_handler.py +++ b/app/authentication/backend/infra/auth/user_auth_handler.py @@ -18,6 +18,8 @@ from app.authentication.backend.models.models import ( UserPasswordDoc, ) +from app.authentication.backend.models.user_profile.models import BasicProfileDoc + class UserAuthHandler: def __init__(self): @@ -286,3 +288,63 @@ class UserAuthHandler: return user_password.password == "" else: return True + + async def is_flid_reset_required(self, user_id: str) -> bool: + basic_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.user_id == user_id + ) + + if basic_profile: + return basic_profile.FLID.update_time == basic_profile.FLID.create_time + + async def is_flid_available(self, user_flid: str) -> bool: + basic_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.FLID.identity == user_flid + ) + + if basic_profile: + return False + else: + return True + + async def get_flid(self, user_id: str) -> str: + basic_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.user_id == user_id + ) + + if basic_profile: + return basic_profile.FLID.identity + else: + return None + + async def update_flid(self, user_id: str, flid: str) -> bool: + basic_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.user_id == user_id + ) + + if basic_profile: + basic_profile.FLID.identity = flid + basic_profile.FLID.update_time = datetime.now(timezone.utc) + basic_profile.FLID.set_by = user_id + await basic_profile.save() + return True + else: + return False + + async def generate_auth_code(self, email: str) -> str: + """send auth code to email address + + Args: + email (str): email address + """ + auth_code = generate_auth_code() + expiry = datetime.now(timezone.utc) + timedelta(minutes=5) + auth_code_doc = AuthCodeDoc( + auth_code=auth_code, + method=email.lower(), + method_type=AuthType.EMAIL, + expiry=expiry, + ) + + await auth_code_doc.create() + return auth_code diff --git a/app/authentication/backend/infra/code_management/depot_handler.py b/app/authentication/backend/infra/code_management/depot_handler.py index 90d1796..4513669 100644 --- a/app/authentication/backend/infra/code_management/depot_handler.py +++ b/app/authentication/backend/infra/code_management/depot_handler.py @@ -1,149 +1,24 @@ -import pytz -import random from infra.log.module_logger import ModuleLogger from typing import Optional -from app.authentication.backend.common.config.app_settings import app_settings - -from app.authentication.backend.models.constants import DepotStatus -from app.authentication.backend.infra.code_management.gitea.gitea import Gitea -from app.authentication.backend.models.gitea.models import CodeDepotDoc -from infra.exception.exceptions import InvalidOperationError -from infra.utils.date import get_sunday -from datetime import datetime -from datetime import datetime, timedelta, timezone -from dateutil.parser import parse - class CodeDepotHandler: def __init__(self) -> None: - self.gitea_url = app_settings.GITEA_URL - self.gitea_token = app_settings.GITEA_TOKEN - self.gitea_org = app_settings.GITEA_DEPOT_ORGANIZATION - - self.code_depot_domain_name = app_settings.CODE_DEPOT_DOMAIN_NAME - self.code_depot_ssh_port = app_settings.CODE_DEPOT_SSH_PORT - self.code_depot_http_port = app_settings.CODE_DEPOT_HTTP_PORT - - self.gitea_admin = Gitea( - self.gitea_url, token_text=self.gitea_token, auth=None, verify=False - ) - self.product_org = self.gitea_admin.get_org_by_name(self.gitea_org) self.module_logger = ModuleLogger(sender_id="CodeDepotManager") async def check_depot_name_availabe(self, code_depot_name: str) -> bool: - """Return True if the depot name is available, otherwise return False - - Parameters: - code_depot_name (str): the name of the code depot - - Returns: - bool: True if the depot name is availabe, otherwise return False - """ - - result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name) - - if result: - return False - else: - return True + pass def check_code_depot_exist(self, code_depot_name: str) -> bool: - """Check and return True if the code deport with the name exist in Gitea, otherwise return False - - Parameters: - code_depot_name (str): the name of the code depot - - Returns: - bool: True if the code depot exist, otherwise return False - """ - all_depots = self.product_org.get_repositories() - for depot in all_depots: - if depot.name.lower() == code_depot_name.lower(): - return True - - return False + pass async def __generate_new_code_depot_name( self, code_depot_name: str, gitea_code_depot_names: list[str] ) -> str: - """Generate a new code depot name if the code depot name already exists - - Parameters: - code_depot_name (str): the name of the code depot - - Returns: - str: the new code depot name - """ - code_depot_name = code_depot_name.lower() - depot_doc = await CodeDepotDoc.find_one( - CodeDepotDoc.depot_name == code_depot_name - ) - - # if the depot name already exists in the database or Gitea, generate a new name - if depot_doc or (code_depot_name in gitea_code_depot_names): - new_code_depot_name = "{}-{}".format( - code_depot_name, random.randint(10000, 99999) - ) - while new_code_depot_name in gitea_code_depot_names: - new_code_depot_name = "{}-{}".format( - code_depot_name, random.randint(10000, 99999) - ) - - return await self.__generate_new_code_depot_name( - new_code_depot_name, gitea_code_depot_names - ) - else: - return code_depot_name + pass async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]: - """Create a new git code depot - - Parameters: - product_id (str): the id of the product - code_depot_name (str): the name of the code depot - - Returns: - str: return code depot id if it's created successfully, else return None - """ - gitea_code_depot_names = [ - depot.name.lower() for depot in self.product_org.get_repositories() - ] - code_depot_name_to_be_created = await self.__generate_new_code_depot_name( - code_depot_name, gitea_code_depot_names - ) - - new_depot_doc = CodeDepotDoc( - depot_name=code_depot_name_to_be_created, - product_id=product_id, - depot_status=DepotStatus.TO_BE_CREATED, - ) - await new_depot_doc.create() - - try: - self.product_org.create_repo( - repoName=code_depot_name_to_be_created, - description=product_id, - private=True, - autoInit=True, - default_branch="main", - ) - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to create depot {code_depot_name_to_be_created} in Gitea. Error: {e}", - properties={ - "code_depot_name_to_be_created": code_depot_name_to_be_created - }, - ) - raise InvalidOperationError( - f"Failed to create depot {code_depot_name_to_be_created} in Gitea" - ) - - await new_depot_doc.set({CodeDepotDoc.depot_status: DepotStatus.CREATED}) - await new_depot_doc.save() - - return str(new_depot_doc.id) + pass def get_depot_ssh_url(self, code_depot_name: str) -> str: """Return the ssh url of the code depot @@ -190,227 +65,28 @@ class CodeDepotHandler: return f"http://{user_name}@{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git" async def get_depot_users(self, code_depot_name: str) -> list[str]: - """Return list of user names have permission to access the depot - - Parameters: - depot_name (str): the name of the depot - - Returns: - list: list of user names - """ - - result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name) - - if result: - return result.collaborators - else: - return [] + pass async def update_depot_user_password(self, user_name: str, password: str) -> bool: - """Update the password of the user in Gitea - - Parameters: - user_name (str): the name of the user - password (str): the new password of the user - - Returns: - bool: True if operations succeed, otherwise return False - """ - depot_user = self.gitea_admin.get_user_by_name(user_name) - - if depot_user: - try: - self.gitea_admin.update_user_password(user_name, password) - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to update password for user {user_name} in Gitea.", - properties={"user_name": user_name}, - ) - return False - else: - await self.module_logger.log_error( - error=f"User {user_name} does not exist in Gitea.", - properties={"user_name": user_name}, - ) - return False - - return True + pass async def create_depot_user( self, user_name: str, password: str, email: str ) -> bool: - """Create a new user in Gitea - Parameters: - user_name (str): the name of the user - password (str): the password of the user - email (str): email address of the user - - Returns: - bool: True if operations succeed, otherwise return False - """ - depot_user = self.gitea_admin.get_user_by_name(user_name) - - if depot_user: - await self.module_logger.log_info( - info=f"User {user_name} exist in Gitea.", - properties={"user_name": user_name}, - ) - return True - else: - try: - await self.module_logger.log_info( - info=f"Create user {user_name} in Gitea with password.", - properties={"user_name": user_name}, - ) - depot_user = self.gitea_admin.create_user( - user_name=user_name, - login_name=user_name, - password=password, - email=email, - change_pw=False, - send_notify=False, - ) - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to create user {user_name} in Gitea.", - properties={"user_name": user_name}, - ) - return False - - return True + pass async def grant_user_depot_access( self, user_name: str, code_depot_name: str ) -> bool: - """Grant user access to the code depot in Gitea - - Parameters: - user_name (str): the name of the user - code_depot_name (str): the name of the code depot - - Returns: - bool: True if operations succeed, otherwise return False - """ - try: - code_depot = self.product_org.get_repository(code_depot_name) - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to get depot {code_depot_name} in Gitea.", - properties={"code_depot_name": code_depot_name}, - ) - return False - - try: - if code_depot.add_collaborator(user_name=user_name, permission="Write"): - code_depot_doc = await CodeDepotDoc.find_one( - CodeDepotDoc.depot_name == code_depot_name - ) - - if code_depot_doc: - if user_name not in code_depot_doc.collaborators: - code_depot_doc.collaborators.append(user_name) - await code_depot_doc.save() - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to grant permission for user {user_name} to access code depot {code_depot_name}.", - properties={"user_name": user_name, "code_depot_name": code_depot_name}, - ) - return False - - return True + pass async def generate_statistic_result( self, code_depot_name: str ) -> Optional[dict[str, any]]: - """Call Gitea API and collect statistic result of the repository. - - Args: - code_depot_name (str): the name of the code depot - - Returns: - dict[str, any]: statistic result - """ - - try: - code_depot = self.product_org.get_repository(code_depot_name) - except Exception as e: - await self.module_logger.log_exception( - exception=e, - text=f"Failed to get depot {code_depot_name} in Gitea.", - properties={"code_depot_name": code_depot_name}, - ) - return None - - commits = code_depot.get_commits() - - if len(commits) > 0: - commit_dates = [] - for commit in commits: - commit_dates.append(parse(commit.created).date()) - - today = datetime.now(timezone.utc) - last_sunday = get_sunday(today) - # only get commits statistic of the last 20 weeks - last_20_sundays = [last_sunday - timedelta(weeks=i) for i in range(20)] - - weekly_commits = {} - for date in last_20_sundays: - weekly_commits[date.strftime("%Y-%m-%d")] = 0 - - for commit_date in commit_dates: - commit_date_sunday = get_sunday(commit_date).strftime("%Y-%m-%d") - if commit_date_sunday in weekly_commits: - weekly_commits[commit_date_sunday] += 1 - - last_update = parse(commits[-1].created) - results = { - "total_commits": len(commits), - "last_commiter": commits[-1].commit["committer"]["name"], - "last_update": last_update.astimezone(pytz.UTC), - "weekly_commits": weekly_commits, - } - else: - results = { - "total_commits": 0, - "last_commiter": "", - "last_update": None, - "weekly_commits": {}, - } - - return results + pass async def analyze_code_depots(self): - code_depot_docs = await CodeDepotDoc.find( - CodeDepotDoc.depot_status == DepotStatus.CREATED - ).to_list() - - for code_depot_doc in code_depot_docs: - await self.module_logger.log_info( - info="Start to analyze code depot {}".format(code_depot_doc.depot_name), - properties={"code_depot_name": code_depot_doc.depot_name}, - ) - statistic_result = await self.generate_statistic_result( - code_depot_doc.depot_name - ) - - # cannot get the statistic result, probably it cannot find the - # git repository in gitea - if not statistic_result: - continue - - code_depot_doc.total_commits = statistic_result["total_commits"] - code_depot_doc.last_commiter = statistic_result["last_commiter"] - code_depot_doc.last_update = statistic_result["last_update"] - code_depot_doc.weekly_commits = statistic_result["weekly_commits"] - await code_depot_doc.save() + pass async def fetch_code_depot(self, code_depot_id: str) -> Optional[dict[str, any]]: - code_depot_doc = await CodeDepotDoc.get(code_depot_id) - if code_depot_doc: - return code_depot_doc.model_dump() - else: - return None + pass diff --git a/app/authentication/backend/infra/user_profile/user_profile_handler.py b/app/authentication/backend/infra/user_profile/user_profile_handler.py index 6e588b1..e8227be 100644 --- a/app/authentication/backend/infra/user_profile/user_profile_handler.py +++ b/app/authentication/backend/infra/user_profile/user_profile_handler.py @@ -1,2 +1,121 @@ +from infra.models.constants import UserRegion +from datetime import datetime, timedelta, timezone +from app.authentication.backend.models.user.models import UserAccountDoc +from app.authentication.backend.models.user.constants import ( + UserAccountProperty, +) +from app.authentication.backend.models.permission.constants import ( + AdministrativeRole, + Capability, +) +from typing import Optional +from app.authentication.backend.models.user_profile.models import ( + SelfIntro, + Tags, + Photo, + Email, + Mobile, + FLID, + Password, + BasicProfileDoc, + ProviderProfileDoc, + ExpectedSalary, +) + +from app.authentication.backend.models.user.constants import UserRegionToCurrency + + class UserProfileHandler: - pass + async def create_new_user_account( + self, + property: UserAccountProperty, + capability: Capability, + user_role: AdministrativeRole, + region: UserRegion, + ) -> UserAccountDoc: + user_account = UserAccountDoc( + profile_id=None, + account_id=None, + service_plan_id=None, + properties=int(property), + capabilities=int(capability), + user_role=int(user_role), + region=region, + ) + return await user_account.create() + + async def create_basic_profile( + self, + user_id: str, + email_address: str, + email_verified: bool, + mobile_number: str, + mobile_verified: bool, + password_setup: bool, + region: UserRegion, + time_zone: Optional[str] = "UTC", + ) -> BasicProfileDoc: + basic_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.user_id == user_id + ) + if basic_profile: + return basic_profile + else: + tags = Tags(skill=[]) + self_intro = SelfIntro(summary="", content_html="", tags=tags) + photo = Photo(url="", base64="", filename="") + email = Email(address=email_address, verified=email_verified) + mobile = Mobile(number=mobile_number, verified=mobile_verified) + current_time = datetime.now(timezone.utc) + flid = FLID( + identity=user_id, + set_by=user_id, + create_time=current_time, + update_time=current_time, + ) + password = Password( + set_up=password_setup, + update_time=current_time, + expiry=(current_time + timedelta(days=365)), + ) + basic_profile = BasicProfileDoc( + user_id=user_id, + self_intro=self_intro, + photo=photo, + email=email, + mobile=mobile, + FLID=flid, + password=password, + region=region, + time_zone=time_zone, + ) + new_basic_profile = await basic_profile.create() + return new_basic_profile + + async def create_provider_profile(self, user_id: str) -> ProviderProfileDoc: + provider_profile = await ProviderProfileDoc.find_one( + ProviderProfileDoc.user_id == user_id + ) + if provider_profile: + return provider_profile + else: + region = await self.__get_user_region(user_id) + expected_salary = ExpectedSalary( + currency=UserRegionToCurrency[region], hourly=0.0 + ) + provider_profile = ProviderProfileDoc( + user_id=user_id, + expected_salary=expected_salary, + accepting_request=False, + ) + new_provider_profile = await provider_profile.create() + return new_provider_profile + + async def get_account_by_id(self, user_id: str) -> UserAccountDoc: + return await UserAccountDoc.get(user_id) + + async def __get_user_region(self, user_id: str) -> UserRegion: + user_profile = await BasicProfileDoc.find_one( + BasicProfileDoc.user_id == user_id + ) + return user_profile.region if user_profile else UserRegion.OTHER diff --git a/app/authentication/backend/models/user/constants.py b/app/authentication/backend/models/user/constants.py index 9a7f9dd..8e07233 100644 --- a/app/authentication/backend/models/user/constants.py +++ b/app/authentication/backend/models/user/constants.py @@ -1,4 +1,5 @@ from enum import IntEnum +from infra.models.constants import UserRegion class NewUserMethod(IntEnum): @@ -22,3 +23,15 @@ class UserLoginAction(IntEnum): EMAIL_NOT_ASSOCIATED_WITH_USER = 3 REVIEW_AND_REVISE_FLID = 4 USER_SIGNED_IN = 100 + + +class Currency(IntEnum): + UNKNOWN = 0 + USD = 1 + CNY = 2 + + +UserRegionToCurrency = { + UserRegion.ZH_CN: Currency.CNY.name, + UserRegion.OTHER: Currency.USD.name, +} diff --git a/app/authentication/backend/models/user/models.py b/app/authentication/backend/models/user/models.py index e5b7657..33a4f8a 100644 --- a/app/authentication/backend/models/user/models.py +++ b/app/authentication/backend/models/user/models.py @@ -3,7 +3,10 @@ from typing import Optional from beanie import Document from .constants import UserAccountProperty -from .permission.constants import AdministrativeRole, Capability +from app.authentication.backend.models.permission.constants import ( + AdministrativeRole, + Capability, +) from infra.models.constants import UserRegion diff --git a/app/authentication/backend/models/user_profile/models.py b/app/authentication/backend/models/user_profile/models.py new file mode 100644 index 0000000..5fb19f3 --- /dev/null +++ b/app/authentication/backend/models/user_profile/models.py @@ -0,0 +1,103 @@ +from datetime import datetime +from typing import List, Optional +from beanie import Document, Indexed +from pydantic import BaseModel, EmailStr +import re + +from decimal import Decimal +from infra.models.constants import UserRegion + + +class Tags(BaseModel): + skill: List[str] + + +class SelfIntro(BaseModel): + summary: str = "" + content_html: str = "" + tags: Tags + + +class Photo(BaseModel): + url: Optional[str] + base64: str + filename: str + + +class Email(BaseModel): + address: Optional[EmailStr] + verified: bool = False + + +class Mobile(BaseModel): + number: Optional[str] + verified: bool + + +class FLID(BaseModel): + identity: str + set_by: str + create_time: datetime + update_time: datetime + + +class Password(BaseModel): + set_up: bool + update_time: datetime + expiry: datetime + + +class BasicProfileDoc(Document): + user_id: str + first_name: Indexed(str) = "" # type: ignore + last_name: Indexed(str) = "" # type: ignore Index for faster search + spoken_language: List[str] = [] + self_intro: SelfIntro + photo: Photo + email: Email + mobile: Mobile + FLID: FLID + password: Password + region: int = UserRegion.OTHER + time_zone: Optional[str] = None + + class Settings: + name = "basic_profile" + indexes = [ + "user_id", # Add index for fast querying by user_id + "email.address", # This adds an index for the 'email.address' field + # Compound text index for fuzzy search across multiple fields + [("first_name", "text"), ("last_name", "text"), ("email.address", "text")], + ] + + @classmethod + async def fuzzy_search(cls, query: str) -> List["BasicProfileDoc"]: + # Create a case-insensitive regex pattern for partial matching + regex = re.compile(f".*{query}.*", re.IGNORECASE) + + # Perform a search on first_name, last_name, and email fields using $or + results = await cls.find( + { + "$or": [ + {"first_name": {"$regex": regex}}, + {"last_name": {"$regex": regex}}, + {"email.address": {"$regex": regex}}, + ] + } + ).to_list() + + return results + + +class ExpectedSalary(BaseModel): + currency: str = "USD" + hourly: Decimal = 0.0 + + +class ProviderProfileDoc(Document): + user_id: str + expected_salary: ExpectedSalary + accepting_request: bool = False + + class Settings: + name = "provider_profile" diff --git a/app/authentication/backend/services/auth/user_auth_service.py b/app/authentication/backend/services/auth/user_auth_service.py index 37067f0..17ac7f3 100644 --- a/app/authentication/backend/services/auth/user_auth_service.py +++ b/app/authentication/backend/services/auth/user_auth_service.py @@ -1,30 +1,46 @@ -from app.authentication.backend.infra.user_management.user_auth_handler import ( - UserAuthManager, +from app.authentication.backend.infra.auth.user_auth_handler import ( + UserAuthHandler, ) from typing import Optional class UserAuthService: def __init__(self): - self.user_auth_manager = UserAuthManager() + self.user_auth_handler = UserAuthHandler() async def get_user_id_by_email(self, email: str) -> Optional[str]: - return await self.user_auth_manager.get_user_id_by_email(email) + return await self.user_auth_handler.get_user_id_by_email(email) - async def verify_email_code(self, email: str, code: str) -> bool: - return await self.user_auth_manager.verify_email_code(email, code) - - async def create_new_user_account(self, method: str, region: str) -> str: - return await self.user_auth_manager.create_user_account(method, region) - - async def initialize_new_user_data( - self, user_id: str, email: str, region: str, time_zone: str - ): - # Initialize user data - await self.user_auth_manager.init_user_data(user_id, email, region, time_zone) - - async def get_user_account(self, user_id: str): - return await self.user_auth_manager.get_user_account(user_id) + async def verify_email_with_code(self, email: str, code: str) -> bool: + return await self.user_auth_handler.verify_email_code(email, code) async def is_password_reset_required(self, user_id: str) -> bool: - return await self.user_auth_manager.is_password_reset_required(user_id) + return await self.user_auth_handler.is_password_reset_required(user_id) + + async def is_flid_reset_required(self, user_id: str) -> bool: + return await self.user_auth_handler.is_flid_reset_required(user_id) + + async def is_flid_available(self, user_flid: str) -> bool: + return await self.user_auth_handler.is_flid_available(user_flid) + + async def get_user_flid(self, user_id: str) -> str: + return await self.user_auth_handler.get_flid(user_id) + + async def update_flid(self, user_id: str, user_flid: str) -> str: + return await self.user_auth_handler.update_flid(user_id, user_flid) + + async def generate_auth_code_for_email(self, email: str) -> str: + return await self.user_auth_handler.generate_auth_code(email) + + async def verify_user_with_password(self, user_id: str, password: str) -> bool: + return await self.user_auth_handler.verify_user_with_password(user_id, password) + + async def reset_password(self, user_id: str): + return await self.user_auth_handler.reset_password(user_id) + + async def save_password_auth_method( + self, user_id: str, user_flid: str, password: str + ): + return await self.user_auth_handler.save_password_auth_method( + user_id, user_flid, password + ) diff --git a/app/authentication/backend/services/code_depot/code_depot_service.py b/app/authentication/backend/services/code_depot/code_depot_service.py new file mode 100644 index 0000000..355cfe6 --- /dev/null +++ b/app/authentication/backend/services/code_depot/code_depot_service.py @@ -0,0 +1,53 @@ +from typing import List, Dict, Optional + + +class CodeDepotService: + def __init__(self) -> None: + pass + + async def check_depot_name_availabe(self, code_depot_name: str) -> bool: + pass + + def check_code_depot_exist(self, code_depot_name: str) -> bool: + pass + + async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]: + pass + + def get_depot_ssh_url(self, code_depot_name: str) -> str: + pass + + def get_depot_http_url(self, code_depot_name: str) -> str: + pass + + def get_depot_http_url_with_user_name( + self, code_depot_name: str, user_name: str + ) -> str: + pass + + async def get_depot_users(self, code_depot_name: str) -> List[str]: + pass + + async def update_depot_user_password(self, user_name: str, password: str) -> bool: + pass + + async def create_depot_user( + self, user_name: str, password: str, email: str + ) -> bool: + pass + + async def grant_user_depot_access( + self, user_name: str, code_depot_name: str + ) -> bool: + pass + + async def generate_statistic_result( + self, code_depot_name: str + ) -> Optional[Dict[str, any]]: + pass + + async def analyze_code_depots(self): + pass + + async def fetch_code_depot(self, code_depot_id: str) -> Optional[Dict[str, any]]: + pass diff --git a/app/authentication/backend/services/user/user_management_service.py b/app/authentication/backend/services/user/user_management_service.py index 1117df3..c0b58aa 100644 --- a/app/authentication/backend/services/user/user_management_service.py +++ b/app/authentication/backend/services/user/user_management_service.py @@ -1,35 +1,23 @@ -import random -from typing import Optional, Dict, Tuple, List from infra.log.module_logger import ModuleLogger +from typing import Optional -from app.authentication.backend.application.models.user.constants import ( +from app.authentication.backend.models.user.constants import ( NewUserMethod, UserAccountProperty, - UserLoginAction, ) -from app.authentication.backend.application.user.models import UserAccountDoc -from app.authentication.backend.business.permission.constants import ( +from app.authentication.backend.models.user.models import UserAccountDoc +from app.authentication.backend.models.permission.constants import ( AdministrativeRole, Capability, ) from app.authentication.backend.infra.auth.user_auth_handler import ( UserAuthHandler, ) -from app.authentication.backend.infra.user.user_profile_handler import ( +from app.authentication.backend.infra.user_profile.user_profile_handler import ( UserProfileHandler, ) -from backend.infra.config.backend import settings -from backend.infra.log.log_utils import log_entry_exit_async -from backend.business.credit.user import UserAchievement -from backend.services.profile.basic import BasicProfileStore -from backend.services.profile.provider import ProviderProfileStore -from backend.services.common.constants import UserRegion -from backend.services.common.region import RegionHandler -from backend.infra.depot.depot_manager import CodeDepotManager -from backend.infra.utils.string import check_password_complexity -from backend.business.notification.notification_center import NotificationCenter -from backend.business.events.user_event_dispatcher import UserEventDispatcher -from backend.infra.exception.exceptions import InvalidDataError +from infra.log.log_utils import log_entry_exit_async +from infra.models.constants import UserRegion class UserManagementService: @@ -41,7 +29,7 @@ class UserManagementService: @log_entry_exit_async async def create_new_user_account( self, method: NewUserMethod, region: UserRegion - ) -> str: + ) -> UserAccountDoc: """create a new user account document in DB Args: @@ -52,29 +40,60 @@ class UserManagementService: str: id of user account """ if NewUserMethod.EMAIL == method: - user_account = UserAccountDoc( - profile_id=None, - account_id=None, - service_plan_id=None, - properties=int(UserAccountProperty.EMAIL_VERIFIED), - capabilities=int(Capability.VISITOR), - user_role=int(AdministrativeRole.PERSONAL), - region=region, + user_account = await self.user_profile_handler.create_new_user_account( + UserAccountProperty.EMAIL_VERIFIED, + Capability.VISITOR, + AdministrativeRole.PERSONAL, + region, ) - user_account = await user_account.create() elif NewUserMethod.MOBILE == method: - user_account = UserAccountDoc( - profile_id=None, - account_id=None, - service_plan_id=None, - properties=int(UserAccountProperty.MOBILE_VERIFIED), - capabilities=int(Capability.VISITOR), - user_role=int(AdministrativeRole.PERSONAL), - region=region, + user_account = await self.user_profile_handler.create_new_user_account( + UserAccountProperty.EMAIL_VERIFIED, + Capability.VISITOR, + AdministrativeRole.PERSONAL, + region, ) - user_account = await user_account.create() # Create other doc in collections for the new user - await UserAchievement(str(user_account.id)).create_activeness_achievement() - return str(user_account.id) + # TODO: Should convert to notification + # await UserAchievement(str(user_account.id)).create_activeness_achievement() + return user_account + + async def initialize_new_user_data( + self, + user_id: str, + method: NewUserMethod, + email_address: str = None, + mobile_number: str = None, + region: UserRegion = UserRegion.ZH_CN, + time_zone: Optional[str] = "UTC", + ): + """Init data for the new user + + Args: + user_id (str): user id + method (NewUserMethod): the method the new user came from + + Returns: + result: True if initilize data for the new user successfully, else return False + """ + + # create basic and provider profile doc for the new user + if NewUserMethod.EMAIL == method: + await self.user_profile_handler.create_basic_profile( + user_id, email_address, True, None, False, False, region, time_zone + ) + await self.user_auth_handler.save_email_auth_method(user_id, email_address) + elif NewUserMethod.MOBILE == method: + await self.user_profile_handler.create_basic_profile( + user_id, None, False, mobile_number, True, False, region, time_zone + ) + else: + return False + + await self.user_profile_handler.create_provider_profile(user_id) + return True + + async def get_account_by_id(self, user_id: str) -> UserAccountDoc: + return await self.user_profile_handler.get_account_by_id(user_id) diff --git a/app/authentication/requirements.txt b/app/authentication/requirements.txt new file mode 100644 index 0000000..4cb839e --- /dev/null +++ b/app/authentication/requirements.txt @@ -0,0 +1,13 @@ +fastapi==0.114.0 +fastapi-mail==1.4.1 +fastapi-jwt==0.2.0 +pika==1.3.2 +pydantic==2.9.2 +loguru==0.7.2 +uvicorn==0.23.2 +beanie==1.21.0 +jieba==0.42.1 +aio-pika +pydantic-settings +python-jose +passlib[bcrypt] \ No newline at end of file diff --git a/app/authentication/webapi/bootstrap/application.py b/app/authentication/webapi/bootstrap/application.py index 7a38aa7..6fdd4d0 100644 --- a/app/authentication/webapi/bootstrap/application.py +++ b/app/authentication/webapi/bootstrap/application.py @@ -2,12 +2,12 @@ import logging from fastapi import FastAPI from fastapi.openapi.utils import get_openapi -from webapi.providers import common -from webapi.providers import logger -from webapi.providers import router -from webapi.providers import database -from webapi.providers import scheduler -from webapi.providers import exception_handler +from app.authentication.webapi.providers import common +from app.authentication.webapi.providers import logger +from app.authentication.webapi.providers import router +from app.authentication.webapi.providers import database +from app.authentication.webapi.providers import scheduler +from app.authentication.webapi.providers import exception_handler from .freeleaps_app import FreeleapsApp @@ -49,11 +49,7 @@ def customize_openapi_security(app: FastAPI) -> None: # Add security scheme to components openapi_schema["components"]["securitySchemes"] = { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" - } + "bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} } # Add security requirement globally diff --git a/app/authentication/webapi/main.py b/app/authentication/webapi/main.py index df0c604..53d716a 100755 --- a/app/authentication/webapi/main.py +++ b/app/authentication/webapi/main.py @@ -1,15 +1,14 @@ -from webapi.bootstrap.application import create_app -from webapi.config.site_settings import site_settings +from app.authentication.webapi.bootstrap.application import create_app +from app.authentication.webapi.config.site_settings import site_settings from fastapi.responses import RedirectResponse from fastapi.middleware.cors import CORSMiddleware -from strawberry.fastapi import GraphQLRouter -from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler import uvicorn from typing import Any app = create_app() + @app.get("/", status_code=301) async def root(): """ @@ -19,12 +18,16 @@ async def root(): if __name__ == "__main__": - uvicorn.run(app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT) + uvicorn.run( + app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT + ) + def get_context() -> Any: # Define your context function. This is where you can set up authentication, database connections, etc. return {} + def get_root_value() -> Any: # Define your root value function. This can be used to customize the root value for GraphQL operations. - return {} \ No newline at end of file + return {} diff --git a/app/authentication/webapi/providers/common.py b/app/authentication/webapi/providers/common.py index 1dd849f..f1eed99 100644 --- a/app/authentication/webapi/providers/common.py +++ b/app/authentication/webapi/providers/common.py @@ -1,5 +1,5 @@ from fastapi.middleware.cors import CORSMiddleware -from webapi.config.site_settings import site_settings +from app.authentication.webapi.config.site_settings import site_settings def register(app): diff --git a/app/authentication/webapi/providers/logger.py b/app/authentication/webapi/providers/logger.py index 4a3f1e7..0b37c27 100644 --- a/app/authentication/webapi/providers/logger.py +++ b/app/authentication/webapi/providers/logger.py @@ -1,7 +1,7 @@ import logging import sys from loguru import logger -from common.config.log_settings import log_settings +from infra.config.log_settings import log_settings def register(app=None): @@ -21,15 +21,8 @@ def register(app=None): logging.getLogger(name).propagate = True # configure loguru - logger.add( - sink=sys.stdout - ) - logger.add( - sink=file_path, - level=level, - retention=retention, - rotation=rotation - ) + logger.add(sink=sys.stdout) + logger.add(sink=file_path, level=level, retention=retention, rotation=rotation) logger.disable("pika.adapters") logger.disable("pika.connection") diff --git a/app/authentication/webapi/providers/router.py b/app/authentication/webapi/providers/router.py index 3ad11ae..2dabd7e 100644 --- a/app/authentication/webapi/providers/router.py +++ b/app/authentication/webapi/providers/router.py @@ -1,4 +1,4 @@ -from webapi.routes import api_router +from app.authentication.webapi.routes import api_router from starlette import routing diff --git a/app/authentication/webapi/routes/__init__.py b/app/authentication/webapi/routes/__init__.py index 3237813..4d3baaa 100644 --- a/app/authentication/webapi/routes/__init__.py +++ b/app/authentication/webapi/routes/__init__.py @@ -1,5 +1,8 @@ from fastapi import APIRouter +from .signin import router as signin_router +from .tokens import router as token_router api_router = APIRouter() - +api_router.include_router(signin_router, tags=["user"]) +api_router.include_router(token_router, tags=["auth"]) websocket_router = APIRouter() diff --git a/app/authentication/webapi/routes/signin/__init__.py b/app/authentication/webapi/routes/signin/__init__.py index 1e4e78e..6cee2d0 100644 --- a/app/authentication/webapi/routes/signin/__init__.py +++ b/app/authentication/webapi/routes/signin/__init__.py @@ -1,12 +1,12 @@ from fastapi import APIRouter from .try_signin_with_email import router as ts_router +from .signin_with_email_and_password import router as se_router from .signin_with_email_and_code import router as sw_router from .update_user_password import router as up_router -from .signin_with_email_and_password import router as se_router -from .sign_out import router as so_router +from .update_new_user_flid import router as uu_router from .reset_password_through_email import router as rp_router -from .refresh_token import router as rt_router -from .update_user_flid import router as uu_router +from .sign_out import router as so_router + router = APIRouter(prefix="/signin") @@ -17,4 +17,3 @@ router.include_router(se_router, tags=["signin"]) router.include_router(so_router, tags=["signin"]) router.include_router(rp_router, tags=["signin"]) router.include_router(uu_router, tags=["signin"]) -router.include_router(rt_router, tags=["signin"]) diff --git a/app/authentication/webapi/routes/signin/reset_password_through_email.py b/app/authentication/webapi/routes/signin/reset_password_through_email.py index d464fbe..15a15ba 100644 --- a/app/authentication/webapi/routes/signin/reset_password_through_email.py +++ b/app/authentication/webapi/routes/signin/reset_password_through_email.py @@ -1,4 +1,4 @@ -from backend.application.user.user_manager import UserManager +from app.authentication.backend.application.signin_hub import SignInHub from pydantic import BaseModel from fastapi import APIRouter from fastapi.encoders import jsonable_encoder @@ -32,6 +32,6 @@ class UserSignWithEmailResponse(BaseModel): async def reset_password_through_email( item: UserSignWithEmailBody, ): - result = await UserManager().reset_password_through_email(item.email, item.host) + result = await SignInHub().reset_password_through_email(item.email, item.host) result = {"action": result} return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/authentication/webapi/routes/signin/sign_out.py b/app/authentication/webapi/routes/signin/sign_out.py new file mode 100644 index 0000000..2a4a32d --- /dev/null +++ b/app/authentication/webapi/routes/signin/sign_out.py @@ -0,0 +1,40 @@ +from app.authentication.backend.application.signin_hub import SignInHub +from pydantic import BaseModel +from fastapi import APIRouter +from infra.token.token_manager import TokenManager +from fastapi import APIRouter, Depends +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED + +router = APIRouter() +token_manager = TokenManager() +# Web API +# sign_out +# + + +class RequestIn(BaseModel): + identity: str + + +@router.post( + "/sign-out", + operation_id="sign-out", + summary="sign out a logged in user", + description="sign out a logged in user", + response_description="none", +) +async def sign_out( + item: RequestIn, + current_user: dict = Depends(token_manager.get_current_user), +): + user_id = current_user.get("id") + + if not user_id: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" + ) + result = await SignInHub().sign_out(user_id) + return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/authentication/webapi/routes/signin/signin_with_email_and_code.py b/app/authentication/webapi/routes/signin/signin_with_email_and_code.py index 1cf5c8e..a8400b0 100644 --- a/app/authentication/webapi/routes/signin/signin_with_email_and_code.py +++ b/app/authentication/webapi/routes/signin/signin_with_email_and_code.py @@ -7,10 +7,12 @@ from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from pydantic import BaseModel -from backend.application.user.user_manager import UserManager -from backend.infra.authentication.auth import access_security +from app.authentication.backend.application.signin_hub import SignInHub +from infra.token.token_manager import TokenManager + router = APIRouter() +token_manager = TokenManager() # Web API # signin-with-email-n-code @@ -56,7 +58,7 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut: identity, flid, preferred_region, - ) = await UserManager().signin_with_email_and_code( + ) = await SignInHub().signin_with_email_and_code( item.email, item.code, item.host, item.time_zone ) @@ -66,9 +68,11 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut: if signed_in and identity and adminstrative_role: subject = {"id": identity, "role": adminstrative_role} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) - expires_in = datetime.now(timezone.utc) + access_security.access_expires_delta + access_token = token_manager.create_access_token(subject=subject) + refresh_token = token_manager.create_refresh_token(subject=subject) + expires_in = ( + datetime.now(timezone.utc) + token_manager.access_token_expire_minutes + ) else: access_token = None refresh_token = None diff --git a/app/authentication/webapi/routes/signin/signin_with_email_and_password.py b/app/authentication/webapi/routes/signin/signin_with_email_and_password.py index 7df183f..41c48e1 100644 --- a/app/authentication/webapi/routes/signin/signin_with_email_and_password.py +++ b/app/authentication/webapi/routes/signin/signin_with_email_and_password.py @@ -1,27 +1,28 @@ import logging -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from typing import Optional +from pydantic import BaseModel from fastapi import APIRouter from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from pydantic import BaseModel +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED -from backend.application.user.user_manager import UserManager +from app.authentication.backend.application.signin_hub import SignInHub from infra.token.token_manager import TokenManager router = APIRouter() token_manager = TokenManager() + # Web API -# signin-with-email-n-code +# signin-with-email-n-password # class RequestIn(BaseModel): email: str - code: str - host: str - time_zone: Optional[str] = "UTC" + password: str class ResponseOut(BaseModel): @@ -37,38 +38,38 @@ class ResponseOut(BaseModel): expires_in: Optional[datetime] = None # the system assigned role of the user. role: Optional[int] = None - # preferred region for user - preferred_region: Optional[str] = None + # the flid of the user + flid: Optional[str] = None @router.post( - "/signin-with-email-and-code", - operation_id="user-signin-with-email-and-code", - summary="try to signin with email and authentication code", - description="client user is trying to sign in with their email and the authenication code \ - the system sent to the email in previous step.", + "/signin-with-email-and-password", + operation_id="user-signin-with-email-and-password", + summary="try to signin with email and password", + description="client user is trying to sign in with their email and the password .", response_model=ResponseOut, ) -async def signin_with_email_and_code(item: RequestIn) -> ResponseOut: +async def signin_with_email_and_password( + item: RequestIn, +) -> ResponseOut: ( signed_in, adminstrative_role, identity, flid, - preferred_region, - ) = await UserManager().signin_with_email_and_code( - item.email, item.code, item.host, item.time_zone - ) + ) = await SignInHub().signin_with_email_and_password(item.email, item.password) logging.debug( f"signedin={signed_in}, adminstrative_role={adminstrative_role}, identity={identity}" ) - if signed_in and identity and adminstrative_role: + if signed_in and adminstrative_role and identity: subject = {"id": identity, "role": adminstrative_role} access_token = token_manager.create_access_token(subject=subject) refresh_token = token_manager.create_refresh_token(subject=subject) - expires_in = datetime.now(timezone.utc) + timedelta(minutes=30) + expires_in = ( + datetime.now(timezone.utc) + token_manager.access_token_expire_minutes + ) else: access_token = None refresh_token = None @@ -82,6 +83,5 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut: "expires_in": expires_in, "role": adminstrative_role, "flid": flid, - "preferred_region": preferred_region, } return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/authentication/webapi/routes/signin/try_signin_with_email.py b/app/authentication/webapi/routes/signin/try_signin_with_email.py index e1e3a29..5db70c4 100644 --- a/app/authentication/webapi/routes/signin/try_signin_with_email.py +++ b/app/authentication/webapi/routes/signin/try_signin_with_email.py @@ -1,31 +1,38 @@ -from fastapi import APIRouter, Security +from app.authentication.backend.application.signin_hub import SignInHub +from pydantic import BaseModel +from fastapi import APIRouter from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from fastapi_jwt import JwtAuthorizationCredentials -from backend.infra.authentication.auth import access_security -from backend.application.user.user_manager import ( - UserManager, -) # Assuming UserManager handles user-related queries - router = APIRouter() +# Web API +# try_signin_with_email +# -@router.get( - "/get-latest-login-by-user-id/{user_id}", - operation_id="get-latest-login-by-user-id", - summary="Get the latest login timestamp for a given user", - description="Fetches the latest login timestamp for a specific user by user_id.", - response_description="Returns the latest login timestamp in Unix time seconds, or null if no login found", + +class UserSignWithEmailBody(BaseModel): + email: str + host: str + + +class UserSignWithEmailResponse(BaseModel): + signin_type: int + + +@router.post( + "/try-signin-with-email", + operation_id="user-try-signin-with-email", + summary="try to signin with email", + description="A client user is trying to sign in with their email. \ + The system will determine to send an authentication code to the email \ + or let the uesr use their FLID and passward to sign in", + response_description="signin_type:0 meaning simplified(using email) signin, \ + 1 meaning standard(using FLID and passward) signin", ) -async def get_latest_login_by_user_id( - user_id: str, - credentials: JwtAuthorizationCredentials = Security(access_security), +async def try_signin_with_email( + item: UserSignWithEmailBody, ): - # Assume UserManager is responsible for handling user data - result = await UserManager().fetch_latest_login(user_id) - - if result is None: - return JSONResponse(content=jsonable_encoder({"timestamp": None})) - - return JSONResponse(content=jsonable_encoder({"timestamp": result})) + result = await SignInHub().try_signin_with_email(item.email, item.host) + result = {"signin_type": result} + return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/authentication/webapi/routes/signin/update_new_user_flid.py b/app/authentication/webapi/routes/signin/update_new_user_flid.py new file mode 100644 index 0000000..4c56c61 --- /dev/null +++ b/app/authentication/webapi/routes/signin/update_new_user_flid.py @@ -0,0 +1,50 @@ +from app.authentication.backend.application.signin_hub import SignInHub +from pydantic import BaseModel +from fastapi import APIRouter +from infra.token.token_manager import TokenManager +from fastapi import APIRouter, Depends +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED + +router = APIRouter() +token_manager = TokenManager() +# Web API +# update_user_flid +# + + +class RequestIn(BaseModel): + flid: str + + +@router.post( + "/update-new-user-flid", + operation_id="user_update_new_user_password", + summary="update the new user's freeleaps user id", + description="Freeleaps user ID(FLID) is a unique identifier to be used in git and other service across the platform", + response_description="signin result to indicate the next step for the client", +) +async def update_new_user_flid( + item: RequestIn, + current_user: dict = Depends(token_manager.get_current_user), +): + user_id = current_user.get("id") + + if not user_id: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" + ) + + ( + signed_in, + flid, + ) = await SignInHub().update_new_user_flid(user_id, item.flid) + + result = { + "signin_result": signed_in, + "flid": flid, + } + + return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/authentication/webapi/routes/signin/update_user_password.py b/app/authentication/webapi/routes/signin/update_user_password.py index 234f6d4..c7587aa 100644 --- a/app/authentication/webapi/routes/signin/update_user_password.py +++ b/app/authentication/webapi/routes/signin/update_user_password.py @@ -1,13 +1,15 @@ -from backend.application.user.user_manager import UserManager +from app.authentication.backend.application.signin_hub import SignInHub from pydantic import BaseModel -from fastapi import APIRouter, Security -from backend.infra.authentication.auth import access_security -from fastapi_jwt import JwtAuthorizationCredentials +from fastapi import APIRouter +from infra.token.token_manager import TokenManager +from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse +from fastapi import Depends, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED router = APIRouter() - +token_manager = TokenManager() # Web API # update_user_password # @@ -28,9 +30,14 @@ class RequestIn(BaseModel): ) async def update_user_password( item: RequestIn, - credentials: JwtAuthorizationCredentials = Security(access_security), + current_user: dict = Depends(token_manager.get_current_user), ): - user_id = credentials["id"] + user_id = current_user.get("id") + + if not user_id: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials" + ) if item.password != item.password2: return JSONResponse( content=jsonable_encoder( @@ -38,5 +45,5 @@ async def update_user_password( ) ) else: - result = await UserManager().update_user_password(user_id, item.password) + result = await SignInHub().update_user_password(user_id, item.password) return JSONResponse(content=jsonable_encoder(result)) diff --git a/app/central_storage/Dockerfile b/app/central_storage/Dockerfile index 9aeaf6c..4aa5175 100644 --- a/app/central_storage/Dockerfile +++ b/app/central_storage/Dockerfile @@ -16,4 +16,4 @@ EXPOSE 8005 # Run the application using the start script -CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0"] +CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0", "--log-level", "warning"] diff --git a/app/central_storage/common/config/log_settings.py b/app/central_storage/common/config/log_settings.py deleted file mode 100644 index 856281d..0000000 --- a/app/central_storage/common/config/log_settings.py +++ /dev/null @@ -1,17 +0,0 @@ - - -class LogSettings(): - LOG_LEVEL: str = "DEBUG" - LOG_PATH_BASE: str = ( - "./logs" - ) - LOG_PATH: str = LOG_PATH_BASE + '/' + "app" + '.log' - LOG_RETENTION: str = "14 days" - LOG_ROTATION: str = "00:00" # mid night - - class Config: - env_file = ".log.env" - env_file_encoding = "utf-8" - - -log_settings = LogSettings() diff --git a/app/central_storage/webapi/providers/logger.py b/app/central_storage/webapi/providers/logger.py index c53e52d..0b37c27 100644 --- a/app/central_storage/webapi/providers/logger.py +++ b/app/central_storage/webapi/providers/logger.py @@ -1,7 +1,7 @@ import logging import sys from loguru import logger -from app.central_storage.common.config.log_settings import log_settings +from infra.config.log_settings import log_settings def register(app=None): diff --git a/docker-compose.yaml b/docker-compose.yaml index 8bf33ab..f051203 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,6 +29,20 @@ services: volumes: - .:/app # Mount the current directory to /app in the container + authentication: + build: + context: app/authentication + dockerfile: Dockerfile + restart: always + ports: + - "8004:8004" # Map the central_storage service port + networks: + - freeleaps_service_hub_network + env_file: + - sites/authentication/.env + volumes: + - .:/app # Mount the current directory to /app in the container + networks: freeleaps_service_hub_network: driver: bridge diff --git a/infra/config/app_settings.py b/infra/config/app_settings.py index d61e3f6..8c72da7 100644 --- a/infra/config/app_settings.py +++ b/infra/config/app_settings.py @@ -3,6 +3,9 @@ from pydantic_settings import BaseSettings class AppSettings(BaseSettings): JWT_SECRET_KEY: str = "" + APPLICATION_ACTIVITY_LOG: str = "application-log" + USER_ACTIVITY_LOG: str = "user-activity-log" + BUSINESS_METRIC_LOG: str = "business-metric-log" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 REFRESH_TOKEN_EXPIRE_DAYS: int = 30 MONGODB_NAME: str = "freeleaps2" diff --git a/infra/log/application_logger.py b/infra/log/application_logger.py index c1222bb..3c13404 100644 --- a/infra/log/application_logger.py +++ b/infra/log/application_logger.py @@ -1,5 +1,5 @@ from .base_logger import LoggerBase -from common.config.app_settings import app_settings +from infra.config.app_settings import app_settings import json diff --git a/infra/log/base_logger.py b/infra/log/base_logger.py index 122dfdb..b919158 100644 --- a/infra/log/base_logger.py +++ b/infra/log/base_logger.py @@ -1,5 +1,5 @@ from loguru import logger as guru_logger -from common.config.log_settings import log_settings +from infra.config.log_settings import log_settings from typing import List import socket import json @@ -11,9 +11,7 @@ class LoggerBase: binded_loggers = {} logger_lock = threading.Lock() - def __init__( - self, logger_name: str, extra_fileds: dict[str, any] - ) -> None: + def __init__(self, logger_name: str, extra_fileds: dict[str, any]) -> None: self.__logger_name = logger_name self.extra_fileds = extra_fileds with LoggerBase.logger_lock: @@ -21,9 +19,7 @@ class LoggerBase: self.logger = LoggerBase.binded_loggers[self.__logger_name] return - log_filename = ( - log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log" - ) + log_filename = log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log" log_retention = log_settings.LOG_RETENTION log_rotation = log_settings.LOG_ROTATION log_level = "INFO" @@ -57,14 +53,14 @@ class LoggerBase: subject: str, event: str, properties: dict[str, any], - text: str = "" + text: str = "", ) -> None: local_logger = self.logger.bind( sender_id=sender_id, receiver_id=receiver_id, subject=subject, event=event, - properties=properties + properties=properties, ) local_logger.info(text) @@ -83,7 +79,7 @@ class LoggerBase: subject=subject, event="exception", properties=properties, - exception=exception + exception=exception, ) local_logger.exception(text) @@ -103,7 +99,7 @@ class LoggerBase: properties=properties, ) local_logger.info(text) - + async def log_warning( self, sender_id: str, @@ -120,7 +116,7 @@ class LoggerBase: properties=properties, ) local_logger.warning(text) - + async def log_error( self, sender_id: str, diff --git a/infra/token/token_manager.py b/infra/token/token_manager.py index 2b54958..8a74532 100644 --- a/infra/token/token_manager.py +++ b/infra/token/token_manager.py @@ -2,6 +2,9 @@ from datetime import datetime, timedelta, timezone from typing import Dict from jose import jwt, JWTError from infra.config.app_settings import app_settings +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from starlette.status import HTTP_401_UNAUTHORIZED class TokenManager: @@ -61,3 +64,17 @@ class TokenManager: return self.create_access_token(subject) else: raise ValueError("Invalid refresh token") + + async def get_current_user( + self, token: str = Depends(OAuth2PasswordBearer(tokenUrl="token")) + ) -> Dict: + """ + Extract and validate user information from the JWT token. + """ + try: + payload = self.decode_token(token) # Decode JWT token + return payload + except ValueError: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or expired token" + ) diff --git a/sites/authentication/.env b/sites/authentication/.env new file mode 100644 index 0000000..d8a6620 --- /dev/null +++ b/sites/authentication/.env @@ -0,0 +1 @@ +export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ== \ No newline at end of file