add auth module

This commit is contained in:
Dan Percic 2022-07-26 18:40:41 +03:00
parent f1fa9464a9
commit 21c581bc61
11 changed files with 176 additions and 24 deletions

View File

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
import { BaseConfigService } from '../services';
import { BaseUserService } from './base-user.service';
@Injectable()
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected readonly _router: Router,
protected readonly _keycloak: KeycloakService,
private readonly _configService: BaseConfigService,
private readonly _userService: BaseUserService,
) {
super(_router, _keycloak);
}
async isAccessAllowed(route: ActivatedRouteSnapshot): Promise<boolean> {
if (!this.authenticated) {
const kcIdpHint = route.queryParamMap.get('kc_idp_hint');
await this._keycloak.login({
idpHint: kcIdpHint ?? this._configService.values.OAUTH_IDP_HINT,
redirectUri: window.location.href,
});
return false;
}
await this._userService.loadCurrentUser();
return true;
}
}

View File

@ -0,0 +1,76 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular';
import { DefaultUserService } from './default-user.service';
import { IIqserUser } from './types/user.response';
import { BaseUserService } from './base-user.service';
import { BASE_HREF, ModuleWithOptions } from '../utils';
import { AuthModuleOptions } from './types/auth-module-options';
import { IqserUser } from './user.model';
import { RoleGuard } from './role.guard';
import { AuthGuard } from './auth.guard';
import { BaseConfigService } from '../services';
function getKeycloakOptions(baseUrl: string, configService: BaseConfigService): KeycloakOptions {
let url: string = configService.values.OAUTH_URL;
url = url.replace(/\/$/, ''); // remove trailing slash
const realm = url.substring(url.lastIndexOf('/') + 1, url.length);
url = url.substring(0, url.lastIndexOf('/realms'));
return {
config: {
url: url,
realm: realm,
clientId: configService.values.OAUTH_CLIENT_ID,
},
initOptions: {
checkLoginIframe: false,
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
flow: 'standard',
},
enableBearerInterceptor: true,
};
}
function configureAutomaticRedirectToLoginScreen(keyCloakService: KeycloakService) {
keyCloakService.getKeycloakInstance().onAuthRefreshError = () => {
void keyCloakService.logout().then();
};
}
export function keycloakInitializer(
keycloakService: KeycloakService,
configService: BaseConfigService,
baseUrl: string,
): () => Promise<void> {
const x = keycloakService.init(getKeycloakOptions(baseUrl, configService));
return () => x.then(() => configureAutomaticRedirectToLoginScreen(keycloakService));
}
@NgModule({
imports: [CommonModule, HttpClientModule, KeycloakAngularModule],
providers: [
AuthGuard,
RoleGuard,
{
provide: APP_INITIALIZER,
useFactory: keycloakInitializer,
multi: true,
deps: [KeycloakService, BaseConfigService, BASE_HREF],
},
],
})
export class AuthModule extends ModuleWithOptions {
static forRoot<I extends IIqserUser, C extends IqserUser & I, S extends BaseUserService<I, C>>(
options: AuthModuleOptions<I, C, S>,
): ModuleWithProviders<AuthModule> {
const userService = this._getService(BaseUserService, DefaultUserService, options.existingUserService);
return {
ngModule: AuthModule,
providers: [userService],
};
}
}

View File

@ -16,10 +16,10 @@ import { IProfileUpdateRequest } from './types/profile-update.request';
import { KeycloakProfile } from 'keycloak-js';
@Injectable()
export abstract class BaseUserService<Interface extends IIqserUser, Class extends IqserUser & Interface> extends EntitiesService<
Interface,
Class
> {
export abstract class BaseUserService<
Interface extends IIqserUser = IIqserUser,
Class extends IqserUser & Interface = IqserUser & Interface,
> extends EntitiesService<Interface, Class> {
readonly currentUser$: Observable<Class | undefined>;
protected abstract readonly _defaultModelPath: string;
protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;

View File

@ -1,10 +1,9 @@
import { Injectable } from '@angular/core';
import { IqserUser } from './user.model';
import { IIqserUser } from './types/user.response';
import { BaseUserService } from './base-user.service';
@Injectable()
export class DefaultUserService extends BaseUserService<IIqserUser, IqserUser> {
export class DefaultUserService extends BaseUserService {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = IqserUser;
}

View File

@ -3,5 +3,10 @@ export * from './types/create-user.request';
export * from './types/reset-password.request';
export * from './types/my-profile-update.request';
export * from './types/profile-update.request';
export * from './types/auth-module-options';
export * from './user.model';
export * from './base-user.service';
export * from './default-user.service';
export * from './auth.module';
export * from './auth.guard';
export * from './role.guard';

View File

@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { LoadingService } from '../loading';
import { BaseUserService } from './base-user.service';
@Injectable()
export class RoleGuard implements CanActivate {
constructor(
private readonly _router: Router,
private readonly _loadingService: LoadingService,
private readonly _userService: BaseUserService,
) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser;
if (!currentUser || !currentUser.hasAnyRole) {
await this._router.navigate(['/auth-error']);
this._loadingService.stop();
return false;
}
const requiredRoles = route.data['requiredRoles'] as string[];
if (requiredRoles) {
return currentUser.hasAny(requiredRoles);
}
return true;
}
}

View File

@ -1,8 +1,8 @@
import { Type } from '@angular/core';
import { BaseUserService } from '../base-user.service';
import { IIqserUser } from './user.response';
import { IqserUser } from '../user.model';
import { Type } from '@angular/core';
export interface AuthModuleOptions<I extends IIqserUser, C extends IqserUser & I, T extends BaseUserService<I, C> = BaseUserService<I, C>> {
existingUserService: Type<T>;
existingUserService?: Type<T>;
}

View File

@ -1,4 +1,4 @@
import { ModuleWithProviders, NgModule, Provider, Type } from '@angular/core';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@ -38,6 +38,7 @@ import { CommonUiOptions } from './utils/types/common-ui-options';
import { BaseUserPreferenceService } from './services';
import { BaseConfigService } from './services/base-config.service';
import { DefaultUserPreferenceService } from './services/default-user-preference.service';
import { ModuleWithOptions } from './utils/module-with-options';
const matModules = [MatIconModule, MatProgressSpinnerModule, MatButtonModule, MatDialogModule, MatCheckboxModule, MatTooltipModule];
const modules = [
@ -75,29 +76,25 @@ const pipes = [SortByPipe, HumanizePipe, CapitalizePipe];
imports: [CommonModule, ...matModules, ...modules, FormsModule, ReactiveFormsModule, KeycloakAngularModule, MatProgressBarModule],
exports: [...components, ...pipes, ...modules],
})
export class CommonUiModule {
export class CommonUiModule extends ModuleWithOptions {
static forRoot<T extends BaseUserPreferenceService, C extends BaseConfigService>(
options?: CommonUiOptions<T, C>,
options: CommonUiOptions<T, C>,
): ModuleWithProviders<CommonUiModule> {
const userPreferenceService = this._getService(
BaseUserPreferenceService,
DefaultUserPreferenceService,
options?.existingUserPreferenceService,
options.existingUserPreferenceService,
);
return {
ngModule: CommonUiModule,
providers: [userPreferenceService],
providers: [
userPreferenceService,
{
provide: BaseConfigService,
useFactory: options.configServiceFactory,
},
],
};
}
private static _getService<B, D, E extends B>(base: B, _default: Type<D>, existing?: E): Provider {
if (existing) {
return {
provide: base,
useExisting: existing,
};
}
return { provide: base, useClass: _default };
}
}

View File

@ -18,5 +18,6 @@ export * from './custom-route-reuse.strategy';
export * from './headers-configuration';
export * from './context.component';
export * from './tokens';
export * from './module-with-options';
export * from './base-app-config';
export * from './types/common-ui-options';

View File

@ -0,0 +1,14 @@
import { Provider, Type } from '@angular/core';
export class ModuleWithOptions {
protected static _getService<B, D, E>(base: B, _default: Type<D>, existing?: E): Provider {
if (existing) {
return {
provide: base,
useExisting: existing,
};
}
return { provide: base, useClass: _default };
}
}

View File

@ -2,6 +2,6 @@ import { BaseConfigService, BaseUserPreferenceService } from '../../services';
import { Type } from '@angular/core';
export interface CommonUiOptions<T extends BaseUserPreferenceService, C extends BaseConfigService> {
existingUserPreferenceService: Type<T>;
existingUserPreferenceService?: Type<T>;
configServiceFactory: () => C;
}