working with Angular (v7.2) and Firebase Auth to build a simple web app with a members area. I've set the dashboard up in a separate module with its own router. I've got a service which does most of the heavy lifting for the auth which is shared between all the components.
I'm not convinced this is the best way of doing things but my auth.service.ts file looks like this:
export class AuthService {
private userData$ = new Subject();
private userData;
public userDataObs = this.userData$.asObservable();
constructor(public afs: AngularFirestore, public afAuth: AngularFireAuth, public functions: AngularFireFunctions, public router: Router, public ngZone: NgZone) {
this.afAuth.authState.subscribe((user) => {
if(user) {
this.userData = user;
this.getCurrentUserAccessLevel().subscribe((result) => {
let userAccessLevel = result.result;
let userObj = {
displayName: this.userData.displayName,
email: this.userData.email,
emailVerified: this.userData.emailVerified,
phoneNumber: this.userData.phoneNumber,
photoURL: this.userData.photoURL,
uid: this.userData.uid,
accessLevel: userAccessLevel
}
this.userData$.next(userObj);
});
localStorage.setItem('user', JSON.stringify(this.userData));
JSON.parse(localStorage.getItem('user'));
} else {
localStorage.setItem('user', null);
JSON.parse(localStorage.getItem('user'));
}
})
}
...
In short the service fetches the user data from the Firebase auth service and stores it into a userData property. It then fetches the accessLevel which is a custom claim in Firebase auth and merges this with some fields in the userData to create a custom userObj object. The userObj is then submitted to the userData$ subject for which there is an observable userDataObs.
Each component subscribes to userDataObs.
In the main dashboard.component.ts there is:
export class DashboardComponent {
private userDataObs;
private userData;
private loaded: boolean = false;
constructor(public authService: AuthService, public router: Router, public ngZone: NgZone) {
this.authService.userDataObs.subscribe((data) => {
this.userData = data;
this.loaded = true;
})
}
}
which simply subscribes to the userDataObs in the authService. In the main dashboard component there is a menu with various routerLinks to other components which are served through a router-outlet in the component. If I click the link to another component the route changes and the component initialises but the observable does not load. There is no error message or anything - just nothing:
dashboard-profile.component.ts:
export class DashboardProfileComponent {
private userDataObs;
private userData;
private loaded: boolean = false;
private fileUploadEvent;
private fileUploadProgress;
private fileUploadURL;
constructor(private authService: AuthService, private router: Router, private uploadService: UploadService) {
console.log('constructor');
this.authService.userDataObs.subscribe((data) => {
this.userData = data;
this.loaded = true;
console.log('loaded');
});
}
If, however, I just navigate to the route, without clicking the routerLink it works fine.
I assume it's something to do with sharing the observable? Ideally I'd just like the child components to somehow inherit the observable from the dashboard.component but from what I can tell this isn't possible, hence I need to set up a new subscription.
Would appreciate any help at all! Thank you!
Related
I have my application up and running with Angular 2.1.0.
The routes are protected via router Guards, canActivate.
When pointing the browser to a protected area like "localhost:8080/customers" I get redirected to my login page just like expected.
But after a successful login, I would like to be redirected back to calling URL ("/customers" in this case).
The code for handling the login looks like this
login(event, username, password) {
event.preventDefault();
var success = this.loginService.login(username, password);
if (success) {
console.log(this.router);
this.router.navigate(['']);
} else {
console.log("Login failed, display error to user");
}
}
The problem is, I don't know how to get a hold of the calling url from inside the login method.
I did find a question (and answer) regarding this but couldn't really make any sense of it.
Angular2 Redirect After Login
There's a tutorial in the Angular Docs, Milestone 5: Route guards. One possible way to achieve this is by using your AuthGuard to check for your login status and store the url on your AuthService.
AuthGuard
import { Injectable } from '#angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '#angular/router';
import { AuthService } from './auth.service';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
}
AuthService or your LoginService
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
#Injectable()
export class AuthService {
isLoggedIn: boolean = false;
// store the URL so we can redirect after logging in
public redirectUrl: string;
constructor (
private http: Http,
private router: Router
) {}
login(username, password): Observable<boolean> {
const body = {
username,
password
};
return this.http.post('api/login', JSON.stringify(body)).map((res: Response) => {
// do whatever with your response
this.isLoggedIn = true;
if (this.redirectUrl) {
this.router.navigate([this.redirectUrl]);
this.redirectUrl = null;
}
}
}
logout(): void {
this.isLoggedIn = false;
}
}
I think this will give an idea how things work, of course you probably need to adapt to your code
This code will handle your request:
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService,
private router: Router) {
}
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.authService.isVerified
.take(1)
.map((isVerified: boolean) => {
if (!isVerified) {
this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}});
return false;
// return true;
}
return true;
});
}
}
but be aware that the URL params will not pass with the URL!!
You can find a nice tutorial here :
http://jasonwatmore.com/post/2016/12/08/angular-2-redirect-to-previous-url-after-login-with-auth-guard
The answers I saw were correct.
But the best way to answer your question is returnUrl.
like this:
export class AuthGuardService implements CanActivate {
constructor(private auth: AuthenticationService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot,
_state: import('#angular/router').RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
let isLoggedIn = false;
const idToken = next && next.queryParamMap.get('id_token');
try {
const expiresAt = idToken && JSON.parse(window.atob(idToken.split('.')[1])).exp * 1000;
if (idToken && expiresAt) {
isLoggedIn = true;
localStorage.setItem('id_token', idToken);
localStorage.setItem('expires_at', String(expiresAt));
} else {
isLoggedIn = this.auth.isLoggedIn();
}
} catch (e) {
console.error(e);
isLoggedIn = this.auth.isLoggedIn();
}
if (!isLoggedIn) {
//this section is important for you:
this.router.navigate(['/login'], { queryParams: { returnUrl: _state.url }});
}
return isLoggedIn;
}
}
This navigate create a url with returnUrl like a param, now you can read returnUrl from param.
GoodLuck.
What is the appropriate place for adding a call to initialize a global listener in Angular app?
Here is the code:
export class AuthService {
constructor(
private store: Store<fromAuth.State>,
private afAuth: AngularFireAuth
) {
this.afAuth.auth.onAuthStateChanged(payload => {
if (payload) {
const user: UserBeta = {
uid: payload.uid,
displayName: payload.displayName,
email: payload.email,
emailVerified: payload.emailVerified
};
this.store.dispatch(AuthActions.authenticated({ user }));
} else {
this.store.dispatch(AuthActions.notAuthenticated());
}
});
}
As you could see I've added it to the constructor of the AuthService but it doesn't seem right for me.
What I'm also concerning about is that the following code has two dependencies: Ngrx and AngularFireAuth.
In this case, would it be correct to move somewhere to the FirebaseModule (i.e. firebase.module.ts) and if yes, how is the call will look like?
You can add it inside ngOnInit(), from the docs:
A callback method that is invoked immediately after the default change detector has checked the directive's data-bound properties for the first time, and before any of the view or content children have been checked. It is invoked only once when the directive is instantiated.
Check here for more info:
https://angular.io/api/core/OnInit
Thank all of you for replies.
I've finally decided to introduce a new initialize() method inside the AuthService and call it inside the ngOnInit() method of the AppComponent.
auth.service.ts:
#Injectable({ providedIn: 'root' })
export class AuthService {
constructor(
private http: HttpClient,
private store: Store<fromAuth.State>,
private afAuth: AngularFireAuth
) { }
initialize() {
this.afAuth.auth.onAuthStateChanged(payload => {
if (payload) {
const user: UserBeta = {
uid: payload.uid,
displayName: payload.displayName,
email: payload.email,
emailVerified: payload.emailVerified
};
this.store.dispatch(AuthActions.authenticated({ user }));
} else {
this.store.dispatch(AuthActions.notAuthenticated());
}
});
}
}
app.component.ts:
export class AppComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit(): void {
this.authService.initialize();
}
}
Update: At my project I'm using ngrx for state management. Since AngularFireAuth also manages user information I've faced difficulty in managing the same state in multiple places, which increased the complexity, so the final solution became quite complicated. In the end, I've decided to stop using the onAuthStateChanged listener and start persisting the ngrx state locally.
I would like to have a string variable contain a value and then use it to set a reference path to my firebase database. Ex. The uid will store the variable from the routed paramMap and then used in the path to fetch the data from the database. I tried this below but it obviously didn't work.
export class HistoryComponent implements OnInit {
snaps: Observable<any[]>;
uid: string;
constructor(db: AngularFireDatabase
private ngZone: NgZone, private afs: AngularFirestore, private route:
ActivatedRoute){
this.snaps = db.list('snapity-tests/12-02-19/'+ $(uid)).valueChanges();
}
ngOnInit() {
this.route.paramMap
.subscribe(params => {
this.uid = params.get('uid');
console.log(this.uid)
})
}}
You need to move your code inside the constructor to the ngOnInit lifecycle hook, because the class constructor is always called before the onInit hook.
As stated in the Angular Docs:
A lifecycle hook that is called after Angular has initialized all data-bound properties of a directive
So, you only need to subscribe to the route params, and then, when you have the value that you want, then you can use it:
export class HistoryComponent implements OnInit {
snaps: Observable<any[]>;
uid: string;
constructor(
private db: AngularFireDatabase,
private ngZone: NgZone,
private afs: AngularFirestore,
private route: ActivatedRoute
){ }
ngOnInit() {
this.route.paramMap
.subscribe(params => {
this.uid = params.get('uid');
this.snaps = db.list('snapity-tests/12-02-19/'+ this.uid).valueChanges();
})
}}
I have an angular application which has a number of steps to complete. Each step can only be done once and must have all previous steps complete. To achieve this I have added route guards to each route. The application makes a http request on start to check the status. However the route guard canActivate method doesn't seem to be subscribing to changes.
In the below example statusService updates the status which should trigger an update in the guards.
statusService
#Injectable({
providedIn: 'root'
})
export class StatusService {
private stepOneComplete: BehaviorSubject<boolean> = new BehaviorSubject(false);
private stepTwoComplete: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(
private http: HttpClient
) { }
public getStepOneComplete(): Observable<boolean> {
return this.stepOneComplete;
};
public updateStepOneComplete(newValue: boolean): void {
this.stepOneComplete.next(newValue);
};
public initialize(): void {
this.http.get(`${apiUrl}status`)
.subscribe((data: any) => {
this.stepOneComplete(data.stepOne);
});
};
};
stepOneGuard
#Injectable()
export class StepOneGuard implements CanActivate {
constructor(
private service: StatusService,
private router: Router
) {}
canActivate(): Observable<boolean> {
return this.service.getStepOneComplete().pipe(
tap(complete => {
if(complete){
this.router.navigate(['/step-two']);
}
}),
map(complete => {
return !complete;
})
);
}
}
What I expect to happen is that after the initialize method runs and updates stepOneComplete then the router should navigate to step two. However no navigation occurs. If I put a console.log in the tap method of the guard it fires on initial load but not when stepOneComplete.next is called.
I think the answer is here:
public getStepOneComplete(): Observable<boolean> {
return this.stepOneComplete.asObservable();
}
This is what I have in my own production code, works just fine.
You should not see a guard as a singleton that controls navigation. Its only purpose is to control whether the user can access a page or not.
I suggest you to have a state service that holds the state of your "wizard", and then you would simply check it in every guard. You shouldn't even need Subjects at all.
I am trying to redirect the users to login page, if the user isn't logged in. Once the user logs, I want to take the user back to the previous page. But I am getting an error as StaticInjectorError[RouterStateSnapshot]: NullInjectorError: No provider for RouterStateSnapshot!.
Here's the code for the CanLoad functionality:
#Injectable()
export class TokenGuardService implements CanLoad {
constructor(private authService: AuthService, private router: Router, private snapshot: RouterStateSnapshot) {}
canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
if(this.authService.doesTokenExist()) {
return true;
} else {
this.router.navigate(['/login'], { queryParams: { returnUrl: this.snapshot.url }});
return false;
}
}
and here's the code my Component
export class LoginComponent implements OnInit {
constructor(private loginService: LoginService, private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
}
onLoginFormSubmit() {
//Some success scenario
if(success) {
this.router.navigate([this.returnUrl]);
}
}
Please help me resolve this issue.