From 223fbe688c512a5473a9e791acbcf694682f6ffa Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sun, 21 May 2023 18:53:40 +0300 Subject: [PATCH] RED-6713: show previous signed in domains --- src/assets/styles/common-base-screen.scss | 120 +++++++++--------- .../services/permissions-guard.service.ts | 4 +- src/lib/shared/logo/logo.component.ts | 4 +- src/lib/shared/spacer/spacer.component.scss | 15 +++ src/lib/shared/spacer/spacer.component.ts | 16 +++ src/lib/tenants/index.ts | 2 +- .../services/keycloak-status.service.ts | 2 +- .../tenants/services/tenant-id-interceptor.ts | 4 +- src/lib/tenants/services/tenants.service.ts | 77 ++++++++++- .../tenant-resolve.component.html | 25 ---- .../tenant-resolve.component.scss | 13 -- .../tenant-select.component.html | 55 ++++++++ .../tenant-select.component.scss | 65 ++++++++++ .../tenant-select.component.ts} | 25 ++-- src/lib/tenants/tenant.pipe.ts | 2 +- src/lib/tenants/tenants.module.ts | 16 ++- .../users/guards/iqser-auth-guard.service.ts | 2 +- .../users/guards/iqser-role-guard.service.ts | 2 +- 18 files changed, 322 insertions(+), 127 deletions(-) create mode 100644 src/lib/shared/spacer/spacer.component.scss create mode 100644 src/lib/shared/spacer/spacer.component.ts delete mode 100644 src/lib/tenants/tenant-resolve/tenant-resolve.component.html delete mode 100644 src/lib/tenants/tenant-resolve/tenant-resolve.component.scss create mode 100644 src/lib/tenants/tenant-select/tenant-select.component.html create mode 100644 src/lib/tenants/tenant-select/tenant-select.component.scss rename src/lib/tenants/{tenant-resolve/tenant-resolve.component.ts => tenant-select/tenant-select.component.ts} (55%) diff --git a/src/assets/styles/common-base-screen.scss b/src/assets/styles/common-base-screen.scss index 7486d77..9353ed2 100644 --- a/src/assets/styles/common-base-screen.scss +++ b/src/assets/styles/common-base-screen.scss @@ -1,74 +1,74 @@ @use 'common-mixins'; .top-bar { - height: var(--iqser-top-bar-height); - width: 100vw; + height: var(--iqser-top-bar-height); + width: 100vw; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 24px; + border-bottom: 1px solid var(--iqser-separator); + box-sizing: border-box; + background-color: var(--iqser-background); + + > *:not(:last-child) { + margin-right: 50px; + } + + .menu { display: flex; - justify-content: space-between; align-items: center; - padding: 0 24px; - border-bottom: 1px solid var(--iqser-separator); - box-sizing: border-box; - background-color: var(--iqser-background); + + &.right { + justify-content: flex-end; + } + } + + .menu-placeholder { + display: flex; + flex: 2; + } + + .buttons { + display: flex; + margin-right: 8px; > *:not(:last-child) { - margin-right: 50px; + margin-right: 14px; } + } +} - .logo { - @include common-mixins.clear-a; - display: flex; - align-items: center; - } +.logo { + @include common-mixins.clear-a; + display: flex; + align-items: center; +} - .app-name { - font-family: var(--iqser-app-name-font-family); - font-size: var(--iqser-app-name-font-size); - color: var(--iqser-app-name-color); - font-weight: 800; - white-space: nowrap; - } - - .menu { - display: flex; - align-items: center; - - &.right { - justify-content: flex-end; - } - } - - .menu-placeholder { - display: flex; - flex: 2; - } - - .buttons { - display: flex; - margin-right: 8px; - - > *:not(:last-child) { - margin-right: 14px; - } - } +.app-name { + font-family: var(--iqser-app-name-font-family); + font-size: var(--iqser-app-name-font-size); + color: var(--iqser-app-name-color); + font-weight: 800; + white-space: nowrap; } .dev-mode { - background-color: var(--iqser-primary); - color: var(--iqser-white); - font-size: 22px; - line-height: 16px; - text-align: center; - position: fixed; - top: 0; - z-index: 100; - right: 0; - height: var(--iqser-top-bar-height); - word-break: break-all; - display: flex; - justify-content: center; - align-items: center; - font-family: monospace; - width: 24px; - font-weight: bold; + background-color: var(--iqser-primary); + color: var(--iqser-white); + font-size: 22px; + line-height: 16px; + text-align: center; + position: fixed; + top: 0; + z-index: 100; + right: 0; + height: var(--iqser-top-bar-height); + word-break: break-all; + display: flex; + justify-content: center; + align-items: center; + font-family: monospace; + width: 24px; + font-weight: bold; } diff --git a/src/lib/permissions/services/permissions-guard.service.ts b/src/lib/permissions/services/permissions-guard.service.ts index 51875a3..e023583 100644 --- a/src/lib/permissions/services/permissions-guard.service.ts +++ b/src/lib/permissions/services/permissions-guard.service.ts @@ -85,10 +85,10 @@ export class IqserPermissionsGuard implements CanActivate, CanMatch, CanActivate } if (Array.isArray(_redirectTo)) { - return this._router.navigate([this._tenantsService.currentTenant, ..._redirectTo]); + return this._router.navigate([this._tenantsService.activeTenantId, ..._redirectTo]); } - return this._router.navigate([`${this._tenantsService.currentTenant}${_redirectTo}`]); + return this._router.navigate([`${this._tenantsService.activeTenantId}${_redirectTo}`]); } #checkRedirect(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) { diff --git a/src/lib/shared/logo/logo.component.ts b/src/lib/shared/logo/logo.component.ts index 9de965e..61871d0 100644 --- a/src/lib/shared/logo/logo.component.ts +++ b/src/lib/shared/logo/logo.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; @Component({ - selector: 'iqser-logo [icon]', + selector: 'iqser-logo', template: ` `, styles: [ ` @@ -21,5 +21,5 @@ import { MatIconModule } from '@angular/material/icon'; imports: [MatIconModule], }) export class LogoComponent { - @Input() icon!: string; + @Input({ required: true }) icon!: string; } diff --git a/src/lib/shared/spacer/spacer.component.scss b/src/lib/shared/spacer/spacer.component.scss new file mode 100644 index 0000000..d1f864b --- /dev/null +++ b/src/lib/shared/spacer/spacer.component.scss @@ -0,0 +1,15 @@ +:host { + --height: 20px; + + div { + height: #{var(--height)}; + + @media screen and (max-width: 1600px) { + height: calc(#{var(--height)} * 0.9); + } + + @media screen and (max-width: 1200px) { + height: calc(#{var(--height)} * 0.85); + } + } +} diff --git a/src/lib/shared/spacer/spacer.component.ts b/src/lib/shared/spacer/spacer.component.ts new file mode 100644 index 0000000..becc99a --- /dev/null +++ b/src/lib/shared/spacer/spacer.component.ts @@ -0,0 +1,16 @@ +import { Component, HostBinding, Input } from '@angular/core'; + +@Component({ + selector: 'iqser-spacer [height]', + template: '
', + styleUrls: ['./spacer.component.scss'], + standalone: true, +}) +export class SpacerComponent { + @Input({ required: true }) height!: number; + + @HostBinding('style.--height') + get heightStyle() { + return this.height + 'px'; + } +} diff --git a/src/lib/tenants/index.ts b/src/lib/tenants/index.ts index 4c6d75d..dfb088e 100644 --- a/src/lib/tenants/index.ts +++ b/src/lib/tenants/index.ts @@ -4,5 +4,5 @@ export * from './guards/if-not-logged-in.guard'; export * from './services'; export * from './tenant.pipe'; export * from './tenants.module'; -export * from './tenant-resolve/tenant-resolve.component'; +export * from './tenant-select/tenant-select.component'; export * from './services/keycloak-status.service'; diff --git a/src/lib/tenants/services/keycloak-status.service.ts b/src/lib/tenants/services/keycloak-status.service.ts index 1b49cf2..75c14e2 100644 --- a/src/lib/tenants/services/keycloak-status.service.ts +++ b/src/lib/tenants/services/keycloak-status.service.ts @@ -30,7 +30,7 @@ export class KeycloakStatusService { } createLoginUrl() { - const currentTenant = this.#tenantsService.currentTenant; + const currentTenant = this.#tenantsService.activeTenantId; if (currentTenant && window.location.href.indexOf('/' + currentTenant) > 0) { return window.location.href; } diff --git a/src/lib/tenants/services/tenant-id-interceptor.ts b/src/lib/tenants/services/tenant-id-interceptor.ts index ab5b242..b0f87af 100644 --- a/src/lib/tenants/services/tenant-id-interceptor.ts +++ b/src/lib/tenants/services/tenant-id-interceptor.ts @@ -8,9 +8,9 @@ export class TenantIdInterceptor implements HttpInterceptor { protected readonly _tenantsService = inject(TenantsService); intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (this._tenantsService.currentTenant) { + if (this._tenantsService.activeTenantId) { const updatedRequest = req.clone({ - setHeaders: { 'X-TENANT-ID': this._tenantsService.currentTenant }, + setHeaders: { 'X-TENANT-ID': this._tenantsService.activeTenantId }, }); return next.handle(updatedRequest); diff --git a/src/lib/tenants/services/tenants.service.ts b/src/lib/tenants/services/tenants.service.ts index b95c6a0..bda98ba 100644 --- a/src/lib/tenants/services/tenants.service.ts +++ b/src/lib/tenants/services/tenants.service.ts @@ -2,7 +2,7 @@ import { computed, inject, Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { NGXLogger } from 'ngx-logger'; -import { BASE_HREF } from '../../utils'; +import { BASE_HREF, List } from '../../utils'; import { Router } from '@angular/router'; export interface IBaseTenant { @@ -11,6 +11,22 @@ export interface IBaseTenant { readonly guid: string; } +export interface IStoredTenantId { + readonly tenantId: string; + readonly email: string; +} + +export type StoredTenantIds = List; + +export interface IStoredTenant { + readonly tenant: IBaseTenant; + readonly email: string; +} + +export type StoredTenants = IStoredTenant[]; + +const STORED_TENANTS_KEY = 'red-stored-tenants'; + @Injectable({ providedIn: 'root' }) export class TenantsService { readonly tenants = signal(undefined); @@ -31,8 +47,11 @@ export class TenantsService { key: localStorage.key.bind(localStorage), }; readonly #activeTenantId = signal(''); + readonly activeTenant = computed(() => { + return this.tenants()?.find(t => t.tenantId === this.#activeTenantId()); + }); - get currentTenant() { + get activeTenantId() { return this.#activeTenantId(); } @@ -82,11 +101,61 @@ export class TenantsService { } this.#mutateStorage(tenantId); - this.#setCurrentTenantId(tenantId); + this.#setActiveTenantId(tenantId); return true; } - #setCurrentTenantId(tenantId: string) { + storeTenant(email: string) { + if (!email) { + this.#logger.warn('[TENANTS] Email is null, skip storing'); + return; + } + + const storedTenants = this.getStoredTenants(); + const activeTenant = this.activeTenant(); + const existing = storedTenants.find(s => s.email === email && s.tenant.tenantId === activeTenant?.tenantId); + if (existing) { + this.#logger.info('[TENANTS] Stored tenant exists: ', storedTenants); + return; + } + + if (!activeTenant) { + this.#logger.error('[TENANTS] Active tenant is null when storing tenants'); + return; + } + + storedTenants.push({ tenant: activeTenant, email: email }); + const serializableTenants = storedTenants.map(s => ({ + tenantId: s.tenant.tenantId, + email: s.email, + })); + this.#storageReference.setItem(STORED_TENANTS_KEY, JSON.stringify(serializableTenants)); + this.#logger.info('[TENANTS] Stored tenants: ', storedTenants); + } + + getStoredTenants(): StoredTenants { + const rawStoredTenants = this.#storageReference.getItem(STORED_TENANTS_KEY); + const stored = JSON.parse(rawStoredTenants ?? '[]') as StoredTenantIds; + const tenants = this.tenants(); + if (!tenants) { + this.#logger.error('[TENANTS] Tenants data is null when retrieving stored tenants'); + return []; + } + + const trueStored: StoredTenants = []; + for (const s of stored) { + const tenant = tenants.find(t => t.tenantId === s.tenantId); + if (!tenant) { + this.#logger.warn(`[TENANTS] Stored tenant with id ${tenant} not found, skipping`); + continue; + } + trueStored.push({ tenant, email: s.email }); + } + + return trueStored; + } + + #setActiveTenantId(tenantId: string) { this.#logger.info('[TENANTS] Set current tenant id: ', tenantId); this.#activeTenantId.set(tenantId); } diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html b/src/lib/tenants/tenant-resolve/tenant-resolve.component.html deleted file mode 100644 index 9a9580f..0000000 --- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html +++ /dev/null @@ -1,25 +0,0 @@ -
-
-
- -
- - - - {{ option.displayName || option.tenantId }} - - - -
- - -
-
diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss b/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss deleted file mode 100644 index 4838f02..0000000 --- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -.tenant-section { - display: flex; - align-items: flex-start; - justify-content: center; - width: 100vw; - height: 100vh; - - margin-top: 32px; - - > form > * { - padding: 8px; - } -} diff --git a/src/lib/tenants/tenant-select/tenant-select.component.html b/src/lib/tenants/tenant-select/tenant-select.component.html new file mode 100644 index 0000000..35161cb --- /dev/null +++ b/src/lib/tenants/tenant-select/tenant-select.component.html @@ -0,0 +1,55 @@ +
+ + + + +
+ +
+
+ + +
+ {{ stored.tenant.displayName }} + {{ stored.email }} +
+ + +
+
+ +
+ + + +
+
+ +
+ {{ url }} + +
+
+ + + +
+ + +
+
+ + +
+
diff --git a/src/lib/tenants/tenant-select/tenant-select.component.scss b/src/lib/tenants/tenant-select/tenant-select.component.scss new file mode 100644 index 0000000..5389816 --- /dev/null +++ b/src/lib/tenants/tenant-select/tenant-select.component.scss @@ -0,0 +1,65 @@ +.tenant-section { + display: flex; + align-items: center; + flex-direction: column; + width: 100vw; + height: 100vh; + + margin-top: 32px; +} + +.actions { + display: flex; + align-items: center; + justify-content: flex-end; +} + +.stored-tenant-card { + width: 450px; + height: 90px; +} + +.card { + display: flex; + align-items: center; + border-radius: 10px; +} + +.card-icon { + margin-right: 20px; + margin-left: 20px; +} + +.upside-down { + transform: rotate(180deg); +} + +.card-content { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.pb-30 { + padding-bottom: 30px; +} + +.input-card { + width: 600px; + height: 65px; +} + +.url { + margin-left: 20px; + margin-right: 10px; +} + +form { + display: flex; + align-items: center; + flex-grow: 1; +} + +.subheading { + font-size: 17px; +} diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts b/src/lib/tenants/tenant-select/tenant-select.component.ts similarity index 55% rename from src/lib/tenants/tenant-resolve/tenant-resolve.component.ts rename to src/lib/tenants/tenant-select/tenant-select.component.ts index 99000d1..c3802a2 100644 --- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts +++ b/src/lib/tenants/tenant-select/tenant-select.component.ts @@ -4,26 +4,27 @@ import { FormBuilder, Validators } from '@angular/forms'; import { TenantsService } from '../services'; import { IconButtonTypes } from '../../buttons'; import { LoadingService } from '../../loading'; +import { Title } from '@angular/platform-browser'; +import { getConfig } from '../../services'; +import { BASE_HREF } from '../../utils'; @Component({ - templateUrl: './tenant-resolve.component.html', - styleUrls: ['./tenant-resolve.component.scss'], + templateUrl: './tenant-select.component.html', + styleUrls: ['./tenant-select.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TenantResolveComponent { +export class TenantSelectComponent { readonly loadingService = inject(LoadingService); readonly iconButtonTypes = IconButtonTypes; - readonly form; + readonly form = inject(FormBuilder).group({ + // eslint-disable-next-line @typescript-eslint/unbound-method + tenantId: ['', Validators.required], + }); + readonly url = getConfig().OAUTH_URL.replace('https://', '').replace('/auth', '') + inject(BASE_HREF) + '/'; protected readonly _tenantsService = inject(TenantsService); + protected readonly storedTenants = this._tenantsService.getStoredTenants(); + protected readonly titleService = inject(Title); readonly #router = inject(Router); - readonly #formBuilder = inject(FormBuilder); - - constructor() { - this.form = this.#formBuilder.group({ - // eslint-disable-next-line @typescript-eslint/unbound-method - tenantId: ['', Validators.required], - }); - } updateTenantSelection() { const tenant = this.form.controls.tenantId.value; diff --git a/src/lib/tenants/tenant.pipe.ts b/src/lib/tenants/tenant.pipe.ts index 5a13295..8b581b7 100644 --- a/src/lib/tenants/tenant.pipe.ts +++ b/src/lib/tenants/tenant.pipe.ts @@ -14,6 +14,6 @@ export class TenantPipe implements PipeTransform { return undefined; } const _value = Array.isArray(value) ? value.join('/') : value; - return '/' + this.#tenantsService.currentTenant + _value; + return '/' + this.#tenantsService.activeTenantId + _value; } } diff --git a/src/lib/tenants/tenants.module.ts b/src/lib/tenants/tenants.module.ts index 0bfe705..2a5aa88 100644 --- a/src/lib/tenants/tenants.module.ts +++ b/src/lib/tenants/tenants.module.ts @@ -3,14 +3,20 @@ import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { MatDialogModule } from '@angular/material/dialog'; import { CircleButtonComponent, IconButtonComponent } from '../buttons'; -import { TenantResolveComponent } from './tenant-resolve/tenant-resolve.component'; +import { TenantSelectComponent } from './tenant-select/tenant-select.component'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { ReactiveFormsModule } from '@angular/forms'; import { TenantIdInterceptor, TenantIdResponseInterceptor, TenantsService } from './services'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { MatCardModule } from '@angular/material/card'; +import { LogoComponent } from '../shared/logo/logo.component'; +import { SpacerComponent } from '../shared/spacer/spacer.component'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { RouterLink } from '@angular/router'; -const components = [TenantResolveComponent]; +const components = [TenantSelectComponent]; @NgModule({ declarations: [...components], @@ -23,6 +29,12 @@ const components = [TenantResolveComponent]; MatSelectModule, IconButtonComponent, ReactiveFormsModule, + MatCardModule, + LogoComponent, + SpacerComponent, + MatIconModule, + MatButtonModule, + RouterLink, ], exports: [...components], }) diff --git a/src/lib/users/guards/iqser-auth-guard.service.ts b/src/lib/users/guards/iqser-auth-guard.service.ts index 413557b..fa3971f 100644 --- a/src/lib/users/guards/iqser-auth-guard.service.ts +++ b/src/lib/users/guards/iqser-auth-guard.service.ts @@ -35,7 +35,7 @@ export class IqserAuthGuard extends KeycloakAuthGuard { const user = await this._userService.loadCurrentUser(); if (user?.hasAnyRole && route.routeConfig?.path === 'auth-error') { - await this._router.navigate([`/${this._tenantsService.currentTenant}/main`]); + await this._router.navigate([`/${this._tenantsService.activeTenantId}/main`]); return false; } diff --git a/src/lib/users/guards/iqser-role-guard.service.ts b/src/lib/users/guards/iqser-role-guard.service.ts index 810b630..0c071bc 100644 --- a/src/lib/users/guards/iqser-role-guard.service.ts +++ b/src/lib/users/guards/iqser-role-guard.service.ts @@ -14,7 +14,7 @@ export class IqserRoleGuard implements CanActivate { async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const currentUser = this._userService.currentUser; if (!currentUser || !currentUser.hasAnyRole) { - await this._router.navigate([`/${this._tenantsService.currentTenant}/auth-error`]); + await this._router.navigate([`/${this._tenantsService.activeTenantId}/auth-error`]); this._loadingService.stop(); return false; }