Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Améliore la page qui liste les membres utilisant la même IP #6545

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }}
Arnaud-D marked this conversation as resolved.
Show resolved Hide resolved
{% 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