angular2: how to get Full path on CanLoad guard while maintaining redirect url - angular2-routing

I am using angular 2.4 version and router version "^3.4.10".
I am trying to handle redirect url using auth guard service.
When user hit url 'domain/assignment/3/detail' and if user is not login then user redirected to 'domain/login' page.
and when user successfully login in to system then redirected to 'domain/assignment/3/detail' previous url which user tries to access.
I have implemented CanLoad guard on assignment module. so when user tries to access url 'domain/assignment/3/detail' and if user is not login, url stores into redirectUrl property of authservice (this.authService.redirectUrl).
so here is the issue comes in my case. i am not able to get full path of the url which user hit.
i am getting 'assignment' instead 'assignment/3/detail' within CanLoad guard.
how can i get full path so that i can redirect user to proper path within CanLoad guard.
CanLoad:
canLoad(route: Route): boolean {
let url = `/${route.path}`; // here i got url path 'assignment' instead 'assignment/3/detail'
return this.checkLogin(url);
}
Main routing app.routes.ts
const routes: Routes = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent},
{
path: 'assignment',
loadChildren: './assignment/assignment.module#AssignmentModule',
canLoad: [AuthGuard]
},
{ path: '**', redirectTo: '', pathMatch: 'full' }];
Assignment routing: assignment-routing.ts
const assignmentRoutes: Routes = [
{
path: '',
component: AssignmentComponent,
canActivate: [AuthGuard]
children: [
{
path: '',
canActivateChild: [AuthGuard],
children: [
{
path: ':assignmentId/detail', component: AssignmentDetailComponent,
canActivate: [AuthGuard]
}
]
}]
}];
AuthGuard: auth-gurad.service.ts
import { Injectable } from '#angular/core';
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild,
NavigationExtras,
CanLoad, Route
} from '#angular/router';
import { AuthService } from './auth.service';
#Injectable()
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
constructor(private authService: AuthService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
return this.canActivate(route, state);
}
canLoad(route: Route): boolean {
let url = `/${route.path}`; // here i got url path 'assignment' instead 'assignment/3/detail'
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.authService.isLoggedIn) {
if(this.authService.redirectUrl!=null){
let redirectUrl = this.authService.redirectUrl;
this.authService.redirectUrl = null;
this.this.router.navigate([redirectUrl]);
}
return true;
}
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page
this.router.navigate(['/login']);
return false;
}
}

If you look the signature of the canLoad method, there is is a second parameter segments which you can use to generate the url.
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean
You can generate the full path using following code:
canLoad(route: Route, segments: UrlSegment[]): boolean {
const fullPath = segments.reduce((path, currentSegment) => {
return `${path}/${currentSegment.path}`;
}, '');
console.log(fullPath);
}
Note: With this method you will get the full path, but you will not get any query parameters.

I have this exact same problem. These issues are related
https://github.com/angular/angular/issues/17359
https://github.com/angular/angular/issues/12411
And they even have an open PR about this which hasn't been merged yet
https://github.com/angular/angular/pull/13127
The workaround for now seems to be to replace the canLoad method with the canActivate, there you can have access to the full path, the bad thing of course is that you're loading the module
Edit: Also for you Assignment routing, there's no need to call canActivate for each child route. Have it defined on the parent route should apply the guard for all child routes.

This workaround from this reply on the github issue worked very well for me.
export class AuthnGuard implements CanLoad {
constructor(private authnService: AuthnService, private router: Router) { }
canLoad( ): Observable<boolean> | Promise<boolean> | boolean {
const navigation = this.router.getCurrentNavigation();
let url = '/';
if (navigation) {
url = navigation.extractedUrl.toString();
}
this.authnService.storeRedirectUrl(url);

There is another temporary workaround instead of replacing canLoad with canActivate:
You can store the url via redirectUrl = location.pathname and redirect later via this.router.navigateByUrl(redirectUrl);. Of course, if you work on a subpath (like domain) you have to adjust the pathname a little bit.

In Angular 9, following reply worked for me.
My code
export class AuthGuard implements CanLoad {
constructor(private router: Router, private loginService: LoginService) { }
canLoad( ): Observable<boolean> | Promise<boolean> | boolean {
if (this.loginService.isLoggedIn()) return true;
this.router.events.pipe(first(_ => _ instanceof NavigationCancel)).subscribe((event: NavigationCancel) => {
this.router.navigate(['/login'], {
queryParams: {
redirectTo: event.url
}
});
});
return false;
}
}

information available on
this.router.getCurrentNavigation().extractedUrl
relates to this issue https://github.com/angular/angular/issues/30633

I'm using
this.router.getCurrentNavigation().extractedUrl.toString()
to access the full route, since
segments.reduce((path, currentSegment) => {
return `${path}/${currentSegment.path}`;
}, '')
will not provide the full path if you have nested routes (only child routes).

Related

NextJS shows I am logged out if I visit my website via a link

In the top right of my website is a little user bar component, and it just simply renders my username if I am logged in, or a "Sign In" link.
But, every single time I visit my website via a link from an external website, the "Sign In" link is rendered instead of my username. My website does not think I am logged in.
But if I just simply refresh the page with no other actions, I am showed as logged in again. When this happens, nxtCookies.getAll() seems to be empty.
Or if I visit my website by URL directly, I am showed as logged in correctly.
What could be causing this?
UserInfoBar
import React from 'react'
import Link from 'next/link'
import { getSession } from '../../lib/session'
export default async function UserInfoBar() {
let sessionData = await getSession();
return (
{
Object.keys(sessionData).length > 0 ?
{sessionData.username}: <Link href='/signin'>Sign In</Link>
}
)
}
session
import { cookies } from 'next/headers';
import prisma from './prisma'
import jwt from 'jsonwebtoken'
export const getSession = async () => {
const nxtCookies = cookies();
if (nxtCookies.has('wp_session')) {
let sessionData = jwt.verify(nxtCookies.get('wp_session').value, process.env.ACCESS_TOKEN_SECRET, async (err, user) => {
if (!err) {
let r = await prisma.user.findUnique({
where: {
id: user.id
}
});
if (r) {
return {
id: r?.id,
email: r?.email,
username: r?.username
};
}
}
return false;
});
if (sessionData) return sessionData;
}
return false;
}
sign-in
( where the cookie is set )
setCookie('wp_session', token, {
path: '/',
maxAge: 3600 * 24 * 7 * 30,
sameSite: true
});
The issue was with the creation of the cookie.
I was mistakenly supplying the sameSite parameter with the value of true.
The recommended value is lax.
setCookie('wp_session', token, {
path: '/',
maxAge: 3600 * 24 * 7 * 30,
sameSite: 'lax'
});
Lax: Cookies are not sent on normal cross-site subrequests (for
example to load images or frames into a third party site), but are
sent when a user is navigating to the origin site (i.e., when
following a link).
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax

nested route using #ApiParam, how to use a custom validator in nestjs?

I’m looking for some help about custom validator & custom decorator in Nest.
FIRST CASE : working one
A DTO, with class-validator anotations :
import { IsNotEmpty, IsString } from 'class-validator';
import { IsOwnerExisting } from '../decorators/is-owner-existing.decorator';
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#IsOwnerExisting() // custom decorator, calling custom validator, using a service to check in db
ownerEmail: string;
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
I use it in a controller :
#Controller()
#ApiTags('/polls')
export class PollsController {
constructor(private readonly pollsService: PollsService) {}
#Post()
public async create(#Body() createPollDto: CreatePollDto): Promise<Poll> {
return await this.pollsService.create(createPollDto);
}
}
When this endpoint is called, the dto is validating by class-validator, and my custom validator works. If the email doesn’t fit any user in database, a default message is displayed.
That is how I understand it.
SECOND CASE : how to make it work ?
Now, I want to do something similar but in a nested route, with an ApiParam. I’d like to check with a custom validator if the param matches some object in database.
In that case, I can’t use a decorator in the dto, because the dto doesn’t handle the "slug" property, it’s a ManyToOne, and the property is on the other side.
// ENTITIES
export class Choice {
#ManyToOne((type) => Poll)
poll: Poll;
}
export class Poll {
#Column({ unique: true })
slug: string;
#OneToMany((type) => Choice, (choice) => choice.poll, { cascade: true, eager: true })
#JoinColumn()
choices?: Choice[];
}
// DTOs
export class CreateChoiceDto {
#IsNotEmpty()
#IsString()
label: string;
#IsOptional()
#IsString()
imageUrl?: string;
}
export class CreatePollDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
#IsOptional()
#IsArray()
#ValidateNested({ each: true })
#Type(() => CreateChoiceDto)
choices: CreateChoiceDto[] = [];
}
So where should I hook my validation ?
I’d like to use some decorator directly in the controller. Maybe it’s not the good place, I don’t know. I could do it in the service too.
#Controller()
#ApiTags('/polls/{slug}/choices')
export class ChoicesController {
constructor(private readonly choicesService: ChoicesService) {}
#Post()
#ApiParam({ name: 'slug', type: String })
async create(#Param('slug') slug: string, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
}
As in my first case, I’d like to use something like following, but in the create method of the controller.
#ValidatorConstraint({ async: true })
export class IsSlugMatchingAnyExistingPollConstraint implements ValidatorConstraintInterface {
constructor(#Inject(forwardRef(() => PollsService)) private readonly pollsService: PollsService) {}
public async validate(slug: string, args: ValidationArguments): Promise<boolean> {
return (await this.pollsService.findBySlug(slug)) ? true : false;
}
public defaultMessage(args: ValidationArguments): string {
return `No poll exists with this slug : $value. Use an existing slug, or register one.`;
}
}
Do you understand what I want to do ? Is it feasible ? What is the good way ?
Thanks a lot !
If you're needing to validate the slug with your custom rules you have one of two options
make a custom pipe that doesn't use class-validator and does the validation directly in it.
Use #Param() { slug }: CreatePollDto. This assumes that everything will be sent via URL parameters. You could always make the DTO a simple one such as
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
And then use #Param() { slug }: SlugDto, and now Nest will do the validation via the ValidationPipe for you.
If it didn't work with you with service try to use
getConnection().createQueryBuilder().select().from().where()
I used it in custom decorator to make a isUnique and it works well, but niot with injectable service.
public async validate(slug: string, args: ValidationArguments): Promise<boolean> { return (await getConnection().createQueryBuilder().select(PollsEntityAlias).from(PollsEntity).where('PollsEntity.slug =:slug',{slug}))) ? true : false; }
That’s so greeat! Thanks a lot, it’s working.
I’ve tried something like that, but can’t find the good way.
The deconstructed { slug }: SlugDto, so tricky & clever ! I’ve tried slug : SlugDto, but it couldn’t work, I was like «..hmmm… how to do that… »
Just something else : in the controller method, I was using (as in documentation) #Param('slug'), but with the slugDto, it can’t work. Instead, it must be just #Param().
Finally, my method :
#Post()
#ApiParam({ name: 'slug', type: String })
public async create(#Param() { slug }: SlugDto, #Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
And the dto :
export class SlugDto {
#IsNotEmpty()
#IsString()
#NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
#IsSlugMatchingAnyExistingPoll()
slug: string;
}
Personally, I wouldn't register this as a class-validator decorator, because these are beyond the scopes of Nestjs's dependency injection. Getting a grasp of a service/database connection in order to check the existence of a poll would be troublesome and messy from a validator constraint. Instead, I would suggest implementing this as a pipe.
If you want to only check if the poll exists, you could do something like:
#Injectable()
export class VerifyPollBySlugPipe implements PipeTransform {
constructor(#InjectRepository(Poll) private repository: Repository<Poll>) {}
transform(slug: string, metadata: ArgumentsMetadata): Promise<string> {
let found: Poll = await repository.findOne({
where: { slug }
});
if (!found) throw new NotFoundException(`No poll with slug ${slug} was found`);
return slug;
}
}
But, since you're already fetching the poll entry from the database, maybe you can give it a use in the service, so the same pipe can work to retrieve the entity and throw if not found. I answered a similar question here, you'd just need to add the throwing of the 404 to match your case.
Hope it helps, good luck!

NestJS passing Authorization header to HttpService

I have a NestJS application which acts as a proxy between a front-end and multiple other back-ends.
I basically want to be able to pass a specific header (Authorization) from incoming #Req (requests) in the controller to the HttpService that then talks to the other back-ends.
user controller (has access to request) ->
user service (injects httpService that somehow already picks the Authorization header) -> External backends.
Right now I need to extract the token from #Headers and then pass token to service which has to paste it to all HttpService calls.
Thanks in advance!
I'm not sure if this will help you, but maybe if you get the header from the controller and put it in your services function...
// Controller:
#Get()
getAll(#Request() req){
const header = req.headers;
return this._zoneService.sendToHttp(header);
}
Maybe microservices can be better ?
Besides the middleware answer, I have another version using interceptor:
#Injectable()
export class HttpServiceInterceptor implements NestInterceptor {
constructor(private httpService: HttpService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// ** if you use normal HTTP module **
const ctx = context.switchToHttp();
const token = ctx.getRequest().headers['authorization'];
// ** if you use GraphQL module **
const ctx = GqlExecutionContext.create(context);
const token = ctx.getContext().token;
if (ctx.token) {
this.httpService.axiosRef.defaults.headers.common['authorization'] =
token;
}
return next.handle().pipe();
}
}
If you use GraphQLModule, do not forget to pass token to context:
GraphQLModule.forRoot({
debug: true,
playground: true,
autoSchemaFile: 'schema.gql',
context: ({ req }) => {
return { token: req.headers.authorization };
},
}),
Once preparation work is ready, let's use interceptors
The interceptor can be injected into a certain controller:
#UseInterceptors(HttpServiceInterceptor)
export class CatsController {}
or register a global interceptor like following:
#Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: HttpServiceInterceptor,
},
],
})
export class AppModule {}
Ok, I managed to solve this by actually using a middleware that I initially thought. I am not a 100% sure this is the NEST way but intercepting the underlying axios reference of the HttpService did the trick:
#Injectable()
export class BearerMiddleware implements NestMiddleware {
constructor(private readonly httpService: HttpService) {}
use(req: Request, res: Response, next: Function) {
this.httpService.axiosRef.interceptors.request.use(request => {
request.headers = {
...request.headers,
Authorization: req.headers.Authorization || '',
};
return request;
});
next();
}
}
Required import HttpModule into your module
import { HttpModule } from '#nestjs/axios';
#Module({
providers: [],
controllers: [YourController],
imports: [
HttpModule,
],
exports: [],
})
export class YourModule {}
In your controller
import { HttpService } from '#nestjs/axios';
import { Controller, HttpStatus, Res, BadRequestException } from '#nestjs/common';
#Controller('your-controller')
export class YourController {
constructor(
private httpService: HttpService,
) {}
async fetchApi(#Res() res) {
const urlAPI = 'https://xxx.vn';
try {
const source = await this.httpService.get(urlAPI,
{
headers: { Authorization: 'Basic XXX-TOKEN' },
},
);
return res.status(HttpStatus.OK).json({ ...source });
} catch (error) {
throw new BadRequestException(error.response?.statusText);
}
}
}

How to use adal.js to authenticate an SPA in an iframe of another SPA (In the same AAD tenant)

I have an HTML application written in Angular JS, and I would like to allow other trusted internal developers to extend the application by creating their own applications in Angular hosted on different endpoints. The extensions want to be embedded inside the main shell by the use of <iframe>
These applications all exist in the same AAD Tenant, as different application registrations.
We have tried to set the iframe src="http://localhost:4200", and inside the inner application have used adal.js to authenticate against the AD. This inner application works fine when hosted directly in the browser, but when embedded in the iframe causes:
Refused to display 'https://login.microsoftonline.com/tenantid/oauth2/authorize?response_type=id_token&client_id=applicationId&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fhome&state=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxab&client-request-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx&x-client-SKU=Js&x-client-Ver=1.0.17&nonce=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx' in a frame because it set 'X-Frame-Options' to 'deny'
Is what we are attempting to do sensible, or is this approach considered a security risk?
I have seen others solve this by also navigating to the child page, but then you lose the shell.
Here is the code from the AngularJS side:
var iframe = document.querySelector("#myiframe");
iframe.src = ENV.iframeURL + "/?data=" + $routeParams.data;
And, the code at the Angular Side is more complex:
Readme.md
I have copied in the readme.md file from the Angular application, so you can to get an insight on what we have done Angular side. Angular App works fine when it is the root of the web page.
Application details
The Main application has the authService injected into it, and onInit calls the applicationInit()
export class AppComponent implements OnInit {
constructor(private authService: AuthService) {
}
ngOnInit() {
this.authService.applicationInit();
}
}
```
The Auth Service is the main part which you will use within your own projects.
Referring to How to load adal.js in webpack inside Angular 2 (Azure-AD)
for details on this workflow
/// <reference path="../../../node_modules/#types/adal/index.d.ts" />
import { Injectable } from '#angular/core';
import { environment } from '../../environments/environment';
import 'expose-loader?AuthenticationContext!../../../node_modules/adal-angular/lib/adal.js';
#Injectable()
export class AuthService {
private context: adal.AuthenticationContext = null;
constructor() {
let adalSettings = environment.adalSettings;
let createAuthContextFn: adal.AuthenticationContextStatic = AuthenticationContext;
this.context = new createAuthContextFn(adalSettings);
}
Environment file while is loaded - loads in the relevant tenant, and applicationId
export const environment = {
production: false,
adalSettings: {
tenant: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
clientId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
redirectUri: 'http://localhost:4200/home',
postLogoutRedirectUri:'http://localhost:4200',
expireOffsetSeconds: 300
}
};
The current User use the service's getUser method: this returns a promise to the user
getUser(): Promise<adal.User> {
var result = new Promise<adal.User>(
(resolve, reject) => {
this.context.getUser((err, user) => {
if (err)
resolve(null);
else
resolve(user);
});
});
return result;
}
## Routes into the application
const routes: Routes = [
{
path: 'login',
component: LoginRedirectComponent
},
{
path: 'home',
component: HomeComponent,
canActivate: [AuthGuard]
},
{
path: '',
pathMatch: 'full',
redirectTo: 'home'
}
];
//TODO add other client routes - e.g. the data which is being posted in
What are your recommendations? Is there a best practice we should be following?
Thanks in Advance

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!!

Resources