Guidance on implementing Okta Redirect with Docusaurus - okta

Having trobule implementing okta redirect with docusaurus using their documentation due to how docusaurus intiallly loads in routes. Can anyone provide any guidance on how to go about this?
https://github.com/okta/okta-react
Expected Behavior:
Initial path to load up redirects to okta and authenticates then returns back to webpage.

I ran into this same issue and saw your posting hoping for an answer. bummer. Then I dug around a little more. It's not fully implemented yet because I'm waiting on the okta creds from my administrators, but this got me to a permission denied screen (which is a good thing to me!)
Few things:
docusaurus currently uses react-router-dom v5. you need to specifically set that instead of defaulting to v6
src/pages/index.tsx (i'm using typescript) should allow you to setup a browserrouter there
react-router-dom package:
"react-router": "^5.3.3",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.3.3"
src/pages/index.tsx - I updated the home component to have a DocusaurusHome component, then made Home hold the routing logic
import React from 'react';
import clsx from 'clsx';
import Link from '#docusaurus/Link';
import useDocusaurusContext from '#docusaurus/useDocusaurusContext';
import Layout from '#theme/Layout';
import HomepageFeatures from '#site/src/components/HomepageFeatures';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import styles from './index.module.css';
import { OktaAuth } from '#okta/okta-auth-js';
import { Security, LoginCallback } from '#okta/okta-react';
import { RequiredAuth } from '../components/RequiredAuth';
// file with client id
import clientId from '../Okta/OktaClientID';
// file with issuer url
import issuerUrl from '../Okta/OktaIssuerUrl';
const config = {
clientId: clientId,
issuer: issuerUrl,
redirectUri: `${location.protocol}//${location.host}/callback`,
scopes: ['openid', 'profile', 'email'],
pkce: true
};
const oktaAuth = new OktaAuth(config);
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro">
Docusaurus Tutorial - 5min ⏱️
</Link>
</div>
</div>
</header>
);
}
/**
* Actual Docusaurus Home component
*/
function DocusaurusHome(): JSX.Element {
const {siteConfig} = useDocusaurusContext();
return (<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>)
}
/**
* component for react-router-dom browserrouter and okta auth
*/
export default function Home(): JSX.Element {
const restore = async (_oktaAuth: OktaAuth, originalUri: string) => {
window.location.replace(originalUri);
};
return (
<BrowserRouter>
<Security oktaAuth={oktaAuth} restoreOriginalUri={restore}>
<Switch>
<Route path='/callback'>
<LoginCallback
errorComponent={(err) => {
// eslint-disable-next-line no-console
console.error(err);
setTimeout(() => {
localStorage.removeItem('okta-token-storage');
window.location.replace(`${location.protocol}//${location.host}/`);
}, 2000);
return null;
}}/>
</Route>
<Route path='/'>
<RequiredAuth />
</Route>
<Route path='*'>
<DocusaurusHome />
</Route>
</Switch>
</Security>
</BrowserRouter>);
}
The RequiredAuth component originally returned an react-router-dom Outlet, which doesn't exist in v5. I think that a Route should suffice in it's place
import React, { useEffect } from 'react';
import { useOktaAuth } from '#okta/okta-react';
import { toRelativeUrl } from '#okta/okta-auth-js';
import { Route } from 'react-router-dom';
export const RequiredAuth: React.FC = () => {
const { oktaAuth, authState } = useOktaAuth();
useEffect(() => {
if (!authState) {
return;
}
if (!authState?.isAuthenticated) {
const originalUri = toRelativeUrl(window.location.href, window.location.origin);
oktaAuth.setOriginalUri(originalUri);
oktaAuth.signInWithRedirect();
}
}, [oktaAuth, !!authState, authState?.isAuthenticated]);
if (!authState || !authState?.isAuthenticated) {
return <></>; // loading screen before okta login
}
return (<Route />);
};

Related

Redux-persist doesn't persist the store when I'm on a different route

I'm trying to implement redux-persist with reduxjs/toolkit. I made it possible to persist the store on main page. However, when I route to MemeForm.jsx different paths in my app where I make another api call to fetch data from the server (which changes the redux-store since this api call adds another slice to the store) and refresh the page it gets crashed. (Edit: I get GET https://memegeneratorv2.netlify.app/217743513/fetch 404 error on netlify when I refresh the page.) It works perfectly in my local computer, though. I guess I couldn't implement redux-persist with reduxjs/toolkit correctly. I can't figure out this problem for a week. A little bit of help would be perfect.
Here is my github repo https://github.com/ahmettulutas/MemeGeneratorV2 and here is the netlify version of the app. https://memegeneratorv2.netlify.app
store.js
import { configureStore } from '#reduxjs/toolkit'
import loadMemesSlice from "./features/loadMemes/loadMemesSlice";
import fetchedMemeSlice from "./features/editMeme/memeFormSlice";
import { combineReducers } from '#reduxjs/toolkit';
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = { // configuration object for redux-persist
key: 'root',
storage, // define which storage to use
whitelist : ['fetchedMemeSlice','loadMemesSlice'],
}
const rootReducer = combineReducers({
loadMemesSlice: loadMemesSlice,
fetchedMemeSlice: fetchedMemeSlice,
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, PAUSE, PURGE, REHYDRATE, REGISTER, PERSIST],
},
}),
})
export const persistor = persistStore(store);
export default store;
app.js
import "./styles.css";
import Header from "./components/header";
import MemeComponent from "./features/editMeme/memeComponent";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AllMemes from "./features/loadMemes/displayMemes";
import FetchedMeme from "./components/fetchedMeme";
import Footer from "./components/footer";
export default function App() {
return (
<Router>
<Header />
<div className="routes-section">
<Routes >
<Route path="/" exact element={<AllMemes/>}/>
<Route path="/:id" element={<MemeComponent />}></Route>
<Route path="/:id/:fetchedmeme" element={<FetchedMeme />}></Route>
</Routes>
</div>
<Footer />
</Router>
);
}
index.js
import { Provider } from "react-redux";
import ReactDOM from "react-dom";
import store from "./store";
import {persistor} from "./store";
import App from "./App";
import { PersistGate } from 'redux-persist/integration/react';
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
rootElement
)
Did you wrap the App Component to PersistGate Component in index.js?
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
Please check if you added the reducer in the whitelist correctly.
whitelist : ['fetchedMemeSlice','loadMemesSlice']
I have just found the answer. Since netlify has it's own rules you need to handle some other configs when you use react-router-dom.
Check this link and do the required settings. https://github.com/vuejs/vuepress/issues/457#issuecomment-390206649
After that; add a netlify.toml file, which sets the redirect root when you refresh the page, to your project's root directory. You can directly copy and paste mine from my github repository https://github.com/ahmettulutas/MemeGeneratorV2 Finally, push your changes if you are using CI option from your github repo. Voila.

Apollo GraphQL failing connection

My root component is already wrapped with an ApolloProvider tag, but the error message tells me it is not.
Error Message
Invariant Violation: Could not find "client" in the context or passed in as an option. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via options.
This error is located at:
in App (created by ExpoRoot)
Problem is my root component is already wrapped with an ApolloProvider tag
React Native Code
IMPORT statements
import {
ApolloClient,
InMemoryCache,
useQuery,
ApolloProvider,
gql,
} from "#apollo/client";
CONNECTION WITH GraphQL
const client = new ApolloClient({
uri: "https://www.outvite.me/gql/gql",
cache: new InMemoryCache(),
defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network' } },
})
TEST QUERY
const USER_QUERY = gql`
query USER {
users {
nodes {
edge {
username
}
}
}
}
`
DEFAULT APP
THIS IS WHERE THE ERROR IS BEING THROWN
const { data, loading } = useQuery(USER_QUERY) is the line that traceback shows
export default function App() {
const { data, loading } = useQuery(USER_QUERY)
return (
<ApolloProvider client={client}>
<View>
<Text style={styles.text}>Open</Text>
<Text style={styles.text}>Another text</Text>
</View>
<Button title="Toggle Sidebar" onPress={() => toggleSidebarView()} />
<Button title="Change theme" onPress={() => toggleColorTheme()} />
</ApolloProvider>
);
}
If I'm not mistaken, the useQuery hook only works if you're in a component that is already wrapped in the ApolloProvider so you probably want to do something like this
export default function MainApp() {
const { data, loading } = useQuery(USER_QUERY)
return (
<View>
... use 'data' in here somewhere...
</View>
);
}
and then the top-level App component would look like
export default function App() {
return (
<ApolloProvider client={client}>
<MainApp />
</ApolloProvider>
);
}

ComponentWillMount gets called twice and render gets called twice. Also, render is being called before reducers finish. React and Redux

This is my console:
action: {type: "##redux/PROBE_UNKNOWN_ACTION_u.0.n.a.j.f"}
action: {type: "##redux/INIT2.4.j.c.2.m"}
in component will mount
inside the hangout_list render method
in component will mount
inside the hangout_list render method
Uncaught TypeError: Cannot read property 'map' of undefined
at HangoutList.render (bundle.js:22963)
at finishClassComponent (bundle.js:11048)
at updateClassComponent (bundle.js:11016)
at beginWork (bundle.js:11641)
at performUnitOfWork (bundle.js:14473)
at workLoop (bundle.js:14502)
at HTMLUnknownElement.callCallback (bundle.js:2759)
at Object.invokeGuardedCallbackDev (bundle.js:2797)
at invokeGuardedCallback (bundle.js:2846)
at replayUnitOfWork (bundle.js:13977)
...
bundle.js:12302 The above error occurred in the <HangoutList> component:
in HangoutList (created by Connect(HangoutList))
in Connect(HangoutList) (created by App)
in div (created by App)
in App
in Provider
...
action: {type: "FETCH_HANGOUTS", payload: {…}}
inside fetch hangouts in the reducer
action: {type: "FETCH_HANGOUTS", payload: {…}}
inside fetch hangouts in the reducer
As you can see, some console.logs are called twice and we have an undefined error which suggests some state data hasn't been set.
I have a react-redux app on localhost:8080 that uses ReduxPromise and is making an api call to localhost:3000 which succeeds... there data returns. It just never sets in time before the component tries to render. What can I do?
My code:
my main index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import ReduxPromise from 'redux-promise'
import App from './components/app';
import reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<App />
</Provider>
, document.querySelector('.container'));
My action:
import axios from 'axios'
export const ROOT_URL = 'http://localhost:3000';
export const FETCH_HANGOUTS = 'FETCH_HANGOUTS';
export function fetchHangouts() {
const path = 'api/v1/hangouts'
const url = `${ROOT_URL}/${path}`;
const request = axios.get(url);
return {
type: FETCH_HANGOUTS,
payload: request
};
}
my App component:
import React, { Component } from 'react';
import HangoutList from '../containers/hangout_list'
export default class App extends Component {
render() {
return (
<div>
<HangoutList />
</div>
);
}
}
HangoutList container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchHangouts } from '../actions/index';
class HangoutList extends Component {
renderHangouts(hangoutData) {
const type = hangoutData.type;
const additional_info = hangoutData.additional_info;
const hangoutKey = hangoutData.id;
return (
<tr key={hangoutKey}>
<td> {type} </td>
<td> {additional_info} </td>
</tr>
)
}
componentWillMount() {
console.log("in component will mount");
this.props.fetchHangouts();
}
render() {
console.log("inside the hangout_list render method");
return (
<table className="table table-hover">
<thead>
<tr>
<th>Type</th>
<th>Details </th>
</tr>
</thead>
<tbody>
{this.props.hangouts.map(this.renderHangouts)}
</tbody>
</table>
)
}
}
function mapStateToProps({ hangouts }) { // es6 shorthand. It's the same as if state was the argument and { hangouts: state.hangouts } was in the return section.
return { hangouts };
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchHangouts }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(HangoutList);
And finally my reducer:
import { FETCH_HANGOUTS } from "../actions/index";
export default function(state = [], action) {
console.log("action:", action);
switch (action.type) {
case FETCH_HANGOUTS:
// return state.concat([ action.payload.data ]); // don't use push. concat creates a new array, while push mutates the old one. YOu want to create a new array, not mutate the old one.
console.log("inside fetch hangouts in the reducer")
return action.payload.data
}
return state;
}
Anyone see what the issue is? I basically don't know why certain console.logs are running twice and why my api call (called in ComponentWillMount) won't finish before the container renders. I thought ReduxPromise was middleware that was supposed to handle this problem?

AOR - UnitTest a simple List

I am using AOR v1.4.0 and trying to write a unit test to test the rendering of a simple list with one row. But nothing gets logged on console as HTML
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as Renderer from 'react-test-renderer';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import {render, shallow, mount} from 'enzyme';
import {Datagrid, List, Admin, Resource} from 'admin-on-rest';
import {CategoryList} from '../Categories';
describe('Categories', ()=>{
it('renders category list correctly', ()=>{
const wrapper = mount(
<Admin title="Mock Admin Client" restClient={ jest.fn().mockImplementation(()=>{
return new Promise((res, rej)=>{
res({
total : 1,
data: ()=>{
return {
id: "0300b4cf-4888-4e73-b4e1-25cf4686e05c",
name: "cat2",
displaySequence: 121
}
}
});
});
})}>
<Resource options={{ label: 'Categories' }} name="category" list={CategoryList}/>
</Admin>
);
console.log(wrapper.html());//<-- does not log anything
});
});
The original component
export const CategoryList = (props: any) => (
<List {...props} perPage={50}>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
<TextField source="displaySequence" />
<EditButton/>
<ShowButton/>
</Datagrid>
</List>
);
Can some one please modify & suggest on how to mock the restClient using JEST ? I guess that is the place I am going wrong.
Also, is there a better way to test the list in isolation ?
As your restClient is async, you have to wait for the next tick to get something in return, see https://stackoverflow.com/a/43855794/1333479

admin-on-rest custom routes redirecting to dashboard

I am trying to add a custom route to AOR that acts as a landing page for an email link. When I navigate to http://localhost:3000/random_page AOR changes the url to http://localhost:3000/random_page#/ and renders the Dashboard component instead of my RandomPage. I am probably missing something simple here but this is a barebones custom route example. Can anyone see what I am doing wrong?
import React from 'react'
import { Route } from 'react-router-dom'
import RandomPage from './RandomPage'
export default [
<Route exact path="/random_page" component={RandomPage} noLayout/>,
]
import React, { Component } from 'react';
import './App.css';
import { jsonServerRestClient, fetchUtils, Admin, Resource, Delete } from 'admin-on-rest';
import { LogList } from './components/LogList';
import { UserLogs } from './components/UserLogs';
import Dashboard from './components/Dashboard'
import authClient from './authClient'
import customRoutes from './customRoutes'
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
options.headers.set('X-AUTH-TOKEN', localStorage.getItem('token'));
return fetchUtils.fetchJson(url, options);
}
class App extends Component {
render() {
return (
<Admin
authClient={authClient}
title = "Logs"
restClient={jsonServerRestClient('http://localhost:3001/admin', httpClient)}
customRoutes={customRoutes}
dashboard={Dashboard}
>
<Resource name="users" list={UserList} show={UserLogs}/>
<Resource name="logs" list={LogList} remove={Delete} />
</Admin>
);
}
}
export default App;
I believe that you're looking for catchAll found under the <Admin> component in the docs. You can then parse out the information you need from the URL and handle accordingly.

Resources