-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ajout d'une page de gestion des sessions
- Loading branch information
Showing
12 changed files
with
261 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
{% extends "member/settings/base.html" %} | ||
{% load i18n %} | ||
{% load date %} | ||
|
||
|
||
{% block title %} | ||
{% trans "Gestion des sessions" %} | ||
{% endblock %} | ||
|
||
|
||
|
||
{% block breadcrumb %} | ||
<li> | ||
{% trans "Gestion des sessions" %} | ||
</li> | ||
{% endblock %} | ||
|
||
|
||
|
||
{% block headline %} | ||
{% trans "Gestion des sessions" %} | ||
{% endblock %} | ||
|
||
|
||
|
||
{% block content %} | ||
{% include "misc/paginator.html" with position="top" %} | ||
|
||
{% if sessions %} | ||
<div class="table-wrapper"> | ||
<table class="fullwidth"> | ||
<thead> | ||
<th>{% trans "Session" %}</th> | ||
<th>{% trans "Appareil" %}</th> | ||
<th>{% trans "Adresse IP" %}</th> | ||
<th>{% trans "Géolocalisation" %}</th> | ||
<th>{% trans "Dernière utilisation" %}</th> | ||
<th>{% trans "Actions" %}</th> | ||
</thead> | ||
<tbody> | ||
{% for session in sessions %} | ||
<tr> | ||
{% if session.is_active %} | ||
<td><strong>{% trans "Session actuelle" %}</strong></td> | ||
{% else %} | ||
<td>{% trans "Autre session" %}</td> | ||
{% endif %} | ||
<td>{{ session.user_agent }}</td> | ||
<td>{{ session.ip_address }}</td> | ||
<td>{{ session.geolocation }}</td> | ||
<td>{{ session.last_visit|date_from_timestamp|format_date }}</td> | ||
<td> | ||
<form method="post" action="{% url 'delete-session' %}"> | ||
{% csrf_token %} | ||
<input type="hidden" name="session_key" value="{{ session.session_key }}"> | ||
<button type="submit" class="btn btn-grey ico-after red cross" {% if session.is_active %}disabled{% endif %}> | ||
{% trans "Déconnecter" %} | ||
</button> | ||
</form> | ||
</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
</div> | ||
{% else %} | ||
<em>{% trans "Aucune session ne correspond à votre compte." %}</em> | ||
{% endif %} | ||
|
||
{% include "misc/paginator.html" with position="bottom" %} | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from django.urls import reverse | ||
from django.test import TestCase | ||
|
||
from zds.member.tests.factories import ProfileFactory | ||
|
||
|
||
class SessionManagementTests(TestCase): | ||
def test_anonymous_cannot_access(self): | ||
self.client.logout() | ||
|
||
response = self.client.get(reverse("list-sessions")) | ||
self.assertRedirects(response, reverse("member-login") + "?next=" + reverse("list-sessions")) | ||
|
||
response = self.client.post(reverse("delete-session")) | ||
self.assertRedirects(response, reverse("member-login") + "?next=" + reverse("delete-session")) | ||
|
||
def test_user_can_access(self): | ||
profile = ProfileFactory() | ||
self.client.force_login(profile.user) | ||
|
||
response = self.client.get(reverse("list-sessions")) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
session_key = self.client.session.session_key | ||
response = self.client.post(reverse("delete-session"), {"session_key": session_key}) | ||
self.assertRedirects(response, reverse("list-sessions")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from django.contrib.auth.mixins import LoginRequiredMixin | ||
from django.shortcuts import redirect | ||
from django.urls import reverse | ||
from django.utils.translation import gettext_lazy as _ | ||
from django.views.generic import View | ||
|
||
from zds.member.utils import get_geo_location_from_ip, get_info_from_user_agent | ||
from zds.utils.custom_cached_db_backend import CustomSession, SessionStore | ||
from zds.utils.paginator import ZdSPagingListView | ||
|
||
|
||
class ListSessions(LoginRequiredMixin, ZdSPagingListView): | ||
"""List the user's sessions with useful information (user agent, IP address, geolocation and last visit).""" | ||
|
||
model = CustomSession | ||
context_object_name = "sessions" | ||
template_name = "member/settings/sessions.html" | ||
paginate_by = 10 | ||
|
||
def get_context_data(self, **kwargs): | ||
self.object_list = [] | ||
for session in CustomSession.objects.filter(account_id=self.request.user.pk).iterator(): | ||
data = session.get_decoded() | ||
session_context = { | ||
"session_key": session.session_key, | ||
"user_agent": get_info_from_user_agent(data.get("user_agent", "")), | ||
"ip_address": data.get("ip_address", ""), | ||
"geolocation": get_geo_location_from_ip(data.get("ip_address", "")) or _("Inconnue"), | ||
"last_visit": data.get("last_visit", 0), | ||
"is_active": session.session_key == self.request.session.session_key, | ||
} | ||
|
||
if session_context["is_active"]: | ||
self.object_list.insert(0, session_context) | ||
else: | ||
self.object_list.append(session_context) | ||
|
||
return super().get_context_data(**kwargs) | ||
|
||
|
||
class DeleteSession(LoginRequiredMixin, View): | ||
"""Delete a user's session.""" | ||
|
||
http_method_names = ["post"] | ||
|
||
def post(self, request, *args, **kwargs): | ||
session_key = request.POST.get("session_key", None) | ||
if session_key and session_key != self.request.session.session_key: | ||
session = SessionStore(session_key=session_key) | ||
if session.get("_auth_user_id", "") == str(self.request.user.pk): | ||
session.flush() | ||
|
||
return redirect(reverse("list-sessions")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from datetime import datetime | ||
|
||
from zds.member.views import get_client_ip | ||
|
||
|
||
class ManageSessionsMiddleware: | ||
"""This middleware adds the current IP address, user agent and timestamp to user sessions. | ||
This gives them the information they need to manage their sessions, and possibly delete some of them.""" | ||
|
||
def __init__(self, get_response): | ||
self.get_response = get_response | ||
|
||
def __call__(self, request): | ||
return self.process_response(request, self.get_response(request)) | ||
|
||
def process_response(self, request, response): | ||
try: | ||
user = request.user | ||
except AttributeError: | ||
user = None | ||
|
||
if user is not None and user.is_authenticated: | ||
session = request.session | ||
session["ip_address"] = get_client_ip(request) | ||
session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") | ||
session["last_visit"] = datetime.now().timestamp() | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from django.contrib.sessions.backends.cached_db import SessionStore as DBStore | ||
from django.contrib.sessions.base_session import AbstractBaseSession | ||
from django.db import models | ||
|
||
|
||
class CustomSession(AbstractBaseSession): | ||
"""Custom session model to link each session to its user. | ||
This is necessary to list a user's sessions without having to browse all sessions. | ||
Based on https://docs.djangoproject.com/en/4.2/topics/http/sessions/#example""" | ||
|
||
account_id = models.IntegerField(null=True, db_index=True) | ||
|
||
@classmethod | ||
def get_session_store_class(cls): | ||
return SessionStore | ||
|
||
|
||
class SessionStore(DBStore): | ||
"""Custom session store for the custom session model.""" | ||
|
||
cache_key_prefix = "zds.utils.custom_cached_db_backend" | ||
|
||
@classmethod | ||
def get_model_class(cls): | ||
return CustomSession | ||
|
||
def create_model_instance(self, data): | ||
obj = super().create_model_instance(data) | ||
try: | ||
account_id = int(data.get("_auth_user_id")) | ||
except (ValueError, TypeError): | ||
account_id = None | ||
obj.account_id = account_id | ||
return obj |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Generated by Django 4.2.6 on 2024-03-09 23:50 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("utils", "0025_move_helpwriting"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="CustomSession", | ||
fields=[ | ||
( | ||
"session_key", | ||
models.CharField(max_length=40, primary_key=True, serialize=False, verbose_name="session key"), | ||
), | ||
("session_data", models.TextField(verbose_name="session data")), | ||
("expire_date", models.DateTimeField(db_index=True, verbose_name="expire date")), | ||
("account_id", models.IntegerField(db_index=True, null=True)), | ||
], | ||
options={ | ||
"verbose_name": "session", | ||
"verbose_name_plural": "sessions", | ||
"abstract": False, | ||
}, | ||
), | ||
] |