NextJS undefined appContext.ctx.req.headers on Cookies - reactjs

I want to store my portfolio password in browser cookies. I added the following code to my _app.js now when visiting the not normal pages I'm getting an error. I don't understand what I have done wrong.
import App from 'next/app'
import Cookies from 'universal-cookie'
import consts from '../consts'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
MyApp.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext)
const cookies = new Cookies(appContext.ctx.req.headers.cookie)
const password = cookies.get(consts.SiteReadCookie) ?? ''
if (password === 'letmein') {
appProps.pageProps.hasReadPermission = true
}
return { ...appProps }
}
export default MyApp
Error

Related

Next.js: prevent Showing index page before redirecting to Keycloak Login page

I've been working on Next.js project which authenticates with a Keycloak.
I used '#react-keycloak/ssr' library to implement frontend authentication.
Here's my _app.js code (Code Reference: https://github.com/react-keycloak/react-keycloak-examples/tree/master/examples/nextjs-app)
import cookie from 'cookie'
import * as React from 'react'
import type { IncomingMessage } from 'http'
import type { AppProps, AppContext } from 'next/app'
import { SSRKeycloakProvider, SSRCookies } from '#react-keycloak/ssr'
const keycloakCfg = {
url: 'http://myauthurl/auth',
realm: 'myrealm',
clientId: 'myclientid',
}
interface InitialProps {
cookies: unknown
}
function MyApp({ Component, pageProps, cookies }: AppProps & InitialProps) {
const initOptions = {
onLoad: 'login-required',
checkLoginIframe: false
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakCfg}
persistor={SSRCookies(cookies)}
initOptions={initOptions}
>
<Component {...pageProps} />
</SSRKeycloakProvider>
)
}
function parseCookies(req?: IncomingMessage) {
if (!req || !req.headers) {
return {}
}
return cookie.parse(req.headers.cookie || '')
}
MyApp.getInitialProps = async (context: AppContext) => {
// Extract cookies from AppContext
return {
cookies: parseCookies(context?.ctx?.req),
}
}
export default MyApp
My goal was to redirect unauthenticated users to keycloak login page. It worked by adding 'initOptions'. However, before redirecting, application shows index page for one second.
EDIT:
After writing code checks the state of authentication, I managed to hide components for users who are not logged in. However, after login success application shows blank page and keycloak is undefined.
import type { AppProps, AppContext } from "next/app";
import { SSRKeycloakProvider, useKeycloak } from "#react-keycloak/ssr";
import { Provider } from "urql";
import {
keycloakConfig,
initOptions,
getPersistor,
Keycloak,
} from "../libs/keycloak";
import { parseCookies } from "../libs/cookie";
import { useMemo } from "react";
import { createUrqlClient, ssrCache } from "../libs/urql";
interface Props extends AppProps {
cookies: unknown;
token?: string;
}
function MyApp({ Component, pageProps, cookies, token }: Props) {
const urqlClient = useMemo(() => createUrqlClient(token), [token]);
const {keycloak} = useKeycloak
console.log(keycloak) //undefined after login success
console.log(keycloak?.authenticated ) //undefined after login success
// SSR cache for urql
if (pageProps?.urqlState) {
ssrCache.restoreData(pageProps.urqlState);
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakConfig}
persistor={getPersistor(cookies)}
initOptions={initOptions}
>
<Provider value={urqlClient}>
{keycloak?.authenticated && <Component {...pageProps} /> }
</Provider>
</SSRKeycloakProvider>
);
}
MyApp.getInitialProps = async (context: AppContext) => {
const keycloak = Keycloak(context?.ctx?.req);
return {
cookies: parseCookies(context?.ctx?.req),
token: keycloak.token,
};
};
export default MyApp;
It has to wait for a split seconds since it needs to check for authentication.. what is your desire behavior?
You can use the useKeycloak hook in your page to show the behavior you want (e.g. Redirecting to login page....)
const IndexPage = () => {
const { keycloak } = useKeycloak<KeycloakInstance>()
const loggedinState = keycloak?.authenticated ? (
<span className="text-success">logged in</span>
) : (
<span className="text-danger">NOT logged in</span>
)
const welcomeMessage =
keycloak?.authenticated
? `Welcome back!`
: 'Welcome visitor. Please login to continue.'
return (
<Layout title="Home | Next.js + TypeScript Example">
<h1 className="mt-5">Hello Next.js + Keycloak 👋</h1>
<div className="mb-5 lead text-muted">
This is an example of a Next.js site using Keycloak.
</div>
<p>You are: {loggedinState}</p>
<p>{welcomeMessage}</p>
</Layout>
)
}
export default IndexPage
Simplified example from https://github.com/react-keycloak/react-keycloak-examples/blob/master/examples/nextjs-app/pages/index.tsx

next-auth / urql - access the access token to set Authorization header in urql client

I am using next-auth to manage JWT tokens and sessions for my next.js application. I'm also using urql as the GraphQL client. I initialise the urql client in a file as follows:
import { createClient } from 'urql';
const client = createClient({
url: process.env.NEXT_PUBLIC_API_URL,
fetchOptions: () => {
return {
headers: {}, // I'd like to set Auth header here...
};
},
});
export default client;
And then this is added to _app.tsx as follows:
import type { AppProps } from 'next/app';
import { Provider as AuthProvider } from 'next-auth/client';
import { Provider as GraphqlProvider } from 'urql';
import client from '../graphql/client';
function App({ Component, pageProps }: AppProps) {
const { session } = pageProps;
return (
<GraphqlProvider value={client}>
<AuthProvider session={session}>
<Component {...pageProps} />
</AuthProvider>
</GraphqlProvider>
);
}
export default App;
Nothing fancy at this stage.
Now my issue is that I want to be able to access the access token to add an Authorization header in client, however because this bit of code exists outside of a React component I can't use the useSession hook provided by next-auth.
Can anyone suggest the best way to approach this issue?
Thanks.
I was able to get a very helpful response from one of the next-auth maintainers on how to handle this scenario: https://github.com/nextauthjs/next-auth/discussions/1806
Important points are to have the AuthProvider as the most parent component in _app.tsx, then create a useClient hook that you pass to urql's provider:
/graphql/client.ts
import { createClient } from '#urql/core';
import { useSession } from 'next-auth/client';
import * as React from 'react';
/**
* Get GraphQL Client in browser environments (frontend).
*
* If the user has an active session, it will add an accessToken to all requests
*/
const useClient = (options?: RequestInit) => {
const [session] = useSession();
const token = session?.accessToken;
// const handleError = useErrorHandler();
return React.useMemo(() => {
const client = createClient({
url: process.env.NEXT_PUBLIC_API_URL,
fetchOptions: () => {
return {
headers: {
Authorization: token ? `Bearer ${token}` : '',
...(options?.headers ? options.headers : {}),
},
};
},
});
return client;
}, [options, token]);
};
export default useClient;
then create the urql provider, passing in the client that you get from useClient hook:
/graphql/provider.tsx
import React from 'react';
import { Provider } from 'urql';
import useClient from './client';
interface GraphqlProviderProps {}
const GraphqlProvider: React.FC<GraphqlProviderProps> = ({ children }) => {
const client = useClient();
return <Provider value={client}>{children}</Provider>;
};
export default GraphqlProvider;
And then use this as child to AuthProvider in _app.tsx
import { Provider as AuthProvider } from 'next-auth/client';
import type { AppProps } from 'next/app';
import GraphqlProvider from '../graphql/provider';
import '../styles/index.css';
function App({ Component, pageProps }: AppProps) {
const { session } = pageProps;
return (
<AuthProvider session={session}>
<GraphqlProvider>
<Component {...pageProps} />
</GraphqlProvider>
</AuthProvider>
);
}
export default App;
Finally, you can pause all useQuery requests until session is initialised:
/pages/index.tsx
import { signIn, signOut, useSession } from 'next-auth/client';
import React from 'react';
import { useQuery } from 'urql';
interface HomeProps {}
const Home: React.FC<HomeProps> = ({}) => {
const [session, loading] = useSession();
const QUERY = `
query {
users {
id
name
email
}
}
`;
const request = useQuery({ query: QUERY, pause: !session });
console.log(request);
return (
<div>
{!session && (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)}
{session && (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)}
</div>
);
};
export default Home;
And it works! You can see the auth header with valid jwt token in the network requests :-)

Is this protecting route solution for Nextjs usable?

import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Login from './index';
import auth from '../services/authService';
export default function App({ Component, pageProps }) {
const [user, setUser] = useState(null);
const router = useRouter();
useEffect(() => {
const user = auth.getCurrentUser();
setUser(user);
if (!user) return router.replace('/');
}, []);
return <Component {...pageProps} />;
}
Here-
Login component contains the login form.
auth.getCurrentUser() returns jwt decoded user object.
I do not want any page except login page to be accessible by a unauthorized user. Will it even work? I am looking for a good solution for this problem.
i suggest to use react context :
for example :
1-create usercontext
export const UserContext = createContext(initialUser)
2- then create userprovider
UserProvider :
<UserContext.Provider value={initialUser}>{children}</UserContext.Provider>
3- for example:
initialUser ={
user :{USER_DATA},
isLogin : true | false //if cookie set or user exist ? true : false
}
you can fetch USER_DATA from api or internet
also UserProvider :
const checkAuth = () =>{ ... }
const logout = () =>{ ... }
const storeUser = user => { ... }
return <UserContext.Provider value={initialUser}>{children}.
</UserContext.Provider>
in _app
import { UserProvider, UserContext } from "PATH_TO_CONTEXT/UserConext";
const initialUser = useContext(UserContext)
<UserProvider >
{initialUser.isLogin ? <Component {...pageProps} /> : <Login {...pageProps} />}
</UserProvider>
as you see i used UserProvider as parent component so each route can access to user info or login info.

NextJS: How to add screen loading for production build?

I want add screen loading in next js project. And I tried to do that with the Router component in next/router.
This is my _app.js in next.js project:
import {CookiesProvider} from 'react-cookie';
import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import withRedux from 'next-redux-wrapper'
import withReduxSaga from 'next-redux-saga'
import createStore from '../src/redux/store'
import Router from "next/router";
import {Loaded, Loading} from "../src/util/Utils";
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ctx})
}
return {pageProps}
}
render() {
Router.onRouteChangeStart = () => {
Loading()
};
Router.onRouteChangeComplete = () => {
Loaded()
};
Router.onRouteChangeError = () => {
Loaded()
};
const {Component, pageProps, store} = this.props;
return (
<CookiesProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</CookiesProvider>
)
}
}
export default withRedux(createStore)(withReduxSaga(MyApp))
This is Loaded() and Loading() functions:
export const Loaded = () => {
setTimeout(() => {
let loading = 'has-loading';
document.body.classList.remove(loading);
}, 100);
};
export const Loading = () => {
let loading = 'has-loading';
document.body.classList.add(loading);
};
The code works well when the project is under development mode. But when the project is built, the loading won't disappear.
Do you know solution of this issue or are you suggesting another solution?
Using apollo client and react hooks you could do as follow.
Example:
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { withApollo } from '../lib/apollo';
import UserCard from '../components/UserCard';
export const USER_INFO_QUERY = gql`
query getUser ($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
}
}
`;
const Index = () => {
const { query } = useRouter();
const { login = 'default' } = query;
const { loading, error, data } = useQuery(USER_INFO_QUERY, {
variables: { login },
});
if (loading) return 'Loading...'; // Loading component
if (error) return `Error! ${error.message}`; // Error component
const { user } = data;
return (
<UserCard
float
href={user.url}
headerImg="example.jpg"
avatarImg={user.avatarUrl}
name={user.name}
bio={user.bio}
/>
);
};
export default withApollo({ ssr: true })(Index);
More info here: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I added the following codes to a wrapper component and the problem was resolved.
componentDidMount() {
Loaded();
}
componentWillUnmount() {
Loading();
}

Send param to fetch in getInitialProps react and nextjs

I 'm traying to send a param to getInitialProp function to made the fecth to the correct json.
here is my code:
hepler.js --> here I made the fetch per se.
export async function getEvents() {
const res = await fetch("https://url/eventos.json");
let new_data = await res.json();
return { events: new_data.data };
}
export async function getDetails(slug) {
const res = await fetch(`https://myurl/${slug}.json`);
let data_detail_event = await res.json();
return { data_detail_event };
}
_app.js // here I have the getInitialProps and works great
import App from "next/app";
import ContextProvider from "../provider/ContextProvider";
import fetch from "isomorphic-unfetch";
import {getEvents, getDetails} from '../helper/index'
export default class MyApp extends App {
static async getInitialProps() {
const events = await getEvents();
return {
events : events.events
};
}
render() {
const { Component, pageProps } = this.props;
return (
<div>
<ContextProvider events={this.props.events} >
<Component {...pageProps} />
</ContextProvider>
</div>
);
}
}
pages/[id].js
import { useRouter } from "next/router";
import Context from "../../config/Context";
/* Components */
import WordCounter from "../../components/word-counter/WordCounter";
function Post(props) {
const router = useRouter();
const context = React.useContext(Context);
return (
<React.Fragment>
<WordCounter />
</React.Fragment>
);
}
Post.getInitialProps = async ({ query}) => {
const detail = await getDetail(query.id) --> here I send the param and it seems never arrive to helper.js, why?
return {detail}
}
export default Post
Where is the problem? HELP!
THAANKS!
i think getInitialProps run in server and your helper function doesn't load there.
use fetch inside getInitialProps .

Resources