diff --git a/backend/src/backend/auth/auth_helper.py b/backend/src/backend/auth/auth_helper.py index 57fb1e969..03b7a7df5 100644 --- a/backend/src/backend/auth/auth_helper.py +++ b/backend/src/backend/auth/auth_helper.py @@ -173,6 +173,9 @@ def get_authenticated_user( # print("-------------------------------------------------") smda_token = token_dict.get("access_token") if token_dict else None + token_dict = cca.acquire_token_silent(scopes=config.GRAPH_SCOPES, account=accounts[0]) + graph_token = token_dict.get("access_token") if token_dict else None + # print(f" get tokens {timer.lap_ms():.1f}ms") _save_token_cache_in_session(request_with_session, token_cache) @@ -187,6 +190,7 @@ def get_authenticated_user( authenticated_user = AuthenticatedUser( user_id=user_id, username=user_name, + graph_access_token=graph_token, sumo_access_token=sumo_token, smda_access_token=smda_token, pdm_access_token=None, diff --git a/backend/src/backend/primary/routers/general.py b/backend/src/backend/primary/routers/general.py index 304c0d3ee..636188e5c 100644 --- a/backend/src/backend/primary/routers/general.py +++ b/backend/src/backend/primary/routers/general.py @@ -3,8 +3,11 @@ import starsessions from starlette.responses import StreamingResponse -from fastapi import APIRouter, HTTPException, Request, status, Depends +from fastapi import APIRouter, HTTPException, Request, status, Depends, Query from pydantic import BaseModel +# Using the same http client as sumo +import httpx +import base64 from src.backend.auth.auth_helper import AuthHelper, AuthenticatedUser from src.backend.primary.user_session_proxy import proxy_to_user_session @@ -14,9 +17,12 @@ class UserInfo(BaseModel): username: str + avatar_b64str: str | None has_sumo_access: bool has_smda_access: bool +class UserAvatar(BaseModel): + avatar: bytes router = APIRouter() @@ -34,7 +40,7 @@ def alive_protected() -> str: @router.get("/logged_in_user", response_model=UserInfo) -async def logged_in_user(request: Request) -> UserInfo: +async def logged_in_user(request: Request, includeAvatar: bool = Query(False, description="Set to true to include user avatar from Microsoft GRAPH Api")) -> UserInfo: print("entering logged_in_user route") await starsessions.load_session(request) @@ -47,12 +53,19 @@ async def logged_in_user(request: Request) -> UserInfo: user_info = UserInfo( username=authenticated_user.get_username(), + avatar_b64str=None, has_sumo_access=authenticated_user.has_sumo_access_token(), has_smda_access=authenticated_user.has_smda_access_token(), ) - return user_info + if includeAvatar: + headers = { "Authorization": f"Bearer {authenticated_user.get_graph_access_token()}" } + async with httpx.AsyncClient() as client: + result = await client.get("https://graph.microsoft.com/v1.0/me/photo/$value", headers=headers) + if result.status_code == 200: + user_info.avatar_b64str = base64.b64encode(result.content) + return user_info @router.get("/user_session_container") async def user_session_container( diff --git a/backend/src/services/utils/authenticated_user.py b/backend/src/services/utils/authenticated_user.py index 97f3ed7df..8b6ae8857 100644 --- a/backend/src/services/utils/authenticated_user.py +++ b/backend/src/services/utils/authenticated_user.py @@ -8,6 +8,7 @@ def __init__( self, user_id: str, username: str, + graph_access_token: Optional[str], sumo_access_token: Optional[str], smda_access_token: Optional[str], pdm_access_token: Optional[str], @@ -15,6 +16,7 @@ def __init__( ) -> None: self._user_id = user_id self._username = username + self._graph_access_token = graph_access_token self._sumo_access_token = sumo_access_token self._smda_access_token = smda_access_token self._pdm_access_token = pdm_access_token @@ -28,6 +30,19 @@ def __eq__(self, other: Any) -> bool: def get_username(self) -> str: return self._username + + def get_graph_access_token(self) -> str: + if isinstance(self._graph_access_token, str) and len(self._graph_access_token) > 0: + return self._graph_access_token + + raise ValueError("User has no graph access token") + + def has_graph_access_token(self) -> bool: + try: + self.get_graph_access_token() + return True + except: + return False def get_sumo_access_token(self) -> str: if isinstance(self._sumo_access_token, str) and len(self._sumo_access_token) > 0: diff --git a/frontend/src/api/models/UserInfo.ts b/frontend/src/api/models/UserInfo.ts index 1d80b9237..bc2dc25c9 100644 --- a/frontend/src/api/models/UserInfo.ts +++ b/frontend/src/api/models/UserInfo.ts @@ -4,6 +4,7 @@ export type UserInfo = { username: string; + avatar_b64str: (string | null); has_sumo_access: boolean; has_smda_access: boolean; }; diff --git a/frontend/src/api/services/DefaultService.ts b/frontend/src/api/services/DefaultService.ts index 6cc8012e0..3a2501e92 100644 --- a/frontend/src/api/services/DefaultService.ts +++ b/frontend/src/api/services/DefaultService.ts @@ -69,13 +69,22 @@ export class DefaultService { /** * Logged In User + * @param includeAvatar Set to true to include user avatar from Microsoft GRAPH Api * @returns UserInfo Successful Response * @throws ApiError */ - public loggedInUser(): CancelablePromise { + public loggedInUser( + includeAvatar: boolean = false, + ): CancelablePromise { return this.httpRequest.request({ method: 'GET', url: '/logged_in_user', + query: { + 'includeAvatar': includeAvatar, + }, + errors: { + 422: `Validation Error`, + }, }); } diff --git a/frontend/src/framework/internal/components/LoginButton/loginButton.tsx b/frontend/src/framework/internal/components/LoginButton/loginButton.tsx index bc365854c..7d13f6e14 100644 --- a/frontend/src/framework/internal/components/LoginButton/loginButton.tsx +++ b/frontend/src/framework/internal/components/LoginButton/loginButton.tsx @@ -29,6 +29,16 @@ export const LoginButton: React.FC = (props) => { function makeIcon() { if (authState === AuthState.LoggedIn) { + if (userInfo?.avatar_b64str) { + return ( + Avatar + ); + } + return ; } else if (authState === AuthState.NotLoggedIn) { return ;