Skip to content

Commit

Permalink
Ajout d'une page de gestion des sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
Situphen committed Mar 9, 2024
1 parent 3269c56 commit 84d9423
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 1 deletion.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ crispy-forms-bootstrap2==2024.1
elasticsearch-dsl==5.4.0
elasticsearch==5.5.3
social-auth-app-django==5.4.0
ua-parser==0.16.1

# Explicit dependencies (references in code)
beautifulsoup4==4.12.2
Expand All @@ -19,6 +20,7 @@ lxml==5.1.0
Pillow==10.2.0
pymemcache==4.0.0
requests==2.31.0
user-agents==2.2.0

# Api dependencies
django-cors-headers==4.3.1
Expand Down
1 change: 1 addition & 0 deletions templates/member/settings/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ <h3>{% trans "Paramètres" %}</h3>
{% if user.profile.is_dev %}
<li><a href="{% url "update-github" %}">{% trans "Token GitHub" %}</a></li>
{% endif %}
<li><a href="{% url "list-sessions" %}">{% trans "Gestion des sessions" %}</a></li>
<li><a href="{% url "member-warning-unregister" %}">{% trans "Désinscription" %}</a></li>
</ul>
</div>
Expand Down
71 changes: 71 additions & 0 deletions templates/member/settings/sessions.html
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.geolocalization }}</td>
<td>{{ session.last_visit|humane_time }}</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 %}
3 changes: 2 additions & 1 deletion zds/member/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def get_absolute_url(self):
def get_city(self):
"""
Uses geo-localization to get physical localization of a profile through
its last IP address.
its last IP address. This works relatively well with IPv4 addresses (~city level),
but is very imprecise with IPv6 or exotic internet providers.
The result is cached on an instance level because this method is called
a lot in the profile.
:return: The city and the country name of this profile.
Expand Down
26 changes: 26 additions & 0 deletions zds/member/tests/views/tests_session.py
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"))
3 changes: 3 additions & 0 deletions zds/member/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from zds.member.views.password_recovery import forgot_password, new_password
from zds.member.views.admin import settings_promote
from zds.member.views.reports import CreateProfileReportView, SolveProfileReportView
from zds.member.views.sessions import ListSessions, DeleteSession


urlpatterns = [
Expand All @@ -62,6 +63,8 @@
path("parametres/profil/maj_avatar/", UpdateAvatarMember.as_view(), name="update-avatar-member"),
path("parametres/compte/", UpdatePasswordMember.as_view(), name="update-password-member"),
path("parametres/user/", UpdateUsernameEmailMember.as_view(), name="update-username-email-member"),
path("parametres/sessions/", ListSessions.as_view(), name="list-sessions"),
path("parametres/sessions/supprimer/", DeleteSession.as_view(), name="delete-session"),
# moderation
path("profil/signaler/<int:profile_pk>/", CreateProfileReportView.as_view(), name="report-profile"),
path("profil/resoudre/<int:alert_pk>/", SolveProfileReportView.as_view(), name="solve-profile-alert"),
Expand Down
1 change: 1 addition & 0 deletions zds/member/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from geoip2.errors import AddressNotFoundError

import logging

from django.conf import settings
Expand Down
58 changes: 58 additions & 0 deletions zds/member/views/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from importlib import import_module
from user_agents import parse

from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sessions.models import Session
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_geolocalization
from zds.utils.paginator import ZdSPagingListView

SessionStore = import_module(settings.SESSION_ENGINE).SessionStore


class ListSessions(LoginRequiredMixin, ZdSPagingListView):
"""List the user's sessions."""

model = Session
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 Session.objects.iterator():
data = session.get_decoded()
if data.get("_auth_user_id") == str(self.request.user.pk):
session_context = {
"session_key": session.session_key,
"user_agent": str(parse(data.get("user_agent", ""))),
"ip_address": data.get("ip_address", ""),
"geolocalization": get_geolocalization(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."""

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"))
24 changes: 24 additions & 0 deletions zds/middlewares/managesessionsmiddleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from datetime import datetime

from zds.member.views import get_client_ip


class ManageSessionsMiddleware:
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
1 change: 1 addition & 0 deletions zds/settings/abstract_base/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"zds.utils.ThreadLocals",
"zds.middlewares.setlastvisitmiddleware.SetLastVisitMiddleware",
"zds.middlewares.matomomiddleware.MatomoMiddleware",
"zds.middlewares.managesessionsmiddleware.ManageSessionsMiddleware",
"zds.member.utils.ZDSCustomizeSocialAuthExceptionMiddleware",
)

Expand Down

0 comments on commit 84d9423

Please sign in to comment.