I need the nextjs middleware to run for two different paths:
First, to protect against unauthorized access which is achieved using next-auth.
Second, to redirect authorized users from certain pages e.g. if an authorized user lands on anything under the auth namespace, then redirect to them to the dashboard. (I know I can do this on the client, but its much cleaner on the middleware level, imo).
Problem is that it looks like next-auth takes over the entire middleware logic:
import { withAuth } from "next-auth/middleware"
export default withAuth(
function middleware(req) {
console.log(req.nextauth.token)
},
{
callbacks: {
authorized: ({ token }) => token?.role === "admin",
},
}
)
export const config = { matcher: ["/dashboard/:path*", "/auth/:path*"] };
Is it possible to move this logic to a regular nextjs middleware? If not, how is it possible to implement logic unrelated next-auth, but in addition to it?
Related
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "/main",
element: (
<AdminAuth redirectTo="/profile">
<Main />
</AdminAuth>
),
},
])
import React from "react";
import { Link, Navigate } from "react-router-dom";
import { isAutheticated } from "../auth";
export const AdminAuth = ({ children, redirectTo }) => {
let auth = isAutheticated().user_Role;
return auth === "admin" ? children : <Navigate to={redirectTo} />;
};
I want to prevent routing while user manually changes url in the browser. I have the "/main" as an admin route, which I'm protecting, but the issue starts when user changes his role in the local storage and tries to access `"/main". I want to prevent user from manually changing the route in their url or show error if they change manually.
EDIT: I'm protecting my route in the backend, but in the frontend I don't user to even access this.
Considering that all code on the client can be changed by the user, setting up protected routes like yours, should only be for a better user experience. You cannot and should not depend on client logic to protect data from non-admin users. This has to be done on the backend/server.
Therefore, using local storage as your business logic for identifying an admin user or not, would be highly critical, since everyone could change that. Instead it should be done by some token, for exmaple JWT (Json Web Token) using a authentication provider like AWS Cognito or similiar, or even building your own server side authetication logic.
Simply by sending prop from previous page, you can control and simply send the user back to the proper page.
I want to prevent user from manually changing the route in their url
or show error if they change manually.
There's simply just no way of knowing from inside the app how exactly the URL changed in the address bar. When a user does this the browser completely reloads the page, so the entire React app is mounted fresh. react-router can then only read what the current URL is to handle route matching and rendering.
If you are certain that you don't trust the frontend client then a solution here would be to validate the user against the backend on every route/component you want to protect each time they are navigated to.
Example implementation:
import React from "react";
import { Navigate } from "react-router-dom";
import { authService } from "../auth";
export const AdminAuth = ({ children, redirectTo, role }) => {
const [userRole, setUserRole] = React.useState();
React.useEffect(() => {
authService.validateUser()
.then(user => {
setUserRole(user.user_Role);
});
}, []);
if (userRole === undefined) {
return null; // or loading indicator/spinner/etc
}
return userRole === role
? children
: <Navigate to={redirectTo} replace />;
};
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "/main",
element: (
<AdminAuth redirectTo="/profile" role="admin">
<Main />
</AdminAuth>
),
},
]);
Note that checking with the backend for each and every route transition will incur a network cost and slow the frontend UI page transitions down. Depending on the sensitivity of the content this may be an acceptable cost. That's a decision only you or your product owners can make.
I have created a file, routes.ts that stores routes I would like to protect. This works for regular routes, such as my '/profile' route, but when I try to add a dynamic url, it does not work (a user that is not authenticated can still view these routes).
routes.ts
export const protectedRoutes = ["/profile", "/profile/[id]", "/timeline/[id]", "/"];
export const authRoutes = ["/login"];
export const publicRoutes = [];
middleware.ts
export function middleware(request: NextRequest) {
const currentUser = request.cookies.get("currentUser")?.value;
if (
protectedRoutes.includes(request.nextUrl.pathname) &&
(!currentUser || Date.now() > JSON.parse(currentUser).expiredAt)
) {
request.cookies.delete("currentUser");
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("currentUser");
return response;
}
if (authRoutes.includes(request.nextUrl.pathname) && currentUser) {
return NextResponse.redirect(new URL("/profile", request.url));
}
}```
I have logged out of my application and tried to view the dynamic routes. If my code was correct, I should have been rerouted to my login page, however, it still shows the data even though I am not authenticated. To make sure the protected routes work, I viewed my static routes, and was successfully rerouted to the login page.
Try creating a config variable in the middleware and apply the middleware on the routes you want to protect. For example you can add this to the middleware for the dynamic routes:
export const config = {
matcher: ["/profile/:path*", "/timeline/:path*"]
};
Please note if include the config in your middleware, the middleware will ONLY be applied to the routes in the matcher array. I just added the above code for the dynamic routes you had as an example.
The matcher is an array of routes to apply the middleware on. It supports the wildcard syntax so you can match a group of routes for the dynamic routes. For example, the /profile/:path* part will Apply the middleware on all the routes that starts with /profile. It will match the routes like /profile/123. Read more about config and matcher here.
I have middleware that I need to check if user authenticated every time he visits page. For now it seems like this, just to make sure if it's able to work with localStorage:
export default function({ store, redirect, route }) {
console.log('Here is auth middleware')
console.log(localStorage.getItem('token'))
}
// nuxt.config.js
plugins: [
'~/plugins/test.client.js'
],
Generally, this middleware has to work with localStorage. Here I found solution of my problem, but it works only one time (from documentation):
In universal mode, middlewares will be called server-side once (on the
first request to the Nuxt app or when page refreshes) and client-side
when navigating to further routes
So, I need to create such middleware, that is going to work only on specific pages and be able to work with localStorage with Nuxt.js app in universal mode. Is it possible to implement?
This problem can be solved using mixins. In mixins/auth.js I have this code:
import { verifyToken } from "#/api";
export default {
async mounted() {
if (!localStorage.getItem('token')) {
await this.$router.push({path: '/login'})
} else {
const checkToken = await verifyToken({ token: localStorage.getItem('token') })
if (checkToken.error) {
localStorage.removeItem('token')
await this.$router.push({path: '/login'})
}
}
}
}
And then, on page where I need to check is there is valid token in localStorage I just do this:
<script>
import auth from '~/mixins/auth'
export default {
name: "Component",
mixins: [auth],
</script>
My Nuxt.js App has this structure:
/pages/index.vue
/pages/_slug/index.vue
When user gets /{any_page}, it will use the path to build the page content:
/pages/_slug/index.vue
<template>
<div>
{{slug}}
</div>
</template>
<script>
import fetch from 'isomorphic-fetch';
export default {
async asyncData({ params }) {
return { slug: params.slug }
}
}
</script>
This works perfectly when running the Nuxt App directly via yarn dev.
When I try to run this using firebase functions:
$ firebase serve --only functions,hosting
The static routes work perfectly, but the dynamic routes always render the default / page, instead of executing the dynamic one. How do I fix this?
If using nuxt generate, then the explanation below should work. If using nuxt build, look at this repo, it is a template i wrote for nuxt + firebase as ssr.
Generate says it skips over dynamic routes, unless you explicitly specify them, hence 404 errors. In your Nuxt config file, you want to specify those routes. Here is an example of fetching from the database during generation. Note that in my example below, the doc id is the slug for the page.
nuxt.config.js
generate: {
async routes() {
const { db } = require('./services/fireInit');
const qs = await db.collection('recipes').get();
return qs.docs.map(x => `/recipes/${x.id}`);
}
},
The downside to this approach is as you add new items via a CMS, you will need to rebuild/deploy your site each time if you want to be able to use these new links without a 404. CMS without redeploy does work if you navigate to new content 'organically'...404 only occurs on a page refresh or on jumping directly to the page from a hyperlink.
I have a "controller" middleware (connected to koa-router).
I am wondering what is the best approach to organize my "internal" app logic (beyond my controller, the middleware function connected to koa-router router.post('/', createCtrl)).
First case: "Everything is a middleware". My controller simply composes the set of middleware functions required to progressively go from the initial request object to a well suited response object. (I read and pass the arguments required by the next middleware through ctx.state).
import * as view from './views'
import * as repo from './repository'
import * as response from '../../services/response'
import { sign } from '../../services/jwt'
import compose from 'koa-compose'
const createCtrl = compose([
repo.create(),
sign(),
view.sessionUser(),
response.success(201)
])
Second case: The application logic is "decoupled" completely from Koa. The controller would be a koa middleware calling none-middleware functions as follow:
import * as view from './views'
import * as repo from './repository'
import * as response from '../../services/response'
import { sign } from '../../services/jwt'
const createCtrl = async function (ctx) {
try {
const entity = await repo.create()
const token = await sign(entity.id)
const dto = view.sessionUser(token, entity)
const response = response.success(201)
response(ctx, dto) // <- this is also a middleware
} catch(err) {
ctx.throw(500, 'my not very well handled error')
}
}
Is it a good idea to think of the controller as a composition of middleware functions? Or is this a misuse of what middleware functions are meant for?
The best way, is to separate the logic from the controller. Having this separation will allow you reuse the same logic in other parts of the application and it's much easier to test it.
Please check this repository: https://github.com/Talento90/typescript-node
I have a server folder where I put all server infrastructure like controllers and routes (both separated) and then I pass my managers to the server. Managers contain the application logic and are passed to the server.
To summarise: Application logic must NEVER depend on infrastructure in this case an HTTP Server.