From c200e709704a6f8c3bada0776995268a975ebd2b Mon Sep 17 00:00:00 2001 From: sunhaolou Date: Mon, 25 Aug 2025 17:31:16 +0800 Subject: [PATCH] feat: add new apis for Magicleaps product use --- .../backend/application/signin_hub.py | 4 + .../backend/business/signin_manager.py | 40 ++++++++ .../webapi/routes/signin/__init__.py | 4 + .../signin_with_magicleaps_email_and_code.py | 97 +++++++++++++++++++ .../try_magicleaps_signin_with_email.py | 37 +++++++ .../backend/business/notification_manager.py | 2 +- apps/notification/backend/models/constants.py | 16 +++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 apps/authentication/webapi/routes/signin/signin_with_magicleaps_email_and_code.py create mode 100644 apps/authentication/webapi/routes/signin/try_magicleaps_signin_with_email.py diff --git a/apps/authentication/backend/application/signin_hub.py b/apps/authentication/backend/application/signin_hub.py index 524fb8e..5455c4f 100644 --- a/apps/authentication/backend/application/signin_hub.py +++ b/apps/authentication/backend/application/signin_hub.py @@ -79,6 +79,10 @@ class SignInHub: async def try_signin_with_email(self, email: str, host: str) -> UserLoginAction: return await self.signin_manager.try_signin_with_email(email=email, host=host) + @log_entry_exit_async + async def try_magicleaps_signin_with_email(self, email: str, host: str) -> UserLoginAction: + return await self.signin_manager.try_magicleaps_signin_with_email(email=email, host=host) + @log_entry_exit_async async def reset_password_through_email(self, email: str, host: str) -> int: return await self.signin_manager.reset_password_through_email( diff --git a/apps/authentication/backend/business/signin_manager.py b/apps/authentication/backend/business/signin_manager.py index fb6adbb..460efa0 100644 --- a/apps/authentication/backend/business/signin_manager.py +++ b/apps/authentication/backend/business/signin_manager.py @@ -276,6 +276,46 @@ class SignInManager: else: return UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED + async def try_magicleaps_signin_with_email(self, email: str, host: str) -> UserLoginAction: + """try signin through email using MagicLeaps branding, generate auth code and send to the email address + + Args: + email (str): email address + host (str): host url that user tried to sign in + + Returns: + int: UserLoginAction + """ + user_id = await self.user_auth_service.get_user_id_by_email(email) + + is_password_reset_required = False + if user_id: + is_password_reset_required = ( + await self.user_auth_service.is_password_reset_required(user_id) + ) + + if user_id is None or is_password_reset_required: + # send auth code through email if the email address + # hasn't been associated with any account. + # Or if the user's password is empty, which means the user's pasword hasn't been set. + + mail_code = await self.user_auth_service.generate_auth_code_for_object( + email, AuthType.EMAIL + ) + await self.notification_service.send_notification( + sender_id=app_settings.SYSTEM_USER_ID, + channels=["2"], # 2 maps to email in NotificationChannel + receiver_id=email, + subject="email", + event="magicleaps_authentication", # Use the new event type + properties={"auth_code": mail_code}, + # TODO: reconsider necessity of adding region info here + # region=RegionHandler().detect_from_host(host), + ) + return UserLoginAction.VERIFY_EMAIL_WITH_AUTH_CODE + else: + return UserLoginAction.EXISTING_USER_PASSWORD_REQUIRED + async def reset_password_through_email(self, email: str, host: str) -> int: """verify the email is exisitng, clear the existing password, generate auth code and send to the email address diff --git a/apps/authentication/webapi/routes/signin/__init__.py b/apps/authentication/webapi/routes/signin/__init__.py index 641f639..2ee91b6 100644 --- a/apps/authentication/webapi/routes/signin/__init__.py +++ b/apps/authentication/webapi/routes/signin/__init__.py @@ -1,7 +1,9 @@ from fastapi import APIRouter from .try_signin_with_email import router as ts_router +from .try_magicleaps_signin_with_email import router as tms_router 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_new_user_flid import router as uu_router from .reset_password_through_email import router as rp_router @@ -11,7 +13,9 @@ from .sign_out import router as so_router router = APIRouter() router.include_router(ts_router, prefix="/signin", tags=["signin"]) +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(se_router, prefix="/signin", tags=["signin"]) router.include_router(so_router, prefix="/signin", tags=["signin"]) diff --git a/apps/authentication/webapi/routes/signin/signin_with_magicleaps_email_and_code.py b/apps/authentication/webapi/routes/signin/signin_with_magicleaps_email_and_code.py new file mode 100644 index 0000000..ce0a435 --- /dev/null +++ b/apps/authentication/webapi/routes/signin/signin_with_magicleaps_email_and_code.py @@ -0,0 +1,97 @@ +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.signin_hub import SignInHub +from common.constants.jwt_constants import USER_ROLE_NAMES, USER_PERMISSIONS +from common.token.token_manager import TokenManager + + +router = APIRouter() +token_manager = TokenManager() + +# Web API +# signin_with_magicleaps_email_and_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-magicleaps-email-and-code", + operation_id="user-signin-with-magicleaps-email-and-code", + summary="try to signin with email and authentication code using MagicLeaps branding", + description="client user is trying to sign in with their email and the authenication code \ + the system sent to the email in previous step using MagicLeaps branding.", + response_model=ResponseOut, +) +async def signin_with_magicleaps_email_and_code(item: RequestIn) -> ResponseOut: + ( + signed_in, + adminstrative_role, + identity, + flid, + preferred_region, + user_role_names, + user_permission_keys + ) = await SignInHub().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, USER_ROLE_NAMES: user_role_names, USER_PERMISSIONS: user_permission_keys} + 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=token_manager.access_token_expire_minutes + ) + 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, + USER_ROLE_NAMES: user_role_names, + USER_PERMISSIONS: user_permission_keys, + "flid": flid, + "preferred_region": preferred_region, + } + + return JSONResponse(content=jsonable_encoder(result)) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/signin/try_magicleaps_signin_with_email.py b/apps/authentication/webapi/routes/signin/try_magicleaps_signin_with_email.py new file mode 100644 index 0000000..50a2843 --- /dev/null +++ b/apps/authentication/webapi/routes/signin/try_magicleaps_signin_with_email.py @@ -0,0 +1,37 @@ +from backend.application.signin_hub import SignInHub +from pydantic import BaseModel +from fastapi import APIRouter +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse + +router = APIRouter() + +# Web API +# try_magicleaps_signin_with_email +# + + +class UserSignWithEmailBody(BaseModel): + email: str + host: str + + +class UserSignWithEmailResponse(BaseModel): + signin_type: int + + +@router.post( + "/try-magicleaps-signin-with-email", + operation_id="user-try-magicleaps-signin-with-email", + summary="try to signin with email using MagicLeaps branding", + description="A client user is trying to sign in with their email using MagicLeaps branding. \ + The system will determine to send an authentication code to the email \ + or let the user use their FLID and password to sign in", + response_description="signin_type:0 meaning simplified(using email) signin, \ + 1 meaning standard(using FLID and password) signin", +) +async def try_magicleaps_signin_with_email( + item: UserSignWithEmailBody, +): + result = await SignInHub().try_magicleaps_signin_with_email(item.email, item.host) + return JSONResponse(content=jsonable_encoder(result)) \ No newline at end of file diff --git a/apps/notification/backend/business/notification_manager.py b/apps/notification/backend/business/notification_manager.py index eac6f63..e1f30cf 100644 --- a/apps/notification/backend/business/notification_manager.py +++ b/apps/notification/backend/business/notification_manager.py @@ -100,7 +100,7 @@ class NotificationManager: message_subject = notification_config["message_subject"] # Handle authentication specific formatting - if event_lower == "authentication" and "auth_code" in properties: + if (event_lower == "authentication" or event_lower == "magicleaps_authentication") and "auth_code" in properties: message = message.format(properties["auth_code"]) # Append content_text if it exists in properties diff --git a/apps/notification/backend/models/constants.py b/apps/notification/backend/models/constants.py index 7702cb4..cfc8f57 100644 --- a/apps/notification/backend/models/constants.py +++ b/apps/notification/backend/models/constants.py @@ -58,12 +58,20 @@ SystemNotifications = { "authentication": { # Event "message_subject": "Freeleaps Support", "message": "The auth code is: {} . \r\nDo not share this to anyone.", + }, + "magicleaps_authentication": { # Event + "message_subject": "MagicLeaps Support", + "message": "The auth code is: {} . \r\nDo not share this to anyone.", } }, "mobile": { # Subject "authentication": { # Event "message_subject": "Freeleaps Support", "message": "The auth code is: {} . \r\nDo not share this to anyone.", + }, + "magicleaps_authentication": { # Event + "message_subject": "MagicLeaps Support", + "message": "The auth code is: {} . \r\nDo not share this to anyone.", } }, "message": { # Subject @@ -112,12 +120,20 @@ SystemNotifications = { "authentication": { # Event "message_subject": "自由跳跃技术支持", "message": "安全认证码为: {} . \r\n切勿分享给他人", + }, + "magicleaps_authentication": { # Event + "message_subject": "MagicLeaps 技术支持", + "message": "安全认证码为: {} . \r\n切勿分享给他人", } }, "mobile": { # Subject "authentication": { # Event "message_subject": "自由跳跃技术支持", "message": "安全认证码为: {} . \r\n切勿分享给他人", + }, + "magicleaps_authentication": { # Event + "message_subject": "MagicLeaps 技术支持", + "message": "安全认证码为: {} . \r\n切勿分享给他人", } }, "message": { # Subject