Add Admin Recovery System with multiple backup options
- Recovery key generation (32-char alphanumeric, dashed format) - Multiple backup methods: text file, QR code, stego image - QR codes obfuscated with XOR (RECOVERY_OBFUSCATION_KEY constant) - Stego backup hides key in image using Stegasoo itself - CLI: `stegasoo admin recover --db path/to/db` - Web routes: /recover, /account/recovery/regenerate - Toast notifications now auto-dismiss after 20s with fade - Updated WEB_UI.md and CLI.md documentation for v4.1.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
frontends/web/templates/setup_recovery.html
Normal file
176
frontends/web/templates/setup_recovery.html
Normal file
@@ -0,0 +1,176 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Recovery Key Setup - Stegasoo{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<i class="bi bi-shield-lock fs-1 d-block mb-2"></i>
|
||||
<h5 class="mb-0">Recovery Key Setup</h5>
|
||||
<small class="text-muted">Step 2 of 2</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Explanation -->
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>What is a recovery key?</strong><br>
|
||||
If you forget your admin password, this key is the ONLY way to reset it.
|
||||
Save it somewhere safe - it will not be shown again.
|
||||
</div>
|
||||
|
||||
<!-- Recovery Key Display -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-key-fill me-1"></i> Your Recovery Key
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control font-monospace text-center"
|
||||
id="recoveryKey" value="{{ recovery_key }}" readonly
|
||||
style="font-size: 1.1em; letter-spacing: 0.5px;">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="copyToClipboard()" title="Copy to clipboard">
|
||||
<i class="bi bi-clipboard" id="copyIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code (if available) -->
|
||||
{% if qr_base64 %}
|
||||
<div class="mb-4 text-center">
|
||||
<label class="form-label d-block">
|
||||
<i class="bi bi-qr-code me-1"></i> QR Code
|
||||
</label>
|
||||
<img src="data:image/png;base64,{{ qr_base64 }}"
|
||||
alt="Recovery Key QR Code" class="img-fluid border rounded"
|
||||
style="max-width: 200px;" id="qrImage">
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Scan with your phone's camera app</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Download Options -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-download me-1"></i> Download Options
|
||||
</label>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="downloadTextFile()">
|
||||
<i class="bi bi-file-text me-1"></i> Text File
|
||||
</button>
|
||||
{% if qr_base64 %}
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="downloadQRImage()">
|
||||
<i class="bi bi-image me-1"></i> QR Image
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Confirmation Form -->
|
||||
<form method="POST" id="recoveryForm">
|
||||
<input type="hidden" name="recovery_key" value="{{ recovery_key }}">
|
||||
|
||||
<!-- Confirm checkbox -->
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="confirmSaved"
|
||||
onchange="updateButtons()">
|
||||
<label class="form-check-label" for="confirmSaved">
|
||||
I have saved my recovery key in a secure location
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 justify-content-between">
|
||||
<!-- Skip button (no recovery) -->
|
||||
<button type="submit" name="action" value="skip"
|
||||
class="btn btn-outline-secondary"
|
||||
onclick="return confirm('Are you sure? Without a recovery key, there is NO way to reset your password if you forget it.')">
|
||||
<i class="bi bi-skip-forward me-1"></i> Skip (No Recovery)
|
||||
</button>
|
||||
|
||||
<!-- Save button (with key) -->
|
||||
<button type="submit" name="action" value="save"
|
||||
class="btn btn-primary" id="saveBtn" disabled>
|
||||
<i class="bi bi-check-lg me-1"></i> Continue
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Notes -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-check me-2"></i>Security Notes
|
||||
</div>
|
||||
<div class="card-body small">
|
||||
<ul class="mb-0">
|
||||
<li>The recovery key is <strong>not stored</strong> - only a hash is saved</li>
|
||||
<li>Keep it separate from your password (different location)</li>
|
||||
<li>Anyone with this key can reset admin passwords</li>
|
||||
<li>If you lose it and forget your password, you must recreate the database</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Copy recovery key to clipboard
|
||||
function copyToClipboard() {
|
||||
const keyInput = document.getElementById('recoveryKey');
|
||||
navigator.clipboard.writeText(keyInput.value).then(() => {
|
||||
const icon = document.getElementById('copyIcon');
|
||||
icon.className = 'bi bi-clipboard-check';
|
||||
setTimeout(() => { icon.className = 'bi bi-clipboard'; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Download as text file
|
||||
function downloadTextFile() {
|
||||
const key = document.getElementById('recoveryKey').value;
|
||||
const content = `Stegasoo Recovery Key
|
||||
=====================
|
||||
|
||||
${key}
|
||||
|
||||
IMPORTANT:
|
||||
- Keep this file in a secure location
|
||||
- Anyone with this key can reset admin passwords
|
||||
- Do not store with your password
|
||||
|
||||
Generated: ${new Date().toISOString()}
|
||||
`;
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'stegasoo-recovery-key.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Download QR as image
|
||||
function downloadQRImage() {
|
||||
const img = document.getElementById('qrImage');
|
||||
if (!img) return;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = img.src;
|
||||
a.download = 'stegasoo-recovery-qr.png';
|
||||
a.click();
|
||||
}
|
||||
|
||||
// Enable save button when checkbox is checked
|
||||
function updateButtons() {
|
||||
const checkbox = document.getElementById('confirmSaved');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
saveBtn.disabled = !checkbox.checked;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user