How to get cookies in getServerSideProps NextJS [duplicate] - javascript

This question already has an answer here:
Why are cookies not sent to the server via getServerSideProps in Next.js?
(1 answer)
Closed 10 months ago.
In my User page I'm using getServerSideProps to get user's data.
At console.log number 1 you can see that cookies with tokens are exsists.
But at console.log number 2 cookies are empty
Also I did same request inside useEffect
And it works as I expect. And at the console.log number 2 cookies object contains tokens.
How to set cookies for getServerSideProps request?
export async function getServerSideProps({req, res}) {
console.log(1, req.cookies)
const response = await $api.get(`/store/profile/user`, {withCredentials: true})
return { props: {} }
}
const User: NextPage = ({props}) => {
const getData = async () => {
const response = await $api.get(`/store/profile/user`, {withCredentials: true})
}
React.useEffect(() => {
getData()
}, [])
return (
<LoginLayout>
<ProfileLayout>
<UserInfo />
</ProfileLayout>
</LoginLayout>
)
}
export default User
export default async function refresh(req, res, next) {
try {
console.log(2, req.cookies)
const {refreshToken} = req.cookies
const userData = await userService.refresh(refreshToken)
res.cookie("refreshToken", userData.refreshToken, {maxAge: 5 * 24 * 60 * 60 * 1000, httpOnly: true})
return res.json(userData)
} catch (e) {
res.status(e.status).json({message: e.message})
}
}
logs for getServerSideProps request:
1 {
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InpoZW4uYWxleGVlZmZAZ21haWwuY29t',
token:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InpoZW4uYWxleGVlZmZAZ21haWwuY29tIiThm'
}
2 {}
logs for request inside useEffect:
2 {
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InpoZW4uYWxleGVlZmZAZ21haWwuY29ih',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InpoZW4uYWxleGVlZmZAZ21haWwuY29tIiwiaWQg'
}

First install cookie package using npm
npm install cookie
and then import it where you need it
import cookie from "cookie";
and the rest must be something like the following:
export const getServerSideProps = async (context) => {
const mycookie = cookie.parse(
(context.req && context.req.headers.cookie) || ""
);
let cookieNameData = {};
if (mycookie.whatevercookienameis) {
cookieNameData = mycookie.whatevercookienameis;
}
return {
props: {
cookieNameData
}
}
}
Hope it gives you a clear picture how to achieve it.

Related

Remix Auth - `destroySession` returning [object Promise] instead of clearing the session

I'm trying to implement User Authentication in a Remix app and have been going around in circles trying to figure out why destroying the session is returning [object Promise] instead of clearing it.
My auth.server.js file contains functions that manage anything relating to creating, retrieving and destroying the Session Cookie, as well as a function to login.
import { hash, compare } from "bcryptjs";
import { prisma } from "./database.server";
import { createCookieSessionStorage, redirect } from "#remix-run/node";
export const sessionStorage = createCookieSessionStorage({
cookie: {
secure: process.env.NODE_ENV === "production",
path: "/",
secrets: [process.env.SESSION_SECRET],
sameSite: "lax",
maxAge: 30 * 24 * 60 * 60, // 30 days
httpOnly: true
}
});
async function createUserSession(userId, redirectPath) {
const session = await sessionStorage.getSession();
session.set("userId", userId);
return redirect(redirectPath, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session)
}
});
}
export async function getUserFromSession(request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const userId = session.get("userId");
return userId ? userId : null;
}
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
"Set-Cookie": sessionStorage.destroySession(session)
}
});
}
export async function login({ email, password }) {
const existingUser = await prisma.user.findFirst({ where: { email } });
if (!existingUser) {
return throwError(
"Could not log you in, please check provided email.",
401
);
}
const passwordCorrect = await compare(password, existingUser.password);
if (!passwordCorrect) {
return throwError(
"Could not log you in, please check provided password.",
401
);
}
return createUserSession(existingUser.id, "/");
}
function throwError(text, code) {
const error = new Error(text);
error.status = code; // authentication error code
throw error; // this triggers ErrorBoundary, as it's not an error response
}
My logout function is in a separate .js file located inside the /routes folder.
import { json } from "#remix-run/node";
import { destroyUserSession } from "~/data/auth.server";
export function action({ request }) {
if (request.method !== "POST") {
throw json({ message: "Invalid request method" }, { status: 400 });
}
return destroyUserSession(request);
}
Finally, the Logout button is inside the navigation bar and contained within a <Form> element.
<Form method="post" action="/logout">
<button type="submit">Logout</button>
</Form>
SOLVED
I was somehow able to solve it by moving the logout function into auth.server.js and leaving the following in the logout.js route:
import { redirect } from "#remix-run/node";
import { logout } from "~/data/auth.server";
export const action = ({ request }) => logout(request);
export const loader = () => redirect("/auth");
The session functions are all async. The reason you had [object Promise] was that you were returning the promise directly. You need to use await.
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
// use await on session functions
"Set-Cookie": await sessionStorage.destroySession(session)
}
});
}
P.S. That's one of the benefits of TypeScript. It would have told you that headers required a string value and would not allow the Promise return.

nextjs-auth0: update user session (without logging out/in) after updating user_metadata

I'm currently struggling to refetch a user's data from Auth0 after updating the user_metadata:
Below a simplified index file. the user selects some object, and will be asked to add this object (or object-id) as a favorite. If the user wants to select this object as a favorite, we want to update the preference in the user_metadata.
// index.tsx
export default function home({user_data, some_data}) {
const [selected, setSelect] = useState(null)
async function handleAddToFavourite() {
if (selected) {
const data = await axios.patch("api/updateMetadata", {some_favorite: selected.id})
// Errorhandling ...
}
}
return (
<div>
<SearchData setData={setSelect} data={some_data}/>
<Button onClick={handleAddToFavorite}>Add to Favorite</Button>
<div>Selected: {selected.id}</div>
<div>My Favorite: {user_data.user_metadata.some_favorite}</div>
</div>
)
}
export const getServerSideProps = withPageAuthRequired({
returnTo: "/foo",
async getServerSideProps(ctx) {
const session = await getSession(ctx.req, ctx.res)
const {data} = await axios.get("https://somedata.com/api")
return {props: {some_data: data, user_data: session.user}}
})
The request is then sent to pages/api/updateMetadata, and the user_metadata is updated with the selected data.
// api/updateMetadata.ts
async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession(req, res);
if (!session || session === undefined || session === null) {
return res.status(401).end();
}
const id = session?.user?.sub;
const { accessToken } = session;
const currentUserManagementClient = new ManagementClient({
token: accessToken,
domain: auth0_domain.replace('https://', ''),
scope: process.env.AUTH0_SCOPE,
});
const user = await currentUserManagementClient.updateUserMetadata({ id }, req.body);
return res.status(200).json(user);
}
export default withApiAuthRequired(handler);
The [...auth0].tsx looks something like this.
// pages/api/auth/[...auth0].tsx
export default handleAuth({
async profile(req, res) {
try {
await handleProfile(req, res, {
refetch: true,
});
} catch (error: any) {
res.status(error.status || 500).end(error.message);
}
},
async login(req, res) {
try {
await handleLogin(req, res, {
authorizationParams: {
audience: `${process.env.AUTH0_ISSUER_BASE_URL}/api/v2/`,
scope: process.env.AUTH0_SCOPE,
},
});
} catch (error: any) {
res.status(error.status || 400).end(error.message);
}
},
});
Now, I get the user_metadata every time I log in, however, I need to log out and log in to see the change take effect. I need to somehow refresh the user-session without logging out, every time the user_metadata is updated.
Does anybody know any workarounds for achieving what I'm trying to do, and perhaps see any mistakes?
Thanks in advance!
Notes:
I have tried using the client-side function useUser(), but this yields the same data as server-side function getSession() for the user_data in index.tsx
I've tried adding updateSession(req, res, session) at the end of the api/updateMetadata handler
I've added an Action to the Auth0 login flow
// Auth0 action flow - login
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://example.com';
const { some_favorite } = event.user.user_metadata;
if (event.authorization) {
// Set claims
api.idToken.setCustomClaim(`${namespace}/some_favorite`, );
}
};
I figured it out, I'll post my solution in case someone else gets stuck with the same issue :)
In api/updateMetadata.ts:
// api/updateMetadata.ts
import { updateSession , ... } from '#auth0/nextjs-auth0';
// ...
// ...
const user = await currentUserManagementClient.updateUserMetadata({ id }, req.body);
await updateSession(req, res, { ...session, user }); // Add this to update the session
return res.status(200) // ...
Then I used checkSession() from the useUser in the client side code, straight after fetching the data.
// index.tsx
import { useUser } from '#auth0/nextjs-auth0/client'
//...
async function handleAddToFavourite() {
if (selected) {
const data = await axios.patch("api/updateMetadata", {some_favorite: selected.id})
// Update the user session for the client side
checkSession()
// Errorhandling ...
}
}
//...
Now, this is what made it all work, modifying the profileHandler:
// pages/api/auth/[...auth0].tsx
// Updating with the new session from the server
const afterRefetch = (req, res, session) => {
const newSession = getSession(req, res)
if (newSession) {
return newSession as Promise<Session>
}
return session
}
export default handleAuth({
async profile(req, res) {
try {
await handleProfile(req, res, {
refetch: true,
afterRefetch // added afterRefetch Function
});
} catch (error: any) {
res.status(error.status || 500).end(error.message);
}
},
// ...
});
Also, it is worth noting that the Auth0 Action Flow for the login is correct too.
Hope this helps someone :)

Canceling request of nuxt fetch hook

Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/

How to use cookie inside `getServerSideProps` method in Next.js?

I have to send current language on endpoint. But getting language from Cookie returns undefined inside getServerSideProps.
export async function getServerSideProps(context) {
const lang = await Cookie.get('next-i18next')
const res = await fetch(`endpoint/${lang}`)
const data = await res.json()
return {
props: { data },
}
}
export default Index;
What is the proper way to get cookie inside getServerSideProps?
You can get the cookies from the req.headers inside getServerSideProps:
export async function getServerSideProps(context) {
const cookies = context.req.headers.cookie;
return {
props: {},
};
}
You could then use the cookie npm package to parse them:
import * as cookie from 'cookie'
export async function getServerSideProps(context) {
const parsedCookies = cookie.parse(context.req.headers.cookie);
return { props: {} }
}
To avoid having to parse the cookies string from context.req.headers.cookie, Next.js also provides the cookies as an object which can be accessed with context.req.cookies.
export async function getServerSideProps(context) {
const lang = context.req.cookies['next-i18next']
// ...
}
From getServerSideProps documentation:
The req in the context passed to getServerSideProps provides built in
middleware that parses the incoming request (req). That middleware is:
req.cookies - An object containing the cookies sent by the request.
Defaults to {}
You can use parseCookies function with cookie package
import cookie from "cookie"
function parseCookies(req){
return cookie.parse(req ? req.headers.cookie || "" : document.cookie);
}
And then get access like that.
export async function getServerSideProps({ req} ) {
const cookies = parseCookies(req);
// And then get element from cookie by name
return {
props: {
jwt: cookies.jwt,
}
}
}
If you are using Axios this is very simple
This will work inside getServerSideProps method. You can't get access to the cookie by using withCredentials because this is on the server.
const { token } = context.req.cookies;
const response = await axios.get('/staff/single', {
headers: { Cookie: `token=${token};` },
});
or try (This will work on the client)
const response = await axios.get('/staff/single', {
headers: { withCredentials: true },
});
how are you doing?
you can use Something like this :
export async function getServerSideProps(context) {
console.log(context.req.cookies)
}
so easy and so beautifuly!

Nuxt auth update auth on custom requests

From nuxt auth website I saw this:
setUserToken(token)
Returns: Promise
Set the auth token and fetch the user using the new token and current strategy.
TIP: This function can properly set the user after registration
this.$auth.setUserToken(token)
.then(() => this.$toast.success('User set!'))
Tried to use it and it said method is undefined, looked up in the source files and none of methods are like this one.
I am not very good with this but, how would I set user and token with nuxt/auth module after registration or anything but login/loginWith?
If there is no option for that why is it there on documentation?
I would also need to know if I need to create custom auth do I need to use both cookies and localstorage or just one of them?
It says that cookies are used for server side and storage for client side.
Can I use just cookies and on nuxtServerInit get cookie for token and set token and user data fetched by api within vuex store? Then use it from there if it is needed?
Nuxt/auth module hurt my brain so long and today I created custom module:
First I have this store structure:
store/
-- index.js
-- mutations.js
-- actions.js
-- state.js
-- getters.js
middleware/
-- redirectIfAuth.js
-- redirectIfNotAuth.js
layouts/
default.vue -> has redirectIfNotAuth.js
guest.vue -> has redirectIfAuth.js
pages/
-- login/
---- index.vue -> uses guest.vue as layout
-- dashboard/
----- index.vue -> uses default.vue as layout without declaration
Inside Index.js I have:
import state from './state'
import * as actions from './actions'
import * as mutations from './mutations'
import * as getters from './getters'
export default {
state,
getters,
mutations,
actions,
modules: {}
}
Inside State.js I have:
export default () => ({
user: null,
token: null,
headers: null
})
Inside Actions.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
// importing server based cookie library
export async function nuxtServerInit ({ commit }, { req, res }) {
// If we have any axios requests we need to add async/await
// And since this works on server mode, we don't need to check is it server
let token = null
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
// If we have token within cookies we get user data from api and we pass Autorization headers with token
if (token !== null && token !== false) {
await axios.get('/api/auth/me', {
headers: {
'Authorization': token
}
}).then((response) => {
// If we get user data we set it to store
commit('setUser', response.data.data)
commit('setToken', token)
commit('setHeaders', token)
}).catch((error) => {
// If we get error, we should logout user by removing data within cookies and store
// Additionally you can create specific code error on backend to check if token is expired or invalid
// and then check for status code and then remove data
commit('setUser', null)
commit('setToken', null)
res.setHeader('Set-Cookie', [`authToken=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`])
// This is only way I found useful for removing cookies from node server
console.warn(error)
})
}
}
Inside Mutations.js I have:
export const setToken = (state, payload) => state.token = payload
export const setUser = (state, payload) => state.user = payload
export const setHeaders = (state, payload) => {
state.headers = {
headers: {
'Authorization': payload
}
}
}
Inside Getters.js I have:
export const getUser = (state) => state.user
export const getToken = (state) => state.token
export const getHeaders = (state) => state.headers
Second I created two middlewares and it seems like nuxt middlewares work on both server and client sides, so I needed to require both libraries for server and client side Then I checked which side it is and then try to get token for further investigations If you include and don't check for server and client but use one of them, your templates wont render but show undefined errors for req on client instead and on server it wont show anything.
Inside redirectIfAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token && token !== false) {
app.store.commit('setToken', token)
app.store.commit('setHeaders', token)
if (app.store.state.user) {
if (app.store.state.user.roles.includes('customer')) {
return redirect({
name: 'customer-slug',
params: { slug: app.store.state.user.username }
})
} else if (app.store.state.user.roles.includes('admin')) {
return redirect({
name: 'dashboard'
})
} else {
return redirect({
name: 'index'
})
}
} else {
return redirect({
name: 'index'
})
}
}
}
Inside redirectIfNotAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, route, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token === null || token === false) {
return redirect({
name: 'login',
query: {
redirect: route.fullPath
}
})
}
}
Now we use these middlewares within pages or layouts as:
export default {
middleware: ['redirectIfAuth']
}
Or
export default {
middleware: ['redirectIfNotAuth']
}
Login:
async login () {
if (this.form.email !== '' && this.form.password !== '') {
await this.$axios.post('/api/auth/login', this.form).then((response) => {
this.$store.commit('setUser', response.data.data)
this.$store.commit('setToken', 'Bearer ' + response.data.meta.access_token)
this.$store.commit('setHeaders', 'Bearer ' + response.data.meta.access_token)
Cookie.set('authToken', 'Bearer ' + response.data.meta.access_token, { expires: 365 })
// Cookie.set('authUser', response.data.data, { expires: 365 }) if you need user data within cookies
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect)
}
this.$router.push('/')
})
}
}
Logout:
async logout () {
await this.$axios.post('/api/auth/logout', {}, this.headers)
// Cookie.remove('authUser') if exists
Cookie.remove('authToken')
this.$router.push('/')
}
I hope this helps someone or someone get idea from this to make something else. I had million problems with official nuxt auth and only this helped me sort things out...

Categories

Resources