From df1223b9e22ac7895dc7da79d8d0494730bc0478 Mon Sep 17 00:00:00 2001 From: sepehr Date: Thu, 9 Apr 2026 20:34:20 +0300 Subject: [PATCH] Better License Error when pro License expired --- src/app/app.module.ts | 10 +- .../providers/license-interceptor.service.ts | 55 +++++++ src/app/providers/license.service.ts | 22 +++ .../license-expired-overlay.component.html | 15 ++ .../license-expired-overlay.component.scss | 139 ++++++++++++++++++ .../license-expired-overlay.component.ts | 24 +++ src/app/shared/shared.module.ts | 16 ++ src/app/views/cloner/cloner.component.html | 3 + src/app/views/cloner/cloner.module.ts | 2 + src/app/views/maps/maps.component.html | 3 + src/app/views/maps/maps.module.ts | 4 +- .../monitoring/monitoring.component.html | 3 + src/app/views/monitoring/monitoring.module.ts | 4 +- .../views/sequences/sequences.component.html | 3 + src/app/views/sequences/sequences.module.ts | 4 +- .../syslog-regex/syslog-regex.component.html | 3 + .../views/syslog-regex/syslog-regex.module.ts | 4 +- src/app/views/vault/vault.component.html | 3 + src/app/views/vault/vault.module.ts | 4 +- src/app/views/vpn/vpn.component.html | 4 +- src/app/views/vpn/vpn.module.ts | 4 +- 21 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 src/app/providers/license-interceptor.service.ts create mode 100644 src/app/providers/license.service.ts create mode 100644 src/app/shared/components/license-expired-overlay/license-expired-overlay.component.html create mode 100644 src/app/shared/components/license-expired-overlay/license-expired-overlay.component.scss create mode 100644 src/app/shared/components/license-expired-overlay/license-expired-overlay.component.ts create mode 100644 src/app/shared/shared.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2384b03..5f63543 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -8,7 +8,9 @@ import Aura from '@primeuix/themes/aura'; import { ReactiveFormsModule,FormsModule } from '@angular/forms'; import { NgScrollbarModule } from 'ngx-scrollbar'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { LicenseService } from './providers/license.service'; +import { LicenseInterceptor } from './providers/license-interceptor.service'; // Import routing module import { AppRoutingModule } from './app-routing.module'; @@ -113,6 +115,12 @@ export function loginStatusProviderFactory(provider: loginChecker) { multi: true, }, Title, + LicenseService, + { + provide: HTTP_INTERCEPTORS, + useClass: LicenseInterceptor, + multi: true + }, provideHttpClient(withInterceptorsFromDi()) ] }) export class AppModule { diff --git a/src/app/providers/license-interceptor.service.ts b/src/app/providers/license-interceptor.service.ts new file mode 100644 index 0000000..3dd2451 --- /dev/null +++ b/src/app/providers/license-interceptor.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, + HttpResponse +} from '@angular/common/http'; +import { Observable, tap } from 'rxjs'; +import { LicenseService } from './license.service'; + +@Injectable() +export class LicenseInterceptor implements HttpInterceptor { + + // Pro-licensed API endpoints that should be monitored + private readonly proEndpoints = [ + 'monitoring/devs/get', + 'monitoring/events/get', + 'monitoring/eventunfixed/get', + '/api/vpn/status', + 'networkmap/get', + 'snippet/sequence/list', + 'cloner/list', + '/api/pssvault/get', + 'snippet/syslogregex/list' + ]; + + constructor(private licenseService: LicenseService) {} + + private isProEndpoint(url: string): boolean { + return this.proEndpoints.some(endpoint => url.includes(endpoint)); + } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + tap((event: HttpEvent) => { + if (event instanceof HttpResponse && this.isProEndpoint(request.url)) { + const body = event.body; + if ( + body && + body.result && + typeof body.result === 'object' && + body.result.err === 'License Expired' && + body.result.status === 'failed' + ) { + this.licenseService.setExpired(true); + } else { + // Pro endpoint responded without license error — license is valid + this.licenseService.setExpired(false); + } + } + }) + ); + } +} diff --git a/src/app/providers/license.service.ts b/src/app/providers/license.service.ts new file mode 100644 index 0000000..54902bf --- /dev/null +++ b/src/app/providers/license.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class LicenseService { + private isExpiredSubject = new BehaviorSubject(false); + public isExpired$: Observable = this.isExpiredSubject.asObservable(); + + constructor() { } + + setExpired(expired: boolean): void { + if (this.isExpiredSubject.value !== expired) { + this.isExpiredSubject.next(expired); + } + } + + isExpired(): boolean { + return this.isExpiredSubject.value; + } +} diff --git a/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.html b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.html new file mode 100644 index 0000000..a81e49a --- /dev/null +++ b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.html @@ -0,0 +1,15 @@ +
+
+
+ +
+

Pro Feature: License Expired

+

This view is restricted to pro-licensed customers. Your license has either expired or is not valid for this feature.

+ +
+
diff --git a/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.scss b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.scss new file mode 100644 index 0000000..9176e7a --- /dev/null +++ b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.scss @@ -0,0 +1,139 @@ +.license-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.4); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + border-radius: inherit; + animation: fadeIn 0.4s ease-out; + + .overlay-content { + background: rgba(255, 255, 255, 0.95); + padding: 2.5rem; + border-radius: 24px; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05); + max-width: 480px; + width: 90%; + display: flex; + flex-direction: column; + align-items: center; + transform: translateY(0); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + transform: translateY(-5px); + } + + .icon-container { + width: 80px; + height: 80px; + background: linear-gradient(135deg, #fde8e8 0%, #f9d2d2 100%); + color: #e55353; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + margin-bottom: 1.5rem; + position: relative; + + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + border: 2px solid #e55353; + animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; + opacity: 0; + } + } + + h2 { + color: #1a1c23; + font-weight: 800; + margin-bottom: 0.75rem; + font-size: 1.75rem; + letter-spacing: -0.025em; + } + + p { + color: #4b5563; + line-height: 1.7; + margin-bottom: 2.5rem; + font-size: 1.05rem; + } + + .actions { + display: flex; + gap: 1.25rem; + width: 100%; + + .btn-renew { + flex: 3; + background: linear-gradient(135deg, #e55353 0%, #c53030 100%); + color: white; + padding: 0.875rem 1.5rem; + border-radius: 14px; + text-decoration: none; + font-weight: 700; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + border: none; + cursor: pointer; + font-size: 1rem; + box-shadow: 0 4px 12px rgba(229, 83, 83, 0.3); + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(229, 83, 83, 0.5); + filter: brightness(1.1); + color: white; + } + + &:active { + transform: translateY(0); + } + } + + .btn-dismiss { + flex: 1; + background: #f3f4f6; + color: #6b7280; + padding: 0.875rem 1rem; + border-radius: 14px; + font-weight: 600; + border: 1px solid #e5e7eb; + cursor: pointer; + transition: all 0.2s; + font-size: 0.95rem; + + &:hover { + background: #e5e7eb; + color: #374151; + } + } + } + } +} + +@keyframes fadeIn { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes ping { + 0% { transform: scale(1); opacity: 0.5; } + 100% { transform: scale(2); opacity: 0; } +} diff --git a/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.ts b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.ts new file mode 100644 index 0000000..697a332 --- /dev/null +++ b/src/app/shared/components/license-expired-overlay/license-expired-overlay.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { LicenseService } from '../../../providers/license.service'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-license-expired-overlay', + templateUrl: './license-expired-overlay.component.html', + styleUrls: ['./license-expired-overlay.component.scss'] +}) +export class LicenseExpiredOverlayComponent implements OnInit { + public isExpired$: Observable; + + constructor(private licenseService: LicenseService) { + this.isExpired$ = this.licenseService.isExpired$; + } + + ngOnInit(): void { + } + + dismiss(): void { + // Locally hide the overlay for the current component session if needed + // But usually, it should stay until the license is fixed. + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 0000000..c83324e --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LicenseExpiredOverlayComponent } from './components/license-expired-overlay/license-expired-overlay.component'; + +@NgModule({ + declarations: [ + LicenseExpiredOverlayComponent + ], + imports: [ + CommonModule + ], + exports: [ + LicenseExpiredOverlayComponent + ] +}) +export class SharedModule { } diff --git a/src/app/views/cloner/cloner.component.html b/src/app/views/cloner/cloner.component.html index 3a87e0b..da56549 100644 --- a/src/app/views/cloner/cloner.component.html +++ b/src/app/views/cloner/cloner.component.html @@ -1,3 +1,4 @@ +
@@ -115,6 +116,8 @@ + +
diff --git a/src/app/views/cloner/cloner.module.ts b/src/app/views/cloner/cloner.module.ts index 8c68cc4..f70daae 100644 --- a/src/app/views/cloner/cloner.module.ts +++ b/src/app/views/cloner/cloner.module.ts @@ -23,6 +23,7 @@ import { InputTextModule as PInputTextModule } from 'primeng/inputtext'; import { TooltipModule as PTooltipModule } from 'primeng/tooltip'; import { NgxSuperSelectModule} from "ngx-super-select"; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ @@ -46,6 +47,7 @@ import { NgxSuperSelectModule} from "ngx-super-select"; TabsModule, BadgeModule, AlertModule, + SharedModule, ], declarations: [ClonerComponent], providers: [TitleCasePipe], diff --git a/src/app/views/maps/maps.component.html b/src/app/views/maps/maps.component.html index 6aa8268..05f094c 100644 --- a/src/app/views/maps/maps.component.html +++ b/src/app/views/maps/maps.component.html @@ -1,3 +1,4 @@ +
@@ -113,3 +114,5 @@ + +
diff --git a/src/app/views/maps/maps.module.ts b/src/app/views/maps/maps.module.ts index beb35e3..a395af3 100644 --- a/src/app/views/maps/maps.module.ts +++ b/src/app/views/maps/maps.module.ts @@ -27,6 +27,7 @@ import { MapsRoutingModule } from "./maps-routing.module"; import { MapsComponent } from "./maps.component"; import { ClipboardModule } from "@angular/cdk/clipboard"; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ @@ -53,7 +54,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; TooltipModule, UtilitiesModule, InfiniteScrollModule, - IconModule + IconModule, + SharedModule ], declarations: [MapsComponent], }) diff --git a/src/app/views/monitoring/monitoring.component.html b/src/app/views/monitoring/monitoring.component.html index b696738..94a0072 100644 --- a/src/app/views/monitoring/monitoring.component.html +++ b/src/app/views/monitoring/monitoring.component.html @@ -1,3 +1,4 @@ +
@@ -176,6 +177,8 @@ + +
diff --git a/src/app/views/monitoring/monitoring.module.ts b/src/app/views/monitoring/monitoring.module.ts index 7c318eb..f3a0644 100644 --- a/src/app/views/monitoring/monitoring.module.ts +++ b/src/app/views/monitoring/monitoring.module.ts @@ -25,6 +25,7 @@ import { MonitoringRoutingModule } from "./monitoring-routing.module"; import { MonitoringComponent } from "./monitoring.component"; import { ClipboardModule } from "@angular/cdk/clipboard"; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ @@ -49,7 +50,8 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; TableModule, TooltipModule, UtilitiesModule, - InfiniteScrollModule + InfiniteScrollModule, + SharedModule ], declarations: [MonitoringComponent], }) diff --git a/src/app/views/sequences/sequences.component.html b/src/app/views/sequences/sequences.component.html index 9d1f755..3f3c5d7 100644 --- a/src/app/views/sequences/sequences.component.html +++ b/src/app/views/sequences/sequences.component.html @@ -1,3 +1,4 @@ +
@@ -118,6 +119,8 @@ + +
diff --git a/src/app/views/sequences/sequences.module.ts b/src/app/views/sequences/sequences.module.ts index 0186b59..cbff0bb 100644 --- a/src/app/views/sequences/sequences.module.ts +++ b/src/app/views/sequences/sequences.module.ts @@ -24,6 +24,7 @@ import { TableModule } from 'primeng/table'; import { InputTextModule } from 'primeng/inputtext'; import { TooltipModule } from 'primeng/tooltip'; import { NgxSuperSelectModule } from "ngx-super-select"; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ @@ -47,7 +48,8 @@ import { NgxSuperSelectModule } from "ngx-super-select"; TabsModule, AlertModule, HighlightJsModule, - CollapseModule + CollapseModule, + SharedModule ], declarations: [SequencesComponent], }) diff --git a/src/app/views/syslog-regex/syslog-regex.component.html b/src/app/views/syslog-regex/syslog-regex.component.html index f3c95d1..6e18e66 100644 --- a/src/app/views/syslog-regex/syslog-regex.component.html +++ b/src/app/views/syslog-regex/syslog-regex.component.html @@ -1,3 +1,4 @@ +
@@ -108,6 +109,8 @@ + +
diff --git a/src/app/views/syslog-regex/syslog-regex.module.ts b/src/app/views/syslog-regex/syslog-regex.module.ts index 2995891..7ffa937 100644 --- a/src/app/views/syslog-regex/syslog-regex.module.ts +++ b/src/app/views/syslog-regex/syslog-regex.module.ts @@ -26,6 +26,7 @@ import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; import { TooltipModule } from 'primeng/tooltip'; import { DropdownModule } from 'primeng/dropdown'; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ @@ -51,7 +52,8 @@ import { DropdownModule } from 'primeng/dropdown'; TabsModule, AlertModule, HighlightJsModule, - CollapseModule + CollapseModule, + SharedModule ], declarations: [SyslogRegexComponent], }) diff --git a/src/app/views/vault/vault.component.html b/src/app/views/vault/vault.component.html index bb7ab56..9f38742 100644 --- a/src/app/views/vault/vault.component.html +++ b/src/app/views/vault/vault.component.html @@ -1,3 +1,4 @@ +
@@ -336,6 +337,8 @@ + +
diff --git a/src/app/views/vault/vault.module.ts b/src/app/views/vault/vault.module.ts index 2f4eb15..6c23ad0 100644 --- a/src/app/views/vault/vault.module.ts +++ b/src/app/views/vault/vault.module.ts @@ -22,6 +22,7 @@ import { TooltipModule as PTooltipModule } from 'primeng/tooltip'; import { MatInputModule } from "@angular/material/input"; import { MatFormFieldModule } from "@angular/material/form-field"; +import { SharedModule } from "../../shared/shared.module"; @NgModule({ imports: [ VaultRoutingModule, @@ -42,7 +43,8 @@ import { MatFormFieldModule } from "@angular/material/form-field"; MatInputModule, MatFormFieldModule, CollapseModule, - TooltipModule + TooltipModule, + SharedModule ], declarations: [VaultComponent], }) diff --git a/src/app/views/vpn/vpn.component.html b/src/app/views/vpn/vpn.component.html index 01b9a93..224e4d8 100644 --- a/src/app/views/vpn/vpn.component.html +++ b/src/app/views/vpn/vpn.component.html @@ -1,3 +1,4 @@ +
@@ -649,6 +650,7 @@ - +
+
\ No newline at end of file diff --git a/src/app/views/vpn/vpn.module.ts b/src/app/views/vpn/vpn.module.ts index d9cf030..def80dd 100644 --- a/src/app/views/vpn/vpn.module.ts +++ b/src/app/views/vpn/vpn.module.ts @@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ChartjsModule } from '@coreui/angular-chartjs'; import { HighlightJsModule } from 'ngx-highlight-js'; import { ClipboardModule } from '@angular/cdk/clipboard'; +import { SharedModule as AppSharedModule } from "../../shared/shared.module"; import { AvatarModule, @@ -67,7 +68,8 @@ import { InputTextModule } from 'primeng/inputtext'; CoreUIGridModule, MatMenuModule, HighlightJsModule, - ClipboardModule + ClipboardModule, + AppSharedModule ], declarations: [VpnComponent] })