diff --git a/apps/authentication/backend/application/signin_hub.py b/apps/authentication/backend/application/signin_hub.py index 5455c4f..834f302 100644 --- a/apps/authentication/backend/application/signin_hub.py +++ b/apps/authentication/backend/application/signin_hub.py @@ -95,6 +95,12 @@ class SignInHub: 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 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) diff --git a/apps/authentication/backend/business/signin_manager.py b/apps/authentication/backend/business/signin_manager.py index 460efa0..2469024 100644 --- a/apps/authentication/backend/business/signin_manager.py +++ b/apps/authentication/backend/business/signin_manager.py @@ -368,6 +368,23 @@ class SignInManager: ) 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: mail_code = await self.user_auth_service.generate_auth_code_for_object( email, AuthType.EMAIL diff --git a/apps/authentication/backend/infra/auth/user_auth_handler.py b/apps/authentication/backend/infra/auth/user_auth_handler.py index 2661fcb..1d5bdf1 100644 --- a/apps/authentication/backend/infra/auth/user_auth_handler.py +++ b/apps/authentication/backend/infra/auth/user_auth_handler.py @@ -235,6 +235,30 @@ class UserAuthHandler: if not result: 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): """clean password auth method from user_password doc diff --git a/apps/authentication/backend/infra/user_profile/user_profile_handler.py b/apps/authentication/backend/infra/user_profile/user_profile_handler.py index 168f84c..5a79fb7 100644 --- a/apps/authentication/backend/infra/user_profile/user_profile_handler.py +++ b/apps/authentication/backend/infra/user_profile/user_profile_handler.py @@ -94,7 +94,7 @@ class UserProfileHandler: async def create_provider_profile(self, user_id: str) -> ProviderProfileDoc: provider_profile = await ProviderProfileDoc.find_one( - ProviderProfileDoc.user_id == user_id + {"user_id": user_id} ) if provider_profile: return provider_profile diff --git a/apps/authentication/backend/services/auth/user_auth_service.py b/apps/authentication/backend/services/auth/user_auth_service.py index bab173f..38cde61 100644 --- a/apps/authentication/backend/services/auth/user_auth_service.py +++ b/apps/authentication/backend/services/auth/user_auth_service.py @@ -51,3 +51,10 @@ class UserAuthService: return await self.user_auth_handler.save_password_auth_method( 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 + ) diff --git a/apps/authentication/webapi/providers/database.py b/apps/authentication/webapi/providers/database.py index e1a2cf8..79d5166 100644 --- a/apps/authentication/webapi/providers/database.py +++ b/apps/authentication/webapi/providers/database.py @@ -10,7 +10,7 @@ from backend.models.user.models import ( AuthCodeDoc, 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 common.config.app_settings import app_settings from common.log.module_logger import ModuleLogger @@ -25,7 +25,7 @@ import os MAIN_CLIENT: Optional[AsyncIOMotorClient] = None TENANT_CACHE: Optional['TenantDBCache'] = None -# Define document models +# Define document models document_models = [ UsageLogDoc, UserAccountDoc, @@ -34,6 +34,7 @@ document_models = [ UserMobileDoc, AuthCodeDoc, BasicProfileDoc, + ProviderProfileDoc, PermissionDoc, RoleDoc, UserRoleDoc @@ -46,6 +47,7 @@ tenant_document_models = [ UserMobileDoc, AuthCodeDoc, BasicProfileDoc, + ProviderProfileDoc, PermissionDoc, RoleDoc, UserRoleDoc @@ -58,7 +60,7 @@ class TenantDBCache: Uses main_db.tenant_doc to resolve mongodb_uri; caches clients with LRU. Database instances are created fresh each time from cached clients. """ - + def __init__(self, main_db: AsyncIOMotorDatabase, max_size: int = 64): self.main_db = main_db self.max_size = max_size @@ -69,13 +71,13 @@ class TenantDBCache: async def get_initialized_db(self, product_id: str) -> AsyncIOMotorDatabase: """Get tenant database with Beanie already initialized""" - + # fast-path: check if client is cached cached_client = self._cache.get(product_id) if cached_client: await self.module_logger.log_info(f"Found cached client for {product_id}") self._cache.move_to_end(product_id) - + # Get fresh database instance from cached client db = cached_client.get_default_database() if db is not None: @@ -95,7 +97,7 @@ class TenantDBCache: if cached_client: await self.module_logger.log_info(f"Double-check found cached client for {product_id}") self._cache.move_to_end(product_id) - + # Get fresh database instance from cached client db = cached_client.get_default_database() if db is not None: @@ -149,7 +151,7 @@ class TenantDBCache: detail=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 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}") @@ -201,7 +203,7 @@ def register(app): @app.on_event("startup") async def start_database(): await initiate_database(app) - + @app.on_event("shutdown") async def shutdown_database(): await cleanup_database() @@ -217,9 +219,9 @@ async def check_database_initialized() -> ProbeResult: async def initiate_database(app): """Initialize main database and tenant cache""" global MAIN_CLIENT, TENANT_CACHE - + module_logger = ModuleLogger(sender_id="DatabaseInit") - + # 1) Create main/catalog client + DB MAIN_CLIENT = AsyncIOMotorClient(app_settings.MONGODB_URI) 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 app.state.main_db = main_db app.state.tenant_cache = TENANT_CACHE - + await module_logger.log_info("Database and tenant cache initialized successfully") async def cleanup_database(): """Cleanup database connections and cache""" global MAIN_CLIENT, TENANT_CACHE - + module_logger = ModuleLogger(sender_id="DatabaseCleanup") - + if TENANT_CACHE: await TENANT_CACHE.aclose() - + if MAIN_CLIENT: MAIN_CLIENT.close() - + await module_logger.log_info("Database connections closed successfully") \ No newline at end of file diff --git a/apps/authentication/webapi/routes/signin/__init__.py b/apps/authentication/webapi/routes/signin/__init__.py index 2ee91b6..1e6aab1 100644 --- a/apps/authentication/webapi/routes/signin/__init__.py +++ b/apps/authentication/webapi/routes/signin/__init__.py @@ -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_magicleaps_email_and_code import router as swm_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 .reset_password_through_email import router as rp_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(swm_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(so_router, prefix="/signin", tags=["signin"]) router.include_router(rp_router, prefix="/signin", tags=["signin"]) diff --git a/apps/authentication/webapi/routes/signin/update_user_password_no_depot.py b/apps/authentication/webapi/routes/signin/update_user_password_no_depot.py new file mode 100644 index 0000000..7781984 --- /dev/null +++ b/apps/authentication/webapi/routes/signin/update_user_password_no_depot.py @@ -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))