I trying to implement the AWS Amplify Authenticator in Ant Design Pro / UmiJS
In the UmiJS config.ts file I have this configuration:
routes: [
(...){
path: '/',
component: '../layouts/AwsSecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
path: '/links',
name: 'fb.links',
icon: 'BarsOutlined',
component: './Links',
},(...)
The base component AwsSecurityLayout wraps the routes who will the guard.
Looks like that:
import React from 'react';
import { withAuthenticator } from '#aws-amplify/ui-react';
import { PageLoading } from '#ant-design/pro-layout';
class AwsSecurityLayout extends React.Component {
render() {
const { children } = this.props;
if (!children) {
return <PageLoading />;
}
return children;
}
}
export default withAuthenticator(AwsSecurityLayout);
Went I use the withAuthenticator func the props from UmiJs are not passed to the component, so props and props.child are all ways undefied.
The original file from Ant Design Pro:
import React from 'react';
import { PageLoading } from '#ant-design/pro-layout';
import { Redirect, connect, ConnectProps } from 'umi';
import { stringify } from 'querystring';
import { ConnectState } from '#/models/connect';
import { CurrentUser } from '#/models/user';
interface SecurityLayoutProps extends ConnectProps {
loading?: boolean;
currentUser?: CurrentUser;
}
interface SecurityLayoutState {
isReady: boolean;
}
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
state: SecurityLayoutState = {
isReady: false,
};
componentDidMount() {
this.setState({
isReady: true,
});
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
}
render() {
const { isReady } = this.state;
const { children, loading, currentUser } = this.props;
// You can replace it to your authentication rule (such as check token exists)
// 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
const isLogin = currentUser && currentUser.userid;
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
export default connect(({ user, loading }: ConnectState) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);
I Just basically wrap the component using withAuthenticator
I solved using the conditional rendering doc: https://docs.amplify.aws/ui/auth/authenticator/q/framework/react#manage-auth-state-and-conditional-app-rendering
code:
import React from 'react';
import { AmplifyAuthenticator } from '#aws-amplify/ui-react';
import { AuthState, onAuthUIStateChange } from '#aws-amplify/ui-components';
const AwsSecurityLayout: React.FunctionComponent = (props: any | undefined) => {
const [authState, setAuthState] = React.useState<AuthState>();
const [user, setUser] = React.useState<any | undefined>();
React.useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData);
});
}, []);
return authState === AuthState.SignedIn && user ? (
props.children
) : (
// TODO: change style / implement https://github.com/mzohaibqc/antd-amplify-react
<AmplifyAuthenticator />
);
}
export default AwsSecurityLayout;
Related
I have a React context which I am using to manage the authentication within my application. I have done this previously and all seemed OK, but in this application the value of the isAuthenticated property is not being updated. I've tried to replicate using CodeSanbox but I get the expected result.
Essentially, I want the context to hold a value of isAuthenticating: true until the authentication flow has finished, once this has finished I will determine if the user is authenticated by checking isAuthenticated === true && authenticatedUser !== undefined however, the state does not seem to be getting updated.
As a bit of additional context to this, I am using turborepo and next.js.
AuthenticationContext:
import { SilentRequest } from '#azure/msal-browser';
import { useMsal } from '#azure/msal-react';
import { User } from 'models';
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { msal, sendRequest } from 'utils';
interface AuthenticationContextType {
authenticatedUser?: User;
isAuthenticating: boolean;
}
const AuthenticationContext = createContext<AuthenticationContextType>({
authenticatedUser: undefined,
isAuthenticating: true
});
export const AuthenticationProvider = (props: { children: React.ReactNode }) => {
const { accounts, instance } = useMsal();
const [user, setUser] = useState<User>();
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(true);
const [currentAccessToken, setCurrentAccessToken] = useState<string>();
const getUserFromToken = useCallback(async () => {
if (user) {
setIsAuthenticating(false);
return;
}
const userRequest = await sendRequest('me');
if (! userRequest.error && userRequest.data) {
setUser(userRequest.data as User);
}
}, [user]);
const getAccessToken = useCallback(async () => {
if (! currentAccessToken) {
const request: SilentRequest = {
...msal.getRedirectRequest(),
account: accounts[0]
}
const response = await instance.acquireTokenSilent(request);
setCurrentAccessToken(response.accessToken);
}
return getUserFromToken();
}, [accounts, currentAccessToken, getUserFromToken, instance]);
useEffect(() => {
async function initialiseAuthentication() {
await getAccessToken();
setIsAuthenticating(false);
}
initialiseAuthentication();
}, [getAccessToken]);
return (
<AuthenticationContext.Provider value={{ authenticatedUser: user, isAuthenticating }}>
{ props.children }
</AuthenticationContext.Provider>
)
}
export const useAuth = () => {
const context = useContext(AuthenticationContext);
if (context === undefined) {
throw new Error("useAuth was used outside of it's provider.")
}
return context;
}
AuthenticationLayout:
import { useEffect, useState } from 'react';
import { AuthenticationProvider, useAuth } from '../hooks/authentication';
import MsalLayout from './msal-layout';
const AuthenticationLayout = (props: { children: React.ReactNode }) => {
const { isAuthenticating, authenticatedUser } = useAuth();
const wasAuthenticationSuccessful = () => {
return ! isAuthenticating && authenticatedUser !== undefined;
}
const renderContent = () => {
if (! wasAuthenticationSuccessful()) {
return (
<p>You are not authorized to view this application.</p>
)
}
return props.children;
}
if (isAuthenticating) {
return (
<p>Authenticating...</p>
)
}
return (
<MsalLayout>
{ renderContent() }
</MsalLayout>
)
}
export default AuthenticationLayout;
MsalLayout:
import { InteractionType } from '#azure/msal-browser';
import {
AuthenticatedTemplate,
MsalAuthenticationTemplate,
MsalProvider,
} from "#azure/msal-react";
import { msalInstance, msal } from 'utils';
import { AuthenticationProvider } from '../hooks/authentication';
msal.initialize();
const MsalLayout = (props: { children: React.ReactNode }) => {
return (
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate interactionType={InteractionType.Redirect} authenticationRequest={msal.getRedirectRequest()}>
<AuthenticatedTemplate>
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
</AuthenticatedTemplate>
</MsalAuthenticationTemplate>
</MsalProvider>
)
}
export default MsalLayout;
Theoretically, once the authentication is finished I would expect the props.children to display.
I think that the problem is AuthenticationLayout is above the provider. You have consumed the provider in MsalLayout. Then AuthenticationLayout uses MsalLayout so the AuthenticationLayout component is above the provider in the component tree. Any component that consumes the context, needs to be a child of the provider for that context.
Therefore the context is stuck on the static default values.
Your capture of this scenario in useAuth where you throw an error is not warning you of this as when its outside the context -- context is not undefined, it is instead the default values which you pass to createContext. So your if guard isn't right.
There are some workarounds to checking if its available -- for example you could use undefined in the default context for isAuthenticating and authenticatedUser and then check that. Or you can change them to getters and set the default context version of this function such that it throws an error.
Trying to implement private ( authenticated ) routes in Nextjs using HOC and cookies but running into error below:
TypeError: Object(...) is not a function
at export default withPrivateRoute(Private);
I have checked elsewhere in the app that cookies are available and also sent with the request. They seem to be available server side.
The HOC at `/components/withPrivateRoute
import { withRouter } from 'next/router';
import { withCookies } from 'react-cookie';
const withPrivateRoute = (authComponent) => {
return class Private extends React.Component {
componentDidMount() {
console.log('PRIVATE ROUTE', this.props);
const { router, cookies } = this.props;
const intendedRoute = router.pathname;
const isAdmin = !!cookies.get('isAdmin');
const isAuthenticated = !!cookies.get('username');
if (!isAuthenticated) {
router.push({
pathname: '/login',
query: { from: intendedRoute },
});
}
if (
isAuthenticated &&
router.pathname.includes('admin') &&
!isAdmin
) {
router.push('/');
}
}
render() {
// eslint-disable-next-line react/jsx-props-no-spreading
return <authComponent {...this.props} />;
}
}
}
export default withCookies(withRouter(withPrivateRoute));
The private route example:
import withPrivateRoute from '../components/withPrivateRoute';
import getCategories from '../lib/getCategories';
const Private = (props) => {
console.log('props', props);
return <div>Private route </div>;
}
export default withPrivateRoute(Private);
export async function getStaticProps() {
let categories = await getCategories();
categories = categories.data.categories;
return {
props: {
categories,
},
};
}
I have since found a better way to handle private routes in Nextjs from this discussion:
Everything is handled inside getServerSideProps, no HOC required.
class Private extends React.Component{
render() {
console.log('props', this.props);
return <p>Private route</p>;
}
}
export default Private;
export async function getServerSideProps(context) {
const { req: { headers, url }, res } = context;
const cookies = {};
if (headers && headers.cookie) {
headers.cookie.split(';').forEach((cookie) => {
const parts = cookie.match(/(.*?)=(.*)$/);
cookies[parts[1].trim()] = (parts[2] || '').trim();
});
}
const isAuthenticated = !!cookies.username;
const isAdmin = !!cookies.isAdmin;
if (!isAuthenticated) {
res.setHeader('Location', `/login?from=${url}`);
res.statusCode = 307;
}
if (
isAuthenticated &&
url.includes('admin') &&
!isAdmin
) {
res.setHeader('Location', '/');
res.statusCode = 307;
}
return {
props: {
},
};
}
I'm trying to migrate our codebase to use react-navigation: 3.11 from v1 and having issues. I created a small gist here to run through the issue and provided codebase.
I am using react-navigation-redux-helpers with the new react-navigation to use the createReduxContainer function in other to maintain my previous redux setup.
I'm getting this error - Cannot read property 'routes' of undefined
https://gist.github.com/bwoodlt/773c62d4ba3dbe0cea150a9d956bec3f
// #flow
// root navigator
import React from 'react';
import { Platform } from 'react-native';
import { createStackNavigator, createAppContainer } from 'react-navigation';
import TabNavigation from './TabNavigation';
import FCLaunch from '../../Components/FCLaunch';
import { FCLAUNCH } from '../constants';
import { HeaderText } from '../Components';
import * as actionsOnboarding from '../../Screens/Onboarding/actions';
import StagingArea from '../StagingArea';
const RouteConfigs = {
StagingArea: {
screen: StagingArea,
defaultNavigationOptions: {
header: null
}
},
[FCLAUNCH]: {
screen: FCLaunch,
navigationOption: () => ({
header: null
})
}
};
const StackNavigatorConfig = {
initialRouteName: 'StagingArea',
mode: Platform.OS === 'ios' ? 'modal' : 'card'
};
export default createAppContainer(
createStackNavigator(RouteConfigs, StackNavigatorConfig)
);
// #flow
// AppWithNavigationState
import * as React from 'react';
import codePush from 'react-native-code-push';
import OneSignal from 'react-native-onesignal';
import { connect } from 'react-redux';
import DeviceInfo from 'react-native-device-info';
import PropTypes from 'prop-types';
import {
createReactNavigationReduxMiddleware,
createReduxContainer
} from 'react-navigation-redux-helpers';
import { createAppContainer } from 'react-navigation';
import { AppNavigation } from './Navigation';
import { Storage } from '../Utils';
import { NAME } from './constants';
import type { Dispatch } from '../Model';
type IAppWithNavigationProps = {
dispatch: Dispatch
};
const codePushOptions = {
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME
};
const middleware = createReactNavigationReduxMiddleware(state => state[NAME]);
class AppWithNavigationStateObject extends React.PureComponent<
{},
IAppWithNavigationProps
> {
async componentDidMount() {
codePush.notifyAppReady();
codePush.sync({
updateDialog: true,
installMode: codePush.InstallMode.IMMEDIATE
});
const deviceName = await Storage.get('deviceName');
if (!deviceName) {
await Storage.set('deviceName', DeviceInfo.getDeviceName());
}
}
componentWillMount() {
OneSignal.setLocationShared(true);
OneSignal.inFocusDisplaying(2);
}
render(): React.Node {
const { dispatch, [NAME]: state } = this.props;
console.log(this.props);
return <AppNavigation navigation={{ dispatch, state }} />;
}
}
AppWithNavigationStateObject.propTypes = {
dispatch: PropTypes.func.isRequired,
[NAME]: PropTypes.object.isRequired
};
const AppWithNavigationStateInfo = createReduxContainer(AppNavigation);
const AppWithNavigationState = connect(state => ({
[NAME]: state[NAME]
}))(codePush(codePushOptions)(AppWithNavigationStateInfo));
export { AppWithNavigationState, middleware };
// #flow
// navReducer
import { handleActions } from 'redux-actions';
import { NavigationActions, StackActions } from 'react-navigation';
import { REHYDRATE } from 'redux-persist/constants';
import { AppNavigation } from './Navigation';
import {
CHAT_MAIN,
CHAT_MESSAGE_AREA,
NEW_MESSAGE,
FCLAUNCH,
} from './constants';
const { getStateForAction } = AppNavigation.router;
const initialState = getStateForAction(NavigationActions.init);
const getCurrentRouteName = navState => {
if (Object.prototype.hasOwnProperty.call(navState, 'index')) {
console.log(navState);
return getCurrentRouteName(navState.routes[navState.index]);
}
return navState.routeName;
};
const generateNavigationAction = (state, routeName, params = {}) => {
console.log(routeName);
// Don't navigate to the same screen if is already there.
if (getCurrentRouteName(state) === routeName) {
return state;
}
const nextState = getStateForAction(
NavigationActions.navigate({
routeName,
params
}),
state
);
return nextState || state;
};
export default handleActions(
{
// Custom actions types
[CHAT_MAIN]: state => generateNavigationAction(state, CHAT_MAIN),
[FCLAUNCH]: state => generateNavigationAction(state, FCLAUNCH),
[CHAT_MESSAGE_AREA]: state =>
generateNavigationAction(state, CHAT_MESSAGE_AREA),
[NEW_MESSAGE]: state => generateNavigationAction(state, NEW_MESSAGE),
// overwritten action types from react-navigation
[NavigationActions.NAVIGATE]: (state, { routeName }) =>
generateNavigationAction(state, routeName),
[NavigationActions.BACK]: state => {
const nextState = getStateForAction(NavigationActions.back(), state);
return nextState || state;
},
[NavigationActions.INIT]: state => {
console.error(NavigationActions.INIT);
return state;
},
[NavigationActions.RESET]: (state, { routeName }) => {
console.error(NavigationActions.RESET);
StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName })]
});
return state;
},
[NavigationActions.SET_PARAMS]: (state, { key, params }) => {
const nextState = getStateForAction(
NavigationActions.setParams({
params,
key
}),
state
);
return nextState || state;
},
[NavigationActions.URI]: state => {
console.error(NavigationActions.URI);
return state;
},
// Default initialRouteName
[REHYDRATE]: (state, { payload: { auth } }) => {
const isLogged =
auth &&
auth.token &&
auth.token.length !== 0 &&
auth.refreshToken &&
auth.status &&
auth.refreshToken.length !== 0;
const nextState = isLogged
? state
: generateNavigationAction(state, FCLAUNCH);
return nextState;
}
},
initialState
);
```[enter image description here][2]
[1]: https://i.stack.imgur.com/j5dm8.png
So I fixed my issue:
The createReduxContainer helper from react-navigation-redux-helpers expects three key params: { dispatch, state, ...props }, I wasn't passing the dispatch and state directly but as part of the navigation object passed. So I fixed this by passing missing parameters.
I'm trying to test my LoginForm component using jest and react-testing-library. When the login form is submitted successfully, my handleLoginSuccess function is supposed to set the 'user' item on localStorage and navigate the user back to the home page using history.push(). This works in my browser in the dev environment, but when I render the component using Jest and mock out the API, localStorage gets updated but the navigation to '/' doesn't happen.
I've tried setting localStorage before calling history.push(). I'm not sure what is responsible for re-rendering in this case, and why it works in dev but not test.
Login.test.jsx
import 'babel-polyfill'
import React from 'react'
import {withRouter} from 'react-router'
import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '#testing-library/react'
import Login from '../../pages/Login'
import API from '../../util/api'
jest.mock('../../util/api')
function renderWithRouter(
ui,
{route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
) {
return {
...render(<Router history={history}>{ui}</Router>),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
}
}
describe('When a user submits the login button', () => {
test('it allows the user to login', async () => {
const fakeUserResponse = {'status': 200, 'data': { 'user': 'Leo' } }
API.mockImplementation(() => {
return {
post: () => {
return Promise.resolve(fakeUserResponse)
}
}
})
const route = '/arbitrary-route'
const {getByLabelText, getByText, findByText} = renderWithRouter(<Login />, {route})
fireEvent.change(getByLabelText(/email/i), {target: {value: 'email#gmail.com '}})
fireEvent.change(getByLabelText(/password/i), {target: {value: 'Foobar123'}})
fireEvent.click(getByText(/Log in/i))
const logout = await findByText(/Log Out/i)
expect(JSON.parse(window.localStorage.getItem('vector-user'))).toEqual(fakeUserResponse.data.user)
})
})
relevant parts of LoginForm.jsx
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disableActions: false,
formErrors: null,
};
}
handleLoginSuccess = () => {
const { loginSuccessCallback, redirectOnLogin, history } = { ...this.props };
if (loginSuccessCallback) {
loginSuccessCallback();
} else {
history.push('/');
}
}
loginUser = ({ user }) => {
localStorage.setItem('vector-user', JSON.stringify(user));
}
handleLoginResponse = (response) => {
if (response.status !== 200) {
this.handleResponseErrors(response.errors);
} else {
this.loginUser(response.data);
this.handleLoginSuccess();
}
}
handleLoginSubmit = (event) => {
event.preventDefault();
const {
disableActions, email, password
} = { ...this.state };
if (disableActions === true) {
return false;
}
const validator = new Validator();
if (!validator.validateForm(event.target)) {
this.handleResponseErrors(validator.errors);
return false;
}
this.setState(prevState => ({ ...prevState, disableActions: true }));
new API().post('login', { email, password }).then(this.handleLoginResponse);
return true;
}
}
Login.jsx
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoginForm from '../components/LoginForm';
class Login extends React.Component {
constructor({ location }) {
super();
const originalRequest = location.state && location.state.originalRequest;
this.state = {
originalRequest
};
}
render() {
const { originalRequest } = { ...this.state };
return (
<div>
<h1>Login</h1>
<LoginForm redirectOnLogin={originalRequest && originalRequest.pathname} />
<Link to="/forgot">Forgot your password?</Link>
</div>
);
}
}
Login.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
originalRequest: PropTypes.shape({
pathname: PropTypes.string
})
})
})
};
export default withRouter(Login);
Currently the await findByText() times out.
I think that's because in your tests you're not rendering any Route components. Without those react-router has no way to know what to render when the route changes. It will always render Login.
The problem is when I update state in Redux, React doesn't run the render function. I am a beginner in Redux so I am not getting what exactly should I be doing to solve this. I read about the #connect function but as I am using CreateReactApp CLI tool, I won't be able to provide support for Decorators without ejecting (Which I dont want to do).
Component:
import React from "react";
import Store from "../store";
Store.subscribe(() => {
console.log(Store.getState().Auth);
});
export default class Login extends React.Component {
login = () => {
Store.dispatch({ type: "AUTH_LOGIN" });
// this.forceUpdate(); If I forceUpdate the view, then it works fine
};
logout = () => {
Store.dispatch({ type: "AUTH_LOGOUT" });
// this.forceUpdate(); If I forceUpdate the view, then it works fine
};
render() {
if (Store.getState().Auth.isLoggedIn) {
return <button onClick={this.logout}>Logout</button>;
} else {
return <button onClick={this.login}>Login</button>;
}
}
}
Reducer:
export default AuthReducer = (
state = {
isLoggedIn: false
},
action
) => {
switch (action.type) {
case "AUTH_LOGIN": {
return { ...state, isLoggedIn: true };
}
case "AUTH_LOGOUT": {
return { ...state, isLoggedIn: false };
}
}
return state;
};
Can anyone please point me in the right direction? Thanks
You can make use of connect HOC instead of decorator, it would be implemented like
import { Provider, connect } from 'react-redux';
import Store from "../store";
class App extends React.Component {
render() {
<Provider store={store}>
{/* Your routes here */}
</Provider>
}
}
import React from "react";
//action creator
const authLogin = () => {
return { type: "AUTH_LOGIN" }
}
const authLogout = () => {
return { type: "AUTH_LOGOUT" }
}
class Login extends React.Component {
login = () => {
this.props.authLogin();
};
logout = () => {
this.props.authLogout();
};
render() {
if (this.props.Auth.isLoggedIn) {
return <button onClick={this.logout}>Logout</button>;
} else {
return <button onClick={this.login}>Login</button>;
}
}
}
const mapStateToProps(state) {
return {
Auth: state.Auth
}
}
export default connect(mapStateToProps, {authLogin, authLogout})(Login);