How to autologin an user if he's already logged in - vue.js

This works for me if the user isn't logged in, however the else if wont work at all even though the route path is "/login" and main.authState is true. How do I set this up properly?
router.beforeEach((to, from, next) => {
const main = useAuthStore(router.pinia);
if (to.matched.some((record) => record.meta.authRequired)) {
if (!main.authState) {
return next("/login");
}
} else if(to.matched.some((record) => !record.meta.authRequired)) {
if (to.path == "/login" && main.authState == true) {
alert(to.path) //"/login"
alert(typeof(main.authState)) //"boolean"
return next();
}
}
next();
});
async signOut() {
await auth.logout();
await localStorage.removeItem("authenticated");
await router.replace({ name: "Login" });
},

If auth is not required for some route, you should not check for authState.
If I didn't understand correctly, explain.
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.authRequired)) {
const { authState } = useAuthStore(router.pinia);
// Ensure what authState is true to proceed to next() route
authState
? next()
: next('/login');
} else {
next();
}
});

You could use a URL query param to store the redirect target, which you then access from your Login.vue component, and can set a default value to redirect to when the user accesses the /login route as an authenticated user without a predefined redirect.
router.beforeResolve(async (to, from, next) => {
if (to.matched.some((record) => record.meta.authRequired)) {
const lastMathcedRoute = to.matched[to.matched.length - 1].path || "/";
// "/" is just a fallback route
const main = useAuthStore(router.pinia);
const isAuthenticated = main.authState;
if (!isAuthenticated ) {
next({ path: "/login", query: { redirect: lastMathcedRoute } });
} else {
next();
}
} else {
next();
}
});
And then in your Login component, you just access the redirect query param if user is already authenticated or when his auth state changes. Basically you need to somehow be notified, or watch the auth state and redirect the user accordingly. So you need authState to be a ref or reactive object that you can watch.
// Login.vue
import { useRouter } from "vue-router";
setup() {
const router = useRouter();
const { authState } = useAuthStore(router.pinia);
watch(authState, () => {
if (authState) {
const redirectLocation = route.query?.redirect?.toString() || '/pannel';
// here '/panel' is the default redirect
return router.push(redirectLocation);
}
}, { immediate: true } )
}
PRO Tip: notice { immediate: true }, this makes it so that the watch immediately fires with the initial value of the authState, instead of firing just when it changes. This is helpful in the case that the authState is already true and it does not change.
You can use other mechanisms instead of a watcher, but in principle the solution is the same.

Related

How do I write tests for pages middleware (Next 12)?

I have added some logic in my pages middleware (using Next 12) and would like to add tests now but am pretty lost on how to get that started. Can someone direct me to a tutorial or resource that shows a complete example of middleware being tested?
Specifically this is what my middleware is doing:
export function middleware(request: NextRequest) {
// Redirect a user if they don't have an auth token and are not the admin role
if (request.nextUrl.pathname.startsWith('/admin')) {
const authTokenCookie = request.cookies.token;
const parsedToken = authTokenCookie ? jose.decodeJwt(authTokenCookie) : null;
const role = typeof parsedToken === 'object' ? parsedToken?.role : null;
if (!authTokenCookie || !role || role !== USER_ROLES.admin) {
return NextResponse.redirect(new URL('/', request.url));
}
}
// Redirect the user if a query parameter is present
if (request.nextUrl.pathname === '/' && request.nextUrl.searchParams.has('directToStore')) {
NextResponse.redirect(new URL('/store', request.url));
}
return NextResponse.next();
}
This is how I ended up testing my middleware:
import { middleware } from '../pages/_middleware';
import { NextResponse, NextRequest } from 'next/server';
import * as jose from 'jose';
describe('Middleware', () => {
const redirectSpy = jest.spyOn(NextResponse, 'redirect');
afterEach(() => {
redirectSpy.mockReset();
});
it('should redirect to the homepage if visiting an admin page as a user without an auth token', async () => {
const req = new NextRequest(new Request('https://www.whatever.com/admin/check-it-out'), {});
req.headers.set(
'cookie',
'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTg3NjczMjYsImV4cCI6MTY5MDMwMzMyNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIn0.G7rkptAKt1icBp92KcHYpGdcWOnn4gO8vWiCMtIHc0c;',
);
const { role } = jose.decodeJwt(req.cookies.token);
await middleware(req);
expect(role).toEqual('user');
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/', req.url));
});
it('should redirect to the store if the directToStore query param is set', async () => {
const req = new NextRequest(new Request('https://www.whatever.com'), {});
req.nextUrl.searchParams.set('directToStore', 'true');
await middleware(req);
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/store', req.url));
});
});

Vue router 4 and beforeEach

I added this code in my router:
Router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login']
const authRequired = !publicPages.includes(to.path)
const user = <Utilisateur>JSON.parse(<string>localStorage.getItem('user'))
// console.log('user', user, authRequired, to, from, next)
if (authRequired && !user && to.path !== '/connexion') {
// console.log('redirect')
next({ name: 'connexion', query: { from: to.fullPath } })
}
next()
})
This warning message is displayed when the redirection is triggered, do you know how I can solve this problem?
|Vue Router warn]: The "next" callback was called more than once in one navigation guard when going from "/" to "/". It should be called exactly one time in each navigation guard. This will fail in production.
You should add else block that runs next():
Router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login']
const authRequired = !publicPages.includes(to.path)
const user = <Utilisateur>JSON.parse(<string>localStorage.getItem('user'))
// console.log('user', user, authRequired, to, from, next)
if (authRequired && !user && to.path !== '/connexion') {
// console.log('redirect')
next({ name: 'connexion', query: { from: to.fullPath } })
} else{
next()
}
})
for more details please check this

How to know the source of route redirection in Vue.js?

I have a navigation guard in place (main.js), which redirects certain routes if a condition is met:
router.beforeEach((to, from, next) => {
if (conditionMet)
next('/another-route');
else
next();
})
Now how can I know which route was redirected to /another-route?
The from object in, In-component navigation guard of /another-route doesn't point to the actual referrer, instead it points to the route which referred the redirected route. Sounds confusing?
In simple terms, If route A had a button which on click goes to route B and then route B is redirected to route C. The from object in C has details of A instead of B.
How do I know which route was actually redirected to C?
beforeRouteEnter(to, from, next) {
console.log(from);
/* */
next();
}
Any help would be appreciated.
You can't do it that way. But you can do a workaround by using query params in /another-route that points to route B. So it will be like /another-route?next=%2Froute-b. And after that, you can just use this.$router.redirect(this.$route.query.next)
This how I do it in my program if unauthorized user accessing some pages, e.g /customer/1. I use r for the query params.
beforeEnter: (to, from, next) => {
let access_token = Vue.cookie.get('access_token')
if (access_token == null) {
// user doesn't have access token, redirect to login
if (from.name !== 'login') { // prevent append /r if from is login page itself
next({ name: 'login', query: { r: to.fullPath } })
}
next({ name: 'login' })
} else {
// user has access token, user can open the page
next()
}
},
The link will become /login?r=%2Fcustomer%2F1
And in login.vue
onSubmit () {
this.submitting = true
let { username, password } = this
this.$api.POST('auth/login', { username, password })
.then(response => {
let { id, access_token } = response.data
this.setCookie(id, access_token)
this.$router.replace(this.$route.query.r)
})
.catch(error => {
if (error.response && error.response.status === 401) {
// show error that username or password is invalid
}
})
.finally(() => {
this.submitting = false
})
},

Vuejs Router guard works unexpected

I have router which work with errors and can't understand how to fix it.
This global router which should check jwt token expiration and handle routing. Everything worked fine before adding some functionality like isActivated account. So now I need to check if User has token and if User account is activated.
1) If user has token it should make next() otherwise next("/login") (redirect)
2) If user has token but his account is not activated yet (first time login), it should redirect on Setup page next("/setup") until he submits some information.
So this is my guard
router.beforeEach((to, from, next) => {
const token = localStorage.getItem("token");
const tokenExp = parseInt(localStorage.getItem("tokenExp"))
const isActivated = localStorage.getItem("isActivated")
const now = new Date().getTime() + 129600000
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
console.log("first")
if (requiresAuth && !token) {
next('/login');
} else if (requiresAuth && token) {
if (now > tokenExp) {
axios.post("/user/t/r", token)
.then(e => {
const token = e.headers['authorization'].replace("Bearer ", "");
localStorage.setItem("token", token);
localStorage.setItem("tokenExp", (new Date().getTime() + 172800000).toString())
if (isActivated === 'true') {
next()
} else {
next("/setup")
}
})
.catch(e => {
localStorage.removeItem("token")
localStorage.removeItem("tokenExp")
localStorage.removeItem("fullName")
localStorage.removeItem("role")
next('/login')
})
} else {
console.log("second")
if (isActivated === 'true') {
console.log("third")
next();
} else {
console.log("fourth")
next("/setup")
}
}
} else {
next();
}
})
And this is my console.log with error when I login:
You are infinitely redirecting to /setup, You code on first run hits "fourth" then sends the user to /setup where that before call is run again and your infinite loop starts.
You need to stop calling next('/setup') or next('/login') if the user is already on that page.
You need to make use of router.currentRoute in order to check you are not going to redirect to page they are already on.
https://router.vuejs.org/api/#router-currentroute

Vue Router - How to stop infinite loop in router.beforeEach()

I have this block of code here.
Router.beforeEach((to, from, next) => {
// AUTH = firebase.auth()
// it allows me to get the auth
// status of the user
AUTH.onAuthStateChanged(user => {
if (to.matched.some(m => m.meta.auth) && user) {
// I am checking if it is the user's first
// time to login so I can mandatory redirect
// them to the setup page.
isFirstLogin().then(firstLogin => {
if (firstLogin) {
next({ name: 'first-login' }) // this causes infinite loop
} else {
next()
}
});
} else {
next({ name: 'logout' }); // this causes infinite loop
}
})
})
However this line next({ name: 'first-login' }) causes infinite loop.
How do I prevent this? Thanks.