rename auth module to users module & add initial avatar & user button

This commit is contained in:
Dan Percic 2022-07-28 00:46:19 +03:00
parent 1bc195eff6
commit b6536f736b
25 changed files with 372 additions and 130 deletions

View File

@ -17,4 +17,4 @@ export * from './lib/search';
export * from './lib/empty-states';
export * from './lib/scrollbar';
export * from './lib/caching';
export * from './lib/auth';
export * from './lib/users';

View File

@ -1,79 +0,0 @@
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 = async () => {
await keyCloakService.logout();
};
}
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,
{
provide: APP_INITIALIZER,
useFactory: keycloakInitializer,
multi: true,
deps: [KeycloakService, BaseConfigService, BASE_HREF],
},
],
})
export class AuthModule extends ModuleWithOptions {
static forRoot<
Interface extends IIqserUser,
Class extends IqserUser & Interface,
UserService extends BaseUserService<Interface, Class>,
RolesGuard extends RoleGuard = RoleGuard,
>(options: AuthModuleOptions<Interface, Class, UserService, RolesGuard>): ModuleWithProviders<AuthModule> {
const userService = this._getService(BaseUserService, DefaultUserService, options.existingUserService);
const roleGuard = this._getService(RoleGuard, RoleGuard, options.existingRoleGuard);
return {
ngModule: AuthModule,
providers: [userService, roleGuard],
};
}
}

View File

@ -1,12 +0,0 @@
export * from './types/user.response';
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

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

View File

@ -0,0 +1,13 @@
<div *ngIf="_user && _user | name: namePipeOptions as userName" class="wrapper">
<div
[className]="colorClass + ' oval ' + size + (hasBorder ? ' border' : '')"
[matTooltipPosition]="tooltipPosition"
[matTooltip]="userName"
>
{{ _user | name: { showInitials: true } }}
</div>
<div *ngIf="withName" [class.disabled]="disabled" class="clamp-1 username" id="avatarUsername">
{{ userName }}
</div>
</div>

View File

@ -0,0 +1,17 @@
.wrapper {
display: flex;
align-items: center;
width: fit-content;
.username {
margin-left: 6px;
&.disabled {
opacity: 0.7;
}
}
}
.border {
border: 1px solid var(--iqser-grey-7);
}

View File

@ -0,0 +1,88 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { IqserUserService } from '../../services/iqser-user.service';
import { NamePipeOptions } from '../../types/name-pipe-options';
import { IqserUser } from '../../iqser-user.model';
import { IIqserUser } from '../../types/user.response';
@Component({
selector: 'iqser-initials-avatar',
templateUrl: './initials-avatar.component.html',
styleUrls: ['./initials-avatar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InitialsAvatarComponent<Interface extends IIqserUser = IIqserUser, Class extends IqserUser & Interface = IqserUser & Interface>
implements OnInit, OnChanges
{
@Input() color = 'lightgray';
@Input() size: 'small' | 'large' = 'small';
@Input() withName = false;
@Input() showYou = false;
@Input() tooltipPosition: 'below' | 'above' = 'above';
@Input() defaultValue: string = this._translateService.instant('initials-avatar.unassigned');
colorClass?: string;
namePipeOptions?: NamePipeOptions;
constructor(private readonly _userService: IqserUserService<Interface, Class>, private readonly _translateService: TranslateService) {}
_user?: Class;
@Input()
set user(user: Class | string) {
if (typeof user === 'string') {
this._user = this._userService.find(user);
} else {
this._user = user;
}
}
get hasBorder(): boolean {
return !!this._user && !this.isCurrentUser && this.showBorderCondition(this._user);
}
get disabled(): boolean {
return !!this._user && !this._isSystemUser && !this._user.hasAnyRole;
}
get isCurrentUser(): boolean {
return this._userService.currentUser?.id === this._user?.id;
}
private get _colorClass() {
if (this.isCurrentUser) {
return 'primary-white';
}
if (this.disabled) {
return 'inactive';
}
if (this.color.includes('-')) {
return this.color;
}
return `${this.color}-dark`;
}
private get _isSystemUser() {
return this._user?.id?.toLowerCase() === 'system';
}
@Input() showBorderCondition: <T extends Class = Class>(user: T) => boolean = () => false;
ngOnChanges(): void {
if (this._isSystemUser) {
this.colorClass = 'primary-white primary';
return;
}
this.colorClass = this._colorClass;
}
ngOnInit(): void {
this.namePipeOptions = {
showYou: this.showYou,
defaultValue: this.defaultValue,
};
}
}

View File

@ -0,0 +1,7 @@
<button [class.overlay]="showDot" mat-button>
<ng-content></ng-content>
<mat-icon svgIcon="iqser:arrow-down"></mat-icon>
</button>
<div *ngIf="showDot" class="dot"></div>

View File

@ -0,0 +1,24 @@
@use '../../../../assets/styles/common-buttons';
:host {
@extend .user-button;
min-width: fit-content;
button {
padding: 0 10px 0 5px;
mat-icon {
width: 14px;
}
}
&[aria-expanded='true'] {
button {
background: rgba(var(--iqser-primary-rgb), 0.1);
}
}
.dot {
left: -2px;
}
}

View File

@ -0,0 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'iqser-user-button',
templateUrl: './user-button.component.html',
styleUrls: ['./user-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserButtonComponent {
@Input() showDot = false;
}

View File

@ -1,16 +1,16 @@
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';
import { BaseConfigService } from '../../services';
import { IqserUserService } from '../services/iqser-user.service';
@Injectable()
export class AuthGuard extends KeycloakAuthGuard {
export class IqserAuthGuard extends KeycloakAuthGuard {
constructor(
protected readonly _router: Router,
protected readonly _keycloak: KeycloakService,
private readonly _configService: BaseConfigService,
private readonly _userService: BaseUserService,
private readonly _userService: IqserUserService,
) {
super(_router, _keycloak);
}

View File

@ -1,13 +1,13 @@
import { inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { LoadingService } from '../loading';
import { BaseUserService } from './base-user.service';
import { LoadingService } from '../../loading';
import { IqserUserService } from '../services/iqser-user.service';
@Injectable()
export class RoleGuard implements CanActivate {
export class IqserRoleGuard implements CanActivate {
protected readonly _router = inject(Router);
protected readonly _loadingService = inject(LoadingService);
protected readonly _userService = inject(BaseUserService);
protected readonly _userService = inject(IqserUserService);
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser;

15
src/lib/users/index.ts Normal file
View File

@ -0,0 +1,15 @@
export * from './types/user.response';
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/iqser-users-module-options';
export * from './types/name-pipe-options';
export * from './iqser-user.model';
export * from './services/iqser-user.service';
export * from './services/default-user.service';
export * from './iqser-users.module';
export * from './guards/iqser-auth-guard.service';
export * from './guards/iqser-role-guard.service';
export * from './components/user-button/user-button.component';
export * from './components/initials-avatar/initials-avatar.component';

View File

@ -0,0 +1,90 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakOptions, KeycloakService } from 'keycloak-angular';
import { DefaultUserService } from './services/default-user.service';
import { IIqserUser } from './types/user.response';
import { IqserUserService } from './services/iqser-user.service';
import { BASE_HREF, ModuleWithOptions } from '../utils';
import { IqserUsersModuleOptions } from './types/iqser-users-module-options';
import { IqserUser } from './iqser-user.model';
import { IqserRoleGuard } from './guards/iqser-role-guard.service';
import { IqserAuthGuard } from './guards/iqser-auth-guard.service';
import { BaseConfigService } from '../services';
import { NamePipe } from './name.pipe';
import { InitialsAvatarComponent } from './components/initials-avatar/initials-avatar.component';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CommonModule } from '@angular/common';
import { UserButtonComponent } from './components/user-button/user-button.component';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
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 = async () => {
await keyCloakService.logout();
};
}
export function keycloakInitializer(
keycloakService: KeycloakService,
configService: BaseConfigService,
baseUrl: string,
): () => Promise<void> {
const x = keycloakService.init(getKeycloakOptions(baseUrl, configService));
return () => x.then(() => configureAutomaticRedirectToLoginScreen(keycloakService));
}
const components = [NamePipe, InitialsAvatarComponent, UserButtonComponent];
@NgModule({
imports: [HttpClientModule, KeycloakAngularModule, MatTooltipModule, CommonModule, MatIconModule, MatButtonModule],
declarations: [...components],
exports: [...components],
})
export class IqserUsersModule extends ModuleWithOptions {
static forRoot<
Interface extends IIqserUser,
Class extends IqserUser & Interface,
UserService extends IqserUserService<Interface, Class>,
RolesGuard extends IqserRoleGuard = IqserRoleGuard,
>(options: IqserUsersModuleOptions<Interface, Class, UserService, RolesGuard>): ModuleWithProviders<IqserUsersModule> {
const userService = this._getService(IqserUserService, DefaultUserService, options.existingUserService);
const roleGuard = this._getService(IqserRoleGuard, IqserRoleGuard, options.existingRoleGuard);
return {
ngModule: IqserUsersModule,
providers: [
userService,
roleGuard,
IqserAuthGuard,
{
provide: APP_INITIALIZER,
useFactory: keycloakInitializer,
multi: true,
deps: [KeycloakService, BaseConfigService, BASE_HREF],
},
],
};
}
}

View File

@ -0,0 +1,63 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { IqserUserService } from './services/iqser-user.service';
import { NamePipeOptions } from './types/name-pipe-options';
import { IqserUser } from './iqser-user.model';
function getInitials(name: string) {
if (name.toLowerCase() === 'system') {
return 'SY';
}
const splittedName = name.split(' ').filter(value => value !== ' ' && value !== '');
return splittedName
.filter((_value, index) => index < 2)
.map(str => str[0])
.join('');
}
@Pipe({
name: 'name',
})
export class NamePipe implements PipeTransform {
protected readonly _defaultOptions: Required<NamePipeOptions> = {
defaultValue: this._translateService.instant('unknown') as string,
showYou: false,
showInitials: false,
};
constructor(private readonly _userService: IqserUserService, private readonly _translateService: TranslateService) {}
transform(value: IqserUser | string, options: NamePipeOptions = this._defaultOptions): string {
let name: string | undefined;
if (!value) {
return this._getDefaultName(options);
}
name = value ? this._userService.getName(value) : options.defaultValue;
if (!name) {
return this._getDefaultName(options);
}
if (options.showYou && this._isCurrentUser(value)) {
name = `${name} (${this._translateService.instant('initials-avatar.you')})`;
}
return options.showInitials ? getInitials(name) : name;
}
protected _getDefaultName(options: NamePipeOptions) {
if (options.showInitials) {
return '?';
}
return options.defaultValue ?? this._defaultOptions.defaultValue;
}
protected _isCurrentUser(user: IqserUser | string): boolean {
const userId = typeof user === 'string' ? user : user.id;
return this._userService.currentUser?.id === userId;
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { IqserUser } from './user.model';
import { BaseUserService } from './base-user.service';
import { IqserUserService } from './iqser-user.service';
import { IqserUser } from '../iqser-user.model';
@Injectable()
export class DefaultUserService extends BaseUserService {
export class DefaultUserService extends IqserUserService {
protected readonly _defaultModelPath = 'user';
protected readonly _entityClass = IqserUser;
protected readonly _rolesFilter = () => true;

View File

@ -3,20 +3,20 @@ import { KeycloakService } from 'keycloak-angular';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import jwt_decode from 'jwt-decode';
import { BASE_HREF, List, mapEach, RequiredParam, Validate } from '../utils';
import { QueryParam } from '../services';
import { CacheApiService } from '../caching';
import { EntitiesService } from '../listing';
import { IqserUser } from './user.model';
import { IIqserUser } from './types/user.response';
import { ICreateUserRequest } from './types/create-user.request';
import { IResetPasswordRequest } from './types/reset-password.request';
import { IMyProfileUpdateRequest } from './types/my-profile-update.request';
import { IProfileUpdateRequest } from './types/profile-update.request';
import { BASE_HREF, List, mapEach, RequiredParam, Validate } from '../../utils';
import { QueryParam } from '../../services';
import { CacheApiService } from '../../caching';
import { EntitiesService } from '../../listing';
import { IIqserUser } from '../types/user.response';
import { ICreateUserRequest } from '../types/create-user.request';
import { IResetPasswordRequest } from '../types/reset-password.request';
import { IMyProfileUpdateRequest } from '../types/my-profile-update.request';
import { IProfileUpdateRequest } from '../types/profile-update.request';
import { KeycloakProfile } from 'keycloak-js';
import { IqserUser } from '../iqser-user.model';
@Injectable()
export abstract class BaseUserService<
export abstract class IqserUserService<
Interface extends IIqserUser = IIqserUser,
Class extends IqserUser & Interface = IqserUser & Interface,
> extends EntitiesService<Interface, Class> {
@ -135,5 +135,5 @@ export abstract class BaseUserService<
}
export function getCurrentUser<Class extends IqserUser = IqserUser>() {
return inject<BaseUserService<IIqserUser, Class>>(BaseUserService).currentUser;
return inject<IqserUserService<IIqserUser, Class>>(IqserUserService).currentUser;
}

View File

@ -0,0 +1,15 @@
import { IqserUserService } from '../services/iqser-user.service';
import { IIqserUser } from './user.response';
import { Type } from '@angular/core';
import { IqserRoleGuard } from '../guards/iqser-role-guard.service';
import { IqserUser } from '../iqser-user.model';
export interface IqserUsersModuleOptions<
I extends IIqserUser = IIqserUser,
C extends IqserUser & I = IqserUser & I,
T extends IqserUserService<I, C> = IqserUserService<I, C>,
R extends IqserRoleGuard = IqserRoleGuard,
> {
existingUserService?: Type<T>;
existingRoleGuard?: Type<R>;
}

View File

@ -0,0 +1,5 @@
export interface NamePipeOptions {
showYou?: boolean;
showInitials?: boolean;
defaultValue?: string;
}