Reputation: 1493
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
Reputation: 8478
There are two things that you can be sure(almost) you are doing it wrong with Observables
when you see them:
subscribe
, andsubsrciption
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