In SolidJS, How Do I Do a Fallback Route? - solid-js

I'm new to Solid JS, coming mainly from a React background. I'm using Solid and Solid-App-Router right now for the first time.
I'm trying to design routes with a fallback, meaning if an endpoint is put into the URL that does not exist, it will redirect to a default location. My problem is this fallback is executing no matter what, overriding all my other routes.
I'll add that namedLazy works great and is simply a wrapper to support named exports with SolidJS lazy(). Here is my routing code:
import { namedLazy } from '../../utils/solidWrappers';
import { Routes, Route } from 'solid-app-router';
import { isAuthenticated } from '../../resources/AuthResources';
const Welcome = namedLazy(() => import('./Welcome'), 'Welcome');
const Categories = namedLazy(() => import('./Categories'), 'Categories');
const Redirect = namedLazy(() => import('../UI/Redirect'), 'Redirect');
export const AppRoutes = () => {
return (
<Routes>
<Route path="/welcome" element={<Welcome />} />
{isAuthenticated() && (
<Route path="/categories" element={<Categories />} />
)}
<Route path="*" element={<Redirect />} />
</Routes>
);
};
And here is my Redirect component:
import { useNavigate } from 'solid-app-router';
export const Redirect = () => {
const navigate = useNavigate();
navigate('/welcome');
return <></>;
};
This kind of fallback route design works in react-router, however it's not working for me with solid-app-router. This is not the only route design, I also tried the configuration/array based route design as well and had the same problem. I'm open to suggestions for how to properly implement this functionality.

As the other commenter said, my code for the fallback route actually does work. This was a reactivity issue.
When the page first loads, isAuthenticated() returns false, because the authentication check is an ajax call that hadn't run yet. Therefore the /categories route wouldn't be rendered, and if I was trying to manually navigate to /categories I would instead by redirected. This made it appear as though the catch-all route was overriding everything, when in fact it was behaving as expected.
I added another check to prevent the routes from rendering until after the authentication check ajax call was made, and then everything worked perfectly.

Related

Mocking authentication when testing MSAL React Apps

Our app is wrapped in the MSAL Authentication Template from #azure/msal-react in a standard way - key code segments are summarized below.
We would like to test app's individual components using react testing library (or something similar). Of course, when a React component such as SampleComponentUnderTest is to be properly rendered by a test as is shown in the simple test below, it must be wrapped in an MSAL component as well.
Is there a proper way to mock the MSAL authentication process for such purposes? Anyway to wrap a component under test in MSAL and directly provide test user's credentials to this component under test? Any references to useful documentation, blog posts, video, etc. to point us in the right direction would be greatly appreciated.
A Simple test
test('first test', () => {
const { getByText } = render(<SampleComponentUnderTest />);
const someText = getByText('A line of text');
expect(someText).toBeInTheDocument();
});
Config
export const msalConfig: Configuration = {
auth: {
clientId: `${process.env.REACT_APP_CLIENT_ID}`,
authority: `https://login.microsoftonline.com/${process.env.REACT_APP_TENANT_ID}`,
redirectUri:
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/'
: process.env.REACT_APP_DEPLOY_URL,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
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:
console.error(message);
}
},
},
},
};
Main app component
const msalInstance = new PublicClientApplication(msalConfig);
<MsalProvider instance={msalInstance}>
{!isAuthenticated && <UnauthenticatedHomePage />}
{isAuthenticated && <Protected />}
</MsalProvider>
Unauthenticated component
const signInClickHandler = (instance: IPublicClientApplication) => {
instance.loginRedirect(loginRequest).catch((e) => {
console.log(e);
});
};
<UnauthenticatedTemplate>
<Button onClick={() => signInClickHandler(instance)}>Sign in</Button>
</UnauthenticatedTemplate>
Protected component
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
errorComponent={ErrorComponent}
loadingComponent={LoadingComponent}
>
<SampleComponentUnderTest />
</MsalAuthenticationTemplate>
I had the same issue as you regarding component's test under msal-react.
It took me a couple of days to figure out how to implement a correct auth mock.
That's why I've created a package you will find here, that encapsulates all the boilerplate code : https://github.com/Mimetis/msal-react-tester
Basically, you can do multiple scenaris (user is already logged, user is not logged, user must log in etc ...) in a couple of lines, without having to configure anything and of course without having to reach Azure AD in any cases:
describe('Home page', () => {
let msalTester: MsalReactTester;
beforeEach(() => {
// new instance of msal tester for each test
msalTester = new MsalReactTester();
// spy all required msal things
msalTester.spyMsal();
});
afterEach(() => {
msalTester.resetSpyMsal();
});
test('Home page render correctly when user is logged in', async () => {
msalTester.isLogged();
render(
<MsalProvider instance={msalTester.client}>
<MemoryRouter>
<Layout>
<HomePage />
</Layout>
</MemoryRouter>
</MsalProvider>,
);
await msalTester.waitForRedirect();
let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
expect(allLoggedInButtons).toHaveLength(2);
});
test('Home page render correctly when user logs in using redirect', async () => {
msalTester.isNotLogged();
render(
<MsalProvider instance={msalTester.client}>
<MemoryRouter>
<Layout>
<HomePage />
</Layout>
</MemoryRouter>
</MsalProvider>,
);
await msalTester.waitForRedirect();
let signin = screen.getByRole('button', { name: 'Sign In - Redirect' });
userEvent.click(signin);
await msalTester.waitForLogin();
let allLoggedInButtons = await screen.findAllByRole('button', { name: `${msalTester.activeAccount.name}` });
expect(allLoggedInButtons).toHaveLength(2);
});
I am also curious about this, but from a slightly different perspective. I am trying to avoid littering the code base with components directly from msal in case we want to swap out identity providers at some point. The primary way to do this is to use a hook as an abstraction layer such as exposing isAuthenticated through that hook rather than the msal component library itself.
The useAuth hook would use the MSAL package directly. For the wrapper component however, I think we have to just create a separate component that either returns the MsalProvider OR a mocked auth provider of your choice. Since MsalProvider uses useContext beneath the hood I don't think you need to wrap it in another context provider.
Hope these ideas help while you are thinking through ways to do this. Know this isn't a direct answer to your question.

NextAuth: authorize all APP with Layout wrapping

I'm trying to use NextAuth for authorize access to my app and I have two main issue.
1 - all the app is wrapped with a Layout in _app.js
<SessionProvider session={pageProps.session}>
<RecoilRoot>
<Layout>
<Component {...pageProps} />
</Layout>
</RecoilRoot>
</SessionProvider>
I'm using the email Provider and if I use the built in signin page everything works great but if I add a custom signin page under pages/signin.js I'm not able to proceed. My undeerstanding is that with custom signin page everything is wrapped with the layout. The layout has a useeffect with a call to an api to get items to show in the menu
import Navbar from "./navbar";
import Sidebar from "./sidebar";
import Main from "./main";
import { useEffect, useState } from "react";
const Layout = ({ children }) => {
const [menu, setMenu] = useState();
useEffect(async () => {
const data = await fetch("api/menu/menu", {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const menuItems = await data.json();
setMenu(menuItems.data);
}, []);
return (
<>
{menu && <Navbar menu={menu.profile} />}
{menu && <Sidebar menu={menu.sidebar} />}
<Main children={children}></Main>
</>
);
};
export default Layout;
In _app.js I'm not able to retrieve the session to conditonally render the page.
Important to note that I'm using _middleware.js to protect all the app and define the custome signin page like so
import withAuth from "next-auth/middleware";
export default withAuth({
pages: {
signIn: "/auth/customSignin",
},
});
Does anyone is able to pint me in the right direction?
2 issue- I'd like top verify if user exists in the database before sending the email. Am I right I can handle it in the signin callback? If yes can I add a second filed like poassword so I can both verify user exists in db, verify with password and then send email?
Many thanks for any help
I solved the first issue using useRouter and checking the pathname for the customSignin page.
if (router.pathname === "/auth/customSignin") {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
return (
<SessionProvider session={pageProps.session}>
<RecoilRoot>
<Layout>
<Component {...pageProps} />
</Layout>
</RecoilRoot>
</SessionProvider>
);
}
export default MyApp;
Now I'm dealing with the second one. Anybody can help on that.
Basically I need to add a second field to the built in Email Provider and check that field against the database

Experiencing redirect loop with a protected route in Gatsby using Auth0 [duplicate]

This question already has an answer here:
Login doesn't show up in Gatsby using Auth0, withAuthenticationRequired
(1 answer)
Closed 1 year ago.
Note: This question is not a duplicate, I'm not sure why anyone is thinking that...
I’m having issue with implementing protected pages(routes) in Gatsby with Auth0
Currently, when I point the browser to localhost:8000/user/protectedpage, it goes to the login screen, and after a successful login, it comes back to that route, and the browser seems to be stuck on a loop loading between two routes.
When I tested with this, the page was doing a indefinite redirect loop while showing "Redirect..." on the page:
export default withAuthenticationRequired(ProtectedPage, {
onRedirecting: () => <div>Redirecting...</div>
});
redirectUri in Auth0Provider is set to redirectUri={window.location.origin + '/user'}
Allowed Callback URLs in the auth0 admin page, is set to, http://localhost:8000/user
If I change these routes to window.location.origin and http://localhost:8000/, then after a successful login, it’ll redirect to that page and stay there.
I need it to redirect to where it was trying to go to instead.
As in, if I navigate to localhost:8000/user/protectedpage, then after logging in, it should redirect to that route and load that page successfully, instead of being stuck in a loop like mentioned earlier.
Here are some codes:
// File structure
src
> pages
> user
> index.js
> protectedpage
index.js
gatsby-browser.js
// gatsby-browser.js
import React from 'react';
import { Auth0Provider } from '#auth0/auth0-react';
import { navigate } from 'gatsby';
const onRedirectCallback = (appState) => {
navigate(appState?.returnTo || '/', { replace: true });
};
export const wrapRootElement = ({ element }) => {
return (
<Auth0Provider
domain={process.env.AUTH0_DOMAIN}
clientId={process.env.AUTH0_CLIENTID}
redirectUri={window.location.origin + '/user'}
onRedirectCallback={onRedirectCallback}
>
{element}
</Auth0Provider>
);
};
// protectedpage.js
import React from 'react';
import { withAuthenticationRequired } from '#auth0/auth0-react';
const ProtectedPage = () => {
return (
<div>
Protected Page
</div>
);
};
export default withAuthenticationRequired(ProtectedPage);
// auth0 Application URIs
Allowed Callback URLs
http://localhost:8000/user
I'm not sure about your full implementation but to me, the fact that it gets stuck in an infinite loop could be related to the fact that you are replacing the history by removing the last visited page in:
navigate(appState?.returnTo || '/', { replace: true });
In addition, the callback is receiving an appState otherwise, it makes the history replacing but you are never providing it at (onRedirectCallback={onRedirectCallback}):
// gatsby-browser.js
import React from 'react';
import { Auth0Provider } from '#auth0/auth0-react';
import { navigate } from 'gatsby';
const onRedirectCallback = (appState) => {
navigate(appState?.returnTo || '/', { replace: true });
};
export const wrapRootElement = ({ element }) => {
return (
<Auth0Provider
domain={process.env.AUTH0_DOMAIN}
clientId={process.env.AUTH0_CLIENTID}
redirectUri={window.location.origin + '/user'}
onRedirectCallback={onRedirectCallback} //<-- here you are not providing an appState
>
{element}
</Auth0Provider>
);
};

StatusBar does not have web implementation

I'm trying to render a component using react-testing-library in an Ionic React based project. There appears to be an issue with StatusBar. The error says StatusBar does not have web implementation.
My code looks something like this:
let component
beforeEach(() => {
component = render(
<ThemeProvider>
<IonReactRouter>
<IonRouterOutlet>
<Login />
</IonRouterOutlet>
</IonReactRouter>
</ThemeProvider>
)
})
describe('snapshot', () => {
it('should match snapshot', () => {
const { asFragment } = component
expect(asFragment()).toMatchSnapshot()
})
})
That's no error, that's the Capacitor Plugin not having the Web Implementation, you could just ignore that or catch it everywhere with .catch(()=>{});
Have you installed #capacitor/status-bar in /src-capacitor? (yarn add #capacitor/status-bar or npm install ....)

SSO on Microsoft Edge not working but does work on IE11, Chrome, Safari, Firefox - via Office.js add-in

The following oAuth2 SSO code in my Office.js addin application works great in IE11, Chrome, Safari and Firefox, but doesn't work in Microsoft Edge. I can see the bearer token is being returned to the pop-up dialog via the url:
https://localhost:3000/login?access_token=ya29.ImG6By-0ZWPQB4MsYxxxxxxxxxxxxxxxxxxxxxxxxxxxxE5XsM9v7SBi-OaUBBQucO05luKVP0pYoSrcYzbaUKAAX&token_type=Bearer
I can also see that the asyncResult.status == succeeded, i.e.
[object Object]: {status: "succeeded", value: Object}
status: "succeeded"
value: Object
addEventHandler: function (){var d=OSF.DDA.SyncMethodCalls[OSF.DDA.SyncMethodNames.AddMessageHandler.id],c=d.verifyAndExtractCall(arguments,a,b),e=c[Microsoft.Office.WebExtension.Parameters.EventType],f=c[Microsoft.Office.WebExtension.Parameters.Handler];return b.addEventHandlerAndFireQueuedEvent(e,f)}
arguments: null
caller: null
length: 0
name: "value"
prototype: Object
proto: function() { [native code] }
close: function (){var c=OSF._OfficeAppFactory.getHostFacade()[OSF.DDA.DispIdHost.Methods.CloseDialog];c(arguments,g,b,a)}
sendMessage: function (){var c=OSF._OfficeAppFactory.getHostFacade()[OSF.DDA.DispIdHost.Methods.SendMessage];return c(arguments,b,a)}
proto: Object
proto: Object
However, the "console.log('hello');" doesn't get called when Microsoft Edge is running the sidebar/add-in.
The pop-up dialog is showing this in the F12 debug console:
HTTP403: FORBIDDEN - The server understood the request, but is refusing to fulfill it.
(XHR)POST - https://browser.pipe.aria.microsoft.com/Collector/3.0/?qsp=true&content-type=application%2Fbond-compact-binary&client-id=NO_AUTH&sdk-version=AWT-Web-JS-1.1.1&x-apikey=a387cfcf60114a43a7699f9fbb49289e-9bceb9fe-1c06-460f-96c5-6a0b247358bc-7238&client-time-epoch-millis=1579626709267&time-delta-to-apply-millis=961
Any ideas?
export function loginUsingOAuth() {
try {
const sealUrl = getFromStorage('seal_url', STORAGE_TYPE.LOCAL_STORAGE);
const redirectUrl = `${window.location.protocol}//${window.location.host}/login`;
let displayInIframe = false;
let promptBeforeOpen = false;
if (typeof sealUrl !== 'undefined' && sealUrl) {
const oAuthUrl = `${sealUrl}/seal-ws/oauth2/login?redirect_uri=${redirectUrl}`;
Office.context.ui.displayDialogAsync(
oAuthUrl,
{
height: 80,
width: 80,
displayInIframe,
promptBeforeOpen
},
asyncResult => {
console.log('asyncResult');
console.log(asyncResult);
addLog(LOG_TYPE.INFO, 'authentication.loginUsingOAuth', asyncResult);
if (asyncResult.status !== 'failed') {
const dialog = asyncResult.value;
dialog.addEventHandler(Office.EventType.DialogMessageReceived, args => {
console.log('hello');
Maybe this is actually a routing issue when executing in Edge? The "/login" callback is routed to the AuthCallback.js component:
const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact path="/login" component={AuthCallback} />
<Route path="/" component={BaseLayout} />
</Switch>
</BrowserRouter>
);
The constructor of the AuthCallback.js component calls messageParent after a short pause:
constructor(props) {
super(props);
const paramsObj = queryString.parse(props.location.search);
const paramsStr = JSON.stringify(paramsObj);
setTimeout(() => {
Office.context.ui.messageParent(paramsStr);
}, 1200);
}
I'm starting to wonder if Edge is messing with the redirect. In the image below you can see that IE and Edge are returning different status codes for the same sign-on operation:
There seems to be two problems with the Edge browser.
The redirect/callback is not calling the components constructor when displayInIframe=false when running on Microsoft Edge. All other browsers work as expected. I've added conditional logic to set displayInIframe=true for the Edge browser use-case
The messageParent method also does not work for the Edge browser when displayInIframe=true. Therefore I've had to extract the auth token in the pop-up dialog callback and stash it away in the local_storage. The parent (the sidebar) is then polling the local_storage to detect that the sign-in has completed. Again, Chrome, Firefox, Safari, IE11 (both Mac and PC) are all fine - its just the Edge browser that is failing.
Whilst this is an ugly solution to the problem it is also imperfect because IF the end-user is not already signed-in to SSO then the Google [Account Selector] dialog is shown, which is a problem when displayInIframe=true as this throws an iframe exception.
I don't see any other option open to us, because the O/S build number and MSWord version dictates which browser is used to render the sidebar. The inability to choose whether IE11 or Edge is used would be bearable if Edge didn't have these functional deficits.