Skip to content

Commit

Permalink
Améliore la page qui liste les membres utilisant la même IP (#6545)
Browse files Browse the repository at this point in the history
* Affiche la ville de l'IP sur la page des comptes utilisant la même IP
* Utilise un tableau pour présenter les membres sur la même IP
En affichant la date d'inscription et la date de dernière connexion.
  • Loading branch information
philippemilink authored Oct 23, 2023
1 parent c29c53f commit 0a733a7
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 50 deletions.
50 changes: 38 additions & 12 deletions templates/member/admin/memberip.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,29 @@

{% block content %}
<p>
{% captureas city %}{% if ip_location %}&nbsp;({{ ip_location }}){% endif %}{% endcaptureas %}
{% blocktrans %}
Liste des membres dont la dernière IP connue est <code>{{ ip }}</code>
Liste des membres dont la dernière IP connue est <code>{{ ip }}</code>{{ city }}
{% endblocktrans %}
</p>

<div class="members">
<ul>
<table>
<thead>
<th>{% trans "Membre" %}</th>
<th>{% trans "Inscription" %}</th>
<th>{% trans "Dernière connexion" %}</th>
</thead>
<tbody>
{% for member in members %}
<li>{% include "misc/member_item.part.html" with member=member info=member.last_visit|format_date:True avatar=True %}</li>
{% captureas last_visit %}{% if member.last_visit %}{{ member.last_visit|format_date:True }}{% else %}<i>{% trans "Jamais" %}</i>{% endif %}{% endcaptureas %}
<tr>
<td>{% include "misc/member_item.part.html" with member=member avatar=True %}</td>
<td>{{ member.user.date_joined|format_date:True }}</td>
<td>{{ last_visit }}</td>
</tr>
{% endfor %}
</ul>
</div>
</tbody>
</table>

{# Checks if it's an IPV6 to show the members from the same IPV6 network #}
{% if ":" in ip %}
Expand All @@ -46,16 +57,31 @@
{% endblocktrans %}
</p>

<div class="members">
<ul>
<table>
<thead>
<th>{% trans "Membre" %}</th>
<th>{% trans "Inscription" %}</th>
<th>{% trans "Dernière connexion" %}</th>
</thead>
<tbody>
{% for member in network_members %}
<li>{% include "misc/member_item.part.html" with member=member info=member.last_visit|format_date:True avatar=True %}</li>
{% captureas last_visit %}{% if member.last_visit %}{{ member.last_visit|format_date:True }}{% else %}<i>{% trans "Jamais" %}</i>{% endif %}{% endcaptureas %}
<tr>
<td>{% include "misc/member_item.part.html" with member=member avatar=True %}</td>
<td>{{ member.user.date_joined|format_date:True }}</td>
<td>{{ last_visit }}</td>
</tr>
{% endfor %}
</ul>
</div>
</tbody>
</table>

<p>
En IPv6, les adresses sont attribuées par bloc d'IP. Un bot de spam peut donc facilement changer d'adresse IP au sein de ce bloc. Sont affichés ici tous les membres dont l'IPv6 fait partie du même bloc que l'IP demandée.
{% blocktrans %}
En IPv6, les adresses sont attribuées par bloc d'IP. Un bot de spam
peut donc facilement changer d'adresse IP au sein de ce bloc. Sont
affichés ici tous les membres dont l'IPv6 fait partie du même bloc que
l'IP demandée.
{% endblocktrans %}
</p>
{% endif %}
{% endblock %}
36 changes: 10 additions & 26 deletions zds/member/models.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import logging
from datetime import datetime
from geoip2.errors import AddressNotFoundError
from hashlib import md5
import logging

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from django.urls import reverse
from django.db import models
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from zds.forum.models import Post, Topic
from zds.notification.models import TopicAnswerSubscription
from zds.forum.models import Forum, Post, Topic
from zds.member import NEW_PROVIDER_USES
from zds.member.managers import ProfileManager
from zds.member.utils import get_geo_location_from_ip
from zds.notification.models import TopicAnswerSubscription
from zds.tutorialv2.models.database import PublishableContent
from zds.utils import old_slugify
from zds.utils.models import Alert, Licence, Hat

from zds.forum.models import Forum
import homoglyphs as hg


Expand Down Expand Up @@ -91,30 +89,16 @@ def get_absolute_url(self):

def get_city(self):
"""
Uses geo-localization to get physical localization of a profile through 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.
Uses geo-localization to get physical localization of a profile through
its last IP address.
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.
"""
if self._cached_city is not None and self._cached_city[0] == self.last_ip_address:
return self._cached_city[1]

try:
geo = GeoIP2().city(self.last_ip_address)
except AddressNotFoundError:
geo_location = ""
except GeoIP2Exception as e:
geo_location = ""
logging.getLogger(__name__).warning(
f"GeoIP2 failed with the following message: '{e}'. "
"The Geolite2 database might not be installed or configured correctly. "
"Check the documentation for guidance on how to install it properly."
)
else:
city = geo["city"]
country = geo["country_name"]
geo_location = ", ".join(i for i in [city, country] if i)
geo_location = get_geo_location_from_ip(self.last_ip_address)

self._cached_city = (self.last_ip_address, geo_location)

Expand Down
35 changes: 31 additions & 4 deletions zds/member/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from geoip2.errors import AddressNotFoundError
import logging

from django.conf import settings
from django.contrib.auth.models import User
from social_django.middleware import SocialAuthExceptionMiddleware
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from django.urls import reverse
import logging
from django.utils.translation import gettext_lazy as _
from social_django.middleware import SocialAuthExceptionMiddleware

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -49,3 +52,27 @@ def get_anonymous_account() -> User:
Used for example as a replacement for unregistered users.
"""
return User.objects.get(username=settings.ZDS_APP["member"]["anonymous_account"])


def get_geo_location_from_ip(ip: str) -> str:
"""
Uses geo-localization to get physical localization of an IP address.
This works relatively well with IPv4 addresses (~city level), but is very
imprecise with IPv6 or exotic internet providers.
:return: The city and the country name of this IP.
"""
try:
geo = GeoIP2().city(ip)
except AddressNotFoundError:
return ""
except GeoIP2Exception as e:
logger.warning(
f"GeoIP2 failed with the following message: '{e}'. "
"The Geolite2 database might not be installed or configured correctly. "
"Check the documentation for guidance on how to install it properly."
)
return ""
else:
city = geo["city"]
country = geo["country_name"]
return ", ".join(i for i in [city, country] if i)
20 changes: 12 additions & 8 deletions zds/member/views/moderation.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import ipaddress
import logging

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponseBadRequest
from django.urls import reverse
from django.http import Http404
from django.http import Http404, HttpResponseBadRequest
from django.shortcuts import redirect, render, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST

Expand All @@ -23,7 +23,7 @@
from zds.member.decorator import can_write_and_read_now
from zds.member.forms import MiniProfileForm
from zds.member.models import Profile, KarmaNote
import logging
from zds.member.utils import get_geo_location_from_ip


@login_required
Expand All @@ -32,17 +32,21 @@ def member_from_ip(request, ip_address):
"""List users connected from a particular IP, and an IPV6 subnetwork."""

members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit")
members_and_ip = {"members": members, "ip": ip_address}
context_data = {
"members": members,
"ip": ip_address,
"ip_location": get_geo_location_from_ip(ip_address),
}

if ":" in ip_address: # Check if it's an IPV6
network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block
# Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network
network_ip = str(network_ip)[:-1]
network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit")
members_and_ip["network_members"] = network_members
members_and_ip["network_ip"] = network_ip
context_data["network_members"] = network_members
context_data["network_ip"] = network_ip

return render(request, "member/admin/memberip.html", members_and_ip)
return render(request, "member/admin/memberip.html", context_data)


@login_required
Expand Down

0 comments on commit 0a733a7

Please sign in to comment.