django-microsys 2.2.4__tar.gz → 2.2.5__tar.gz

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.
Files changed (273) hide show
  1. {django_microsys-2.2.4 → django_microsys-2.2.5}/PKG-INFO +1 -1
  2. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/PKG-INFO +1 -1
  3. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/SOURCES.txt +2 -0
  4. django_microsys-2.2.5/microsys/VERSION +1 -0
  5. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/forms.py +25 -0
  6. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/microsys_settings.py +1 -0
  7. django_microsys-2.2.5/microsys/migrations/0007_systemsettings_prevent_multiple_active_sessions.py +18 -0
  8. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/models.py +3 -0
  9. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/system_setup.js +1 -1
  10. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/profile_2fa.js +46 -3
  11. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/profile.html +25 -5
  12. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/translations.py +20 -0
  13. django_microsys-2.2.5/microsys/trust.py +227 -0
  14. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/urls.py +1 -0
  15. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/utils.py +11 -0
  16. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/__init__.py +1 -1
  17. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/profile.py +54 -10
  18. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/twofa.py +17 -102
  19. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/users.py +2 -0
  20. django_microsys-2.2.4/microsys/VERSION +0 -1
  21. {django_microsys-2.2.4 → django_microsys-2.2.5}/LICENSE +0 -0
  22. {django_microsys-2.2.4 → django_microsys-2.2.5}/MANIFEST.in +0 -0
  23. {django_microsys-2.2.4 → django_microsys-2.2.5}/README.md +0 -0
  24. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/dependency_links.txt +0 -0
  25. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/entry_points.txt +0 -0
  26. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/requires.txt +0 -0
  27. {django_microsys-2.2.4 → django_microsys-2.2.5}/django_microsys.egg-info/top_level.txt +0 -0
  28. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/__init__.py +0 -0
  29. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/__main__.py +0 -0
  30. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/admin.py +0 -0
  31. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/api.py +0 -0
  32. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/apps.py +0 -0
  33. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/cli.py +0 -0
  34. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/constants.py +0 -0
  35. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/context_processors.py +0 -0
  36. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/discovery.py +0 -0
  37. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/fetcher.py +0 -0
  38. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/filters.py +0 -0
  39. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/fonts.py +0 -0
  40. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/formats/__init__.py +0 -0
  41. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/formats/ar/__init__.py +0 -0
  42. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/formats/ar/formats.py +0 -0
  43. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/formats/en/__init__.py +0 -0
  44. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/formats/en/formats.py +0 -0
  45. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/guards.py +0 -0
  46. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/__init__.py +0 -0
  47. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/microsys_check.py +0 -0
  48. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/microsys_setup.py +0 -0
  49. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/migrator.py +0 -0
  50. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/management/commands/seed_activity_log.py +0 -0
  51. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/managers.py +0 -0
  52. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/middleware.py +0 -0
  53. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0001_initial.py +0 -0
  54. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0002_public_registration.py +0 -0
  55. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0003_public_root_split.py +0 -0
  56. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0004_client_ip_and_trusted_devices.py +0 -0
  57. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0005_systemsettings_allow_user_font_override_and_more.py +0 -0
  58. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/0006_systemsettings_navbar_config.py +0 -0
  59. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/migrations/__init__.py +0 -0
  60. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/navbar.py +0 -0
  61. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/patches.py +0 -0
  62. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/registration.py +0 -0
  63. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold.py +0 -0
  64. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/README.md.tmpl +0 -0
  65. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/__init__.py.tmpl +0 -0
  66. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/apps.py.tmpl +0 -0
  67. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/filters.py.tmpl +0 -0
  68. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/forms.py.tmpl +0 -0
  69. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/migrations/__init__.py.tmpl +0 -0
  70. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/models.py.tmpl +0 -0
  71. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/tables.py.tmpl +0 -0
  72. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/templates/example_record_confirm_delete.html.tmpl +0 -0
  73. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/templates/example_record_detail.html.tmpl +0 -0
  74. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/templates/example_record_form.html.tmpl +0 -0
  75. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/templates/example_record_list.html.tmpl +0 -0
  76. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/tests/__init__.py.tmpl +0 -0
  77. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/tests/test_app.py.tmpl +0 -0
  78. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/translations.py.tmpl +0 -0
  79. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/urls.py.tmpl +0 -0
  80. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/app/views.py.tmpl +0 -0
  81. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/.nginx/nginx.conf.tmpl +0 -0
  82. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/.secrets/.env.tmpl +0 -0
  83. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/Dockerfile.tmpl +0 -0
  84. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/README.md.tmpl +0 -0
  85. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/compose.dev.yml.tmpl +0 -0
  86. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/compose.yml.tmpl +0 -0
  87. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/dockerignore.tmpl +0 -0
  88. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/docs/README.md.tmpl +0 -0
  89. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/entrypoint.sh.tmpl +0 -0
  90. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/gitattributes.tmpl +0 -0
  91. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/gitignore.tmpl +0 -0
  92. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/gunicorn.py.tmpl +0 -0
  93. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/manage.py.tmpl +0 -0
  94. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/__init__.py.tmpl +0 -0
  95. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/asgi.py.tmpl +0 -0
  96. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/celery.py.tmpl +0 -0
  97. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/settings.py.tmpl +0 -0
  98. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/urls.py.tmpl +0 -0
  99. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/package/wsgi.py.tmpl +0 -0
  100. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/req.txt.tmpl +0 -0
  101. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/start.ps1.tmpl +0 -0
  102. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/start.sh.tmpl +0 -0
  103. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/tests/__init__.py.tmpl +0 -0
  104. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/tests/test_scaffold.py.tmpl +0 -0
  105. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/scaffold_templates/project/tools/smtp_relay.py.tmpl +0 -0
  106. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/signals.py +0 -0
  107. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap-icons.css +0 -0
  108. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap-icons.woff +0 -0
  109. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap-icons.woff2 +0 -0
  110. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.bundle.min.js +0 -0
  111. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.bundle.min.js.map +0 -0
  112. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.min.css +0 -0
  113. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.min.css.map +0 -0
  114. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.rtl.min.css +0 -0
  115. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/bootstrap/bootstrap.rtl.min.css.map +0 -0
  116. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/favicon.ico +0 -0
  117. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/img/base_logo.webp +0 -0
  118. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/img/default_profile.webp +0 -0
  119. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/img/login_logo.webp +0 -0
  120. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/accessibility/css/main.css +0 -0
  121. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/accessibility/js/main.js +0 -0
  122. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/activitylog/js/main.js +0 -0
  123. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/TwemojiCountryFlags.woff2 +0 -0
  124. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/cairo-bold.woff2 +0 -0
  125. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/cairo-medium.woff2 +0 -0
  126. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/cairo-regular.woff2 +0 -0
  127. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/shabwa-bold.woff2 +0 -0
  128. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/shabwa-medium.woff2 +0 -0
  129. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/fonts/shabwa-regular.woff2 +0 -0
  130. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/forms/css/file_field.css +0 -0
  131. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/forms/css/form_actions.css +0 -0
  132. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/forms/css/form_fields.css +0 -0
  133. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/forms/js/file_field.js +0 -0
  134. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/forms/js/filter_form.js +0 -0
  135. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/autofill/js/main.js +0 -0
  136. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/context_menu/css/main.css +0 -0
  137. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/context_menu/js/main.js +0 -0
  138. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/context_menu/js/section_manager.js +0 -0
  139. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/dynamic_modal/js/main.js +0 -0
  140. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/prevent_double_submit.js +0 -0
  141. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/scan_link/js/main.js +0 -0
  142. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/scan_link/js/scan_button.js +0 -0
  143. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/helpers/wizard/js/main.js +0 -0
  144. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/language/css/main.css +0 -0
  145. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/language/js/main.js +0 -0
  146. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/language/js/translations.js +0 -0
  147. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/buttons.css +0 -0
  148. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/dropdowns.css +0 -0
  149. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/index_cards.css +0 -0
  150. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/main.css +0 -0
  151. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/navbar.css +0 -0
  152. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/options.css +0 -0
  153. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/pagination.css +0 -0
  154. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/selectors.css +0 -0
  155. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/system_setup.css +0 -0
  156. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/tables.css +0 -0
  157. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/template_cleanup.css +0 -0
  158. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/css/titlebar.css +0 -0
  159. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/base_head.js +0 -0
  160. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/base_runtime.js +0 -0
  161. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/navbar.js +0 -0
  162. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/options.js +0 -0
  163. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/selectors.js +0 -0
  164. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/main/js/tables.js +0 -0
  165. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sections/js/manage_sections.js +0 -0
  166. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/css/main.css +0 -0
  167. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/css/reorder.css +0 -0
  168. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/css/theme_picker.css +0 -0
  169. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/js/main.js +0 -0
  170. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/js/preload.js +0 -0
  171. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/js/reorder.js +0 -0
  172. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/sidebar/js/theme_picker.js +0 -0
  173. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/blue.css +0 -0
  174. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/dark.css +0 -0
  175. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/gold.css +0 -0
  176. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/gothic.css +0 -0
  177. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/green.css +0 -0
  178. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/light.css +0 -0
  179. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/mono.css +0 -0
  180. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/neon.css +0 -0
  181. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/red.css +0 -0
  182. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/css/retro.css +0 -0
  183. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/themes/js/main.js +0 -0
  184. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/tutorial/css/main.css +0 -0
  185. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/tutorial/js/driver.js +0 -0
  186. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/tutorial/js/main.js +0 -0
  187. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/css/login.css +0 -0
  188. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/css/permissions.css +0 -0
  189. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/css/profile.css +0 -0
  190. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/css/user_hub.css +0 -0
  191. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/login.js +0 -0
  192. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/manage_users.js +0 -0
  193. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/permissions.js +0 -0
  194. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/profile_image_widget.js +0 -0
  195. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/twofa_verify.js +0 -0
  196. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/microsys/users/js/user_hub.js +0 -0
  197. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/vanillajs-datepicker/datepicker-bs5.min.css +0 -0
  198. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/static/vanillajs-datepicker/datepicker.min.js +0 -0
  199. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/tables.py +0 -0
  200. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/bootstrap5/layout/field_file.html +0 -0
  201. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/2fa/verify.html +0 -0
  202. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/activitylog/activity_log.html +0 -0
  203. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/activitylog/activity_log_detail_modal.html +0 -0
  204. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/base.html +0 -0
  205. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/form_base.html +0 -0
  206. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/assets_head.html +0 -0
  207. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/assets_scripts.html +0 -0
  208. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/crispy_file_field.html +0 -0
  209. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/file_input.html +0 -0
  210. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/filter_assets_head.html +0 -0
  211. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/forms/filter_assets_scripts.html +0 -0
  212. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/dynamic_modal.html +0 -0
  213. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/dynamic_modal_combined.html +0 -0
  214. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/dynamic_modal_detail.html +0 -0
  215. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/dynamic_modal_form.html +0 -0
  216. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/dynamic_modal_list.html +0 -0
  217. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/helpers/micro_context_menu.html +0 -0
  218. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/font_previews.html +0 -0
  219. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/font_settings_matrix.html +0 -0
  220. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/language_catalog_editor.html +0 -0
  221. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/language_fonts_editor.html +0 -0
  222. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/language_previews.html +0 -0
  223. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/navbar.html +0 -0
  224. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/navbar_builder.html +0 -0
  225. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/navbar_mode_previews.html +0 -0
  226. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/options.html +0 -0
  227. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/sidebar_builder.html +0 -0
  228. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/sidebar_density_previews.html +0 -0
  229. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/sidebar_items.html +0 -0
  230. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/system_names_editor.html +0 -0
  231. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/system_setup.html +0 -0
  232. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/table_density_previews.html +0 -0
  233. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/theme_previews.html +0 -0
  234. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/theme_settings_matrix.html +0 -0
  235. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/titlebar.html +0 -0
  236. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/translation_matrix_editor.html +0 -0
  237. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/includes/tutorial.html +0 -0
  238. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/list_base.html +0 -0
  239. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/registration/pending.html +0 -0
  240. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/scopes/scope_actions.html +0 -0
  241. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/scopes/scope_form.html +0 -0
  242. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/scopes/scope_manager.html +0 -0
  243. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sections/manage_sections.html +0 -0
  244. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sections/subsection_select.html +0 -0
  245. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sidebar/auto.html +0 -0
  246. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sidebar/extra_groups.html +0 -0
  247. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sidebar/main.html +0 -0
  248. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/sidebar/tree.html +0 -0
  249. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/tables/table.html +0 -0
  250. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/grouped_permissions.html +0 -0
  251. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/manage_users.html +0 -0
  252. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/profile_image_widget.html +0 -0
  253. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/user_detail_modal.html +0 -0
  254. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/microsys/users/user_hub.html +0 -0
  255. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/registration/email/verify_registration.txt +0 -0
  256. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/registration/login.html +0 -0
  257. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/registration/register.html +0 -0
  258. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/registration/register_sent.html +0 -0
  259. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templates/registration/register_verify.html +0 -0
  260. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templatetags/__init__.py +0 -0
  261. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templatetags/microsys_tags.py +0 -0
  262. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templatetags/microsys_translation.py +0 -0
  263. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/templatetags/sidebar_tags.py +0 -0
  264. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/themes.py +0 -0
  265. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/activitylog.py +0 -0
  266. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/general.py +0 -0
  267. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/registration.py +0 -0
  268. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/scopes.py +0 -0
  269. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/sections.py +0 -0
  270. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/views/sidebar.py +0 -0
  271. {django_microsys-2.2.4 → django_microsys-2.2.5}/microsys/widgets.py +0 -0
  272. {django_microsys-2.2.4 → django_microsys-2.2.5}/pyproject.toml +0 -0
  273. {django_microsys-2.2.4 → django_microsys-2.2.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django_microsys
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: Django microSYS (System Integration Service) - Multilingual Django Starter Pack, Packed with Features.
5
5
  Author-email: DeBeski <debeski1@gmail.com>
6
6
  License: NON-COMMERCIAL
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-microsys
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: Django microSYS (System Integration Service) - Multilingual Django Starter Pack, Packed with Features.
5
5
  Author-email: DeBeski <debeski1@gmail.com>
6
6
  License: NON-COMMERCIAL
@@ -34,6 +34,7 @@ microsys/signals.py
34
34
  microsys/tables.py
35
35
  microsys/themes.py
36
36
  microsys/translations.py
37
+ microsys/trust.py
37
38
  microsys/urls.py
38
39
  microsys/utils.py
39
40
  microsys/widgets.py
@@ -54,6 +55,7 @@ microsys/migrations/0003_public_root_split.py
54
55
  microsys/migrations/0004_client_ip_and_trusted_devices.py
55
56
  microsys/migrations/0005_systemsettings_allow_user_font_override_and_more.py
56
57
  microsys/migrations/0006_systemsettings_navbar_config.py
58
+ microsys/migrations/0007_systemsettings_prevent_multiple_active_sessions.py
57
59
  microsys/migrations/__init__.py
58
60
  microsys/scaffold_templates/app/README.md.tmpl
59
61
  microsys/scaffold_templates/app/__init__.py.tmpl
@@ -0,0 +1 @@
1
+ 2.2.5
@@ -1616,6 +1616,10 @@ class SystemSettingsForm(forms.ModelForm):
1616
1616
  required=False,
1617
1617
  initial=False,
1618
1618
  )
1619
+ prevent_multiple_active_sessions = forms.BooleanField(
1620
+ required=False,
1621
+ initial=False,
1622
+ )
1619
1623
  client_ip_config = forms.CharField(
1620
1624
  widget=forms.HiddenInput(),
1621
1625
  required=False,
@@ -1675,6 +1679,7 @@ class SystemSettingsForm(forms.ModelForm):
1675
1679
  'allow_user_language_override',
1676
1680
  'default_table_density',
1677
1681
  'email_2fa',
1682
+ 'prevent_multiple_active_sessions',
1678
1683
  'client_ip_config',
1679
1684
  'public_root',
1680
1685
  'public_root_split_enabled',
@@ -1955,6 +1960,8 @@ class SystemSettingsForm(forms.ModelForm):
1955
1960
  'help_sys_email_2fa',
1956
1961
  'Allow users to enable two-factor authentication via email. Requires Microsys email delivery to be ready.',
1957
1962
  )
1963
+ self.fields['prevent_multiple_active_sessions'].label = s.get('form_sys_prevent_multiple_active_sessions')
1964
+ self.fields['prevent_multiple_active_sessions'].help_text = s.get('help_sys_prevent_multiple_active_sessions')
1958
1965
  self.fields['client_ip_mode'].label = s.get('form_sys_client_ip_mode')
1959
1966
  self.fields['client_ip_mode'].help_text = s.get('help_sys_client_ip_mode')
1960
1967
  self.fields['client_ip_mode'].choices = (
@@ -2326,6 +2333,10 @@ class SystemSettingsForm(forms.ModelForm):
2326
2333
  getattr(self.instance, 'email_2fa', False)
2327
2334
  or config.get('email_2fa', False)
2328
2335
  )
2336
+ self.initial['prevent_multiple_active_sessions'] = bool(
2337
+ getattr(self.instance, 'prevent_multiple_active_sessions', False)
2338
+ or config.get('prevent_multiple_active_sessions', False)
2339
+ )
2329
2340
  initial_client_ip_config = normalize_client_ip_config(
2330
2341
  (
2331
2342
  getattr(self.instance, 'client_ip_config', None)
@@ -2721,6 +2732,7 @@ class SystemSettingsForm(forms.ModelForm):
2721
2732
  Row(
2722
2733
  build_settings_toggle_field(self, 'public_root', css_class='col-lg-6'),
2723
2734
  build_settings_toggle_field(self, 'email_2fa', css_class='col-lg-6'),
2735
+ build_settings_toggle_field(self, 'prevent_multiple_active_sessions', css_class='col-lg-12'),
2724
2736
  css_class='g-3 mb-3',
2725
2737
  ),
2726
2738
  HTML(f"<h6 class='fw-bold my-3'>{s.get('client_ip_settings_title')}</h6>"),
@@ -3071,6 +3083,17 @@ class SystemSettingsForm(forms.ModelForm):
3071
3083
  raise ValidationError("Invalid table density choice.")
3072
3084
  return value
3073
3085
 
3086
+ def clean_prevent_multiple_active_sessions(self):
3087
+ if (
3088
+ self.is_bound
3089
+ and self.mode != 'setup'
3090
+ and self.single_step_mode
3091
+ and self.single_step_index != 2
3092
+ and 'prevent_multiple_active_sessions' not in self.data
3093
+ ):
3094
+ return bool(getattr(self.instance, 'prevent_multiple_active_sessions', False))
3095
+ return bool(self.cleaned_data.get('prevent_multiple_active_sessions', False))
3096
+
3074
3097
  def clean_sidebar_density(self):
3075
3098
  value = self.cleaned_data.get('sidebar_density') or DEFAULT_SIDEBAR_DENSITY
3076
3099
  if value not in SIDEBAR_DENSITY_VALUES:
@@ -3247,6 +3270,7 @@ class SystemSettingsForm(forms.ModelForm):
3247
3270
  'allow_user_language_override',
3248
3271
  'default_table_density',
3249
3272
  'email_2fa',
3273
+ 'prevent_multiple_active_sessions',
3250
3274
  'client_ip_config',
3251
3275
  'public_root',
3252
3276
  'public_root_split_enabled',
@@ -3495,6 +3519,7 @@ class SystemSettingsForm(forms.ModelForm):
3495
3519
  'allow_user_language_override': bool(self.cleaned_data.get('allow_user_language_override', True)),
3496
3520
  'default_table_density': self.cleaned_data.get('default_table_density', DEFAULT_TABLE_DENSITY),
3497
3521
  'email_2fa': bool(self.cleaned_data.get('email_2fa', False)),
3522
+ 'prevent_multiple_active_sessions': bool(self.cleaned_data.get('prevent_multiple_active_sessions', False)),
3498
3523
  'client_ip_config': self.cleaned_data.get('client_ip_config', default_client_ip_config()),
3499
3524
  'public_root': bool(self.cleaned_data.get('public_root', False)),
3500
3525
  'public_root_split_enabled': bool(self.cleaned_data.get('public_root_split_enabled', False)),
@@ -61,6 +61,7 @@ class Command(BaseCommand):
61
61
  self.stdout.write(f"home_url: {instance.home_url or ''}")
62
62
  self.stdout.write(f"default_language: {instance.default_language or ''}")
63
63
  self.stdout.write(f"default_theme: {instance.default_theme or ''}")
64
+ self.stdout.write(f"prevent_multiple_active_sessions: {bool(getattr(instance, 'prevent_multiple_active_sessions', False))}")
64
65
  self.stdout.write(f"sidebar_enabled: {bool((instance.sidebar_config or {}).get('enabled', True))}")
65
66
  self.stdout.write(f"navbar_enabled: {bool((instance.navbar_config or {}).get('enabled', False))}")
66
67
 
@@ -0,0 +1,18 @@
1
+ # Generated by Django on 2026-05-24
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('microsys', '0006_systemsettings_navbar_config'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='systemsettings',
15
+ name='prevent_multiple_active_sessions',
16
+ field=models.BooleanField(default=False, verbose_name='Prevent Multiple Active Sessions'),
17
+ ),
18
+ ]
@@ -148,6 +148,8 @@ class SingletonModel(models.Model):
148
148
  obj.navbar_config = config.get('navbar')
149
149
  if hasattr(obj, 'email_2fa') and 'email_2fa' in config:
150
150
  obj.email_2fa = bool(config.get('email_2fa'))
151
+ if hasattr(obj, 'prevent_multiple_active_sessions') and 'prevent_multiple_active_sessions' in config:
152
+ obj.prevent_multiple_active_sessions = bool(config.get('prevent_multiple_active_sessions'))
151
153
  if hasattr(obj, 'public_root') and 'public_root' in config:
152
154
  obj.public_root = bool(config.get('public_root'))
153
155
  if hasattr(obj, 'public_root_split_enabled') and 'public_root_split_enabled' in config:
@@ -213,6 +215,7 @@ class SystemSettings(SingletonModel):
213
215
  sidebar_config = models.JSONField(default=dict, blank=True, verbose_name="Sidebar Configuration")
214
216
  navbar_config = models.JSONField(default=default_navbar_config, blank=True, verbose_name="Nav Bar Configuration")
215
217
  titlebar_config = models.JSONField(default=default_titlebar_config, blank=True, verbose_name="Titlebar Configuration")
218
+ prevent_multiple_active_sessions = models.BooleanField(default=False, verbose_name="Prevent Multiple Active Sessions")
216
219
 
217
220
  class Meta:
218
221
  verbose_name = "System Settings"
@@ -3209,7 +3209,7 @@
3209
3209
  }
3210
3210
  });
3211
3211
 
3212
- ['allow_user_theme_override', 'allow_user_font_override', 'allow_user_language_override', 'email_2fa', 'public_root', 'public_root_split_enabled', 'public_registration_enabled', 'registration_throttle_enabled'].forEach((name) => {
3212
+ ['allow_user_theme_override', 'allow_user_font_override', 'allow_user_language_override', 'email_2fa', 'prevent_multiple_active_sessions', 'public_root', 'public_root_split_enabled', 'public_registration_enabled', 'registration_throttle_enabled'].forEach((name) => {
3213
3213
  if (Object.prototype.hasOwnProperty.call(settings, name)) {
3214
3214
  setCheckboxField(form, name, settings[name]);
3215
3215
  }
@@ -29,9 +29,17 @@ document.addEventListener('DOMContentLoaded', function() {
29
29
 
30
30
  document.body.addEventListener('submit', function(e) {
31
31
  const revokeForm = e.target.closest('.profile-session-revoke-form');
32
- if (!revokeForm) return;
33
- e.preventDefault();
34
- confirmSessionRevoke(revokeForm);
32
+ if (revokeForm) {
33
+ e.preventDefault();
34
+ confirmSessionRevoke(revokeForm);
35
+ return;
36
+ }
37
+
38
+ const trustForm = e.target.closest('.profile-session-trust-form');
39
+ if (trustForm) {
40
+ e.preventDefault();
41
+ confirmSessionTrust(trustForm);
42
+ }
35
43
  });
36
44
 
37
45
  if (otpSetupForm) {
@@ -631,6 +639,41 @@ function confirmSessionRevoke(form) {
631
639
  });
632
640
  }
633
641
 
642
+ function confirmSessionTrust(form) {
643
+ const confirmMsg = form.dataset.confirmMsg || '';
644
+ const csrfToken = form.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
645
+ const submitButton = form.querySelector('button[type="submit"]');
646
+
647
+ showConfirmation({
648
+ message: confirmMsg,
649
+ requirePassword: true,
650
+ onConfirm: function(currentPassword) {
651
+ const body = new URLSearchParams(new FormData(form));
652
+ body.set('current_password', currentPassword);
653
+ setButtonLoading(submitButton, true);
654
+
655
+ return fetch(form.action, {
656
+ method: 'POST',
657
+ headers: {
658
+ 'Content-Type': 'application/x-www-form-urlencoded',
659
+ 'X-CSRFToken': csrfToken,
660
+ 'X-Requested-With': 'XMLHttpRequest'
661
+ },
662
+ body: body.toString()
663
+ })
664
+ .then(parseJsonResponse)
665
+ .then(data => {
666
+ if (data.status !== 'success') {
667
+ throw new Error(data.message || 'Request failed.');
668
+ }
669
+ window.location.assign(data.redirect_url || window.location.href);
670
+ return true;
671
+ })
672
+ .finally(() => setButtonLoading(submitButton, false));
673
+ }
674
+ });
675
+ }
676
+
634
677
  function downloadBackupCodes() {
635
678
  const container = document.getElementById('backupCodesContainer');
636
679
  // Get text content effectively
@@ -305,10 +305,23 @@
305
305
  </div>
306
306
  <div class="profile-session-action">
307
307
  {% if session.is_current %}
308
- <button type="button" class="btn btn-sm btn-outline-secondary rounded-pill" disabled>
309
- {{ MS_TRANS.current }}
310
- </button>
311
- {% else %}
308
+ {% if session.is_trusted %}
309
+ <button type="button" class="btn btn-sm btn-outline-secondary rounded-pill" disabled>
310
+ {{ MS_TRANS.current }}
311
+ </button>
312
+ {% else %}
313
+ <form method="post"
314
+ action="{% url 'trust_current_device' %}"
315
+ class="profile-session-trust-form"
316
+ data-confirm-msg="{{ MS_TRANS.msg_confirm_trust_current_device }}">
317
+ {% csrf_token %}
318
+ <input type="hidden" name="current_password" value="">
319
+ <button type="submit" class="btn btn-sm btn-outline-primary rounded-pill">
320
+ <i class="bi bi-shield-plus me-1"></i>{{ MS_TRANS.trust_this_device }}
321
+ </button>
322
+ </form>
323
+ {% endif %}
324
+ {% elif session.can_revoke %}
312
325
  <form method="post"
313
326
  action="{% url 'revoke_profile_session' session.session_key %}"
314
327
  class="profile-session-revoke-form"
@@ -319,6 +332,13 @@
319
332
  <i class="bi bi-box-arrow-right me-1"></i>{{ MS_TRANS.sign_out }}
320
333
  </button>
321
334
  </form>
335
+ {% else %}
336
+ <button type="button"
337
+ class="btn btn-sm btn-outline-secondary rounded-pill"
338
+ data-ms-tooltip="{{ MS_TRANS.session_revoke_trusted_denied }}"
339
+ disabled>
340
+ <i class="bi bi-shield-lock me-1"></i>{{ MS_TRANS.protected }}
341
+ </button>
322
342
  {% endif %}
323
343
  </div>
324
344
  </div>
@@ -635,5 +655,5 @@
635
655
  {% endblock %}
636
656
 
637
657
  {% block scripts %}
638
- <script src="{% static 'microsys/users/js/profile_2fa.js' %}?v=20260522b" nonce="{{ request.csp_nonce }}"></script>
658
+ <script src="{% static 'microsys/users/js/profile_2fa.js' %}?v=20260524a" nonce="{{ request.csp_nonce }}"></script>
639
659
  {% endblock %}
@@ -253,6 +253,8 @@ MICROSYS_STRINGS = {
253
253
  'titlebar_surface_glass_desc': 'سطح زجاجي مع ضبابية خفيفة.',
254
254
  'form_sys_email_2fa': 'تفعيل التحقق الثنائي عبر البريد الإلكتروني',
255
255
  'help_sys_email_2fa': 'السماح للمستخدمين بتفعيل التحقق الثنائي عبر البريد الإلكتروني. يتطلب جاهزية إعدادات توصيل البريد في Microsys.',
256
+ 'form_sys_prevent_multiple_active_sessions': 'منع تعدد الجلسات النشطة',
257
+ 'help_sys_prevent_multiple_active_sessions': 'عند التفعيل، تصبح الجلسة الموثوقة الجديدة هي الجلسة النشطة الوحيدة لهذا المستخدم. تسجيل الدخول غير الموثوق يبقى مسموحاً لكنه لا يطرد الجلسات الموثوقة.',
256
258
  'form_sys_client_ip_mode': 'مصدر عنوان IP للعميل',
257
259
  'help_sys_client_ip_mode': 'اختر رأس الطلب الذي يجب أن يثق به Microsys عند تسجيل عناوين IP الخاصة بتسجيل الدخول والجلسات والأمان.',
258
260
  'client_ip_mode_x_forwarded_for': 'X-Forwarded-For',
@@ -662,8 +664,15 @@ MICROSYS_STRINGS = {
662
664
  'current_session': 'الجلسة الحالية',
663
665
  'trusted_device_badge': 'جهاز موثوق',
664
666
  'trusted_until': 'موثوق حتى',
667
+ 'trust_this_device': 'ثق بهذا الجهاز',
668
+ 'trusted_device_added_success': 'تم اعتبار هذا الجهاز موثوقاً.',
669
+ 'session_revoke_trusted_denied': 'لا يمكن لجلسة غير موثوقة إنهاء جلسة موثوقة.',
670
+ 'session_revoked_success': 'تم إنهاء الجلسة.',
671
+ 'session_revoke_denied': 'هذه الجلسة لا تنتمي إلى حسابك.',
665
672
  'session_expires': 'تنتهي في',
666
673
  'current': 'الحالي',
674
+ 'protected': 'محمي',
675
+ 'sign_out': 'تسجيل الخروج',
667
676
  'no_active_sessions': 'لم يتم العثور على جلسات نشطة.',
668
677
 
669
678
  # Messages
@@ -743,6 +752,7 @@ MICROSYS_STRINGS = {
743
752
  'msg_confirm_generate_backup': 'سيؤدي إنشاء رموز احتياطية جديدة إلى إلغاء صلاحية أي رموز سابقة. هل أنت متأكد من رغبتك في الاستمرار؟',
744
753
  'msg_confirm_disable_2fa': 'هل أنت متأكد من رغبتك في تعطيل المصادقة الثنائية لهذه الطريقة؟',
745
754
  'msg_confirm_sign_out_session': 'هل أنت متأكد من رغبتك في إنهاء جلسة هذا الجهاز؟',
755
+ 'msg_confirm_trust_current_device': 'هل تريد اعتبار هذا الجهاز موثوقاً لمدة 30 يوماً؟',
746
756
  'current_password_prompt': 'يرجى إدخال كلمة المرور الحالية للمتابعة.',
747
757
  'current_password_required': 'يرجى إدخال كلمة المرور الحالية.',
748
758
  'current_password_incorrect': 'كلمة المرور الحالية غير صحيحة.',
@@ -1172,6 +1182,8 @@ MICROSYS_STRINGS = {
1172
1182
  'titlebar_surface_glass_desc': 'Blurred glass-style titlebar surface.',
1173
1183
  'form_sys_email_2fa': 'Enable Email 2FA',
1174
1184
  'help_sys_email_2fa': 'Allow users to enable two-factor authentication via email. Requires Microsys email delivery to be ready.',
1185
+ 'form_sys_prevent_multiple_active_sessions': 'Prevent multiple active sessions',
1186
+ 'help_sys_prevent_multiple_active_sessions': 'When enabled, a newly trusted session becomes this user’s only active session. Untrusted logins remain allowed but cannot force out trusted sessions.',
1175
1187
  'form_sys_client_ip_mode': 'Client IP Source',
1176
1188
  'help_sys_client_ip_mode': 'Choose which request header Microsys should trust when recording login, session, and security IP addresses.',
1177
1189
  'client_ip_mode_x_forwarded_for': 'X-Forwarded-For',
@@ -1578,8 +1590,15 @@ MICROSYS_STRINGS = {
1578
1590
  'current_session': 'Current Session',
1579
1591
  'trusted_device_badge': 'Trusted Device',
1580
1592
  'trusted_until': 'Trusted Until',
1593
+ 'trust_this_device': 'Trust This Device',
1594
+ 'trusted_device_added_success': 'This device is now trusted.',
1595
+ 'session_revoke_trusted_denied': 'An untrusted session cannot sign out a trusted session.',
1596
+ 'session_revoked_success': 'Session signed out.',
1597
+ 'session_revoke_denied': 'That session does not belong to your account.',
1581
1598
  'session_expires': 'Expires',
1582
1599
  'current': 'Current',
1600
+ 'protected': 'Protected',
1601
+ 'sign_out': 'Sign Out',
1583
1602
  'no_active_sessions': 'No active sessions were found.',
1584
1603
 
1585
1604
  # Messages
@@ -1657,6 +1676,7 @@ MICROSYS_STRINGS = {
1657
1676
  'msg_confirm_generate_backup': 'Generating new backup codes will invalidate any existing codes. Are you sure you want to proceed?',
1658
1677
  'msg_confirm_disable_2fa': 'Are you sure you want to disable 2FA for this method?',
1659
1678
  'msg_confirm_sign_out_session': 'Are you sure you want to sign out this device session?',
1679
+ 'msg_confirm_trust_current_device': 'Trust this device for 30 days?',
1660
1680
  'current_password_prompt': 'Please enter your current password to continue.',
1661
1681
  'current_password_required': 'Please enter your current password.',
1662
1682
  'current_password_incorrect': 'Current password is incorrect.',
@@ -0,0 +1,227 @@
1
+ import hashlib
2
+ import secrets
3
+ from datetime import timedelta
4
+
5
+ from django.apps import apps
6
+ from django.contrib.sessions.models import Session
7
+ from django.core.signing import BadSignature
8
+ from django.db.models import Q
9
+ from django.utils import timezone
10
+
11
+ from .translations import get_strings
12
+ from .utils import get_client_ip, get_system_config
13
+
14
+
15
+ TRUSTED_DEVICE_COOKIE_NAME = 'microsys_trusted_device'
16
+ TRUSTED_DEVICE_COOKIE_SALT = 'microsys.trusted_device'
17
+ TRUSTED_DEVICE_MAX_AGE = 30 * 24 * 60 * 60
18
+
19
+
20
+ def trusted_device_model():
21
+ return apps.get_model('microsys', 'TrustedDevice')
22
+
23
+
24
+ def trusted_device_token_hash(raw_token):
25
+ return hashlib.sha256(str(raw_token or '').encode('utf-8')).hexdigest()
26
+
27
+
28
+ def device_label(user_agent):
29
+ s = get_strings()
30
+ user_agent = str(user_agent or '').strip()
31
+ lowered = user_agent.lower()
32
+ if not user_agent:
33
+ return s.get('device_unknown')
34
+ if 'edg/' in lowered or 'edge/' in lowered:
35
+ browser = 'Edge'
36
+ elif 'firefox/' in lowered:
37
+ browser = 'Firefox'
38
+ elif 'chrome/' in lowered or 'chromium/' in lowered:
39
+ browser = 'Chrome'
40
+ elif 'safari/' in lowered:
41
+ browser = 'Safari'
42
+ else:
43
+ browser = 'Browser'
44
+ if 'android' in lowered:
45
+ platform = 'Android'
46
+ elif 'iphone' in lowered or 'ipad' in lowered:
47
+ platform = 'iOS'
48
+ elif 'windows' in lowered:
49
+ platform = 'Windows'
50
+ elif 'mac os' in lowered or 'macintosh' in lowered:
51
+ platform = 'macOS'
52
+ elif 'linux' in lowered:
53
+ platform = 'Linux'
54
+ else:
55
+ platform = s.get('device_platform_generic')
56
+ return s.get('device_label_pattern').format(browser=browser, platform=platform)
57
+
58
+
59
+ def get_trusted_device_for_login(request, user):
60
+ if not request or not user:
61
+ return None
62
+ try:
63
+ raw_token = request.get_signed_cookie(
64
+ TRUSTED_DEVICE_COOKIE_NAME,
65
+ salt=TRUSTED_DEVICE_COOKIE_SALT,
66
+ )
67
+ except (KeyError, BadSignature):
68
+ return None
69
+ if not raw_token:
70
+ return None
71
+ return trusted_device_model().objects.filter(
72
+ user=user,
73
+ token_hash=trusted_device_token_hash(raw_token),
74
+ revoked_at__isnull=True,
75
+ trusted_until__gt=timezone.now(),
76
+ ).first()
77
+
78
+
79
+ def sync_session_device_metadata(request, trusted_device=None):
80
+ session = getattr(request, 'session', None)
81
+ if session is None:
82
+ return {}
83
+ if not getattr(session, 'session_key', None):
84
+ session.save()
85
+ now = timezone.now().isoformat()
86
+ existing = session.get('microsys_device')
87
+ if not isinstance(existing, dict):
88
+ existing = {}
89
+ metadata = {
90
+ 'user_agent': str(request.META.get('HTTP_USER_AGENT') or existing.get('user_agent') or '')[:500],
91
+ 'ip_address': str(get_client_ip(request) or existing.get('ip_address') or '').strip(),
92
+ 'first_seen': existing.get('first_seen') or now,
93
+ 'last_seen': now,
94
+ }
95
+ if trusted_device is not None:
96
+ metadata['trusted_device_id'] = trusted_device.pk
97
+ metadata['trusted_until'] = trusted_device.trusted_until.isoformat()
98
+ elif existing.get('trusted_device_id'):
99
+ metadata['trusted_device_id'] = existing.get('trusted_device_id')
100
+ metadata['trusted_until'] = existing.get('trusted_until')
101
+ session['microsys_device'] = metadata
102
+ session.modified = True
103
+ if trusted_device is not None:
104
+ trusted_device.session_key = getattr(session, 'session_key', '') or ''
105
+ trusted_device.device_label = device_label(metadata.get('user_agent'))
106
+ trusted_device.ip_address = metadata.get('ip_address') or None
107
+ trusted_device.user_agent = metadata.get('user_agent') or ''
108
+ trusted_device.last_used_at = timezone.now()
109
+ trusted_device.save(update_fields=['session_key', 'device_label', 'ip_address', 'user_agent', 'last_used_at'])
110
+ return metadata
111
+
112
+
113
+ def trusted_device_for_session(user, session_key, session_data=None):
114
+ if not user or not session_key:
115
+ return None
116
+ now = timezone.now()
117
+ metadata = {}
118
+ if isinstance(session_data, dict):
119
+ metadata = session_data.get('microsys_device') if isinstance(session_data.get('microsys_device'), dict) else {}
120
+ devices = trusted_device_model().objects.filter(
121
+ user=user,
122
+ revoked_at__isnull=True,
123
+ trusted_until__gt=now,
124
+ )
125
+ trusted_device_id = metadata.get('trusted_device_id')
126
+ if trusted_device_id is not None:
127
+ try:
128
+ by_id = devices.filter(pk=int(trusted_device_id)).first()
129
+ except (TypeError, ValueError):
130
+ by_id = None
131
+ if by_id is not None:
132
+ return by_id
133
+ return devices.filter(session_key=session_key).first()
134
+
135
+
136
+ def current_session_trusted_device(request):
137
+ user = getattr(request, 'user', None)
138
+ if not user or not getattr(user, 'is_authenticated', False):
139
+ return None
140
+ session = getattr(request, 'session', None)
141
+ if session is None:
142
+ return None
143
+ return trusted_device_for_session(user, getattr(session, 'session_key', None), dict(session.items()))
144
+
145
+
146
+ def revoke_linked_session_trust(user, session_keys, trusted_device_ids=None, exclude_trusted_device_id=None):
147
+ session_keys = [key for key in (session_keys or []) if key]
148
+ trusted_device_ids = [pk for pk in (trusted_device_ids or []) if pk]
149
+ devices = trusted_device_model().objects.filter(user=user, revoked_at__isnull=True)
150
+ if exclude_trusted_device_id:
151
+ devices = devices.exclude(pk=exclude_trusted_device_id)
152
+ criteria = Q()
153
+ if trusted_device_ids and session_keys:
154
+ criteria = Q(pk__in=trusted_device_ids) | Q(session_key__in=session_keys)
155
+ elif trusted_device_ids:
156
+ criteria = Q(pk__in=trusted_device_ids)
157
+ elif session_keys:
158
+ criteria = Q(session_key__in=session_keys)
159
+ else:
160
+ return 0
161
+ return devices.filter(criteria).update(revoked_at=timezone.now())
162
+
163
+
164
+ def revoke_other_user_sessions(user, keep_session_key=None, keep_trusted_device_id=None):
165
+ now = timezone.now()
166
+ target_keys = []
167
+ trusted_ids = []
168
+ user_id = str(user.pk)
169
+ for session in Session.objects.filter(expire_date__gt=now):
170
+ if keep_session_key and session.session_key == keep_session_key:
171
+ continue
172
+ try:
173
+ data = session.get_decoded()
174
+ except Exception:
175
+ continue
176
+ if str(data.get('_auth_user_id') or '') != user_id:
177
+ continue
178
+ target_keys.append(session.session_key)
179
+ metadata = data.get('microsys_device') if isinstance(data.get('microsys_device'), dict) else {}
180
+ trusted_device_id = metadata.get('trusted_device_id')
181
+ if trusted_device_id is not None:
182
+ try:
183
+ trusted_ids.append(int(trusted_device_id))
184
+ except (TypeError, ValueError):
185
+ pass
186
+ if target_keys:
187
+ Session.objects.filter(session_key__in=target_keys).delete()
188
+ revoke_linked_session_trust(
189
+ user,
190
+ target_keys,
191
+ trusted_device_ids=trusted_ids,
192
+ exclude_trusted_device_id=keep_trusted_device_id,
193
+ )
194
+ return len(target_keys)
195
+
196
+
197
+ def enforce_single_active_trusted_session(request, user, trusted_device):
198
+ if not trusted_device:
199
+ return 0
200
+ if not bool(get_system_config().get('prevent_multiple_active_sessions', False)):
201
+ return 0
202
+ return revoke_other_user_sessions(
203
+ user,
204
+ keep_session_key=getattr(getattr(request, 'session', None), 'session_key', None),
205
+ keep_trusted_device_id=trusted_device.pk,
206
+ )
207
+
208
+
209
+ def issue_trusted_device(request, response, user):
210
+ raw_token = secrets.token_urlsafe(32)
211
+ trusted_device = trusted_device_model().objects.create(
212
+ user=user,
213
+ token_hash=trusted_device_token_hash(raw_token),
214
+ trusted_until=timezone.now() + timedelta(days=30),
215
+ )
216
+ sync_session_device_metadata(request, trusted_device=trusted_device)
217
+ response.set_signed_cookie(
218
+ TRUSTED_DEVICE_COOKIE_NAME,
219
+ raw_token,
220
+ salt=TRUSTED_DEVICE_COOKIE_SALT,
221
+ max_age=TRUSTED_DEVICE_MAX_AGE,
222
+ httponly=True,
223
+ secure=request.is_secure(),
224
+ samesite='Lax',
225
+ )
226
+ enforce_single_active_trusted_session(request, user, trusted_device)
227
+ return trusted_device
@@ -15,6 +15,7 @@ urlpatterns = [
15
15
  path('accounts/register/verify/<str:token>/', views.register_verify_view, name='register_verify'),
16
16
  path('accounts/profile/', views.user_profile, name='user_profile'),
17
17
  path('accounts/profile/sessions/<str:session_key>/revoke/', views.revoke_profile_session, name='revoke_profile_session'),
18
+ path('accounts/profile/device/trust/', views.trust_current_device, name='trust_current_device'),
18
19
  path('accounts/profile/edit/<str:pk>/modal/', views.DynamicModalManagerView.as_view(
19
20
  model=views.User,
20
21
  form_class=views.UserProfileEditForm,
@@ -1722,6 +1722,7 @@ SYSTEM_SETTINGS_EXPORT_FIELDS = (
1722
1722
  'allow_user_language_override',
1723
1723
  'default_table_density',
1724
1724
  'email_2fa',
1725
+ 'prevent_multiple_active_sessions',
1725
1726
  'client_ip_config',
1726
1727
  'public_root',
1727
1728
  'public_root_split_enabled',
@@ -1862,6 +1863,7 @@ def normalize_system_settings_import_payload(payload):
1862
1863
  'allow_user_font_override',
1863
1864
  'allow_user_language_override',
1864
1865
  'email_2fa',
1866
+ 'prevent_multiple_active_sessions',
1865
1867
  'public_root',
1866
1868
  'public_root_split_enabled',
1867
1869
  'public_registration_enabled',
@@ -1982,6 +1984,7 @@ def get_system_config():
1982
1984
  'allow_user_language_override': True,
1983
1985
  'default_table_density': DEFAULT_TABLE_DENSITY,
1984
1986
  'email_2fa': False,
1987
+ 'prevent_multiple_active_sessions': False,
1985
1988
  'client_ip': default_client_ip_config(),
1986
1989
  'public_root': False,
1987
1990
  'public_root_split_enabled': False,
@@ -2105,6 +2108,14 @@ def get_system_config():
2105
2108
  and _should_apply_db_override(bool(sys_settings.email_2fa), default_config['email_2fa'])
2106
2109
  ):
2107
2110
  db_config['email_2fa'] = bool(sys_settings.email_2fa)
2111
+ if (
2112
+ hasattr(sys_settings, 'prevent_multiple_active_sessions')
2113
+ and _should_apply_db_override(
2114
+ bool(sys_settings.prevent_multiple_active_sessions),
2115
+ default_config['prevent_multiple_active_sessions'],
2116
+ )
2117
+ ):
2118
+ db_config['prevent_multiple_active_sessions'] = bool(sys_settings.prevent_multiple_active_sessions)
2108
2119
  client_ip_config = normalize_client_ip_config(getattr(sys_settings, 'client_ip_config', {}))
2109
2120
  if _should_apply_db_override(client_ip_config, default_config['client_ip']):
2110
2121
  db_config['client_ip'] = client_ip_config
@@ -62,7 +62,7 @@ from .activitylog import (
62
62
  )
63
63
 
64
64
  # Profile
65
- from .profile import revoke_profile_session, user_profile
65
+ from .profile import revoke_profile_session, trust_current_device, user_profile
66
66
 
67
67
  # Public registration
68
68
  from .registration import (