Feature/33 api fails when no pilots or logbook entries #35
@@ -1,37 +1,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ISiteNavProps } from './ISiteNavProps';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Navbar,
|
Navbar,
|
||||||
PlaneIcon,
|
PlaneIcon,
|
||||||
SignOutIcon,
|
SignOutIcon,
|
||||||
Spinner,
|
|
||||||
Typography
|
Typography
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||||
import { InteractionStatus } from '@azure/msal-browser';
|
|
||||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import { User } from '@microsoft/microsoft-graph-types';
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
import { loginRequest } from '../../hooks/auth/authConfig';
|
||||||
|
|
||||||
type EventPayloadExtended = EventPayload & { accessToken: string };
|
|
||||||
|
|
||||||
const SiteNav: React.FC<unknown> = () => {
|
const SiteNav: React.FC<unknown> = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [userPhoto, setUserPhoto] = useState<string>();
|
const [userPhoto, setUserPhoto] = useState<string>();
|
||||||
const httpClient: AxiosInstance = useHttpClient();
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const isAuthenticated = useIsAuthenticated();
|
||||||
const { getAccessToken } = useAccessToken();
|
const { getAccessToken } = useAccessToken();
|
||||||
const { inProgress, instance } = useMsal();
|
const { instance } = useMsal();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const pages = [
|
const pages = [
|
||||||
{
|
{
|
||||||
@@ -43,28 +33,23 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
url: '/pilots'
|
url: '/pilots'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const handleSignIn = async () => {
|
|
||||||
// instance.handleRedirectPromise().then((response) => {
|
const handleSignInRedirect = () => {
|
||||||
// if (response) {
|
instance
|
||||||
// console.log(response)
|
.loginRedirect({
|
||||||
// console.log('successful login', response);
|
...loginRequest,
|
||||||
// } else {
|
prompt: 'create',
|
||||||
// instance.loginRedirect({
|
})
|
||||||
// scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
.catch((error) => console.log(error));
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }).catch((error) => {
|
|
||||||
// console.log(error);
|
|
||||||
// })
|
|
||||||
try {
|
|
||||||
await instance.loginPopup();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleSignOut = () => {
|
|
||||||
instance.logoutRedirect();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSignOutRedirect = () => {
|
||||||
|
instance.logoutRedirect({
|
||||||
|
postLogoutRedirectUri: '/'
|
||||||
|
})
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
const getUserProfile = async (accessToken: string): Promise<User> => {
|
const getUserProfile = async (accessToken: string): Promise<User> => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
||||||
@@ -79,6 +64,7 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
||||||
@@ -103,7 +89,7 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
return (
|
return (
|
||||||
<MenuItem onClick={handleSignOut}>
|
<MenuItem onClick={handleSignOutRedirect}>
|
||||||
<SignOutIcon />
|
<SignOutIcon />
|
||||||
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
|
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
|
||||||
Sign Out
|
Sign Out
|
||||||
@@ -112,46 +98,9 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const callback = instance.addEventCallback(
|
|
||||||
async (message: EventMessage) => {
|
|
||||||
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const eventPayload: EventPayloadExtended =
|
|
||||||
message.payload as EventPayloadExtended;
|
|
||||||
const userProfile: User = await getUserProfile(
|
|
||||||
eventPayload.accessToken
|
|
||||||
);
|
|
||||||
const userPhoto = await getUserPhoto(eventPayload.accessToken);
|
|
||||||
|
|
||||||
appContext.dispatch({
|
|
||||||
type: 'SET_USER_PROFILE',
|
|
||||||
payload: userProfile
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (callback) {
|
|
||||||
instance.removeEventCallback(callback);
|
|
||||||
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setUserProfile = async () => {
|
const setUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const accessToken: string = await getAccessToken();
|
const accessToken: string = await getAccessToken();
|
||||||
const userProfile = await getUserProfile(accessToken);
|
const userProfile = await getUserProfile(accessToken);
|
||||||
const userPhoto = await getUserPhoto(accessToken);
|
const userPhoto = await getUserPhoto(accessToken);
|
||||||
@@ -164,8 +113,6 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,7 +127,7 @@ const SiteNav: React.FC<unknown> = () => {
|
|||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
handlePageClick={handlePageClick}
|
handlePageClick={handlePageClick}
|
||||||
handleSignIn={handleSignIn}
|
handleSignIn={handleSignInRedirect}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
logo={<PlaneIcon size="2x" />}
|
logo={<PlaneIcon size="2x" />}
|
||||||
pages={pages}
|
pages={pages}
|
||||||
|
|||||||
7
app/src/hooks/auth/UseAuthProvider.tsx
Normal file
7
app/src/hooks/auth/UseAuthProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { AuthProvider } from "./authProvider"
|
||||||
|
|
||||||
|
export const useAuthProvider = () => {
|
||||||
|
return {
|
||||||
|
AuthProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
70
app/src/hooks/auth/authConfig.ts
Normal file
70
app/src/hooks/auth/authConfig.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LogLevel } from '@azure/msal-browser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object to be passed to MSAL instance on creation.
|
||||||
|
* For a full list of MSAL.js configuration parameters, visit:
|
||||||
|
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const msalConfig = {
|
||||||
|
auth: {
|
||||||
|
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
|
||||||
|
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
|
||||||
|
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
|
||||||
|
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
|
||||||
|
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
|
||||||
|
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
loggerOptions: {
|
||||||
|
loggerCallback: (level: any, message: any, containsPii: any) => {
|
||||||
|
if (containsPii) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Error:
|
||||||
|
console.error(message);
|
||||||
|
return;
|
||||||
|
case LogLevel.Info:
|
||||||
|
console.info(message);
|
||||||
|
return;
|
||||||
|
case LogLevel.Verbose:
|
||||||
|
console.debug(message);
|
||||||
|
return;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
console.warn(message);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scopes you add here will be prompted for user consent during sign-in.
|
||||||
|
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
|
||||||
|
* For more information about OIDC scopes, visit:
|
||||||
|
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
|
||||||
|
*/
|
||||||
|
export const loginRequest = {
|
||||||
|
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional silentRequest object can be used to achieve silent SSO
|
||||||
|
* between applications by providing a "login_hint" property.
|
||||||
|
*/
|
||||||
|
// export const silentRequest = {
|
||||||
|
// scopes: ["openid", "profile"],
|
||||||
|
// loginHint: "example@domain.net"
|
||||||
|
// };
|
||||||
32
app/src/hooks/auth/authProvider.tsx
Normal file
32
app/src/hooks/auth/authProvider.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { AuthenticationResult, EventType, PublicClientApplication } from "@azure/msal-browser";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { msalConfig } from "./authConfig";
|
||||||
|
import { MsalProvider } from "@azure/msal-react";
|
||||||
|
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: AuthProviderProps ) => {
|
||||||
|
const msalInstance = new PublicClientApplication(msalConfig);
|
||||||
|
|
||||||
|
// Default to using the first account if no account is active on page load
|
||||||
|
if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) {
|
||||||
|
// Account selection logic is app dependent. Adjust as needed for different use cases.
|
||||||
|
msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for sign-in event and set active account
|
||||||
|
msalInstance.addEventCallback((event) => {
|
||||||
|
const authenticationResult = event.payload as AuthenticationResult;
|
||||||
|
const account = authenticationResult?.account;
|
||||||
|
|
||||||
|
if (event.eventType === EventType.LOGIN_SUCCESS && account) {
|
||||||
|
msalInstance.setActiveAccount(account);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <MsalProvider instance={msalInstance}>
|
||||||
|
{children}
|
||||||
|
</MsalProvider>
|
||||||
|
}
|
||||||
@@ -5,27 +5,21 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import '@noahspan/noahspan-components/noahspan-components.css';
|
import '@noahspan/noahspan-components/noahspan-components.css';
|
||||||
import { PublicClientApplication } from '@azure/msal-browser';
|
import { EventMessage, EventPayload, EventType, LogLevel, PublicClientApplication } from '@azure/msal-browser';
|
||||||
import { MsalProvider } from '@azure/msal-react';
|
import { MsalProvider } from '@azure/msal-react';
|
||||||
|
import { useAuthProvider } from './hooks/auth/UseAuthProvider.tsx';
|
||||||
|
|
||||||
const msalInstance: PublicClientApplication = new PublicClientApplication({
|
const { AuthProvider } = useAuthProvider();
|
||||||
auth: {
|
|
||||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
|
<React.StrictMode>
|
||||||
redirectUri: import.meta.env.VITE_REDIRECT_URL
|
<AuthProvider>
|
||||||
}
|
<AppContextProvider>
|
||||||
});
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</AppContextProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
msalInstance.initialize().then(() => {
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<MsalProvider instance={msalInstance}>
|
|
||||||
<AppContextProvider>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</AppContextProvider>
|
|
||||||
</MsalProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -8,55 +8,66 @@ locals {
|
|||||||
container_app_app_container_image = {
|
container_app_app_container_image = {
|
||||||
dev = "noahspan/flying-app:v0.0.1"
|
dev = "noahspan/flying-app:v0.0.1"
|
||||||
test = "noahspan/flying-app:v0.0.1"
|
test = "noahspan/flying-app:v0.0.1"
|
||||||
|
prod = "noahspan/flying-app:v0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_app_container_name = {
|
container_app_app_container_name = {
|
||||||
dev = "flying-app-dev"
|
dev = "flying-app-dev"
|
||||||
test = "flying-app-test"
|
test = "flying-app-test"
|
||||||
|
prod = "flying-app-prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_api_name = {
|
container_app_api_name = {
|
||||||
dev = "flying-api-dev"
|
dev = "flying-api-dev"
|
||||||
test = "flying-api-test"
|
test = "flying-api-test"
|
||||||
|
prod = "flying-api-prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_api_container_image = {
|
container_app_api_container_image = {
|
||||||
dev = "noahspan/flying-api:v0.0.1"
|
dev = "noahspan/flying-api:v0.0.1"
|
||||||
test = "noahspan/flying-api:v0.0.1"
|
test = "noahspan/flying-api:v0.0.1"
|
||||||
|
prod = "noahspan/flying-api:v0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_api_container_name = {
|
container_app_api_container_name = {
|
||||||
dev = "flying-api-dev"
|
dev = "flying-api-dev"
|
||||||
test = "flying-api-test"
|
test = "flying-api-test"
|
||||||
|
prod = "flying-api-prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_api_dapr_app_id = {
|
container_app_api_dapr_app_id = {
|
||||||
dev = "flyingapidev"
|
dev = "flyingapidev"
|
||||||
test = "flyingapitest"
|
test = "flyingapitest"
|
||||||
|
prod = "flyingpapiprod"
|
||||||
}
|
}
|
||||||
|
|
||||||
container_app_environment_name = {
|
container_app_environment_name = {
|
||||||
dev = "flying-dev"
|
dev = "flying-dev"
|
||||||
test = "flying-test"
|
test = "flying-test"
|
||||||
|
prod = "flying-prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
logbook_feature_flag_active = {
|
logbook_feature_flag_active = {
|
||||||
dev = "true"
|
dev = "true"
|
||||||
test = "true"
|
test = "true"
|
||||||
|
prod = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
log_analytics_workspace_name = {
|
log_analytics_workspace_name = {
|
||||||
dev = "flying-log-analytics-workspace-dev"
|
dev = "flying-log-analytics-workspace-dev"
|
||||||
test = "flying-log-analytics-workspace-test"
|
test = "flying-log-analytics-workspace-test"
|
||||||
|
prod = "flying-log-analytics-workspace-prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
pilots_feature_flag_active = {
|
pilots_feature_flag_active = {
|
||||||
dev = "true"
|
dev = "true"
|
||||||
test = "true"
|
test = "true"
|
||||||
|
prod = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
storage_account_name = {
|
storage_account_name = {
|
||||||
dev = "noahspanflyingdev"
|
dev = "noahspanflyingdev"
|
||||||
test = "noahspanflyingtest"
|
test = "noahspanflyingtest"
|
||||||
|
prod = "noahspanflyingprod"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,10 +30,6 @@ output "container_app_environment_name" {
|
|||||||
value = local.container_app_environment_name[var.environment]
|
value = local.container_app_environment_name[var.environment]
|
||||||
}
|
}
|
||||||
|
|
||||||
output "key_vault_name" {
|
|
||||||
value = local.key_vault_name[var.environment]
|
|
||||||
}
|
|
||||||
|
|
||||||
output "logbook_feature_flag_active" {
|
output "logbook_feature_flag_active" {
|
||||||
value = local.logbook_feature_flag_active[var.environment]
|
value = local.logbook_feature_flag_active[var.environment]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user