Create T3 App Redirect inside a TRPC middleware if user is not signed - javascript

How can I trigger a redirect on the server side if a signed in user has not completed their profile page
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// redirect to profile page if user has not completed profile
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});

This is not currently possible in the way you are describing to the best of my knowledge.
Here are some alternatives that might be helpful:
In getServerSideProps
this only works if you want to redirect before the initial page load. You could also create a wrapper around gSSP to make this more DRY if you're going to use it on a lot of pages.
import { type GetServerSidePropsContext } from "next";
import { getServerAuthSession } from "../server/auth";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getServerAuthSession(ctx);
if (!session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
export default function AuthedPage() {
return <div>Authed</div>;
}
As part of a query or mutation clientside
this is useful for a query or mutation that is only fired after the page has loaded. Again this is a very simple example and could be DRYed, probably the easiest way would be to extract into a custom hook.
import { useRouter } from "next/router";
import { api } from "../utils/api";
export default function AuthedPage() {
const router = useRouter();
// `authedHello` is the example Create T3 App "hello" procedure
// but as a protectedProcedure, ie throws "UNAUTHORIZED" if no session.
// Replace this with a middleware that throws on whatever condition you need it to.
const authedHello = api.example.protectedHello.useQuery(
{ text: "world" },
{
retry: (_count, err) => {
// `onError` only runs once React Query stops retrying
if (err.data?.code === "UNAUTHORIZED") {
return false;
}
return true;
},
onError: (err) => {
if (err.data?.code === "UNAUTHORIZED") {
void router.push("/");
}
},
}
);
return (
<div>
<h1>Authed Page</h1>
<p>{authedHello.data?.greeting}</p>
</div>
);
}
Using Next.js middleware
This is easy to apply to a bunch of routes using the matcher, but it falls a bit outside of T3 conventions.
// pages/middleware.ts
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "../server/auth";
import type { NextApiRequest, NextApiResponse } from "next";
export async function middleware(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user) {
return NextResponse.redirect(new URL("/", req.url));
}
}
export const config = {
matcher: ["/protectedPage", "/anotherProtectedPage"],
};
Using require in next-auth's useSession
this is useful if you want to guard a page but can't use getServerSideProps. It doesn't quite solve your specific problem, but might be useful to other people who find this. See: https://next-auth.js.org/getting-started/client#require-session

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 :)

I am losing user information (session) on refreshing and between tabs while using iron-session and next.js

I am currently working on a project using iron-session and next.js. I don't lose my user on clicking Link tags. But if I refresh user becomes undefined. Cookie is set and doesn't get deleted on refresh. I don't know what is wrong.
Here is my login.ts code:
export default withIronSessionApiRoute(loginRoute, sessionOptions);
async function loginRoute(req: NextApiRequest, res: NextApiResponse) {
try {
const {
data: {tcId, admin, userName},
} = await axios('http://localhost:8080/user/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(req.body),
});
const user = {tcId: tcId, userName: userName, admin: admin} as User;
req.session.user = user;
await req.session.save();
res.json(user);
} catch (error) {
res.status(500).json({message: (error as Error).message})
}}
Here is my session.ts code:
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import type { IronSessionOptions } from 'iron-session'
import type { User } from '../pages/api/user'
export const sessionOptions: IronSessionOptions = {
password: process.env.SECRET_COOKIE_PASSWORD as string,
cookieName: 'eys-cookie',
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
};
// This is where we specify the typings of req.session.*
declare module 'iron-session' {
interface IronSessionData {
user?: User
}
}
As I said previously. I don't lose my user. While routing around using Link tags from next.
Refreshing causes to lose my user. Also other tabs don't reach my user.
I can show more code if wanted. But I think problem is here.
I had a similar issue and solved the tab switching by setting:
revalidateOnFocus = false,
for the useSWR function
sample
const fetcher = (url: any) => fetch(url).then((res) => res.json());
const options = {
revalidateOnFocus: false,
};
const {
data: session,
error,
mutate: mutateUser,
} = useSWR<Session>("/api/session", fetcher, options);
you can also explore other options here https://swr.vercel.app/docs/options
Probably you forgot to add SWRconfig on the index or app component:
import { AppProps } from 'next/app'
import { SWRConfig } from 'swr'
import fetchJson from 'lib/fetchJson'
function MyApp({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
fetcher: fetchJson,
onError: (err) => {
console.error(err)
},
}}
>
<Component {...pageProps} />
</SWRConfig>
)
}
export default MyApp

Auth0 Endpoint "api/auth/me" returns a 404 Error in Next.js App

I have gone through the following tutorial to get my Next.js App integrated with Auth0.
I am able to log in and log out just fine but when trying to display user information on the page after login, the user object is unable to be returned. I have ensured that there is nothing wrong with the Profile.js page that is rendering the user object or the env.local file with my app's secret keys.
After further inspection I noticed that I get an error in the browser console that reads: Failed to Load Resource ... 404 Not Found: http://localhost:3000/api/auth/me.
This error gives me a gut feeling that there is a discrepancy in the mapping between my next.js app and Auth0 since I have modified the basepath in next.config.js:
module.exports = {
basePath: '/my_path',
webpack: (config) => {
return config
},
env: {
},
publicRuntimeConfig: {
BACKEND_API_URL: process.env.BACKEND_API_URL,
CONSENT_COOKIE_NAME: 'ConsentCookie'
},
}
Is there a way to add my basepath into the endpoint that the user object is being returned from? The end result would look something like: https://localhost:3000/my_path/api/auth/me
I am not 100% certain that this will fix my issue with getting the user object returned properly, so I am open to any other suggestions and willing to add more context surrounding specific files in my app.
Edit:
After bringing this issue up on the Auth0 forums (link), I was pointed towards this link, which is another example Next.js Auth0 sample app, except they have written their frontend with TypeScript (which I am not familiar with). They are manipulating the UserContext object and resetting the ProfileURL, which is what I am after; so what would be the JavaScript equivalent to this?
The same repsonse to the Auth0 forum post I mentioned also included another link to an example function that creates a custom URL for the login. This is very close to what I am after since again, I am trying to create a custom auth URL to retrieve the User object and get rid of the 404 ... /api/auth/me not found error.
Due to my inexperience with JS, my attempts at trying to create a similar function to the example stated previously have failed, so what would this look like?
I am feeling intense bittersweet emotions after finding an insultingly simple solution to this issue.
Found in the readme.md of the NextJS-Auth0 repository...
This small snippet of code fixed all of my issues after hours of searching for a solution -
// _app.js
function App({ Component, pageProps }) {
return (
<UserProvider loginUrl="/foo/api/auth/login" profileUrl="/foo/api/auth/me">
<Component {...pageProps} />
</UserProvider>
);
}
Now to get back to wiping the tears off my desk..
I have been having this issue too. What was happening for my Next app deployed on Vercel is that all the api/auth/* routes were not working in production but everything worked locally.
I'm using the Auth0 Universal Login Experience
// package.json
...
"dependencies": {
"#auth0/nextjs-auth0": "^1.9.2",
}
...
All I had before was the function
// api/auth/[...auth0].ts
import { handleAuth } from "#auth0/nextjs-auth0";
export default handleAuth();
So what I did is create all the paths I'd need in my application in their respective files. I think Next.js was not creating the dynamic files at [...auth0].ts
// api/auth/callback.ts
import { handleCallback } from "#auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";
const callbackHandler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await handleCallback(req, res);
} catch (error) {
res.status(error.status || 400).end(error.message);
}
};
export default callbackHandler;
// api/auth/login.ts
import { handleLogin } from "#auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";
const loginHandler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await handleLogin(req, res, {
authorizationParams: {
screen_hint: "login",
},
});
} catch (error) {
res.status(error.status || 400).end(error.message);
}
};
export default loginHandler;
// api/auth/logout.ts
import { handleLogout } from "#auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";
const logoutHandler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await handleLogout(req, res);
} catch (error) {
res.status(error.status || 400).end(error.message);
}
};
export default logoutHandler;
// api/auth/me.ts
// not api/auth/profile.ts
import { handleProfile } from "#auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";
const profileHandler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await handleProfile(req, res);
} catch (error) {
res.status(error.status || 400).end(error.message);
}
};
export default profileHandler;
// api/auth/signup.ts
import { handleLogin } from "#auth0/nextjs-auth0";
import { NextApiRequest, NextApiResponse } from "next";
const signupHandler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
await handleLogin(req, res, {
authorizationParams: {
screen_hint: "signup",
},
});
} catch (error) {
res.status(error.status || 400).end(error.message);
}
};
export default signupHandler;

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