sessions.html 227 lignes · 11673 octets
{% extends "base.html" %}

{% block title %}{{ page_title }}{% endblock %}

{% block navbar %}
{% include "components/navbar.html" %}
{% endblock %}

{% block content %}
<div class="container mx-auto px-4 py-8">
    <!-- Breadcrumb -->
    <div class="text-sm breadcrumbs mb-6">
        <ul>
            <li><a href="/">{{ breadcrumb_home }}</a></li>
            <li><a href="/profile">{{ breadcrumb_settings }}</a></li>
            <li>{{ breadcrumb_sessions }}</li>
        </ul>
    </div>

    <!-- Page Header -->
    <div class="mb-8">
        <h1 class="text-3xl font-bold mb-2">{{ title }}</h1>
        <p class="text-base-content/70">{{ subtitle }}</p>
    </div>

    <!-- Active Sessions List -->
    <div class="space-y-4 max-w-4xl">
        {% if sessions|length > 0 %}
            {% for session in sessions %}
            <div class="card bg-base-100 shadow-xl">
                <div class="card-body">
                    <div class="flex justify-between items-start">
                        <div class="flex-1">
                            <h3 class="card-title text-lg">
                                {% if session.is_current %}
                                <span class="badge badge-primary badge-sm">{{ current_session }}</span>
                                {% endif %}
                                {% if !session.device_info.is_empty() %}
                                    {{ session.device_info }}
                                {% else %}
                                    {{ unknown_device }}
                                {% endif %}
                            </h3>
                            
                            <div class="mt-3 space-y-2 text-sm">
                                <!-- IP Address -->
                                <div class="flex items-center gap-2">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                                    <span class="text-base-content/70">{{ ip_address }}</span>
                                    <span class="font-mono">{{ session.ip_address }}</span>
                                </div>
                                
                                <!-- Created At -->
                                <div class="flex items-center gap-2">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                                    <span class="text-base-content/70">{{ created_at }}</span>
                                    <span>{{ session.created_at }}</span>
                                </div>
                                
                                <!-- Last Activity -->
                                <div class="flex items-center gap-2">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
                                    <span class="text-base-content/70">{{ last_activity }}</span>
                                    <span>{{ session.last_activity }}</span>
                                </div>
                                
                                <!-- Session ID (for debugging) -->
                                <div class="flex items-center gap-2">
                                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
                                    <span class="text-base-content/70">{{ session_id }}</span>
                                    <span class="font-mono text-xs">{{ session.id }}</span>
                                </div>
                            </div>
                        </div>
                        
                        <!-- Revoke Button -->
                        <div class="card-actions">
                            {% if !session.is_current %}
                            <button
                                class="btn btn-error btn-sm btn-revoke-session"
                                data-session-id="{{ session.id }}"
                                data-device-info="{{ session.device_info }}"
                            >
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
                                {{ revoke }}
                            </button>
                            {% else %}
                            <div class="badge badge-lg badge-success gap-2">
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
                                {{ current_badge }}
                            </div>
                            {% endif %}
                        </div>
                    </div>
                </div>
            </div>
            {% endfor %}
            
            <!-- Revoke All Button -->
            {% if sessions|length > 1 %}
            <div class="card bg-base-200 shadow-xl">
                <div class="card-body">
                    <h3 class="card-title text-base text-error">
                        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
                        {{ danger_zone }}
                    </h3>
                    <p class="text-sm text-base-content/70">
                        {{ danger_zone_desc }}
                    </p>
                    <div class="card-actions justify-end">
                        <button class="btn btn-error" onclick="revokeAllSessions()">
                            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
                            {{ revoke_all }}
                        </button>
                    </div>
                </div>
            </div>
            {% endif %}
        {% else %}
            <!-- No Sessions State -->
            <div class="card bg-base-100 shadow-xl">
                <div class="card-body items-center text-center">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-base-content/30 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
                    <h3 class="card-title">{{ no_sessions }}</h3>
                    <p class="text-base-content/70">{{ no_sessions_desc }}</p>
                </div>
            </div>
        {% endif %}
    </div>

    <!-- Info Card -->
    <div class="card bg-info text-info-content shadow-xl max-w-4xl mt-8">
        <div class="card-body">
            <h3 class="card-title text-base">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
                {{ about_title }}
            </h3>
            <p class="text-sm">
                {{ about_desc }}
            </p>
        </div>
    </div>

    <!-- Back to Settings -->
    <div class="mt-8">
        <a href="/profile" class="btn btn-outline">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
            {{ back_to_settings }}
        </a>
    </div>
</div>

<script>
// SEC-FIX: Use data-* attributes instead of inline onclick to prevent XSS
document.querySelectorAll('.btn-revoke-session').forEach(function(btn) {
    btn.addEventListener('click', async function() {
        var sessionId = this.dataset.sessionId;
        var deviceInfo = this.dataset.deviceInfo;
        if (!confirm('Êtes-vous sûr de vouloir révoquer la session pour "' + deviceInfo + '" ?\n\nCet appareil sera déconnecté immédiatement.')) {
            return;
        }

        try {
            const response = await fetch('/settings/sessions/' + encodeURIComponent(sessionId), {
                method: 'DELETE',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': '{{ csrf_token }}',
                }
            });

            if (response.ok) {
                alert('Session révoquée avec succès !');
                window.location.reload();
            } else {
                const result = await response.json();
                alert('Erreur : ' + (result.error || result.message || 'Échec de la révocation'));
            }
        } catch (error) {
            console.error('Error:', error);
            alert('Erreur réseau. Veuillez réessayer.');
        }
    });
});

// Revoke all other sessions
const revokeAllConfirmMsg = "{{ revoke_all_confirm }}";
const revokeAllSuccessMsg = "{{ revoke_all_success }}";
const revokeAllErrorMsg = "{{ revoke_all_error }}";

async function revokeAllSessions() {
    if (!confirm(revokeAllConfirmMsg)) {
        return;
    }
    
    // Get all session IDs except current one
    const sessionCards = document.querySelectorAll('.card');
    const promises = [];
    
    sessionCards.forEach(card => {
        const revokeBtn = card.querySelector('.btn-revoke-session');
        if (revokeBtn) {
            const sessionId = revokeBtn.dataset.sessionId;
            promises.push(
                fetch('/settings/sessions/' + encodeURIComponent(sessionId), {
                    method: 'DELETE',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-Token': '{{ csrf_token }}',
                    }
                })
            );
        }
    });
    
    try {
        await Promise.all(promises);
        alert('✅ ' + revokeAllSuccessMsg);
        window.location.reload();
    } catch (error) {
        console.error('Error:', error);
        alert('❌ ' + revokeAllErrorMsg);
        window.location.reload();
    }
}
</script>
{% endblock %}