Temp commit

This commit is contained in:
jetli 2024-10-19 23:28:13 +00:00
parent 378ae41b39
commit e118af5d53
51 changed files with 3782 additions and 25 deletions

View File

@ -0,0 +1,151 @@
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
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)
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]]:
"""
Interacts with the business layer to handle the sign-in process with email and code.
"""
return await self.signin_manager.signin_with_email_and_code(
email=email, code=code, host=host, time_zone=time_zone
)
@log_entry_exit_async
async def signin_with_email_and_code(
self, email: str, code: str, host: str, time_zone: Optional[str] = "UTC"
) -> 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.
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
"""
# 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,
]
@log_entry_exit_async
async def __create_new_user_account(
self, method: NewUserMethod, region: UserRegion
) -> str:
"""create a new user account document in DB
Args:
method (NewUserMethod): the method the new user came from
region : preferred user region detected via the user log-in website
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()
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()
# Create other doc in collections for the new user
await UserAchievement(str(user_account.id)).create_activeness_achievement()
return str(user_account.id)

View File

@ -0,0 +1,95 @@
# business/auth/signin_business.py
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 infra.exception.exceptions import InvalidAuthCodeException
from app.authentication.backend.models.constants import UserLoginAction
from app.authentication.backend.services.user.user_management_service import (
UserManagementService,
)
class SignInManager:
def __init__(self):
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()
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]]:
"""
Handles the business logic for signing in with email and code.
"""
user_id = await self.user_auth_service.get_user_id_by_email(email)
is_new_user = user_id is None
preferred_region = self.region_handler.detect_from_host(host)
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_service.get_user_account(user_id)
# 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):
return (
UserLoginAction.REVIEW_AND_REVISE_FLID,
user_account.role,
user_id,
email.split("@")[0],
preferred_region,
)
flid = await self.profile_service.get_user_flid(user_id)
if await self.user_service.is_password_reset_required(user_id):
return (
UserLoginAction.NEW_USER_SET_PASSWORD,
user_account.role,
user_id,
flid,
preferred_region,
)
return (
UserLoginAction.USER_SIGNED_IN,
user_account.role,
user_id,
flid,
preferred_region,
)
else:
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()
is_new_user = user_id is None
preferred_region = self.region_handler.detect_from_host(host)
return user_id, is_new_user, preferred_region

View File

@ -0,0 +1,288 @@
import bcrypt
from datetime import datetime, timedelta, timezone
from typing import Optional
from infra.utils.string import generate_auth_code
from app.authentication.backend.infra.code_management.depot_handler import (
CodeDepotHandler,
)
from app.authentication.backend.models.constants import (
AuthType,
)
from app.authentication.backend.models.models import (
AuthCodeDoc,
UserEmailDoc,
UserMobileDoc,
UserPasswordDoc,
)
class UserAuthHandler:
def __init__(self):
self.code_depot_manager = CodeDepotHandler()
async def verify_user_with_password(self, user_id: str, password: str) -> bool:
"""Verify user's password
Args:
user_id (str): user identity, _id in UserAccountDoc
password (str): password user provided, clear text
Returns:
bool: True if password is correct, else return False
"""
user_password = await UserPasswordDoc.find(
UserPasswordDoc.user_id == user_id
).first_or_none()
if user_password:
# password is reseted to empty string, cannot be verified
if user_password.password == "":
return False
if bcrypt.checkpw(
password.encode("utf-8"), user_password.password.encode("utf-8")
):
return True
else:
return False
else:
return False
async def get_user_password(self, user_id: str) -> Optional[str]:
"""Get user password through the user_id
Args:
user_id (str): user identity, _id in UserAccountDoc
Returns:
str: password hash
"""
user_password = await UserPasswordDoc.find(
UserPasswordDoc.user_id == user_id
).first_or_none()
if user_password is None:
return None
else:
return user_password.password
async def get_user_email(self, user_id: str) -> Optional[str]:
"""get user email through the user_id
Args:
user_id (str): user identity, _id in UserAccountDoc
Returns:
str: email address
"""
user_email = await UserEmailDoc.find(
UserEmailDoc.user_id == user_id
).first_or_none()
if user_email is None:
return None
else:
return user_email.email
async def get_user_id_by_email(self, email: str) -> Optional[str]:
"""get user id through email from user_email doc
Args:
email (str): email address, compare email address in lowercase
Returns:
Optional[str]: user_id or None
"""
user_email = await UserEmailDoc.find(
UserEmailDoc.email == email.lower()
).first_or_none()
if user_email is None:
return None
else:
return user_email.user_id
def user_sign_out(self, token):
pass
async def generate_auth_code_for_email(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
async def verify_email_code(self, email: str, code: str) -> bool:
"""sign in with email and code
Args:
email (str): email address
code (str): auth code to be verified
Returns:
bool: True if code is valid, False otherwise
"""
result = await AuthCodeDoc.find(
AuthCodeDoc.method == email.lower(),
AuthCodeDoc.auth_code == code,
AuthCodeDoc.expiry > datetime.now(timezone.utc),
AuthCodeDoc.method_type == AuthType.EMAIL,
).first_or_none()
if result:
return True
else:
return False
async def get_user_mobile(self, user_id: str) -> Optional[str]:
"""get user mobile number through the user_id
Args:
user_id (str): user identity, _id in UserAccountDoc
Returns:
str: mobile number
"""
user_mobile = await UserMobileDoc.find(
UserMobileDoc.user_id == user_id
).first_or_none()
if user_mobile is None:
return None
else:
return user_mobile.mobile
async def generate_auth_code_for_mobile(self, mobile: str) -> str:
"""send auth code to mobile number
Args:
mobile (str): mobile number
"""
auth_code = generate_auth_code()
expiry = datetime.now(timezone.utc) + timedelta(minutes=5)
auth_code_doc = AuthCodeDoc(
auth_code=auth_code,
method=mobile.lower(),
method_type=AuthType.MOBILE,
expiry=expiry,
)
await auth_code_doc.create()
return auth_code
async def verify_mobile_with_code(self, mobile, code):
"""sign in with mobile and code
Args:
mobile (str): mobile number
code (str): auth code to be verified
Returns:
bool: True if code is valid, False otherwise
"""
result = await AuthCodeDoc.find(
AuthCodeDoc.method == mobile.lower(),
AuthCodeDoc.auth_code == code,
AuthCodeDoc.expiry > datetime.now(timezone.utc),
AuthCodeDoc.method_type == AuthType.MOBILE,
).first_or_none()
if result:
return True
else:
return False
async def save_email_auth_method(self, user_id: str, email: str):
"""save email auth method to user_email doc
Args:
user_id (str): user id
email (str): email address
"""
user_email = await UserEmailDoc.find(
UserEmailDoc.user_id == user_id
).first_or_none()
if user_email is None:
new_user_email = UserEmailDoc(user_id=user_id, email=email.lower())
await new_user_email.create()
else:
user_email.email = email.lower()
await user_email.save()
async def save_password_auth_method(self, user_id: str, user_flid, password: str):
"""save password auth method to user_password doc
Args:
user_id (str): user id
password (str): user password
"""
password_hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
user_password = await UserPasswordDoc.find(
UserPasswordDoc.user_id == user_id
).first_or_none()
if user_password is None:
new_user_password = UserPasswordDoc(
user_id=user_id, password=password_hashed
)
await new_user_password.create()
else:
user_password.password = password_hashed
await user_password.save()
result = await self.code_depot_manager.update_depot_user_password(
user_flid, password
)
if not result:
raise Exception("Failed to update user password in code depot")
async def reset_password(self, user_id: str):
"""clean password auth method from user_password doc
Args:
user_id (str): user id
"""
user_password = await UserPasswordDoc.find(
UserPasswordDoc.user_id == user_id
).first_or_none()
if user_password:
user_password.password = ""
await user_password.save()
else:
raise Exception("User password was not set before.")
async def is_password_reset_required(self, user_id: str) -> bool:
"""check if password is required for the user
Args:
user_id (str): user id
Returns:
bool: True if password is required, False otherwise
"""
user_password = await UserPasswordDoc.find(
UserPasswordDoc.user_id == user_id
).first_or_none()
if user_password:
return user_password.password == ""
else:
return True

View File

@ -0,0 +1,416 @@
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
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
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
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)
def get_depot_ssh_url(self, code_depot_name: str) -> str:
"""Return the ssh url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the ssh url of the code depot
"""
if self.code_depot_ssh_port != "22":
return f"git@{self.code_depot_domain_name}:{self.code_depot_ssh_port}/{self.gitea_org}/{code_depot_name}.git"
else:
return f"git@{self.code_depot_domain_name}/{self.gitea_org}/{code_depot_name}.git"
def get_depot_http_url(self, code_depot_name: str) -> str:
"""Return the http url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the http url of the code depot
"""
if self.code_depot_http_port in ["443", "3443"]:
return f"https://{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
else:
return f"http://{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
def get_depot_http_url_with_user_name(
self, code_depot_name: str, user_name: str
) -> str:
"""Return the http url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the http url of the code depot
"""
if self.code_depot_http_port in ["443", "3443"]:
return f"https://{user_name}@{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
else:
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 []
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
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
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
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
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()
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

View File

@ -0,0 +1,32 @@
from .api_objects import (
AlreadyExistsException,
Branch,
Comment,
Commit,
Content,
Issue,
MigrationServices,
Milestone,
NotFoundException,
Organization,
Repository,
Team,
User,
)
from .gitea import AlreadyExistsException, Gitea, NotFoundException
__all__ = [
"Gitea",
"User",
"Organization",
"Team",
"Repository",
"Branch",
"NotFoundException",
"AlreadyExistsException",
"Issue",
"Milestone",
"Commit",
"Comment",
"Content",
]

View File

@ -0,0 +1,120 @@
from .exceptions import (
MissiongEqualyImplementation,
ObjectIsInvalid,
RawRequestEndpointMissing,
)
class ReadonlyApiObject:
def __init__(self, gitea):
self.gitea = gitea
self.deleted = False # set if .delete was called, so that an exception is risen
def __str__(self):
return "GiteaAPIObject (%s):" % (type(self))
def __eq__(self, other):
"""Compare only fields that are part of the gitea-data identity"""
raise MissiongEqualyImplementation()
def __hash__(self):
"""Hash only fields that are part of the gitea-data identity"""
raise MissiongEqualyImplementation()
_fields_to_parsers = {}
@classmethod
def request(cls, gitea):
if hasattr("API_OBJECT", cls):
return cls._request(gitea)
else:
raise RawRequestEndpointMissing()
@classmethod
def _request(cls, gitea, args):
result = cls._get_gitea_api_object(gitea, args)
api_object = cls.parse_response(gitea, result)
return api_object
@classmethod
def _get_gitea_api_object(cls, gitea, args):
"""Retrieving an object always as GET_API_OBJECT"""
return gitea.requests_get(cls.API_OBJECT.format(**args))
@classmethod
def parse_response(cls, gitea, result) -> "ReadonlyApiObject":
# gitea.logger.debug("Found api object of type %s (id: %s)" % (type(cls), id))
api_object = cls(gitea)
cls._initialize(gitea, api_object, result)
return api_object
@classmethod
def _initialize(cls, gitea, api_object, result):
for name, value in result.items():
if name in cls._fields_to_parsers and value is not None:
parse_func = cls._fields_to_parsers[name]
value = parse_func(gitea, value)
cls._add_read_property(name, value, api_object)
# add all patchable fields missing in the request to be writable
for name in cls._fields_to_parsers.keys():
if not hasattr(api_object, name):
cls._add_read_property(name, None, api_object)
@classmethod
def _add_read_property(cls, name, value, api_object):
if not hasattr(api_object, name):
setattr(api_object, "_" + name, value)
prop = property((lambda n: lambda self: self._get_var(n))(name))
setattr(cls, name, prop)
else:
raise AttributeError(f"Attribute {name} already exists on api object.")
def _get_var(self, name):
if self.deleted:
raise ObjectIsInvalid()
return getattr(self, "_" + name)
class ApiObject(ReadonlyApiObject):
_patchable_fields = set()
def __init__(self, gitea):
super().__init__(gitea)
self._dirty_fields = set()
def commit(self):
raise NotImplemented()
_parsers_to_fields = {}
def get_dirty_fields(self):
dirty_fields_values = {}
for field in self._dirty_fields:
value = getattr(self, field)
if field in self._parsers_to_fields:
dirty_fields_values[field] = self._parsers_to_fields[field](value)
else:
dirty_fields_values[field] = value
return dirty_fields_values
@classmethod
def _initialize(cls, gitea, api_object, result):
super()._initialize(gitea, api_object, result)
for name in cls._patchable_fields:
cls._add_write_property(name, None, api_object)
@classmethod
def _add_write_property(cls, name, value, api_object):
if not hasattr(api_object, "_" + name):
setattr(api_object, "_" + name, value)
prop = property(
(lambda n: lambda self: self._get_var(n))(name),
(lambda n: lambda self, v: self.__set_var(n, v))(name),
)
setattr(cls, name, prop)
def __set_var(self, name, i):
if self.deleted:
raise ObjectIsInvalid()
self._dirty_fields.add(name)
setattr(self, "_" + name, i)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
class AlreadyExistsException(Exception):
pass
class NotFoundException(Exception):
pass
class ObjectIsInvalid(Exception):
pass
class ConflictException(Exception):
pass
class RawRequestEndpointMissing(Exception):
"""This ApiObject can only be obtained through other api objects and does not have
diret .request method."""
pass
class MissiongEqualyImplementation(Exception):
"""
Each Object obtained from the gitea api must be able to check itself for equality in relation to its
fields obtained from gitea. Risen if an api object is lacking the proper implementation.
"""
pass

View File

@ -0,0 +1,392 @@
import json
import logging
from typing import Dict, List, Union
import requests
import urllib3
from frozendict import frozendict
from .api_objects import Organization, Repository, Team, User
from .exceptions import AlreadyExistsException, ConflictException, NotFoundException
class Gitea:
"""Object to establish a session with Gitea."""
ADMIN_CREATE_USER = """/admin/users"""
ADMIN_EDIT_USER = """/admin/users/%s""" # <username>
GET_USERS_ADMIN = """/admin/users"""
ADMIN_REPO_CREATE = """/admin/users/%s/repos""" # <ownername>
GITEA_VERSION = """/version"""
GET_USER = """/user"""
CREATE_ORG = """/admin/users/%s/orgs""" # <username>
CREATE_TEAM = """/orgs/%s/teams""" # <orgname>
def __init__(
self, gitea_url: str, token_text=None, auth=None, verify=True, log_level="INFO"
):
"""Initializing Gitea-instance
Args:
gitea_url (str): The Gitea instance URL.
token_text (str, None): The access token, by default None.
auth (tuple, None): The user credentials
`(username, password)`, by default None.
verify (bool): If True, allow insecure server connections
when using SSL.
log_level (str): The log level, by default `INFO`.
"""
self.logger = logging.getLogger(__name__)
self.logger.setLevel(log_level)
self.headers = {
"Content-type": "application/json",
}
self.url = gitea_url
self.requests = requests.Session()
# Manage authentification
if not token_text and not auth:
raise ValueError("Please provide auth or token_text, but not both")
if token_text:
self.headers["Authorization"] = "token " + token_text
if auth:
self.requests.auth = auth
# Manage SSL certification verification
self.requests.verify = verify
if not verify:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def __get_url(self, endpoint):
url = self.url + "/api/v1" + endpoint
self.logger.debug("Url: %s" % url)
return url
@staticmethod
def parse_result(result) -> Dict:
"""Parses the result-JSON to a dict."""
if result.text and len(result.text) > 3:
return json.loads(result.text)
return {}
def requests_get(self, endpoint: str, params=frozendict(), sudo=None):
combined_params = {}
combined_params.update(params)
if sudo:
combined_params["sudo"] = sudo.username
request = self.requests.get(
self.__get_url(endpoint), headers=self.headers, params=combined_params
)
if request.status_code not in [200, 201, 204]:
message = f"Received status code: {request.status_code} ({request.url})"
if request.status_code in [404]:
raise NotFoundException(message)
if request.status_code in [403]:
raise Exception(
f"Unauthorized: {request.url} - Check your permissions and try again! ({message})"
)
if request.status_code in [409]:
raise ConflictException(message)
raise Exception(message)
return self.parse_result(request)
def requests_get_paginated(
self,
endpoint: str,
params=frozendict(),
sudo=None,
page_key: str = "page",
page_limit: int = 0,
):
page = 1
combined_params = {}
combined_params.update(params)
aggregated_result = []
while True:
combined_params[page_key] = page
result = self.requests_get(endpoint, combined_params, sudo)
if not result:
return aggregated_result
aggregated_result.extend(result)
page += 1
if page_limit and page > page_limit:
return aggregated_result
def requests_put(self, endpoint: str, data: dict = None):
if not data:
data = {}
request = self.requests.put(
self.__get_url(endpoint), headers=self.headers, data=json.dumps(data)
)
if request.status_code not in [200, 204]:
message = f"Received status code: {request.status_code} ({request.url}) {request.text}"
self.logger.error(message)
raise Exception(message)
def requests_delete(self, endpoint: str):
request = self.requests.delete(self.__get_url(endpoint), headers=self.headers)
if request.status_code not in [204]:
message = f"Received status code: {request.status_code} ({request.url})"
self.logger.error(message)
raise Exception(message)
def requests_post(self, endpoint: str, data: dict):
request = self.requests.post(
self.__get_url(endpoint), headers=self.headers, data=json.dumps(data)
)
if request.status_code not in [200, 201, 202]:
if (
"already exists" in request.text
or "e-mail already in use" in request.text
):
self.logger.warning(request.text)
raise AlreadyExistsException()
self.logger.error(
f"Received status code: {request.status_code} ({request.url})"
)
self.logger.error(f"With info: {data} ({self.headers})")
self.logger.error(f"Answer: {request.text}")
raise Exception(
f"Received status code: {request.status_code} ({request.url}), {request.text}"
)
return self.parse_result(request)
def requests_patch(self, endpoint: str, data: dict):
request = self.requests.patch(
self.__get_url(endpoint), headers=self.headers, data=json.dumps(data)
)
if request.status_code not in [200, 201]:
error_message = (
f"Received status code: {request.status_code} ({request.url}) {data}"
)
self.logger.error(error_message)
raise Exception(error_message)
return self.parse_result(request)
def get_orgs_public_members_all(self, orgname):
path = "/orgs/" + orgname + "/public_members"
return self.requests_get(path)
def get_orgs(self):
path = "/admin/orgs"
results = self.requests_get(path)
return [Organization.parse_response(self, result) for result in results]
def get_org_by_name(self, org_name: str) -> Organization:
path = "/admin/orgs"
results = self.requests_get(path)
for result in results:
org = Organization.parse_response(self, result)
if org.name == org_name:
return org
def get_user(self):
result = self.requests_get(Gitea.GET_USER)
return User.parse_response(self, result)
def get_version(self) -> str:
result = self.requests_get(Gitea.GITEA_VERSION)
return result["version"]
def get_users(self) -> List[User]:
results = self.requests_get(Gitea.GET_USERS_ADMIN)
return [User.parse_response(self, result) for result in results]
def get_user_by_email(self, email: str) -> User:
users = self.get_users()
for user in users:
if user.email == email or email in user.emails:
return user
return None
def get_user_by_name(self, username: str) -> User:
users = self.get_users()
for user in users:
if user.username == username:
return user
return None
def update_user_password(self, user_name: str, password: str):
request_data = {"password": password, "login_name": user_name}
result = self.requests_patch(
Gitea.ADMIN_EDIT_USER % user_name, data=request_data
)
self.logger.debug("Gitea response - update_user_password(): %s", result)
def create_user(
self,
user_name: str,
email: str,
password: str,
full_name: str = None,
login_name: str = None,
change_pw=True,
send_notify=True,
source_id=0,
):
"""Create User.
Throws:
AlreadyExistsException, if the User exists already
Exception, if something else went wrong.
"""
if not login_name:
login_name = user_name
if not full_name:
full_name = user_name
request_data = {
"source_id": source_id,
"login_name": login_name,
"full_name": full_name,
"username": user_name,
"email": email,
"password": password,
"send_notify": send_notify,
"must_change_password": change_pw,
}
self.logger.debug("Gitea post payload: %s", request_data)
result = self.requests_post(Gitea.ADMIN_CREATE_USER, data=request_data)
if "id" in result:
self.logger.info(
"Successfully created User %s <%s> (id %s)",
result["login"],
result["email"],
result["id"],
)
self.logger.debug("Gitea response: %s", result)
else:
self.logger.error(result["message"])
raise Exception("User not created... (gitea: %s)" % result["message"])
user = User.parse_response(self, result)
return user
def create_repo(
self,
repoOwner: Union[User, Organization],
repoName: str,
description: str = "",
private: bool = False,
autoInit=True,
gitignores: str = None,
license: str = None,
readme: str = "Default",
issue_labels: str = None,
default_branch="master",
):
"""Create a Repository as the administrator
Throws:
AlreadyExistsException: If the Repository exists already.
Exception: If something else went wrong.
Note:
Non-admin users can not use this method. Please use instead
`gitea.User.create_repo` or `gitea.Organization.create_repo`.
"""
# although this only says user in the api, this also works for
# organizations
assert isinstance(repoOwner, User) or isinstance(repoOwner, Organization)
result = self.requests_post(
Gitea.ADMIN_REPO_CREATE % repoOwner.username,
data={
"name": repoName,
"description": description,
"private": private,
"auto_init": autoInit,
"gitignores": gitignores,
"license": license,
"issue_labels": issue_labels,
"readme": readme,
"default_branch": default_branch,
},
)
if "id" in result:
self.logger.info("Successfully created Repository %s " % result["name"])
else:
self.logger.error(result["message"])
raise Exception("Repository not created... (gitea: %s)" % result["message"])
return Repository.parse_response(self, result)
def create_org(
self,
owner: User,
orgName: str,
description: str,
location="",
website="",
full_name="",
):
assert isinstance(owner, User)
result = self.requests_post(
Gitea.CREATE_ORG % owner.username,
data={
"username": orgName,
"description": description,
"location": location,
"website": website,
"full_name": full_name,
},
)
if "id" in result:
self.logger.info(
"Successfully created Organization %s" % result["username"]
)
else:
self.logger.error(
"Organization not created... (gitea: %s)" % result["message"]
)
self.logger.error(result["message"])
raise Exception(
"Organization not created... (gitea: %s)" % result["message"]
)
return Organization.parse_response(self, result)
def create_team(
self,
org: Organization,
name: str,
description: str = "",
permission: str = "read",
can_create_org_repo: bool = False,
includes_all_repositories: bool = False,
units=(
"repo.code",
"repo.issues",
"repo.ext_issues",
"repo.wiki",
"repo.pulls",
"repo.releases",
"repo.ext_wiki",
),
):
"""Creates a Team.
Args:
org (Organization): Organization the Team will be part of.
name (str): The Name of the Team to be created.
description (str): Optional, None, short description of the new Team.
permission (str): Optional, 'read', What permissions the members
"""
result = self.requests_post(
Gitea.CREATE_TEAM % org.username,
data={
"name": name,
"description": description,
"permission": permission,
"can_create_org_repo": can_create_org_repo,
"includes_all_repositories": includes_all_repositories,
"units": units,
},
)
if "id" in result:
self.logger.info("Successfully created Team %s" % result["name"])
else:
self.logger.error("Team not created... (gitea: %s)" % result["message"])
self.logger.error(result["message"])
raise Exception("Team not created... (gitea: %s)" % result["message"])
api_object = Team.parse_response(self, result)
setattr(
api_object, "_organization", org
) # fixes strange behaviour of gitea not returning a valid organization here.
return api_object

View File

@ -0,0 +1,7 @@
This folder will be extracted to a separate service - DevOps Service
Scope:
- Giteas code management. Continuous Deployment.
See doc:
- Freeleaps All-In-One development solution spec & high-level design.docx .
Code temporarily put here before it's up.

View File

@ -0,0 +1,2 @@
class UserProfileHandler:
pass

View File

@ -0,0 +1,43 @@
from enum import IntEnum
class NewUserMethod(IntEnum):
EMAIL = 1
MOBILE = 2
class UserAccountProperty(IntEnum):
EMAIL_VERIFIED = 1
MOBILE_VERIFIED = 2
PAYMENT_SETUP = 4
ACCEPT_REQUEST = 8
READY_PROVIDER = 16
MANAGE_PROJECT = 32
class UserLoginAction(IntEnum):
VERIFY_EMAIL_WITH_AUTH_CODE = 0
EXISTING_USER_PASSWORD_REQUIRED = 1
NEW_USER_SET_PASSWORD = 2
EMAIL_NOT_ASSOCIATED_WITH_USER = 3
REVIEW_AND_REVISE_FLID = 4
USER_SIGNED_IN = 100
class AuthType(IntEnum):
MOBILE = 0
EMAIL = 1
PASSWORD = 2
class DepotStatus(IntEnum):
TO_BE_CREATED = 0
CREATED = 1
DELETED = 2
class UserAccountStatus(IntEnum):
TO_BE_CREATED = 0
CREATED = 1
DELETED = 2
DEACTIVATED = 3

View File

@ -0,0 +1,14 @@
from enum import IntEnum
class DepotStatus(IntEnum):
TO_BE_CREATED = 0
CREATED = 1
DELETED = 2
class UserAccountStatus(IntEnum):
TO_BE_CREATED = 0
CREATED = 1
DELETED = 2
DEACTIVATED = 3

View File

@ -0,0 +1,21 @@
from typing import Dict, Optional
from datetime import datetime, timezone
from beanie import Document
from app.authentication.backend.models.gitea.constants import (
DepotStatus,
UserAccountStatus,
)
class CodeDepotDoc(Document):
depot_name: str
product_id: str
depot_status: DepotStatus
collaborators: list[str] = []
total_commits: Optional[int] = 0
last_commiter: Optional[str] = ""
last_update: Optional[datetime] = datetime.now(timezone.utc)
weekly_commits: Optional[Dict[str, int]] = {}
class Settings:
name = "code_depot"

View File

@ -0,0 +1 @@
Models here will be moved out when building the DevOps Service

View File

@ -0,0 +1,37 @@
from datetime import datetime
from beanie import Document
from .constants import AuthType
class UserPasswordDoc(Document):
user_id: str
password: str
class Settings:
name = "user_password"
class UserEmailDoc(Document):
user_id: str
email: str
class Settings:
name = "user_email"
class UserMobileDoc(Document):
user_id: str
mobile: str
class Settings:
name = "user_mobile"
class AuthCodeDoc(Document):
auth_code: str
method: str
method_type: AuthType
expiry: datetime
class Settings:
name = "user_auth_code"

View File

@ -0,0 +1,26 @@
from enum import IntEnum
class AdministrativeRole(IntEnum):
NONE = 0
PERSONAL = 1
BUSINESS = 2
CONTRIBUTOR = 4
ADMINISTRATOR = 8
# now UI cannot siginin if user role is 8
class Capability(IntEnum):
VISITOR = 1
COMMUNICATOR = 2
REQUESTER = 4
PROVIDER = 8
DEVELOPER = 16
class Feature(IntEnum):
ANY = 0xFFFFFFFF
SENDMESSAGE = 0x1
INITIATEREQUEST = 0x2
MAKEPROPOSAL = 0x4
CREATEPROJECT = 0x8

View File

@ -0,0 +1,24 @@
from enum import IntEnum
class NewUserMethod(IntEnum):
EMAIL = 1
MOBILE = 2
class UserAccountProperty(IntEnum):
EMAIL_VERIFIED = 1
MOBILE_VERIFIED = 2
PAYMENT_SETUP = 4
ACCEPT_REQUEST = 8
READY_PROVIDER = 16
MANAGE_PROJECT = 32
class UserLoginAction(IntEnum):
VERIFY_EMAIL_WITH_AUTH_CODE = 0
EXISTING_USER_PASSWORD_REQUIRED = 1
NEW_USER_SET_PASSWORD = 2
EMAIL_NOT_ASSOCIATED_WITH_USER = 3
REVIEW_AND_REVISE_FLID = 4
USER_SIGNED_IN = 100

View File

@ -0,0 +1,20 @@
from typing import Optional
from beanie import Document
from .constants import UserAccountProperty
from .permission.constants import AdministrativeRole, Capability
from infra.models.constants import UserRegion
class UserAccountDoc(Document):
profile_id: Optional[str]
account_id: Optional[str]
service_plan_id: Optional[str]
properties: UserAccountProperty
capabilities: Capability
user_role: int = AdministrativeRole.NONE
preferred_region: UserRegion = UserRegion.ZH_CN
class Settings:
name = "user_account"

View File

@ -0,0 +1,30 @@
from app.authentication.backend.infra.user_management.user_auth_handler import (
UserAuthManager,
)
from typing import Optional
class UserAuthService:
def __init__(self):
self.user_auth_manager = UserAuthManager()
async def get_user_id_by_email(self, email: str) -> Optional[str]:
return await self.user_auth_manager.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 is_password_reset_required(self, user_id: str) -> bool:
return await self.user_auth_manager.is_password_reset_required(user_id)

View File

@ -0,0 +1,80 @@
import random
from typing import Optional, Dict, Tuple, List
from infra.log.module_logger import ModuleLogger
from app.authentication.backend.application.models.user.constants import (
NewUserMethod,
UserAccountProperty,
UserLoginAction,
)
from app.authentication.backend.application.user.models import UserAccountDoc
from app.authentication.backend.business.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 (
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
class UserManagementService:
def __init__(self) -> None:
self.user_auth_handler = UserAuthHandler()
self.user_profile_handler = UserProfileHandler()
self.module_logger = ModuleLogger(sender_id=UserManagementService)
@log_entry_exit_async
async def create_new_user_account(
self, method: NewUserMethod, region: UserRegion
) -> str:
"""create a new user account document in DB
Args:
method (NewUserMethod): the method the new user came from
region : preferred user region detected via the user log-in website
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()
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()
# Create other doc in collections for the new user
await UserAchievement(str(user_account.id)).create_activeness_achievement()
return str(user_account.id)

View File

@ -0,0 +1,20 @@
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
NAME: str = "central_storage"
GITEA_URL: str = ""
GITEA_TOKEN: str = ""
GITEA_DEPOT_ORGANIZATION: str = ""
CODE_DEPOT_DOMAIN_NAME: str = ""
CODE_DEPOT_SSH_PORT: str = ""
CODE_DEPOT_HTTP_PORT: str = ""
class Config:
env_file = ".myapp.env"
env_file_encoding = "utf-8"
app_settings = AppSettings()

View File

@ -0,0 +1,75 @@
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 .freeleaps_app import FreeleapsApp
def create_app() -> FastAPI:
logging.info("App initializing")
app = FreeleapsApp()
register(app, exception_handler)
register(app, database)
register(app, logger)
register(app, router)
register(app, scheduler)
register(app, common)
# Call the custom_openapi function to change the OpenAPI version
customize_openapi_security(app)
return app
# This function overrides the OpenAPI schema version to 3.0.0
def customize_openapi_security(app: FastAPI) -> None:
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
# Generate OpenAPI schema
openapi_schema = get_openapi(
title="FreeLeaps API",
version="3.1.0",
description="FreeLeaps API Documentation",
routes=app.routes,
)
# Ensure the components section exists in the OpenAPI schema
if "components" not in openapi_schema:
openapi_schema["components"] = {}
# Add security scheme to components
openapi_schema["components"]["securitySchemes"] = {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
# Add security requirement globally
openapi_schema["security"] = [{"bearerAuth": []}]
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
def register(app, provider):
logging.info(provider.__name__ + " registering")
provider.register(app)
def boot(app, provider):
logging.info(provider.__name__ + " booting")
provider.boot(app)

View File

@ -0,0 +1,6 @@
from fastapi import FastAPI
class FreeleapsApp(FastAPI):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -0,0 +1,25 @@
import os
from pydantic_settings import BaseSettings
class SiteSettings(BaseSettings):
NAME: str = "appname"
DEBUG: bool = True
ENV: str = "dev"
SERVER_HOST: str = "0.0.0.0"
SERVER_PORT: int = 8103
URL: str = "http://localhost"
TIME_ZONE: str = "UTC"
BASE_PATH: str = os.path.dirname(os.path.dirname((os.path.abspath(__file__))))
class Config:
env_file = ".devbase-webapi.env"
env_file_encoding = "utf-8"
site_settings = SiteSettings()

View File

@ -0,0 +1,30 @@
from webapi.bootstrap.application import create_app
from 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():
"""
TODO: redirect client to /doc#
"""
return RedirectResponse("docs")
if __name__ == "__main__":
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 {}

View File

@ -0,0 +1,31 @@
from fastapi.middleware.cors import CORSMiddleware
from webapi.config.site_settings import site_settings
def register(app):
app.debug = site_settings.DEBUG
app.title = site_settings.NAME
add_global_middleware(app)
# This hook ensures that a connection is opened to handle any queries
# generated by the request.
@app.on_event("startup")
def startup():
pass
# This hook ensures that the connection is closed when we've finished
# processing the request.
@app.on_event("shutdown")
def shutdown():
pass
def add_global_middleware(app):
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

@ -0,0 +1,25 @@
from infra.config.app_settings import app_settings
from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from app.central_storage.backend.models.models import DocumentDoc
def register(app):
app.debug = "auth_mongo_debug"
app.title = "auth_mongo_name"
@app.on_event("startup")
async def start_database():
await initiate_database()
async def initiate_database():
client = AsyncIOMotorClient(
app_settings.MONGODB_URI,
serverSelectionTimeoutMS=60000,
minPoolSize=5, # Minimum number of connections in the pool
maxPoolSize=20, # Maximum number of connections in the pool
)
await init_beanie(
database=client[app_settings.MONGODB_NAME], document_models=[DocumentDoc]
)

View File

@ -0,0 +1,39 @@
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import (
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_500_INTERNAL_SERVER_ERROR,
)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.detail},
)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=HTTP_400_BAD_REQUEST,
content={"error": str(exc)},
)
async def exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
content={"error": str(exc)},
)
def register(app: FastAPI):
app.add_exception_handler(HTTPException, custom_http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(Exception, exception_handler)

View File

@ -0,0 +1,60 @@
import logging
import sys
from loguru import logger
from common.config.log_settings import log_settings
def register(app=None):
level = log_settings.LOG_LEVEL
file_path = log_settings.LOG_PATH
retention = log_settings.LOG_RETENTION
rotation = log_settings.LOG_ROTATION
# intercept everything at the root logger
logging.root.handlers = [InterceptHandler()]
logging.root.setLevel(level)
# remove every other logger's handlers
# and propagate to root logger
for name in logging.root.manager.loggerDict.keys():
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = True
# configure loguru
logger.add(
sink=sys.stdout
)
logger.add(
sink=file_path,
level=level,
retention=retention,
rotation=rotation
)
logger.disable("pika.adapters")
logger.disable("pika.connection")
logger.disable("pika.channel")
logger.disable("pika.callback")
logger.disable("pika.frame")
logger.disable("pika.spec")
logger.disable("aiormq.connection")
logger.disable("urllib3.connectionpool")
class InterceptHandler(logging.Handler):
def emit(self, record):
# Get corresponding Loguru level if it exists
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# Find caller from where originated the logged message
frame, depth = logging.currentframe(), 2
while frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)

View File

@ -0,0 +1,34 @@
from webapi.routes import api_router
from starlette import routing
def register(app):
app.include_router(
api_router,
prefix="/api",
tags=["api"],
dependencies=[],
responses={404: {"description": "no page found"}},
)
if app.debug:
for route in app.routes:
if not isinstance(route, routing.WebSocketRoute):
print(
{
"path": route.path,
"endpoint": route.endpoint,
"name": route.name,
"methods": route.methods,
}
)
else:
print(
{
"path": route.path,
"endpoint": route.endpoint,
"name": route.name,
"type": "web socket route",
}
)

View File

@ -0,0 +1,8 @@
import asyncio
def register(app):
@app.on_event("startup")
async def start_scheduler():
#create your scheduler here
pass

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
api_router = APIRouter()
websocket_router = APIRouter()

View File

@ -0,0 +1,15 @@
from fastapi.routing import APIRoute
from starlette import routing
def post_process_router(app) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
if hasattr(route, "operation_id"):
route.operation_id = route.name # in this case, 'read_items'

View File

@ -0,0 +1,20 @@
from fastapi import APIRouter
from .try_signin_with_email import router as ts_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 .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
router = APIRouter(prefix="/signin")
router.include_router(ts_router, tags=["signin"])
router.include_router(sw_router, tags=["signin"])
router.include_router(up_router, tags=["signin"])
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

@ -0,0 +1,37 @@
from backend.application.user.user_manager import UserManager
from pydantic import BaseModel
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
router = APIRouter()
# Web API
# reset_password_through_email
#
class UserSignWithEmailBody(BaseModel):
email: str
host: str
class UserSignWithEmailResponse(BaseModel):
signin_type: int
@router.post(
"/reset-password-through-email",
operation_id="user-reset-password-through-email",
summary="user reset password through email",
description="A client user forgets the password. \
The system will send auth code the their email\
to let the user reset the password",
response_description="action: UserLoginAction",
)
async def reset_password_through_email(
item: UserSignWithEmailBody,
):
result = await UserManager().reset_password_through_email(item.email, item.host)
result = {"action": result}
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,87 @@
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter
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
router = APIRouter()
# Web API
# signin-with-email-n-code
#
class RequestIn(BaseModel):
email: str
code: str
host: str
time_zone: Optional[str] = "UTC"
class ResponseOut(BaseModel):
# 1: succeeded
signin_result: int
# the access token for futhur communication with server
access_token: Optional[str] = None
# the refresh token for new access token generation
refresh_token: Optional[str] = None
# the identity of the signed in user
identity: Optional[str] = None
# the date time when the access toke will be expired
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
@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.",
response_model=ResponseOut,
)
async def signin_with_email_and_code(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
)
logging.debug(
f"signedin={signed_in}, adminstrative_role={adminstrative_role}, identity={identity}"
)
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
else:
access_token = None
refresh_token = None
expires_in = None
result = {
"signin_result": signed_in,
"access_token": access_token,
"refresh_token": refresh_token,
"identity": identity,
"expires_in": expires_in,
"role": adminstrative_role,
"flid": flid,
"preferred_region": preferred_region,
}
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,87 @@
import logging
from datetime import datetime, timezone, timedelta
from typing import Optional
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from backend.application.user.user_manager import UserManager
from infra.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager()
# Web API
# signin-with-email-n-code
#
class RequestIn(BaseModel):
email: str
code: str
host: str
time_zone: Optional[str] = "UTC"
class ResponseOut(BaseModel):
# 1: succeeded
signin_result: int
# the access token for futhur communication with server
access_token: Optional[str] = None
# the refresh token for new access token generation
refresh_token: Optional[str] = None
# the identity of the signed in user
identity: Optional[str] = None
# the date time when the access toke will be expired
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
@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.",
response_model=ResponseOut,
)
async def signin_with_email_and_code(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
)
logging.debug(
f"signedin={signed_in}, adminstrative_role={adminstrative_role}, identity={identity}"
)
if signed_in and identity and adminstrative_role:
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)
else:
access_token = None
refresh_token = None
expires_in = None
result = {
"signin_result": signed_in,
"access_token": access_token,
"refresh_token": refresh_token,
"identity": identity,
"expires_in": expires_in,
"role": adminstrative_role,
"flid": flid,
"preferred_region": preferred_region,
}
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,31 @@
from fastapi import APIRouter, Security
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()
@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",
)
async def get_latest_login_by_user_id(
user_id: str,
credentials: JwtAuthorizationCredentials = Security(access_security),
):
# 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}))

View File

@ -0,0 +1,42 @@
from backend.application.user.user_manager import UserManager
from pydantic import BaseModel
from fastapi import APIRouter, Security
from backend.infra.authentication.auth import access_security
from fastapi_jwt import JwtAuthorizationCredentials
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
router = APIRouter()
# Web API
# update_user_password
#
class RequestIn(BaseModel):
password: str
password2: str
@router.post(
"/update-user-password",
operation_id="user_update_user_password",
summary="updathe user's sign-in password",
description="Update the user's sign-in password. If the password was not set yet, this will enable the user to log in using the password",
response_description="signin_type:0 meaning simplified(using email) signin, \
1 meaning standard(using FLID and passward) signin",
)
async def update_user_password(
item: RequestIn,
credentials: JwtAuthorizationCredentials = Security(access_security),
):
user_id = credentials["id"]
if item.password != item.password2:
return JSONResponse(
content=jsonable_encoder(
{"error": "password and password2 are not the same"}
)
)
else:
result = await UserManager().update_user_password(user_id, item.password)
return JSONResponse(content=jsonable_encoder(result))

View File

@ -0,0 +1,12 @@
from fastapi import APIRouter
from .generate_tokens import router as generate_tokens_router
from .refresh_token import router as refresh_token_router
from .verify_token import router as verify_token_router
router = APIRouter()
router.include_router(
generate_tokens_router, prefix="/token", tags=["Token Management"]
)
router.include_router(refresh_token_router, prefix="/token", tags=["Token Management"])
router.include_router(verify_token_router, prefix="/token", tags=["Token Management"])

View File

@ -0,0 +1,35 @@
from fastapi import APIRouter
from pydantic import BaseModel
from datetime import datetime, timedelta, timezone
from infra.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager() # Initialize TokenManager
class TokenRequest(BaseModel):
id: str
role: int
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
expires_in: datetime
@router.post("/generate-tokens", response_model=TokenResponse)
async def generate_tokens(request: TokenRequest):
"""
Endpoint to generate access and refresh tokens.
"""
subject = {"id": request.id, "role": request.role}
access_token = token_manager.create_access_token(subject)
refresh_token = token_manager.create_refresh_token(subject)
expires_in = datetime.now(timezone.utc) + timedelta(
minutes=token_manager.access_token_expire_minutes
)
return TokenResponse(
access_token=access_token, refresh_token=refresh_token, expires_in=expires_in
)

View File

@ -0,0 +1,33 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from infra.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager() # Initialize TokenManager
class RefreshTokenRequest(BaseModel):
refresh_token: str
id: str
role: int
class RefreshTokenResponse(BaseModel):
access_token: str
@router.post("/refresh-token", response_model=RefreshTokenResponse)
async def refresh_token(request: RefreshTokenRequest):
"""
Endpoint to refresh the access token using a valid refresh token.
"""
subject = {"id": request.id, "role": request.role}
try:
access_token = token_manager.refresh_access_token(
request.refresh_token, subject
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return RefreshTokenResponse(access_token=access_token)

View File

@ -0,0 +1,27 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from infra.token.token_manager import TokenManager
router = APIRouter()
token_manager = TokenManager() # Initialize TokenManager
class VerifyTokenRequest(BaseModel):
token: str
class VerifyTokenResponse(BaseModel):
valid: bool
payload: dict
@router.post("/verify-token", response_model=VerifyTokenResponse)
async def verify_token(request: VerifyTokenRequest):
"""
Endpoint to verify if a token is valid and return the payload.
"""
try:
payload = token_manager.decode_token(request.token)
return VerifyTokenResponse(valid=True, payload=payload)
except ValueError:
raise HTTPException(status_code=401, detail="Invalid or expired token")

View File

@ -1,4 +0,0 @@
#!/bin/sh
# This script starts the FastAPI server using uvicorn
uvicorn webapi.bootstrap.application:create_app --reload --host 0.0.0.0 --port 8005

View File

@ -21,3 +21,8 @@ class InvalidOperationError(Exception):
class InvalidDataError(Exception):
def __init__(self, message: str = "Invalid Data"):
self.message = message
class InvalidAuthCodeException(Exception):
def __init__(self, message: str = "Invalid Auth Code"):
self.message = message

View File

@ -0,0 +1,19 @@
from infra.models.constants import UserRegion
class RegionHandler:
def __init__(self):
self._zh_cn_patterns = [".cn", "cn.", "host"]
def detect_from_host(self, host: str) -> UserRegion:
# Now we set user preferred region based on host
for parttern in self._zh_cn_patterns:
if parttern in host.lower():
return UserRegion.ZH_CN
return UserRegion.OTHER
# 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

@ -0,0 +1,6 @@
from enum import IntEnum
class UserRegion(IntEnum):
OTHER = 0
ZH_CN = 1

View File

@ -1,7 +1,6 @@
# application/auth/token/token_manager.py
from datetime import datetime, timedelta, timezone
from typing import Dict
from jose import jwt
from jose import jwt, JWTError
from infra.config.app_settings import app_settings
@ -15,11 +14,6 @@ class TokenManager:
def create_access_token(self, subject: Dict[str, str]) -> str:
"""
Generates an access token with a short expiration time.
Args:
subject (Dict[str, str]): A dictionary containing user information like 'id' and 'role'.
Returns:
str: Encoded JWT access token.
"""
expire = datetime.now(timezone.utc) + timedelta(
minutes=self.access_token_expire_minutes
@ -31,11 +25,6 @@ class TokenManager:
def create_refresh_token(self, subject: Dict[str, str]) -> str:
"""
Generates a refresh token with a longer expiration time.
Args:
subject (Dict[str, str]): A dictionary containing user information like 'id' and 'role'.
Returns:
str: Encoded JWT refresh token.
"""
expire = datetime.now(timezone.utc) + timedelta(
days=self.refresh_token_expire_days
@ -47,13 +36,28 @@ class TokenManager:
def decode_token(self, token: str) -> Dict:
"""
Decodes a JWT token and returns the payload.
Args:
token (str): Encoded JWT token.
Returns:
Dict: Decoded token payload.
Raises:
JWTError: If the token is invalid or expired.
"""
return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
return payload
except JWTError:
raise ValueError("Invalid token")
def verify_refresh_token(self, token: str) -> bool:
"""
Verifies a refresh token to ensure it is valid and not expired.
"""
try:
payload = self.decode_token(token)
return True
except ValueError:
return False
def refresh_access_token(self, refresh_token: str, subject: Dict[str, str]) -> str:
"""
Verifies the refresh token and creates a new access token.
"""
if self.verify_refresh_token(refresh_token):
return self.create_access_token(subject)
else:
raise ValueError("Invalid refresh token")

22
infra/utils/date.py Normal file
View File

@ -0,0 +1,22 @@
import datetime
from datetime import timedelta, timezone
def get_sunday(date):
return date - datetime.timedelta(days=date.weekday()) + timedelta(days=6)
def get_last_sunday_dates(number, include_current_week=True):
now_utc = datetime.datetime.now(timezone.utc)
today = datetime.datetime(now_utc.year, now_utc.month, now_utc.day)
if include_current_week:
days_to_last_sunday = (6 - today.weekday()) % 7
last_sunday = today + datetime.timedelta(days=days_to_last_sunday)
else:
days_to_last_sunday = (today.weekday() - 6) % 7
last_sunday = today - datetime.timedelta(days=days_to_last_sunday)
last_n_sundays = []
for i in range(number):
sunday = last_sunday - datetime.timedelta(days=i * 7)
last_n_sundays.append(sunday.date())
return last_n_sundays

88
infra/utils/string.py Normal file
View File

@ -0,0 +1,88 @@
import random
import re
import jieba
from typing import List
SKILL_TAGS = [
"C++",
"Java",
"Python",
"TypeScript",
"iOS",
"Android",
"Web",
"Javascript",
"Vue",
"Go",
]
# dynamically update skill tags? maybe based on the most commonly extracted keywords to help the system adapt to change
def updateSkillTags(string):
SKILL_TAGS.append(string)
def generate_auth_code():
filtered = "0123456789"
code = "".join(random.choice(filtered) for i in range(6))
return code
# TODO: Need to optimize
def generate_self_intro_summary(content_html: str) -> str:
element_html = re.compile("<.*?>")
content_text = re.sub(element_html, "", content_html).strip()
return content_text[:50]
# TODO: Need to optimize
def extract_skill_tags(content_html: str) -> List[str]:
element_html = re.compile("<.*?>")
content_text = re.sub(element_html, "", content_html).strip()
words = set([word.lower() for word in jieba.cut(content_text) if word.strip()])
results = []
for tag in SKILL_TAGS:
if tag.lower() in words:
results.append(tag)
return results
def extract_title(content_html: str) -> List[str]:
element_html = re.compile("<.*?>")
content_text = re.sub(element_html, "\n", content_html).strip()
cut_point_indexes = []
for cut_point in [".", ",", ";", "\r", "\n"]:
result = content_text.find(cut_point)
if result > 0:
cut_point_indexes.append(result)
title = (
content_text[: min(cut_point_indexes)]
if len(cut_point_indexes) > 0
else content_text
)
return title
def check_password_complexity(password):
lowercase_pattern = r"[a-z]"
uppercase_pattern = r"[A-Z]"
digit_pattern = r"\d"
special_pattern = r'[!@#$%^&*(),.?":{}|<>]'
password_lowercase_one = bool(re.search(lowercase_pattern, password))
password_uppercase_one = bool(re.search(uppercase_pattern, password))
password_digit_one = bool(re.search(digit_pattern, password))
password_special_one = bool(re.search(special_pattern, password))
if (
password_lowercase_one
and password_uppercase_one
and password_digit_one
and password_special_one
):
return True
else:
return False