Skip to content

WIP: LOGOUT_ON_TABS_CLOSED #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
__pycache__
*.egg-info
*.sqlite3
*.log
venv
htmlcov
build
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,62 @@ See `TEMPLATES` → `OPTIONS` → `context_processors` in your `settings.py` fil

---

## ✨ Logout a user when all his tabs are closed (experimental)

If all tabs are closed or if the browser is closed, actually...

Add to `AUTO_LOGOUT` settings:

```python
AUTO_LOGOUT['LOGOUT_ON_TABS_CLOSED'] = True
```

Also for this option you should add a context processor:

```python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',

# ↓↓↓ Add this ↓↓↓
'django_auto_logout.context_processors.auto_logout_client',
],
},
},
]
```

Add `logout_on_tabs_closed` variable to your template layout:

```
{{ logout_on_tabs_closed }}
```

It works for almost all browsers on 🖥️:

- IE ≥ 8
- Edge ≥ 12
- Firefox ≥ 3.5
- Chrome ≥ 4
- Safari ≥ 4
- Opera ≥ 11.5

And 📱 browsers:

- iOS Safari ≥ 3.2
- Android Browser ≥ 94
- Android Chrome ≥ 94
- Android Firefox ≥ 92
- Opera Mobile ≥ 12

## 🌈 Combine configurations

You can combine previous configurations. For example, you may want to logout a user
Expand All @@ -115,6 +171,7 @@ from datetime import timedelta
AUTO_LOGOUT = {
'IDLE_TIME': timedelta(minutes=5),
'SESSION_TIME': timedelta(minutes=30),
'LOGOUT_ON_TABS_CLOSED': True,
'MESSAGE': 'The session has expired. Please login again to continue.',
}
```
59 changes: 59 additions & 0 deletions django_auto_logout/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.conf import settings
from django.utils.safestring import mark_safe

LOGOUT_URL = settings.AUTO_LOGOUT.get('LOGOUT_URL', '/djal-send-logout/')


def trim(s: str) -> str:
return ''.join([line.strip() for line in s.split('\n')])


class LogoutOnTabClosed:
template = trim(f'''
<script>
(function() {{
var w = window,
s = w.localStorage;

function out() {{
var x = new XMLHttpRequest();
x.open("POST", '{ LOGOUT_URL }', true);
x.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
x.send("");
}}

w.addEventListener('load', function() {{
s['djalTabCounter']=Number(s['djalTabCounter']||0)+1;
}});

var unload = w.onbeforeunload;
w.onbeforeunload = function () {{
var c = Number(s['djalTabCounter']||0);
if (c > 1) s['djalTabCounter']=c-1;
else {{
s['djalTabCounter']=0;
out();
}}
unload();
}};
}})();
</script>
''')

def __init__(self, request):
self._request = request

def __str__(self):
return mark_safe(self.template)


def auto_logout_client(request):
if settings.AUTO_LOGOUT.get('LOGOUT_ON_TABS_CLOSED'):
html = LogoutOnTabClosed(request)
else:
html = ''

return {
'logout_on_tabs_closed': html,
'logout_url': LOGOUT_URL,
}
18 changes: 15 additions & 3 deletions django_auto_logout/middleware.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
from datetime import datetime, timedelta
import logging
from typing import Callable
from typing import Callable, Optional
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.contrib.auth import get_user_model, logout
from django.contrib.messages import info
from pytz import timezone

LOGOUT_URL = settings.AUTO_LOGOUT.get('LOGOUT_URL', '/djal-send-logout/')
UserModel = get_user_model()
logger = logging.getLogger(__name__)


def _auto_logout(request: HttpRequest, options):
def _auto_logout(request: HttpRequest, options) -> Optional[HttpResponse]:
user = request.user
should_logout = False
replace_response: Optional[HttpResponse] = None

if settings.USE_TZ:
now = datetime.now(tz=timezone(settings.TIME_ZONE))
else:
now = datetime.now()

if settings.AUTO_LOGOUT.get('LOGOUT_ON_TABS_CLOSED'):
if request.path == LOGOUT_URL and request.method.lower() == 'post':
should_logout |= True
replace_response = HttpResponse()
logger.info('Client %r requested for logout', user)

if options.get('SESSION_TIME') is not None:
if isinstance(options['SESSION_TIME'], timedelta):
ttl = options['SESSION_TIME']
Expand Down Expand Up @@ -64,11 +72,15 @@ def _auto_logout(request: HttpRequest, options):
if options.get('MESSAGE') is not None:
info(request, options['MESSAGE'])

return replace_response


def auto_logout(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable:
def middleware(request: HttpRequest) -> HttpResponse:
if not request.user.is_anonymous and hasattr(settings, 'AUTO_LOGOUT'):
_auto_logout(request, settings.AUTO_LOGOUT)
replace_response = _auto_logout(request, settings.AUTO_LOGOUT)
if replace_response:
return replace_response

return get_response(request)
return middleware
8 changes: 6 additions & 2 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',

'django_auto_logout.context_processors.auto_logout_client',
],
},
},
Expand Down Expand Up @@ -159,11 +161,13 @@
}

LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/login-required/'


# DJANGO AUTO LOGIN
AUTO_LOGOUT = {
'IDLE_TIME': 10, # 10 seconds
'SESSION_TIME': 120, # 2 minutes
'IDLE_TIME': 300, # 5 minutes
'SESSION_TIME': 1800, # 30 minutes
'MESSAGE': 'The session has expired. Please login again to continue.',
'LOGOUT_ON_TABS_CLOSED': True,
}
4 changes: 2 additions & 2 deletions example/example/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.contrib import admin
from django.urls import path

from some_app_login_required.views import login_page, login_required_view
from some_app_login_required.views import UserLoginView, login_required_view

urlpatterns = [
path('admin/', admin.site.urls),
path('login/', login_page),
path('login/', UserLoginView.as_view()),
path('login-required/', login_required_view),
]
9 changes: 8 additions & 1 deletion example/some_app_login_required/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% for message in messages %}
Expand All @@ -11,6 +11,13 @@
</div>
{% endfor %}

<p>
<a href="/login-required/">internal link</a>
<a href="https://github.com/bugov">external link</a>
</p>

{% block content %}{% endblock %}

{{ logout_on_tabs_closed }}
</body>
</html>
19 changes: 16 additions & 3 deletions example/some_app_login_required/templates/login_page.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
{% extends 'layout.html' %}

{% block title %}login page{% endblock %}

{% block content %}
<p>
login page
</p>
<div class="box">
<h4 class="form-header">login page</h4>

<form method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<table>
{{ form }}
<tr>
<td colspan="2"><button type="submit" class="btn btn-primary btn-block">Log in</button></td>
</tr>
</table>
</form>
</div>
{% endblock %}
2 changes: 2 additions & 0 deletions example/some_app_login_required/templates/login_required.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% extends 'layout.html' %}

{% block title %}login required page{% endblock %}

{% block content %}
<p>
login required view
Expand Down
Loading