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
Related
In a vue 3 application which is using pinia, I want to achieve the following
redirect a user to the sign in page whenever a user is not authenticated
redirect a user to a verification page if the user authenticated but not verified
redirect a user to dashboard if the user is authenticated & verified
At the moment I have been able to redirect unauthenticated users to the sign in page and redirect them to the dashboard when authenticated by writing my router index.js file like this
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/signin",
name: "signin",
component: () => import("../views/SignIn.vue"),
},
{
path: "/verify",
name: "verify",
component: () => import("../views/Verification.vue"),
meta: { needsAuth: true }
},
{
path: "/dashboard",
name: "dashboard",
component: () => import("../views/UserDashboard.vue"),
meta: { needsAuth: true }
}
]
})
router.beforeEach(async (to, from, next) => {
if (to.meta.needsAuth && localStorage.getItem('accessToken') == null) next('signin')
else next()
})
export default router
and here is the method that handles signin
const loginUser = async () => {
try {
const res = await axios.post(
"https://some-api-url/login",
signin.value,
{
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
localStorage.setItem("accessToken", res.data.data.accessToken);
// redirect home
router.push("/dashboard");
} catch (error) {
error = error.response.data.message;
alert(error);
}
};
Now my challenge is the signin endpoint I am calling only return an access token but the dashboard endpoint return the verified status. How can I achieve redirect unverified users to the verification page?
I was able to redirecting an authenticated but unverified user to the verification page by adding pre-route guard to the dashboard part like this
{
path: "/dashboard",
name: "dashboard",
component: () => import("../views/UserDashboard.vue"),
meta: { needsAuth: true },
beforeEnter: (to, from, next) => {
if (localStorage.getItem('verified') == "false") next('verify')
}
}
So what happens is the global guard will check if the user is authenticated while the pre route guard will check if the user is verified. It works now but I am not sure if this is the most efficient way to do it as I have more routes that need the user to be verified.
Isn't it possible to do it inside the global guard as well? Funny I am asking a question inside a supposed answer
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.
I'm trying to implement authentification using firebase auth, the things is that when it comes to persistance i'm stuck.
I tried to use local storage as you can see below :
AuthService.ts :
import {HttpClient} from '#angular/common/http';
import { Injectable } from '#angular/core';
import {AngularFireAuth} from '#angular/fire/auth';
import {Router} from '#angular/router';
import firebase from 'firebase/app';
#Injectable({
providedIn: 'root'
})
export class AuthService {
userData: any;
constructor(private http: HttpClient, private firebaseAuth: AngularFireAuth, private router: Router) {
this.firebaseAuth.useEmulator('http://localhost:9099');
this.firebaseAuth.authState.subscribe(user => {
if (user) {
this.userData = user;
localStorage.setItem('user', JSON.stringify(this.userData));
JSON.parse(localStorage.getItem('user'));
} else {
localStorage.setItem('user', null);
JSON.parse(localStorage.getItem('user'));
}
});
this.firebaseAuth.onAuthStateChanged( user => {
console.log(user);
});
}
signIn(email: string, password: string): void {
this.firebaseAuth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).then(() => {
this.firebaseAuth.signInWithEmailAndPassword(email, password)
.then(result => {
this.router.navigate(['/dashboard/accueil']);
console.log('Nice, it worked!');
})
.catch(error => {
console.log('Something went wrong:', error.message);
});
}).catch(error => {
console.log('Something went wrong:', error.message);
});
}
signOut(): void {
this.firebaseAuth.signOut().then(() => {
localStorage.removeItem('user');
this.router.navigate(['/dashboard/connexion']);
});
}
forgetPassword(email: string): void {
this.firebaseAuth.sendPasswordResetEmail(email).then(() => {
window.alert('Password reset email sent, check your inbox.');
}).catch((error) => {
window.alert(error);
});
}
get isLoggedIn(): boolean {
const user = JSON.parse(localStorage.getItem('user'));
return (user !== null) ? true : false;
}
}
But the problem is that firebaseAuth.onAuthStateChanged return null after refreshing the page with F5 even authState. It's like onAuthStateChange is losing his last state after refresh.
Note that i'm using firebase Emulator.
app.module.ts
Firebase module has been well import
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
You can take a look at the confing here :
environnement.ts
firebase: {
apiKey: 'xxxxxxxxxxxx',
projectId: 'xxxx'
}
Maybe it's link to this error that i'm getting in the web console when i'm refreshing the page :
POST https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=xxxxxxx 400
Note that key is equal to my apiKey state in the config.
When i look closer here what the error say :
{
"error": {
"code": 400,
"message": "INVALID_ID_TOKEN",
"errors": [
{
"message": "INVALID_ID_TOKEN",
"domain": "global",
"reason": "invalid"
}
]
}
}
Any help would be greatly appreciate.
EDIT
Here what i get after refresh my page from onAuthStateChanged :
There is no 2 calls, only 1 that return false.
Firebase Auth will persist the login state by default, so you don't have to manually save the token into local storage. As Frank said, onAuthStateChanged will fire will user being null initially, but if the user is signed in, it will fire again shortly after that with the actual user value.
Following to the comment section of the accepted answer, I decided to highlight the fact that this appears to be en emulator issue.
This was driving me crazy. No matter what I did, even jumping up and down, the currentUser was always null. I saw the same behavior: onAuthStateChanged() got called only once and all I got was a null-user.
After removing my provider for the Auth-emulator, everything started to work without issues.
providers: [
// ...
{ provide: REGION, useValue: envFirebase.region},
// THE PROBLEM:
{
provide: USE_AUTH_EMULATOR,
useValue: envFirebase.useEmulators ? ['http://localhost:9099'] : undefined
},
],
I would like to display a small loading logo while the firebase authentication is retrieving a user token, before starting "for real" the single page application.
So far I have an authentication service :
constructor(
public afAuth: AngularFireAuth,
) {
this.afAuth.onAuthStateChanged(user => {
if (user) {
this.setCredentials(user)
}
})
}
setCredentials(user: firebase.User) {
return user.getIdTokenResult(true).then(idTokenResult => {
this.credentials = {
userId: idTokenResult.claims.id,
role: idTokenResult.claims.role,
token: idTokenResult.token,
};
// STARTS THE APPLICATION NOW ?
})
}
Is it possible to achieve such behavior ? I've read about APP_INITIALIZER without success. I want to avoid localstorage / session storage and instead rely solely on this initialization.
Update :
created an init function :
export function initApp(auth: AuthService, afAuth: AngularFireAuth) {
return () => {
return new Promise((resolve) => {
afAuth.user.pipe(
take(1),
).subscribe(user => {
if (user) {
auth.setCredentials(user)
.then(() => resolve())
} else {
resolve();
}
})
});
}
}
And edited AppModule providers:
providers: [
interceptorProviders /* my interceptors */,
{
provide: APP_INITIALIZER,
useFactory: initApp,
deps: [AuthService, AngularFireAuth],
multi: true
}
]
Still need to figure out how to add a waiting logo but it's another question. I'll update asap.
Answering to my own question
To summarize I wanted to make sure my token claims (role, and user id per say) associated with a firebase user were stored in my auth service before dealing with routing, because components inside these routes would use those credentials.
In the end I did not follow the APP_INITIALIZER that is not really a good solution.
Auth Service
private _credentials: BehaviorSubject<Credentials> = new BehaviorSubject<Credentials>(null);
public readonly credentials$: Observable<Credentials> = this._credentials.asObservable();
constructor(private afAuth: AngularFireAuth) {
this.afAuth.authState.subscribe(user => {
this._credentials.next(null);
if (user) {
user.getIdTokenResult().then(data => {
const credentials = {
role: data.claims.role,
token: data.token,
userId: data.claims.userId
}
this._credentials.next(credentials);
console.log(credentials);
})
} else {
this._credentials.next({role: null, token: null, userId: null});
}
})
}
get credentials(): Credentials {
return this._credentials.value;
}
Display a waiting spinner in app.component
Below prevents routes from displaying if credentials not set.
In the template :
<div *ngIf="!(credentials$ | async)" class="logged-wrapper">
<div class="spinner-wrapper">
<mat-spinner class="spinner"></mat-spinner>
</div>
</div>
<router-outlet *ngIf="(credentials$ | async)"></router-outlet>
In the component :
credentials$: Observable<any>;
constructor(
private auth: AuthService,
) {
this.credentials$ = this.auth.credentials$;
}
Auth Guard
The takewhile allows me to make sure my credentials are set before going further.
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot):Promise<boolean> {
return new Promise((resolve) => {
this.auth.credentials$.pipe(
takeWhile(credentials => credentials === null),
).subscribe({
complete: () => {
const credentials = this.auth.credentials
if (!credentials.role) {
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } })
resolve(false);
}
if (next.data.roles && next.data.roles.indexOf(credentials.role) === -1) {
this.router.navigate(['/']);
resolve(false);
}
resolve(true)
}
})
})
}
You should use your authentication service in a CanActivate router guard: https://angular.io/api/router/CanActivate
This means your AppModule will initially load and then your child route (ex. MainModule with router path '') has the guard. Then in AppModule you can check for the status of the service and show a loading information until MainModule is activated (when firebase auth is finished)
I'm building an authentication in angular. When logged in, the user should be redirected. My problem is that the router.navigate() function doesn't seem to work...
This is the code that doesn't work:
async login() {
if(!this.email || !this.password) {
return;
}
this.showSpinner = true;
this.authService.login(this.email, this.password).then(
(data) => {
this.router.navigate(['/chore/show']);
console.log(this.router);
},
(error) => {
this.error = error.message;
this.showSpinner = false;
}
);
}
The console.log does show when the login succeeds, but it won't navigate to the chore/show route.
app routes:
const routes: Routes = [
{ path: '', redirectTo: 'auth', pathMatch: 'full' },
{ path: 'auth', loadChildren: './modules/auth/auth.module#AuthModule' },
{ path: 'chore', loadChildren:
'./modules/chore/chore.module#ChoreModule', canActivate: [AuthGuard] }
];
auth module routes:
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent},
{ path: '', redirectTo: 'login', pathMatch: "full"}
];
and the chore module routing:
const routes: Routes = [
{ path: 'show', component: ShowChoreComponent},
{ path: '', redirectTo: 'show', pathMatch: 'full' },
];
For some reason i can't get it to redirect after login. Any suggestions?
EDIT: Added app routes. Code is on https://github.com/tomgelmers/stack
Checking your Code on Github, i would say the problem is that the navigation gets blocked by your AuthGuard.
The AuthGuard is calling the isLoggedIn function of your authService, which checks localstorage for userdata, while the authState subscriber is still writing to the localstorage.
This kind of thing can either work or don't work, it is pretty much luck and how fast the localstorage api works.
I recommend you check in the isLoggedIn function the variable user of your AuthService instead of localstorage. You can check localstorage in case the user variable is undefined.
That should be relatively save, however i don't know what the timing is between the login function returning and the authState subscriber firing. You might want to set the user variable in the login function. the signInWithEmailAndPassword function does return a Promise with Credentials that has a user property on it.
You could change the login function to
async login(email: string, password: string) {
return this.afAuth.signInWithEmailAndPassword(email, password).then(userData => {
this.user = userData.user
return userData
}
}
And the isLoggedIn function to
get isLoggedIn(): boolean {
if(this.user) return !!this.user
const user = JSON.parse(localStorage.getItem('user'));
return user !== null;
}
Oh and for completions sake, you should probably set the user variable to undefined or null in your logout function. Even if the user logs out, the user data still hangs around in memory