fieldwitness/frontends/web/templates/account.html
Aaron D. Lee 490f9d4a1d Rebrand SooSeF to FieldWitness
Complete project rebrand for better positioning in the press freedom
and digital security space. FieldWitness communicates both field
deployment and evidence testimony — appropriate for the target audience
of journalists, NGOs, and human rights organizations.

Rename mapping:
- soosef → fieldwitness (package, CLI, all imports)
- soosef.stegasoo → fieldwitness.stego
- soosef.verisoo → fieldwitness.attest
- ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name)
- SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR
- SoosefConfig → FieldWitnessConfig
- SoosefError → FieldWitnessError

Also includes:
- License switch from MIT to GPL-3.0
- C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py
- README repositioned to lead with provenance/federation, stego backgrounded
- Threat model skeleton at docs/security/threat-model.md
- Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:05:13 -04:00

446 lines
19 KiB
HTML

{% extends "base.html" %}
{% block title %}Account - Stego{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-person-gear me-2"></i>Account Settings</h5>
</div>
<div class="card-body">
<p class="text-muted mb-4">
Logged in as <strong>{{ username }}</strong>
{% if is_admin %}
<span class="badge bg-warning text-dark ms-2">
<i class="bi bi-shield-check me-1"></i>Admin
</span>
{% endif %}
</p>
{% if is_admin %}
<div class="mb-4">
<a href="{{ url_for('admin_users') }}" class="btn btn-outline-primary w-100">
<i class="bi bi-people me-2"></i>Manage Users
</a>
</div>
<!-- Recovery Key Management (Admin only) -->
<div class="card bg-dark mb-4">
<div class="card-body py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-shield-lock me-2"></i>
<strong>Recovery Key</strong>
{% if has_recovery %}
<span class="badge bg-success ms-2">Configured</span>
{% else %}
<span class="badge bg-secondary ms-2">Not Set</span>
{% endif %}
</div>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('regenerate_recovery') }}" class="btn btn-outline-warning"
onclick="return confirm('Generate a new recovery key? This will invalidate any existing key.')">
<i class="bi bi-arrow-repeat me-1"></i>
{{ 'Regenerate' if has_recovery else 'Generate' }}
</a>
{% if has_recovery %}
<form method="POST" action="{{ url_for('disable_recovery') }}" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-outline-danger"
onclick="return confirm('Disable recovery? If you forget your password, you will NOT be able to recover your account.')">
<i class="bi bi-x-lg"></i>
</button>
</form>
{% endif %}
</div>
</div>
<small class="text-muted d-block mt-2">
{% if has_recovery %}
Allows password reset if you're locked out.
{% else %}
No recovery option - most secure, but no password reset possible.
{% endif %}
</small>
</div>
</div>
{% endif %}
<h6 class="text-muted mb-3">Change Password</h6>
<form method="POST" action="{{ url_for('account') }}" id="accountForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-3">
<label class="form-label">
<i class="bi bi-key me-1"></i> Current Password
</label>
<div class="input-group">
<input type="password" name="current_password" class="form-control"
id="currentPasswordInput" required>
<button class="btn btn-outline-secondary" type="button"
onclick="togglePassword('currentPasswordInput', this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">
<i class="bi bi-key-fill me-1"></i> New Password
</label>
<div class="input-group">
<input type="password" name="new_password" class="form-control"
id="newPasswordInput" required minlength="8">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePassword('newPasswordInput', this)">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">Minimum 8 characters</div>
</div>
<div class="mb-4">
<label class="form-label">
<i class="bi bi-key-fill me-1"></i> Confirm New Password
</label>
<div class="input-group">
<input type="password" name="new_password_confirm" class="form-control"
id="newPasswordConfirmInput" required minlength="8">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePassword('newPasswordConfirmInput', this)">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-lg me-2"></i>Update Password
</button>
</form>
</div>
</div>
<!-- Saved Channel Keys Section -->
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-key-fill me-2"></i>Saved Channel Keys</h5>
<span class="badge bg-secondary">{{ channel_keys|length }} / {{ max_channel_keys }}</span>
</div>
<div class="card-body">
{% if channel_keys %}
<div class="list-group list-group-flush mb-3">
{% for key in channel_keys %}
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
<div>
<strong>{{ key.name }}</strong>
<br>
<code class="small text-muted">{{ key.channel_key[:4] }}...{{ key.channel_key[-4:] }}</code>
{% if key.last_used_at %}
<span class="text-muted small ms-2">Last used: {{ key.last_used_at[:10] }}</span>
{% endif %}
</div>
<div class="btn-group btn-group-sm">
{% if is_admin %}
<button type="button" class="btn btn-outline-info"
onclick="showKeyQr('{{ key.channel_key }}', '{{ key.name }}')"
title="Show QR Code">
<i class="bi bi-qr-code"></i>
</button>
{% endif %}
<button type="button" class="btn btn-outline-secondary"
onclick="renameKey({{ key.id }}, '{{ key.name }}')"
title="Rename">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ url_for('account_delete_key', key_id=key.id) }}"
style="display:inline;"
onsubmit="return confirm('Delete key &quot;{{ key.name }}&quot;?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted mb-3">No saved channel keys. Save keys for quick access on encode/decode pages.</p>
{% endif %}
{% if can_save_key %}
<hr>
<h6 class="text-muted mb-3">Add New Key</h6>
<form method="POST" action="{{ url_for('account_save_key') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="row g-2 mb-2">
<div class="col-5">
<input type="text" name="key_name" class="form-control form-control-sm"
placeholder="Key name" required maxlength="50">
</div>
<div class="col-7">
<div class="input-group input-group-sm">
<input type="text" name="channel_key" id="channelKeyInput"
class="form-control font-monospace"
placeholder="XXXX-XXXX-..." required
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}">
<button type="button" class="btn btn-outline-secondary" id="scanChannelKeyBtn"
title="Scan QR code with camera">
<i class="bi bi-camera"></i>
</button>
</div>
</div>
</div>
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-plus-lg me-1"></i>Save Key
</button>
</form>
{% else %}
<div class="alert alert-info mb-0 small">
<i class="bi bi-info-circle me-1"></i>
Maximum of {{ max_channel_keys }} keys reached. Delete a key to add more.
</div>
{% endif %}
</div>
</div>
<!-- Logout -->
<div class="mt-4">
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger w-100">
<i class="bi bi-box-arrow-left me-2"></i>Logout
</a>
</div>
</div>
</div>
<!-- Rename Modal -->
<div class="modal fade" id="renameModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<form method="POST" id="renameForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="modal-header">
<h6 class="modal-title">Rename Key</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" name="new_name" class="form-control" id="renameInput"
required maxlength="50">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-sm btn-primary">Rename</button>
</div>
</form>
</div>
</div>
</div>
{% if is_admin %}
<!-- QR Code Modal (Admin only) -->
<div class="modal fade" id="qrModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title"><i class="bi bi-qr-code me-2"></i><span id="qrKeyName">Channel Key</span></h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<canvas id="qrCanvas" class="bg-white p-2 rounded"></canvas>
<div class="mt-2">
<code class="small" id="qrKeyDisplay"></code>
</div>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrDownload">
<i class="bi bi-download me-1"></i>Download
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrPrint">
<i class="bi bi-printer me-1"></i>Print Sheet
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script src="{{ url_for('static', filename='js/fieldwitness.js') }}"></script>
{% if is_admin %}
<script src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
{% endif %}
<script>
StegoAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
// Webcam QR scanning for channel key input (v4.1.5)
document.getElementById('scanChannelKeyBtn')?.addEventListener('click', function() {
Stego.showQrScanner((text) => {
const input = document.getElementById('channelKeyInput');
if (input) {
// Clean and format the key
const clean = text.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
if (clean.length === 32) {
input.value = clean.match(/.{4}/g).join('-');
} else {
input.value = text.toUpperCase();
}
}
}, 'Scan Channel Key');
});
// Format channel key input as user types
document.getElementById('channelKeyInput')?.addEventListener('input', function() {
Stego.formatChannelKeyInput(this);
});
function renameKey(keyId, currentName) {
document.getElementById('renameInput').value = currentName;
document.getElementById('renameForm').action = '/account/keys/' + keyId + '/rename';
new bootstrap.Modal(document.getElementById('renameModal')).show();
}
{% if is_admin %}
function showKeyQr(channelKey, keyName) {
// Format key with dashes if not already
const clean = channelKey.replace(/-/g, '').toUpperCase();
const formatted = clean.match(/.{4}/g)?.join('-') || clean;
// Update modal content
document.getElementById('qrKeyName').textContent = keyName;
document.getElementById('qrKeyDisplay').textContent = formatted;
// Generate QR code using QRious
const canvas = document.getElementById('qrCanvas');
if (typeof QRious !== 'undefined' && canvas) {
try {
new QRious({
element: canvas,
value: formatted,
size: 200,
level: 'M'
});
new bootstrap.Modal(document.getElementById('qrModal')).show();
} catch (error) {
console.error('QR generation error:', error);
}
}
}
// Download QR as PNG
document.getElementById('qrDownload')?.addEventListener('click', function() {
const canvas = document.getElementById('qrCanvas');
const keyName = document.getElementById('qrKeyName').textContent;
if (canvas) {
const link = document.createElement('a');
link.download = 'stego-channel-key-' + keyName.toLowerCase().replace(/\s+/g, '-') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
});
// Print tiled QR sheet (US Letter)
document.getElementById('qrPrint')?.addEventListener('click', function() {
const canvas = document.getElementById('qrCanvas');
const keyText = document.getElementById('qrKeyDisplay').textContent;
const keyName = document.getElementById('qrKeyName').textContent;
if (canvas && keyText) {
printQrSheet(canvas, keyText, keyName);
}
});
// Print QR codes tiled on US Letter paper (8.5" x 11")
function printQrSheet(canvas, keyText, title) {
const qrDataUrl = canvas.toDataURL('image/png');
const printWindow = window.open('', '_blank');
if (!printWindow) {
alert('Please allow popups to print');
return;
}
// US Letter: 8.5" x 11" - create 4x5 grid of QR codes
const cols = 4;
const rows = 5;
// Split key into two lines (4 groups each)
const keyParts = keyText.split('-');
const keyLine1 = keyParts.slice(0, 4).join('-');
const keyLine2 = keyParts.slice(4).join('-');
let qrGrid = '';
for (let i = 0; i < rows * cols; i++) {
qrGrid += `
<div class="qr-tile">
<div class="key-text">${keyLine1}</div>
<img src="${qrDataUrl}" alt="QR">
<div class="key-text">${keyLine2}</div>
</div>
`;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title></title>
<style>
@page {
size: letter;
margin: 0.2in;
margin-top: 0.1in;
margin-bottom: 0.1in;
}
@media print {
@page { margin: 0.15in; }
html, body { margin: 0; padding: 0; }
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: white;
}
.grid {
display: grid;
grid-template-columns: repeat(${cols}, 1fr);
gap: 0;
margin-top: 0.09in;
}
.qr-tile {
border: 1px dashed #ccc;
padding: 0.04in;
text-align: center;
page-break-inside: avoid;
}
.qr-tile img {
width: 1.6in;
height: 1.6in;
}
.key-text {
font-size: 10pt;
font-weight: bold;
color: #333;
line-height: 1.2;
}
.footer {
display: none;
}
</style>
</head>
<body>
<div class="grid">${qrGrid}</div>
<div class="footer">Cut along dashed lines</div>
<script>
window.onload = function() { window.print(); };
<\/script>
</body>
</html>
`);
printWindow.document.close();
}
{% endif %}
</script>
{% endblock %}