Add all sign-in and token management APIs, localhost up, need clean-up and tuning Gitea APIs, Notification center
This commit is contained in:
parent
e118af5d53
commit
26eedb1b89
19
app/authentication/Dockerfile
Normal file
19
app/authentication/Dockerfile
Normal file
@ -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"]
|
||||||
@ -1,29 +1,17 @@
|
|||||||
from app.authentication.backend.models.constants import (
|
|
||||||
NewUserMethod,
|
|
||||||
UserAccountProperty,
|
|
||||||
UserLoginAction,
|
|
||||||
)
|
|
||||||
from typing import Optional, Tuple
|
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 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.business.signin_manager import SignInManager
|
||||||
|
from app.authentication.backend.models.user.constants import UserLoginAction
|
||||||
|
|
||||||
|
|
||||||
class SignInHub:
|
class SignInHub:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.user_auth_manager = UserAuthManager()
|
|
||||||
self.signin_manager = SignInManager()
|
self.signin_manager = SignInManager()
|
||||||
self.basic_profile_store = BasicProfileStore()
|
# TODO: Dax - Event dispatch and notification center
|
||||||
self.provider_profile_store = ProviderProfileStore()
|
# self.notification_center = NotificationCenter(sender_id=settings.SYSTEM_USER_ID)
|
||||||
self.code_depot_manager = CodeDepotManager()
|
# self.event_dispatcher = UserEventDispatcher(owner_id=settings.SYSTEM_USER_ID)
|
||||||
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)
|
|
||||||
|
|
||||||
|
@log_entry_exit_async
|
||||||
async def signin_with_email_and_code(
|
async def signin_with_email_and_code(
|
||||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
||||||
@ -35,16 +23,14 @@ class SignInHub:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@log_entry_exit_async
|
@log_entry_exit_async
|
||||||
async def signin_with_email_and_code(
|
async def signin_with_email_and_password(
|
||||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
self, email: str, password: str
|
||||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||||
"""Try to signin with email and code.
|
"""Try to signin with email and password.
|
||||||
create a new user account, if the email address has never been used before.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
email (str): email address
|
email (str): email address
|
||||||
code (str): auth code to be verified
|
password (str): password to be verified
|
||||||
host (str): the host address by which the client access the frontend service
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
[int, Optional[int], Optional[str], Optional[str]]:
|
[int, Optional[int], Optional[str], Optional[str]]:
|
||||||
@ -53,99 +39,35 @@ class SignInHub:
|
|||||||
- Optional[str]: user_id
|
- Optional[str]: user_id
|
||||||
- Optional[str]: flid
|
- Optional[str]: flid
|
||||||
"""
|
"""
|
||||||
|
return await self.signin_manager.signin_with_email_and_password(
|
||||||
# Step 1: Verify the email and code
|
email=email, password=password
|
||||||
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,
|
|
||||||
]
|
|
||||||
|
|
||||||
@log_entry_exit_async
|
@log_entry_exit_async
|
||||||
async def __create_new_user_account(
|
async def update_new_user_flid(
|
||||||
self, method: NewUserMethod, region: UserRegion
|
self, user_id: str, user_flid: str
|
||||||
) -> str:
|
) -> Tuple[UserLoginAction, Optional[str]]:
|
||||||
"""create a new user account document in DB
|
return await self.signin_manager.update_new_user_flid(
|
||||||
|
user_id=user_id, user_flid=user_flid
|
||||||
|
)
|
||||||
|
|
||||||
Args:
|
@log_entry_exit_async
|
||||||
method (NewUserMethod): the method the new user came from
|
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction:
|
||||||
region : preferred user region detected via the user log-in website
|
return await self.signin_manager.try_signin_with_email(email=email, host=host)
|
||||||
|
|
||||||
Returns:
|
@log_entry_exit_async
|
||||||
str: id of user account
|
async def reset_password_through_email(self, email: str, host: str) -> int:
|
||||||
"""
|
return await self.signin_manager.reset_password_through_email(
|
||||||
if NewUserMethod.EMAIL == method:
|
email=email, host=host
|
||||||
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()
|
|
||||||
|
|
||||||
elif NewUserMethod.MOBILE == method:
|
@log_entry_exit_async
|
||||||
user_account = UserAccountDoc(
|
async def update_user_password(self, user_id: str, password: str) -> dict[str, any]:
|
||||||
profile_id=None,
|
return await self.signin_manager.update_user_password(
|
||||||
account_id=None,
|
user_id=user_id, password=password
|
||||||
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()
|
|
||||||
|
|
||||||
# Create other doc in collections for the new user
|
@log_entry_exit_async
|
||||||
await UserAchievement(str(user_account.id)).create_activeness_achievement()
|
async def sign_out(self, identity: str) -> bool:
|
||||||
return str(user_account.id)
|
# TODO: to be implemented
|
||||||
|
return True
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
# business/auth/signin_business.py
|
import random
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
from app.authentication.backend.services.auth.user_auth_service import UserAuthService
|
from app.authentication.backend.services.auth.user_auth_service import UserAuthService
|
||||||
from infra.i18n.region_handler import RegionHandler
|
from infra.i18n.region_handler import RegionHandler
|
||||||
from services.auth.achievement_service import AchievementService
|
from app.authentication.backend.models.user.constants import (
|
||||||
from services.auth.event_service import EventService
|
UserLoginAction,
|
||||||
from services.auth.profile_service import ProfileService
|
NewUserMethod,
|
||||||
from utils.enums.auth import UserLoginAction, NewUserMethod
|
)
|
||||||
from infra.config import settings
|
|
||||||
from infra.exception.exceptions import InvalidAuthCodeException
|
from infra.exception.exceptions import InvalidAuthCodeException
|
||||||
from app.authentication.backend.models.constants import UserLoginAction
|
from app.authentication.backend.models.constants import UserLoginAction
|
||||||
from app.authentication.backend.services.user.user_management_service import (
|
from app.authentication.backend.services.user.user_management_service import (
|
||||||
UserManagementService,
|
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:
|
class SignInManager:
|
||||||
@ -19,77 +24,299 @@ class SignInManager:
|
|||||||
self.user_auth_service = UserAuthService()
|
self.user_auth_service = UserAuthService()
|
||||||
self.region_handler = RegionHandler()
|
self.region_handler = RegionHandler()
|
||||||
self.user_management_service = UserManagementService()
|
self.user_management_service = UserManagementService()
|
||||||
self.achievement_service = AchievementService()
|
self.module_logger = ModuleLogger(sender_id=SignInManager)
|
||||||
self.event_service = EventService()
|
self.code_depot_service = CodeDepotService()
|
||||||
self.profile_service = ProfileService()
|
# TODO: Dax: notification service
|
||||||
|
# self.event_service = EventService()
|
||||||
|
|
||||||
async def signin_with_email_and_code(
|
async def signin_with_email_and_code(
|
||||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
) -> 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)
|
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
|
is_new_user = user_id is None
|
||||||
preferred_region = self.region_handler.detect_from_host(host)
|
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 await self.user_auth_service.verify_email_with_code(email, code):
|
||||||
if is_new_user:
|
if is_new_user:
|
||||||
user_id = await self.user_management_service.create_new_user_account(
|
user_account = (
|
||||||
method=NewUserMethod.EMAIL, region=preferred_region
|
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_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
|
# This will be done by sending the notification back to Freeleaps App
|
||||||
# await self.achievement_service.record_login_event(user_id)
|
# 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 (
|
return (
|
||||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||||
user_account.role,
|
user_account.user_role,
|
||||||
user_id,
|
user_id,
|
||||||
email.split("@")[0],
|
email.split("@")[0],
|
||||||
preferred_region,
|
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 (
|
return (
|
||||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||||
user_account.role,
|
user_account.user_role,
|
||||||
user_id,
|
user_id,
|
||||||
flid,
|
user_flid,
|
||||||
preferred_region,
|
preferred_region,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
UserLoginAction.USER_SIGNED_IN,
|
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||||
user_account.role,
|
user_account.user_role,
|
||||||
user_id,
|
user_id,
|
||||||
flid,
|
user_flid,
|
||||||
preferred_region,
|
preferred_region,
|
||||||
)
|
)
|
||||||
else:
|
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
|
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None
|
||||||
|
|
||||||
async def verify_email_with_code(self, email: str, code: str, host: str):
|
async def signin_with_email_and_password(
|
||||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
self, email: str, password: str
|
||||||
if not await self.user_auth_service.verify_email_with_code(email, code):
|
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||||
raise InvalidAuthCodeException()
|
|
||||||
|
|
||||||
|
# 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
|
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}
|
||||||
|
|||||||
@ -18,6 +18,8 @@ from app.authentication.backend.models.models import (
|
|||||||
UserPasswordDoc,
|
UserPasswordDoc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from app.authentication.backend.models.user_profile.models import BasicProfileDoc
|
||||||
|
|
||||||
|
|
||||||
class UserAuthHandler:
|
class UserAuthHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -286,3 +288,63 @@ class UserAuthHandler:
|
|||||||
return user_password.password == ""
|
return user_password.password == ""
|
||||||
else:
|
else:
|
||||||
return True
|
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
|
||||||
|
|||||||
@ -1,149 +1,24 @@
|
|||||||
import pytz
|
|
||||||
import random
|
|
||||||
from infra.log.module_logger import ModuleLogger
|
from infra.log.module_logger import ModuleLogger
|
||||||
from typing import Optional
|
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:
|
class CodeDepotHandler:
|
||||||
def __init__(self) -> None:
|
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")
|
self.module_logger = ModuleLogger(sender_id="CodeDepotManager")
|
||||||
|
|
||||||
async def check_depot_name_availabe(self, code_depot_name: str) -> bool:
|
async def check_depot_name_availabe(self, code_depot_name: str) -> bool:
|
||||||
"""Return True if the depot name is available, otherwise return False
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def check_code_depot_exist(self, code_depot_name: str) -> bool:
|
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
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def __generate_new_code_depot_name(
|
async def __generate_new_code_depot_name(
|
||||||
self, code_depot_name: str, gitea_code_depot_names: list[str]
|
self, code_depot_name: str, gitea_code_depot_names: list[str]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate a new code depot name if the code depot name already exists
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]:
|
async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]:
|
||||||
"""Create a new git code depot
|
pass
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def get_depot_ssh_url(self, code_depot_name: str) -> str:
|
def get_depot_ssh_url(self, code_depot_name: str) -> str:
|
||||||
"""Return the ssh url of the code depot
|
"""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"
|
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]:
|
async def get_depot_users(self, code_depot_name: str) -> list[str]:
|
||||||
"""Return list of user names have permission to access the depot
|
pass
|
||||||
|
|
||||||
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 []
|
|
||||||
|
|
||||||
async def update_depot_user_password(self, user_name: str, password: str) -> bool:
|
async def update_depot_user_password(self, user_name: str, password: str) -> bool:
|
||||||
"""Update the password of the user in Gitea
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def create_depot_user(
|
async def create_depot_user(
|
||||||
self, user_name: str, password: str, email: str
|
self, user_name: str, password: str, email: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Create a new user in Gitea
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
async def grant_user_depot_access(
|
async def grant_user_depot_access(
|
||||||
self, user_name: str, code_depot_name: str
|
self, user_name: str, code_depot_name: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Grant user access to the code depot in Gitea
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def generate_statistic_result(
|
async def generate_statistic_result(
|
||||||
self, code_depot_name: str
|
self, code_depot_name: str
|
||||||
) -> Optional[dict[str, any]]:
|
) -> Optional[dict[str, any]]:
|
||||||
"""Call Gitea API and collect statistic result of the repository.
|
pass
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def analyze_code_depots(self):
|
async def analyze_code_depots(self):
|
||||||
code_depot_docs = await CodeDepotDoc.find(
|
pass
|
||||||
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()
|
|
||||||
|
|
||||||
async def fetch_code_depot(self, code_depot_id: str) -> Optional[dict[str, any]]:
|
async def fetch_code_depot(self, code_depot_id: str) -> Optional[dict[str, any]]:
|
||||||
code_depot_doc = await CodeDepotDoc.get(code_depot_id)
|
pass
|
||||||
if code_depot_doc:
|
|
||||||
return code_depot_doc.model_dump()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|||||||
@ -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:
|
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
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from infra.models.constants import UserRegion
|
||||||
|
|
||||||
|
|
||||||
class NewUserMethod(IntEnum):
|
class NewUserMethod(IntEnum):
|
||||||
@ -22,3 +23,15 @@ class UserLoginAction(IntEnum):
|
|||||||
EMAIL_NOT_ASSOCIATED_WITH_USER = 3
|
EMAIL_NOT_ASSOCIATED_WITH_USER = 3
|
||||||
REVIEW_AND_REVISE_FLID = 4
|
REVIEW_AND_REVISE_FLID = 4
|
||||||
USER_SIGNED_IN = 100
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,10 @@ from typing import Optional
|
|||||||
from beanie import Document
|
from beanie import Document
|
||||||
|
|
||||||
from .constants import UserAccountProperty
|
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
|
from infra.models.constants import UserRegion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
103
app/authentication/backend/models/user_profile/models.py
Normal file
103
app/authentication/backend/models/user_profile/models.py
Normal file
@ -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"
|
||||||
@ -1,30 +1,46 @@
|
|||||||
from app.authentication.backend.infra.user_management.user_auth_handler import (
|
from app.authentication.backend.infra.auth.user_auth_handler import (
|
||||||
UserAuthManager,
|
UserAuthHandler,
|
||||||
)
|
)
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class UserAuthService:
|
class UserAuthService:
|
||||||
def __init__(self):
|
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]:
|
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:
|
async def verify_email_with_code(self, email: str, code: str) -> bool:
|
||||||
return await self.user_auth_manager.verify_email_code(email, code)
|
return await self.user_auth_handler.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 is_password_reset_required(self, user_id: str) -> bool:
|
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
|
||||||
|
)
|
||||||
|
|||||||
@ -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
|
||||||
@ -1,35 +1,23 @@
|
|||||||
import random
|
|
||||||
from typing import Optional, Dict, Tuple, List
|
|
||||||
from infra.log.module_logger import ModuleLogger
|
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,
|
NewUserMethod,
|
||||||
UserAccountProperty,
|
UserAccountProperty,
|
||||||
UserLoginAction,
|
|
||||||
)
|
)
|
||||||
from app.authentication.backend.application.user.models import UserAccountDoc
|
from app.authentication.backend.models.user.models import UserAccountDoc
|
||||||
from app.authentication.backend.business.permission.constants import (
|
from app.authentication.backend.models.permission.constants import (
|
||||||
AdministrativeRole,
|
AdministrativeRole,
|
||||||
Capability,
|
Capability,
|
||||||
)
|
)
|
||||||
from app.authentication.backend.infra.auth.user_auth_handler import (
|
from app.authentication.backend.infra.auth.user_auth_handler import (
|
||||||
UserAuthHandler,
|
UserAuthHandler,
|
||||||
)
|
)
|
||||||
from app.authentication.backend.infra.user.user_profile_handler import (
|
from app.authentication.backend.infra.user_profile.user_profile_handler import (
|
||||||
UserProfileHandler,
|
UserProfileHandler,
|
||||||
)
|
)
|
||||||
from backend.infra.config.backend import settings
|
from infra.log.log_utils import log_entry_exit_async
|
||||||
from backend.infra.log.log_utils import log_entry_exit_async
|
from infra.models.constants import UserRegion
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class UserManagementService:
|
class UserManagementService:
|
||||||
@ -41,7 +29,7 @@ class UserManagementService:
|
|||||||
@log_entry_exit_async
|
@log_entry_exit_async
|
||||||
async def create_new_user_account(
|
async def create_new_user_account(
|
||||||
self, method: NewUserMethod, region: UserRegion
|
self, method: NewUserMethod, region: UserRegion
|
||||||
) -> str:
|
) -> UserAccountDoc:
|
||||||
"""create a new user account document in DB
|
"""create a new user account document in DB
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -52,29 +40,60 @@ class UserManagementService:
|
|||||||
str: id of user account
|
str: id of user account
|
||||||
"""
|
"""
|
||||||
if NewUserMethod.EMAIL == method:
|
if NewUserMethod.EMAIL == method:
|
||||||
user_account = UserAccountDoc(
|
user_account = await self.user_profile_handler.create_new_user_account(
|
||||||
profile_id=None,
|
UserAccountProperty.EMAIL_VERIFIED,
|
||||||
account_id=None,
|
Capability.VISITOR,
|
||||||
service_plan_id=None,
|
AdministrativeRole.PERSONAL,
|
||||||
properties=int(UserAccountProperty.EMAIL_VERIFIED),
|
region,
|
||||||
capabilities=int(Capability.VISITOR),
|
|
||||||
user_role=int(AdministrativeRole.PERSONAL),
|
|
||||||
region=region,
|
|
||||||
)
|
)
|
||||||
user_account = await user_account.create()
|
|
||||||
|
|
||||||
elif NewUserMethod.MOBILE == method:
|
elif NewUserMethod.MOBILE == method:
|
||||||
user_account = UserAccountDoc(
|
user_account = await self.user_profile_handler.create_new_user_account(
|
||||||
profile_id=None,
|
UserAccountProperty.EMAIL_VERIFIED,
|
||||||
account_id=None,
|
Capability.VISITOR,
|
||||||
service_plan_id=None,
|
AdministrativeRole.PERSONAL,
|
||||||
properties=int(UserAccountProperty.MOBILE_VERIFIED),
|
region,
|
||||||
capabilities=int(Capability.VISITOR),
|
|
||||||
user_role=int(AdministrativeRole.PERSONAL),
|
|
||||||
region=region,
|
|
||||||
)
|
)
|
||||||
user_account = await user_account.create()
|
|
||||||
|
|
||||||
# Create other doc in collections for the new user
|
# Create other doc in collections for the new user
|
||||||
await UserAchievement(str(user_account.id)).create_activeness_achievement()
|
# TODO: Should convert to notification
|
||||||
return str(user_account.id)
|
# 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)
|
||||||
|
|||||||
13
app/authentication/requirements.txt
Normal file
13
app/authentication/requirements.txt
Normal file
@ -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]
|
||||||
@ -2,12 +2,12 @@ import logging
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.openapi.utils import get_openapi
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
|
||||||
from webapi.providers import common
|
from app.authentication.webapi.providers import common
|
||||||
from webapi.providers import logger
|
from app.authentication.webapi.providers import logger
|
||||||
from webapi.providers import router
|
from app.authentication.webapi.providers import router
|
||||||
from webapi.providers import database
|
from app.authentication.webapi.providers import database
|
||||||
from webapi.providers import scheduler
|
from app.authentication.webapi.providers import scheduler
|
||||||
from webapi.providers import exception_handler
|
from app.authentication.webapi.providers import exception_handler
|
||||||
from .freeleaps_app import FreeleapsApp
|
from .freeleaps_app import FreeleapsApp
|
||||||
|
|
||||||
|
|
||||||
@ -49,11 +49,7 @@ def customize_openapi_security(app: FastAPI) -> None:
|
|||||||
|
|
||||||
# Add security scheme to components
|
# Add security scheme to components
|
||||||
openapi_schema["components"]["securitySchemes"] = {
|
openapi_schema["components"]["securitySchemes"] = {
|
||||||
"bearerAuth": {
|
"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
|
||||||
"type": "http",
|
|
||||||
"scheme": "bearer",
|
|
||||||
"bearerFormat": "JWT"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add security requirement globally
|
# Add security requirement globally
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
from webapi.bootstrap.application import create_app
|
from app.authentication.webapi.bootstrap.application import create_app
|
||||||
from webapi.config.site_settings import site_settings
|
from app.authentication.webapi.config.site_settings import site_settings
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from strawberry.fastapi import GraphQLRouter
|
|
||||||
from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", status_code=301)
|
@app.get("/", status_code=301)
|
||||||
async def root():
|
async def root():
|
||||||
"""
|
"""
|
||||||
@ -19,12 +18,16 @@ async def root():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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:
|
def get_context() -> Any:
|
||||||
# Define your context function. This is where you can set up authentication, database connections, etc.
|
# Define your context function. This is where you can set up authentication, database connections, etc.
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_root_value() -> Any:
|
def get_root_value() -> Any:
|
||||||
# Define your root value function. This can be used to customize the root value for GraphQL operations.
|
# Define your root value function. This can be used to customize the root value for GraphQL operations.
|
||||||
return {}
|
return {}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
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):
|
def register(app):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from common.config.log_settings import log_settings
|
from infra.config.log_settings import log_settings
|
||||||
|
|
||||||
|
|
||||||
def register(app=None):
|
def register(app=None):
|
||||||
@ -21,15 +21,8 @@ def register(app=None):
|
|||||||
logging.getLogger(name).propagate = True
|
logging.getLogger(name).propagate = True
|
||||||
|
|
||||||
# configure loguru
|
# configure loguru
|
||||||
logger.add(
|
logger.add(sink=sys.stdout)
|
||||||
sink=sys.stdout
|
logger.add(sink=file_path, level=level, retention=retention, rotation=rotation)
|
||||||
)
|
|
||||||
logger.add(
|
|
||||||
sink=file_path,
|
|
||||||
level=level,
|
|
||||||
retention=retention,
|
|
||||||
rotation=rotation
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.disable("pika.adapters")
|
logger.disable("pika.adapters")
|
||||||
logger.disable("pika.connection")
|
logger.disable("pika.connection")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from webapi.routes import api_router
|
from app.authentication.webapi.routes import api_router
|
||||||
|
|
||||||
from starlette import routing
|
from starlette import routing
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from .signin import router as signin_router
|
||||||
|
from .tokens import router as token_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
api_router.include_router(signin_router, tags=["user"])
|
||||||
|
api_router.include_router(token_router, tags=["auth"])
|
||||||
websocket_router = APIRouter()
|
websocket_router = APIRouter()
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from .try_signin_with_email import router as ts_router
|
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 .signin_with_email_and_code import router as sw_router
|
||||||
from .update_user_password import router as up_router
|
from .update_user_password import router as up_router
|
||||||
from .signin_with_email_and_password import router as se_router
|
from .update_new_user_flid import router as uu_router
|
||||||
from .sign_out import router as so_router
|
|
||||||
from .reset_password_through_email import router as rp_router
|
from .reset_password_through_email import router as rp_router
|
||||||
from .refresh_token import router as rt_router
|
from .sign_out import router as so_router
|
||||||
from .update_user_flid import router as uu_router
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/signin")
|
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(so_router, tags=["signin"])
|
||||||
router.include_router(rp_router, tags=["signin"])
|
router.include_router(rp_router, tags=["signin"])
|
||||||
router.include_router(uu_router, tags=["signin"])
|
router.include_router(uu_router, tags=["signin"])
|
||||||
router.include_router(rt_router, tags=["signin"])
|
|
||||||
|
|||||||
@ -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 pydantic import BaseModel
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
@ -32,6 +32,6 @@ class UserSignWithEmailResponse(BaseModel):
|
|||||||
async def reset_password_through_email(
|
async def reset_password_through_email(
|
||||||
item: UserSignWithEmailBody,
|
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}
|
result = {"action": result}
|
||||||
return JSONResponse(content=jsonable_encoder(result))
|
return JSONResponse(content=jsonable_encoder(result))
|
||||||
|
|||||||
40
app/authentication/webapi/routes/signin/sign_out.py
Normal file
40
app/authentication/webapi/routes/signin/sign_out.py
Normal file
@ -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))
|
||||||
@ -7,10 +7,12 @@ from fastapi.encoders import jsonable_encoder
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from backend.application.user.user_manager import UserManager
|
from app.authentication.backend.application.signin_hub import SignInHub
|
||||||
from backend.infra.authentication.auth import access_security
|
from infra.token.token_manager import TokenManager
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
token_manager = TokenManager()
|
||||||
|
|
||||||
# Web API
|
# Web API
|
||||||
# signin-with-email-n-code
|
# signin-with-email-n-code
|
||||||
@ -56,7 +58,7 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
|||||||
identity,
|
identity,
|
||||||
flid,
|
flid,
|
||||||
preferred_region,
|
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
|
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:
|
if signed_in and identity and adminstrative_role:
|
||||||
subject = {"id": identity, "role": adminstrative_role}
|
subject = {"id": identity, "role": adminstrative_role}
|
||||||
access_token = access_security.create_access_token(subject=subject)
|
access_token = token_manager.create_access_token(subject=subject)
|
||||||
refresh_token = access_security.create_refresh_token(subject=subject)
|
refresh_token = token_manager.create_refresh_token(subject=subject)
|
||||||
expires_in = datetime.now(timezone.utc) + access_security.access_expires_delta
|
expires_in = (
|
||||||
|
datetime.now(timezone.utc) + token_manager.access_token_expire_minutes
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
access_token = None
|
access_token = None
|
||||||
refresh_token = None
|
refresh_token = None
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
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
|
from infra.token.token_manager import TokenManager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
token_manager = TokenManager()
|
token_manager = TokenManager()
|
||||||
|
|
||||||
# Web API
|
# Web API
|
||||||
# signin-with-email-n-code
|
# signin-with-email-n-password
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class RequestIn(BaseModel):
|
class RequestIn(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
code: str
|
password: str
|
||||||
host: str
|
|
||||||
time_zone: Optional[str] = "UTC"
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseOut(BaseModel):
|
class ResponseOut(BaseModel):
|
||||||
@ -37,38 +38,38 @@ class ResponseOut(BaseModel):
|
|||||||
expires_in: Optional[datetime] = None
|
expires_in: Optional[datetime] = None
|
||||||
# the system assigned role of the user.
|
# the system assigned role of the user.
|
||||||
role: Optional[int] = None
|
role: Optional[int] = None
|
||||||
# preferred region for user
|
# the flid of the user
|
||||||
preferred_region: Optional[str] = None
|
flid: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/signin-with-email-and-code",
|
"/signin-with-email-and-password",
|
||||||
operation_id="user-signin-with-email-and-code",
|
operation_id="user-signin-with-email-and-password",
|
||||||
summary="try to signin with email and authentication code",
|
summary="try to signin with email and password",
|
||||||
description="client user is trying to sign in with their email and the authenication code \
|
description="client user is trying to sign in with their email and the password .",
|
||||||
the system sent to the email in previous step.",
|
|
||||||
response_model=ResponseOut,
|
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,
|
signed_in,
|
||||||
adminstrative_role,
|
adminstrative_role,
|
||||||
identity,
|
identity,
|
||||||
flid,
|
flid,
|
||||||
preferred_region,
|
) = await SignInHub().signin_with_email_and_password(item.email, item.password)
|
||||||
) = await UserManager().signin_with_email_and_code(
|
|
||||||
item.email, item.code, item.host, item.time_zone
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"signedin={signed_in}, adminstrative_role={adminstrative_role}, identity={identity}"
|
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}
|
subject = {"id": identity, "role": adminstrative_role}
|
||||||
access_token = token_manager.create_access_token(subject=subject)
|
access_token = token_manager.create_access_token(subject=subject)
|
||||||
refresh_token = token_manager.create_refresh_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:
|
else:
|
||||||
access_token = None
|
access_token = None
|
||||||
refresh_token = None
|
refresh_token = None
|
||||||
@ -82,6 +83,5 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
|||||||
"expires_in": expires_in,
|
"expires_in": expires_in,
|
||||||
"role": adminstrative_role,
|
"role": adminstrative_role,
|
||||||
"flid": flid,
|
"flid": flid,
|
||||||
"preferred_region": preferred_region,
|
|
||||||
}
|
}
|
||||||
return JSONResponse(content=jsonable_encoder(result))
|
return JSONResponse(content=jsonable_encoder(result))
|
||||||
|
|||||||
@ -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.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Web API
|
||||||
|
# try_signin_with_email
|
||||||
|
#
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/get-latest-login-by-user-id/{user_id}",
|
class UserSignWithEmailBody(BaseModel):
|
||||||
operation_id="get-latest-login-by-user-id",
|
email: str
|
||||||
summary="Get the latest login timestamp for a given user",
|
host: str
|
||||||
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 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(
|
async def try_signin_with_email(
|
||||||
user_id: str,
|
item: UserSignWithEmailBody,
|
||||||
credentials: JwtAuthorizationCredentials = Security(access_security),
|
|
||||||
):
|
):
|
||||||
# Assume UserManager is responsible for handling user data
|
result = await SignInHub().try_signin_with_email(item.email, item.host)
|
||||||
result = await UserManager().fetch_latest_login(user_id)
|
result = {"signin_type": result}
|
||||||
|
return JSONResponse(content=jsonable_encoder(result))
|
||||||
if result is None:
|
|
||||||
return JSONResponse(content=jsonable_encoder({"timestamp": None}))
|
|
||||||
|
|
||||||
return JSONResponse(content=jsonable_encoder({"timestamp": result}))
|
|
||||||
|
|||||||
@ -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))
|
||||||
@ -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 pydantic import BaseModel
|
||||||
from fastapi import APIRouter, Security
|
from fastapi import APIRouter
|
||||||
from backend.infra.authentication.auth import access_security
|
from infra.token.token_manager import TokenManager
|
||||||
from fastapi_jwt import JwtAuthorizationCredentials
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
token_manager = TokenManager()
|
||||||
# Web API
|
# Web API
|
||||||
# update_user_password
|
# update_user_password
|
||||||
#
|
#
|
||||||
@ -28,9 +30,14 @@ class RequestIn(BaseModel):
|
|||||||
)
|
)
|
||||||
async def update_user_password(
|
async def update_user_password(
|
||||||
item: RequestIn,
|
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:
|
if item.password != item.password2:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=jsonable_encoder(
|
content=jsonable_encoder(
|
||||||
@ -38,5 +45,5 @@ async def update_user_password(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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))
|
return JSONResponse(content=jsonable_encoder(result))
|
||||||
|
|||||||
@ -16,4 +16,4 @@ EXPOSE 8005
|
|||||||
|
|
||||||
|
|
||||||
# Run the application using the start script
|
# 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"]
|
||||||
|
|||||||
@ -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()
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from loguru import logger
|
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):
|
def register(app=None):
|
||||||
|
|||||||
@ -29,6 +29,20 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/app # Mount the current directory to /app in the container
|
- .:/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:
|
networks:
|
||||||
freeleaps_service_hub_network:
|
freeleaps_service_hub_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@ -3,6 +3,9 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
JWT_SECRET_KEY: str = ""
|
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
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
|
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
|
||||||
MONGODB_NAME: str = "freeleaps2"
|
MONGODB_NAME: str = "freeleaps2"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from .base_logger import LoggerBase
|
from .base_logger import LoggerBase
|
||||||
from common.config.app_settings import app_settings
|
from infra.config.app_settings import app_settings
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from loguru import logger as guru_logger
|
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
|
from typing import List
|
||||||
import socket
|
import socket
|
||||||
import json
|
import json
|
||||||
@ -11,9 +11,7 @@ class LoggerBase:
|
|||||||
binded_loggers = {}
|
binded_loggers = {}
|
||||||
logger_lock = threading.Lock()
|
logger_lock = threading.Lock()
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, logger_name: str, extra_fileds: dict[str, any]) -> None:
|
||||||
self, logger_name: str, extra_fileds: dict[str, any]
|
|
||||||
) -> None:
|
|
||||||
self.__logger_name = logger_name
|
self.__logger_name = logger_name
|
||||||
self.extra_fileds = extra_fileds
|
self.extra_fileds = extra_fileds
|
||||||
with LoggerBase.logger_lock:
|
with LoggerBase.logger_lock:
|
||||||
@ -21,9 +19,7 @@ class LoggerBase:
|
|||||||
self.logger = LoggerBase.binded_loggers[self.__logger_name]
|
self.logger = LoggerBase.binded_loggers[self.__logger_name]
|
||||||
return
|
return
|
||||||
|
|
||||||
log_filename = (
|
log_filename = log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log"
|
||||||
log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log"
|
|
||||||
)
|
|
||||||
log_retention = log_settings.LOG_RETENTION
|
log_retention = log_settings.LOG_RETENTION
|
||||||
log_rotation = log_settings.LOG_ROTATION
|
log_rotation = log_settings.LOG_ROTATION
|
||||||
log_level = "INFO"
|
log_level = "INFO"
|
||||||
@ -57,14 +53,14 @@ class LoggerBase:
|
|||||||
subject: str,
|
subject: str,
|
||||||
event: str,
|
event: str,
|
||||||
properties: dict[str, any],
|
properties: dict[str, any],
|
||||||
text: str = ""
|
text: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
local_logger = self.logger.bind(
|
local_logger = self.logger.bind(
|
||||||
sender_id=sender_id,
|
sender_id=sender_id,
|
||||||
receiver_id=receiver_id,
|
receiver_id=receiver_id,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
event=event,
|
event=event,
|
||||||
properties=properties
|
properties=properties,
|
||||||
)
|
)
|
||||||
local_logger.info(text)
|
local_logger.info(text)
|
||||||
|
|
||||||
@ -83,7 +79,7 @@ class LoggerBase:
|
|||||||
subject=subject,
|
subject=subject,
|
||||||
event="exception",
|
event="exception",
|
||||||
properties=properties,
|
properties=properties,
|
||||||
exception=exception
|
exception=exception,
|
||||||
)
|
)
|
||||||
local_logger.exception(text)
|
local_logger.exception(text)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,9 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
from infra.config.app_settings import app_settings
|
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:
|
class TokenManager:
|
||||||
@ -61,3 +64,17 @@ class TokenManager:
|
|||||||
return self.create_access_token(subject)
|
return self.create_access_token(subject)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid refresh token")
|
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"
|
||||||
|
)
|
||||||
|
|||||||
1
sites/authentication/.env
Normal file
1
sites/authentication/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==
|
||||||
Loading…
Reference in New Issue
Block a user