Add phase 5 secure credential vault

This commit is contained in:
A R R R Associates
2026-07-23 14:44:02 +05:30
parent d113db0d20
commit 1b9f555dcf
16 changed files with 442 additions and 0 deletions
@@ -0,0 +1,6 @@
{% extends "base/layout.html" %}{% block content %}
<div class="mx-auto max-w-7xl space-y-6 p-4 sm:p-6">
<div class="flex flex-wrap items-center justify-between gap-3"><div><h1 class="text-2xl font-bold">Secure Credential Vault</h1><p class="text-sm text-slate-600">Encrypted client and portal credentials. Secret values are never displayed on this page.</p></div>{% if can_manage %}<a href="/credential-vault/new" class="rounded-lg bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Add credential</a>{% endif %}</div>
<div class="grid gap-3 sm:grid-cols-3"><div class="rounded-xl bg-white p-4 shadow"><div class="text-xs text-slate-500">Visible credentials</div><div class="text-2xl font-bold">{{ counts.total }}</div></div><div class="rounded-xl bg-white p-4 shadow"><div class="text-xs text-slate-500">Rotation due soon</div><div class="text-2xl font-bold text-amber-700">{{ counts.due_soon }}</div></div><div class="rounded-xl bg-white p-4 shadow"><div class="text-xs text-slate-500">Overdue</div><div class="text-2xl font-bold text-red-700">{{ counts.overdue }}</div></div></div>
<div class="overflow-x-auto rounded-xl bg-white shadow"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left"><tr><th class="p-3">Credential</th><th class="p-3">Client</th><th class="p-3">Category</th><th class="p-3">Rotation/expiry</th><th class="p-3">State</th><th class="p-3"></th></tr></thead><tbody>{% for entry, client, state in rows %}<tr class="border-t"><td class="p-3"><div class="font-semibold">{{ entry.title }}</div><div class="text-xs text-slate-500">{{ entry.reference_number or 'No reference number' }}</div></td><td class="p-3">{{ client.client_name if client else 'Firm credential' }}</td><td class="p-3">{{ entry.category|replace('_',' ')|title }}</td><td class="p-3">{{ entry.rotation_due_on or entry.expires_on or '-' }}</td><td class="p-3"><span class="rounded-full px-2 py-1 text-xs {% if state=='overdue' %}bg-red-100 text-red-700{% elif state=='due_soon' %}bg-amber-100 text-amber-700{% else %}bg-slate-100 text-slate-700{% endif %}">{{ state|replace('_',' ')|title }}</span></td><td class="p-3"><a class="font-semibold text-brand-700" href="/credential-vault/{{ entry.id }}">Open</a></td></tr>{% else %}<tr><td colspan="6" class="p-8 text-center text-slate-500">No credentials are visible in this workspace.</td></tr>{% endfor %}</tbody></table></div>
</div>{% endblock %}
@@ -0,0 +1 @@
{% extends "base/layout.html" %}{% block content %}<div class="mx-auto max-w-5xl space-y-5 p-4 sm:p-6"><div class="flex items-start justify-between"><div><h1 class="text-2xl font-bold">{{ entry.title }}</h1><p class="text-sm text-slate-600">{{ client.client_name if client else 'Firm-level credential' }} · {{ entry.category|replace('_',' ')|title }}</p></div><a href="/credential-vault" class="rounded-lg border px-3 py-2">Back</a></div><div class="grid gap-4 md:grid-cols-2"><section class="rounded-xl bg-white p-5 shadow"><h2 class="font-semibold">Metadata</h2><dl class="mt-3 grid grid-cols-2 gap-3 text-sm"><dt class="text-slate-500">Portal</dt><dd>{% if entry.portal_url %}<a class="text-brand-700" href="{{ entry.portal_url }}" target="_blank" rel="noopener noreferrer">Open portal</a>{% else %}-{% endif %}</dd><dt class="text-slate-500">Reference</dt><dd>{{ entry.reference_number or '-' }}</dd><dt class="text-slate-500">Sensitivity</dt><dd>{{ entry.sensitivity|title }}</dd><dt class="text-slate-500">Rotation due</dt><dd>{{ entry.rotation_due_on or '-' }}</dd><dt class="text-slate-500">Expiry</dt><dd>{{ entry.expires_on or '-' }}</dd><dt class="text-slate-500">Status</dt><dd>{{ entry.status|title }}</dd></dl></section><section class="rounded-xl border border-amber-200 bg-amber-50 p-5"><h2 class="font-semibold">Reveal protected values</h2><p class="mt-1 text-sm text-amber-900">Re-authentication and a business reason are mandatory. The reveal is audit logged.</p><form method="post" action="/credential-vault/{{ entry.id }}/reveal" class="mt-3 space-y-3"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input required name="reason" placeholder="Business reason" class="w-full rounded-lg border p-2"><input required type="password" name="current_password" autocomplete="current-password" placeholder="Current ERP password" class="w-full rounded-lg border p-2"><button class="rounded-lg bg-slate-900 px-4 py-2 font-semibold text-white">Reveal once</button></form></section></div>{% if can_manage %}<section class="rounded-xl bg-white p-5 shadow"><h2 class="font-semibold">Rotate credential</h2><form method="post" action="/credential-vault/{{ entry.id }}/rotate" class="mt-3 grid gap-3 sm:grid-cols-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input name="username" placeholder="New username/login ID" class="rounded-lg border p-2"><input required type="password" name="secret" autocomplete="new-password" placeholder="New password/token" class="rounded-lg border p-2"><input type="password" name="additional_secret" autocomplete="new-password" placeholder="New PIN/additional secret" class="rounded-lg border p-2"><input type="date" name="rotation_due_on" class="rounded-lg border p-2"><textarea name="notes" placeholder="Encrypted notes" class="rounded-lg border p-2 sm:col-span-2"></textarea><input required name="reason" placeholder="Rotation reason" class="rounded-lg border p-2 sm:col-span-2"><button class="w-fit rounded-lg bg-brand-600 px-4 py-2 font-semibold text-white">Rotate and preserve history</button></form></section>{% endif %}<section class="rounded-xl bg-white p-5 shadow"><h2 class="font-semibold">Access audit</h2><div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><thead><tr><th class="p-2 text-left">When</th><th class="p-2 text-left">User</th><th class="p-2 text-left">Action</th><th class="p-2 text-left">Reason</th><th class="p-2 text-left">Result</th></tr></thead><tbody>{% for log,user_row in logs %}<tr class="border-t"><td class="p-2">{{ log.created_at_utc }}</td><td class="p-2">{{ user_row.full_name or user_row.email if user_row else '-' }}</td><td class="p-2">{{ log.action|title }}</td><td class="p-2">{{ log.reason or '-' }}</td><td class="p-2">{{ 'Success' if log.success else 'Denied' }}</td></tr>{% else %}<tr><td colspan="5" class="p-4 text-slate-500">No access events.</td></tr>{% endfor %}</tbody></table></div></section></div>{% endblock %}
@@ -0,0 +1 @@
{% extends "base/layout.html" %}{% block content %}<div class="mx-auto max-w-4xl p-4 sm:p-6"><h1 class="mb-5 text-2xl font-bold">Add encrypted credential</h1><form method="post" class="grid gap-4 rounded-xl bg-white p-6 shadow sm:grid-cols-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><label class="sm:col-span-2">Title<input required name="title" class="mt-1 w-full rounded-lg border p-2"></label><label>Category<select name="category" class="mt-1 w-full rounded-lg border p-2"><option value="government_portal">Government portal</option><option value="banking">Banking</option><option value="email">Email</option><option value="software">Software</option><option value="api_key">API key</option><option value="digital_signature">Digital signature</option><option value="other">Other</option></select></label><label>Sensitivity<select name="sensitivity" class="mt-1 w-full rounded-lg border p-2"><option>high</option><option>critical</option><option>standard</option></select></label><label>Client<select name="client_id" class="mt-1 w-full rounded-lg border p-2"><option value="">Firm-level credential</option>{% for c in clients %}<option value="{{ c.id }}">{{ c.client_name }}</option>{% endfor %}</select></label><label>Registration record<select name="registration_id" class="mt-1 w-full rounded-lg border p-2"><option value="">Not linked</option>{% for r in registrations %}<option value="{{ r.id }}">{{ r.registration_number }}</option>{% endfor %}</select></label><label class="sm:col-span-2">Portal URL<input name="portal_url" type="url" class="mt-1 w-full rounded-lg border p-2"></label><label>Reference number<input name="reference_number" class="mt-1 w-full rounded-lg border p-2"></label><label>Username/login ID<input name="username" autocomplete="off" class="mt-1 w-full rounded-lg border p-2"></label><label>Secret/password/token<input required name="secret" type="password" autocomplete="new-password" class="mt-1 w-full rounded-lg border p-2"></label><label>Additional secret/PIN<input name="additional_secret" type="password" autocomplete="new-password" class="mt-1 w-full rounded-lg border p-2"></label><label>Expires on<input name="expires_on" type="date" class="mt-1 w-full rounded-lg border p-2"></label><label>Rotation due on<input name="rotation_due_on" type="date" class="mt-1 w-full rounded-lg border p-2"></label><label>Owner<select name="owner_user_id" class="mt-1 w-full rounded-lg border p-2"><option value="">Current user</option>{% for u in users %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></label><fieldset><legend>Explicit staff access</legend><div class="mt-1 max-h-32 overflow-auto rounded-lg border p-2">{% for u in users %}<label class="block text-sm"><input type="checkbox" name="allowed_user_ids" value="{{ u.id }}"> {{ u.full_name or u.email }}</label>{% endfor %}</div></fieldset><label class="sm:col-span-2">Encrypted notes<textarea name="notes" rows="3" class="mt-1 w-full rounded-lg border p-2"></textarea></label><div class="sm:col-span-2 flex gap-2"><button class="rounded-lg bg-brand-600 px-4 py-2 font-semibold text-white">Save securely</button><a href="/credential-vault" class="rounded-lg border px-4 py-2">Cancel</a></div></form></div>{% endblock %}
@@ -0,0 +1 @@
{% extends "base/layout.html" %}{% block content %}<div class="mx-auto max-w-3xl p-4 sm:p-6"><div class="rounded-xl border border-red-200 bg-white p-6 shadow"><div class="flex items-start justify-between"><div><h1 class="text-xl font-bold">One-time credential reveal</h1><p class="text-sm text-red-700">Do not leave this page unattended. Values disappear when you navigate away.</p></div><a href="/credential-vault/{{ entry.id }}" class="rounded-lg border px-3 py-2">Close reveal</a></div><dl class="mt-5 space-y-4"><div><dt class="text-xs font-semibold uppercase text-slate-500">Username/Login ID</dt><dd class="mt-1 break-all rounded-lg bg-slate-100 p-3 font-mono">{{ values.username or '-' }}</dd></div><div><dt class="text-xs font-semibold uppercase text-slate-500">Password/Token/Secret</dt><dd class="mt-1 break-all rounded-lg bg-slate-900 p-3 font-mono text-white">{{ values.secret }}</dd></div>{% if values.additional_secret %}<div><dt class="text-xs font-semibold uppercase text-slate-500">Additional secret/PIN</dt><dd class="mt-1 break-all rounded-lg bg-slate-100 p-3 font-mono">{{ values.additional_secret }}</dd></div>{% endif %}{% if values.notes %}<div><dt class="text-xs font-semibold uppercase text-slate-500">Encrypted notes</dt><dd class="mt-1 whitespace-pre-wrap rounded-lg bg-slate-100 p-3">{{ values.notes }}</dd></div>{% endif %}</dl><p class="mt-5 text-xs text-slate-500">Audit reason: {{ reason }}</p></div></div>{% endblock %}