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 infra.i18n.region_handler import RegionHandler
|
||||
from infra.models.constants import UserRegion
|
||||
from infra.log.log_utils import log_entry_exit_async
|
||||
from app.authentication.backend.infra.user_management.user_auth_handler import (
|
||||
UserAuthManager,
|
||||
)
|
||||
from app.authentication.backend.business.signin_manager import SignInManager
|
||||
from app.authentication.backend.models.user.constants import UserLoginAction
|
||||
|
||||
|
||||
class SignInHub:
|
||||
def __init__(self) -> None:
|
||||
self.user_auth_manager = UserAuthManager()
|
||||
self.signin_manager = SignInManager()
|
||||
self.basic_profile_store = BasicProfileStore()
|
||||
self.provider_profile_store = ProviderProfileStore()
|
||||
self.code_depot_manager = CodeDepotManager()
|
||||
self.notification_center = NotificationCenter(sender_id=settings.SYSTEM_USER_ID)
|
||||
self.event_dispatcher = UserEventDispatcher(owner_id=settings.SYSTEM_USER_ID)
|
||||
self.module_logger = ModuleLogger(sender_id=UserManager)
|
||||
# TODO: Dax - Event dispatch and notification center
|
||||
# self.notification_center = NotificationCenter(sender_id=settings.SYSTEM_USER_ID)
|
||||
# self.event_dispatcher = UserEventDispatcher(owner_id=settings.SYSTEM_USER_ID)
|
||||
|
||||
@log_entry_exit_async
|
||||
async def signin_with_email_and_code(
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
||||
@ -35,16 +23,14 @@ class SignInHub:
|
||||
)
|
||||
|
||||
@log_entry_exit_async
|
||||
async def signin_with_email_and_code(
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
async def signin_with_email_and_password(
|
||||
self, email: str, password: str
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||
"""Try to signin with email and code.
|
||||
create a new user account, if the email address has never been used before.
|
||||
"""Try to signin with email and password.
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
code (str): auth code to be verified
|
||||
host (str): the host address by which the client access the frontend service
|
||||
password (str): password to be verified
|
||||
|
||||
Returns:
|
||||
[int, Optional[int], Optional[str], Optional[str]]:
|
||||
@ -53,99 +39,35 @@ class SignInHub:
|
||||
- Optional[str]: user_id
|
||||
- Optional[str]: flid
|
||||
"""
|
||||
|
||||
# Step 1: Verify the email and code
|
||||
try:
|
||||
user_id, is_new_user, preferred_region = (
|
||||
await self.signin_manager.verify_email_with_code(email, code, host)
|
||||
)
|
||||
except InvalidAuthCodeException:
|
||||
await self.logger.log_info(
|
||||
info="The auth code is invalid.",
|
||||
properties={"email": email, "code": code},
|
||||
)
|
||||
return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None]
|
||||
|
||||
# Step 2: Handle new user creation if necessary
|
||||
# This will be moved to the Freeleaps
|
||||
if is_new_user:
|
||||
user_id = await self.user_service.create_new_user(
|
||||
NewUserMethod.EMAIL, preferred_region, email, time_zone
|
||||
)
|
||||
await self.event_service.log_signup_event(
|
||||
user_id, email, preferred_region, time_zone
|
||||
)
|
||||
|
||||
# Step 3: Fetch user account and handle login actions
|
||||
user_account = await self.user_service.get_user_account(user_id)
|
||||
await self.event_service.log_user_login_event(user_id)
|
||||
|
||||
# Step 4: Handle special actions (FLID reset, password reset, etc.)
|
||||
if await self.user_service.is_flid_reset_required(user_id):
|
||||
return [
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
email.split("@")[0],
|
||||
preferred_region,
|
||||
]
|
||||
|
||||
user_flid = await self.user_service.get_flid(user_id)
|
||||
|
||||
if await self.auth_service.is_password_reset_required(user_id):
|
||||
return [
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
user_flid,
|
||||
user_account.preferred_region,
|
||||
]
|
||||
|
||||
return [
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
user_flid,
|
||||
user_account.preferred_region,
|
||||
]
|
||||
return await self.signin_manager.signin_with_email_and_password(
|
||||
email=email, password=password
|
||||
)
|
||||
|
||||
@log_entry_exit_async
|
||||
async def __create_new_user_account(
|
||||
self, method: NewUserMethod, region: UserRegion
|
||||
) -> str:
|
||||
"""create a new user account document in DB
|
||||
async def update_new_user_flid(
|
||||
self, user_id: str, user_flid: str
|
||||
) -> Tuple[UserLoginAction, Optional[str]]:
|
||||
return await self.signin_manager.update_new_user_flid(
|
||||
user_id=user_id, user_flid=user_flid
|
||||
)
|
||||
|
||||
Args:
|
||||
method (NewUserMethod): the method the new user came from
|
||||
region : preferred user region detected via the user log-in website
|
||||
@log_entry_exit_async
|
||||
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction:
|
||||
return await self.signin_manager.try_signin_with_email(email=email, host=host)
|
||||
|
||||
Returns:
|
||||
str: id of user account
|
||||
"""
|
||||
if NewUserMethod.EMAIL == method:
|
||||
user_account = UserAccountDoc(
|
||||
profile_id=None,
|
||||
account_id=None,
|
||||
service_plan_id=None,
|
||||
properties=int(UserAccountProperty.EMAIL_VERIFIED),
|
||||
capabilities=int(Capability.VISITOR),
|
||||
user_role=int(AdministrativeRole.PERSONAL),
|
||||
region=region,
|
||||
)
|
||||
user_account = await user_account.create()
|
||||
@log_entry_exit_async
|
||||
async def reset_password_through_email(self, email: str, host: str) -> int:
|
||||
return await self.signin_manager.reset_password_through_email(
|
||||
email=email, host=host
|
||||
)
|
||||
|
||||
elif NewUserMethod.MOBILE == method:
|
||||
user_account = UserAccountDoc(
|
||||
profile_id=None,
|
||||
account_id=None,
|
||||
service_plan_id=None,
|
||||
properties=int(UserAccountProperty.MOBILE_VERIFIED),
|
||||
capabilities=int(Capability.VISITOR),
|
||||
user_role=int(AdministrativeRole.PERSONAL),
|
||||
region=region,
|
||||
)
|
||||
user_account = await user_account.create()
|
||||
@log_entry_exit_async
|
||||
async def update_user_password(self, user_id: str, password: str) -> dict[str, any]:
|
||||
return await self.signin_manager.update_user_password(
|
||||
user_id=user_id, password=password
|
||||
)
|
||||
|
||||
# Create other doc in collections for the new user
|
||||
await UserAchievement(str(user_account.id)).create_activeness_achievement()
|
||||
return str(user_account.id)
|
||||
@log_entry_exit_async
|
||||
async def sign_out(self, identity: str) -> bool:
|
||||
# TODO: to be implemented
|
||||
return True
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
# business/auth/signin_business.py
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
from app.authentication.backend.services.auth.user_auth_service import UserAuthService
|
||||
from infra.i18n.region_handler import RegionHandler
|
||||
from services.auth.achievement_service import AchievementService
|
||||
from services.auth.event_service import EventService
|
||||
from services.auth.profile_service import ProfileService
|
||||
from utils.enums.auth import UserLoginAction, NewUserMethod
|
||||
from infra.config import settings
|
||||
from app.authentication.backend.models.user.constants import (
|
||||
UserLoginAction,
|
||||
NewUserMethod,
|
||||
)
|
||||
from infra.exception.exceptions import InvalidAuthCodeException
|
||||
from app.authentication.backend.models.constants import UserLoginAction
|
||||
from app.authentication.backend.services.user.user_management_service import (
|
||||
UserManagementService,
|
||||
)
|
||||
from app.authentication.backend.services.code_depot.code_depot_service import (
|
||||
CodeDepotService,
|
||||
)
|
||||
from infra.log.module_logger import ModuleLogger
|
||||
from infra.utils.string import check_password_complexity
|
||||
from infra.exception.exceptions import InvalidDataError
|
||||
|
||||
|
||||
class SignInManager:
|
||||
@ -19,77 +24,299 @@ class SignInManager:
|
||||
self.user_auth_service = UserAuthService()
|
||||
self.region_handler = RegionHandler()
|
||||
self.user_management_service = UserManagementService()
|
||||
self.achievement_service = AchievementService()
|
||||
self.event_service = EventService()
|
||||
self.profile_service = ProfileService()
|
||||
self.module_logger = ModuleLogger(sender_id=SignInManager)
|
||||
self.code_depot_service = CodeDepotService()
|
||||
# TODO: Dax: notification service
|
||||
# self.event_service = EventService()
|
||||
|
||||
async def signin_with_email_and_code(
|
||||
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
|
||||
) -> Tuple[int, Optional[int], Optional[str], Optional[str]]:
|
||||
"""Try to signin with email and code.
|
||||
create a new user account, if the email address has never been used before.
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
code (str): auth code to be verified
|
||||
host (str): the host address by which the client access the frontend service
|
||||
|
||||
Returns:
|
||||
[int, Optional[int], Optional[str], Optional[str]]:
|
||||
- int: UserLoginAction
|
||||
- Optional[int]: user role
|
||||
- Optional[str]: user_id
|
||||
- Optional[str]: flid
|
||||
"""
|
||||
Handles the business logic for signing in with email and code.
|
||||
"""
|
||||
# check if the user account exist
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
|
||||
# if it cannot find user account according to the email address, new user
|
||||
is_new_user = user_id is None
|
||||
preferred_region = self.region_handler.detect_from_host(host)
|
||||
|
||||
# verify the email through auth code
|
||||
if await self.user_auth_service.verify_email_with_code(email, code):
|
||||
if is_new_user:
|
||||
user_id = await self.user_management_service.create_new_user_account(
|
||||
method=NewUserMethod.EMAIL, region=preferred_region
|
||||
)
|
||||
await self.user_service.initialize_new_user_data(
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
region=preferred_region,
|
||||
time_zone=time_zone,
|
||||
)
|
||||
await self.event_service.log_signup_event(
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
region=preferred_region,
|
||||
time_zone=time_zone,
|
||||
user_account = (
|
||||
await self.user_management_service.create_new_user_account(
|
||||
method=NewUserMethod.EMAIL, region=preferred_region
|
||||
)
|
||||
)
|
||||
|
||||
user_account = await self.user_service.get_user_account(user_id)
|
||||
user_id = str(user_account.id)
|
||||
await self.user_management_service.initialize_new_user_data(
|
||||
user_id=str(user_account.id),
|
||||
method=NewUserMethod.EMAIL,
|
||||
email_address=email,
|
||||
region=preferred_region,
|
||||
time_zone=time_zone,
|
||||
)
|
||||
# TODO: Dax - Add notification for log_signup_event
|
||||
# await self.event_service.log_signup_event(
|
||||
# user_id=user_id,
|
||||
# email=email,
|
||||
# region=preferred_region,
|
||||
# time_zone=time_zone,
|
||||
# )
|
||||
|
||||
# TODO: Dax - Add notification for log_signup_event
|
||||
# This will be done by sending the notification back to Freeleaps App
|
||||
# await self.achievement_service.record_login_event(user_id)
|
||||
|
||||
if await self.profile_service.is_flid_reset_required(user_id):
|
||||
if await self.user_auth_service.is_flid_reset_required(user_id):
|
||||
return (
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
user_account.role,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
email.split("@")[0],
|
||||
preferred_region,
|
||||
)
|
||||
|
||||
flid = await self.profile_service.get_user_flid(user_id)
|
||||
user_flid = await self.user_auth_service.get_user_flid(user_id)
|
||||
|
||||
if await self.user_service.is_password_reset_required(user_id):
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
return (
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
user_account.role,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
flid,
|
||||
user_flid,
|
||||
preferred_region,
|
||||
)
|
||||
|
||||
return (
|
||||
UserLoginAction.USER_SIGNED_IN,
|
||||
user_account.role,
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
flid,
|
||||
user_flid,
|
||||
preferred_region,
|
||||
)
|
||||
else:
|
||||
await self.module_logger.log_warning(
|
||||
info="The auth code is invalid.",
|
||||
properties={"email": email, "code": code},
|
||||
)
|
||||
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None, None
|
||||
|
||||
async def verify_email_with_code(self, email: str, code: str, host: str):
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
if not await self.user_auth_service.verify_email_with_code(email, code):
|
||||
raise InvalidAuthCodeException()
|
||||
async def signin_with_email_and_password(
|
||||
self, email: str, password: str
|
||||
) -> Tuple[UserLoginAction, Optional[int], Optional[str], Optional[str]]:
|
||||
|
||||
# check if the user account exist
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
|
||||
# if it cannot find user account according to the email address, new user
|
||||
is_new_user = user_id is None
|
||||
preferred_region = self.region_handler.detect_from_host(host)
|
||||
return user_id, is_new_user, preferred_region
|
||||
|
||||
if is_new_user:
|
||||
# cannot find the email address
|
||||
return [UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE, None, None, None]
|
||||
else:
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
# password hasn't been set before, save password for the user
|
||||
return [
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
else:
|
||||
if await self.user_auth_service.verify_user_with_password(
|
||||
user_id, password
|
||||
):
|
||||
# TODO: Add user achievement logic in Freeleaps
|
||||
# TODO: Add notification system
|
||||
# user logins count + 1
|
||||
# await UserAchievement(user_id).record_user_login_event()
|
||||
# for new users, log the sign up event
|
||||
# await self.event_dispatcher.dispatch_event(
|
||||
# receiver_id=settings.SYSTEM_USER_ID,
|
||||
# subject="login",
|
||||
# event="signin",
|
||||
# properties={
|
||||
# "user_id": user_id,
|
||||
# },
|
||||
# )
|
||||
|
||||
user_account = await self.user_management_service.get_account_by_id(
|
||||
user_id=user_id
|
||||
)
|
||||
if await self.user_auth_service.is_flid_reset_required(user_id):
|
||||
return [
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
email.split("@")[0],
|
||||
]
|
||||
|
||||
user_flid = await self.user_auth_service.get_user_flid(user_id)
|
||||
|
||||
# password verification passed
|
||||
return [
|
||||
UserLoginAction.USER_SIGNED_IN,
|
||||
user_account.user_role,
|
||||
user_id,
|
||||
user_flid,
|
||||
]
|
||||
else:
|
||||
# ask user to input password again.
|
||||
# TODO: we need to limit times of user to input the wrong password
|
||||
return [
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
async def update_new_user_flid(
|
||||
self, user_id: str, user_flid: str
|
||||
) -> Tuple[UserLoginAction, Optional[str]]:
|
||||
if await self.user_auth_service.is_flid_available(user_flid):
|
||||
|
||||
code_depot_email = "{}@freeleaps.com".format(user_flid)
|
||||
result = await self.code_depot_service.create_depot_user(
|
||||
user_flid, user_id, code_depot_email
|
||||
)
|
||||
|
||||
if not result:
|
||||
await self.module_logger.log_error(
|
||||
error="Failed to create depot user for {} with flid {} and email {}".format(
|
||||
user_id, user_flid, code_depot_email
|
||||
),
|
||||
properties={
|
||||
"user_id": user_id,
|
||||
"user_flid": user_flid,
|
||||
"code_depot_email": code_depot_email,
|
||||
},
|
||||
)
|
||||
return [
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
"{}{}".format(user_flid, random.randint(100, 999)),
|
||||
]
|
||||
|
||||
await self.user_auth_service.update_flid(user_id, user_flid)
|
||||
|
||||
if await self.user_auth_service.is_password_reset_required(user_id):
|
||||
return [
|
||||
UserLoginAction.NEW_USER_SET_PASSWORD,
|
||||
user_flid,
|
||||
]
|
||||
else:
|
||||
return [
|
||||
UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED,
|
||||
user_flid,
|
||||
]
|
||||
else:
|
||||
return [
|
||||
UserLoginAction.REVIEW_AND_REVISE_FLID,
|
||||
"{}{}".format(user_flid, random.randint(100, 999)),
|
||||
]
|
||||
|
||||
async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction:
|
||||
"""try signin through email, generate auth code and send to the email address
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
host (str): host url that user tried to sign in
|
||||
|
||||
Returns:
|
||||
int: UserLoginAction
|
||||
"""
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
|
||||
is_password_reset_required = False
|
||||
if user_id:
|
||||
is_password_reset_required = (
|
||||
await self.user_auth_service.is_password_reset_required(user_id)
|
||||
)
|
||||
|
||||
if user_id is None or is_password_reset_required:
|
||||
# send auth code through email if the email address
|
||||
# hasn't been associated with any account.
|
||||
# Or if the user's password is empty, which means the user's pasword hasn't been set.
|
||||
|
||||
mail_code = await self.user_auth_service.generate_auth_code_for_email(email)
|
||||
region = RegionHandler().detect_from_host(host)
|
||||
# TODO: Add notification
|
||||
# async with self.notification_center:
|
||||
# await self.notification_center.send_email_notification(
|
||||
# receiver_id=email,
|
||||
# subject="email",
|
||||
# event="authentication",
|
||||
# properties={"auth_code": mail_code},
|
||||
# region=region,
|
||||
# )
|
||||
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE
|
||||
else:
|
||||
return UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED
|
||||
|
||||
async def reset_password_through_email(self, email: str, host: str) -> int:
|
||||
"""verify the email is exisitng, clear the existing password,
|
||||
generate auth code and send to the email address
|
||||
so in the following steps, the user can reset their password.
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
host (str): host that user will perform the reset on
|
||||
|
||||
Returns:
|
||||
int: UserLoginAction
|
||||
"""
|
||||
|
||||
user_id = await self.user_auth_service.get_user_id_by_email(email)
|
||||
if user_id is not None:
|
||||
# send auth code through email if the email address
|
||||
# hasn been associated with any account.
|
||||
mail_code = await self.user_auth_service.generate_auth_code_for_email(email)
|
||||
region = RegionHandler().detect_from_host(host)
|
||||
# TODO: Add notification support
|
||||
# async with self.notification_center:
|
||||
# await self.notification_center.send_email_notification(
|
||||
# receiver_id=email,
|
||||
# subject="email",
|
||||
# event="authentication",
|
||||
# properties={"auth_code": mail_code},
|
||||
# region=region,
|
||||
# )
|
||||
|
||||
await self.user_auth_service.reset_password(user_id)
|
||||
|
||||
return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE
|
||||
else:
|
||||
return UserLoginAction.EMAIL_NOT_ASSOCIATED_WITH_USER
|
||||
|
||||
async def update_user_password(self, user_id: str, password: str) -> dict[str, any]:
|
||||
error_message = """
|
||||
Password does not pass complexity requirements:
|
||||
- At least one lowercase character
|
||||
- At least one uppercase character
|
||||
- At least one digit
|
||||
- At least one special character (punctuation, brackets, quotes, etc.)
|
||||
"""
|
||||
if not check_password_complexity(password):
|
||||
raise InvalidDataError(error_message)
|
||||
|
||||
user_flid = await self.user_auth_service.get_user_flid(user_id)
|
||||
await self.user_auth_service.save_password_auth_method(
|
||||
user_id, user_flid, password
|
||||
)
|
||||
return {"succeeded": True}
|
||||
|
||||
@ -18,6 +18,8 @@ from app.authentication.backend.models.models import (
|
||||
UserPasswordDoc,
|
||||
)
|
||||
|
||||
from app.authentication.backend.models.user_profile.models import BasicProfileDoc
|
||||
|
||||
|
||||
class UserAuthHandler:
|
||||
def __init__(self):
|
||||
@ -286,3 +288,63 @@ class UserAuthHandler:
|
||||
return user_password.password == ""
|
||||
else:
|
||||
return True
|
||||
|
||||
async def is_flid_reset_required(self, user_id: str) -> bool:
|
||||
basic_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.user_id == user_id
|
||||
)
|
||||
|
||||
if basic_profile:
|
||||
return basic_profile.FLID.update_time == basic_profile.FLID.create_time
|
||||
|
||||
async def is_flid_available(self, user_flid: str) -> bool:
|
||||
basic_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.FLID.identity == user_flid
|
||||
)
|
||||
|
||||
if basic_profile:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def get_flid(self, user_id: str) -> str:
|
||||
basic_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.user_id == user_id
|
||||
)
|
||||
|
||||
if basic_profile:
|
||||
return basic_profile.FLID.identity
|
||||
else:
|
||||
return None
|
||||
|
||||
async def update_flid(self, user_id: str, flid: str) -> bool:
|
||||
basic_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.user_id == user_id
|
||||
)
|
||||
|
||||
if basic_profile:
|
||||
basic_profile.FLID.identity = flid
|
||||
basic_profile.FLID.update_time = datetime.now(timezone.utc)
|
||||
basic_profile.FLID.set_by = user_id
|
||||
await basic_profile.save()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
async def generate_auth_code(self, email: str) -> str:
|
||||
"""send auth code to email address
|
||||
|
||||
Args:
|
||||
email (str): email address
|
||||
"""
|
||||
auth_code = generate_auth_code()
|
||||
expiry = datetime.now(timezone.utc) + timedelta(minutes=5)
|
||||
auth_code_doc = AuthCodeDoc(
|
||||
auth_code=auth_code,
|
||||
method=email.lower(),
|
||||
method_type=AuthType.EMAIL,
|
||||
expiry=expiry,
|
||||
)
|
||||
|
||||
await auth_code_doc.create()
|
||||
return auth_code
|
||||
|
||||
@ -1,149 +1,24 @@
|
||||
import pytz
|
||||
import random
|
||||
from infra.log.module_logger import ModuleLogger
|
||||
from typing import Optional
|
||||
|
||||
from app.authentication.backend.common.config.app_settings import app_settings
|
||||
|
||||
from app.authentication.backend.models.constants import DepotStatus
|
||||
from app.authentication.backend.infra.code_management.gitea.gitea import Gitea
|
||||
from app.authentication.backend.models.gitea.models import CodeDepotDoc
|
||||
from infra.exception.exceptions import InvalidOperationError
|
||||
from infra.utils.date import get_sunday
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from dateutil.parser import parse
|
||||
|
||||
|
||||
class CodeDepotHandler:
|
||||
def __init__(self) -> None:
|
||||
self.gitea_url = app_settings.GITEA_URL
|
||||
self.gitea_token = app_settings.GITEA_TOKEN
|
||||
self.gitea_org = app_settings.GITEA_DEPOT_ORGANIZATION
|
||||
|
||||
self.code_depot_domain_name = app_settings.CODE_DEPOT_DOMAIN_NAME
|
||||
self.code_depot_ssh_port = app_settings.CODE_DEPOT_SSH_PORT
|
||||
self.code_depot_http_port = app_settings.CODE_DEPOT_HTTP_PORT
|
||||
|
||||
self.gitea_admin = Gitea(
|
||||
self.gitea_url, token_text=self.gitea_token, auth=None, verify=False
|
||||
)
|
||||
self.product_org = self.gitea_admin.get_org_by_name(self.gitea_org)
|
||||
self.module_logger = ModuleLogger(sender_id="CodeDepotManager")
|
||||
|
||||
async def check_depot_name_availabe(self, code_depot_name: str) -> bool:
|
||||
"""Return True if the depot name is available, otherwise return False
|
||||
|
||||
Parameters:
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
bool: True if the depot name is availabe, otherwise return False
|
||||
"""
|
||||
|
||||
result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name)
|
||||
|
||||
if result:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
pass
|
||||
|
||||
def check_code_depot_exist(self, code_depot_name: str) -> bool:
|
||||
"""Check and return True if the code deport with the name exist in Gitea, otherwise return False
|
||||
|
||||
Parameters:
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
bool: True if the code depot exist, otherwise return False
|
||||
"""
|
||||
all_depots = self.product_org.get_repositories()
|
||||
for depot in all_depots:
|
||||
if depot.name.lower() == code_depot_name.lower():
|
||||
return True
|
||||
|
||||
return False
|
||||
pass
|
||||
|
||||
async def __generate_new_code_depot_name(
|
||||
self, code_depot_name: str, gitea_code_depot_names: list[str]
|
||||
) -> str:
|
||||
"""Generate a new code depot name if the code depot name already exists
|
||||
|
||||
Parameters:
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
str: the new code depot name
|
||||
"""
|
||||
code_depot_name = code_depot_name.lower()
|
||||
depot_doc = await CodeDepotDoc.find_one(
|
||||
CodeDepotDoc.depot_name == code_depot_name
|
||||
)
|
||||
|
||||
# if the depot name already exists in the database or Gitea, generate a new name
|
||||
if depot_doc or (code_depot_name in gitea_code_depot_names):
|
||||
new_code_depot_name = "{}-{}".format(
|
||||
code_depot_name, random.randint(10000, 99999)
|
||||
)
|
||||
while new_code_depot_name in gitea_code_depot_names:
|
||||
new_code_depot_name = "{}-{}".format(
|
||||
code_depot_name, random.randint(10000, 99999)
|
||||
)
|
||||
|
||||
return await self.__generate_new_code_depot_name(
|
||||
new_code_depot_name, gitea_code_depot_names
|
||||
)
|
||||
else:
|
||||
return code_depot_name
|
||||
pass
|
||||
|
||||
async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]:
|
||||
"""Create a new git code depot
|
||||
|
||||
Parameters:
|
||||
product_id (str): the id of the product
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
str: return code depot id if it's created successfully, else return None
|
||||
"""
|
||||
gitea_code_depot_names = [
|
||||
depot.name.lower() for depot in self.product_org.get_repositories()
|
||||
]
|
||||
code_depot_name_to_be_created = await self.__generate_new_code_depot_name(
|
||||
code_depot_name, gitea_code_depot_names
|
||||
)
|
||||
|
||||
new_depot_doc = CodeDepotDoc(
|
||||
depot_name=code_depot_name_to_be_created,
|
||||
product_id=product_id,
|
||||
depot_status=DepotStatus.TO_BE_CREATED,
|
||||
)
|
||||
await new_depot_doc.create()
|
||||
|
||||
try:
|
||||
self.product_org.create_repo(
|
||||
repoName=code_depot_name_to_be_created,
|
||||
description=product_id,
|
||||
private=True,
|
||||
autoInit=True,
|
||||
default_branch="main",
|
||||
)
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to create depot {code_depot_name_to_be_created} in Gitea. Error: {e}",
|
||||
properties={
|
||||
"code_depot_name_to_be_created": code_depot_name_to_be_created
|
||||
},
|
||||
)
|
||||
raise InvalidOperationError(
|
||||
f"Failed to create depot {code_depot_name_to_be_created} in Gitea"
|
||||
)
|
||||
|
||||
await new_depot_doc.set({CodeDepotDoc.depot_status: DepotStatus.CREATED})
|
||||
await new_depot_doc.save()
|
||||
|
||||
return str(new_depot_doc.id)
|
||||
pass
|
||||
|
||||
def get_depot_ssh_url(self, code_depot_name: str) -> str:
|
||||
"""Return the ssh url of the code depot
|
||||
@ -190,227 +65,28 @@ class CodeDepotHandler:
|
||||
return f"http://{user_name}@{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
|
||||
|
||||
async def get_depot_users(self, code_depot_name: str) -> list[str]:
|
||||
"""Return list of user names have permission to access the depot
|
||||
|
||||
Parameters:
|
||||
depot_name (str): the name of the depot
|
||||
|
||||
Returns:
|
||||
list: list of user names
|
||||
"""
|
||||
|
||||
result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name)
|
||||
|
||||
if result:
|
||||
return result.collaborators
|
||||
else:
|
||||
return []
|
||||
pass
|
||||
|
||||
async def update_depot_user_password(self, user_name: str, password: str) -> bool:
|
||||
"""Update the password of the user in Gitea
|
||||
|
||||
Parameters:
|
||||
user_name (str): the name of the user
|
||||
password (str): the new password of the user
|
||||
|
||||
Returns:
|
||||
bool: True if operations succeed, otherwise return False
|
||||
"""
|
||||
depot_user = self.gitea_admin.get_user_by_name(user_name)
|
||||
|
||||
if depot_user:
|
||||
try:
|
||||
self.gitea_admin.update_user_password(user_name, password)
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to update password for user {user_name} in Gitea.",
|
||||
properties={"user_name": user_name},
|
||||
)
|
||||
return False
|
||||
else:
|
||||
await self.module_logger.log_error(
|
||||
error=f"User {user_name} does not exist in Gitea.",
|
||||
properties={"user_name": user_name},
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
pass
|
||||
|
||||
async def create_depot_user(
|
||||
self, user_name: str, password: str, email: str
|
||||
) -> bool:
|
||||
"""Create a new user in Gitea
|
||||
Parameters:
|
||||
user_name (str): the name of the user
|
||||
password (str): the password of the user
|
||||
email (str): email address of the user
|
||||
|
||||
Returns:
|
||||
bool: True if operations succeed, otherwise return False
|
||||
"""
|
||||
depot_user = self.gitea_admin.get_user_by_name(user_name)
|
||||
|
||||
if depot_user:
|
||||
await self.module_logger.log_info(
|
||||
info=f"User {user_name} exist in Gitea.",
|
||||
properties={"user_name": user_name},
|
||||
)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
await self.module_logger.log_info(
|
||||
info=f"Create user {user_name} in Gitea with password.",
|
||||
properties={"user_name": user_name},
|
||||
)
|
||||
depot_user = self.gitea_admin.create_user(
|
||||
user_name=user_name,
|
||||
login_name=user_name,
|
||||
password=password,
|
||||
email=email,
|
||||
change_pw=False,
|
||||
send_notify=False,
|
||||
)
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to create user {user_name} in Gitea.",
|
||||
properties={"user_name": user_name},
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
pass
|
||||
|
||||
async def grant_user_depot_access(
|
||||
self, user_name: str, code_depot_name: str
|
||||
) -> bool:
|
||||
"""Grant user access to the code depot in Gitea
|
||||
|
||||
Parameters:
|
||||
user_name (str): the name of the user
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
bool: True if operations succeed, otherwise return False
|
||||
"""
|
||||
try:
|
||||
code_depot = self.product_org.get_repository(code_depot_name)
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to get depot {code_depot_name} in Gitea.",
|
||||
properties={"code_depot_name": code_depot_name},
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
if code_depot.add_collaborator(user_name=user_name, permission="Write"):
|
||||
code_depot_doc = await CodeDepotDoc.find_one(
|
||||
CodeDepotDoc.depot_name == code_depot_name
|
||||
)
|
||||
|
||||
if code_depot_doc:
|
||||
if user_name not in code_depot_doc.collaborators:
|
||||
code_depot_doc.collaborators.append(user_name)
|
||||
await code_depot_doc.save()
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to grant permission for user {user_name} to access code depot {code_depot_name}.",
|
||||
properties={"user_name": user_name, "code_depot_name": code_depot_name},
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
pass
|
||||
|
||||
async def generate_statistic_result(
|
||||
self, code_depot_name: str
|
||||
) -> Optional[dict[str, any]]:
|
||||
"""Call Gitea API and collect statistic result of the repository.
|
||||
|
||||
Args:
|
||||
code_depot_name (str): the name of the code depot
|
||||
|
||||
Returns:
|
||||
dict[str, any]: statistic result
|
||||
"""
|
||||
|
||||
try:
|
||||
code_depot = self.product_org.get_repository(code_depot_name)
|
||||
except Exception as e:
|
||||
await self.module_logger.log_exception(
|
||||
exception=e,
|
||||
text=f"Failed to get depot {code_depot_name} in Gitea.",
|
||||
properties={"code_depot_name": code_depot_name},
|
||||
)
|
||||
return None
|
||||
|
||||
commits = code_depot.get_commits()
|
||||
|
||||
if len(commits) > 0:
|
||||
commit_dates = []
|
||||
for commit in commits:
|
||||
commit_dates.append(parse(commit.created).date())
|
||||
|
||||
today = datetime.now(timezone.utc)
|
||||
last_sunday = get_sunday(today)
|
||||
# only get commits statistic of the last 20 weeks
|
||||
last_20_sundays = [last_sunday - timedelta(weeks=i) for i in range(20)]
|
||||
|
||||
weekly_commits = {}
|
||||
for date in last_20_sundays:
|
||||
weekly_commits[date.strftime("%Y-%m-%d")] = 0
|
||||
|
||||
for commit_date in commit_dates:
|
||||
commit_date_sunday = get_sunday(commit_date).strftime("%Y-%m-%d")
|
||||
if commit_date_sunday in weekly_commits:
|
||||
weekly_commits[commit_date_sunday] += 1
|
||||
|
||||
last_update = parse(commits[-1].created)
|
||||
results = {
|
||||
"total_commits": len(commits),
|
||||
"last_commiter": commits[-1].commit["committer"]["name"],
|
||||
"last_update": last_update.astimezone(pytz.UTC),
|
||||
"weekly_commits": weekly_commits,
|
||||
}
|
||||
else:
|
||||
results = {
|
||||
"total_commits": 0,
|
||||
"last_commiter": "",
|
||||
"last_update": None,
|
||||
"weekly_commits": {},
|
||||
}
|
||||
|
||||
return results
|
||||
pass
|
||||
|
||||
async def analyze_code_depots(self):
|
||||
code_depot_docs = await CodeDepotDoc.find(
|
||||
CodeDepotDoc.depot_status == DepotStatus.CREATED
|
||||
).to_list()
|
||||
|
||||
for code_depot_doc in code_depot_docs:
|
||||
await self.module_logger.log_info(
|
||||
info="Start to analyze code depot {}".format(code_depot_doc.depot_name),
|
||||
properties={"code_depot_name": code_depot_doc.depot_name},
|
||||
)
|
||||
statistic_result = await self.generate_statistic_result(
|
||||
code_depot_doc.depot_name
|
||||
)
|
||||
|
||||
# cannot get the statistic result, probably it cannot find the
|
||||
# git repository in gitea
|
||||
if not statistic_result:
|
||||
continue
|
||||
|
||||
code_depot_doc.total_commits = statistic_result["total_commits"]
|
||||
code_depot_doc.last_commiter = statistic_result["last_commiter"]
|
||||
code_depot_doc.last_update = statistic_result["last_update"]
|
||||
code_depot_doc.weekly_commits = statistic_result["weekly_commits"]
|
||||
await code_depot_doc.save()
|
||||
pass
|
||||
|
||||
async def fetch_code_depot(self, code_depot_id: str) -> Optional[dict[str, any]]:
|
||||
code_depot_doc = await CodeDepotDoc.get(code_depot_id)
|
||||
if code_depot_doc:
|
||||
return code_depot_doc.model_dump()
|
||||
else:
|
||||
return None
|
||||
pass
|
||||
|
||||
@ -1,2 +1,121 @@
|
||||
from infra.models.constants import UserRegion
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from app.authentication.backend.models.user.models import UserAccountDoc
|
||||
from app.authentication.backend.models.user.constants import (
|
||||
UserAccountProperty,
|
||||
)
|
||||
from app.authentication.backend.models.permission.constants import (
|
||||
AdministrativeRole,
|
||||
Capability,
|
||||
)
|
||||
from typing import Optional
|
||||
from app.authentication.backend.models.user_profile.models import (
|
||||
SelfIntro,
|
||||
Tags,
|
||||
Photo,
|
||||
Email,
|
||||
Mobile,
|
||||
FLID,
|
||||
Password,
|
||||
BasicProfileDoc,
|
||||
ProviderProfileDoc,
|
||||
ExpectedSalary,
|
||||
)
|
||||
|
||||
from app.authentication.backend.models.user.constants import UserRegionToCurrency
|
||||
|
||||
|
||||
class UserProfileHandler:
|
||||
pass
|
||||
async def create_new_user_account(
|
||||
self,
|
||||
property: UserAccountProperty,
|
||||
capability: Capability,
|
||||
user_role: AdministrativeRole,
|
||||
region: UserRegion,
|
||||
) -> UserAccountDoc:
|
||||
user_account = UserAccountDoc(
|
||||
profile_id=None,
|
||||
account_id=None,
|
||||
service_plan_id=None,
|
||||
properties=int(property),
|
||||
capabilities=int(capability),
|
||||
user_role=int(user_role),
|
||||
region=region,
|
||||
)
|
||||
return await user_account.create()
|
||||
|
||||
async def create_basic_profile(
|
||||
self,
|
||||
user_id: str,
|
||||
email_address: str,
|
||||
email_verified: bool,
|
||||
mobile_number: str,
|
||||
mobile_verified: bool,
|
||||
password_setup: bool,
|
||||
region: UserRegion,
|
||||
time_zone: Optional[str] = "UTC",
|
||||
) -> BasicProfileDoc:
|
||||
basic_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.user_id == user_id
|
||||
)
|
||||
if basic_profile:
|
||||
return basic_profile
|
||||
else:
|
||||
tags = Tags(skill=[])
|
||||
self_intro = SelfIntro(summary="", content_html="", tags=tags)
|
||||
photo = Photo(url="", base64="", filename="")
|
||||
email = Email(address=email_address, verified=email_verified)
|
||||
mobile = Mobile(number=mobile_number, verified=mobile_verified)
|
||||
current_time = datetime.now(timezone.utc)
|
||||
flid = FLID(
|
||||
identity=user_id,
|
||||
set_by=user_id,
|
||||
create_time=current_time,
|
||||
update_time=current_time,
|
||||
)
|
||||
password = Password(
|
||||
set_up=password_setup,
|
||||
update_time=current_time,
|
||||
expiry=(current_time + timedelta(days=365)),
|
||||
)
|
||||
basic_profile = BasicProfileDoc(
|
||||
user_id=user_id,
|
||||
self_intro=self_intro,
|
||||
photo=photo,
|
||||
email=email,
|
||||
mobile=mobile,
|
||||
FLID=flid,
|
||||
password=password,
|
||||
region=region,
|
||||
time_zone=time_zone,
|
||||
)
|
||||
new_basic_profile = await basic_profile.create()
|
||||
return new_basic_profile
|
||||
|
||||
async def create_provider_profile(self, user_id: str) -> ProviderProfileDoc:
|
||||
provider_profile = await ProviderProfileDoc.find_one(
|
||||
ProviderProfileDoc.user_id == user_id
|
||||
)
|
||||
if provider_profile:
|
||||
return provider_profile
|
||||
else:
|
||||
region = await self.__get_user_region(user_id)
|
||||
expected_salary = ExpectedSalary(
|
||||
currency=UserRegionToCurrency[region], hourly=0.0
|
||||
)
|
||||
provider_profile = ProviderProfileDoc(
|
||||
user_id=user_id,
|
||||
expected_salary=expected_salary,
|
||||
accepting_request=False,
|
||||
)
|
||||
new_provider_profile = await provider_profile.create()
|
||||
return new_provider_profile
|
||||
|
||||
async def get_account_by_id(self, user_id: str) -> UserAccountDoc:
|
||||
return await UserAccountDoc.get(user_id)
|
||||
|
||||
async def __get_user_region(self, user_id: str) -> UserRegion:
|
||||
user_profile = await BasicProfileDoc.find_one(
|
||||
BasicProfileDoc.user_id == user_id
|
||||
)
|
||||
return user_profile.region if user_profile else UserRegion.OTHER
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from enum import IntEnum
|
||||
from infra.models.constants import UserRegion
|
||||
|
||||
|
||||
class NewUserMethod(IntEnum):
|
||||
@ -22,3 +23,15 @@ class UserLoginAction(IntEnum):
|
||||
EMAIL_NOT_ASSOCIATED_WITH_USER = 3
|
||||
REVIEW_AND_REVISE_FLID = 4
|
||||
USER_SIGNED_IN = 100
|
||||
|
||||
|
||||
class Currency(IntEnum):
|
||||
UNKNOWN = 0
|
||||
USD = 1
|
||||
CNY = 2
|
||||
|
||||
|
||||
UserRegionToCurrency = {
|
||||
UserRegion.ZH_CN: Currency.CNY.name,
|
||||
UserRegion.OTHER: Currency.USD.name,
|
||||
}
|
||||
|
||||
@ -3,7 +3,10 @@ from typing import Optional
|
||||
from beanie import Document
|
||||
|
||||
from .constants import UserAccountProperty
|
||||
from .permission.constants import AdministrativeRole, Capability
|
||||
from app.authentication.backend.models.permission.constants import (
|
||||
AdministrativeRole,
|
||||
Capability,
|
||||
)
|
||||
from infra.models.constants import UserRegion
|
||||
|
||||
|
||||
|
||||
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 (
|
||||
UserAuthManager,
|
||||
from app.authentication.backend.infra.auth.user_auth_handler import (
|
||||
UserAuthHandler,
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UserAuthService:
|
||||
def __init__(self):
|
||||
self.user_auth_manager = UserAuthManager()
|
||||
self.user_auth_handler = UserAuthHandler()
|
||||
|
||||
async def get_user_id_by_email(self, email: str) -> Optional[str]:
|
||||
return await self.user_auth_manager.get_user_id_by_email(email)
|
||||
return await self.user_auth_handler.get_user_id_by_email(email)
|
||||
|
||||
async def verify_email_code(self, email: str, code: str) -> bool:
|
||||
return await self.user_auth_manager.verify_email_code(email, code)
|
||||
|
||||
async def create_new_user_account(self, method: str, region: str) -> str:
|
||||
return await self.user_auth_manager.create_user_account(method, region)
|
||||
|
||||
async def initialize_new_user_data(
|
||||
self, user_id: str, email: str, region: str, time_zone: str
|
||||
):
|
||||
# Initialize user data
|
||||
await self.user_auth_manager.init_user_data(user_id, email, region, time_zone)
|
||||
|
||||
async def get_user_account(self, user_id: str):
|
||||
return await self.user_auth_manager.get_user_account(user_id)
|
||||
async def verify_email_with_code(self, email: str, code: str) -> bool:
|
||||
return await self.user_auth_handler.verify_email_code(email, code)
|
||||
|
||||
async def is_password_reset_required(self, user_id: str) -> bool:
|
||||
return await self.user_auth_manager.is_password_reset_required(user_id)
|
||||
return await self.user_auth_handler.is_password_reset_required(user_id)
|
||||
|
||||
async def is_flid_reset_required(self, user_id: str) -> bool:
|
||||
return await self.user_auth_handler.is_flid_reset_required(user_id)
|
||||
|
||||
async def is_flid_available(self, user_flid: str) -> bool:
|
||||
return await self.user_auth_handler.is_flid_available(user_flid)
|
||||
|
||||
async def get_user_flid(self, user_id: str) -> str:
|
||||
return await self.user_auth_handler.get_flid(user_id)
|
||||
|
||||
async def update_flid(self, user_id: str, user_flid: str) -> str:
|
||||
return await self.user_auth_handler.update_flid(user_id, user_flid)
|
||||
|
||||
async def generate_auth_code_for_email(self, email: str) -> str:
|
||||
return await self.user_auth_handler.generate_auth_code(email)
|
||||
|
||||
async def verify_user_with_password(self, user_id: str, password: str) -> bool:
|
||||
return await self.user_auth_handler.verify_user_with_password(user_id, password)
|
||||
|
||||
async def reset_password(self, user_id: str):
|
||||
return await self.user_auth_handler.reset_password(user_id)
|
||||
|
||||
async def save_password_auth_method(
|
||||
self, user_id: str, user_flid: str, password: str
|
||||
):
|
||||
return await self.user_auth_handler.save_password_auth_method(
|
||||
user_id, user_flid, password
|
||||
)
|
||||
|
||||
@ -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 typing import Optional
|
||||
|
||||
from app.authentication.backend.application.models.user.constants import (
|
||||
from app.authentication.backend.models.user.constants import (
|
||||
NewUserMethod,
|
||||
UserAccountProperty,
|
||||
UserLoginAction,
|
||||
)
|
||||
from app.authentication.backend.application.user.models import UserAccountDoc
|
||||
from app.authentication.backend.business.permission.constants import (
|
||||
from app.authentication.backend.models.user.models import UserAccountDoc
|
||||
from app.authentication.backend.models.permission.constants import (
|
||||
AdministrativeRole,
|
||||
Capability,
|
||||
)
|
||||
from app.authentication.backend.infra.auth.user_auth_handler import (
|
||||
UserAuthHandler,
|
||||
)
|
||||
from app.authentication.backend.infra.user.user_profile_handler import (
|
||||
from app.authentication.backend.infra.user_profile.user_profile_handler import (
|
||||
UserProfileHandler,
|
||||
)
|
||||
from backend.infra.config.backend import settings
|
||||
from backend.infra.log.log_utils import log_entry_exit_async
|
||||
from backend.business.credit.user import UserAchievement
|
||||
from backend.services.profile.basic import BasicProfileStore
|
||||
from backend.services.profile.provider import ProviderProfileStore
|
||||
from backend.services.common.constants import UserRegion
|
||||
from backend.services.common.region import RegionHandler
|
||||
from backend.infra.depot.depot_manager import CodeDepotManager
|
||||
from backend.infra.utils.string import check_password_complexity
|
||||
from backend.business.notification.notification_center import NotificationCenter
|
||||
from backend.business.events.user_event_dispatcher import UserEventDispatcher
|
||||
from backend.infra.exception.exceptions import InvalidDataError
|
||||
from infra.log.log_utils import log_entry_exit_async
|
||||
from infra.models.constants import UserRegion
|
||||
|
||||
|
||||
class UserManagementService:
|
||||
@ -41,7 +29,7 @@ class UserManagementService:
|
||||
@log_entry_exit_async
|
||||
async def create_new_user_account(
|
||||
self, method: NewUserMethod, region: UserRegion
|
||||
) -> str:
|
||||
) -> UserAccountDoc:
|
||||
"""create a new user account document in DB
|
||||
|
||||
Args:
|
||||
@ -52,29 +40,60 @@ class UserManagementService:
|
||||
str: id of user account
|
||||
"""
|
||||
if NewUserMethod.EMAIL == method:
|
||||
user_account = UserAccountDoc(
|
||||
profile_id=None,
|
||||
account_id=None,
|
||||
service_plan_id=None,
|
||||
properties=int(UserAccountProperty.EMAIL_VERIFIED),
|
||||
capabilities=int(Capability.VISITOR),
|
||||
user_role=int(AdministrativeRole.PERSONAL),
|
||||
region=region,
|
||||
user_account = await self.user_profile_handler.create_new_user_account(
|
||||
UserAccountProperty.EMAIL_VERIFIED,
|
||||
Capability.VISITOR,
|
||||
AdministrativeRole.PERSONAL,
|
||||
region,
|
||||
)
|
||||
user_account = await user_account.create()
|
||||
|
||||
elif NewUserMethod.MOBILE == method:
|
||||
user_account = UserAccountDoc(
|
||||
profile_id=None,
|
||||
account_id=None,
|
||||
service_plan_id=None,
|
||||
properties=int(UserAccountProperty.MOBILE_VERIFIED),
|
||||
capabilities=int(Capability.VISITOR),
|
||||
user_role=int(AdministrativeRole.PERSONAL),
|
||||
region=region,
|
||||
user_account = await self.user_profile_handler.create_new_user_account(
|
||||
UserAccountProperty.EMAIL_VERIFIED,
|
||||
Capability.VISITOR,
|
||||
AdministrativeRole.PERSONAL,
|
||||
region,
|
||||
)
|
||||
user_account = await user_account.create()
|
||||
|
||||
# Create other doc in collections for the new user
|
||||
await UserAchievement(str(user_account.id)).create_activeness_achievement()
|
||||
return str(user_account.id)
|
||||
# TODO: Should convert to notification
|
||||
# await UserAchievement(str(user_account.id)).create_activeness_achievement()
|
||||
return user_account
|
||||
|
||||
async def initialize_new_user_data(
|
||||
self,
|
||||
user_id: str,
|
||||
method: NewUserMethod,
|
||||
email_address: str = None,
|
||||
mobile_number: str = None,
|
||||
region: UserRegion = UserRegion.ZH_CN,
|
||||
time_zone: Optional[str] = "UTC",
|
||||
):
|
||||
"""Init data for the new user
|
||||
|
||||
Args:
|
||||
user_id (str): user id
|
||||
method (NewUserMethod): the method the new user came from
|
||||
|
||||
Returns:
|
||||
result: True if initilize data for the new user successfully, else return False
|
||||
"""
|
||||
|
||||
# create basic and provider profile doc for the new user
|
||||
if NewUserMethod.EMAIL == method:
|
||||
await self.user_profile_handler.create_basic_profile(
|
||||
user_id, email_address, True, None, False, False, region, time_zone
|
||||
)
|
||||
await self.user_auth_handler.save_email_auth_method(user_id, email_address)
|
||||
elif NewUserMethod.MOBILE == method:
|
||||
await self.user_profile_handler.create_basic_profile(
|
||||
user_id, None, False, mobile_number, True, False, region, time_zone
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
await self.user_profile_handler.create_provider_profile(user_id)
|
||||
return True
|
||||
|
||||
async def get_account_by_id(self, user_id: str) -> UserAccountDoc:
|
||||
return await self.user_profile_handler.get_account_by_id(user_id)
|
||||
|
||||
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.openapi.utils import get_openapi
|
||||
|
||||
from webapi.providers import common
|
||||
from webapi.providers import logger
|
||||
from webapi.providers import router
|
||||
from webapi.providers import database
|
||||
from webapi.providers import scheduler
|
||||
from webapi.providers import exception_handler
|
||||
from app.authentication.webapi.providers import common
|
||||
from app.authentication.webapi.providers import logger
|
||||
from app.authentication.webapi.providers import router
|
||||
from app.authentication.webapi.providers import database
|
||||
from app.authentication.webapi.providers import scheduler
|
||||
from app.authentication.webapi.providers import exception_handler
|
||||
from .freeleaps_app import FreeleapsApp
|
||||
|
||||
|
||||
@ -49,11 +49,7 @@ def customize_openapi_security(app: FastAPI) -> None:
|
||||
|
||||
# Add security scheme to components
|
||||
openapi_schema["components"]["securitySchemes"] = {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
|
||||
}
|
||||
|
||||
# Add security requirement globally
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
from webapi.bootstrap.application import create_app
|
||||
from webapi.config.site_settings import site_settings
|
||||
from app.authentication.webapi.bootstrap.application import create_app
|
||||
from app.authentication.webapi.config.site_settings import site_settings
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from strawberry.fastapi import GraphQLRouter
|
||||
from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler
|
||||
import uvicorn
|
||||
from typing import Any
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
@app.get("/", status_code=301)
|
||||
async def root():
|
||||
"""
|
||||
@ -19,12 +18,16 @@ async def root():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT)
|
||||
uvicorn.run(
|
||||
app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT
|
||||
)
|
||||
|
||||
|
||||
def get_context() -> Any:
|
||||
# Define your context function. This is where you can set up authentication, database connections, etc.
|
||||
return {}
|
||||
|
||||
|
||||
def get_root_value() -> Any:
|
||||
# Define your root value function. This can be used to customize the root value for GraphQL operations.
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from webapi.config.site_settings import site_settings
|
||||
from app.authentication.webapi.config.site_settings import site_settings
|
||||
|
||||
|
||||
def register(app):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import sys
|
||||
from loguru import logger
|
||||
from common.config.log_settings import log_settings
|
||||
from infra.config.log_settings import log_settings
|
||||
|
||||
|
||||
def register(app=None):
|
||||
@ -21,15 +21,8 @@ def register(app=None):
|
||||
logging.getLogger(name).propagate = True
|
||||
|
||||
# configure loguru
|
||||
logger.add(
|
||||
sink=sys.stdout
|
||||
)
|
||||
logger.add(
|
||||
sink=file_path,
|
||||
level=level,
|
||||
retention=retention,
|
||||
rotation=rotation
|
||||
)
|
||||
logger.add(sink=sys.stdout)
|
||||
logger.add(sink=file_path, level=level, retention=retention, rotation=rotation)
|
||||
|
||||
logger.disable("pika.adapters")
|
||||
logger.disable("pika.connection")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from webapi.routes import api_router
|
||||
from app.authentication.webapi.routes import api_router
|
||||
|
||||
from starlette import routing
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from .signin import router as signin_router
|
||||
from .tokens import router as token_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(signin_router, tags=["user"])
|
||||
api_router.include_router(token_router, tags=["auth"])
|
||||
websocket_router = APIRouter()
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
from .try_signin_with_email import router as ts_router
|
||||
from .signin_with_email_and_password import router as se_router
|
||||
from .signin_with_email_and_code import router as sw_router
|
||||
from .update_user_password import router as up_router
|
||||
from .signin_with_email_and_password import router as se_router
|
||||
from .sign_out import router as so_router
|
||||
from .update_new_user_flid import router as uu_router
|
||||
from .reset_password_through_email import router as rp_router
|
||||
from .refresh_token import router as rt_router
|
||||
from .update_user_flid import router as uu_router
|
||||
from .sign_out import router as so_router
|
||||
|
||||
|
||||
router = APIRouter(prefix="/signin")
|
||||
|
||||
@ -17,4 +17,3 @@ router.include_router(se_router, tags=["signin"])
|
||||
router.include_router(so_router, tags=["signin"])
|
||||
router.include_router(rp_router, tags=["signin"])
|
||||
router.include_router(uu_router, tags=["signin"])
|
||||
router.include_router(rt_router, tags=["signin"])
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from backend.application.user.user_manager import UserManager
|
||||
from app.authentication.backend.application.signin_hub import SignInHub
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@ -32,6 +32,6 @@ class UserSignWithEmailResponse(BaseModel):
|
||||
async def reset_password_through_email(
|
||||
item: UserSignWithEmailBody,
|
||||
):
|
||||
result = await UserManager().reset_password_through_email(item.email, item.host)
|
||||
result = await SignInHub().reset_password_through_email(item.email, item.host)
|
||||
result = {"action": result}
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
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 pydantic import BaseModel
|
||||
|
||||
from backend.application.user.user_manager import UserManager
|
||||
from backend.infra.authentication.auth import access_security
|
||||
from app.authentication.backend.application.signin_hub import SignInHub
|
||||
from infra.token.token_manager import TokenManager
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
token_manager = TokenManager()
|
||||
|
||||
# Web API
|
||||
# signin-with-email-n-code
|
||||
@ -56,7 +58,7 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
identity,
|
||||
flid,
|
||||
preferred_region,
|
||||
) = await UserManager().signin_with_email_and_code(
|
||||
) = await SignInHub().signin_with_email_and_code(
|
||||
item.email, item.code, item.host, item.time_zone
|
||||
)
|
||||
|
||||
@ -66,9 +68,11 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
|
||||
if signed_in and identity and adminstrative_role:
|
||||
subject = {"id": identity, "role": adminstrative_role}
|
||||
access_token = access_security.create_access_token(subject=subject)
|
||||
refresh_token = access_security.create_refresh_token(subject=subject)
|
||||
expires_in = datetime.now(timezone.utc) + access_security.access_expires_delta
|
||||
access_token = token_manager.create_access_token(subject=subject)
|
||||
refresh_token = token_manager.create_refresh_token(subject=subject)
|
||||
expires_in = (
|
||||
datetime.now(timezone.utc) + token_manager.access_token_expire_minutes
|
||||
)
|
||||
else:
|
||||
access_token = None
|
||||
refresh_token = None
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Depends, HTTPException
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
from backend.application.user.user_manager import UserManager
|
||||
from app.authentication.backend.application.signin_hub import SignInHub
|
||||
from infra.token.token_manager import TokenManager
|
||||
|
||||
router = APIRouter()
|
||||
token_manager = TokenManager()
|
||||
|
||||
# Web API
|
||||
# signin-with-email-n-code
|
||||
# signin-with-email-n-password
|
||||
#
|
||||
|
||||
|
||||
class RequestIn(BaseModel):
|
||||
email: str
|
||||
code: str
|
||||
host: str
|
||||
time_zone: Optional[str] = "UTC"
|
||||
password: str
|
||||
|
||||
|
||||
class ResponseOut(BaseModel):
|
||||
@ -37,38 +38,38 @@ class ResponseOut(BaseModel):
|
||||
expires_in: Optional[datetime] = None
|
||||
# the system assigned role of the user.
|
||||
role: Optional[int] = None
|
||||
# preferred region for user
|
||||
preferred_region: Optional[str] = None
|
||||
# the flid of the user
|
||||
flid: Optional[str] = None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/signin-with-email-and-code",
|
||||
operation_id="user-signin-with-email-and-code",
|
||||
summary="try to signin with email and authentication code",
|
||||
description="client user is trying to sign in with their email and the authenication code \
|
||||
the system sent to the email in previous step.",
|
||||
"/signin-with-email-and-password",
|
||||
operation_id="user-signin-with-email-and-password",
|
||||
summary="try to signin with email and password",
|
||||
description="client user is trying to sign in with their email and the password .",
|
||||
response_model=ResponseOut,
|
||||
)
|
||||
async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
async def signin_with_email_and_password(
|
||||
item: RequestIn,
|
||||
) -> ResponseOut:
|
||||
(
|
||||
signed_in,
|
||||
adminstrative_role,
|
||||
identity,
|
||||
flid,
|
||||
preferred_region,
|
||||
) = await UserManager().signin_with_email_and_code(
|
||||
item.email, item.code, item.host, item.time_zone
|
||||
)
|
||||
) = await SignInHub().signin_with_email_and_password(item.email, item.password)
|
||||
|
||||
logging.debug(
|
||||
f"signedin={signed_in}, adminstrative_role={adminstrative_role}, identity={identity}"
|
||||
)
|
||||
|
||||
if signed_in and identity and adminstrative_role:
|
||||
if signed_in and adminstrative_role and identity:
|
||||
subject = {"id": identity, "role": adminstrative_role}
|
||||
access_token = token_manager.create_access_token(subject=subject)
|
||||
refresh_token = token_manager.create_refresh_token(subject=subject)
|
||||
expires_in = datetime.now(timezone.utc) + timedelta(minutes=30)
|
||||
expires_in = (
|
||||
datetime.now(timezone.utc) + token_manager.access_token_expire_minutes
|
||||
)
|
||||
else:
|
||||
access_token = None
|
||||
refresh_token = None
|
||||
@ -82,6 +83,5 @@ async def signin_with_email_and_code(item: RequestIn) -> ResponseOut:
|
||||
"expires_in": expires_in,
|
||||
"role": adminstrative_role,
|
||||
"flid": flid,
|
||||
"preferred_region": preferred_region,
|
||||
}
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
@ -1,31 +1,38 @@
|
||||
from fastapi import APIRouter, Security
|
||||
from app.authentication.backend.application.signin_hub import SignInHub
|
||||
from pydantic import BaseModel
|
||||
from fastapi import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_jwt import JwtAuthorizationCredentials
|
||||
from backend.infra.authentication.auth import access_security
|
||||
from backend.application.user.user_manager import (
|
||||
UserManager,
|
||||
) # Assuming UserManager handles user-related queries
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Web API
|
||||
# try_signin_with_email
|
||||
#
|
||||
|
||||
@router.get(
|
||||
"/get-latest-login-by-user-id/{user_id}",
|
||||
operation_id="get-latest-login-by-user-id",
|
||||
summary="Get the latest login timestamp for a given user",
|
||||
description="Fetches the latest login timestamp for a specific user by user_id.",
|
||||
response_description="Returns the latest login timestamp in Unix time seconds, or null if no login found",
|
||||
|
||||
class UserSignWithEmailBody(BaseModel):
|
||||
email: str
|
||||
host: str
|
||||
|
||||
|
||||
class UserSignWithEmailResponse(BaseModel):
|
||||
signin_type: int
|
||||
|
||||
|
||||
@router.post(
|
||||
"/try-signin-with-email",
|
||||
operation_id="user-try-signin-with-email",
|
||||
summary="try to signin with email",
|
||||
description="A client user is trying to sign in with their email. \
|
||||
The system will determine to send an authentication code to the email \
|
||||
or let the uesr use their FLID and passward to sign in",
|
||||
response_description="signin_type:0 meaning simplified(using email) signin, \
|
||||
1 meaning standard(using FLID and passward) signin",
|
||||
)
|
||||
async def get_latest_login_by_user_id(
|
||||
user_id: str,
|
||||
credentials: JwtAuthorizationCredentials = Security(access_security),
|
||||
async def try_signin_with_email(
|
||||
item: UserSignWithEmailBody,
|
||||
):
|
||||
# Assume UserManager is responsible for handling user data
|
||||
result = await UserManager().fetch_latest_login(user_id)
|
||||
|
||||
if result is None:
|
||||
return JSONResponse(content=jsonable_encoder({"timestamp": None}))
|
||||
|
||||
return JSONResponse(content=jsonable_encoder({"timestamp": result}))
|
||||
result = await SignInHub().try_signin_with_email(item.email, item.host)
|
||||
result = {"signin_type": result}
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
@ -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 fastapi import APIRouter, Security
|
||||
from backend.infra.authentication.auth import access_security
|
||||
from fastapi_jwt import JwtAuthorizationCredentials
|
||||
from fastapi import APIRouter
|
||||
from infra.token.token_manager import TokenManager
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import Depends, HTTPException
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
token_manager = TokenManager()
|
||||
# Web API
|
||||
# update_user_password
|
||||
#
|
||||
@ -28,9 +30,14 @@ class RequestIn(BaseModel):
|
||||
)
|
||||
async def update_user_password(
|
||||
item: RequestIn,
|
||||
credentials: JwtAuthorizationCredentials = Security(access_security),
|
||||
current_user: dict = Depends(token_manager.get_current_user),
|
||||
):
|
||||
user_id = credentials["id"]
|
||||
user_id = current_user.get("id")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
|
||||
)
|
||||
if item.password != item.password2:
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(
|
||||
@ -38,5 +45,5 @@ async def update_user_password(
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = await UserManager().update_user_password(user_id, item.password)
|
||||
result = await SignInHub().update_user_password(user_id, item.password)
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
@ -16,4 +16,4 @@ EXPOSE 8005
|
||||
|
||||
|
||||
# Run the application using the start script
|
||||
CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0"]
|
||||
CMD ["uvicorn", "app.central_storage.webapi.main:app", "--reload", "--port=8005", "--host=0.0.0.0", "--log-level", "warning"]
|
||||
|
||||
@ -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 sys
|
||||
from loguru import logger
|
||||
from app.central_storage.common.config.log_settings import log_settings
|
||||
from infra.config.log_settings import log_settings
|
||||
|
||||
|
||||
def register(app=None):
|
||||
|
||||
@ -29,6 +29,20 @@ services:
|
||||
volumes:
|
||||
- .:/app # Mount the current directory to /app in the container
|
||||
|
||||
authentication:
|
||||
build:
|
||||
context: app/authentication
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "8004:8004" # Map the central_storage service port
|
||||
networks:
|
||||
- freeleaps_service_hub_network
|
||||
env_file:
|
||||
- sites/authentication/.env
|
||||
volumes:
|
||||
- .:/app # Mount the current directory to /app in the container
|
||||
|
||||
networks:
|
||||
freeleaps_service_hub_network:
|
||||
driver: bridge
|
||||
|
||||
@ -3,6 +3,9 @@ from pydantic_settings import BaseSettings
|
||||
|
||||
class AppSettings(BaseSettings):
|
||||
JWT_SECRET_KEY: str = ""
|
||||
APPLICATION_ACTIVITY_LOG: str = "application-log"
|
||||
USER_ACTIVITY_LOG: str = "user-activity-log"
|
||||
BUSINESS_METRIC_LOG: str = "business-metric-log"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
|
||||
MONGODB_NAME: str = "freeleaps2"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from .base_logger import LoggerBase
|
||||
from common.config.app_settings import app_settings
|
||||
from infra.config.app_settings import app_settings
|
||||
import json
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from loguru import logger as guru_logger
|
||||
from common.config.log_settings import log_settings
|
||||
from infra.config.log_settings import log_settings
|
||||
from typing import List
|
||||
import socket
|
||||
import json
|
||||
@ -11,9 +11,7 @@ class LoggerBase:
|
||||
binded_loggers = {}
|
||||
logger_lock = threading.Lock()
|
||||
|
||||
def __init__(
|
||||
self, logger_name: str, extra_fileds: dict[str, any]
|
||||
) -> None:
|
||||
def __init__(self, logger_name: str, extra_fileds: dict[str, any]) -> None:
|
||||
self.__logger_name = logger_name
|
||||
self.extra_fileds = extra_fileds
|
||||
with LoggerBase.logger_lock:
|
||||
@ -21,9 +19,7 @@ class LoggerBase:
|
||||
self.logger = LoggerBase.binded_loggers[self.__logger_name]
|
||||
return
|
||||
|
||||
log_filename = (
|
||||
log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log"
|
||||
)
|
||||
log_filename = log_settings.LOG_PATH_BASE + "/" + self.__logger_name + ".log"
|
||||
log_retention = log_settings.LOG_RETENTION
|
||||
log_rotation = log_settings.LOG_ROTATION
|
||||
log_level = "INFO"
|
||||
@ -57,14 +53,14 @@ class LoggerBase:
|
||||
subject: str,
|
||||
event: str,
|
||||
properties: dict[str, any],
|
||||
text: str = ""
|
||||
text: str = "",
|
||||
) -> None:
|
||||
local_logger = self.logger.bind(
|
||||
sender_id=sender_id,
|
||||
receiver_id=receiver_id,
|
||||
subject=subject,
|
||||
event=event,
|
||||
properties=properties
|
||||
properties=properties,
|
||||
)
|
||||
local_logger.info(text)
|
||||
|
||||
@ -83,7 +79,7 @@ class LoggerBase:
|
||||
subject=subject,
|
||||
event="exception",
|
||||
properties=properties,
|
||||
exception=exception
|
||||
exception=exception,
|
||||
)
|
||||
local_logger.exception(text)
|
||||
|
||||
@ -103,7 +99,7 @@ class LoggerBase:
|
||||
properties=properties,
|
||||
)
|
||||
local_logger.info(text)
|
||||
|
||||
|
||||
async def log_warning(
|
||||
self,
|
||||
sender_id: str,
|
||||
@ -120,7 +116,7 @@ class LoggerBase:
|
||||
properties=properties,
|
||||
)
|
||||
local_logger.warning(text)
|
||||
|
||||
|
||||
async def log_error(
|
||||
self,
|
||||
sender_id: str,
|
||||
|
||||
@ -2,6 +2,9 @@ from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict
|
||||
from jose import jwt, JWTError
|
||||
from infra.config.app_settings import app_settings
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TokenManager:
|
||||
@ -61,3 +64,17 @@ class TokenManager:
|
||||
return self.create_access_token(subject)
|
||||
else:
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
async def get_current_user(
|
||||
self, token: str = Depends(OAuth2PasswordBearer(tokenUrl="token"))
|
||||
) -> Dict:
|
||||
"""
|
||||
Extract and validate user information from the JWT token.
|
||||
"""
|
||||
try:
|
||||
payload = self.decode_token(token) # Decode JWT token
|
||||
return payload
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
|
||||
)
|
||||
|
||||
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