I am using NextJS with NextAuth with google and email providers. Unfortunately, the session returns only few fields that does not include userId of the user from the database.
I created however a function that I intend to use with every getServerSideProps request. The function returns the following:
{
user: {
name: 'daniel sas',
email: 'emailofuser#gmail.com',
image: 'https://lh3.gooleusercontent.com/a/AEdFTp6r44ZwqcfJORNnuYtbVv_LYbab-wv5Uyxk=s96-c',
userId: 'clbcpc0hi0002sb1wsiea3q5d'
},
expires: '2022-12-17T20:18:52.580Z'
}
The problem is I am getting an error that does not allow me to pass the props in the page:
Error: Your `getServerSideProps` function did not return an object. Did you forget to add a `return`?
In the function I get the user by the email, and attach the userId.
import { getSession } from "next-auth/react";
import prisma from './prisma'
// This function get the email and returns a new session object that includes
// the userId
export const requireAuthentication = async context => {
const session = await getSession(context);
const errorOrUserNotFound = () => {
return {
redirect: {
destination: '/signup',
permanent: false
}
}
}
// If there is no user or there is an error ret to signup page
if (!session) {
errorOrUserNotFound();
}
// If the user is not found return same redirect to signup
else {
try {
const user = await prisma.user.findUnique({where: { email: session.user.email }});
if (!user) return errorOrUserNotFound();
// Must return a new session here that contains the userId...
else {
const newSession = {
user: {
...session.user,
userId: user.id
},
expires: session.expires
};
console.log(newSession);
return {
props: {
session: newSession
}
}
}
}
catch (error) {
if (error) {
console.log(error);
}
}
}
}
The react component looks like this. In the getServerSideProps i return the await function. The problem is that when I log the prop in the serverside, I get the following:
{
props: { session: { user: [Object], expires: '2022-12-17T20:18:52.580Z' } }
}
However, if i log the props in the clientside, I get an empty object...
//Clientside compoen
import { getSession } from "next-auth/react"
import { Fragment, useState } from "react";
import { requireAuthentication } from "../../lib/requireAuthentication";
import CreateListModal from "./CreateListModal";
const DashboardPage = props => {
const [loading, setloading] = useState(false);
console.log(props);
return (
<section className="border-4 border-orange-800 max-w-5xl mx-auto">
<CreateListModal userId={props.userId} loading={loading} setloading={setloading} />
</section>
)
}
export const getServerSideProps = async context => {
const session = await getSession(context);
const reqAuth = await requireAuthentication(context);
console.log(reqAuth);
return reqAuth
}
export default DashboardPage;
I'm using graphql yoga and apollo client in my next app. Graphql yoga suggests this example for apollo client. link It works fine on non-serverless environment but when I deploy on vercel, subscriptions not working. I have tried checking for if it's running on browser but didn't do much. Here's my current apollo client
import {
ApolloLink,
Observable,
ApolloClient,
InMemoryCache,
} from '#apollo/client/core'
import { split } from '#apollo/client/link/core'
import { HttpLink } from '#apollo/client/link/http'
import { print, getOperationAST } from 'graphql'
import { setContext } from '#apollo/client/link/context'
import { getCookie } from 'cookies-next'
const isBrowser = typeof window !== 'undefined'
const token = getCookie('token')
class SSELink extends ApolloLink {
constructor() {
super()
}
request(operation) {
const url = new URL(`${process.env.NEXT_PUBLIC_APP_BASE_URL}/api/graphql`)
url.searchParams.append('query', print(operation.query))
if (operation.operationName) {
url.searchParams.append(
'operationName',
JSON.stringify(operation.operationName)
)
}
if (operation.variables) {
url.searchParams.append('variables', JSON.stringify(operation.variables))
}
if (operation.extensions) {
url.searchParams.append(
'extensions',
JSON.stringify(operation.extensions)
)
}
return new Observable((sink) => {
const eventsource = new EventSource(url.toString(), this.options)
eventsource.onmessage = function (event) {
const data = JSON.parse(event.data)
sink.next(data)
if (eventsource.readyState === 2) {
sink.complete()
}
}
eventsource.onerror = function (error) {
sink.error(error)
}
return () => eventsource.close()
})
}
}
const uri = `${process.env.NEXT_PUBLIC_APP_BASE_URL}/api/graphql`
const sseLink = isBrowser ? new SSELink({ uri }) : null
const httpLink = new HttpLink({ uri })
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
...(isBrowser && token && { authorization: `Bearer ${token}` }),
},
}
})
const link = isBrowser
? split(
({ query, operationName }) => {
const definition = getOperationAST(query, operationName)
return (
definition?.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
sseLink,
httpLink
)
: authLink.concat(httpLink)
export default new ApolloClient({
ssrMode: !isBrowser,
link,
cache: new InMemoryCache(),
})
I'm trying to do authorization using JWT access&refresh tokens (Next.js SSR + Redux Toolkit RTK Query + NestJS). When I receive a response from the server on the client (for example using Postman) the cookies sent by the server are saved. But when I do it on SSR using RTK Query Set-Cookie from the server just doesn't do anything. Sorry if I misunderstood something, I'm new to this.
NSETJS auth.controller.ts:
import {
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
UseGuards
} from '#nestjs/common';
import { Response } from 'express';
import { ApiTags } from '#nestjs/swagger';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { DiscordAuthGuard } from './guards/discord-auth.guard';
import JwtAuthGuard from './guards/jwt-auth.guard';
import { RequestWithUser } from './auth.interface';
import JwtRefreshGuard from './guards/jwt-auth-refresh.guard';
import { UserService } from '#/modules/user/user.service';
#Controller('auth')
#ApiTags('Auth routes')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService
) {}
#Get('login')
#HttpCode(HttpStatus.OK)
#UseGuards(DiscordAuthGuard)
login(#Req() _req: Request) {}
#Get('redirect')
#HttpCode(HttpStatus.OK)
#UseGuards(DiscordAuthGuard)
async redirect(#Req() req: RequestWithUser, #Res() res: Response) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
const refreshTokenCookie = this.authService.getCookieWithJwtRefreshToken(
user.id
);
await this.userService.setCurrentRefreshToken(
refreshTokenCookie.token,
user.id
);
req.res.setHeader('Set-Cookie', [
accessTokenCookie.cookie,
refreshTokenCookie.cookie
]);
return res.redirect('http://localhost:3000');
}
#Get('refresh')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtRefreshGuard)
async refresh(#Req() req: RequestWithUser) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
req.res.setHeader('Set-Cookie', [accessTokenCookie.cookie]);
return user;
}
#Get('me')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtAuthGuard)
me(#Req() req: RequestWithUser) {
const { user } = req;
return user;
}
#Post('logout')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtAuthGuard)
async logout(#Req() req: RequestWithUser) {
const { user } = req;
await this.userService.removeRefreshToken(user.id);
req.res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
}
}
NEXT.JS _app.tsx:
import '#/styles/globals.scss';
import { AppProps } from 'next/app';
import { wrapper } from '#/store';
import { me } from '#/store/auth/auth.api';
import { setCredentials } from '#/store/auth/auth.slice';
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
App.getInitialProps = wrapper.getInitialAppProps(
(store) =>
async ({ ctx, Component }) => {
try {
const { data: user } = await store.dispatch(me.initiate());
if (user !== undefined) {
store.dispatch(
setCredentials({
user,
})
);
}
} catch (err) {
console.log(err);
}
return {
pageProps: {
...(Component.getInitialProps
? await Component.getInitialProps({ ...ctx, store })
: {}),
},
};
}
);
export default wrapper.withRedux(App);
NEXT.JS auth.api.ts:
import { parseCookies } from 'nookies';
import {
BaseQueryFn,
createApi,
fetchBaseQuery,
} from '#reduxjs/toolkit/query/react';
import { HYDRATE } from 'next-redux-wrapper';
import { Mutex } from 'async-mutex';
import { NextPageContext } from 'next/types';
import { IUser } from './auth.interface';
import { destroyCredentials } from './auth.slice';
const mutex = new Mutex();
const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:7777/api/auth',
prepareHeaders: (headers, { extra }) => {
const ctx = extra as Pick<NextPageContext<any>, 'req'>;
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (windowAvailable()) {
console.log('running on browser, skipping header manipulation');
return headers;
}
const cookies = parseCookies(ctx);
// Build a cookie string from object
const cookieValue = Object.entries(cookies)
// .filter(([k]) => k === 'JSESSIONID') // only include relevant cookies
.map(([k, v]) => `${k}=${v}`) // rfc6265
.join('; ');
console.log('figured out cookie value: ' + cookieValue);
headers.set('Cookie', cookieValue);
return headers;
},
credentials: 'include',
});
const baseQueryWithReauth: BaseQueryFn = async (args, api, extraOptions) => {
await mutex.waitForUnlock();
console.log('🔥🔥🔥 sending request to server');
let result = await baseQuery(args, api, extraOptions);
if (result?.error?.status === 401) {
if (!mutex.isLocked()) {
console.log('🔥🔥🔥 401, sending refresh token');
const release = await mutex.acquire();
try {
const refreshResult = await baseQuery('refresh', api, extraOptions);
const setCookies =
refreshResult.meta?.response?.headers.get('set-cookie');
console.log('🔥🔥🔥 response set-cookies:', setCookies);
console.log(
'🔥🔥🔥 refresh response status:',
refreshResult.meta?.response?.status
);
if (refreshResult.data) {
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (!windowAvailable()) {
console.log('🔥🔥🔥 running on server');
}
result = await baseQuery(args, api, extraOptions);
console.log(
'🔥🔥🔥 request response status after /refresh',
result.meta?.response?.status
);
} else {
api.dispatch(destroyCredentials());
}
} finally {
release();
}
}
}
return result;
};
export const authApi = createApi({
reducerPath: 'api/auth',
baseQuery: baseQueryWithReauth,
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath];
}
},
tagTypes: ['Auth'],
endpoints: (build) => ({
me: build.query<IUser, void>({
query: () => ({
url: '/me',
method: 'GET',
}),
providesTags: ['Auth'],
}),
logout: build.mutation<void, void>({
query: () => ({
url: '/logout',
method: 'POST',
}),
invalidatesTags: ['Auth'],
}),
}),
});
// Export hooks for usage in functional components
export const {
useMeQuery,
useLogoutMutation,
util: { getRunningOperationPromises },
} = authApi;
// export endpoints for use in SSR
export const { me, logout } = authApi.endpoints;
NEXT.JS auth.slice.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { RootState } from '..';
import { IUser } from './auth.interface';
export interface IAuthState {
user: IUser | null;
}
const initialState: IAuthState = {
user: null,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (
state,
action: PayloadAction<{
user: IUser | null;
}>
) => {
const { user } = action.payload;
state.user = user;
},
destroyCredentials: (state) => {
state.user = null;
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.auth,
};
},
},
});
export const { setCredentials, destroyCredentials } = authSlice.actions;
export const selectCurrentUser = (state: RootState) => state.auth.user;
export default authSlice.reducer;
NEXT.JS store/index.ts:
import {
configureStore,
ImmutableStateInvariantMiddlewareOptions,
SerializableStateInvariantMiddlewareOptions,
ThunkAction,
} from '#reduxjs/toolkit';
import { Action, combineReducers } from 'redux';
import { Context, createWrapper } from 'next-redux-wrapper';
import botApi from './bots/bot.api';
import authReducer from './auth/auth.slice';
import { authApi } from './auth/auth.api';
// ThunkOptions not exported in getDefaultMiddleware, so we have a copy here
interface MyThunkOptions<E> {
extraArgument: E;
}
// GetDefaultMiddlewareOptions in getDefaultMiddleware does not allow
// providing type for ThunkOptions, so here is our custom version
// https://redux-toolkit.js.org/api/getDefaultMiddleware#api-reference
interface MyDefaultMiddlewareOptions {
thunk?: boolean | MyThunkOptions<Context>;
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions;
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions;
}
const rootReducer = combineReducers({
// Add the generated reducer as a specific top-level slice
[botApi.reducerPath]: botApi.reducer,
[authApi.reducerPath]: authApi.reducer,
auth: authReducer,
});
const makeStore = (wtf: any) => {
const ctx = wtf.ctx as Context;
return configureStore({
reducer: rootReducer,
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (gDM) =>
gDM<MyDefaultMiddlewareOptions>({
thunk: {
// https://github.com/reduxjs/redux-toolkit/issues/2228#issuecomment-1095409011
extraArgument: ctx,
},
}).concat(botApi.middleware, authApi.middleware),
devTools: process.env.NODE_ENV !== 'production',
});
};
export type RootState = ReturnType<AppStore['getState']>;
export type AppStore = ReturnType<typeof makeStore>;
export type AppDispatch = ReturnType<typeof makeStore>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action
>;
export const wrapper = createWrapper<AppStore>(makeStore);
I would be grateful to everyone for any help with a solution 🙂.
I'm looking for a solution/module where I don't need to inject inital/fallback data for swr/react-query things from getServerSideProps. Like...
from
// fetcher.ts
export default fetcher = async (url: string) => {
return await fetch(url)
.then(res => res.json())
}
// getUserData.ts
export default function getUserData() {
return fetcher('/api')
}
// index.tsx
const Page = (props: {
// I know this typing doesn't work, only to deliver my intention
userData: Awaited<ReturnType<typeof getServerSideProps>>['props']
}) => {
const { data } = useSWR('/api', fetcher, {
fallbackData: props.userData,
})
// ...SSR with data...
}
export const getServerSideProps = async (ctx: ...) => {
const userData = await getUserData()
return {
props: {
userData,
},
}
}
to
// useUserData.ts
const fetcher = async (url: string) => {
return await fetch(url)
.then(res => res.json())
};
const url = '/api';
function useUserData() {
let fallbackData: Awaited<ReturnType<typeof fetcher>>;
if (typeof window === 'undefined') {
fallbackData = await fetcher(url);
}
const data = useSWR(
url,
fetcher,
{
fallbackData: fallbackData!,
}
);
return data;
}
// index.tsx
const Page = () => {
const data = useUserData()
// ...SSR with data...
}
My goal is making things related to userData modularized into a component.
I've got several pages that I want to call a query inside of getServerSideProps to request the currentUser.
What I have currently is something like this:
import { NextPageContext } from 'next';
import { withAuth } from 'hoc/withAuth';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
import Profile from 'components/Profile';
const ProfilePage: React.FC = () => <Profile />;
export const getServerSideProps = async (
context: NextPageContext
): Promise<any> => {
// withAuth(context);
const client = initializeApollo();
const { my_token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: my_token ? `Bearer ${my_token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};
export default ProfilePage;
This works, and I can verify in my Apollo devtools that the cache is being updated with the User.
When I try to move the Apollo initialization and query in to a separate file, the cache is never updated for some reason.
Inside of a withAuth.tsx file, I had something like this:
import { NextPageContext } from 'next';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
export const withAuth = async (context: any,) => {
const client = initializeApollo();
const { gc_token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: gc_token ? `Bearer ${gc_token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};
With this, all I have to do is call withAuth() in the getServerSideProps. There are no errors, however the cache doesn't update.
How can I extract that code to a separate file correctly?
Thanks to #juliomalves in the comments, I simply forgot to return the withAuth function!
Here's how it looks now:
pages/index.tsx
export const getServerSideProps = async (
context: NextPageContext
): Promise<any> => await withAuth(context);
withAuth.tsx
/* eslint-disable #typescript-eslint/explicit-module-boundary-types */
import { getSession } from 'next-auth/client';
import redirectToLogin from 'helpers/redirectToLogin';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
export const withAuth = async (context: any) => {
const session = await getSession(context);
const isUser = !!session?.user;
// no authenticated session
if (!isUser) redirectToLogin();
const client = initializeApollo();
const { token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};