diff --git a/web_session_auto_close/README.rst b/web_session_auto_close/README.rst new file mode 100644 index 000000000000..478c621a1383 --- /dev/null +++ b/web_session_auto_close/README.rst @@ -0,0 +1,92 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +====================== +Web Session Auto Close +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8244d213321585d6b7f6632ffc7e98fa6fb6902f1ac2e3e883efb596bbf55572 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/19.0/web_session_auto_close + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-19-0/web-19-0-web_session_auto_close + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module automatically closes inactive user sessions based on a +configurable timeout. If no activity is detected within the set +duration, the session is destroyed, and the page reloads. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The timeout can be adjusted in **Settings > General under "Session +Auto-Close Timeout"**, where the value is set in seconds. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Laurent Stukkens (https://www.acsone.eu) +- Souheil Bejaoui (https://www.acsone.eu) +- `Komit `__: + + - Vang Nguyen Phu + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_session_auto_close/__init__.py b/web_session_auto_close/__init__.py new file mode 100644 index 000000000000..91c5580fed36 --- /dev/null +++ b/web_session_auto_close/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/web_session_auto_close/__manifest__.py b/web_session_auto_close/__manifest__.py new file mode 100644 index 000000000000..ac6eac60c68c --- /dev/null +++ b/web_session_auto_close/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Web Session Auto Close", + "summary": """Automatically logs out inactive users based on a configurable + timeout.""", + "version": "19.0.1.0.1", + "license": "AGPL-3", + "author": "ACSONE SA/NV, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "data": ["views/res_config_settings.xml"], + "assets": { + "web.assets_backend": [ + "web_session_auto_close/static/src/js/session_auto_close.esm.js", + ], + }, +} diff --git a/web_session_auto_close/controllers/__init__.py b/web_session_auto_close/controllers/__init__.py new file mode 100644 index 000000000000..12a7e529b674 --- /dev/null +++ b/web_session_auto_close/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/web_session_auto_close/controllers/main.py b/web_session_auto_close/controllers/main.py new file mode 100644 index 000000000000..24d42a425721 --- /dev/null +++ b/web_session_auto_close/controllers/main.py @@ -0,0 +1,22 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import http +from odoo.http import request + + +class WebSessionAutoCloseController(http.Controller): + @http.route("/web/session/get_timeout", type="jsonrpc", auth="user") + def get_session_timeout(self): + default_sec = 600 + timeout_sec = ( + request.env["ir.config_parameter"] + .sudo() + .get_param("web_session_auto_close.timeout", default_sec) + ) + try: + timeout_int = int(timeout_sec) + except (TypeError, ValueError): + timeout_int = default_sec + + return timeout_int * 1000 diff --git a/web_session_auto_close/i18n/it.po b/web_session_auto_close/i18n/it.po new file mode 100644 index 000000000000..03821fbb5b17 --- /dev/null +++ b/web_session_auto_close/i18n/it.po @@ -0,0 +1,27 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_session_auto_close +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-09 10:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: web_session_auto_close +#: model:ir.model,name:web_session_auto_close.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: web_session_auto_close +#: model:ir.model.fields,field_description:web_session_auto_close.field_res_config_settings__session_auto_close_timeout +msgid "Session Auto-Close Timeout (seconds)" +msgstr "Timeout chiusura automatica sessione (secondi)" diff --git a/web_session_auto_close/i18n/web_session_auto_close.pot b/web_session_auto_close/i18n/web_session_auto_close.pot new file mode 100644 index 000000000000..10cc8a11bbb3 --- /dev/null +++ b/web_session_auto_close/i18n/web_session_auto_close.pot @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_session_auto_close +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_session_auto_close +#: model:ir.model,name:web_session_auto_close.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: web_session_auto_close +#: model:ir.model.fields,field_description:web_session_auto_close.field_res_config_settings__session_auto_close_timeout +msgid "Session Auto-Close Timeout (seconds)" +msgstr "" diff --git a/web_session_auto_close/models/__init__.py b/web_session_auto_close/models/__init__.py new file mode 100644 index 000000000000..0deb68c46806 --- /dev/null +++ b/web_session_auto_close/models/__init__.py @@ -0,0 +1 @@ +from . import res_config_settings diff --git a/web_session_auto_close/models/res_config_settings.py b/web_session_auto_close/models/res_config_settings.py new file mode 100644 index 000000000000..5ea44efae8f2 --- /dev/null +++ b/web_session_auto_close/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + session_auto_close_timeout = fields.Integer( + string="Session Auto-Close Timeout (seconds)", + config_parameter="web_session_auto_close.timeout", + default=600, + ) diff --git a/web_session_auto_close/pyproject.toml b/web_session_auto_close/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/web_session_auto_close/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_session_auto_close/readme/CONFIGURE.md b/web_session_auto_close/readme/CONFIGURE.md new file mode 100644 index 000000000000..134a3f1de9d7 --- /dev/null +++ b/web_session_auto_close/readme/CONFIGURE.md @@ -0,0 +1,2 @@ +The timeout can be adjusted in **Settings > General under "Session Auto-Close Timeout"**, +where the value is set in seconds. diff --git a/web_session_auto_close/readme/CONTRIBUTORS.md b/web_session_auto_close/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..03bed7cc06b4 --- /dev/null +++ b/web_session_auto_close/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- Laurent Stukkens \<\> + () +- Souheil Bejaoui \<\> + () +- [Komit](https://komit-consulting.com): + - Vang Nguyen Phu \ No newline at end of file diff --git a/web_session_auto_close/readme/DESCRIPTION.md b/web_session_auto_close/readme/DESCRIPTION.md new file mode 100644 index 000000000000..c8081fedf5a4 --- /dev/null +++ b/web_session_auto_close/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module automatically closes inactive user sessions based on a +configurable timeout. If no activity is detected within the set +duration, the session is destroyed, and the page reloads. diff --git a/web_session_auto_close/static/description/icon.png b/web_session_auto_close/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/web_session_auto_close/static/description/icon.png differ diff --git a/web_session_auto_close/static/description/index.html b/web_session_auto_close/static/description/index.html new file mode 100644 index 000000000000..34e09e01a192 --- /dev/null +++ b/web_session_auto_close/static/description/index.html @@ -0,0 +1,442 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Web Session Auto Close

+ +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

This module automatically closes inactive user sessions based on a +configurable timeout. If no activity is detected within the set +duration, the session is destroyed, and the page reloads.

+

Table of contents

+ +
+

Configuration

+

The timeout can be adjusted in Settings > General under “Session +Auto-Close Timeout”, where the value is set in seconds.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/web_session_auto_close/static/src/js/session_auto_close.esm.js b/web_session_auto_close/static/src/js/session_auto_close.esm.js new file mode 100644 index 000000000000..fb1c050aeebd --- /dev/null +++ b/web_session_auto_close/static/src/js/session_auto_close.esm.js @@ -0,0 +1,141 @@ +/* Copyright 2025 ACSONE SA/NV + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */ + +import {rpc} from "@web/core/network/rpc"; +import {session} from "@web/session"; + +// Default session timeout in ms (will be updated from server settings) +const SESSION_TIMEOUT = 600000; + +export class SessionAutoCloseService { + constructor() { + this.sessionTimeout = SESSION_TIMEOUT; + this._checkIntervalId = null; + this._boundCheckInactivity = () => this.checkInactivity(); + this._boundHandleUserActivity = () => this.handleUserActivity(); + } + + /** + * Storage key for last activity timestamp in localStorage. + * @returns {String} + */ + getActivityStorageKey() { + return "lastActivityTime"; + } + + /** + * Get the last recorded user activity timestamp from localStorage + * if no record is found, returns the current timestamp + */ + getLastActivityTime() { + const key = this.getActivityStorageKey(); + const value = globalThis.window.localStorage.getItem(key); + return parseInt(value, 10) || Date.now(); + } + + /** + * Set the last activity timestamp in localStorage + * this is called whenever user interaction is detected + */ + updateActivityTime() { + const key = this.getActivityStorageKey(); + globalThis.window.localStorage.setItem(key, String(Date.now())); + } + + /** + * Destroy the session + * removes the last activity record and reloads the page + */ + async closeSession() { + await rpc("/web/session/destroy", {}); + const key = this.getActivityStorageKey(); + globalThis.window.localStorage.removeItem(key); + globalThis.window.location.reload(); + } + + /** + * Handler for activity events; by default just updates the activity time. + */ + handleUserActivity() { + this.updateActivityTime(); + } + + /** + * Checks for user inactivity and closes the session if the timeout is exceeded + */ + checkInactivity() { + const now = Date.now(); + const lastActivityTime = this.getLastActivityTime(); + if (now - lastActivityTime >= this.sessionTimeout) { + this.closeSession(); + } + } + + /** + * Whether the service should start (e.g. only when session exists). + * @returns {Boolean} + */ + shouldStart() { + return Boolean(session); + } + + /** + * Fetch timeout from server. + * @returns {Promise} Timeout in ms + */ + async getTimeout() { + const timeout = await rpc("/web/session/get_timeout", {}); + return parseInt(timeout, 10) || SESSION_TIMEOUT; + } + + /** + * Event bindings for activity detection. + * @returns {{ target: EventTarget, events: string[] }} + */ + getActivityEvents() { + return { + target: globalThis.window, + events: ["mousemove", "keydown"], + }; + } + + /** + * Attach activity listeners and start the periodic inactivity check. + */ + _startMonitoring() { + const key = this.getActivityStorageKey(); + if (globalThis.window.localStorage.getItem(key)) { + this.checkInactivity(); + } + + const {target, events} = this.getActivityEvents(); + for (const eventName of events) { + target.addEventListener(eventName, this._boundHandleUserActivity); + } + + this.updateActivityTime(); + const intervalMs = this.sessionTimeout / 2; + this._checkIntervalId = globalThis.setInterval( + this._boundCheckInactivity, + intervalMs + ); + } + + /** + * Start the service: load timeout, then start monitoring if shouldStart(). + * Call once after construction. + */ + async start() { + this.sessionTimeout = await this.getTimeout(); + if (!this.shouldStart()) { + return; + } + this._startMonitoring(); + } +} + +/** + * Default service instance, started on load. + */ +const service = new SessionAutoCloseService(); +service.start(); diff --git a/web_session_auto_close/tests/__init__.py b/web_session_auto_close/tests/__init__.py new file mode 100644 index 000000000000..81fcd17fef9b --- /dev/null +++ b/web_session_auto_close/tests/__init__.py @@ -0,0 +1 @@ +from . import test_get_timeout_controller diff --git a/web_session_auto_close/tests/test_get_timeout_controller.py b/web_session_auto_close/tests/test_get_timeout_controller.py new file mode 100644 index 000000000000..6b31cc4cda80 --- /dev/null +++ b/web_session_auto_close/tests/test_get_timeout_controller.py @@ -0,0 +1,43 @@ +from odoo.tests import tagged + +from odoo.addons.base.tests.common import HttpCaseWithUserDemo + + +@tagged("-at_install", "post_install") +class TestGetTimeoutController(HttpCaseWithUserDemo): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.session = cls.authenticate(cls, "demo", "demo") + + def setUp(self): + super().setUp() + self.opener.cookies["session_id"] = self.session.sid + + def test_get_timeout_returns_ms(self): + self.env["ir.config_parameter"].sudo().set_param( + "web_session_auto_close.timeout", 123 + ) + timeout = self.make_jsonrpc_request("/web/session/get_timeout", {}) + self.assertEqual(timeout, 123 * 1000) + + def test_get_timeout_default_when_invalid_value(self): + self.env["ir.config_parameter"].sudo().set_param( + "web_session_auto_close.timeout", "invalid" + ) + timeout = self.make_jsonrpc_request("/web/session/get_timeout", {}) + self.assertEqual(timeout, 600 * 1000) + + def test_get_timeout_default_when_float_string(self): + self.env["ir.config_parameter"].sudo().set_param( + "web_session_auto_close.timeout", "12.5" + ) + timeout = self.make_jsonrpc_request("/web/session/get_timeout", {}) + self.assertEqual(timeout, 600 * 1000) + + def test_get_timeout_zero(self): + self.env["ir.config_parameter"].sudo().set_param( + "web_session_auto_close.timeout", 0 + ) + timeout = self.make_jsonrpc_request("/web/session/get_timeout", {}) + self.assertEqual(timeout, 0) diff --git a/web_session_auto_close/views/res_config_settings.xml b/web_session_auto_close/views/res_config_settings.xml new file mode 100644 index 000000000000..f95479624339 --- /dev/null +++ b/web_session_auto_close/views/res_config_settings.xml @@ -0,0 +1,16 @@ + + + + + res.config.settings + + + + + + + + + +