0

I have a 'Teams' collection in Firestore. A team document will include a team_users map field where userID: true if they are a part of that team.

I have a query in a service that returns and displays all team documents where the logged in user id matches an ID within the team_users field.

getTeamsList(user_id: string) {
  this.teamsCollection = this.afs.collection<any>(`teams`, ref => ref.where(`team_users.${user_id}`, "==", true))
  this.teams = this.teamsCollection.snapshotChanges().pipe(
  map(actions => actions.map(a => {
    const data = a.payload.doc.data();
    const id = a.payload.doc.id;
    return { id, ...data };
  }))
 ).pipe(shareReplay());
}

What I need to achieve is a route guard that performs the following:

  1. Prevents a user accessing a team they don't belong to / use to belong to. For example, if the user has a bookmark for a document whilst they were in Team A, but have since left, they should not be able to access this route and should instead be redirected back to the url of /teams
  2. If the user does not belong to any teams (the collection query doesn't produce any results), then no routes are accessible a part from the /teams listing url

Note: A user can be a part of multiple teams. If the user was a part of Team A, B and C and has bookmarks for pages in Team A, but has since left, the route guard should prevent them from accessing any pages that were a part of Team A, but still allow access to Team B and C pages.

Does this mean that:

  1. Every URL within my app will need this guard?
  2. That every URL within my app will need to contain the team ID?

Any help would be greatly appreciated.

Haven
  • 87
  • 1
  • 9

2 Answers2

2

A user is on /teams page displaying teams (You don't need a guard on this route). When a user clicks on one of the teams navigate to /teams/teamID (Now this route needs a guard to prevent a user that doesn't belong to teamID from accessing it. To set the guard do this:

  1. Write a function that checks on firestore if a user is a member of a team.

isTeamMember(userId: string, teamId: string): Observable < Boolean > {
  return this.firestore.doc < any > (`teams/${teamId}`).snapshotChanges().pipe(
    map(value => value.payload.data().team_users[userId] === true));
}
  1. Call the function in your Guard (CanActivate implemented like this). Remember to inject a service/store containing your current user and retrieve teamId from Url. The 2 are used to call the function on firestore

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable < boolean > | Promise < boolean > | boolean {
  return this.firestoreService.isTeamMember(userIdFromWhereYouStoreYourCurrentUser, teamIdFromTheCurrentUrl).pipe(map(isTeamMember => {
      if (isTeamMember) {
        return true;
      } else {
        //Add your not team member logic here, eg Stay in the current route, may be show an error message
        return false;
      }
    }),
    take(1));
}
Berchmans
  • 313
  • 2
  • 10
  • Hey @Berchmans! - Thanks for taking the time to reply. In regards to the `isTeamMember` function, wouldn't that then be a duplicate query to the one i've have shown in my service? For example, wouldn't that function perform a read request to firestore on every single URL, every single time that the user navigates anywhere? – Haven Jun 09 '20 at 07:40
  • I haven't clearly understood your comment but it's not a duplicate. Your initial function retrieves all teams that a user belongs to. You don't want to retrieve all teams when a user has clicked on only one team. The function would query firestore only for the routes that have a guard. For /teams for example it will not be called but for /teams/teamID it will. Does this answer your question? – Berchmans Jun 09 '20 at 07:48
  • Thanks Berchmans. It does. However, it does mean that this guard will be on every single route apart from the teams listing page. Looking at the firestore debug logs, my query will only run once, no matter if the user views the teams page multiple times, so I was kinda hoping to jump in on that and not have a query each time. For example, if there are 15 pages in my app, and the user spends 20 minutes going back and forth through the app, that's going to add up to a lot of queries isn't it? Again, appreciate the time. – Haven Jun 09 '20 at 07:53
  • Let's say on /teams page you have team1 and team2 displayed. When you arrive on /teams page the isTeamMember function wont be called b'se /teams has no guard. But when you click on team1 it will be called b'se /teams/team1 has a guard. If you go back to teams and reclick again on team1 or team2 it will be called again. That's to secure your route, checks must be done every time you pass through a security gate not just a first time – Berchmans Jun 09 '20 at 08:01
  • I fully get that, just wondered if there was an alternative. I appreciate the time. Is there any way of integrated the 'activatedRoute' check to get the request team param ID? Additionally, what about a `user.uid` to hand over? I usually have an async function that returns the current user ID, but that doesn't seem to work in this instance. Thanks again mate, much appreciated. – Haven Jun 09 '20 at 08:10
  • To get teamID parameter in Guard use the 'next' variable of type ActivatedRouteSnapshot like this: next.params['teamID']. For current user you have to call you async function in the guard. You will thus be having 2 async functions: isTeamMember and getCurrentUser, you will need to use one of the RxJs operators eg. combineLatest to wait for both of the functions to return before proceeding. That's a different question. – Berchmans Jun 09 '20 at 08:35
  • As a note...I think just by adding 'shareReplay' to the end of the isTeamMember function will avoid multiple queries. If the page is refreshed however, it will perform the query again. – Haven Jun 09 '20 at 09:14
0

You can do it this way: 1. Make an Auth guard , let say it "AG". 2. In this "AG", make a REST call to your backend. Sending the bookmark ID , USer ID, Current Team Id. "AG" will get triggered on route change where-ever applied. 3. This REST call will compare the current team Id and the Team Id saved with that Bookmark(Assumption: Bookmark table has the column of Team Id as foreign key). 4. So when the service return TRUE/FALSE, then "AG" will know that if it is authorized or not. 5. If it is FALSE, then route user to whatever url.

Example of sample code:

@Injectable()
export class AuthGuardService implements CanActivate 
{ 
   constructor(public auth: AuthService, public router: Router) {}  

   canActivate(): boolean 
   {
    if (!this.auth.isAuthenticated()) {
      this.router.navigate(['team']);
      return false;
    }
    return true;
    }
}

Adding the route example:

export const ROUTES: Routes = [
  { path: '', component: Home},
  { 
    path: 'team/{id}',
    component: TeamComponent,
    canActivate: [AuthGuard] 
  },
  { path: '**', redirectTo: '' }
];
akash verma
  • 159
  • 1
  • 15
  • Thanks Akash! - However, there might be times when I won't know the current team ID. For example, if the user has just logged in, they are sitting on a `/teams` url without a current team. At this point, ideally they will select a team to open from the list. That's when they will navigate to `teamID/anotherlist` - However, if whilst on the teams listing page, they click a bookmark for an old team, i won't know the current team at that point. – Haven Jun 08 '20 at 14:50
  • Thanks. The AuthGuard code is where I really need the help, to understand how and if I can hook it up to the query that already exists in my service. – Haven Jun 08 '20 at 14:51
  • @Haven : If you dont know the current time ID at frontend, then - when the REST call is made, you can find the Current Team ID of user at backend only. Either it will be null or will have some value. And then do the comparision – akash verma Jun 08 '20 at 14:54
  • Of course, much appreciated. However I've written guards before, but i'm specifically finding it difficult to work out the code for this one. The code in your answer, I fully understand, but I feel it's missing the interaction with my current firestore query. – Haven Jun 08 '20 at 14:58
  • @Haven: You need to write a service that will hit a http request. In the above code of "AG", there is the object "auth" of service in constructor.https://angular.io/guide/http – akash verma Jun 08 '20 at 15:36