I am working on a next.js app which uses firebase. I need to use firebase auth package to restrict access to pages. The with-firebase-authentication example doesn't show authentication for multiple pages.
import React from 'react';
import Router from 'next/router';
import { firebase } from '../../firebase';
import * as routes from '../../constants/routes';
const withAuthorization = (needsAuthorization) => (Component) => {
class WithAuthorization extends React.Component {
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser && needsAuthorization) {
Router.push(routes.SIGN_IN)
}
});
}
render() {
return (
<Component { ...this.props } />
);
}
}
return WithAuthorization;
}
export default withAuthorization;
This is a React Firebase Authentication example, but it should work with next.js as well.
The main idea is to create a Higher Order Component, which checks if the user is authenticated and wrap all pages around that:
import React from 'react';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
render() {
return <Component {...this.props} />;
}
}
return WithAuthentication;
};
export default withAuthentication;
You could override the _app.js and only return <Component {...pageProps} /> if the user is authenticated.
You could do something like this:
const withAuthorization = (needsAuthorization) => (Component) => {
class WithAuthorization extends React.Component {
state = { authenticated: null }
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser && needsAuthorization) {
Router.push(routes.SIGN_IN)
} else {
// authenticated
this.setState({ authenticated: true })
}
});
}
render() {
if (!this.state.authenticated) {
return 'Loading...'
}
return (
<Component { ...this.props } />
);
}
}
return WithAuthorization;
}
Best would be to handle this on the server.
Struggled with integrating firebase auth as well, ended up using the approach detailed in the with-iron-session example on nextjs: https://github.com/hajola/with-firebase-auth-iron-session
Hi after some research here there seems to be two ways of doing this. Either you alternate the initialization process of the page using Custom to include authentication there - in which case you can transfer the authentication state as prop to the next page - or you would ask for a new authentication state for each page load.
Related
I created a wrapper for the pages which will bounce unauthenticated users to the login page.
PrivateRoute Wrapper:
import { useRouter } from 'next/router'
import { useUser } from '../../lib/hooks'
import Login from '../../pages/login'
const withAuth = Component => {
const Auth = (props) => {
const { user } = useUser();
const router = useRouter();
if (user === null && typeof window !== 'undefined') {
return (
<Login />
);
}
return (
<Component {...props} />
);
};
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default withAuth;
That works \o/, However I noticed a behavior when I log out, using Router.push('/',), to return the user to the homepage the back button contains the state of previous routes, I want the state to reset, as a user who is not authenticated should have an experience as if they're starting from scratch...
Thank you in advance!
You can always use Router.replace('/any-route') and the user will not be able to go back with back button
This page is the most relevant information I can find but it isn't enough.
I have a generic component that displays an appbar for my site. This appbar displays a user avatar that comes from a separate API which I store in the users session. My problem is that anytime I change pages through next/link the avatar disappears unless I implement getServerSideProps on every single page of my application to access the session which seems wasteful.
I have found that I can implement getInitialProps in _app.js like so to gather information
MyApp.getInitialProps = async ({ Component, ctx }) => {
await applySession(ctx.req, ctx.res);
if(!ctx.req.session.hasOwnProperty('user')) {
return {
user: {
avatar: null,
username: null
}
}
}
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
user: {
avatar: `https://cdn.discordapp.com/avatars/${ctx.req.session.user.id}/${ctx.req.session.user.avatar}`,
username: ctx.req.session.user.username
},
pageProps
}
}
I think what's happening is this is being called client side on page changes where the session of course doesn't exist which results in nothing being sent to props and the avatar not being displayed. I thought that maybe I could solve this with local storage if I can differentiate when this is being called on the server or client side but I want to know if there are more elegant solutions.
I managed to solve this by creating a state in my _app.js and then setting the state in a useEffect like this
function MyApp({ Component, pageProps, user }) {
const [userInfo, setUserInfo] = React.useState({});
React.useEffect(() => {
if(user.avatar) {
setUserInfo(user);
}
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<NavDrawer user={userInfo} />
<Component {...pageProps} />
</ThemeProvider>
);
}
Now the user variable is only set once and it's sent to my NavDrawer bar on page changes as well.
My solution for this using getServerSideProps() in _app.tsx:
// _app.tsx:
export type AppContextType = {
navigation: NavigationParentCollection
}
export const AppContext = createContext<AppContextType>(null)
function App({ Component, pageProps, navigation }) {
const appData = { navigation }
return (
<>
<AppContext.Provider value={appData}>
<Layout>
<Component {...pageProps} />
</Layout>
</AppContext.Provider>
</>
)
}
App.getInitialProps = async function () {
// Fetch the data and pass it into the App
return {
navigation: await getNavigation()
}
}
export default App
Then anywhere inside the app:
const { navigation } = useContext(AppContext)
To learn more about useContext check out the React docs here.
I am trying to test the follow component, which is based on a pretty common "ProtectedRoute" HOC/wrapper example on several React training sites. The basic concept is to wrap protected components at the <Route> level in order to redirect unauthenticated users.
//Authenticated.js
import React from 'react';
import PropTypes from 'prop-types';
import { history } from 'store';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
export default function (Component) {
class Authenticated extends React.Component {
constructor(props){
super(props);
this.state = {
authenticated: props.authenticated
}
}
static getDerivedStateFromProps(props) {
return {
authenticated: props.authenticated
}
}
componentDidMount() {
this._checkAndRedirect();
}
componentDidUpdate() {
this._checkAndRedirect();
}
_checkAndRedirect() {
const { authenticated, redirect } = this.props;
if (!authenticated) {
redirect();
}
}
render() {
return (
<div>
{ this.props.authenticated ? <Component {...this.props} /> : null }
</div>
);
}
}
const mapStoreStateToProps = (store) => {
return {
authenticated: store.auth.authenticated
};
};
const mapDispatchToProps = dispatch => bindActionCreators({
redirect: () => history.push('/')
}, dispatch)
Authenticated.propTypes = {
authenticated: PropTypes.bool,
redirect: PropTypes.func.isRequired
}
return connect(
mapStoreStateToProps,
mapDispatchToProps
)(Authenticated);
}
So far, I've written the following tests using react-test-renderer and redux-mock-store:
//Authenticated.spec.js
import React from 'react';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router-dom';
import { createMemoryHistory } from "history";
import renderer from 'react-test-renderer';
import mockStore from 'tests/mockStore';
import Authenticated from 'components/common/Authenticated';
import Home from 'components/Home';
describe('Authenticated', () => {
it('displays protected component if authenticated', () => {
const AuthenticatedComponent = Authenticated(Home);
const store = mockStore({ auth: { authenticated: true } });
store.dispatch = jest.fn();
const history = createMemoryHistory();
history.push('/admin')
const component = renderer.create(
<Provider store={store}>
<Router history={history} >
<AuthenticatedComponent />
</Router>
</Provider>
);
expect(component.toJSON()).toMatchSnapshot();
expect(history.location.pathname).toBe("/admin");
});
it('redirects to login if not authenticated', () => {
const AuthenticatedComponent = Authenticated(Home);
const store = mockStore({ auth: { authenticated: false } });
store.dispatch = jest.fn();
const history = createMemoryHistory();
const component = renderer.create(
<Provider store={store}>
<Router history={history} >
<AuthenticatedComponent />
</Router>
</Provider>
);
expect(component.toJSON()).toMatchSnapshot();
expect(history.location.pathname).toBe("/");
});
});
Both of these tests run fine, and pass as expected, however I am now interested in testing following real world scenario:
A user is currently logged in, with authenticated=true in the store. After some time their cookie expires. For our example, let's say we periodically check the state of auth with server, and it triggers some action to fire, causing the store's new state to result in authenticated=false.
Unless I just never came across an example, I am fairly certain redux-mock-store (even though it's literally named "mock-store") does not actually call any reducers to return a new "mock store" state when you call store.dispatch(myAction()). I am able to test the state changes in myAction.spec.js, but I am still unable to simulate the HOC receiving the updated state/prop in getDerivedStateFromProps
From what I can tell the tests I've written cover the two scenarios of where the component loads with authenticated=true/false initial states and that works as expected, but when I look at the coverage results, the getDerivedStateFromProps function is not being covered by these tests.
How would I go about writing a test to simulate the change of authentication boolean in the store state and asserting that the Authenticated component received the change in getDerivedStateFromProps (previously componentWillReceiveProps) in order to make sure I am covering this in testing?
When I create a simple app, I found that next.js will automatically do the server side render.
But when I tried to fetch the data from backend, I found that server side won't get the data.
How to fetch the data from server side? So that I can do the server side render?
components/test.js
import React, { Component } from 'react';
class Test extends Component {
constructor(){
super();
this.state={
'test':''
}
}
setTest(){
axios.get(serverName+'/api/articles/GET/test').then(response=>{
let test;
test = response.data.test;
this.setState({test});
}).catch(function (error) {
console.log(error);
});
}
}
render() {
return (
<div>
{this.state.test}
</div>
);
}
}
export default Test;
backend is just like following:
function getTest(Request $request){
return response()->json(['test'=>'this is a test']);
}
Next.js uses getInitialProps that is executed on the server on initial load only.
From docs
For the initial page load, getInitialProps will execute on the server
only. getInitialProps will only be executed on the client when
navigating to a different route via the Link component or using the
routing APIs.
All other lifecycle methods/actions on React components (componentDidMount, onClick, onChange etc) are executed on the client side.
Example code
class Test extends Component {
static async getInitialProps() {
const response = await axios.get(serverName + '/api/articles/GET/test');
return { test: response.data.test }
}
render() {
return <div>{this.props.test}</div>;
}
}
export default Test;
Like below. I would recomend to use getInitialProps. This is recommended approach by next.js to get data at server.
import React from 'react'
export default class extends React.Component {
static async getInitialProps({ req }) {
axios.get(serverName+'/api/articles/GET/test').then(response=>{
let test;
test = response.data.test;
return { test }
}).catch(function (error) {
return { response }
});
}
render() {
return (
<div>
Hello World {this.props.test.name}
</div>
)
}
}
I'm trying to direct the user to Main Component every time the app starts up.
I use react-native-router-flux as my navigation wrapper, Firebase as backend and Redux as state management library.
Here's the problem. I do firebase.auth().onAuthStateChanged() in my Router Component to determine whether the user is loggedIn.
If there is no user, then return Login component.
If there is a user logged in, return Main Component, as the base component in the stack.
This SO POST has the exact problem and the answer proposed suggests using Switch Feature from react-native-router-flux. I gave it a shot, not helpful.
Here is my code.
App.js
import React, { Component } from 'react';
import firebase from 'firebase';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk'
import reducers from './reducers';
import Router from './Router';
class App extends Component {
componentWillMount() {
firebase.initializeApp({
//my firebase config
});
}
render() {
const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
return (
<Provider store={store}>
<Router />
</Provider>
);
}
}
export default App;
Router.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { isLoggedIn } from './actions';
import { Scene, Router, Actions, Switch } from 'react-native-router-flux';
import firebase from 'firebase';
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import Main from './components/Main';
class RouterComponent extends Component {
state = { loggedIn: false }
componentWillMount() {
this.props.isLoggedIn();
}
render() {
return(
<Router sceneStyle={{ }} hideNavBar={true}>
<Scene key="root">
<Scene key="authContainer" initial>
<Scene key="login" component={Login} title="Login Page" />
<Scene key="register" component={Register} title="Register Page" />
</Scene>
<Scene key="mainContainer">
<Scene key="main" component={Main} title="Main Page" />
</Scene>
</Scene>
</Router>
)
}
}
export default connect(null, { isLoggedIn })(RouterComponent);
Some of my redux action creators
export const isLoggedIn = (dispatch) => {
return(dispatch) => {
firebase.auth().onAuthStateChanged((user) => loginUserSuccess(dispatch, user));
};
};
const loginUserSuccess = (dispatch, user) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user
});
Actions.mainContainer({ type: 'reset'});
};
I understand the exact motivation can be achieved with the async snippet below:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// return <Main />
} else {
// Stay at login screen
}
});
but I'm trying to recreate it with react-native-router-flux.
Here's a gif-shot of my app to help you guys understand more.
A user has already logged in. The Main screen is being dispatched after some time. Any thought?
I don't know if there are any specific constraints in your app but I'm using a Splash screen (also disable the default one generated by Xcode) and perform authentication checking on this screen.
If user logged in, reset the stack to Main component, otherwise reset the stack to Login component.