I'm new to Firebase, and I'm trying to implement authentication from an angular 7 application.
Here is my Authentication service:
#Injectable({
providedIn: 'root'
})
export class AuthService {
private user: Observable<firebase.User>;
private userDetails: firebase.User;
constructor(private angularFireAuth: AngularFireAuth) {
this.user = this.angularFireAuth.user;
this.user.subscribe(
(user) => {
if (user) {
this.userDetails = user;
}
else {
this.userDetails = null;
}
}
);
}
signInGoogleLogin() {
return this.angularFireAuth.auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL)
.then(() =>
this.angularFireAuth.auth.signInWithPopup(
new firebase.auth.GoogleAuthProvider()
)
);
}
isLoggedIn(): boolean {
return this.userDetails != null;
}
}
And here is my AuthGuard implementation:
#Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService: AuthService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<boolean> | Promise<boolean> | boolean {
if (this.authService.isLoggedIn()) {
return true;
}
this.router.navigate(['login'], { queryParams: { returnUrl: state.url}});
return false;
}
}
My problem is: the persistence does not seem to work. Whenever I refresh the page, I have to log in, whenever I'm navigating to another component that needs authentication, I need to log in again.
Of course, if I use "signInWithRedirect" instead of "signInWithPopup" I fall into a logging loop where I get redirected to my login page which finds that I'm not logged in, then try to log me, redirects me to my login page which finds I'm not logged in, and so on.
I think all these problems are actually related to the same problem: my auth state persistence implementation is somewhat wrong.
So my question is really simple: what am I doing wrong ? :)
I'd like to be able to log in, and then stay logged in when a refresh occurs.
Thank you for your help. :)
If anyone comes here looking for an answer this is how I did it
auth.service.ts
import { auth, firestore } from 'firebase/app';
constructor(
private _fAuth: AngularFireAuth,
) {}
public async login(authInfo: UserAuthInfo) {
if(authInfo.rememberMe) {
await this._fAuth.setPersistence(auth.Auth.Persistence.LOCAL)
console.log("local persistance", true);
}
const credential = await this._fAuth.signInWithEmailAndPassword(authInfo.username, authInfo.pass);
...
}
auth.guard.ts
export class AuthGuard implements CanActivate {
constructor(
private _fAuth: AngularFireAuth,
private _router: Router
) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this._authService.fAuth.authState.pipe(
first(),
map(user => !!user),
tap(authenticated => {
console.log("auth guard loggedin", authenticated);
authenticated || this._router.parseUrl('/auth/login')
})
)
}
}
Related
I am able to get the current logged in user object in the service.ts with this const loggedin = this.request.user
export class PostService {
constructor(
#Inject(REQUEST) private request: Request,
private userService: UserService
) { }
async GetPosts(){
const loggedin = this.request.user
loggedin.firstname;
}
}
But I cant access the properites of the current user
I tried loggedin.firstname but I got Property 'firstname' does not exist on type 'User'
How can I fix this?
I solve the issue,
export class PostService {
constructor(
#Inject(REQUEST) private request: any,
private userService: UserService
) { }
async GetPosts(){
const loggedin = this.request.user
}
}
I changed the request type to any and I destructure it like this
const { _id, firstname } = this.request.user
I Need to implement 'search' by passing queryParams through route from the search component to the userList component (example. /search-result?user="Alfred"). Before loading the userList component, i need to make an API call using the queryParams in the userList resolver but the query params keeps showing undefined.
Search Component
search(searchTerm: string) {
if (searchTerm) {
this.router.navigate(['search-result'], { queryParams: { user: searchTerm } });
}
}
UserList Resolver
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(): Observable<User[]> {
const searchTerm: string = this.route.snapshot.queryParams['user'];
console.log(searchTerm); //Logs Undefined
return this.userService.getUsers(searchTerm);
}
}
On latest versions of Angular you can get the ActivatedRouteSnapshot on the resolver function.
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(**route: ActivatedRouteSnapshot**): Observable<User[]> {
**console.log(route.queryParams)**
return this.userService.getUsers(searchTerm);
}
}
Maybe the resolve function is running before the queryParams are populated in the url. Try doing it in an Rxjs way.
import { filter, map, switchMap, tap } from 'rxjs/operators';
...
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService, private route: ActivatedRoute) { }
resolve(): Observable<User[]> {
return this.route.queryParams.pipe(
tap(params => console.log(`Params: ${params}`)),
// wait until params has user in it
filter(params => !!params['user']),
tap(params => console.log('after filter')),
// extract the value of the user param
map(params => params['user']),
// switch to a new observable stream once we know the searchTerm
switchMap(searchTerm => this.userService.getUsers(searchTerm)),
);
}
}
Edit
Use the tap operator to debug the stream. See what the log is and make sure console.log(Params: ${params}) has the user params.
Edit2
Try
this.router.navigateByUrl(`/search-result?user=${searchTerm}`);
, I am thinking there is something wrong with how you navigate.
Edit 3
I am thinking queryParams can only be read when the component itself loads and not at the run time of the route resolvers because it is saying, I need to go to the route of search-result, give me the data before I go to search-result and it is independent of the queryParams. To fix this, I followed this guide (https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html).
1.) In app-routing-module.ts, change the registration of the path to:
{ path: 'search-result/:user', component: UserListComponent, resolve: { users: UserResolver } },
Now the user will be the parameter we are after in the URL.
2.) In search.component.ts, change search to:
search(searchTerm: string) {
if (searchTerm) {
this.router.navigate([`search-result/${searchTerm}`]);
}
}
3.) In user-resolver.service.ts, change it to this:
#Injectable({
providedIn: 'root'
})
export class UserResolver implements Resolve<User[]> {
constructor(private userService: UserService) { }
resolve(route: ActivatedRouteSnapshot): Observable<User[]> {
const searchTerm: string = route.paramMap.get('user');
return this.userService.getUsers(searchTerm);
}
}
I when console logging searchTerm, it is the accurate value. Thanks for providing the StackBlitz, it helped you and me.
Is it possible to pass data from one resolver to the other on the same route?
{
path: 'book-view/:id',
component: BookViewComponent,
resolve: {
book: BookViewResolver,
user: UserResolver
}
}
Let's say I want to pass a data (uploader_id) from the data in BookViewResolver to Make an HTTP call
from my UserResolver.
BookViewResolver
export class BookViewResolver implements Resolve<any> {
constructor(private bookService: BookService) { }
resolve(route: ActivatedRouteSnapshot) {
return this.bookService.getBook(route.params['id']);
}
}
UserResolver
export class UserResolver implements Resolve<any> {
constructor(private authService: AuthService) { }
resolve(route: ActivatedRouteSnapshot) {
return this.authService.getUser(uploader_id from BookViewResolver);
}
}
Before the data gets to the final component.
No it isn't possible but I suggest you use 1 resolver containing both Book and AuthService and use RxJS's switchMap for it if ever these services emits and Observable.
Router
{
path: 'book-view/:id',
component: BookViewComponent,
resolve: {
book: BookViewResolver
}
}
BookViewResolver
export class BookViewResolver implements Resolve<any> {
constructor(
private bookService: BookService,
private authService: AuthService
) { }
resolve(route: ActivatedRouteSnapshot) {
const id = route.params['id'];
// After the BookService fetches its corresponding data, you can pass
// its response to the next call which is the authService and pass the
// uploaderId from the response data itself
return this.bookService
.getBook(id)
.pipe(switchMap(data => this.authService.getUser(data.uploaderId)))
}
}
In the template component AppComponent, depending on the value, the variable this.loggedInService.isLoggedIn switches between the logIn() and logout() methods, which in the application component AppComponent are subscribed to these methods in the service LoggedinService and depending on the method, change the value of the variable to true or false.
Also in the Guard's method checkLogin (url: string) I return true or false depending on the value of the variable this.loggedInService.isLoggedIn
Everything works, but when I reset the page, I need to keep the value of the input or output button. I try to do this in the login() and logout() methods in the service, but after reloading the page, the changes are still not saved. Help solve this problem so that the changes remain after the page reboot.
template of AppComponent:
<li class="nav-item">
<a class="btn btn-outline-success"
[class.btn-outline-success]="!this.loggedInService.isLoggedIn"
[class.btn-outline-danger]="this.loggedInService.isLoggedIn"
(click)="this.loggedInService.isLoggedIn ? logout() : logIn()">
{{this.loggedInService.isLoggedIn ? 'Exit' : 'Enter'}}
</a>
</li>
code of AppComponent:
export class AppComponent implements OnInit {
constructor(private loggedInService: LoggedinService,
private router: Router) {
}
ngOnInit() {}
logIn(): void {
this.loggedInService.login();
if (this.loggedInService.isLoggedIn) {
let redirect = this.loggedInService.redirectUrl ? this.loggedInService.redirectUrl :
'/gallery';
this.router.navigate([redirect]);
}
}
logout(): void {
this.loggedInService.logout();
this.router.navigate(['/']);
}
}
LoggedinService:
export class LoggedinService implements OnInit {
isLoggedIn: boolean = false;
redirectUrl: string;
constructor() {
}
ngOnInit() {
this.CheckAuthentication();
}
enter code here
CheckAuthentication(): boolean {
if (localStorage.getItem('login') === 'true') {
return this.isLoggedIn = true;
} else if (localStorage.getItem('login') === 'false') {
return this.isLoggedIn = false;
}
}
login() {
localStorage.setItem('login', 'true')
}
logout() {
localStorage.removeItem('login');
localStorage.setItem('login', 'false')
}
AuthGuard:
export class AuthGuard implements CanActivate {
constructor(private loggedInService: LoggedinService) {
}
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean{
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.loggedInService.isLoggedIn) {
return true;
} else {
this.loggedInService.redirectUrl = url;
return false;
}
}
}
Change is isLoggedIn to be get method base on localStorage item
export class LoggedinService implements OnInit {
redirectUrl: string;
constructor() {}
get isLoggedIn(): boolean {
return localStorage.getItem('login') ? true : false;
}
login(){
localStorage.setItem('login','true')
}
logout(){
localStorage.removeItem('login')
}
}
app.component
export class AppComponent {
constructor(private loggedInService: LoggedinService,
private router: Router) {
}
logIn(): void {
this.loggedInService.login(); // set the state as login
let redirect = this.loggedInService.redirectUrl ? this.loggedInService.redirectUrl :
'/gallery';
this.router.navigate([redirect]);
}
logout(): void {
this.loggedInService.logout(); //// set the state as logout
this.router.navigate(['/']);
}
}
stackblitz demo
I have a doubt with your code.
In LoggedInService onInit why are you calling login() and logout() directly?
this.CheckAuthentication();
this.login();
this.logout();
Doing that is adding and deleting from your localStorage. Also, you can check data in your local storage by typing localStorage in browser console.I think you should comment or remove onInit method
i have a login page and 3 more pages along with that. Now i need to do authentication function. If the authentication is undefined then it must redirected to login page, if authentication is true it must go to checkLogin function. I am not getting how to do this but i had tried in this way,in one of the pages.
ts:
import { Router, CanActivate } from '#angular/router';
#Injectable()
export class PageComponent implements CanActivate {
constructor(private router: Router) { }
canActivate() {
if (localStorage.getItem('authToken') == undefined ) {
this.router.navigate(['/login']);
}
else {
/*Checklogin*/
this.ApiService
.checklogin()
.subscribe(
user => {}
}
}
but i get an error:
Class 'PageComponent' incorrectly implements interface 'CanActivate'.
Types of property 'canActivate' are incompatible.
Type '() => void' is not assignable to type '(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => boolean | Observable<boolean> | Pr...'.
Type 'void' is not assignable to type 'boolean | Observable<boolean> | Promise<boolean>'.
I dont know what i am doing is correct or not, can anyone guide me
You have to return
boolean or Observable<boolean> or Promise<boolean> from canActivate
Also
canActivate should be something like this
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
//Your logic
}
Example
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
if (localStorage.getItem('authToken') == undefined ) {
return false;
}
else {
return new Observable<boolean>((observer)=>{
this.ApiService
.checklogin()
.subscribe((res)=>{
//Check user
if(condition){
observer.next(true);
}
else{
observer.next(false);
}
observer.complete();
});
});
}
}
CanActivate route guard expect a boolean, Observable or a Promise to be returned. Based on the value returned the page is authenticated.
In your case you can probably do something like this.
canActivate() {
if (localStorage.getItem('authToken') == undefined ) {
return false;
}
else {
return this.ApiService.checklogin()
}
}
In the implementation of checklogin you resolve/reject you promise/observable.
You can go through following article:
https://ryanchenkie.com/angular-authentication-using-route-guards