mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 14:11:08 -05:00
first commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from allauth.usersessions.internal.flows import sessions
|
||||
|
||||
|
||||
__all__ = ["sessions"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
129
venv/lib/python3.11/site-packages/allauth/usersessions/models.py
Normal file
129
venv/lib/python3.11/site-packages/allauth/usersessions/models.py
Normal file
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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-REMOVED]},
|
||||
)
|
||||
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-REMOVED]},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert len(UserSession.objects.purge_and_list(user)) == 1
|
||||
|
||||
new_[PASSWORD-REMOVED]()
|
||||
resp = client.post(
|
||||
reverse("account_change_password"),
|
||||
{
|
||||
"old[PASSWORD-REMOVED],
|
||||
"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