|
1 | | -/** @odoo-module **/ |
2 | | - |
3 | 1 | /* Copyright 2025 ACSONE SA/NV |
4 | 2 | * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ |
5 | 3 |
|
6 | 4 | import {rpc} from "@web/core/network/rpc"; |
7 | 5 | import {session} from "@web/session"; |
8 | 6 |
|
9 | 7 | // Default session timeout in ms (will be updated from server settings) |
10 | | -let SESSION_TIMEOUT = 600000; |
| 8 | +const SESSION_TIMEOUT = 600000; |
11 | 9 |
|
12 | | -/** |
13 | | - * Get the last recorded user activity timestamp from localStorage |
14 | | - * if no record is found, returns the current timestamp |
15 | | - */ |
16 | | -function getLastActivityTime() { |
17 | | - return ( |
18 | | - parseInt(globalThis.window.localStorage.getItem("lastActivityTime"), 10) || |
19 | | - Date.now() |
20 | | - ); |
21 | | -} |
| 10 | +export class SessionAutoCloseService { |
| 11 | + constructor() { |
| 12 | + this.sessionTimeout = SESSION_TIMEOUT; |
| 13 | + this._checkIntervalId = null; |
| 14 | + this._boundCheckInactivity = () => this.checkInactivity(); |
| 15 | + this._boundHandleUserActivity = () => this.handleUserActivity(); |
| 16 | + } |
22 | 17 |
|
23 | | -/** |
24 | | - * Set the last activity timestamp in localStorage |
25 | | - * this is called whenever user interaction is detected |
26 | | - */ |
27 | | -function updateActivityTime() { |
28 | | - const now = Date.now(); |
29 | | - globalThis.window.localStorage.setItem("lastActivityTime", now); |
30 | | -} |
| 18 | + /** |
| 19 | + * Storage key for last activity timestamp in localStorage. |
| 20 | + * @returns {String} |
| 21 | + */ |
| 22 | + getActivityStorageKey() { |
| 23 | + return "lastActivityTime"; |
| 24 | + } |
31 | 25 |
|
32 | | -/** |
33 | | - * Destroy the session |
34 | | - * removes the last activity record and reloads the page |
35 | | - */ |
36 | | -function closeSession() { |
37 | | - rpc("/web/session/destroy", {}).then(() => { |
38 | | - globalThis.window.localStorage.removeItem("lastActivityTime"); |
| 26 | + /** |
| 27 | + * Get the last recorded user activity timestamp from localStorage |
| 28 | + * if no record is found, returns the current timestamp |
| 29 | + */ |
| 30 | + getLastActivityTime() { |
| 31 | + const key = this.getActivityStorageKey(); |
| 32 | + const value = globalThis.window.localStorage.getItem(key); |
| 33 | + return parseInt(value, 10) || Date.now(); |
| 34 | + } |
| 35 | + |
| 36 | + /** |
| 37 | + * Set the last activity timestamp in localStorage |
| 38 | + * this is called whenever user interaction is detected |
| 39 | + */ |
| 40 | + updateActivityTime() { |
| 41 | + const key = this.getActivityStorageKey(); |
| 42 | + globalThis.window.localStorage.setItem(key, String(Date.now())); |
| 43 | + } |
| 44 | + |
| 45 | + /** |
| 46 | + * Destroy the session |
| 47 | + * removes the last activity record and reloads the page |
| 48 | + */ |
| 49 | + async closeSession() { |
| 50 | + await rpc("/web/session/destroy", {}); |
| 51 | + const key = this.getActivityStorageKey(); |
| 52 | + globalThis.window.localStorage.removeItem(key); |
39 | 53 | globalThis.window.location.reload(); |
40 | | - }); |
41 | | -} |
| 54 | + } |
42 | 55 |
|
43 | | -/** |
44 | | - * Checks for user inactivity and closes the session if the timeout is exceeded |
45 | | - */ |
46 | | -function checkInactivity() { |
47 | | - const now = Date.now(); |
48 | | - const lastActivityTime = getLastActivityTime(); |
49 | | - if (now - lastActivityTime >= SESSION_TIMEOUT) { |
50 | | - closeSession(); |
| 56 | + /** |
| 57 | + * Handler for activity events; by default just updates the activity time. |
| 58 | + */ |
| 59 | + handleUserActivity() { |
| 60 | + this.updateActivityTime(); |
51 | 61 | } |
52 | | -} |
53 | 62 |
|
54 | | -/** |
55 | | - * Init the session auto-close mechanism |
56 | | - * attaches event listeners to detect user activity and sets periodic |
57 | | - inactivity checks |
58 | | - */ |
59 | | -function startSessionAutoClose() { |
60 | | - if (!session) { |
61 | | - return; |
| 63 | + /** |
| 64 | + * Checks for user inactivity and closes the session if the timeout is exceeded |
| 65 | + */ |
| 66 | + checkInactivity() { |
| 67 | + const now = Date.now(); |
| 68 | + const lastActivityTime = this.getLastActivityTime(); |
| 69 | + if (now - lastActivityTime >= this.sessionTimeout) { |
| 70 | + this.closeSession(); |
| 71 | + } |
62 | 72 | } |
63 | 73 |
|
64 | | - // Immediately check inactivity if a last activity timestamp exists |
65 | | - if (globalThis.window.localStorage.getItem("lastActivityTime")) { |
66 | | - checkInactivity(); |
| 74 | + /** |
| 75 | + * Whether the service should start (e.g. only when session exists). |
| 76 | + * @returns {Boolean} |
| 77 | + */ |
| 78 | + shouldStart() { |
| 79 | + return Boolean(session); |
67 | 80 | } |
68 | 81 |
|
69 | | - function handleUserActivity() { |
70 | | - updateActivityTime(); |
| 82 | + /** |
| 83 | + * Fetch timeout from server. |
| 84 | + * @returns {Promise<number>} Timeout in ms |
| 85 | + */ |
| 86 | + async getTimeout() { |
| 87 | + const timeout = await rpc("/web/session/get_timeout", {}); |
| 88 | + return parseInt(timeout, 10) || SESSION_TIMEOUT; |
71 | 89 | } |
72 | 90 |
|
73 | | - // Listen for user interactions to reset the activity timer |
74 | | - globalThis.window.addEventListener("mousemove", handleUserActivity); |
75 | | - globalThis.window.addEventListener("keydown", handleUserActivity); |
| 91 | + /** |
| 92 | + * Event bindings for activity detection. |
| 93 | + * @returns {{ target: EventTarget, events: string[] }} |
| 94 | + */ |
| 95 | + getActivityEvents() { |
| 96 | + return { |
| 97 | + target: globalThis.window, |
| 98 | + events: ["mousemove", "keydown"], |
| 99 | + }; |
| 100 | + } |
76 | 101 |
|
77 | | - // Set the initial activity time and start the periodic inactivity check |
78 | | - updateActivityTime(); |
79 | | - globalThis.setInterval(checkInactivity, SESSION_TIMEOUT / 2); |
| 102 | + /** |
| 103 | + * Attach activity listeners and start the periodic inactivity check. |
| 104 | + */ |
| 105 | + _startMonitoring() { |
| 106 | + const key = this.getActivityStorageKey(); |
| 107 | + if (globalThis.window.localStorage.getItem(key)) { |
| 108 | + this.checkInactivity(); |
| 109 | + } |
| 110 | + |
| 111 | + const {target, events} = this.getActivityEvents(); |
| 112 | + for (const eventName of events) { |
| 113 | + target.addEventListener(eventName, this._boundHandleUserActivity); |
| 114 | + } |
| 115 | + |
| 116 | + this.updateActivityTime(); |
| 117 | + const intervalMs = this.sessionTimeout / 2; |
| 118 | + this._checkIntervalId = globalThis.setInterval( |
| 119 | + this._boundCheckInactivity, |
| 120 | + intervalMs |
| 121 | + ); |
| 122 | + } |
| 123 | + |
| 124 | + /** |
| 125 | + * Start the service: load timeout, then start monitoring if shouldStart(). |
| 126 | + * Call once after construction. |
| 127 | + */ |
| 128 | + async start() { |
| 129 | + this.sessionTimeout = await this.getTimeout(); |
| 130 | + if (!this.shouldStart()) { |
| 131 | + return; |
| 132 | + } |
| 133 | + this._startMonitoring(); |
| 134 | + } |
80 | 135 | } |
81 | 136 |
|
82 | | -// Fetch the session timeout value from Odoo settings and then start the session monitoring |
83 | | -rpc("/web/session/get_timeout", {}).then((timeout) => { |
84 | | - SESSION_TIMEOUT = parseInt(timeout, 10) || SESSION_TIMEOUT; |
85 | | - startSessionAutoClose(); |
86 | | -}); |
| 137 | +/** |
| 138 | + * Default service instance, started on load. |
| 139 | + */ |
| 140 | +const service = new SessionAutoCloseService(); |
| 141 | +service.start(); |
0 commit comments