Skip to content

Commit

Permalink
Adjusted according to review comments and other improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms committed Sep 22, 2023
1 parent c1b281d commit e098335
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 26 deletions.
40 changes: 24 additions & 16 deletions backend/src/backend/primary/routers/general.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import asyncio
import datetime
import logging
import json

import starsessions
from starlette.responses import StreamingResponse
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
from src.services.graph_access.graph_access import GraphApiAccess

LOGGER = logging.getLogger(__name__)

Expand All @@ -23,6 +21,7 @@ class UserInfo(BaseModel):
has_sumo_access: bool
has_smda_access: bool


router = APIRouter()


Expand All @@ -39,7 +38,12 @@ def alive_protected() -> str:


@router.get("/logged_in_user", response_model=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:
async def logged_in_user(
request: Request,
includeGraphApiInfo: bool = Query(
False, description="Set to true to include user avatar and display name from Microsoft Graph API"
),
) -> UserInfo:
print("entering logged_in_user route")

await starsessions.load_session(request)
Expand All @@ -58,20 +62,24 @@ async def logged_in_user(request: Request, includeAvatar: bool = Query(False, de
has_smda_access=authenticated_user.has_smda_access_token(),
)

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)

async with httpx.AsyncClient() as client:
result = await client.get("https://graph.microsoft.com/v1.0/me", headers=headers)
if result.status_code == 200:
user_info.display_name = json.loads(result.content.decode("utf-8")).get("displayName", None)
if authenticated_user.has_graph_access_token() and includeGraphApiInfo:
graph_api_access = GraphApiAccess(authenticated_user.get_graph_access_token())
try:
avatar_b64str_future = asyncio.create_task(graph_api_access.get_user_profile_photo())
graph_user_info_future = asyncio.create_task(graph_api_access.get_user_info())

avatar_b64str = await avatar_b64str_future
graph_user_info = await graph_user_info_future

user_info.avatar_b64str = avatar_b64str
if graph_user_info is not None:
user_info.display_name = graph_user_info.get("displayName", None)
except Exception as e:
print("Error while fetching user avatar and info from Microsoft Graph API:\n", e)

return user_info


@router.get("/user_session_container")
async def user_session_container(
request: Request, authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)
Expand Down
39 changes: 39 additions & 0 deletions backend/src/services/graph_access/graph_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import base64
from typing import Mapping

# Using the same http client as sumo
import httpx


class GraphApiAccess:
def __init__(self, access_token: str):
self._access_token = access_token

def _make_headers(self) -> Mapping[str, str]:
return {"Authorization": f"Bearer {self._access_token}"}

async def _request(self, url: str) -> httpx.Response:
async with httpx.AsyncClient() as client:
response = await client.get(
url,
headers=self._make_headers(),
)
return response

async def get_user_profile_photo(self) -> str | None:
print("entering get_user_profile_photo")
response = await self._request("https://graph.microsoft.com/v1.0/me/photo/$value")

if response.status_code == 200:
return base64.b64encode(response.content)
else:
return None

async def get_user_info(self) -> Mapping[str, str] | None:
print("entering get_user_info")
response = await self._request("https://graph.microsoft.com/v1.0/me")

if response.status_code == 200:
return response.json()
else:
return None
8 changes: 4 additions & 4 deletions backend/src/services/utils/authenticated_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ 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:
if isinstance(self._graph_access_token, str) and self._graph_access_token:
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:
except ValueError:
return False

def get_sumo_access_token(self) -> str:
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/api/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,18 @@ export class DefaultService {

/**
* Logged In User
* @param includeAvatar Set to true to include user avatar from Microsoft GRAPH Api
* @param includeGraphApiInfo Set to true to include user avatar and display name from Microsoft Graph API
* @returns UserInfo Successful Response
* @throws ApiError
*/
public loggedInUser(
includeAvatar: boolean = false,
includeGraphApiInfo: boolean = false,
): CancelablePromise<UserInfo> {
return this.httpRequest.request({
method: 'GET',
url: '/logged_in_user',
query: {
'includeAvatar': includeAvatar,
'includeGraphApiInfo': includeGraphApiInfo,
},
errors: {
422: `Validation Error`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import { resolveClassNames } from "@lib/utils/resolveClassNames";
import { getTextWidth } from "@lib/utils/textSize";
import { Dropdown, MenuButton } from "@mui/base";

function makeInitials(name: string): string | null {
const regExp = new RegExp(/([^()]+)(\([\w ]+\))/);
const match = regExp.exec(name);

if (match) {
const names = match[1].trim().split(" ");
if (names.length > 1) {
return names[0].charAt(0) + names[names.length - 1].charAt(0);
}
}
return null;
}

export type LoginButtonProps = {
className?: string;
showText?: boolean;
Expand All @@ -34,12 +47,22 @@ export const LoginButton: React.FC<LoginButtonProps> = (props) => {
<img
src={`data:image/png;base64,${userInfo.avatar_b64str}`}
alt="Avatar"
className="w-4 h-4 rounded-full"
className="w-5 h-5 rounded-full mr-1"
/>
);
}

if (userInfo?.display_name) {
const initials = makeInitials(userInfo.display_name);
if (initials) {
return (
<div className="w-5 h-5 rounded-full bg-slate-300 text-[0.6em] flex items-center justify-center mr-1">
{initials}
</div>
);
}
}
return <UserIcon className="w-5 h-5 mr-1" />;

} else if (authState === AuthState.NotLoggedIn) {
return <ArrowLeftOnRectangleIcon className="w-5 h-5 mr-1" />;
} else {
Expand Down Expand Up @@ -81,7 +104,7 @@ export const LoginButton: React.FC<LoginButtonProps> = (props) => {
>
<span
className="flex items-center gap-2"
title={authState === AuthState.LoggedIn ? `Signed in as ${userInfo?.username}` : "Sign in"}
title={makeText()}
>
{makeIcon()}
<span className="overflow-hidden text-ellipsis min-w-0 whitespace-nowrap" ref={textRef}>
Expand Down

0 comments on commit e098335

Please sign in to comment.