import datetime from django.contrib import admin, messages from django.core.mail import send_mail from django.db.models import QuerySet from django.http import HttpRequest from django.utils import timezone from .enrollment import issue_join_token, join_command from .models import BanEvent, EnrollmentRequest, JoinToken, NodeRegistry from .views import NODE_OFFLINE_THRESHOLD_SECONDS @admin.register(BanEvent) class BanEventAdmin(admin.ModelAdmin): list_display = ('received_at', 'node', 'jail_name', 'action', 'ip_address', 'bantime') list_filter = ('jail_name', 'action', 'node') search_fields = ('ip_address', 'node', 'jail_name') ordering = ('-received_at',) @admin.register(NodeRegistry) class NodeRegistryAdmin(admin.ModelAdmin): """Seule UI d'édition d'alias/pays par noeud — pas de page dédiée. node_id reste en premier et hors list_editable (Django exige que la première colonne serve de lien vers la page de modification).""" list_display = ('node_id', 'alias', 'country', 'dashboard_url', 'is_online', 'last_seen', 'first_seen') list_editable = ('alias', 'country', 'dashboard_url') search_fields = ('node_id', 'alias') ordering = ('node_id',) @admin.display(boolean=True, description='En ligne') def is_online(self, obj: NodeRegistry) -> bool: threshold = timezone.now() - datetime.timedelta(seconds=NODE_OFFLINE_THRESHOLD_SECONDS) return obj.last_seen >= threshold @admin.register(JoinToken) class JoinTokenAdmin(admin.ModelAdmin): """Visibilité des jetons émis (manage.py create_join_token) — jamais créés/édités ici, token_hash n'a d'ailleurs aucun intérêt à être affiché (c'est un hash, le jeton en clair n'est montré qu'une fois, en ligne de commande, jamais persisté).""" list_display = ('node_name', 'created_at', 'expires_at', 'used_at', 'status') search_fields = ('node_name',) ordering = ('-created_at',) @admin.display(description='Statut') def status(self, obj: JoinToken) -> str: if obj.used_at: return 'Utilisé' if timezone.now() > obj.expires_at: return 'Expiré' return 'Actif' @admin.register(EnrollmentRequest) class EnrollmentRequestAdmin(admin.ModelAdmin): """Demandes soumises via la page publique /communaute/ — jamais de jeton émis/envoyé sans passer par l'action approve_and_send_token ci-dessous, déclenchée à la main.""" list_display = ('email', 'node_name', 'country', 'status', 'created_at', 'processed_at') list_filter = ('status',) search_fields = ('email', 'node_name') ordering = ('-created_at',) actions = ['approve_and_send_token', 'reject'] @admin.action(description="Approuver et envoyer le jeton d'inscription par email") def approve_and_send_token(self, request: HttpRequest, queryset: QuerySet[EnrollmentRequest]) -> None: approved = 0 for enrollment in queryset.filter(status=EnrollmentRequest.STATUS_PENDING): token, _join_token = issue_join_token(enrollment.node_name) send_mail( subject='Fail2banActionBanisher — inscription acceptée', message=( f"Votre demande pour rejoindre la communauté (noeud '{enrollment.node_name}') " 'a été acceptée.\n\n' 'Sur votre serveur, après avoir cloné le dépôt et configuré .env :\n\n' f' {join_command(enrollment.node_name, token)}\n' ' sudo make install\n\n' 'Ce jeton est à usage unique et expire dans 60 minutes.' ), from_email=None, # DEFAULT_FROM_EMAIL recipient_list=[enrollment.email], ) enrollment.status = EnrollmentRequest.STATUS_APPROVED enrollment.processed_at = timezone.now() enrollment.save(update_fields=['status', 'processed_at']) approved += 1 self.message_user(request, f'{approved} demande(s) approuvée(s), jeton envoyé par email.', messages.SUCCESS) @admin.action(description='Rejeter') def reject(self, request: HttpRequest, queryset: QuerySet[EnrollmentRequest]) -> None: updated = queryset.filter(status=EnrollmentRequest.STATUS_PENDING).update( status=EnrollmentRequest.STATUS_REJECTED, processed_at=timezone.now(), ) self.message_user(request, f'{updated} demande(s) rejetée(s).', messages.SUCCESS)