security.html 182 lignes · 10961 octets
{% extends "base.html" %}

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

{% block content %}
<div class="container mx-auto max-w-4xl px-4 py-8">
    <!-- Breadcrumb -->
    <div class="text-sm breadcrumbs mb-6">
        <ul>
            <li><a href="/home">{{ breadcrumb_home }}</a></li>
            <li><a href="/settings/preferences">{{ breadcrumb_settings }}</a></li>
            <li>{{ breadcrumb_security }}</li>
        </ul>
    </div>

    {% if force_2fa %}
    <div class="alert alert-warning mb-6 shadow-lg">
        <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><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>
        <div class="flex-1">
            <span>{{ force_2fa_message }}</span>
        </div>
        <button class="btn btn-sm btn-ghost"
            onclick="localStorage.removeItem('refresh_token'); localStorage.removeItem('remember_me'); localStorage.removeItem('jwt_expires_in'); fetch('/api/v1/auth/logout', {method:'POST', credentials:'include'}).then(()=>window.location.href='/login');">
            {{ force_2fa_logout }}
        </button>
    </div>
    {% endif %}

    <div class="flex items-center gap-3 mb-8">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
        </svg>
        <div>
            <h1 class="text-2xl font-bold">{{ title }}</h1>
            <p class="text-base-content/70">{{ subtitle }}</p>
        </div>
    </div>

    <div x-data="securityPage" data-csrf="{{ csrf_token }}" data-regenerate-confirm="{{ regenerate_confirm }}" class="space-y-6">
        <!-- Status Card -->
        <div class="card bg-base-100 shadow-lg">
            <div class="card-body">
                <div class="flex items-center justify-between">
                    <div class="flex items-center gap-3">
                        <h2 class="card-title">{{ totp_title }}</h2>
                        {% if totp_enabled %}
                        <span class="badge badge-success gap-1">
                            <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>
                            {{ status_enabled }}
                        </span>
                        {% else %}
                        <span class="badge badge-warning gap-1">
                            <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="M12 9v2m0 4h.01" /></svg>
                            {{ status_disabled }}
                        </span>
                        {% endif %}
                    </div>
                </div>

                {% if totp_enabled %}
                <!-- Enabled state -->
                <div class="mt-4 space-y-4">
                    <div class="alert alert-success">
                        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
                        <span>{{ enabled_message }}</span>
                    </div>
                    <p class="text-sm text-base-content/70">{{ backup_codes_remaining }}: <strong>{{ backup_codes_count }}</strong></p>
                    <div class="flex gap-3">
                        <button class="btn btn-outline btn-sm" @click="regenerateBackupCodes()">
                            <span x-show="!regenerating">{{ regenerate_button }}</span>
                            <span x-show="regenerating" class="loading loading-spinner loading-sm"></span>
                        </button>
                        <button class="btn btn-error btn-outline btn-sm" @click="showDisableModal = true">{{ disable_button }}</button>
                    </div>
                </div>
                {% else %}
                <!-- Disabled state -->
                <div class="mt-4">
                    <p class="text-base-content/70 mb-4">{{ disabled_message }}</p>
                    <button class="btn btn-primary" @click="startSetup()" x-show="!setupStarted">
                        {{ enable_button }}
                    </button>
                </div>
                {% endif %}

                <!-- Setup flow (shown when user clicks Enable) -->
                <div x-show="setupStarted && !setupComplete" x-cloak class="mt-6 space-y-6">
                    <div class="divider"></div>

                    <!-- QR Code -->
                    <div class="text-center" x-show="qrCode">
                        <p class="font-semibold mb-3">{{ scan_qr }}</p>
                        <div class="flex justify-center mb-4">
                            <img :src="qrCode" alt="QR Code" class="w-48 h-48 rounded-lg border border-base-300 p-2 bg-white" />
                        </div>
                        <div class="collapse collapse-arrow bg-base-200 max-w-md mx-auto">
                            <input type="checkbox" />
                            <div class="collapse-title text-sm font-medium">{{ manual_entry }}</div>
                            <div class="collapse-content">
                                <code class="text-xs break-all select-all" x-text="secretBase32"></code>
                            </div>
                        </div>
                    </div>

                    <!-- Verification code input -->
                    <div class="max-w-sm mx-auto" x-show="qrCode">
                        <label class="label"><span class="label-text font-semibold">{{ enter_code }}</span></label>
                        <div class="join w-full">
                            <input
                                type="text"
                                x-model="verifyCode"
                                maxlength="6"
                                pattern="[0-9]*"
                                inputmode="numeric"
                                autocomplete="one-time-code"
                                class="input input-bordered join-item flex-1 text-center text-2xl tracking-widest"
                                placeholder="{{ code_placeholder }}"
                                @keyup.enter="verifyAndEnable()"
                            />
                            <button class="btn btn-primary join-item" @click="verifyAndEnable()" :disabled="verifyCode.length !== 6 || verifying">
                                <span x-show="!verifying">{{ verify_button }}</span>
                                <span x-show="verifying" class="loading loading-spinner loading-sm"></span>
                            </button>
                        </div>
                        <p x-show="setupError" class="text-error text-sm mt-2" x-text="setupError"></p>
                    </div>
                </div>
            </div>
        </div>

        <!-- Backup Codes Modal -->
        <dialog id="backupCodesModal" class="modal" :class="{ 'modal-open': showBackupCodes }">
            <div class="modal-box max-w-lg">
                <h3 class="font-bold text-lg mb-2">{{ backup_codes_title }}</h3>
                <div class="alert alert-warning mb-4">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01" /></svg>
                    <span class="text-sm">{{ backup_codes_warning }}</span>
                </div>
                <div class="grid grid-cols-2 gap-2 mb-4">
                    <template x-for="code in backupCodes" :key="code">
                        <code class="bg-base-200 p-2 rounded text-center font-mono text-sm select-all" x-text="code"></code>
                    </template>
                </div>
                <div class="flex gap-2">
                    <button class="btn btn-sm btn-outline" @click="copyBackupCodes()">
                        <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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
                        {{ copy_button }}
                    </button>
                </div>
                <div class="modal-action">
                    <button class="btn btn-primary" @click="acknowledgeBackupCodes()">{{ saved_codes_button }}</button>
                </div>
            </div>
            <form method="dialog" class="modal-backdrop" @click="acknowledgeBackupCodes()">
                <button>close</button>
            </form>
        </dialog>

        <!-- Disable 2FA Modal -->
        <dialog id="disableModal" class="modal" :class="{ 'modal-open': showDisableModal }">
            <div class="modal-box">
                <h3 class="font-bold text-lg mb-4">{{ confirm_disable }}</h3>
                <div class="form-control">
                    <label class="label"><span class="label-text">{{ password_label }}</span></label>
                    <input type="password" x-model="disablePassword" class="input input-bordered" @keyup.enter="disableTotp()" />
                </div>
                <p x-show="disableError" class="text-error text-sm mt-2" x-text="disableError"></p>
                <div class="modal-action">
                    <button class="btn" @click="closeDisableModal()">{{ cancel_button }}</button>
                    <button class="btn btn-error" @click="disableTotp()" :disabled="!disablePassword || disabling">
                        <span x-show="!disabling">{{ disable_button }}</span>
                        <span x-show="disabling" class="loading loading-spinner loading-sm"></span>
                    </button>
                </div>
            </div>
            <form method="dialog" class="modal-backdrop" @click="showDisableModal = false;">
                <button>close</button>
            </form>
        </dialog>
    </div>
</div>
{% endblock %}