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 %}
GitRust