I'm trying to implement a role based system on my application and I have the following:
authorize.service.ts
public isAuthenticated(): Observable<boolean> {
return this.getUser().pipe(map(u => !!u));
}
public hasRole(roles: Array<string>): Observable<boolean> {
return this.getUser().pipe(map(u => {
if (!!!u) {
return false;
}
const role = u['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'];
return roles.some(r => r === role);
}));
}
app.routing.ts
...
{ path: 'list', component: ListComponent, canActivate: [AuthorizeGuard], data: { title: 'List', roles: [Role.Admin, Role.Developer, Role.Guest, Role.User]} }
...
And I would like to have the following logic on my AuthorizeGuard:
If isn't authenticated navigate to the login page.
If is logged in and has the required role navigate to the page otherwise navigate to a "forbidden" page.
I have this code on my canActivate:
canActivate(
_next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
this.authorize.isAuthenticated().subscribe(isAuthenticated => {
if (isAuthenticated) {
this.authorize.hasRole(roles).subscribe(hasRole => {
if (hasRole) {
return true;
} else {
this.router.navigate(['forbidden']);
}
});
} else {
this.router.navigate(ApplicationPaths.LoginPathComponents, {
queryParams: {
[QueryParameterNames.ReturnUrl]: state.url
}
});
}
});
return true;
}
But it doesn't work because of the subscribers.
Is there anyway to make wait for the result and return it instead of always return true?
Thanks
You can't mix 2 subscribe and return true in the end, cause it always is true(last return).
You need return Observable<boolean> from the start and chain it to have Observable of false || true at the end
This code is changed in this editor, so it maybe won't work from the start, but you would get the idea of chaining
return this.authorize.isAuthenticated().pipe(
concatMap(isAuth => isAuth ? this.authorize.hasRole(roles) : throwError('NO_AUTH')),
concatMap(hasRole => hasRole ? of(true) : throwError('NO_ROLE')),
catchError(error => {
const isAuthError = error === 'NO_AUTH';
if (isAuthError) {
this.router.navigate(ApplicationPaths.LoginPathComponents, {
queryParams: {
[QueryParameterNames.ReturnUrl]: state.url
}
});
} else {
this.router.navigate(['forbidden']);
}
return of(false);
}),
);
p.s approach with catchError could be reworked to some other
Related
I'm trying to add a new AsyncValidator to check whether user's email already exist in database.
Below is may validator:
export class UniqueEmailValidator implements AsyncValidator {
constructor(private webService: WebWrapperService) {}
validate(ctrl: AbstractControl): Promise < ValidationErrors | null > | Observable < ValidationErrors | null > {
return this.webService.isEmailExistEx(ctrl.value)
.pipe(
map(res => {
console.log("get response" + res);
if (res) {
return { 'uniqueEmail': true};
}
return null;
})
);
}
}
The function isEmailExistEx in service will send a post request to server.
isEmailExistEx(email: string): Observable<boolean> {
this.http.post(this.baseUrl + "auth/verify",
{
"email": email
})
.subscribe(
(val: any) => {
if (!val.result) {
return of(false);
} else {
return of(true);
}
},
response => {
return of(false);
},
() => {
return of(false);
});
}
It reports following error:
A function whose declared type is neither 'void' nor 'any' must return a value.
How should I modify this function?
You're subscribeing to the Observable which will consume the value wrapped in it.
Use map instead of subscribeing and return a boolean value from it::
isEmailExistEx(email: string): Observable<boolean> {
return this.http.post(this.baseUrl + "auth/verify", { email })
.pipe(
map((val: any) => val.result ? true : false)
);
}
I have some code that goes to my server and returns some data. I have noticed, that i am unable to catch/handle a 403 response.
Code:
canActivate(route: ActivatedRouteRequest, state: RouterStateSnapshot): Observable <boolean> {
return this.myService.getEntitlements()
.pipe(
map(data => {
this.isEntitled = data.hasEntitlements;
if(this.isEntitled === true) {
return true;
}
else {
return false;
}
}),
catchError((err: any) => {
return of(false);
})
};
}
I'm displaying a LoadingController when the user tries to login. Meanwhile, an API is being called.
I’m able to dismiss the LoadingController when I get a SUCCESS response from subscribe, but when I get an ERROR response, I’m not able to dismiss. Please help!
I’m a professional Python developer and a total newbie to Ionic, just started a day ago. So, please assist as such.
import { Component, OnInit } from '#angular/core';
import { ToastController, LoadingController } from '#ionic/angular';
import { CallapiService } from '../callapi.service';
#Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
userEmail = '';
userPassword = '';
loginUrl = 'login/';
loginMethod = 'POST';
postBody = {};
constructor(
public toastController: ToastController,
public loadingController: LoadingController,
private callApiService: CallapiService,
) { }
ngOnInit() {
}
async presentToast(displayMessage) {
const toast = await this.toastController.create({
message: displayMessage,
duration: 2000,
position: 'middle',
});
return await toast.present();
}
async presentLoading(loadingMessage) {
const loading = await this.loadingController.create({
message: loadingMessage,
});
return await loading.present();
}
loginUser() {
if (this.userEmail === '' || this.userPassword === '') {
this.presentToast('Email and password are required.');
}
else {
this.presentLoading('Processing...');
this.postBody = {
email: this.userEmail,
password: this.userPassword,
};
this.callApiService.callApi(this.loginUrl, this.postBody, this.loginMethod).subscribe(
(success) => {
console.log(success);
this.loadingController.dismiss();
},
(error) => {
console.log(error);
this.loadingController.dismiss();
}
);
this.loadingController.dismiss();
}
}
}
Without any service,
Same issue I faced while using Ionic 4 loading controller.
After trial and error I got working solution.
As loading controller functions are using async and await because both are asynchronous functions.
dismiss() function will called before present() function because, dismiss function will not wait until creating and presenting the loader, it will fire before present() as soon function will call.
Below is working code,
loading:HTMLIonLoadingElement;
constructor(public loadingController: LoadingController){}
presentLoading() {
if (this.loading) {
this.loading.dismiss();
}
return new Promise((resolve)=>{
resolve(this.loadingController.create({
message: 'Please wait...'
}));
})
}
async dismissLoading(): Promise<void> {
if (this.loading) {
this.loading.dismiss();
}
}
someFunction(){
this.presentLoading().then((loadRes:any)=>{
this.loading = loadRes
this.loading.present()
someTask(api call).then((res:any)=>{
this.dismissLoading();
})
})
}
this.callApiService.callApi(this.loginUrl, this.postBody, this.loginMethod)
.subscribe(
(data) => {
// Called when success
},
(error) => {
// Called when error
},
() => {
// Called when operation is complete (both success and error)
this.loadingController.dismiss();
});
Source: https://stackoverflow.com/a/54115530/5442966
Use Angular property binding. Create a component to your loading:
import { Component, Input } from '#angular/core';
import { LoadingController } from '#ionic/angular';
#Component({
selector: 'app-loading',
template: ''
})
export class LoadingComponent {
private loadingSpinner: HTMLIonLoadingElement;
#Input()
set show(show: boolean) {
if (show) {
this.loadingController.create().then(loadingElem => {
this.loadingSpinner = loadingElem;
this.loadingSpinner.present();
});
} else {
if (this.loadingSpinner) {
this.loadingSpinner.dismiss();
}
}
}
constructor(private loadingController: LoadingController) {}
}
...then in 'login.page.html' use your componente:
...
<app-loading [show]="showLoading"></app-loading>
... in 'LoginPage' create a property 'showLoading' and set it to true or false where you whant:
//.... some source code
export class LoginPage implements OnInit {
showLoading;
userEmail = '';
userPassword = '';
loginUrl = 'login/';
loginMethod = 'POST';
postBody = {};
//.... some source code
loginUser() {
if (this.userEmail === '' || this.userPassword === '') {
this.presentToast('Email and password are required.');
} else {
this.showLoading = true;
this.postBody = {
email: this.userEmail,
password: this.userPassword
};
this.callApiService
.callApi(this.loginUrl, this.postBody, this.loginMethod)
.subscribe(
success => {
console.log(success);
this.showLoading = false;
},
error => {
console.log(error);
this.showLoading = false;
}
);
this.showLoading = false;
}
}
}
This works for me, I reuse the loading component on others pages!
Recommended reading: https://angular.io/start
I actually ran into this exact issue and for me the answer was just to use await.
The functions for both creating and dismissing loaders return promises. What I realized was happening is that the subscribe/promise rejection was halting all other promises from completing. Now, I just await both presenting and dismissing and I have no issue:
async getData() {
//await presenting
await this.presentLoading('Loading...');
try {
let response = await this.httpService.getData();
await this.loadingController.dismiss();
//...
catch(err) {
this.loadingController.dismiss();
//handle error
//...
}
}
async presentLoading(msg: string) {
const loading = await this.loadingController.create({
spinner: 'crescent',
message: msg
});
await loading.present();
}
I hope this simple solution helps!
I have guarded routes, and they depend on the value in the store.
loginUser() { // i call this function when user press "Login"
this.$http.get('http://localhost:3000/users')
.then(resp => {
return resp.json()
})
.then(resp => resp.filter(item => item.email === this.email && item.password === this.password))
.then(res => res.length > 0 ? (this.$router.push('/'), this.$store.commit('logUser')) : this.visible = true); // here i change value in the store
}
// Store
userLogged: false // state
mutations: {
logUser(state) { // i call this function when loginUser() function calls
state.userLogged = true
}
}
// router index.js
{
path: '/',
name: 'Home',
component: Home,
beforeEnter: AuthGuard
}
export default function (to, from, next) { // this is AuthGuard function
if (store.getters.getUser) { // get value from store
next();
} else {
next('/login')
}
}
// store
getters: { // get value
getUser(state) {
return state.userLogged
}
}
This works in the not appropriate way. I have to click the login button twice because getUser() get the state and logUser() changing userLogged simultaneously i.e. getUser() return false and when i am clicking second time its true. How can i fix this?
i'm working with a angular and i'm trying to apply some AuthGard on some Paths.
The problem is canActivate() renders the content before it checks with the SecurityContext, after a verification that no SecurityContext is applied then a redirection to the default page (login) page is applied.
This is the portion of code responsible for this.
app.routing.ts
{
path: 'admin',
canActivate: [AuthGard],
component: HomeComponent,
children : [
{
path: 'add-merchant-admin',
component : AddMerchantAdminComponent,
},
{
path: 'list-merchant-admin',
component : ListMerchantAdminComponent,
}
]
},
AuthGard.ts
canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
this._authService.getRoles().subscribe(
res => {
if (res.status == 200) {
this.roles = JSON.parse(res.text());
this.role = this.roles[0].authority;
localStorage.setItem('role', this.role);
if (this.role == 'ROLE_ADMIN') {
this._router.navigate(['admin']);
} else {
if (this.role == 'ROLE_ANONYMOUS') {
this._router.navigate(['login']);
this.error = false;
}
}
} else {
this._router.navigate(['login']);
this.error = true;
}
}, err => {
this._router.navigate(['login']);
this.error = true;
}
);
return !this.error;
};
AuthService
getRoles() {
let headers = new Headers({'Content-Type': 'application/json'});
let options = new RequestOptions({headers: headers, withCredentials: true});
return this.http.get('http://10.0.0.239:8080/**/**/RolesResource/getRole', options)
.map((res) => res)
.catch((error: any) => Observable.throw(error.text() || 'Server error'));
}
All Services are correctly injected,
Normally a redirection to protected area or default page should be applied after the verification is made using getRole() method.
The problem you are having is that this._authService.getRoles() makes a network call which is asynchronous. return !this.error; is being fired before the network call is being returned so !this.error does not change and is therefore still truthy.
To solve this issue you should be able to return an observable as follows:
return this._authService.getRoles().map(
res => {
if (res.status == 200) {
this.roles = JSON.parse(res.text());
this.role = this.roles[0].authority;
localStorage.setItem('role', this.role);
if (this.role == 'ROLE_ADMIN') {
this._router.navigate(['admin']);
} else {
if (this.role == 'ROLE_ANONYMOUS') {
this._router.navigate(['login']);
return false;
}
}
} else {
this._router.navigate(['login']);
return true;
}
}).catch((err) => {
this._router.navigate(['login']);
return Observable.of(false);
}
);
Something like this should work
canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this._authService.getRoles()
.map(response => JSON.parse(response.text())[0].authority)
.do(role => localStorage.setItem('role', role))
.map( role => role === 'ROLE_ADMIN')
.catch(() => this._router.navigate(['login']));
};
You can try with return observable, which can be updated either true or false.
instead of returning return !this.error; which is always true, try to return
canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this._authService.getRoles().map(
res => {
if (res.status == 200) {
this.roles = JSON.parse(res.text());
this.role = this.roles[0].authority;
localStorage.setItem('role', this.role);
if (this.role == 'ROLE_ADMIN') {
this._router.navigate(['admin']);
} else {
if (this.role == 'ROLE_ANONYMOUS') {
this._router.navigate(['login']);
return false;
}
}
} else {
this._router.navigate(['login']);
return true;
}
}, err => {
this._router.navigate(['login']);
return Observable.of(false);
}
);
};
Edited