Merge pull request 'merge dev to master' (#86) from dev into master
Reviewed-on: freeleaps/freeleaps-service-hub#86
This commit is contained in:
commit
fbafb19de6
@ -95,6 +95,12 @@ class SignInHub:
|
|||||||
user_id=user_id, password=password
|
user_id=user_id, password=password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@log_entry_exit_async
|
||||||
|
async def update_user_password_no_depot(self, user_id: str, password: str) -> dict[str, any]:
|
||||||
|
return await self.signin_manager.update_user_password_no_depot(
|
||||||
|
user_id=user_id, password=password
|
||||||
|
)
|
||||||
|
|
||||||
@log_entry_exit_async
|
@log_entry_exit_async
|
||||||
async def send_email_code(self, sender_id: str, email: str) -> dict[str, any]:
|
async def send_email_code(self, sender_id: str, email: str) -> dict[str, any]:
|
||||||
result = await self.signin_manager.send_email_code(sender_id, email)
|
result = await self.signin_manager.send_email_code(sender_id, email)
|
||||||
|
|||||||
@ -368,6 +368,23 @@ class SignInManager:
|
|||||||
)
|
)
|
||||||
return {"succeeded": True}
|
return {"succeeded": True}
|
||||||
|
|
||||||
|
async def update_user_password_no_depot(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_no_depot(
|
||||||
|
user_id, user_flid, password
|
||||||
|
)
|
||||||
|
return {"succeeded": True}
|
||||||
|
|
||||||
async def send_email_code(self, sender_id: str, email: str) -> bool:
|
async def send_email_code(self, sender_id: str, email: str) -> bool:
|
||||||
mail_code = await self.user_auth_service.generate_auth_code_for_object(
|
mail_code = await self.user_auth_service.generate_auth_code_for_object(
|
||||||
email, AuthType.EMAIL
|
email, AuthType.EMAIL
|
||||||
|
|||||||
@ -235,6 +235,30 @@ class UserAuthHandler:
|
|||||||
if not result:
|
if not result:
|
||||||
raise Exception("Failed to update user password in code depot")
|
raise Exception("Failed to update user password in code depot")
|
||||||
|
|
||||||
|
async def save_password_auth_method_no_depot(self, user_id: str, user_flid, password: str):
|
||||||
|
"""save password auth method to user_password doc without updating depot service
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Skip depot service call - users don't exist in Gitea, so we don't update depot password
|
||||||
|
|
||||||
async def reset_password(self, user_id: str):
|
async def reset_password(self, user_id: str):
|
||||||
"""clean password auth method from user_password doc
|
"""clean password auth method from user_password doc
|
||||||
|
|
||||||
|
|||||||
@ -94,7 +94,7 @@ class UserProfileHandler:
|
|||||||
|
|
||||||
async def create_provider_profile(self, user_id: str) -> ProviderProfileDoc:
|
async def create_provider_profile(self, user_id: str) -> ProviderProfileDoc:
|
||||||
provider_profile = await ProviderProfileDoc.find_one(
|
provider_profile = await ProviderProfileDoc.find_one(
|
||||||
ProviderProfileDoc.user_id == user_id
|
{"user_id": user_id}
|
||||||
)
|
)
|
||||||
if provider_profile:
|
if provider_profile:
|
||||||
return provider_profile
|
return provider_profile
|
||||||
|
|||||||
@ -51,3 +51,10 @@ class UserAuthService:
|
|||||||
return await self.user_auth_handler.save_password_auth_method(
|
return await self.user_auth_handler.save_password_auth_method(
|
||||||
user_id, user_flid, password
|
user_id, user_flid, password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def save_password_auth_method_no_depot(
|
||||||
|
self, user_id: str, user_flid: str, password: str
|
||||||
|
):
|
||||||
|
return await self.user_auth_handler.save_password_auth_method_no_depot(
|
||||||
|
user_id, user_flid, password
|
||||||
|
)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from backend.models.user.models import (
|
|||||||
AuthCodeDoc,
|
AuthCodeDoc,
|
||||||
UsageLogDoc
|
UsageLogDoc
|
||||||
)
|
)
|
||||||
from backend.models.user_profile.models import BasicProfileDoc
|
from backend.models.user_profile.models import BasicProfileDoc, ProviderProfileDoc
|
||||||
from backend.models.permission.models import PermissionDoc, RoleDoc, UserRoleDoc
|
from backend.models.permission.models import PermissionDoc, RoleDoc, UserRoleDoc
|
||||||
from common.config.app_settings import app_settings
|
from common.config.app_settings import app_settings
|
||||||
from common.log.module_logger import ModuleLogger
|
from common.log.module_logger import ModuleLogger
|
||||||
@ -25,7 +25,7 @@ import os
|
|||||||
MAIN_CLIENT: Optional[AsyncIOMotorClient] = None
|
MAIN_CLIENT: Optional[AsyncIOMotorClient] = None
|
||||||
TENANT_CACHE: Optional['TenantDBCache'] = None
|
TENANT_CACHE: Optional['TenantDBCache'] = None
|
||||||
|
|
||||||
# Define document models
|
# Define document models
|
||||||
document_models = [
|
document_models = [
|
||||||
UsageLogDoc,
|
UsageLogDoc,
|
||||||
UserAccountDoc,
|
UserAccountDoc,
|
||||||
@ -34,6 +34,7 @@ document_models = [
|
|||||||
UserMobileDoc,
|
UserMobileDoc,
|
||||||
AuthCodeDoc,
|
AuthCodeDoc,
|
||||||
BasicProfileDoc,
|
BasicProfileDoc,
|
||||||
|
ProviderProfileDoc,
|
||||||
PermissionDoc,
|
PermissionDoc,
|
||||||
RoleDoc,
|
RoleDoc,
|
||||||
UserRoleDoc
|
UserRoleDoc
|
||||||
@ -46,6 +47,7 @@ tenant_document_models = [
|
|||||||
UserMobileDoc,
|
UserMobileDoc,
|
||||||
AuthCodeDoc,
|
AuthCodeDoc,
|
||||||
BasicProfileDoc,
|
BasicProfileDoc,
|
||||||
|
ProviderProfileDoc,
|
||||||
PermissionDoc,
|
PermissionDoc,
|
||||||
RoleDoc,
|
RoleDoc,
|
||||||
UserRoleDoc
|
UserRoleDoc
|
||||||
@ -58,7 +60,7 @@ class TenantDBCache:
|
|||||||
Uses main_db.tenant_doc to resolve mongodb_uri; caches clients with LRU.
|
Uses main_db.tenant_doc to resolve mongodb_uri; caches clients with LRU.
|
||||||
Database instances are created fresh each time from cached clients.
|
Database instances are created fresh each time from cached clients.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, main_db: AsyncIOMotorDatabase, max_size: int = 64):
|
def __init__(self, main_db: AsyncIOMotorDatabase, max_size: int = 64):
|
||||||
self.main_db = main_db
|
self.main_db = main_db
|
||||||
self.max_size = max_size
|
self.max_size = max_size
|
||||||
@ -69,13 +71,13 @@ class TenantDBCache:
|
|||||||
|
|
||||||
async def get_initialized_db(self, product_id: str) -> AsyncIOMotorDatabase:
|
async def get_initialized_db(self, product_id: str) -> AsyncIOMotorDatabase:
|
||||||
"""Get tenant database with Beanie already initialized"""
|
"""Get tenant database with Beanie already initialized"""
|
||||||
|
|
||||||
# fast-path: check if client is cached
|
# fast-path: check if client is cached
|
||||||
cached_client = self._cache.get(product_id)
|
cached_client = self._cache.get(product_id)
|
||||||
if cached_client:
|
if cached_client:
|
||||||
await self.module_logger.log_info(f"Found cached client for {product_id}")
|
await self.module_logger.log_info(f"Found cached client for {product_id}")
|
||||||
self._cache.move_to_end(product_id)
|
self._cache.move_to_end(product_id)
|
||||||
|
|
||||||
# Get fresh database instance from cached client
|
# Get fresh database instance from cached client
|
||||||
db = cached_client.get_default_database()
|
db = cached_client.get_default_database()
|
||||||
if db is not None:
|
if db is not None:
|
||||||
@ -95,7 +97,7 @@ class TenantDBCache:
|
|||||||
if cached_client:
|
if cached_client:
|
||||||
await self.module_logger.log_info(f"Double-check found cached client for {product_id}")
|
await self.module_logger.log_info(f"Double-check found cached client for {product_id}")
|
||||||
self._cache.move_to_end(product_id)
|
self._cache.move_to_end(product_id)
|
||||||
|
|
||||||
# Get fresh database instance from cached client
|
# Get fresh database instance from cached client
|
||||||
db = cached_client.get_default_database()
|
db = cached_client.get_default_database()
|
||||||
if db is not None:
|
if db is not None:
|
||||||
@ -149,7 +151,7 @@ class TenantDBCache:
|
|||||||
detail=f"No default database found for tenant {product_id}",
|
detail=f"No default database found for tenant {product_id}",
|
||||||
headers={"X-Error-Message": f"No default database found for tenant {product_id}"}
|
headers={"X-Error-Message": f"No default database found for tenant {product_id}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize Beanie for this tenant database
|
# Initialize Beanie for this tenant database
|
||||||
await init_beanie(database=db, document_models=tenant_document_models)
|
await init_beanie(database=db, document_models=tenant_document_models)
|
||||||
await self.module_logger.log_info(f"Beanie initialization completed for new tenant database {product_id}")
|
await self.module_logger.log_info(f"Beanie initialization completed for new tenant database {product_id}")
|
||||||
@ -201,7 +203,7 @@ def register(app):
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def start_database():
|
async def start_database():
|
||||||
await initiate_database(app)
|
await initiate_database(app)
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def shutdown_database():
|
async def shutdown_database():
|
||||||
await cleanup_database()
|
await cleanup_database()
|
||||||
@ -217,9 +219,9 @@ async def check_database_initialized() -> ProbeResult:
|
|||||||
async def initiate_database(app):
|
async def initiate_database(app):
|
||||||
"""Initialize main database and tenant cache"""
|
"""Initialize main database and tenant cache"""
|
||||||
global MAIN_CLIENT, TENANT_CACHE
|
global MAIN_CLIENT, TENANT_CACHE
|
||||||
|
|
||||||
module_logger = ModuleLogger(sender_id="DatabaseInit")
|
module_logger = ModuleLogger(sender_id="DatabaseInit")
|
||||||
|
|
||||||
# 1) Create main/catalog client + DB
|
# 1) Create main/catalog client + DB
|
||||||
MAIN_CLIENT = AsyncIOMotorClient(app_settings.MONGODB_URI)
|
MAIN_CLIENT = AsyncIOMotorClient(app_settings.MONGODB_URI)
|
||||||
main_db = MAIN_CLIENT[app_settings.MONGODB_NAME]
|
main_db = MAIN_CLIENT[app_settings.MONGODB_NAME]
|
||||||
@ -234,20 +236,20 @@ async def initiate_database(app):
|
|||||||
# 4) Store on app state for middleware to access
|
# 4) Store on app state for middleware to access
|
||||||
app.state.main_db = main_db
|
app.state.main_db = main_db
|
||||||
app.state.tenant_cache = TENANT_CACHE
|
app.state.tenant_cache = TENANT_CACHE
|
||||||
|
|
||||||
await module_logger.log_info("Database and tenant cache initialized successfully")
|
await module_logger.log_info("Database and tenant cache initialized successfully")
|
||||||
|
|
||||||
|
|
||||||
async def cleanup_database():
|
async def cleanup_database():
|
||||||
"""Cleanup database connections and cache"""
|
"""Cleanup database connections and cache"""
|
||||||
global MAIN_CLIENT, TENANT_CACHE
|
global MAIN_CLIENT, TENANT_CACHE
|
||||||
|
|
||||||
module_logger = ModuleLogger(sender_id="DatabaseCleanup")
|
module_logger = ModuleLogger(sender_id="DatabaseCleanup")
|
||||||
|
|
||||||
if TENANT_CACHE:
|
if TENANT_CACHE:
|
||||||
await TENANT_CACHE.aclose()
|
await TENANT_CACHE.aclose()
|
||||||
|
|
||||||
if MAIN_CLIENT:
|
if MAIN_CLIENT:
|
||||||
MAIN_CLIENT.close()
|
MAIN_CLIENT.close()
|
||||||
|
|
||||||
await module_logger.log_info("Database connections closed successfully")
|
await module_logger.log_info("Database connections closed successfully")
|
||||||
@ -5,6 +5,7 @@ from .signin_with_email_and_password import router as se_router
|
|||||||
from .signin_with_email_and_code import router as sw_router
|
from .signin_with_email_and_code import router as sw_router
|
||||||
from .signin_with_magicleaps_email_and_code import router as swm_router
|
from .signin_with_magicleaps_email_and_code import router as swm_router
|
||||||
from .update_user_password import router as up_router
|
from .update_user_password import router as up_router
|
||||||
|
from .update_user_password_no_depot import router as upnd_router
|
||||||
from .update_new_user_flid import router as uu_router
|
from .update_new_user_flid import router as uu_router
|
||||||
from .reset_password_through_email import router as rp_router
|
from .reset_password_through_email import router as rp_router
|
||||||
from .sign_out import router as so_router
|
from .sign_out import router as so_router
|
||||||
@ -17,6 +18,7 @@ router.include_router(tms_router, prefix="/signin", tags=["signin"])
|
|||||||
router.include_router(sw_router, prefix="/signin", tags=["signin"])
|
router.include_router(sw_router, prefix="/signin", tags=["signin"])
|
||||||
router.include_router(swm_router, prefix="/signin", tags=["signin"])
|
router.include_router(swm_router, prefix="/signin", tags=["signin"])
|
||||||
router.include_router(up_router, prefix="/signin", tags=["signin"])
|
router.include_router(up_router, prefix="/signin", tags=["signin"])
|
||||||
|
router.include_router(upnd_router, prefix="/signin", tags=["signin"])
|
||||||
router.include_router(se_router, prefix="/signin", tags=["signin"])
|
router.include_router(se_router, prefix="/signin", tags=["signin"])
|
||||||
router.include_router(so_router, prefix="/signin", tags=["signin"])
|
router.include_router(so_router, prefix="/signin", tags=["signin"])
|
||||||
router.include_router(rp_router, prefix="/signin", tags=["signin"])
|
router.include_router(rp_router, prefix="/signin", tags=["signin"])
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
from backend.application.signin_hub import SignInHub
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi import APIRouter, Security, HTTPException
|
||||||
|
from common.token.token_manager import TokenManager
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
from jose import jwt
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from common.config.app_settings import app_settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
token_manager = TokenManager()
|
||||||
|
# Web API
|
||||||
|
# update_user_password_no_depot
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class RequestIn(BaseModel):
|
||||||
|
password: str
|
||||||
|
password2: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/update-user-password-no-depot",
|
||||||
|
operation_id="user_update_user_password_no_depot",
|
||||||
|
summary="update user's sign-in password without depot service",
|
||||||
|
description="Update the user's sign-in password without updating code depot. 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_no_depot(
|
||||||
|
item: RequestIn,
|
||||||
|
credentials: HTTPAuthorizationCredentials = Security(HTTPBearer()),
|
||||||
|
):
|
||||||
|
payload = jwt.decode(
|
||||||
|
credentials.credentials,
|
||||||
|
app_settings.JWT_SECRET_KEY,
|
||||||
|
algorithms=[app_settings.JWT_ALGORITHM],
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = payload.get("subject").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(
|
||||||
|
{"error": "password and password2 are not the same"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = await SignInHub().update_user_password_no_depot(user_id, item.password)
|
||||||
|
return JSONResponse(content=jsonable_encoder(result))
|
||||||
Loading…
Reference in New Issue
Block a user