Raold
Raold

Reputation: 1493

User access for specific routes with parameters

I am using Angular with firebase. In my app users can create "projects" and invite another users to that "project" in other words one project can have multiple users. I have user collection and project collection in firestore.

User collection

users/userID/ -> { name: string }

in users collection are documents which have id value and data (just name)

Project collection

project/projectID/ -> 
{
  projectName: String,
  users: [
    { 
     userID: (here id of user),
     role: String
    }
  ]

in project collection are documents which have id value and data. Data are projectName and array of users. In this array of users i have ids of users that can access the project.

Scenario

User signIn to my app, now he is in simple intro page at route /hello. Now user can see his projects, when he click at some project he will be routed to this path /project/:id and here he can use other functions e.g. /project/:id/overview. It's similar to firebase console https://console.firebase.google.com/u/0/project/myprojectid/authentication/users :) I hope I just clarify what i am trying to achieve. If you are still confused let me know in comments.

What's the problem?

In my project this works good, but problem is when someone copy id of different project to url and hits enter. I want to prevent users to get to others users projects so if i enter this path /project/notMyProject/overview in url, I should be redirected or something. Hmm thats easy, just create router guard I thought. So I started...

app-routing.module.ts

const routes: Routes = [
  { path: 'project/:pid', component: ProjectComponent, 
   children: [
    { path: '', component: OverviewComponent}, 
    { path: 'users', component: ProjectUsersComponent, }, 
   ],
   canActivate: [ProjectGuard, AuthGuard]
  },
  { path: '', component: PageComponent }
];

project.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../core/auth.service';
import { map } from 'rxjs/operators';
import {
 AngularFirestore,
 AngularFirestoreDocument
} from 'angularfire2/firestore';

@Injectable({
 providedIn: 'root'
})
export class ProjectGuard implements CanActivate {

private itemDoc: AngularFirestoreDocument<any>;
item: Observable<any>;

constructor(
 private auth: AuthService,
 private afs: AngularFirestore,
) {}

canActivate(
 next: ActivatedRouteSnapshot,
 state: RouterStateSnapshot): Observable<boolean> | boolean {

  this.itemDoc = this.afs.doc<any>.(`project/${next.paramMap.get('pid')}`);

  this.item = this.itemDoc.valueChanges();

  this.item.subscribe(response => {
    console.log(response)
    response.users.forEach( (element) => {
      if(element.userID == this.auth.user.subscribe( data => { return data.uid })) {
        return true;
      }
    })
  })
 }
}

and here I am. I quite new to rxjs and all that observable stuff. That code is asynchronous and I don't know how to make this work,I am getting error that I have to return Observable or boolean...

Question

Is my general approach to this specific problem correct? Is there some better options that I should consider? How other pages solve this problem e.g. firebase (try this Firebase). And if my solution is good can you help me with my project.guard?? Thank you very much

I tried to describe my problem as clear as possible. If you missing some informations, please let me know in comments and I will update my question. I searched many hours how to solve this but i could not find anything or I just use wrong search keywords. I hope this will help others too.

Upvotes: 0

Views: 161

Answers (1)

CozyAzure
CozyAzure

Reputation: 8478

There are two things that you can be sure(almost) you are doing it wrong with Observables when you see them:

  1. Doing a nested subscribe, and
  2. Assigning a results to a subsrciption aka var result == someObservables.subscribe()

And these are the two exact mistake you have in the code.

The reason mistake 1 happens is because most of the time Observables are asynchronous, and which is also your case, and your code cannot ensure the sequence the results will be back. Your code runs synchronously, and cannot deliberately wait for the execution of the async tasks. That is why operators exist (maps, filters, concatMap, etc), so that you deal with the Observables (per se), and not inside the subscription when the Observables finally emit its values.

Mistake 2 is not uncommon, as for someone who's new to Observables will always think that the results of the Observables comes from the function of the Observables - they don't. They comes in the parameter in the subscription callback function:

var thisIsSubscriptionNotResult = myObservable.subscribe(results=>{
    var thisisTheRealResult = results;
}

After understanding the two mistakes, its fairly easy to solve the issues in your code. You will need the help of combineLatest here, to combine two different observables.

canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

    this.itemDoc =  this.afs.doc<any>(`project/${next.paramMap.get('pid')}`);
    return Observable
        .combineLatest(this.itemDoc.valueChanges, this.auth.user)
        .map(([response, authUser]) => {
            return response.users.some(x => x.userID === authUser.uid);
        });
}

What the code above does is that it combines the two Observables, this.itemDoc.valueChanges and also this.auth.user. But the angular router guard needs a Observable<boolean>, so you need to map them in order to transform it to a boolean.

If you are using rxjs 6, the syntax is slightly different:

return combineLatest(this.itemDoc.valueChanges, this.authU.user)
    .pipe(
        map(([response, authUser]) => {
            return response.users.any(x => x.userID === authUser.uid);
        })
    );

Upvotes: 1

Related Questions