I'm using popup login with Angular Material, when I added Angular Universal the auth guard is the problem. If some route is protected by auth guard the page just start pending and never finishes. Behavior without Angular Universal is when page is reloaded It just open up popup for login.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(readonly auth: AuthService, public router: Router, private dialog: MatDialog, private store: Store<fromAuth.State>) {}
/** Performs the user authentication prompting the user when neeed or resolving to the current authenticated user otherwise */
public authenticate(action: loginAction = 'signIn') {
return this.store.pipe(
select(fromAuth.selectAuthState),
take(1),
switchMap(user => !user.user ? this.prompt(action) : of(user.user))
).toPromise();
}
public prompt(data: loginAction = 'signIn'): Promise<any> {
return this.dialog.open<LogInComponent, loginAction>(LogInComponent, { data }).afterClosed().toPromise();
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
// Gets the authorization mode when specified
// const mode = route.queryParamMap.get('authMode') || 'signIn';
// Prompts the user for authentication
return this.authenticate()
.then(user => !!user);
}
}
If I'm directly accessing ngrx store inside canActivate It's working but I want to work with .toPromise()
I'm using httponly cookies and on every reload Angular is sending http request to nodejs db to fetch user data. On every other route It's working as expected.
currently not having too much time but some thoughts in short:
If you are rendering the angular page on the server side (Angular Universal), why don't you handle the auth process on the server? Check if the user is logged in on each request and redirect the user on a login page - you would need a standalone login page instead of an overlay.
I have multiple projects running with AuthGuard / User / Auth, and I would not recommend to return a promise but a boolean for canActivate.
Because:
-> This way you can only check if there is a current user object set in your canActivate method and return true or false.
So in my opinion: Either use server side rendering fully which means also check the auth state of the user in the backend. Or use angular as a real front end project and handle the auth process there. Mixing those two worlds will lead to some nasty problems and makes it unnecessarily complex to maintain.
Example Routing
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuardService, CanDeactivateGuard } from '@app/core/services';
import { AppPagesConfig } from '@app/config';
import * as Pages from '@app/pages';
const routes: Routes = [
{
path: 'dashboard',
component: Pages.DashboardPage,
canActivate: [AuthGuardService],
data: {
permissions: []
}
}, {
path: 'calendar',
children: [
{
path: '',
redirectTo: AppPagesConfig.calendarPersonal.path,
pathMatch: 'full'
}, {
path: AppPagesConfig.calendarPersonal.path,
component: Pages.CalendarPersonalPage,
canActivate: [AuthGuardService],
data: {
permissions: 'Foo-canEdit'
}
}, {
path: AppPagesConfig.calendarTeam.path,
component: Pages.CalendarTeamPage,
canActivate: [AuthGuardService],
data: {
permissions: '0100'
}
},
]
}, {
path: 'contacts',
children: [
{
path: '',
redirectTo: 'private',
pathMatch: 'full'
}, {
path: 'private',
component: Pages.ContactsPage,
canActivate: [AuthGuardService],
canDeactivate: [CanDeactivateGuard],
data: {
permissions: []
}
},
]
}, {
path: 'errors',
children: [
{
path: '',
redirectTo: '404',
pathMatch: 'full'
}, {
path: '404',
component: Pages.ErrorNotFoundPage
}, {
path: '403',
component: Pages.ErrorNoPermissionsPage
},
]
}, {
path: 'login',
component: Pages.LoginPage
}, {
path: '**',
component: Pages.ErrorNotFoundPage
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
useHash: true
})
],
exports: [
RouterModule
]
})
export class AppRoutingModule { }
Example UserModel
import { uniq, union } from 'lodash';
import { UserBaseModel } from '@app/models/user-base';
import { Deserializable } from './deserializable.model';
export class User implements Deserializable {
public permissions: string[];
/**
* Call this function to fill the model with data.
*/
public deserialize(input: UserBaseModel): this {
Object.assign(this, input);
this.updateUserPermissions();
return this;
}
/**
* Checks if the user has all required permissions.
*/
public hasPermissions(requiredPermissions: string[]): boolean {
// If there where no required permissions given it is valid.
if (!requiredPermissions || !requiredPermissions.length) {
return true;
}
// If there are required permissions given but the user has no permissions at all it is always invalid.
if (requiredPermissions.length && !this.permissions.length) {
return false;
}
// Check the users permissions to contain all required permissions.
for (const permission of requiredPermissions) {
if (!this.permissions.includes(permission)) {
return false;
}
}
return true;
}
}
Example AuthGuard
import { isEmpty } from 'lodash';
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { SessionService } from './session.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(
private readonly _router: Router,
private readonly sessionService: SessionService
) { }
/**
* Check if user is allowed to navigate to the new state.
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const currentUser = this.sessionService.getCurrentUser();
// Not logged in so redirect to login page.
if (!currentUser) {
this.sessionService.logoutUser();
this._router.navigate(['/login']);
return false;
}
// Route is not protected so continue (for routes without auth permission needed).
if (isEmpty(route.data) || !route.data.permissions || !route.data.permissions.length) {
return true;
}
// If the permissions do not match redirect.
if (currentUser && !currentUser.hasPermissions(route.data.permissions)) {
this._router.navigate(['/errors/403'], {
queryParams: {
referrerUrl: state.url
}
});
return false;
}
// If the permissions do match continue.
if (currentUser && currentUser.hasPermissions(route.data.permissions)) {
return true;
}
// If nothing matches log out the user.
this.sessionService.logoutUser();
this._router.navigate(['/login']);
return false;
}
}