Add all sign-in and token management APIs, localhost up, need clean-up and tuning Gitea APIs, Notification center

This commit is contained in:
jetli 2024-10-20 19:04:04 +00:00
parent e118af5d53
commit 26eedb1b89
36 changed files with 1039 additions and 678 deletions

View 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"]

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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

View 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"

View File

@ -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
)

View File

@ -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

View File

@ -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)

View 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]

View File

@ -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

View File

@ -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 {}

View File

@ -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):

View File

@ -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")

View File

@ -1,4 +1,4 @@
from webapi.routes import api_router
from app.authentication.webapi.routes import api_router
from starlette import routing

View File

@ -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()

View File

@ -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"])

View File

@ -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))

View 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))

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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"]

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -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"
)

View File

@ -0,0 +1 @@
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==