@uxland/primary-shell 7.41.8 → 7.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{component-BgR3-MlL.js → component-DCk1Rgkl.js} +2 -2
- package/dist/{component-BgR3-MlL.js.map → component-DCk1Rgkl.js.map} +1 -1
- package/dist/{index-mgf5fUfq.js → index-BPXzFbQm.js} +617 -572
- package/dist/index-BPXzFbQm.js.map +1 -0
- package/dist/index.js +12 -10
- package/dist/index.umd.cjs +6 -6
- package/dist/index.umd.cjs.map +1 -1
- package/dist/primary/shell/src/api/activity-monitor/activity-monitor.d.ts +13 -0
- package/dist/primary/shell/src/api/activity-monitor/activity-monitor.test.d.ts +1 -0
- package/dist/primary/shell/src/api/api.d.ts +2 -0
- package/dist/primary/shell/src/api/session-refresh-timer/session-refresh-timer.d.ts +20 -0
- package/dist/primary/shell/src/api/session-refresh-timer/session-refresh-timer.test.d.ts +1 -0
- package/dist/primary/shell/src/api/token-manager/token-manager.d.ts +1 -1
- package/package.json +1 -1
- package/src/api/activity-monitor/activity-monitor.test.ts +67 -0
- package/src/api/activity-monitor/activity-monitor.ts +26 -0
- package/src/api/api.ts +4 -0
- package/src/api/ecap-event-manager/ecap-event-manager.ts +1 -0
- package/src/api/session-refresh-timer/session-refresh-timer.test.ts +151 -0
- package/src/api/session-refresh-timer/session-refresh-timer.ts +79 -0
- package/src/api/token-manager/token-manager.test.ts +26 -44
- package/src/api/token-manager/token-manager.ts +14 -19
- package/src/bootstrapper.ts +3 -2
- package/src/disposer.ts +3 -1
- package/dist/index-mgf5fUfq.js.map +0 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ActivityMonitor {
|
|
2
|
+
start: () => void;
|
|
3
|
+
stop: () => void;
|
|
4
|
+
getLastActivityTimestamp: () => number;
|
|
5
|
+
}
|
|
6
|
+
export declare class ActivityMonitorImpl implements ActivityMonitor {
|
|
7
|
+
private lastActivityTimestamp;
|
|
8
|
+
private readonly activityHandler;
|
|
9
|
+
start: () => void;
|
|
10
|
+
stop: () => void;
|
|
11
|
+
getLastActivityTimestamp: () => number;
|
|
12
|
+
}
|
|
13
|
+
export declare const createActivityMonitor: () => ActivityMonitor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -32,6 +32,8 @@ export interface PrimariaApi extends HarmonixApi {
|
|
|
32
32
|
importDataManager: PrimariaImportDataManager;
|
|
33
33
|
}
|
|
34
34
|
export declare const PrimariaRegionHost: any;
|
|
35
|
+
export declare const activityMonitor: import('./activity-monitor/activity-monitor').ActivityMonitor;
|
|
36
|
+
export declare const sessionRefreshTimer: import('./session-refresh-timer/session-refresh-timer').SessionRefreshTimer;
|
|
35
37
|
/**
|
|
36
38
|
* Factory function that creates a Primaria API instance.
|
|
37
39
|
*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ActivityMonitor } from '../activity-monitor/activity-monitor';
|
|
2
|
+
import { PrimariaBroker } from '../broker/primaria-broker';
|
|
3
|
+
import { TokenManager } from '../token-manager/token-manager';
|
|
4
|
+
export interface SessionRefreshTimer {
|
|
5
|
+
start: () => void;
|
|
6
|
+
stop: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class SessionRefreshTimerImpl implements SessionRefreshTimer {
|
|
9
|
+
private readonly tokenManager;
|
|
10
|
+
private readonly activityMonitor;
|
|
11
|
+
private readonly broker;
|
|
12
|
+
private intervalId;
|
|
13
|
+
private isRefreshing;
|
|
14
|
+
constructor(tokenManager: TokenManager, activityMonitor: ActivityMonitor, broker: PrimariaBroker);
|
|
15
|
+
start: () => void;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
private getTokenExpiry;
|
|
18
|
+
private checkAndRefresh;
|
|
19
|
+
}
|
|
20
|
+
export declare const createSessionRefreshTimer: (tokenManager: TokenManager, activityMonitor: ActivityMonitor, broker: PrimariaBroker) => SessionRefreshTimer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ActivityMonitorImpl } from "./activity-monitor";
|
|
3
|
+
|
|
4
|
+
describe("ActivityMonitor", () => {
|
|
5
|
+
let monitor: ActivityMonitorImpl;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
monitor = new ActivityMonitorImpl();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
monitor.stop();
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return 0 before any activity", () => {
|
|
18
|
+
expect(monitor.getLastActivityTimestamp()).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should update timestamp on keydown", () => {
|
|
22
|
+
monitor.start();
|
|
23
|
+
const before = Date.now();
|
|
24
|
+
window.dispatchEvent(new Event("keydown"));
|
|
25
|
+
expect(monitor.getLastActivityTimestamp()).toBeGreaterThanOrEqual(before);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should update timestamp on click", () => {
|
|
29
|
+
monitor.start();
|
|
30
|
+
const before = Date.now();
|
|
31
|
+
window.dispatchEvent(new Event("click"));
|
|
32
|
+
expect(monitor.getLastActivityTimestamp()).toBeGreaterThanOrEqual(before);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should update timestamp on scroll", () => {
|
|
36
|
+
monitor.start();
|
|
37
|
+
const before = Date.now();
|
|
38
|
+
window.dispatchEvent(new Event("scroll"));
|
|
39
|
+
expect(monitor.getLastActivityTimestamp()).toBeGreaterThanOrEqual(before);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should reflect the most recent activity time", () => {
|
|
43
|
+
monitor.start();
|
|
44
|
+
|
|
45
|
+
window.dispatchEvent(new Event("keydown"));
|
|
46
|
+
const first = monitor.getLastActivityTimestamp();
|
|
47
|
+
|
|
48
|
+
vi.advanceTimersByTime(5000);
|
|
49
|
+
window.dispatchEvent(new Event("click"));
|
|
50
|
+
const second = monitor.getLastActivityTimestamp();
|
|
51
|
+
|
|
52
|
+
expect(second).toBeGreaterThan(first);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should not update timestamp after stop", () => {
|
|
56
|
+
monitor.start();
|
|
57
|
+
window.dispatchEvent(new Event("keydown"));
|
|
58
|
+
const timestampBeforeStop = monitor.getLastActivityTimestamp();
|
|
59
|
+
|
|
60
|
+
monitor.stop();
|
|
61
|
+
vi.advanceTimersByTime(5000);
|
|
62
|
+
window.dispatchEvent(new Event("click"));
|
|
63
|
+
|
|
64
|
+
expect(monitor.getLastActivityTimestamp()).toBe(timestampBeforeStop);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const ACTIVITY_EVENTS = ["keydown", "click", "scroll"] as const;
|
|
2
|
+
|
|
3
|
+
export interface ActivityMonitor {
|
|
4
|
+
start: () => void;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
getLastActivityTimestamp: () => number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ActivityMonitorImpl implements ActivityMonitor {
|
|
10
|
+
private lastActivityTimestamp = 0;
|
|
11
|
+
private readonly activityHandler = () => {
|
|
12
|
+
this.lastActivityTimestamp = Date.now();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
start = () => {
|
|
16
|
+
for (const e of ACTIVITY_EVENTS) window.addEventListener(e, this.activityHandler);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
stop = () => {
|
|
20
|
+
for (const e of ACTIVITY_EVENTS) window.removeEventListener(e, this.activityHandler);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
getLastActivityTimestamp = () => this.lastActivityTimestamp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const createActivityMonitor = (): ActivityMonitor => new ActivityMonitorImpl();
|
package/src/api/api.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { PrimariaBroker } from "./broker/primaria-broker";
|
|
|
5
5
|
import { EcapEventManager, createEcapEventManager } from "./ecap-event-manager/ecap-event-manager";
|
|
6
6
|
import { ExitGuardManager, ExitGuardManagerImpl } from "./exit-guard-manager/exit-guard-manager";
|
|
7
7
|
import { PrimariaGlobalStateManager, createGlobalStateManager } from "./global-state/global-state";
|
|
8
|
+
import { createActivityMonitor } from "./activity-monitor/activity-monitor";
|
|
8
9
|
import { HttpClient, createHttpClient } from "./http-client/http-client";
|
|
9
10
|
import { PrimariaInteractionService } from "./interaction-service";
|
|
11
|
+
import { createSessionRefreshTimer } from "./session-refresh-timer/session-refresh-timer";
|
|
10
12
|
import { ParimariaInteractionServiceImpl } from "./interaction-service/interaction-service-impl";
|
|
11
13
|
import { createLocaleManager } from "./localization/localization";
|
|
12
14
|
import { PrimariaNotificationService } from "./notification-service/notification-service";
|
|
@@ -44,6 +46,8 @@ const regionManager: RegionManager = createRegionManager("primaria");
|
|
|
44
46
|
export const PrimariaRegionHost: any = createRegionHost(regionManager as any);
|
|
45
47
|
const tokenManager = createTokenManager();
|
|
46
48
|
const userManager = createUserManager(tokenManager);
|
|
49
|
+
export const activityMonitor = createActivityMonitor();
|
|
50
|
+
export const sessionRefreshTimer = createSessionRefreshTimer(tokenManager, activityMonitor, broker);
|
|
47
51
|
const globalStateManager: PrimariaGlobalStateManager = createGlobalStateManager(broker);
|
|
48
52
|
const contextManager = createContextManager();
|
|
49
53
|
const pluginBusyManager = new PluginBusyManagerImpl();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BROKER_EVENTS } from "../broker/broker-events";
|
|
3
|
+
import { SessionRefreshTimerImpl } from "./session-refresh-timer";
|
|
4
|
+
|
|
5
|
+
const createJwt = (exp: number): string => {
|
|
6
|
+
const payload = btoa(JSON.stringify({ exp }))
|
|
7
|
+
.replace(/=/g, "")
|
|
8
|
+
.replace(/\+/g, "-")
|
|
9
|
+
.replace(/\//g, "_");
|
|
10
|
+
return `header.${payload}.sig`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createMockTokenManager = (overrides: Record<string, any> = {}) => ({
|
|
14
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)),
|
|
15
|
+
setInitialTokens: vi.fn(),
|
|
16
|
+
refreshToken: vi.fn().mockResolvedValue("new_token"),
|
|
17
|
+
...overrides,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createMockActivityMonitor = (lastActivity = Date.now()) => ({
|
|
21
|
+
start: vi.fn(),
|
|
22
|
+
stop: vi.fn(),
|
|
23
|
+
getLastActivityTimestamp: vi.fn().mockReturnValue(lastActivity),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const createMockBroker = () => ({
|
|
27
|
+
publish: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
subscribe: vi.fn(),
|
|
29
|
+
send: vi.fn(),
|
|
30
|
+
registerRequest: vi.fn(),
|
|
31
|
+
events: BROKER_EVENTS,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("SessionRefreshTimer", () => {
|
|
35
|
+
let mockBroker: ReturnType<typeof createMockBroker>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.useFakeTimers();
|
|
39
|
+
mockBroker = createMockBroker();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should skip tick when there is no recent activity", async () => {
|
|
48
|
+
const tokenManager = createMockTokenManager({
|
|
49
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
|
|
50
|
+
});
|
|
51
|
+
const timer = new SessionRefreshTimerImpl(
|
|
52
|
+
tokenManager,
|
|
53
|
+
createMockActivityMonitor(0),
|
|
54
|
+
mockBroker,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
timer.start();
|
|
58
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
59
|
+
|
|
60
|
+
expect(tokenManager.refreshToken).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should skip tick when token is not near expiry", async () => {
|
|
64
|
+
const tokenManager = createMockTokenManager({
|
|
65
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 3600)),
|
|
66
|
+
});
|
|
67
|
+
const timer = new SessionRefreshTimerImpl(
|
|
68
|
+
tokenManager,
|
|
69
|
+
createMockActivityMonitor(),
|
|
70
|
+
mockBroker,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
timer.start();
|
|
74
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
75
|
+
|
|
76
|
+
expect(tokenManager.refreshToken).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should refresh when token is near expiry and user is active", async () => {
|
|
80
|
+
const tokenManager = createMockTokenManager({
|
|
81
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
|
|
82
|
+
});
|
|
83
|
+
const timer = new SessionRefreshTimerImpl(
|
|
84
|
+
tokenManager,
|
|
85
|
+
createMockActivityMonitor(),
|
|
86
|
+
mockBroker,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
timer.start();
|
|
90
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
91
|
+
|
|
92
|
+
expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should refresh when token is already expired and user is active", async () => {
|
|
96
|
+
const tokenManager = createMockTokenManager({
|
|
97
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) - 60)),
|
|
98
|
+
});
|
|
99
|
+
const timer = new SessionRefreshTimerImpl(
|
|
100
|
+
tokenManager,
|
|
101
|
+
createMockActivityMonitor(),
|
|
102
|
+
mockBroker,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
timer.start();
|
|
106
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
107
|
+
|
|
108
|
+
expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should publish refreshTokenFailed when refresh throws", async () => {
|
|
112
|
+
const tokenManager = createMockTokenManager({
|
|
113
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) - 60)),
|
|
114
|
+
refreshToken: vi.fn().mockRejectedValue(new Error("Refresh failed")),
|
|
115
|
+
});
|
|
116
|
+
const timer = new SessionRefreshTimerImpl(
|
|
117
|
+
tokenManager,
|
|
118
|
+
createMockActivityMonitor(),
|
|
119
|
+
mockBroker,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
timer.start();
|
|
123
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
124
|
+
|
|
125
|
+
expect(mockBroker.publish).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should not trigger concurrent refreshes", async () => {
|
|
129
|
+
let resolveRefresh!: () => void;
|
|
130
|
+
const tokenManager = createMockTokenManager({
|
|
131
|
+
getToken: vi.fn().mockReturnValue(createJwt(Math.floor(Date.now() / 1000) + 60)),
|
|
132
|
+
refreshToken: vi.fn().mockImplementation(
|
|
133
|
+
() => new Promise((resolve) => { resolveRefresh = () => resolve("new_token"); }),
|
|
134
|
+
),
|
|
135
|
+
});
|
|
136
|
+
const timer = new SessionRefreshTimerImpl(
|
|
137
|
+
tokenManager,
|
|
138
|
+
createMockActivityMonitor(),
|
|
139
|
+
mockBroker,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
timer.start();
|
|
143
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
144
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
145
|
+
|
|
146
|
+
resolveRefresh();
|
|
147
|
+
await Promise.resolve();
|
|
148
|
+
|
|
149
|
+
expect(tokenManager.refreshToken).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jwtDecode } from "jwt-decode";
|
|
2
|
+
import { ActivityMonitor } from "../activity-monitor/activity-monitor";
|
|
3
|
+
import { BROKER_EVENTS } from "../broker/broker-events";
|
|
4
|
+
import { PrimariaBroker } from "../broker/primaria-broker";
|
|
5
|
+
import { TokenManager } from "../token-manager/token-manager";
|
|
6
|
+
|
|
7
|
+
const CHECK_INTERVAL_MS = 30_000;
|
|
8
|
+
const REFRESH_THRESHOLD_S = 120;
|
|
9
|
+
const ACTIVITY_WINDOW_MS = 15 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export interface SessionRefreshTimer {
|
|
12
|
+
start: () => void;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SessionRefreshTimerImpl implements SessionRefreshTimer {
|
|
17
|
+
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
18
|
+
private isRefreshing = false;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly tokenManager: TokenManager,
|
|
22
|
+
private readonly activityMonitor: ActivityMonitor,
|
|
23
|
+
private readonly broker: PrimariaBroker,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
start = () => {
|
|
27
|
+
this.intervalId = setInterval(this.checkAndRefresh, CHECK_INTERVAL_MS);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
stop = () => {
|
|
31
|
+
if (this.intervalId !== null) {
|
|
32
|
+
clearInterval(this.intervalId);
|
|
33
|
+
this.intervalId = null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
private getTokenExpiry = (): number => {
|
|
38
|
+
try {
|
|
39
|
+
const { exp } = jwtDecode(this.tokenManager.getToken()) as { exp?: number };
|
|
40
|
+
return exp ?? 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
private checkAndRefresh = async () => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
|
|
49
|
+
// Skip if the user hasn't interacted in the last ACTIVITY_WINDOW_MS.
|
|
50
|
+
// Avoids keeping sessions alive for unattended tabs.
|
|
51
|
+
if (now - this.activityMonitor.getLastActivityTimestamp() > ACTIVITY_WINDOW_MS) return;
|
|
52
|
+
|
|
53
|
+
// Skip if a refresh is already in flight (this interval can fire again before the previous awaits).
|
|
54
|
+
if (this.isRefreshing) return;
|
|
55
|
+
|
|
56
|
+
const exp = this.getTokenExpiry();
|
|
57
|
+
|
|
58
|
+
// Skip if the token can't be decoded (e.g. not yet initialized).
|
|
59
|
+
if (exp === 0) return;
|
|
60
|
+
|
|
61
|
+
// exp is in seconds, now is in milliseconds — compare in the same unit.
|
|
62
|
+
if (exp - now / 1000 <= REFRESH_THRESHOLD_S) {
|
|
63
|
+
this.isRefreshing = true;
|
|
64
|
+
try {
|
|
65
|
+
await this.tokenManager.refreshToken();
|
|
66
|
+
} catch {
|
|
67
|
+
this.broker.publish(BROKER_EVENTS.shell.refreshTokenFailed, {});
|
|
68
|
+
} finally {
|
|
69
|
+
this.isRefreshing = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const createSessionRefreshTimer = (
|
|
76
|
+
tokenManager: TokenManager,
|
|
77
|
+
activityMonitor: ActivityMonitor,
|
|
78
|
+
broker: PrimariaBroker,
|
|
79
|
+
): SessionRefreshTimer => new SessionRefreshTimerImpl(tokenManager, activityMonitor, broker);
|
|
@@ -3,10 +3,10 @@ import axios from "axios";
|
|
|
3
3
|
|
|
4
4
|
vi.mock("axios");
|
|
5
5
|
|
|
6
|
-
const access_token =
|
|
7
|
-
const refresh_token =
|
|
8
|
-
const new_access_token =
|
|
9
|
-
const new_refresh_token =
|
|
6
|
+
const access_token = "test_access_token";
|
|
7
|
+
const refresh_token = "test_refresh_token";
|
|
8
|
+
const new_access_token = "new_access_token";
|
|
9
|
+
const new_refresh_token = "new_refresh_token";
|
|
10
10
|
|
|
11
11
|
describe("TokenManager", () => {
|
|
12
12
|
let mockLocation: Partial<Location>;
|
|
@@ -14,14 +14,12 @@ describe("TokenManager", () => {
|
|
|
14
14
|
let createTokenManager: any;
|
|
15
15
|
|
|
16
16
|
beforeEach(async () => {
|
|
17
|
-
// Reset module state by requiring a fresh instance
|
|
18
17
|
vi.resetModules();
|
|
19
18
|
mockLocation = {
|
|
20
|
-
search: `?access_token=${access_token}&refresh_token=${refresh_token}
|
|
19
|
+
search: `?access_token=${access_token}&refresh_token=${refresh_token}`,
|
|
21
20
|
};
|
|
22
|
-
vi.spyOn(window,
|
|
21
|
+
vi.spyOn(window, "location", "get").mockReturnValue(mockLocation as Location);
|
|
23
22
|
|
|
24
|
-
// Import fresh module
|
|
25
23
|
const module = await import("./token-manager");
|
|
26
24
|
TokenManagerImpl = module.TokenManagerImpl;
|
|
27
25
|
createTokenManager = module.createTokenManager;
|
|
@@ -34,23 +32,18 @@ describe("TokenManager", () => {
|
|
|
34
32
|
describe("getToken", () => {
|
|
35
33
|
it("should initialize token from URL params on first call", () => {
|
|
36
34
|
const tokenManager = new TokenManagerImpl();
|
|
37
|
-
|
|
38
|
-
expect(token).toBe(access_token);
|
|
35
|
+
expect(tokenManager.getToken()).toBe(access_token);
|
|
39
36
|
});
|
|
40
37
|
|
|
41
38
|
it("should return empty string if URL params are missing", () => {
|
|
42
39
|
mockLocation.search = "";
|
|
43
40
|
const tokenManager = new TokenManagerImpl();
|
|
44
|
-
|
|
45
|
-
expect(token).toBe("");
|
|
41
|
+
expect(tokenManager.getToken()).toBe("");
|
|
46
42
|
});
|
|
47
43
|
|
|
48
44
|
it("should return the same token on subsequent calls", () => {
|
|
49
45
|
const tokenManager = new TokenManagerImpl();
|
|
50
|
-
|
|
51
|
-
const token2 = tokenManager.getToken();
|
|
52
|
-
expect(token1).toBe(token2);
|
|
53
|
-
expect(token1).toBe(access_token);
|
|
46
|
+
expect(tokenManager.getToken()).toBe(tokenManager.getToken());
|
|
54
47
|
});
|
|
55
48
|
});
|
|
56
49
|
|
|
@@ -64,67 +57,58 @@ describe("TokenManager", () => {
|
|
|
64
57
|
it("should throw error if tokens are already initialized", () => {
|
|
65
58
|
const tokenManager = new TokenManagerImpl();
|
|
66
59
|
tokenManager.setInitialTokens("manual_access", "manual_refresh");
|
|
67
|
-
expect(() => tokenManager.setInitialTokens("another_access", "another_refresh"))
|
|
68
|
-
|
|
60
|
+
expect(() => tokenManager.setInitialTokens("another_access", "another_refresh")).toThrow(
|
|
61
|
+
"Token already initialized",
|
|
62
|
+
);
|
|
69
63
|
});
|
|
70
64
|
|
|
71
65
|
it("should throw error if tokens were already initialized from URL", () => {
|
|
72
66
|
const tokenManager = new TokenManagerImpl();
|
|
73
|
-
tokenManager.getToken();
|
|
74
|
-
expect(() => tokenManager.setInitialTokens("manual_access", "manual_refresh"))
|
|
75
|
-
|
|
67
|
+
tokenManager.getToken();
|
|
68
|
+
expect(() => tokenManager.setInitialTokens("manual_access", "manual_refresh")).toThrow(
|
|
69
|
+
"Token already initialized",
|
|
70
|
+
);
|
|
76
71
|
});
|
|
77
72
|
});
|
|
78
73
|
|
|
79
74
|
describe("refreshToken", () => {
|
|
80
75
|
it("should refresh token successfully", async () => {
|
|
81
76
|
vi.mocked(axios.post).mockResolvedValue({
|
|
82
|
-
data: {
|
|
83
|
-
access_token: new_access_token,
|
|
84
|
-
refresh_token: new_refresh_token
|
|
85
|
-
}
|
|
77
|
+
data: { access_token: new_access_token, refresh_token: new_refresh_token },
|
|
86
78
|
});
|
|
87
79
|
|
|
88
80
|
const tokenManager = new TokenManagerImpl();
|
|
89
81
|
tokenManager.setInitialTokens(access_token, refresh_token);
|
|
90
82
|
|
|
91
|
-
const
|
|
83
|
+
const refreshed = await tokenManager.refreshToken();
|
|
92
84
|
|
|
93
|
-
expect(axios.post).toHaveBeenCalledWith(
|
|
94
|
-
expect(
|
|
85
|
+
expect(axios.post).toHaveBeenCalledWith("/api/token/refresh", { token: refresh_token });
|
|
86
|
+
expect(refreshed).toBe(new_access_token);
|
|
95
87
|
expect(tokenManager.getToken()).toBe(new_access_token);
|
|
96
88
|
});
|
|
97
89
|
|
|
98
90
|
it("should initialize tokens from URL before refreshing if not initialized", async () => {
|
|
99
91
|
vi.mocked(axios.post).mockResolvedValue({
|
|
100
|
-
data: {
|
|
101
|
-
access_token: new_access_token,
|
|
102
|
-
refresh_token: new_refresh_token
|
|
103
|
-
}
|
|
92
|
+
data: { access_token: new_access_token, refresh_token: new_refresh_token },
|
|
104
93
|
});
|
|
105
94
|
|
|
106
95
|
const tokenManager = new TokenManagerImpl();
|
|
107
96
|
await tokenManager.refreshToken();
|
|
108
97
|
|
|
109
|
-
expect(axios.post).toHaveBeenCalledWith(
|
|
110
|
-
expect(tokenManager.getToken()).toBe(new_access_token);
|
|
98
|
+
expect(axios.post).toHaveBeenCalledWith("/api/token/refresh", { token: refresh_token });
|
|
111
99
|
});
|
|
112
100
|
|
|
113
101
|
it("should throw error if refresh response doesn't contain access_token", async () => {
|
|
114
|
-
vi.mocked(axios.post).mockResolvedValue({
|
|
115
|
-
data: {}
|
|
116
|
-
});
|
|
102
|
+
vi.mocked(axios.post).mockResolvedValue({ data: {} });
|
|
117
103
|
|
|
118
104
|
const tokenManager = new TokenManagerImpl();
|
|
119
105
|
tokenManager.setInitialTokens(access_token, refresh_token);
|
|
120
106
|
|
|
121
|
-
await expect(tokenManager.refreshToken())
|
|
122
|
-
.rejects.toThrow("Invalid refresh token response");
|
|
107
|
+
await expect(tokenManager.refreshToken()).rejects.toThrow("Invalid refresh token response");
|
|
123
108
|
});
|
|
124
109
|
|
|
125
110
|
it("should handle axios errors", async () => {
|
|
126
|
-
|
|
127
|
-
vi.mocked(axios.post).mockRejectedValue(error);
|
|
111
|
+
vi.mocked(axios.post).mockRejectedValue(new Error("Network error"));
|
|
128
112
|
|
|
129
113
|
const tokenManager = new TokenManagerImpl();
|
|
130
114
|
tokenManager.setInitialTokens(access_token, refresh_token);
|
|
@@ -135,9 +119,7 @@ describe("TokenManager", () => {
|
|
|
135
119
|
|
|
136
120
|
describe("createTokenManager", () => {
|
|
137
121
|
it("should return a singleton instance", () => {
|
|
138
|
-
|
|
139
|
-
const tokenManager2 = createTokenManager();
|
|
140
|
-
expect(tokenManager1).toBe(tokenManager2);
|
|
122
|
+
expect(createTokenManager()).toBe(createTokenManager());
|
|
141
123
|
});
|
|
142
124
|
});
|
|
143
125
|
});
|
|
@@ -9,14 +9,14 @@ export interface TokenManager {
|
|
|
9
9
|
let token: string;
|
|
10
10
|
let refreshToken: string;
|
|
11
11
|
let tokenInitialized = false;
|
|
12
|
+
|
|
12
13
|
export class TokenManagerImpl implements TokenManager {
|
|
13
14
|
getUrlParams = (): URLSearchParams => {
|
|
14
15
|
return new URLSearchParams(window.location.search);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
private initToken = () => {
|
|
18
|
-
if (tokenInitialized)
|
|
19
|
-
throw new Error("Token already initialized");
|
|
19
|
+
if (tokenInitialized) throw new Error("Token already initialized");
|
|
20
20
|
tokenInitialized = true;
|
|
21
21
|
const searchString = this.getUrlParams();
|
|
22
22
|
token = searchString.get("access_token") || "";
|
|
@@ -25,37 +25,32 @@ export class TokenManagerImpl implements TokenManager {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
setInitialTokens = (access_token: string, refresh_token: string) => {
|
|
28
|
-
if (tokenInitialized)
|
|
29
|
-
throw new Error("Token already initialized");
|
|
28
|
+
if (tokenInitialized) throw new Error("Token already initialized");
|
|
30
29
|
token = access_token;
|
|
31
30
|
refreshToken = refresh_token;
|
|
32
31
|
tokenInitialized = true;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
getToken = () => {
|
|
36
|
-
if (!tokenInitialized)
|
|
37
|
-
this.initToken();
|
|
35
|
+
if (!tokenInitialized) this.initToken();
|
|
38
36
|
return token;
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
refreshToken = async () => {
|
|
42
|
-
if (!tokenInitialized)
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const {access_token, refresh_token} = response.data;
|
|
47
|
-
if(!access_token){
|
|
48
|
-
throw new Error("Invalid refresh token response");
|
|
49
|
-
}
|
|
40
|
+
if (!tokenInitialized) this.initToken();
|
|
41
|
+
const response = await axios.post("/api/token/refresh", { token: refreshToken });
|
|
42
|
+
const { access_token, refresh_token } = response.data;
|
|
43
|
+
if (!access_token) throw new Error("Invalid refresh token response");
|
|
50
44
|
token = access_token;
|
|
51
45
|
refreshToken = refresh_token;
|
|
52
46
|
return token;
|
|
53
47
|
};
|
|
54
48
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
|
|
50
|
+
let tokenManager: TokenManagerImpl;
|
|
51
|
+
|
|
52
|
+
export const createTokenManager = () => {
|
|
53
|
+
if (tokenManager) return tokenManager;
|
|
58
54
|
tokenManager = new TokenManagerImpl();
|
|
59
|
-
//tokenManager.initToken();
|
|
60
55
|
return tokenManager;
|
|
61
|
-
}
|
|
56
|
+
};
|
package/src/bootstrapper.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { PrimariaShell } from "./UI/components/primaria-shell/primaria-shell";
|
|
2
|
-
import { shellApi } from "./api/api";
|
|
2
|
+
import { activityMonitor, sessionRefreshTimer, shellApi } from "./api/api";
|
|
3
3
|
import { EcapContext } from "./api/context-manager/context-manager";
|
|
4
4
|
import { useFeatures } from "./features/bootstrapper";
|
|
5
5
|
import { useLocalization } from "./locales";
|
|
6
6
|
import { useUI } from "./UI/bootstrapper";
|
|
7
7
|
|
|
8
8
|
export const initializeShell = (hostAppElement: HTMLElement, ecapContext?: EcapContext) => {
|
|
9
|
-
// Initialize the context manager with ECAP parameters
|
|
10
9
|
if (ecapContext) {
|
|
11
10
|
(shellApi.contextManager as any).initializeContext(ecapContext);
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
activityMonitor.start();
|
|
14
|
+
sessionRefreshTimer.start();
|
|
14
15
|
useLocalization(shellApi);
|
|
15
16
|
useUI();
|
|
16
17
|
useFeatures(shellApi);
|