how to deny the route to switch until has data in store with ngrx and resolve? - ngrx

i want to deny the user from getting to route until i have data in my store.
i tried with resolver but it still go to the route and im getting an error because im using this data in the state effect .
i searched in the web but i not see example that holds the user in resolver until the data is resolved
how can i "hold" the user until i will have the necessary data in my store?

Use a canActivate or a canLoad guard. Todd Motto has an article on how to do this Preloading ngrx/store with Route Guards
#Injectable()
export class CoursesGuard implements CanActivate {
constructor(private store: Store<CoursesState>) {}
getFromStoreOrAPI(): Observable<any> {
return this.store
.select(getCoursesState)
.do((data: any) => {
if (!data.courses.length) {
this.store.dispatch(new Courses.CoursesGet());
}
})
.filter((data: any) => data.courses.length)
.take(1);
}
canActivate(): Observable<boolean> {
return this.getFromStoreOrAPI()
.switchMap(() => of(true))
.catch(() => of(false));
}

Related

How to use secret key for on-demand revalidation for ISR (Next.js) in the frontend without exposing it?

According to the documentation, you should use a SECRET_TOKEN to prevent unauthorized access to your revalidate API route i.e.
https://<your-site.com>/api/revalidate?secret=<token>
But how are you supposed to call that route from the frontend and keep the token secret?
For example, if you have a simple POST that you then want to trigger the revalidate off of, you'd have to expose your secret token via NEXT_PUBLIC to be able to use it:
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}
What am I missing here? How can you call the API route through the frontend without exposing the SECRET_TOKEN?
I've been trying out On-Demand ISR and stumbled on a similar problem. I was trying to revalidate data after CRUD actions from my Admin dashboard living on the client, behind protected routes ("/admin/...").
If you have an authentication process setup and you're using Next-Auth's JWT strategy, it gives you access to the getToken() method, which decrypts the JWT of the current authenticated user.
You can then use whatever information you have passed through your callbacks to validate the request instead of relying on a SECRET_TOKEN.
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const user = await getToken({ req, secret });
if (!user || user.role !== "ADMIN") {
return res.status(401).json({ message: "Revalidation not authorized"});
}
try {
// unstable_revalidate is being used in Next 12.1
// I'm passing the revalidation url through the query params
await res.unstable_revalidate(req.query.url as string);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send("Error revalidating");
}
}
The Next.js video demo don't actually use a SECRET_KEY.
https://www.youtube.com/watch?v=BGexHR1tuOA
So I guess I'll just have to omit it and hope nobody abuses the revalidate API?
I think you need to create one file called ".env".
Inside the file, you put the params .env like this:
NEXT_PUBLIC_SECRET_TOKEN=123password
You must install the dependency dotenv:
npm i dotenv
and then you can call inside your function like this
function handleSubmit(payload) {
axios.post(POST_URL, payload)
.then(() => {
axios.get(`/api/revalidate?secret=${process.env.NEXT_PUBLIC_SECRET_TOKEN}`)
})
.then(() => {
// redirect to on-demand revalidated page
})
}

Asynchronicity issue with an Angular 2 app and the Angular 2 router

I am facing a tricky issue with asynchronicity in an angular 2 app.
I am basically trying to rehydrate/reload information from the backend when the app is reloaded/bootstrapped in the user's browser (think F5/refresh). The issue is that before the backend async method returns the result, a router guard is called and blocks...
I reload the information from the root component's ngOnInit method as follows:
from root component:
ngOnInit() {
//reloadPersonalInfo returns an Observable
this.sessionService.reloadPersonalInfo()
.subscribe();
}
from sessionService:
reloadPersonalInfo() {
//FIRST: the app execution flow gets here
let someCondition: boolean = JSON.parse(localStorage.getItem('someCondition'));
if (someCondition) {
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_AUTHENTICATED});
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
return Observable.empty();
}
The trouble is that I have a router CanActivate guard as follows:
canActivate() {
//SECOND: then the execution flow get here and because reloadPersonalInfo has not completed yet, isAuthenticated will return false and the guard will block and redirect to '/signin'
const isAuthenticated = this.sessionService.isAuthenticated();
if (!isAuthenticated) {
this.router.navigate(['/signin']);
}
return isAuthenticated;
}
isAuthenticated method from sessionService:
isAuthenticated(): boolean {
let isAuthenticated = false;
this.store.select(s => s.authenticated)
.subscribe(authenticated => isAuthenticated = authenticated);
return isAuthenticated;
}
So to recap:
FIRST: the reloadPersonalInfo method on sessionService is called by root component ngOnInit. The execution flow enters this method.
SECOND: in the meantime, the router guard is called and sees that the state of authenticated is false (because reloadPersonalInfo has not completed yet and therefore not set the authenticated state to true.
THIRD: reloadPersonalInfo completes too late and sets the authenticated state to true (but the router guard has already blocked).
Can anyone please help?
edit 1: Let me stress that the authenticated state that matters is the one in the store; it is set by this line: this.store.dispatch({type: SET_AUTHENTICATED});.
edit 2: I changed the condition from authenticated to someCondition in order to reduce confusion. Previously, there was another state variable called authenticated...
edit 3: I have changed the isAuthenticated() method return type to Observable<boolean> instead of boolean (to follow Martin's advice) and adapted the canActivate method as follows:
canActivate() {
return this.sessionService.isAuthenticated().map(isAuthenticated => {
if (isAuthenticated) {
return true;
}
this.router.navigate(['/signin']);
return false;
});
}
from sessionService:
isAuthenticated(): Observable<boolean> {
return this.store.select(s => s.authenticated);
}
It makes no difference unfortunately...
Can someone please advise as to how to sort this asynchronicity issue?
There should be two possible ways to solve this:
Solution 1
The quickest, would be to distinguish your isAuthenticated not into 2 but 3 states, this way you can encode one more cruical piece of information into the state: At the time of bootstrapping(when no response from the server has been received), there is no way the client can know for sure if its credentials/tokens are valid or not, thus the state should correctly be "unknown".
First you have to change the initial state of authenticated in your store to null (you also may choose undefined or even use numbers depending on your personal taste). And then you just have to add a .filter to your guard, that renders the guard practically "motionless":
canActivate() {
return this.sessionService.isAuthenticated()
.filter(isAuthenticated => isAuthenticated !== null) // this will cause the guard to halt until the isAuthenticated-question/request has been resolved
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
Solution 2
The second solution would be very similar, but instead of encoding a 3rd state into authenticated you'd add a new flag to your store called authRequestRunning, that is set to true while the auth-request is being made, and set to false after it completes. Your guard would then look only slightly different:
canActivate() {
return this.sessionService.isAuthenticated()
.switchMap(isAuth => this.sessionService.isAuthRequestRunning()
.filter(running => !running) // don't emit any data, while the request is running
.mapTo(isAuth);
)
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
With solution #2 you might have some more code. and you have to be careful that the authRequestRunning is set to false first before the authenticated-state is updated.
Edit: I have edited the code in solution #2, so the order of setting the running-status and the auth-status does not matter any more.
The reason why I would use solution #2 is, because in most cases such a state-flag already exists and is being used to display a loading-indicator or something like that.
canActivate itself can return an Observable.
Instead of returning the boolean result in canActivate, return the isAuthenticated Observable.
why are you not setting Authenticated before you give a retrieveCurrentUserAccount call? IT seems you already know if your user is authenticated or not based upon the value inside localStorage
if (isAuthenticated) {
// set before you give a async call.
this.store.dispatch({type: SET_AUTHENTICATED});
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
Update
Try below,
import { Component, Injectable } from '#angular/core';
import { Router, Routes, RouterModule, CanActivate } from '#angular/router';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
#Injectable()
export class SessionService{
private _isAuthenticated: Subject<boolean> = new Subject<boolean>();
public get isAuthenticated(){
return this._isAuthenticated.asObservable();
}
reloadPersonalInfo(){
setTimeout(() => {
this._isAuthenticated.next(true);
// do something else too...
}, 2000);
}
}
#Component({
selector: 'my-app',
template: `<h3>Angular CanActivate observable</h3>
<hr />
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(private router: Router,
private sessionService : SessionService) { }
ngOnInit() {
this.sessionService.reloadPersonalInfo();
}
}
#Component({
template: '<h3>Dashboard</h3>'
})
export class DashboardComponent { }
#Component({
template: '<h3>Login</h3>'
})
export class LoginComponent { }
#Injectable()
export class DashboardAuthGuard implements CanActivate {
constructor(private router: Router, private sessionService : SessionService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
return this.sessionService.isAuthenticated.map(res => {
if(res){
return true;
}
this.router.navigate(['login']);
}).take(1);
}
}
let routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
canActivate: [DashboardAuthGuard],
component: DashboardComponent
},
{
path: 'login',
component: LoginComponent
}
]
export const APP_ROUTER_PROVIDERS = [
DashboardAuthGuard
];
export const routing: ModuleWithProviders
= RouterModule.forRoot(routes);
Here is the Plunker!!
Hope this helps!!

RxJS and angular2

Let's say I have a service getting a list of properties:
export class PropertyService {
private properties: any;
constructor(#Inject(Http) http) {
this.http.get('/properties.json')
.map((res:Response) => res.json())
.subscribe(
data => this.properties = data
);
}
getProperty(property: string) {
this.properties[property];
}
}
The problem is that properties is not yet loaded when getProperty is called.
How can I make getProperty to wait for the subscription to populate the array?
I would rather not return a subscription to the components.
EDIT:
I tried paulpdaniels answer and worked with pluck. But I got stuck pretty fast.
So basically I have this PropertyService which returns a HOST.
I have an ApiService that uses that host to do another ajax call to get data.
export class PropertyService {
private properties: any;
constructor(#Inject(Http) http) {
this.properties = http.get('/properties.json')
.map((res:Response) => res.json()).cache(1);
}
getProperty(property: string) {
this.properties.pluck(property);
}
}
export class ApiService {
private http: Http;
private host: string;
constructor(#Inject(Http) http, #Inject(PropertyService) propertyService) {
this.http = http;
this.host = propertyServiceService.getProperty("API.URL"); //Is a subscription
}
getData(): any {
//Here I must subscribe to host, and once available,
//use host to do an ajax call that returns another Observable for the component
}
}
How to realize this?
Short answer is you can't, at least not without introducing some level of asynchronicity to your service.
The simple fact is there is no way to force blocking behavior and wait for the Observable to complete, you should return an Observable which your components should then also know how to consume.
export class PropertyService {
private properties: Observable<any>;
constructor(#Inject(Http) http) {
this.properties = this.http.get('/properties.json')
.map((res:Response) => res.json())
//This will hold onto a cached value of the result
//without re-executing
.cache(1);
}
getProperty(property: string) {
this.properties.pluck(property);
}
}
Edit 1
As Frank Herbert once wrote (paraphrasing) "the stream must flow!". If you need to use nested Observables operators like flatMap allow you to flatten those into a single stream, which you can continue to chain.
export class ApiService {
private http: Http;
private host: Observable<string>;
constructor(#Inject(Http) http, #Inject(PropertyService) propertyService) {
this.http = http;
this.host = propertyServiceService.getProperty("API.URL"); //Is an Observable
}
getData(): Observable<any> {
return this.host.flatMap(url => this.http.get(url), (_, res) => res.json());
}
}
What you can do is to return properties at observable itself and then in destination service start from that properties observable and .flatMap a http request after that. That means introducing more asynchronicity as was mentioned in other answers. Othr solution would be to resolve properties before app / component really start executing by retrieving properties in canActivate router guard. In this case you can be sure that when the component calls method of other service which depends on properties being available synchronosuly the properties were already resolved.

How to subscribe to service that receives data from router (master-detail scenario)?

I am new to Angular2 and typescript, so this may be an easy one for any experienced folks! I develop a master-detail App using the Angular Heroes tutorial as a starter. I added a http service that calls a rest api. It properly lists decisions made in our town ( Antwerp Belgium :) ). Now when I click to view the details, the resulting json object is 'undefined'.
In my decision-detail.component.ts file, I have an error at decision => this.decision = decision, "Type 'Decision[]' is not assignable to type 'Decision'."
export class DecisionDetailComponent implements OnInit {
#Input() decision: Decision;
errorMessage: string;
constructor(
private _decisionService: DecisionService,
private _routeParams: RouteParams) {
}
ngOnInit() {
let id = +this._routeParams.get('id');
this._decisionService.getDecision(id)
.subscribe(
decision => this.decision = decision,
error => this.errorMessage = <any>error);
}
}
Here's my decision.service.ts code:
getDecision (id: number) {
return this.http.get(this._decisionsUrl+id)
.map(res => <Decision[]> res.json().data)
.do(data => console.log(data)) //eyeball results in the console
.catch(this.handleError)
Any help much appreciated!!
I got this working. Deleted the #input and declared decisions as a property in the class.

Http request made multiple times in Angular2 service

I have created a service that makes a simple GET request:
private accountObservable = null;
constructor(private _http: Http) {
}
getAccount () {
// If we have account cached, use it instead
if (this.accountObservable === null) {
this.accountObservable = this._http.get('http://localhost/api/account')
.map(res => <Account> res.json().data)
.catch(this.handleError);
}
return this.accountObservable;
}
I have added that service in my bootstrap function to provide it globally (my hope is to provide the same instance to all components):
provide(AccountService, { useClass: AccountService })
The problem is when I call this service in different components, a GET request is made every time. So if I add it to 3 components, 3 GET requests will be made even though I check if an observable already exist.
ngOnInit() {
this._accountService.getAccount().subscribe(
account => this.account = account,
error => this.errorMessage = <any>error
);
}
How can I prevent the GET request to be made multiple times?
Use Observable.share():
if (this.accountObservable === null) {
this.accountObservable = this._http.get('./data/data.json')
.share()
.map(res => res.json())
.catch(this.handleError);
}
Plunker
In the Plunker, AppComponent and Component2 both call getAccount().subscribe() twice.
With share(), the Chrome Developer tools Network tab shows one HTTP request for data.json. With share() commented out, there are 4 requests.
There are two types of observables.
Cold Observable : each subscriber receive all the events ( from the begining )
Hot observable : each subscriber receive the events that are emited after subscription.
Cold Observables are the default one. That's what the WS calling is triggered many times.
To make an Observable Hot you have to use following Rx's operators chain :
.publish().refCount()
In your case :
getAccount () {
let accountObservable = this._http.get('http://localhost/api/account')
.map(res => <Account> res.json().data)
.catch(this.handleError);
return accountObservable.publish().refCount();
}
In my case it was because of form post and button clik was set to same listener
The updated solution is:
1) Change your getAccount() method to use share:
getAccount () {
// If we have account cached, use it instead
if (this.accountObservable === null) {
this.accountObservable = this._http.get('http://localhost/api/account')
.pipe(share())
.map(res => <Account> res.json().data)
.catch(this.handleError);
}
return this.accountObservable;
}
2) Add import { share } from 'rxjs/operators'; to the top of your .ts file to get rid of the error on share.

Resources