I have an application with routes guarded by an AuthGuard that implements CanActivate. Can activate checks to see if user is logged in and then checks if configuration variables are set before returning true or false. If the user is signed in but configuration variables have not been sent, AuthGuard makes an http call to retrieve configuration setup and returns true once the http call has been resolved without error (false otherwise).
The issue is that the Router is cancelling the navigation to the requested route before the configuration call has been resolved.
Below is the AuthGuard canActivate method:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
let authenticated = this.isAuthenticated();
if (authenticated) {
console.log("User authenticated, checking for configs...");
if (this.config.defined) {
console.log("Config defined!");
return true;
} else {
***** HERE ******
console.log("Config not defined, setting configs...");
this.authService.setConfig()
.take(1)
.subscribe(
config => {
// SET CONFIG VARIABLES
console.log("Config variables set");
// allow route access
return true;
},
err => {
console.error(err);
this.router.navigate(['/Login']);
return false;
}
);
this.router.navigate(['/Login']);
return false;
}
} else {
console.log("User not authenticated, back to login");
this.router.navigate(['/Login']);
return false;
}
}
So when I am logged in and the config variables are not set when I try to access a page (i.e. I'm in logical block denoted by **** HERE ****), I see in the console:
Setting config...
NavigationCancel {id: 1, url: "/", reason: ""}
NavigationStart {id: 2, url: "/Login"}
RoutesRecognized {id: 2, url: "/Login", urlAfterRedirects: "/Login", state: RouterStateSnapshot}
NavigationEnd {id: 2, url: "/Login", urlAfterRedirects: "/Login"}
Config variables set
Before the AuthGuard config http call has a chance to resolve, the navigation is cancelled and the router redirects as if the AuthGuard had returned false. I would like to find a way to have AuthGuard return its result on resolution of the http call within.
If anyone else if having this problem, I solved the issue by replacing the contents of the else block (starting with *****HERE******) with the following:
return this.authService.setConfig()
.map(
config => {
// SET CONFIG VARIABLES
console.log("Config variables set");
// allow route access
return true;
})
.catch(err => {
console.error(err);
this.router.navigate(['/Login']);
return Observable.of(false);
});
Related
So I have a /login route that renders the login view and I'm trying to make it so that if the user is already logged in, he gets redirected to another route. The problem is that when I type the url www.example.com/login, authenticated evaluates to false for some reason. This is how my code works:
Login Url:
{
path: '/login',
name: 'login',
component: Login,
beforeEnter: (to, from, next) => {
if (store.getters.authenticated) {
next({ name: "adminOrders" })
} else {
next()
}
}
},
Vuex authentication store:
import router from '../router'
import axios from 'axios'
const authentication = {
state: {
user: null
},
mutations: {
setUser(state, user){
state.user = user
localStorage.setItem('userId', user.id);
localStorage.setItem('username', user.username);
localStorage.setItem('token', user.token);
}
},
actions: {
autoLogin({commit, dispatch}){
const userId = localStorage.getItem('userId')
const username = localStorage.getItem('username')
const token = localStorage.getItem('token')
if (!token) {
return
}
let user = {
id: userId,
username: username,
token: token
}
commit('setUser', user)
axios.interceptors.request.use(function (config) {
config.headers.Authorization = 'Bearer ' + token
return config
});
},
},
getters: {
authenticated: state => {
return state.user ? true : false
}
}
}
export default authentication
And I call the autoLogin on App.vue mounted like this:
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
mounted(){
this.$store.dispatch('autoLogin');
}
}
</script>
beforeEnter is called before App is mounted, because router gets instantiated as soon as App is created.
Since you call autoLogin action from App's mounted() it's actually run after the router reads the getter from the store.
However, you could call an async action from beforeEnter, which would return whether or not there is a token in localStorage.
At a minimum, here's what would work:
// routes:
beforeEnter: async (to, from, next) => {
const hasToken = await store.dispatch('hasToken');
next(hasToken ? { name: 'adminOrders' } : undefined);
}
// store:
actions: {
hasToken() {
return !!localStorage.getItem('token')
}
// ...
}
Make sure you wipe the token out from localStorage when you get a 401 error in your axios interceptors (basically means "token expired"). If you do not clear the token from localStorage before trying to go to /login (which is what usually happens on 401), the beforeEnter will redirect to adminOrders, adminOrders will attempt to load data, data calls will return 401 as token is expired and you end up in a loop.
Alternatively, you could just get a new token on 401's and update localStorage.
Is there any examples and/or way to redirect to private dashboard on successful sign in or sign up for credentials type in next-auth?
I couldn't find any clear docs around this.
I was looking at adding redirect below but wasn't sure if it was the right approach:
callbacks.signIn = async (data, account, profile) => {
if ((account.provider === 'google' && profile.verified_email === true) || (account.type === 'credentials' && data.status === 200)) {
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
}
This can actually happen when initiating the signin. From the docs., you can pass a callback URL to the sign in. you code will look like this.
signIn(provider.id, {
callbackUrl: `${window.location.origin}/protected`,
})
With the new versions of Next.js you can do the redirect on 'getStaticProps' method like the following,
Resource : https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
export async function getStaticProps(context) {
// some imperative work..
//
if (!user) {
return {
redirect: {
destination: '/', // some destination '/dashboard' Ex,
permanent: false,
},
}
}
return {
props: {},
}
}
This is my code for requiresAuth and requiresGuest
requiresAuth : Users who satisfy this cannot go to login page or signup page ( meta for / )
requiresGuest : Users who satisfy this cannot go to / or any other page having requiresAuth meta for /login and /signup
These 2 conditions are working perfectly fine for my page
Problem :
Step-1 Lets say i have been given a url like localhost:8000/api/createapi/.......
Step-2 So currently i am not logged in, I enter the above URL and it redirects me to the log in page (which is expected)
Step-3 But when i log back in it redirects me to / (which is not ideal )
What i want :
After Step-2 When i log in it should redirect me automatically to localhost:8000/api/createapi/.......
Since that was the requested URL in Step-1
router.beforeEach((to, from, next) => {
// check for required auth guard
if (to.matched.some(record => record.meta.requiresAuth)) {
requiresAuthLogic(to, next, from)
} else if (to.matched.some(record => record.meta.requiresGuest)) {
requiresGuestLogic(to, next)
} else {
// Proceed to route
next()
}
})
function requiresAuthLogic (to:Route, next:Function) {
// check if NOT logged in
if (!isUserLoggedIn()) {
// Go to login
next({
path: '/login',
query: {
redirect: to.fullPath
}
})
} else if (isUserEmailVerified() === true) {
// Proceed to route
next()
}
}
function requiresGuestLogic (to:Route, next:Function) {
if (isUserLoggedIn() && isUserEmailVerified() === true) {
next({
path: '/',
query: {
redirect: to.fullPath
}
})
} else {
// Proceed to route
next()
}
}
If I've understood correctly you need to use the value which is being passed via the redirect parameter
This should be done in your login function if login is successful, you haven't shared your login function but something like this:
loginUser() {
this.$store.dispatch('loginUser', {
email: this.email,
password: this.password
})
.then(() => {
this.$router.push(this.$route.query.redirect)
})
}
AuthService
login(user: User){
return this.http.post<any>(`${this.API_URL}`, {
email: user.username,
password: user.password
});
}
isUserLoggedIn(){
return this.http.get<any>(`${this.API_URL}/1`);
}
logoff(){
//this.loggedIn.next(null);
this.router.navigate(['/login']);
}
LoginComponent
onSubmit(){
this.submitted = true;
this.authService.login(this.loginForm.value).subscribe(r => {
if(r.success){
this.router.navigate(['/home']);
}
});
}
Auth Guard
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any{
return this.authService.isUserLoggedIn()
.pipe(
map(res => {
if (res.isAuth){
return true;
}
this.router.navigate(['/login']);
return false;
})
);
}
LoginComponent: sends the data to an API for login (username and password). The API will return success = true if the user exists on the database;
Guard: (isUserLoggedIn()) check if user is logged in
and if the session is still alive.
Is there a better approach for user authentication in the Angular 4+?
Is this approach used in most projects?
Maybe, you could store user's data in local storage when he logins the first time. Then use a BehaviorSubject from rxjs and convert it to Observable. So every time the user has any changes (language, first name, etc), in any parts of your app you can access to that information.
So a thing like that:
Login Component
this.userService.login(username, password).subscribe(s => {
this.userService.user = s;
this.router.navigate(['/home'])
})
User Service
private userSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
readonly currentUser$: Observable<any> = this.userSubject.asObservable();
set user(user: any) {
// manipulate your user info and put it in localStorage after
this.userSubject.next(user);
}
get user() {
return this.userSubject.value ? this.userSubject.value : window.localStorage.getItem('user');
}
Auth Guard
Now in your guard, you can check the current user from the get method or you can subscribe to user observable, it's your choice
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any{
return this.userServivce.user ? true : false;
}
You can complicate your logic as you wish.
I want to navigate my user on another route after login.
I found out in documentation that something like this should work for it correctly:
this.router.navigate(['/users']);
My full method in compoenent looks like:
// imports on top
import { Component } from '#angular/core';
import { LoginService } from './login.service';
import { Router } from '#angular/router';
// method somewhere below
login(event, username, password) {
event.preventDefault();
let body = JSON.stringify({ username, password });
this.loginService.login(body)
.then(res => localStorage.setItem('token', res.msg))
.catch(error => this.error = error);
this.router.navigate(['/users']);
}
However it doesn't redirect me. Basically route stays without change and no error spotted in console.
What am I doing wrong?
Edit:
My routes looks like:
const appRoutes: Routes = [
{ path: 'login', component: LoginComponent},
// users route protected by auth guard
{ path: 'users', component: UsersComponent, canActivate: [AuthGuard] },
// { path: 'user/:id', component: HeroDetailComponent },
{ path: '**', redirectTo: 'users' }
];
My AuthGuard looks like:
export class AuthGuard implements CanActivate {
constructor(private router: Router) { }
canActivate() {
if (localStorage.getItem('token')) {
// logged in so return true
return true;
}
// not logged in so redirect to login page
this.router.navigate(['/login']);
return false;
}
}
I had a very similar problem on my post. The problem is that there is a delay in authenticating your user but router.navigate executes instantly. As the users part of your site is protected and your technically still unauthenticated, it fails the redirect.
By redirecting on the .then it waits for your user to login, and then if successful it redirects your user.
Try using this:
this.loginService.login(body)
.then(
res => localStorage.setItem('token', res.msg)
this.router.navigate(['/users']);
)
.catch(error => this.error = error);
this.router.navigate(['/users']);
This should be called after localStorage.setItem(), inside the .then() function
If it still doesn't work, may you put the code of the class in your message please