diff --git a/src/app/adf-user-management/df-login/df-login.component.ts b/src/app/adf-user-management/df-login/df-login.component.ts index 85f955fc..41e9e74f 100644 --- a/src/app/adf-user-management/df-login/df-login.component.ts +++ b/src/app/adf-user-management/df-login/df-login.component.ts @@ -32,6 +32,8 @@ import { UntilDestroy } from '@ngneat/until-destroy'; import { DfThemeService } from 'src/app/shared/services/df-theme.service'; import { CommonModule } from '@angular/common'; import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; +import { PopupOverlayService } from 'src/app/shared/components/df-popup/popup-overlay.service'; + @UntilDestroy({ checkProperties: true }) @Component({ selector: 'df-user-login', @@ -58,6 +60,7 @@ import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; ], }) export class DfLoginComponent implements OnInit { + private readonly MINIMUM_PASSWORD_LENGTH = 17; alertMsg = ''; showAlert = false; alertType: AlertType = 'error'; @@ -78,7 +81,8 @@ export class DfLoginComponent implements OnInit { private authService: DfAuthService, private router: Router, private themeService: DfThemeService, - private snackbarService: DfSnackbarService + private snackbarService: DfSnackbarService, + private popupOverlay: PopupOverlayService ) { this.loginForm = this.fb.group({ services: [''], @@ -132,6 +136,8 @@ export class DfLoginComponent implements OnInit { if (this.loginForm.invalid) { return; } + + const isPasswordTooShort = this.loginForm.value.password.length < this.MINIMUM_PASSWORD_LENGTH; const credentials: LoginCredentials = { password: this.loginForm.value.password, }; @@ -143,17 +149,31 @@ export class DfLoginComponent implements OnInit { } else { credentials.email = this.loginForm.value.email; } + this.authService .login(credentials) .pipe( catchError(err => { - this.alertMsg = err.error.error.message; - this.showAlert = true; + if (err.status === 401 && isPasswordTooShort) { + this.popupOverlay.open({ + message: `It looks like your password is too short. Our new system requires at least ${this.MINIMUM_PASSWORD_LENGTH} characters. Please reset your password to continue.`, + showRemindMeLater: false, + }); + } else { + this.alertMsg = err.error?.error?.message || 'Login failed'; + this.showAlert = true; + } return throwError(() => new Error(err)); }) ) .subscribe(() => { this.showAlert = false; + if (isPasswordTooShort) { + this.popupOverlay.open({ + message: `Your current password is shorter than recommended (less than ${this.MINIMUM_PASSWORD_LENGTH} characters). For better security, we recommend updating your password to a longer one.`, + showRemindMeLater: true, + }); + } this.router.navigate([ROUTES.HOME]); }); } diff --git a/src/app/adf-user-management/services/df-auth.service.ts b/src/app/adf-user-management/services/df-auth.service.ts index 6d49db53..fa0c4cfe 100644 --- a/src/app/adf-user-management/services/df-auth.service.ts +++ b/src/app/adf-user-management/services/df-auth.service.ts @@ -104,7 +104,7 @@ export class DfAuthService { ); } - logout() { + logout(redirectTo: any[] = [ROUTES.AUTH, ROUTES.LOGIN]) { this.http .delete( this.userDataService.userData?.isSysAdmin @@ -114,7 +114,7 @@ export class DfAuthService { .subscribe(() => { this.userDataService.clearToken(); this.userDataService.userData = null; - this.router.navigate([ROUTES.AUTH, ROUTES.LOGIN]); + this.router.navigate(redirectTo); }); } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 553f32bf..d11f663d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { DfLoadingSpinnerService } from './shared/services/df-loading-spinner.service'; import { NgIf, AsyncPipe } from '@angular/common'; import { RouterOutlet, Router, ActivatedRoute } from '@angular/router'; -import { DfSideNavComponent } from './shared/components/df-side-nav/df-side-nav.component'; import { DfLicenseCheckService } from './shared/services/df-license-check.service'; import { UntilDestroy } from '@ngneat/until-destroy'; import { AuthService } from './shared/services/auth.service'; @@ -15,7 +14,7 @@ import { LoginResponse } from './shared/types/auth.types'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, - imports: [DfSideNavComponent, RouterOutlet, NgIf, AsyncPipe], + imports: [RouterOutlet, NgIf, AsyncPipe], }) export class AppComponent implements OnInit { title = 'df-admin-interface'; diff --git a/src/app/shared/components/df-popup/df-popup.component.html b/src/app/shared/components/df-popup/df-popup.component.html new file mode 100644 index 00000000..b20b0ac5 --- /dev/null +++ b/src/app/shared/components/df-popup/df-popup.component.html @@ -0,0 +1,24 @@ + diff --git a/src/app/shared/components/df-popup/df-popup.component.scss b/src/app/shared/components/df-popup/df-popup.component.scss new file mode 100644 index 00000000..7d6b8d50 --- /dev/null +++ b/src/app/shared/components/df-popup/df-popup.component.scss @@ -0,0 +1,110 @@ +// Popup Container +.popup-container { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 10000; +} + +.popup { + position: relative; + width: 90%; + max-width: 500px; + background: #ffffff; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + padding: 24px; + z-index: 10001; + animation: popupFadeIn 0.3s ease-out; + + .popup-header { + margin-bottom: 20px; + text-align: center; + + h2 { + margin: 0; + color: #333333; + font-size: 1.5rem; + font-weight: 600; + } + } + + .popup-content { + margin-bottom: 24px; + text-align: center; + + p { + margin: 8px 0; + color: #666666; + line-height: 1.5; + } + } + + .popup-actions { + display: flex; + justify-content: center; + gap: 12px; + + button { + min-width: 120px; + padding: 8px 16px; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + } + } + } +} + +@keyframes popupFadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.actions { + display: flex; + flex-direction: row; +} + +// Popup Header +.popup-header { + font-size: 18px; + font-weight: bold; + color: #6d4ec9; // Matches sidebar color + margin-bottom: 10px; +} + +// Popup Content +.popup-content { + font-size: 14px; + margin-bottom: 15px; +} + +// Close Button +.popup-close { + background: #6d4ec9; // Match theme + color: #fff; + border: none; + padding: 10px 15px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: background 0.3s ease; + + &:hover { + background: #5a3bb3; // Darker on hover + } +} diff --git a/src/app/shared/components/df-popup/df-popup.component.ts b/src/app/shared/components/df-popup/df-popup.component.ts new file mode 100644 index 00000000..cbb7c58c --- /dev/null +++ b/src/app/shared/components/df-popup/df-popup.component.ts @@ -0,0 +1,42 @@ +import { Component, Inject, Optional } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { TranslocoPipe } from '@ngneat/transloco'; +import { Router } from '@angular/router'; +import { ROUTES } from '../../types/routes'; +import { PopupOverlayService } from './popup-overlay.service'; +import { DfAuthService } from 'src/app/adf-user-management/services/df-auth.service'; +import { POPUP_CONFIG, PopupConfig } from './popup-config'; + +@Component({ + selector: 'df-popup', + templateUrl: './df-popup.component.html', + standalone: true, + styleUrls: ['./df-popup.component.scss'], + imports: [CommonModule, MatButtonModule, MatDialogModule, TranslocoPipe], +}) +export class PopupComponent { + constructor( + private router: Router, + private popupOverlay: PopupOverlayService, + private authService: DfAuthService, + @Optional() @Inject(POPUP_CONFIG) public config: PopupConfig + ) {} + + get message() { + return this.config?.message || + 'Your current password is shorter than recommended (less than 17 characters). For better security, we recommend updating your password to a longer one.'; + } + get showRemindMeLater() { + return this.config?.showRemindMeLater !== false; + } + + closePopup(shouldRedirect = false) { + this.popupOverlay.close(); + if (shouldRedirect) { + this.authService.logout(); + this.router.navigate([ROUTES.AUTH, ROUTES.RESET_PASSWORD]); + } + } +} diff --git a/src/app/shared/components/df-popup/popup-config.ts b/src/app/shared/components/df-popup/popup-config.ts new file mode 100644 index 00000000..fb157bb3 --- /dev/null +++ b/src/app/shared/components/df-popup/popup-config.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; + +export interface PopupConfig { + message: string; + showRemindMeLater: boolean; +} + +export const POPUP_CONFIG = new InjectionToken('POPUP_CONFIG'); diff --git a/src/app/shared/components/df-popup/popup-overlay.service.ts b/src/app/shared/components/df-popup/popup-overlay.service.ts new file mode 100644 index 00000000..20bd5ed5 --- /dev/null +++ b/src/app/shared/components/df-popup/popup-overlay.service.ts @@ -0,0 +1,41 @@ +import { Injectable, Injector } from '@angular/core'; +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { PopupComponent } from './df-popup.component'; +import { POPUP_CONFIG, PopupConfig } from './popup-config'; + +@Injectable({ providedIn: 'root' }) +export class PopupOverlayService { + private overlayRef: OverlayRef | null = null; + + constructor( + private overlay: Overlay, + private injector: Injector + ) {} + + open(config?: PopupConfig) { + if (this.overlayRef) return; + const injector = Injector.create({ + providers: [{ provide: POPUP_CONFIG, useValue: config }], + parent: this.injector, + }); + this.overlayRef = this.overlay.create({ + hasBackdrop: true, + backdropClass: 'popup-backdrop', + positionStrategy: this.overlay + .position() + .global() + .centerHorizontally() + .centerVertically(), + scrollStrategy: this.overlay.scrollStrategies.block(), + }); + const portal = new ComponentPortal(PopupComponent, null, injector); + this.overlayRef.attach(portal); + this.overlayRef.backdropClick().subscribe(() => this.close()); + } + + close() { + this.overlayRef?.dispose(); + this.overlayRef = null; + } +} diff --git a/src/app/shared/services/popup.service.ts b/src/app/shared/services/popup.service.ts new file mode 100644 index 00000000..a6c64216 --- /dev/null +++ b/src/app/shared/services/popup.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PopupService { + private storageKey = 'showPasswordPopup'; + private popupStateSubject = new BehaviorSubject(false); + popupState$ = this.popupStateSubject.asObservable(); + + constructor() { + // Initialize from localStorage + const savedState = this.shouldShowPopup(); + this.popupStateSubject.next(savedState); + } + + setShowPopup(value: boolean): void { + localStorage.setItem(this.storageKey, JSON.stringify(value)); + this.popupStateSubject.next(value); + } + + shouldShowPopup(): boolean { + return JSON.parse(localStorage.getItem(this.storageKey) || 'false'); + } +} diff --git a/src/styles.scss b/src/styles.scss index 123f7800..67e2c05d 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -230,3 +230,9 @@ a { .swagger-ui .opblock .opblock-section-header { background: unset !important; } + +.popup-backdrop { + background: rgba(0, 0, 0, 0.6) !important; + backdrop-filter: blur(6px) !important; + -webkit-backdrop-filter: blur(6px) !important; +}