mirror of https://github.com/abpframework/abp.git
committed by
GitHub
4 changed files with 265 additions and 4 deletions
@ -0,0 +1,255 @@ |
|||||
|
import { DestroyRef, DOCUMENT, inject, Injectable } from '@angular/core'; |
||||
|
import { OAuthStorage } from 'angular-oauth2-oidc'; |
||||
|
import { AbpLocalStorageService } from '@abp/ng.core'; |
||||
|
import { fromEvent } from 'rxjs'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root', |
||||
|
}) |
||||
|
export class MemoryTokenStorageService implements OAuthStorage { |
||||
|
private static workerUrl: string | null = null; |
||||
|
|
||||
|
private keysShouldStoreInMemory = [ |
||||
|
'access_token', |
||||
|
'id_token', |
||||
|
'expires_at', |
||||
|
'id_token_claims_obj', |
||||
|
'id_token_expires_at', |
||||
|
'id_token_stored_at', |
||||
|
'access_token_stored_at', |
||||
|
'abpOAuthClientId', |
||||
|
'granted_scopes', |
||||
|
]; |
||||
|
|
||||
|
private worker?: any; |
||||
|
private port?: MessagePort; |
||||
|
private cache = new Map<string, string>(); |
||||
|
private localStorageService = inject(AbpLocalStorageService); |
||||
|
private _document = inject(DOCUMENT); |
||||
|
private destroyRef = inject(DestroyRef); |
||||
|
private useSharedWorker = false; |
||||
|
|
||||
|
constructor() { |
||||
|
this.initializeStorage(); |
||||
|
this.setupCleanup(); |
||||
|
} |
||||
|
|
||||
|
private initializeStorage(): void { |
||||
|
console.log('Initialize Storage -->>', typeof SharedWorker !== 'undefined'); |
||||
|
// @ts-ignore
|
||||
|
if (typeof SharedWorker !== 'undefined') { |
||||
|
try { |
||||
|
console.log('Shared worker is loaded'); |
||||
|
console.log('refresh view'); |
||||
|
// Create worker from data URL to avoid path resolution issues in consuming apps
|
||||
|
// Data URLs are deterministic - same content produces same URL across all tabs
|
||||
|
if (!MemoryTokenStorageService.workerUrl) { |
||||
|
MemoryTokenStorageService.workerUrl = this.createWorkerDataUrl(); |
||||
|
} |
||||
|
// @ts-ignore
|
||||
|
this.worker = new SharedWorker(MemoryTokenStorageService.workerUrl, { |
||||
|
name: 'oauth-token-storage', |
||||
|
}); |
||||
|
console.log('loaded worker -->>', this.worker); |
||||
|
this.port = this.worker.port; |
||||
|
this.port.start(); |
||||
|
this.useSharedWorker = true; |
||||
|
|
||||
|
this.port.onmessage = event => { |
||||
|
const { action, key, value } = event.data; |
||||
|
|
||||
|
switch (action) { |
||||
|
case 'set': |
||||
|
this.checkAuthStateChanges(key); |
||||
|
this.cache.set(key, value); |
||||
|
break; |
||||
|
case 'remove': |
||||
|
this.cache.delete(key); |
||||
|
this.refreshDocument(); |
||||
|
break; |
||||
|
case 'clear': |
||||
|
this.cache.clear(); |
||||
|
this.refreshDocument(); |
||||
|
break; |
||||
|
case 'get': |
||||
|
if (value !== null) { |
||||
|
this.cache.set(key, value); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Load all tokens from SharedWorker on initialization
|
||||
|
this.keysShouldStoreInMemory.forEach(key => { |
||||
|
this.port?.postMessage({ action: 'get', key }); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.log(error); |
||||
|
this.useSharedWorker = false; |
||||
|
} |
||||
|
} else { |
||||
|
this.useSharedWorker = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getItem(key: string): string | null { |
||||
|
if (!this.keysShouldStoreInMemory.includes(key)) { |
||||
|
return this.localStorageService.getItem(key); |
||||
|
} |
||||
|
return this.cache.get(key) || null; |
||||
|
} |
||||
|
|
||||
|
setItem(key: string, value: string): void { |
||||
|
if (!this.keysShouldStoreInMemory.includes(key)) { |
||||
|
this.localStorageService.setItem(key, value); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this.useSharedWorker && this.port) { |
||||
|
this.cache.set(key, value); |
||||
|
this.port.postMessage({ action: 'set', key, value }); |
||||
|
} else { |
||||
|
this.cache.set(key, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
removeItem(key: string): void { |
||||
|
if (!this.keysShouldStoreInMemory.includes(key)) { |
||||
|
this.localStorageService.removeItem(key); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (this.useSharedWorker && this.port) { |
||||
|
this.cache.delete(key); |
||||
|
this.port.postMessage({ action: 'remove', key }); |
||||
|
} else { |
||||
|
this.cache.delete(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear(): void { |
||||
|
if (this.useSharedWorker && this.port) { |
||||
|
this.port.postMessage({ action: 'clear' }); |
||||
|
} |
||||
|
this.cache.clear(); |
||||
|
} |
||||
|
|
||||
|
private cleanupPort(): void { |
||||
|
if (this.useSharedWorker && this.port) { |
||||
|
try { |
||||
|
this.port.postMessage({ action: 'disconnect' }); |
||||
|
} catch (error) { |
||||
|
console.log('Error disconnecting port:', error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setupCleanup(): void { |
||||
|
if (this._document.defaultView) { |
||||
|
fromEvent(this._document.defaultView, 'beforeunload') |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(() => this.cleanupPort()); |
||||
|
|
||||
|
fromEvent(this._document.defaultView, 'pagehide') |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(() => this.cleanupPort()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private checkAuthStateChanges = (key: string) => { |
||||
|
if (key === 'access_token' && !this.cache.get('access_token')) { |
||||
|
this.refreshDocument(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private refreshDocument(): void { |
||||
|
this.cleanupPort(); |
||||
|
setTimeout(() => { |
||||
|
this._document.defaultView?.location.reload(); |
||||
|
}, 100); |
||||
|
} |
||||
|
|
||||
|
private createWorkerDataUrl(): string { |
||||
|
const workerScript = `const tokenStore = new Map();
|
||||
|
const ports = new Set(); |
||||
|
|
||||
|
function broadcastToOtherPorts(senderPort, message) { |
||||
|
const deadPorts = []; |
||||
|
ports.forEach(p => { |
||||
|
if (p !== senderPort) { |
||||
|
try { |
||||
|
p.postMessage(message); |
||||
|
} catch (error) { |
||||
|
console.log('Dead port detected during broadcast, removing...'); |
||||
|
deadPorts.push(p); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
deadPorts.forEach(p => ports.delete(p)); |
||||
|
if (deadPorts.length > 0) { |
||||
|
console.log('Cleaned up', deadPorts.length, 'dead ports. Total ports:', ports.size); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function removePort(port) { |
||||
|
if (ports.has(port)) { |
||||
|
ports.delete(port); |
||||
|
console.log('Port disconnected. Total ports:', ports.size); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
self.onconnect = (event) => { |
||||
|
const port = event.ports[0]; |
||||
|
ports.add(port); |
||||
|
console.log('Port connected. Total ports:', ports.size); |
||||
|
|
||||
|
port.addEventListener('messageerror', () => { |
||||
|
removePort(port); |
||||
|
}); |
||||
|
|
||||
|
port.onmessage = (e) => { |
||||
|
const { action, key, value } = e.data; |
||||
|
|
||||
|
switch (action) { |
||||
|
case 'set': |
||||
|
if (key && value !== undefined) { |
||||
|
tokenStore.set(key, value); |
||||
|
broadcastToOtherPorts(port, { action: 'set', key, value }); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case 'remove': |
||||
|
if (key) { |
||||
|
tokenStore.delete(key); |
||||
|
broadcastToOtherPorts(port, { action: 'remove', key }); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case 'clear': |
||||
|
tokenStore.clear(); |
||||
|
broadcastToOtherPorts(port, { action: 'clear' }); |
||||
|
break; |
||||
|
|
||||
|
case 'get': |
||||
|
if (key) { |
||||
|
const value = tokenStore.get(key) ?? null; |
||||
|
port.postMessage({ action: 'get', key, value }); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case 'disconnect': |
||||
|
console.log('Port requested disconnect'); |
||||
|
removePort(port); |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
console.warn('Unknown action:', action); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
port.start(); |
||||
|
};`;
|
||||
|
return 'data:application/javascript;base64,' + btoa(workerScript); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue