├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── django_auto_logout ├── __init__.py ├── context_processors.py ├── middleware.py └── utils.py ├── example ├── example │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── some_app_login_required │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── templates │ ├── layout.html │ ├── login_page.html │ └── login_required.html │ ├── tests.py │ └── views.py ├── requirements-dev.txt ├── requirements.txt ├── runtests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !.gitignore 3 | !.travis.yml 4 | __pycache__ 5 | *.egg-info 6 | *.sqlite3 7 | *.log 8 | venv 9 | htmlcov 10 | build 11 | dist 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | - "3.9" 5 | install: 6 | - pip install -r requirements-dev.txt 7 | script: 8 | - tox -e $(echo py$TRAVIS_PYTHON_VERSION | tr -d .) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Georgy Bazhukov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-auto-logout 2 | 3 | [![Build Status](https://app.travis-ci.com/bugov/django-auto-logout.svg?branch=master)](https://app.travis-ci.com/bugov/django-auto-logout) 4 | 5 | Auto logout a user after specific time in Django. 6 | 7 | Works with 8 | - Python🐍 ≥ 3.7, 9 | - Django🌐 ≥ 3.0. 10 | 11 | **Documentation** 12 | - [How to install](#installation) 13 | - User logout in case of: 14 | - [downtime](#idle-time) 15 | - [session duration limitation](#session-time) 16 | - [Auto-reload the browser page when the time runs out](#reload) 17 | - [Add a message to inform the user about logging out](#message) 18 | 19 | ## ✔️ Installation 20 | 21 | ```bash 22 | pip install django-auto-logout 23 | ``` 24 | 25 | Append to `settings.py` middlewares: 26 | 27 | ```python 28 | MIDDLEWARE = [ 29 | # append after default middlewares 30 | 'django_auto_logout.middleware.auto_logout', 31 | ] 32 | ``` 33 | 34 | --- 35 | 36 | **NOTE** 37 | 38 | Make sure that the following middlewares are used before doing this: 39 | 40 | - `django.contrib.sessions.middleware.SessionMiddleware` 41 | - `django.contrib.auth.middleware.AuthenticationMiddleware` 42 | - `django.contrib.messages.middleware.MessageMiddleware` 43 | 44 | --- 45 | 46 | ## 💤 Logout in case of idle 47 | 48 | Logout a user if there are no requests for a long time. 49 | 50 | Add to `settings.py`: 51 | 52 | ```python 53 | AUTO_LOGOUT = {'IDLE_TIME': 600} # logout after 10 minutes of downtime 54 | ``` 55 | 56 | or the same, but with `datetime.timedelta` (more semantically): 57 | 58 | ```python 59 | from datetime import timedelta 60 | 61 | AUTO_LOGOUT = {'IDLE_TIME': timedelta(minutes=10)} 62 | ``` 63 | 64 | The user will log out the next time the page is requested. 65 | See `REDIRECT_TO_LOGIN_IMMEDIATELY` to log out right after the idle-time has expired 66 | (and redirect to login page). 67 | 68 | ### 🔄 `REDIRECT_TO_LOGIN_IMMEDIATELY` after the idle-time has expired 69 | 70 | Use the `REDIRECT_TO_LOGIN_IMMEDIATELY` option 71 | if you want to redirect the user to the login page 72 | immediately after the idle-time expires: 73 | 74 | ```python 75 | from datetime import timedelta 76 | 77 | AUTO_LOGOUT = { 78 | 'IDLE_TIME': timedelta(minutes=10), 79 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 80 | } 81 | ``` 82 | 83 | This requires a client-side script, so you should 84 | modify your `context_processors` in `settings.py`: 85 | 86 | ```python 87 | TEMPLATES = [ 88 | { 89 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 90 | 'DIRS': [], 91 | 'APP_DIRS': True, 92 | 'OPTIONS': { 93 | 'context_processors': [ 94 | 'django.template.context_processors.debug', 95 | 'django.template.context_processors.request', 96 | 'django.contrib.auth.context_processors.auth', 97 | 'django.contrib.messages.context_processors.messages', 98 | # ↓↓↓ Add this ↓↓↓ 99 | 'django_auto_logout.context_processors.auto_logout_client', 100 | ], 101 | }, 102 | }, 103 | ] 104 | ``` 105 | 106 | And add this to your templates (will add a redirect script to your html): 107 | 108 | ``` 109 | {{ redirect_to_login_immediately }} 110 | ``` 111 | 112 | If you want to use this in your JavaScript code, following template variables may be useful: 113 | 114 | ``` 115 | var sessionEnd = {{ seconds_until_session_end }}; 116 | var idleEnd = {{ seconds_until_idle_end }}; 117 | ``` 118 | 119 | `REDIRECT_TO_LOGIN_IMMEDIATELY` works with `SESSION_TIME` too. 120 | 121 | ## ⌛ Limit session time 122 | 123 | Logout a user after 3600 seconds (hour) from the last login. 124 | 125 | Add to `settings.py`: 126 | 127 | ```python 128 | AUTO_LOGOUT = {'SESSION_TIME': 3600} 129 | ``` 130 | 131 | or the same, but with `datetime.timedelta` (more semantically): 132 | 133 | ```python 134 | from datetime import timedelta 135 | 136 | AUTO_LOGOUT = {'SESSION_TIME': timedelta(hours=1)} 137 | ``` 138 | 139 | --- 140 | 141 | **NOTE** 142 | 143 | See `REDIRECT_TO_LOGIN_IMMEDIATELY` option 144 | if you want to redirect user to the login page 145 | right after the idle-time has expired. 146 | 147 | --- 148 | 149 | ## ✉️ Show messages when logging out automatically 150 | 151 | Set the message that will be displayed after the user automatically logs out of the system: 152 | 153 | ```python 154 | AUTO_LOGOUT = { 155 | 'SESSION_TIME': 3600, 156 | 'MESSAGE': 'The session has expired. Please login again to continue.', 157 | } 158 | ``` 159 | 160 | It uses `django.contrib.messages`. Don't forget to display messages in templates: 161 | 162 | ```html 163 | {% for message in messages %} 164 |
165 | {{ message }} 166 |
167 | {% endfor %} 168 | ``` 169 | 170 | --- 171 | 172 | **NOTE** 173 | 174 | `messages` template variable provides by `django.contrib.messages.context_processors.messages` 175 | context processor. 176 | 177 | See `TEMPLATES` → `OPTIONS` → `context_processors` in your `settings.py` file. 178 | 179 | --- 180 | 181 | ## 🌈 Combine configurations 182 | 183 | You can combine previous configurations. For example, you may want to logout a user 184 | in case of downtime (5 minutes or more) and not allow working within one session 185 | for more than half an hour: 186 | 187 | ```python 188 | from datetime import timedelta 189 | 190 | AUTO_LOGOUT = { 191 | 'IDLE_TIME': timedelta(minutes=5), 192 | 'SESSION_TIME': timedelta(minutes=30), 193 | 'MESSAGE': 'The session has expired. Please login again to continue.', 194 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 195 | } 196 | ``` 197 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-auto-logout 2 | ================== 3 | 4 | .. image:: https://app.travis-ci.com/bugov/django-auto-logout.svg?branch=master 5 | :target: https://app.travis-ci.com/bugov/django-auto-logout 6 | 7 | Auto logout a user after specific time in Django. 8 | 9 | Works with Python >= 3.7, Django >= 3.0. 10 | 11 | Installation 12 | ------------ 13 | 14 | .. code:: bash 15 | 16 | pip install django-auto-logout 17 | 18 | 19 | Append to `settings` middlewares: 20 | 21 | .. code:: python 22 | 23 | MIDDLEWARE = [ 24 | ... 25 | 'django_auto_logout.middleware.auto_logout', 26 | ] 27 | 28 | .. note:: 29 | 30 | Make sure that the following middlewares are used before doing this: 31 | 32 | - `django.contrib.sessions.middleware.SessionMiddleware` 33 | - `django.contrib.auth.middleware.AuthenticationMiddleware` 34 | - `django.contrib.messages.middleware.MessageMiddleware` 35 | 36 | Logout in case of idle 37 | ---------------------- 38 | 39 | Logout a user if there are no requests for a long time. 40 | 41 | Add to `settings`: 42 | 43 | .. code:: python 44 | 45 | AUTO_LOGOUT = {'IDLE_TIME': 600} # logout after 10 minutes of downtime 46 | 47 | or the same, but with `datetime.timedelta` (more semantically): 48 | 49 | .. code:: python 50 | 51 | AUTO_LOGOUT = {'IDLE_TIME': timedelta(minutes=10)} 52 | 53 | The user will log out the next time the page is requested. 54 | See `REDIRECT_TO_LOGIN_IMMEDIATELY` to log out right after the idle-time has expired 55 | (and redirect to login page). 56 | 57 | REDIRECT_TO_LOGIN_IMMEDIATELY after the idle-time has expired 58 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 59 | 60 | Use the `REDIRECT_TO_LOGIN_IMMEDIATELY` option 61 | if you want to redirect the user to the login page 62 | immediately after the idle-time expires: 63 | 64 | .. code:: python 65 | 66 | from datetime import timedelta 67 | AUTO_LOGOUT = { 68 | 'IDLE_TIME': timedelta(minutes=10), 69 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 70 | } 71 | 72 | This requires a client-side script, so you should 73 | modify your `context_processors` in `settings.py`: 74 | 75 | .. code:: python 76 | 77 | TEMPLATES = [ 78 | { 79 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 80 | 'DIRS': [], 81 | 'APP_DIRS': True, 82 | 'OPTIONS': { 83 | 'context_processors': [ 84 | 'django.template.context_processors.debug', 85 | 'django.template.context_processors.request', 86 | 'django.contrib.auth.context_processors.auth', 87 | 'django.contrib.messages.context_processors.messages', 88 | # !!! Add this !!! 89 | 'django_auto_logout.context_processors.auto_logout_client', 90 | ], 91 | }, 92 | }, 93 | ] 94 | 95 | And add this to your templates (will add a redirect script to your html): 96 | 97 | .. code:: bash 98 | 99 | {{ redirect_to_login_immediately }} 100 | 101 | If you want to use this in your JavaScript code, following template variables may be useful: 102 | 103 | .. code:: javascript 104 | 105 | var sessionEnd = {{ seconds_until_session_end }}; 106 | var idleEnd = {{ seconds_until_idle_end }}; 107 | 108 | `REDIRECT_TO_LOGIN_IMMEDIATELY` works with `SESSION_TIME` too. 109 | 110 | Limit session time 111 | ------------------ 112 | 113 | Logout a user after 3600 seconds (hour) from the last login. 114 | 115 | Add to `settings`: 116 | 117 | .. code:: python 118 | 119 | AUTO_LOGOUT = {'SESSION_TIME': 3600} 120 | 121 | or the same, but with `datetime.timedelta` (more semantically): 122 | 123 | .. code:: python 124 | 125 | AUTO_LOGOUT = {'SESSION_TIME': timedelta(hours=1)} 126 | 127 | .. note:: 128 | 129 | See `REDIRECT_TO_LOGIN_IMMEDIATELY` option 130 | if you want to redirect user to the login page 131 | right after the idle-time has expired. 132 | 133 | 134 | Show messages when logging out automatically 135 | -------------------------------------------- 136 | 137 | Set the message that will be displayed after the user automatically logs out of the system: 138 | 139 | .. code:: python 140 | 141 | AUTO_LOGOUT = { 142 | 'SESSION_TIME': 3600, 143 | 'MESSAGE': 'The session has expired. Please login again to continue.', 144 | } 145 | 146 | It uses `django.contrib.messages`. Don't forget to display messages in templates: 147 | 148 | .. code:: html 149 | 150 | {% for message in messages %} 151 |
152 | {{ message }} 153 |
154 | {% endfor %} 155 | 156 | .. note:: 157 | 158 | `messages` template variable provides by `django.contrib.messages.context_processors.messages` 159 | context processor. 160 | 161 | See `TEMPLATES` - `OPTIONS` - `context_processors` in your `settings.py` file. 162 | 163 | Combine configurations 164 | ---------------------- 165 | 166 | You can combine previous configurations. For example, you may want to logout a user 167 | in case of downtime (5 minutes or more) and not allow working within one session 168 | for more than half an hour: 169 | 170 | 171 | .. code:: python 172 | 173 | from datetime import timedelta 174 | 175 | AUTO_LOGOUT = { 176 | 'IDLE_TIME': timedelta(minutes=5), 177 | 'SESSION_TIME': timedelta(minutes=30), 178 | 'MESSAGE': 'The session has expired. Please login again to continue.', 179 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 180 | } 181 | -------------------------------------------------------------------------------- /django_auto_logout/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5.1' 2 | -------------------------------------------------------------------------------- /django_auto_logout/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.safestring import mark_safe 3 | from .utils import now, seconds_until_session_end, seconds_until_idle_time_end 4 | 5 | LOGOUT_TIMEOUT_SCRIPT_PATTERN = """ 6 | 29 | """ 30 | 31 | 32 | def _trim(s: str) -> str: 33 | return ''.join([line.strip() for line in s.split('\n')]) 34 | 35 | 36 | def auto_logout_client(request): 37 | if request.user.is_anonymous: 38 | return {} 39 | 40 | options = getattr(settings, 'AUTO_LOGOUT') 41 | if not options: 42 | return {} 43 | 44 | ctx = {} 45 | current_time = now() 46 | 47 | if 'SESSION_TIME' in options: 48 | ctx['seconds_until_session_end'] = seconds_until_session_end(request, options['SESSION_TIME'], current_time) 49 | 50 | if 'IDLE_TIME' in options: 51 | ctx['seconds_until_idle_end'] = seconds_until_idle_time_end(request, options['IDLE_TIME'], current_time) 52 | 53 | if options.get('REDIRECT_TO_LOGIN_IMMEDIATELY'): 54 | at = None 55 | 56 | if 'SESSION_TIME' in options and 'IDLE_TIME' in options: 57 | at = ( 58 | f"at=Date.now()+Math.max(Math.min({ ctx['seconds_until_session_end'] }," 59 | f"{ ctx['seconds_until_idle_end'] }),0)*1000+999;" 60 | ) 61 | elif 'SESSION_TIME' in options: 62 | at = f"at=Date.now()+Math.max({ ctx['seconds_until_session_end'] },0)*1000+999;" 63 | elif 'IDLE_TIME' in options: 64 | at = f"at=Date.now()+Math.max({ ctx['seconds_until_idle_end'] },0)*1000+999;" 65 | 66 | if at: 67 | ctx['redirect_to_login_immediately'] = mark_safe(_trim(LOGOUT_TIMEOUT_SCRIPT_PATTERN % at)) 68 | 69 | return ctx 70 | -------------------------------------------------------------------------------- /django_auto_logout/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Callable 3 | from django.conf import settings 4 | from django.http import HttpRequest, HttpResponse 5 | from django.contrib.auth import get_user_model, logout 6 | from django.contrib.messages import info 7 | 8 | from .utils import now, seconds_until_idle_time_end, seconds_until_session_end 9 | 10 | UserModel = get_user_model() 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def _auto_logout(request: HttpRequest, options): 15 | should_logout = False 16 | current_time = now() 17 | 18 | if 'SESSION_TIME' in options: 19 | session_time = seconds_until_session_end(request, options['SESSION_TIME'], current_time) 20 | should_logout |= session_time < 0 21 | logger.debug('Check SESSION_TIME: %ss until session ends.', session_time) 22 | 23 | if 'IDLE_TIME' in options: 24 | idle_time = seconds_until_idle_time_end(request, options['IDLE_TIME'], current_time) 25 | should_logout |= idle_time < 0 26 | logger.debug('Check IDLE_TIME: %ss until idle ends.', idle_time) 27 | 28 | if should_logout and 'django_auto_logout_last_request' in request.session: 29 | del request.session['django_auto_logout_last_request'] 30 | else: 31 | request.session['django_auto_logout_last_request'] = current_time.isoformat() 32 | 33 | if should_logout: 34 | logger.debug('Logout user %s', request.user) 35 | logout(request) 36 | 37 | if 'MESSAGE' in options: 38 | info(request, options['MESSAGE']) 39 | 40 | 41 | def auto_logout(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable: 42 | def middleware(request: HttpRequest) -> HttpResponse: 43 | if not request.user.is_anonymous and hasattr(settings, 'AUTO_LOGOUT'): 44 | _auto_logout(request, settings.AUTO_LOGOUT) 45 | 46 | return get_response(request) 47 | return middleware 48 | -------------------------------------------------------------------------------- /django_auto_logout/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Union 3 | from django.http import HttpRequest 4 | from django.utils.timezone import now 5 | 6 | now = now 7 | 8 | 9 | def seconds_until_session_end( 10 | request: HttpRequest, 11 | session_time: Union[int, timedelta], 12 | current_time: datetime 13 | ) -> float: 14 | """ 15 | Get seconds until the end of the session. 16 | :param request: django.http.HttpRequest 17 | :param session_time: int - for seconds | timedelta 18 | :param current_time: datetime - use django_auto_logout.utils.now 19 | :return: float 20 | """ 21 | if isinstance(session_time, timedelta): 22 | ttl = session_time 23 | elif isinstance(session_time, int): 24 | ttl = timedelta(seconds=session_time) 25 | else: 26 | raise TypeError(f"AUTO_LOGOUT['SESSION_TIME'] should be `int` or `timedelta`, " 27 | f"not `{type(session_time).__name__}`.") 28 | 29 | return (request.user.last_login - current_time + ttl).total_seconds() 30 | 31 | 32 | def seconds_until_idle_time_end( 33 | request: HttpRequest, 34 | idle_time: Union[int, timedelta], 35 | current_time: datetime 36 | ) -> float: 37 | """ 38 | Get seconds until the end of downtime. 39 | :param request: django.http.HttpRequest 40 | :param idle_time: int - for seconds | timedelta 41 | :param current_time: datetime - use django_auto_logout.utils.now 42 | :return: float 43 | """ 44 | if isinstance(idle_time, timedelta): 45 | ttl = idle_time 46 | elif isinstance(idle_time, int): 47 | ttl = timedelta(seconds=idle_time) 48 | else: 49 | raise TypeError(f"AUTO_LOGOUT['IDLE_TIME'] should be `int` or `timedelta`, " 50 | f"not `{type(idle_time).__name__}`.") 51 | 52 | if 'django_auto_logout_last_request' in request.session: 53 | last_req = datetime.fromisoformat(request.session['django_auto_logout_last_request']) 54 | else: 55 | last_req = current_time 56 | 57 | return (last_req - current_time + ttl).total_seconds() 58 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugov/django-auto-logout/7ee7c965d01d0a0b86dd7082689428fed7a87655/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for example project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-1d*y)p@6kwiv_oh+6wh(k=z3+mb90+z3)3oyzkz67*8lb^(*-9' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'some_app_login_required', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | 53 | 'django_auto_logout.middleware.auto_logout', 54 | ] 55 | 56 | ROOT_URLCONF = 'example.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | 70 | 'django_auto_logout.context_processors.auto_logout_client', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'example.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | # Default primary key field type 129 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 130 | 131 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 132 | 133 | 134 | LOGGING = { 135 | 'version': 1, 136 | 'disable_existing_loggers': False, 137 | 'filters': { 138 | 'require_debug_false': { 139 | '()': 'django.utils.log.RequireDebugFalse' 140 | } 141 | }, 142 | 'formatters': { 143 | 'standard': { 144 | 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' 145 | }, 146 | }, 147 | 'handlers': { 148 | 'default': { 149 | 'level': 'DEBUG', 150 | 'formatter': 'standard', 151 | 'class': 'logging.StreamHandler', 152 | }, 153 | }, 154 | 'loggers': { 155 | '': { 156 | 'handlers': ['default'], 157 | 'level': 'DEBUG', 158 | 'propagate': False, 159 | }, 160 | }, 161 | } 162 | 163 | LOGIN_URL = '/login/' 164 | LOGIN_REDIRECT_URL = '/login-required/' 165 | 166 | # DJANGO AUTO LOGIN 167 | AUTO_LOGOUT = { 168 | 'IDLE_TIME': 10, # 10 seconds 169 | 'SESSION_TIME': 120, # 2 minutes 170 | 'MESSAGE': 'The session has expired. Please login again to continue.', 171 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 172 | } 173 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from some_app_login_required.views import UserLoginView, login_required_view 5 | 6 | urlpatterns = [ 7 | path('admin/', admin.site.urls), 8 | path('login/', UserLoginView.as_view()), 9 | path('login-required/', login_required_view), 10 | ] 11 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /example/some_app_login_required/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugov/django-auto-logout/7ee7c965d01d0a0b86dd7082689428fed7a87655/example/some_app_login_required/__init__.py -------------------------------------------------------------------------------- /example/some_app_login_required/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /example/some_app_login_required/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SomeAppLoginRequiredConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'some_app_login_required' 7 | -------------------------------------------------------------------------------- /example/some_app_login_required/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bugov/django-auto-logout/7ee7c965d01d0a0b86dd7082689428fed7a87655/example/some_app_login_required/migrations/__init__.py -------------------------------------------------------------------------------- /example/some_app_login_required/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /example/some_app_login_required/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | {% for message in messages %} 9 |
10 | {{ message }} 11 |
12 | {% endfor %} 13 | 14 |

15 | internal link 16 | external link 17 |

18 | 19 | {% block content %}{% endblock %} 20 | 21 | {{ redirect_to_login_immediately }} 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/some_app_login_required/templates/login_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}login page{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

login page

8 | 9 |
10 | {% csrf_token %} 11 | 12 | 13 | {{ form }} 14 | 15 | 16 | 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /example/some_app_login_required/templates/login_required.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}login required page{% endblock %} 4 | 5 | {% block content %} 6 |

7 | login required view 8 |

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/some_app_login_required/tests.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from datetime import timedelta 3 | from django.test import TestCase 4 | from django.conf import settings 5 | from django.contrib.auth import get_user_model 6 | 7 | UserModel = get_user_model() 8 | 9 | 10 | class TestAutoLogout(TestCase): 11 | def setUp(self): 12 | self.user = UserModel.objects.create_user('user', 'user@localhost', 'pass') 13 | self.superuser = UserModel.objects.create_superuser('superuser', 'superuser@localhost', 'pass') 14 | self.url = '/login-required/' 15 | 16 | def assertLoginRequiredIsOk(self): 17 | resp = self.client.get(self.url) 18 | self.assertContains(resp, 'login required view', msg_prefix='Fine with authorized') 19 | return resp 20 | 21 | def assertLoginRequiredRedirect(self): 22 | resp = self.client.get(self.url) 23 | self.assertEqual(resp.status_code, 302, msg='Redirect for anonymous') 24 | self.assertEqual(resp['location'], f'{settings.LOGIN_URL}?next={self.url}') 25 | return resp 26 | 27 | 28 | class TestAutoLogoutSessionTime(TestAutoLogout): 29 | def _logout_session_time(self): 30 | self.assertLoginRequiredRedirect() 31 | 32 | self.client.force_login(self.user) 33 | self.assertLoginRequiredIsOk() 34 | 35 | sleep(1) 36 | self.assertLoginRequiredRedirect() 37 | 38 | def test_logout_session_time(self): 39 | settings.AUTO_LOGOUT = {'SESSION_TIME': 1} 40 | settings.USE_TZ = False 41 | self._logout_session_time() 42 | 43 | def test_logout_session_time_using_tz_utc(self): 44 | settings.AUTO_LOGOUT = {'SESSION_TIME': 1} 45 | settings.USE_TZ = True 46 | self._logout_session_time() 47 | 48 | def test_logout_session_time_using_tz_non_utc(self): 49 | settings.AUTO_LOGOUT = {'SESSION_TIME': 1} 50 | settings.USE_TZ = True 51 | settings.TIME_ZONE = 'Asia/Yekaterinburg' 52 | self._logout_session_time() 53 | 54 | def test_logout_session_time_timedelta(self): 55 | settings.AUTO_LOGOUT = {'SESSION_TIME': timedelta(seconds=1)} 56 | settings.USE_TZ = False 57 | self._logout_session_time() 58 | 59 | def test_logout_session_time_using_tz_utc_timedelta(self): 60 | settings.AUTO_LOGOUT = {'SESSION_TIME': timedelta(seconds=1)} 61 | settings.USE_TZ = True 62 | self._logout_session_time() 63 | 64 | def test_logout_session_time_using_tz_non_utc_timedelta(self): 65 | settings.AUTO_LOGOUT = {'SESSION_TIME': timedelta(seconds=1)} 66 | settings.USE_TZ = True 67 | settings.TIME_ZONE = 'Asia/Yekaterinburg' 68 | self._logout_session_time() 69 | 70 | def test_session_time_wrong_type(self): 71 | settings.AUTO_LOGOUT = { 72 | 'IDLE_TIME': 1, 73 | 'SESSION_TIME': '2', 74 | } 75 | 76 | self.client.force_login(self.user) 77 | 78 | exc_message = "AUTO_LOGOUT['SESSION_TIME'] should be `int` or `timedelta`, not `str`." 79 | with self.assertRaisesMessage(TypeError, exc_message): 80 | self.client.get(self.url) 81 | 82 | 83 | class TestAutoLogoutIdleTime(TestAutoLogout): 84 | def _test_logout_idle_time_no_idle(self): 85 | self.client.force_login(self.user) 86 | self.assertLoginRequiredIsOk() 87 | 88 | for _ in range(10): 89 | sleep(0.5) 90 | self.assertLoginRequiredIsOk() 91 | 92 | def test_logout_idle_time_no_idle(self): 93 | settings.AUTO_LOGOUT = {'IDLE_TIME': 1} 94 | self._test_logout_idle_time_no_idle() 95 | 96 | def test_logout_idle_time_no_idle_timedelta(self): 97 | settings.AUTO_LOGOUT = {'IDLE_TIME': timedelta(seconds=1)} 98 | self._test_logout_idle_time_no_idle() 99 | 100 | def _test_logout_idle_time(self): 101 | self.client.force_login(self.user) 102 | self.assertLoginRequiredIsOk() 103 | 104 | sleep(1.5) 105 | self.assertLoginRequiredRedirect() 106 | 107 | def test_logout_idle_time(self): 108 | settings.AUTO_LOGOUT = {'IDLE_TIME': 1} 109 | self._test_logout_idle_time() 110 | 111 | def test_logout_idle_time_timedelta(self): 112 | settings.AUTO_LOGOUT = {'IDLE_TIME': timedelta(seconds=1)} 113 | self._test_logout_idle_time() 114 | 115 | def test_idle_time_wrong_type(self): 116 | settings.AUTO_LOGOUT = { 117 | 'IDLE_TIME': '1', 118 | 'SESSION_TIME': 2, 119 | } 120 | 121 | self.client.force_login(self.user) 122 | 123 | exc_message = "AUTO_LOGOUT['IDLE_TIME'] should be `int` or `timedelta`, not `str`." 124 | with self.assertRaisesMessage(TypeError, exc_message): 125 | self.client.get(self.url) 126 | 127 | 128 | class TestAutoLogoutCombineConfigs(TestAutoLogout): 129 | def test_combine_idle_and_session_time(self): 130 | settings.AUTO_LOGOUT = { 131 | 'IDLE_TIME': 1, 132 | 'SESSION_TIME': 2, 133 | } 134 | 135 | self.client.force_login(self.user) 136 | self.assertLoginRequiredIsOk() 137 | 138 | sleep(0.5) 139 | self.assertLoginRequiredIsOk() 140 | sleep(0.5) 141 | self.assertLoginRequiredIsOk() 142 | sleep(0.5) 143 | self.assertLoginRequiredIsOk() 144 | sleep(0.5) 145 | self.assertLoginRequiredRedirect() 146 | 147 | def test_combine_idle_and_session_time_but_session_less_than_idle(self): 148 | settings.AUTO_LOGOUT = { 149 | 'IDLE_TIME': 2, 150 | 'SESSION_TIME': 1, 151 | } 152 | 153 | self.client.force_login(self.user) 154 | self.assertLoginRequiredIsOk() 155 | sleep(0.5) 156 | self.assertLoginRequiredIsOk() 157 | sleep(0.5) 158 | self.assertLoginRequiredRedirect() 159 | 160 | self.client.force_login(self.user) 161 | self.assertLoginRequiredIsOk() 162 | sleep(0.5) 163 | self.assertLoginRequiredIsOk() 164 | sleep(0.5) 165 | self.assertLoginRequiredRedirect() 166 | 167 | self.client.force_login(self.user) 168 | self.assertLoginRequiredIsOk() 169 | sleep(1) 170 | self.assertLoginRequiredRedirect() 171 | 172 | 173 | class TestAutoLogoutMessage(TestAutoLogout): 174 | def test_message_on_auto_logout(self): 175 | settings.AUTO_LOGOUT = { 176 | 'SESSION_TIME': 1, 177 | 'MESSAGE': 'The session has expired. Please login again to continue.', 178 | } 179 | self.client.force_login(self.user) 180 | self.assertLoginRequiredIsOk() 181 | sleep(1) 182 | resp = self.assertLoginRequiredRedirect() 183 | 184 | # display message after redirect 185 | resp = self.client.get(resp['location']) 186 | self.assertContains(resp, 'login page', msg_prefix=resp.content.decode()) 187 | self.assertContains(resp, 'class="message info"', msg_prefix=resp.content.decode()) 188 | self.assertContains(resp, settings.AUTO_LOGOUT['MESSAGE']) 189 | 190 | # message displays only once 191 | resp = self.assertLoginRequiredRedirect() 192 | resp = self.client.get(resp['location']) 193 | self.assertContains(resp, 'login page', msg_prefix=resp.content.decode()) 194 | self.assertNotContains(resp, 'class="message info"', msg_prefix=resp.content.decode()) 195 | 196 | def test_no_messages_if_no_messages(self): 197 | settings.AUTO_LOGOUT = { 198 | 'SESSION_TIME': 1, 199 | 'MESSAGE': None, 200 | } 201 | self.client.force_login(self.user) 202 | self.assertLoginRequiredIsOk() 203 | sleep(1) 204 | resp = self.assertLoginRequiredRedirect() 205 | resp = self.client.get(resp['location']) 206 | self.assertContains(resp, 'login page', msg_prefix=resp.content.decode()) 207 | self.assertNotContains(resp, 'class="message info"', msg_prefix=resp.content.decode()) 208 | 209 | 210 | class TestAutoLogoutRedirectToLoginPage(TestAutoLogout): 211 | def test_script_anon(self): 212 | settings.AUTO_LOGOUT = { 213 | 'IDLE_TIME': 10, # 10 seconds 214 | 'SESSION_TIME': 120, # 2 minutes 215 | 'REDIRECT_TO_LOGIN_IMMEDIATELY': True, 216 | } 217 | resp = self.client.get(settings.LOGIN_URL) 218 | self.assertNotContains(resp, '