Firestore query not fetching latest data - javascript

I have made a route guard in Angular, that checks for a certain value in firestore before allowing access to said route.
The component is routed to once a HTTP cloud function has completed. This cloud function creates an object in a firestore document called agreement, which the route then listens to the status within this object, to decide whether to allow access or not. The issue we are having though, is that maybe 9/10 times, the guard seems to think that the agreement object does not exist in the document, and therefore throws an error. (Though I can see the agreement object there in firestore!).
I have logged the flow, and it correctly logs within the subscription, then within the guard, so I know that the HTTP function is completing before trying to route and calling the guard class.
This is the guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges();
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
})
);
}
GetTenancyOfferById:
this.tenancyOfferCollection = afs.collection<any>('tenancy_offers');
getTenancyOfferById(id: string) {
return this.tenancyOfferCollection.doc(id);
}
the error is coming from this part: tenancyOffer.agreement.status === 'incomplete', saying TypeError: Cannot read property 'status' of undefined.
The HTTP function I'm calling is here, with the routing to this component in the subscribe:
landlordAcceptReferences() {
this.hasAcceptedRefs = true;
this.processingAcceptRefs = true;
this._references.landlordAcceptReferences(this.offerId).pipe(
finalize(() => {
this.processingAcceptRefs = false;
})
).subscribe(e => {
console.log({e});
this.dref.close();
this._toastr.success(`You have accepted tenant references`);
this._router.navigate(['../../', Landlord.manage.applications.agreement.base, this.offerId], {relativeTo: this._activatedRoute});
}, () => this.hasAcceptedRefs = false);
}
I made the response of the cloud function the entire firestore document, and that HAS the agreement object within it which is logged console.log({e});. I then log (AFTER) in the guard, the value of the document, and it does not exist, even though this is where I make another call to firestore?
Does anyone have any ideas why the getTenancyOfferById(route.params.offerId) function within the guard, is not returning the latest version of this document from firestore, even though the cloud function needs to end before routing in the subscription?

I guess this.tenancyOfferCollection = afs.collection<any>('tenancy_offers'); is defined somewhere in your service. If this is true, the collection will be loaded as soon as your application is loaded.
I recommend to store tenancy_offers values in your service and read it from your guard.
Service:
export class YourService {
private tenancyOfferMap: Map<string, any> = new Map();
constructor(
private afs: AngularFirestore,
) {
afs.collection<any>('tenancy_offers').snapshotChanges()
.subscribe( actions => {
actions.map( action => {
this.tenancyOfferMap.set(action.payload.doc.id, action.payload.doc.data());
});
});
}
getTenancyOfferById(id: string) {
return this.tenancyOfferMap.get(id);
}
}
Guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const tenancyOffer = this.yourService.getTenancyOfferById(route.params.offerId);
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
return this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
}

It may depend on the fact that your stream does not complete.
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges()
.pipe(take(1)); // <-- ensure your stream completes in guards
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer && tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
return false; // <-- always return something in guards
}
})
);
}

Related

Reset BehaviorSubject from another component

I've made a facade service to avoid multiple calls to the API.
It call retrieveMyUser each time the request is made.
If the request has never been made it store the value usingBehaviorSubject. If it has already been made it take the value stored.
I want to clear the data of my BehaviorSubject in auth.service.ts when a user logout. My try to do that is that I call a clearUser() method from facade-service.ts.
facade-service.ts :
...
export class UserServiceFacade extends UserService {
public readonly user = new BehaviorSubject(null);
retrieveMyUser() {
console.log(this.user.value);
return this.user.pipe(
startWith(this.user.value),
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
)
}
private getUserFromServer() {
return super.retrieveMyUser(null, environment.liveMode).pipe(tap(user => this.storeUser(user)));
}
public clearUser() {
console.log("cleared");
this.storeUser(null)
console.log(this.user.value); // Output null
}
private storeUser(user: V2UserOutput) {
this.user.next(user);
}
}
auth.service.ts :
...
logout() {
var cognitoUser = this.userPool.getCurrentUser();
if (cognitoUser) {
this.userServiceFacade.clearUser()
cognitoUser.signOut();
}
this._router.navigate(['/login']);
}
...
The method clearUser() in auth.service.ts is well called and print cleared correctly.
But when I login, after I logout the console.log(this.user.value); in retrieveMyUser still output the previous value. It was null when at logout though.
So, how do I clear BehaviorSubject cache or to reset BehaviorSubject from another service ?
There are many things in your code which sound weird at reading:
You shouldn't access immediately to the value of a BehaviorSubject without using the asObservable() as recommended by ESLint here.
Instead, you could use another variable which will keep the latest value for the user.
You should use the power of TypeScript in order to help you with types definition and quality code (in my opinion).
The use of a BehaviorSubject with a startWith operator can be simplified using a ReplaySubject with a bufferSize of 1 (replay the latest change)
Your subject acting like a source storage should be private in order to limit the accessibility from outside.
I took your code and make some updates from what I said above:
export class UserServiceFacade extends UserService {
private _user: V2UserOutput;
private readonly _userSource = new ReplaySubject<V2UserOutput>(1);
public get user(): V2UserOutput { // Use for accessing to the user data without the use of an observable.
return this._user;
}
constructor() {
super();
this.clearUser(); // It will make your ReplaySubject as "alive".
}
public retrieveMyUser$(): Observable<V2UserOutput> {
return this._userSource.asObservable()
.pipe(
switchMap(user => (user ? of(user) : this.getUserFromServer())),
take(1)
);
}
private getUserFromServer(): Observable<V2UserOutput> {
return super.retrieveMyUser(null, 'environment.liveMode')
.pipe(
tap(user => this.storeUser(user))
);
}
public clearUser() {
console.log('cleared');
this.storeUser(null);
}
private storeUser(user: V2UserOutput) {
this._user = user;
this._userSource.next(user);
}
}
Cheers!

Reactive Form , check if username exists

I have a problem in Ionic/Firebase with a value of validator in a reactive form. In particular I have this 2 function below that check if a username in a realtime database of firebase exists. The 2 functions return a Promise boolean:
export class UsernameValidator {
static async getSnapshot(fc: FormControl){
let isPresent:boolean = false;
await firebase.database().ref().child("users").orderByChild("username")
.equalTo(fc.value)
.once("value", snapshot => {
}).then((data)=> {
if(data.exists())
isPresent = true;
else
isPresent = false;
});
console.log(isPresent);
return isPresent;
}
static async validUsername(fc: FormControl){
try{
let present:boolean =await UsernameValidator.getSnapshot(fc)
if(present==true)
return (null);
else{
return ({validUsername: true});
}
}catch(e){
console.log(e)
}
}
Then , I have I class in which I define a FormGroup and validator:
constructor(private route: ActivatedRoute, private router: Router,
public pfService: ProfileService, public fb: FormBuilder,
public authService: AuthenticationService)
{
this.id = this.authService.userData.uid;
//Underscore and dot can't be next to each other (e.g user_.name).
//Underscore or dot can't be used multiple times in a row (e.g user__name / user..name).
this.validPattern = "^(?=.{6,20}$)(?!.*[_.]{2})[a-z0-9._]+$";
this.validPatternName = "^[a-z]{3,10}$";
this.userForm = fb.group({
txtUsername: ["",[Validators.required,Validators.pattern(this.validPattern),
UsernameValidator.validUsername]],
txtName: ["",[Validators.required,Validators.pattern(this.validPatternName)]],
});
this.userForm .valueChanges.subscribe(()=> {
console.log(this.userForm.getError('validUsername'))
})
};
The problem is that validUsername , from the console, is always null regardless of the value of isPresent, also when isPresent is false. How can I fix this?
You were close, but you've mixed different syntaxes in your attempts to fix the problem which has lead to confusion.
One other thing that is leading you into trouble, is confusing the two different types of DataSnapshot.
For a direct reference (e.g. database().ref("path/to/data")), you can use exists() and val() to get information about that location's data.
For a queried reference (e.g. database().ref("path/to/group").orderByChild("name").equalTo("Tim's Group")), the data is returned as a list where you can use numChildren() to get the number of matching results, hasChildren() to see if there are any results (similar to exists()) and you can iterate through the results using forEach().
static async isUsernameTaken(fc: FormControl) { // renamed from getSnapshot()
return firebase.database().ref() // note the return here
.child("users")
.orderByChild("username")
.equalTo(fc.value)
.once("value")
.then((querySnapshot) => querySnapshot.hasChildren());
}
However, I do not recommend searching /users for just usernames as it means that your user's data is world-readable and it's also inefficient. Instead you should create an index in your database that contains only usernames.
"/usernames": {
"bob": "userId1",
"frank": "userId2",
"c00lguy": "userId3"
}
If you secure this using these Realtime Database Security Rules, you can also make use of the following simple functions.
{
"usernames": {
"$username": {
// world readable
".read": true,
// must be logged in to edit, you can only claim free usernames OR delete owned usernames
".write": "auth != null && (!newData.exists() || auth.uid == newData.val()) && (!data.exists() || data.val() == auth.uid)",
// strings only
".validate": "newData.isString()",
}
}
}
To check if a username is available:
static async isUsernameTaken(fc: FormControl) {
return firebase.database().ref()
.child("usernames")
.child(fc.value)
.once("value")
.then((dataSnapshot) => dataSnapshot.exists());
}
To claim a username (if the write itself fails, assume the username is taken):
static async claimUsername(fc: FormControl) {
const user = firebase.auth().currentUser;
if (!user) {
throw new Error("You need to login first!")
}
return firebase.database().ref()
.child("usernames")
.child(fc.value)
.set(user.uid);
}

Type-GraphQL: Allow document owner and admin/moderator access with the #Authorized() decorator

Details
I am using type-graphql and are trying to restrict data access or actions for a specific group of users with the #Authorized() decorator and a custom auth checker as a guard. Everything is working and access is blocked/allowed appropriately according to the decorator.
Problem
I want the owner/author of the document to be able to edit/delete it even if its marked with #Authorized(["MODERATOR", "ADMIN"]) optionally I could mark it with something like #Authorized(["OWNER", "MODERATOR", "ADMIN"]) if that makes it easier to implement.
As far as I know I dont have any information as to what Model/document the user is trying to access in the auth checker only the arguments of the mutation. In other words I have the ID of what they want to access but, not the Model it belongs to.
Question
Is there any way I can check if the user owns the document in the auth checker or will I have to mark it as #Authorized() and check if the user owns the document or is an admin/moderator in every mutation/query?
Code
index.d.ts
declare module "type-graphql" {
function Authorized(): MethodAndPropDecorator;
function Authorized(roles: AccessLevels[]): MethodAndPropDecorator;
function Authorized(...roles: AccessLevels[]): MethodAndPropDecorator;
}
types.ts
type AccessLevels = "MODERATOR" | "ADMIN";
authChecker.ts
const authChecker: AuthChecker<{ user: User | null }, AccessLevels> = ({ context }, roles): boolean => {
if (!context.user) {
return false;
}
if (roles.length === 0) {
return true;
}
if (context.user.accessLevel > 0 && roles.includes("MODERATOR")) {
return true;
}
return context.user.accessLevel > 1 && roles.includes("ADMIN");
};
EstablishmentResolver.ts
#Authorized(["MODERATOR", "ADMIN"])
#Mutation(() => EstablishmentType, { description: "Deletes a establishment by ID" })
async deleteEstablishment(#Args() { id }: EstablishmentIdArg): Promise<Establishment> {
const establishment = await Establishment.findOne({ where: { id } });
if (!establishment) {
throw new Error("Establishment does not exist");
}
await establishment.destroy();
return establishment;
}
Try to modify the signature and logic to allow passing a callback into the decorator, that will resolve the "owner" condition:
#ObjectType
class User {
#Authorized({
roles: ["MODERATOR", "ADMIN"],
owner: ({ root, context }) => {
return root.id === context.user.id;
},
})
#Field()
email: string;
}
Be aware that you can't always do that inside the authChecker or middleware as they run before your resolver code, so in case of deleteEstablishment there's no universal way to match establishmentId with user.id to detect if it's an owner or not.

Shared service with BehaviorSubject not emitting correct value

I am trying to implement a shared service for managing Roles on my app, with an Observable so that, from other components, you can either change the current role and be notified when it changes. The problem I have is that when I publish a new value through the service, the components that subscribe to the observable always recieve the same value (the initial one). Then, I never receive the new role number and I can't update the component state.
Apparently
I have the following set of components:
RolesService: The shared Service, which manages role change, gets the available roles from the user token, manages persistence of the current role for the logged in user. It uses localStorage to persist the role index. It receives changes
HeaderComponent: This is an example of a component receiving changes for the role change, because it needs to update the title of the user. It subscribes to the observable and changes the title accordingly
EscullRolComponent: And this is an example of a component that changes the role the user is currently using (by action of the user, of course). It has some buttons and sends to the service the new index.
Here is the relevant code for this issue:
// RolesService file
#Injectable()
export class RolesService {
private _currentRole: BehaviorSubject<Rol> = new BehaviorSubject(null);
currentRole = this._currentRole.asObservable();
private get currentIndex(): number {
const ras = localStorage.getItem('current_role');
// console.log("Guardat aixo: ", ras);
if (ras === '' || ras === null || ras === 'NaN' || ras === '-1' || parseInt(ras, 10) === NaN) {
return 0;
} else {
return parseInt(ras, 10);
}
}
private set currentIndex(val) {
localStorage.setItem('current_role', val.toString());
}
currentToken: NbAuthJWTToken;
constructor(private authService: NbAuthService,
private http: HttpClient) {
// console.log(this.currentRole);
this.authService.getToken().subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
this.currentToken = token;
console.log("Executing token change");
this.setRolActual(0);
}
});
}
protected publishCurrentRol(i: number): void {
console.log("Publishing rol id: ", i); // LOG 1
this._currentRole.next(this.getUserRoles()[i]);
}
setRolActual(i: number) {
this.publishCurrentRol(i);
this.currentIndex = i;
}
}
The following is the component the user has to change the role, and that calls the service with the new role.
#Component({
templateUrl: 'escull-rol.component.html',
styleUrls: ['escull-rol.component.scss'],
})
export class EscullRolComponent {
rols: Array<Rol> = [];
actual: number;
constructor( private rolesService: RolesService,
private route: ActivatedRoute,
private router: Router,
private location: Location ) {
this.rols = this.rolesService.getUserRoles();
this.actual = this.rolesService.getRolActualIndex();
}
buttonRolClicked(index: number) {
this.rolesService.setRolActual(index);
this.router.navigate(['inici']);
// console.log('Boto del rol ' + index + ' clicat');
}
}
And here the header, which changes its state depending on the role:
#Component({
selector: 'ngx-header',
styleUrls: ['./header.component.scss'],
templateUrl: './header.component.html',
})
export class HeaderComponent implements OnInit {
#Input() position = 'normal';
user: any = {};
picture: string;
client: stream.Client;
logoPath = '';
logoEra = '';
rol: string;
ids: Array<string>;
constructor(private sidebarService: NbSidebarService,
/* ... more injections .. */
private imatgesService: ImatgesService,
private notificacionsService: NotificacionsService) {
this.logoEra = 'assets/images/logoEra.png';
this.authService.onTokenChange()
.subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
if (token.getValue()) {
this.user = token.getPayload(); // Posem les dades del token a l'objecte user
// console.log('token changed, changing user in header', token);
}
}
}, error => {
console.error('Error en carregar el token en el header');
throw error;
});
this.rolesService.currentRole.subscribe((rol: Rol) => {
// This subscription here should receive the change from the service
if(rol) {
console.log("Changing rol on header to ", rol.getIndex()); // LOG 2
this.rol = rol.getTitle();
this.ids = rol.getRolIDs();
}
});
this.imatgesService.getProfileImagePath().subscribe((path: string) => {
this.picture = path;
}, (err) => {
this.picture = '';
});
}
}
The behaviour that I'm seeing is, the EscullRol component calling the setRolActual(id) method with the new id, and then the service calling its internal method publishCurrentRole(id) with the same id, so at LOG 1 I can see the expected outoput. But then immediately next I can see the output form LOG 2 at the Header Component with the wrong id, which is always the number that we had initially saved at the localStorage when the app started up.
I don't really know if the issue is with how I use the observables, with the service-component communication model or with how components and observables are initailsed and treated in angular.
Few thing to try
First make your service as a singleton using
#Injectable({ providedIn: "root" })
Improvement
Also, make sure that the service is not provided on child modules, as that would create their own instance and it wouldn't be a singleton anymore. If there are more than one instance of the service, the Subject-Observer pattern will not work across all the app.
Then this code
currentRole = this._currentRole.asObservable();
You should create a function to return the data not defined as an variable like
getCurrentRole() {
return this._currentRole.asObservable();
}

How can I check google extension storage before an SubjectBehavior?

I've an authentication guard that checks results from a BehaviorSubject but, before checking it, I need to check chrome local storage that return values in a callback and if the token is invalid renew it and inform BehaviorSubject to allow specific route.
How can I check local storage in get function?
Follow the code to better understand the flow.
auth.guard.ts
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authService.isLoggedIn
.take(1)
.map((isLoggedIn: boolean) => {
if (isLoggedIn) {
return true;
}
this.router.navigate(['/login']);
return false;
});
}
}
auth.service.ts
export class AuthService {
private loggedIn = new BehaviorSubject<boolean>(false);
get isLoggedIn() {
// Here I need check Google Storage that return a callback
// The sintax for is chrome.storage.sync.get('keys', callback);
return this.loggedIn.asObservable();
}
}
If this is all you are doing then you don't need to use observables. You aren't doing anything asynchronous. You just want to check the current value of local storage to see if the user is still authenticated.
The CanActivate interface allows you to return Observable<boolean> | Promise<boolean> | boolean. So you could just do something like this:
auth.guard.ts
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
if(this.authService.isLoggedIn) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
auth.service.ts
export class AuthService {
get isLoggedIn() {
return localStorage.getItem('my-auth-key');
}
}
If you need to expose the authentication state as an observable for other reasons in your app then you could set a timer when you put their session in local storage. It could either poll on an acceptable interval, set it to the absolute expiration of the session, or something like that depending on your authentication scheme. The basic concept is that the only way to make your localstorage "observable" is to use a timer or some event.
For one app I worked on we have a token with an absolute expiration that we refresh on its session half-life. When I put it in local storage I set a timer for its absolute expiration. When I refresh the token and put it back in storage I cancel the previous timer. If the refresh fails for an extended period then the timer will eventually fire. It will forcefully remove the session from local storage since it is expired and it will broadcast (ngrx in my case but BehaviorSubject in yours) that the session has expired.
If you want to have a route change trigger an auth check just to be sure then you could do this:
auth.service.ts
export class AuthService {
private loggedIn = new BehaviorSubject<boolean>(false);
get isLoggedIn() {
this.loggedIn.next(localStorage.getItem('my-auth-key'));
return this.loggedIn.asObservable();
}
}
Since it is a behavior subject you can push a new value to it before you return it.
EDIT
Since you mentioned that you have to do chrome.storage.sync.get and it is async you could do the following (I'm not doing anything with parsing whatever comes out of storage):
auth.service.ts
export class AuthService {
private loggedIn = new Subject<boolean>();
get isLoggedIn() {
chrome.storage.sync.get('my-auth-key', (isAuthenticated) => {
this.loggedIn.next(isAuthenticated);
});
return this.loggedIn.asObservable();
}
}
Note that I changed it to just Subject rather than BehaviorSubject. This will force the recipient to wait for the .next call. This assumes that you want to check the authentication state each time someone tries to subscribe.
The more elegant solution would be to subscribe to chrome.storage.onChanged to feed your BehaviorSubject. You would probably set that up in your constructor. Something like this... I have not used this api before so this is just the general idea... you may need to do null checks or do parsing or something... and if the values don't expire from the store then you would still need a timer to remove them to fire the change.
auth.service.ts
export class AuthService {
private loggedIn = new BehaviorSubject<boolean>(false);
constructor(){
chrome.storage.onChanged.addListener((changes, namespace) => {
this.loggedIn.next(change['my-auth-key'].newValue);
});
}
get isLoggedIn() {
return this.loggedIn.asObservable();
}
}
You could just create an observable out of chrome storage methods:
export class RxChromeStore {
get(key: string): Observable<any> {
return Observable.create(obs => {
let cb = (err, data) => { // use whatever the chrome storage callback syntax is, this is typical cb structure
if (err) {
obs.error(err);
} else {
obs.next(data);
}
};
chrome.storage.sync.get(key, cb);
}).first();
}
}
export class AuthService {
private loggedIn = new BehaviorSubject<boolean>(false);
get isLoggedIn() {
return Observable.zip(RxChromeStore.get('your-auth-key'), this.loggedIn.asObservable(), (chromeAuth, authSubj) => chromeAuth || authSubj);
}
}
your question is a little vague so I'm not sure what your exact goal is, but the basic point here is that you can always create an observable out of soemthing that is callback based using the create method, then you can treat it like any other observable:
RxChromeStore.get('auth-key').switchMap((auth) => (auth.invalid) ? this.http.get('reauthendpoint') : this.loggedIn.asObservable());
or whatever stream you need
You can use localStorage variable to store/fetch data.
Storing: localStorage.setItem(key,value);
Fetching: localStorage.getItem(key);
This localStorage variable is provided by typescript node_module which you will already been having in your angular project.

Categories