mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 09:31:07 -05:00
okay fine
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
from allauth.core.internal.adapter import BaseAdapter
|
||||
from allauth.usersessions import app_settings
|
||||
from allauth.utils import import_attribute
|
||||
|
||||
|
||||
class DefaultUserSessionsAdapter(BaseAdapter):
|
||||
"""The adapter class allows you to override various functionality of the
|
||||
``allauth.usersessions`` app. To do so, point
|
||||
``settings.USERSESSIONS_ADAPTER`` to your own class that derives from
|
||||
``DefaultUserSessionsAdapter`` and override the behavior by altering the
|
||||
implementation of the methods according to your own needs.
|
||||
"""
|
||||
|
||||
def end_sessions(self, sessions):
|
||||
for session in sessions:
|
||||
session.end()
|
||||
|
||||
|
||||
def get_adapter():
|
||||
return import_attribute(app_settings.ADAPTER)()
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from allauth.usersessions.models import UserSession
|
||||
|
||||
|
||||
@admin.register(UserSession)
|
||||
class UserSessionAdmin(admin.ModelAdmin):
|
||||
raw_id_fields = ("user",)
|
||||
list_display = ("user", "created_at", "last_seen_at", "ip", "user_agent")
|
||||
@@ -0,0 +1,30 @@
|
||||
class AppSettings:
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
|
||||
def _setting(self, name, dflt):
|
||||
from allauth.utils import get_setting
|
||||
|
||||
return get_setting(self.prefix + name, dflt)
|
||||
|
||||
@property
|
||||
def ADAPTER(self):
|
||||
return self._setting(
|
||||
"ADAPTER", "allauth.usersessions.adapter.DefaultUserSessionsAdapter"
|
||||
)
|
||||
|
||||
@property
|
||||
def TRACK_ACTIVITY(self):
|
||||
"""Whether or not sessions are to be actively tracked. When tracking is
|
||||
enabled, the last seen IP address and last seen timestamp will be kept
|
||||
track of.
|
||||
"""
|
||||
return self._setting("TRACK_ACTIVITY", False)
|
||||
|
||||
|
||||
_app_settings = AppSettings("USERSESSIONS_")
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
# See https://peps.python.org/pep-0562/
|
||||
return getattr(_app_settings, name)
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth import app_settings
|
||||
|
||||
|
||||
class UserSessionsConfig(AppConfig):
|
||||
name = "allauth.usersessions"
|
||||
verbose_name = _("User Sessions")
|
||||
default_auto_field = (
|
||||
app_settings.DEFAULT_AUTO_FIELD or "django.db.models.BigAutoField"
|
||||
)
|
||||
|
||||
def ready(self):
|
||||
from allauth.account.signals import (
|
||||
password_changed,
|
||||
password_set,
|
||||
user_logged_in,
|
||||
)
|
||||
from allauth.usersessions import signals
|
||||
|
||||
user_logged_in.connect(receiver=signals.on_user_logged_in)
|
||||
for sig in [password_set, password_changed]:
|
||||
sig.connect(receiver=signals.on_password_changed)
|
||||
@@ -0,0 +1,12 @@
|
||||
from django import forms
|
||||
|
||||
from allauth.usersessions.internal import flows
|
||||
|
||||
|
||||
class ManageUserSessionsForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self, request):
|
||||
flows.sessions.end_other_sessions(request, request.user)
|
||||
@@ -0,0 +1,4 @@
|
||||
from allauth.usersessions.internal.flows import sessions
|
||||
|
||||
|
||||
__all__ = ["sessions"]
|
||||
@@ -0,0 +1,19 @@
|
||||
from allauth.account.internal import flows
|
||||
from allauth.usersessions.adapter import get_adapter
|
||||
from allauth.usersessions.models import UserSession
|
||||
|
||||
|
||||
def end_other_sessions(request, user):
|
||||
sessions_to_end = []
|
||||
for session in UserSession.objects.filter(user=user):
|
||||
if session.is_current():
|
||||
continue
|
||||
sessions_to_end.append(session)
|
||||
end_sessions(request, sessions_to_end)
|
||||
|
||||
|
||||
def end_sessions(request, sessions):
|
||||
has_current = any([session.is_current() for session in sessions])
|
||||
get_adapter().end_sessions(sessions)
|
||||
if has_current:
|
||||
flows.logout.logout(request)
|
||||
@@ -0,0 +1,19 @@
|
||||
from allauth.usersessions import app_settings
|
||||
from allauth.usersessions.models import UserSession
|
||||
|
||||
|
||||
class UserSessionsMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if (
|
||||
app_settings.TRACK_ACTIVITY
|
||||
and hasattr(request, "session")
|
||||
and request.session.session_key
|
||||
and hasattr(request, "user")
|
||||
and request.user.is_authenticated
|
||||
):
|
||||
UserSession.objects.create_from_request(request)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 4.2.6 on 2023-12-05 11:44
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserSession",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("ip", models.GenericIPAddressField()),
|
||||
(
|
||||
"last_seen_at",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"session_key",
|
||||
models.CharField(
|
||||
editable=False,
|
||||
max_length=40,
|
||||
unique=True,
|
||||
verbose_name="session key",
|
||||
),
|
||||
),
|
||||
("user_agent", models.CharField(max_length=200)),
|
||||
("data", models.JSONField(default=dict)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models, transaction
|
||||
from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth import app_settings as allauth_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.core import context
|
||||
|
||||
|
||||
if not allauth_settings.USERSESSIONS_ENABLED:
|
||||
raise ImproperlyConfigured(
|
||||
"allauth.usersessions not installed, yet its models are imported."
|
||||
)
|
||||
|
||||
|
||||
class UserSessionManager(models.Manager):
|
||||
def purge_and_list(self, user):
|
||||
ret = []
|
||||
sessions = UserSession.objects.filter(user=user)
|
||||
for session in sessions.iterator():
|
||||
if not session.purge():
|
||||
ret.append(session)
|
||||
return ret
|
||||
|
||||
def create_from_request(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
raise ValueError()
|
||||
if not request.session.session_key:
|
||||
request.session.save()
|
||||
ua = request.META.get("HTTP_USER_AGENT", "")[
|
||||
0 : UserSession._meta.get_field("user_agent").max_length
|
||||
]
|
||||
|
||||
defaults = dict(
|
||||
user=request.user,
|
||||
ip=get_adapter().get_client_ip(request),
|
||||
user_agent=ua,
|
||||
)
|
||||
|
||||
from_session = None
|
||||
with transaction.atomic():
|
||||
from allauth.usersessions.signals import session_client_changed
|
||||
|
||||
session, created = UserSession.objects.get_or_create(
|
||||
session_key=request.session.session_key, defaults=defaults
|
||||
)
|
||||
|
||||
if not created:
|
||||
from_session = UserSession(
|
||||
session_key=session.session_key,
|
||||
user=session.user,
|
||||
ip=session.ip,
|
||||
user_agent=session.user_agent,
|
||||
data=session.data,
|
||||
created_at=session.created_at,
|
||||
last_seen_at=session.last_seen_at,
|
||||
)
|
||||
# Update session
|
||||
session.user = defaults["user"]
|
||||
session.ip = defaults["ip"]
|
||||
session.user_agent = defaults["user_agent"]
|
||||
session.last_seen_at = timezone.now()
|
||||
|
||||
session.save()
|
||||
|
||||
if from_session and (
|
||||
from_session.ip != session.ip
|
||||
or from_session.user_agent != session.user_agent
|
||||
):
|
||||
session_client_changed.send(
|
||||
sender=UserSession,
|
||||
request=request,
|
||||
from_session=from_session,
|
||||
to_session=session,
|
||||
)
|
||||
|
||||
|
||||
class UserSession(models.Model):
|
||||
objects = UserSessionManager()
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
ip = models.GenericIPAddressField()
|
||||
last_seen_at = models.DateTimeField(default=timezone.now)
|
||||
session_key = models.CharField(
|
||||
_("session key"), max_length=40, unique=True, editable=False
|
||||
)
|
||||
user_agent = models.CharField(max_length=200)
|
||||
data = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ip} ({self.user_agent})"
|
||||
|
||||
def _session_store(self, *args):
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
return engine.SessionStore(*args)
|
||||
|
||||
def exists(self):
|
||||
return self._session_store().exists(self.session_key)
|
||||
|
||||
def purge(self):
|
||||
purge = not self.exists()
|
||||
if not purge:
|
||||
# Even if the session still exists, it might be the case that the
|
||||
# user session hash is out of sync. So, let's see if
|
||||
# `django.contrib.auth` can find a user...
|
||||
request = HttpRequest()
|
||||
request.session = self._session_store(self.session_key)
|
||||
user = get_user(request)
|
||||
purge = not user or user.is_anonymous
|
||||
if purge:
|
||||
self.delete()
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_current(self):
|
||||
return self.session_key == context.request.session.session_key
|
||||
|
||||
def end(self):
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
store = engine.SessionStore()
|
||||
store.delete(self.session_key)
|
||||
self.delete()
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
from allauth.account import app_settings
|
||||
|
||||
from .models import UserSession
|
||||
|
||||
|
||||
# Provides the arguments "request", "from_session", "to_session"
|
||||
session_client_changed = Signal()
|
||||
|
||||
|
||||
def on_user_logged_in(sender, **kwargs):
|
||||
request = kwargs["request"]
|
||||
UserSession.objects.create_from_request(request)
|
||||
|
||||
|
||||
def on_password_changed(sender, **kwargs):
|
||||
if not app_settings.LOGOUT_ON_PASSWORD_CHANGE:
|
||||
request = kwargs["request"]
|
||||
UserSession.objects.create_from_request(request)
|
||||
@@ -0,0 +1,93 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
|
||||
from allauth.usersessions.middleware import UserSessionsMiddleware
|
||||
from allauth.usersessions.models import UserSession
|
||||
from allauth.usersessions.signals import session_client_changed
|
||||
|
||||
|
||||
def test_mw_without_request_user(rf, db, settings):
|
||||
settings.USERSESSIONS_TRACK_ACTIVITY = True
|
||||
mw = UserSessionsMiddleware(lambda request: None)
|
||||
request = rf.get("/")
|
||||
mw(request)
|
||||
assert UserSession.objects.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("track_activity", [False, True])
|
||||
def test_mw_with_request_user(rf, db, settings, user, track_activity):
|
||||
settings.USERSESSIONS_TRACK_ACTIVITY = track_activity
|
||||
mw = UserSessionsMiddleware(lambda request: None)
|
||||
request = rf.get("/")
|
||||
request.user = user
|
||||
request.session = Mock()
|
||||
request.session.session_key = "sess-123"
|
||||
mw(request)
|
||||
assert (
|
||||
UserSession.objects.filter(session_key="sess-123", user=user).exists()
|
||||
is track_activity
|
||||
)
|
||||
|
||||
|
||||
def test_mw_with_anonymous_request_user(rf, db, settings):
|
||||
settings.USERSESSIONS_TRACK_ACTIVITY = True
|
||||
mw = UserSessionsMiddleware(lambda request: None)
|
||||
request = rf.get("/")
|
||||
request.user = AnonymousUser()
|
||||
request.session = Mock()
|
||||
request.session.session_key = "sess-123"
|
||||
mw(request)
|
||||
assert not UserSession.objects.exists()
|
||||
|
||||
|
||||
@override_settings(USERSESSIONS_TRACK_ACTIVITY=True)
|
||||
def test_mw_change_ip_and_useragent(rf, db, user):
|
||||
mw = UserSessionsMiddleware(lambda request: None)
|
||||
|
||||
# First request
|
||||
request1 = rf.get("/")
|
||||
request1.user = user
|
||||
request1.session = Mock()
|
||||
request1.session.session_key = "sess-123"
|
||||
request1.META["HTTP_USER_AGENT"] = "Old User Agent"
|
||||
request1.META["REMOTE_ADDR"] = "1.1.1.1"
|
||||
mw(request1)
|
||||
|
||||
# Second request with changed IP and User Agent
|
||||
request2 = rf.get("/")
|
||||
request2.user = user
|
||||
request2.session = Mock()
|
||||
request2.session.session_key = "sess-123"
|
||||
request2.META["HTTP_USER_AGENT"] = "New User Agent"
|
||||
request2.META["REMOTE_ADDR"] = "2.2.2.2"
|
||||
|
||||
# Set up signal receiver
|
||||
signal_received = []
|
||||
|
||||
def signal_handler(sender, request, from_session, to_session, **kwargs):
|
||||
signal_received.append((from_session, to_session))
|
||||
|
||||
session_client_changed.connect(signal_handler)
|
||||
|
||||
# Process second request
|
||||
mw(request2)
|
||||
|
||||
# Check if UserSession was updated
|
||||
user_session = UserSession.objects.get(session_key="sess-123", user=user)
|
||||
assert user_session.ip == "2.2.2.2"
|
||||
assert user_session.user_agent == "New User Agent"
|
||||
|
||||
# Check if signal was triggered
|
||||
assert len(signal_received) == 1
|
||||
from_session, to_session = signal_received[0]
|
||||
assert from_session.ip == "1.1.1.1"
|
||||
assert from_session.user_agent == "Old User Agent"
|
||||
assert to_session.ip == "2.2.2.2"
|
||||
assert to_session.user_agent == "New User Agent"
|
||||
|
||||
# Clean up signal connection
|
||||
session_client_changed.disconnect(signal_handler)
|
||||
@@ -0,0 +1,60 @@
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
|
||||
from allauth.usersessions.models import UserSession
|
||||
|
||||
|
||||
def test_overall_flow(user, user_password):
|
||||
firefox = Client(HTTP_USER_AGENT="Mozilla Firefox")
|
||||
nyxt = Client(HTTP_USER_AGENT="Nyxt")
|
||||
for client in [firefox, nyxt]:
|
||||
resp = client.post(
|
||||
reverse("account_login"),
|
||||
{"login": user.username, "password": user_password},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert UserSession.objects.filter(user=user).count() == 2
|
||||
sessions = list(UserSession.objects.filter(user=user).order_by("pk"))
|
||||
assert sessions[0].user_agent == "Mozilla Firefox"
|
||||
assert sessions[1].user_agent == "Nyxt"
|
||||
for client in [firefox, nyxt]:
|
||||
resp = client.get(reverse("usersessions_list"))
|
||||
assert resp.status_code == 200
|
||||
resp = firefox.post(reverse("usersessions_list"))
|
||||
assert resp.status_code == 302
|
||||
assert UserSession.objects.filter(user=user).count() == 1
|
||||
assert UserSession.objects.filter(user=user, pk=sessions[0].pk).exists()
|
||||
assert not UserSession.objects.filter(user=user, pk=sessions[1].pk).exists()
|
||||
resp = nyxt.get(reverse("usersessions_list"))
|
||||
assert resp.status_code == 302
|
||||
assert resp["location"] == reverse("account_login") + "?next=" + reverse(
|
||||
"usersessions_list"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("logout_on_passwd_change", [True, False])
|
||||
def test_change_password_updates_user_session(
|
||||
settings, logout_on_passwd_change, client, user, user_password, password_factory
|
||||
):
|
||||
settings.ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = logout_on_passwd_change
|
||||
resp = client.post(
|
||||
reverse("account_login"),
|
||||
{"login": user.username, "password": user_password},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert len(UserSession.objects.purge_and_list(user)) == 1
|
||||
|
||||
new_password = password_factory()
|
||||
resp = client.post(
|
||||
reverse("account_change_password"),
|
||||
{
|
||||
"oldpassword": user_password,
|
||||
"password1": new_password,
|
||||
"password2": new_password,
|
||||
},
|
||||
)
|
||||
assert len(UserSession.objects.purge_and_list(user)) == (
|
||||
0 if logout_on_passwd_change else 1
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from allauth.usersessions import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.list_usersessions, name="usersessions_list"),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from allauth.account import app_settings as account_settings
|
||||
from allauth.account.adapter import get_adapter as get_account_adapter
|
||||
from allauth.usersessions import app_settings
|
||||
from allauth.usersessions.forms import ManageUserSessionsForm
|
||||
from allauth.usersessions.models import UserSession
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ListUserSessionsView(FormView):
|
||||
template_name = (
|
||||
"usersessions/usersession_list." + account_settings.TEMPLATE_EXTENSION
|
||||
)
|
||||
form_class = ManageUserSessionsForm
|
||||
success_url = reverse_lazy("usersessions_list")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ret = super().get_context_data(**kwargs)
|
||||
sessions = sorted(
|
||||
UserSession.objects.purge_and_list(self.request.user),
|
||||
key=lambda s: s.created_at,
|
||||
)
|
||||
ret["sessions"] = sessions
|
||||
ret["session_count"] = len(sessions)
|
||||
ret["show_last_seen_at"] = app_settings.TRACK_ACTIVITY
|
||||
return ret
|
||||
|
||||
def get_form_kwargs(self):
|
||||
ret = super().get_form_kwargs()
|
||||
ret["request"] = self.request
|
||||
return ret
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save(self.request)
|
||||
get_account_adapter().add_message(
|
||||
self.request,
|
||||
messages.INFO,
|
||||
"usersessions/messages/sessions_logged_out.txt",
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
list_usersessions = ListUserSessionsView.as_view()
|
||||
Reference in New Issue
Block a user