feat(ui): add Credential Audit page under Admin menu

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-22 19:39:01 +00:00
parent a46cd6634b
commit 3fa24c0686
3 changed files with 265 additions and 0 deletions
+68
View File
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
from app.utils.config_validator.masking import mask_sensitive_value
from app.utils.settings_service import (
SETTING_METADATA,
get_all_settings_from_db,
get_setting_metadata,
get_settings_by_category,
@@ -116,3 +117,70 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
except Exception as e:
logger.error(f"Error loading settings page: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to load settings page")
@router.get("/admin/credentials")
@require_login
@require_admin_access
async def credentials_page(request: Request, db: Session = Depends(get_db)):
"""
Credential audit page - admin only.
Displays all sensitive credential settings grouped by category, showing
whether each is configured and whether it comes from the database or an
environment variable. Supports the credential rotation workflow.
"""
try:
db_settings = get_all_settings_from_db(db)
categories: dict[str, list[dict]] = {}
for key, meta in SETTING_METADATA.items():
if not meta.get("sensitive", False):
continue
env_value = getattr(settings, key, None)
in_db = key in db_settings and db_settings[key]
if in_db:
source = "db"
configured = True
elif env_value:
source = "env"
configured = True
else:
source = None
configured = False
category = meta.get("category", "Other")
if category not in categories:
categories[category] = []
categories[category].append(
{
"key": key,
"description": meta.get("description", ""),
"configured": configured,
"source": source,
"restart_required": meta.get("restart_required", False),
}
)
total = sum(len(v) for v in categories.values())
configured_count = sum(1 for creds in categories.values() for c in creds if c["configured"])
return templates.TemplateResponse(
"credentials.html",
{
"request": request,
"categories": categories,
"total": total,
"configured_count": configured_count,
"unconfigured_count": total - configured_count,
"app_version": settings.version,
},
)
except Exception as e:
logger.error(f"Error loading credentials page: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to load credentials page"
)
+6
View File
@@ -77,6 +77,9 @@
<a href="/settings" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-cog w-4 mr-2 text-gray-500"></i> Settings
</a>
<a href="/admin/credentials" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-key w-4 mr-2 text-yellow-500"></i> Credentials
</a>
<a href="/env" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-terminal w-4 mr-2 text-gray-500"></i> Environment
</a>
@@ -136,6 +139,9 @@
<a href="/settings" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-cog mr-2 text-gray-400"></i> Settings
</a>
<a href="/admin/credentials" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-key mr-2 text-yellow-400"></i> Credentials
</a>
<a href="/env" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-terminal mr-2 text-gray-400"></i> Environment
</a>
+191
View File
@@ -0,0 +1,191 @@
{% extends "base.html" %}
{% block title %}Credential Audit - DocuElevate{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-2">
<i class="fas fa-key text-yellow-500 text-2xl"></i>
<h1 class="text-3xl font-bold">Credential Audit</h1>
</div>
<p class="text-gray-600">
Overview of all sensitive credentials used by DocuElevate. No secret values are shown here — only
whether each credential is configured and where it comes from.
</p>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 text-blue-600 text-xl">
<i class="fas fa-list"></i>
</span>
<div>
<div class="text-2xl font-bold text-gray-800">{{ total }}</div>
<div class="text-sm text-gray-500">Total Credentials</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-green-100 text-green-600 text-xl">
<i class="fas fa-check-circle"></i>
</span>
<div>
<div class="text-2xl font-bold text-green-700">{{ configured_count }}</div>
<div class="text-sm text-gray-500">Configured</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-5 flex items-center gap-4">
<span class="inline-flex items-center justify-center h-12 w-12 rounded-full bg-gray-100 text-gray-500 text-xl">
<i class="fas fa-circle-question"></i>
</span>
<div>
<div class="text-2xl font-bold text-gray-600">{{ unconfigured_count }}</div>
<div class="text-sm text-gray-500">Not Configured</div>
</div>
</div>
</div>
<!-- Legend -->
<div class="bg-blue-50 border-l-4 border-blue-500 text-blue-800 p-4 mb-6 rounded-r-md">
<p class="font-semibold mb-2"><i class="fas fa-circle-info mr-1"></i> Legend</p>
<div class="flex flex-wrap gap-4 text-sm">
<span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mr-1">DB</span>
Value stored in the database (encrypted at rest; overrides environment variable)
</span>
<span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-1">ENV</span>
Value from environment variable or <code>.env</code> file
</span>
<span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 mr-1">
<i class="fas fa-circle-exclamation mr-0.5 text-xs"></i> Missing
</span>
Credential not set — integration will not work
</span>
<span>
<span class="text-orange-500 mr-1"><i class="fas fa-rotate"></i></span>
Restart required when this credential is rotated
</span>
</div>
</div>
<!-- Credential Categories -->
{% for category, creds in categories.items() %}
<div class="bg-white shadow rounded-lg mb-6">
<!-- Category Header -->
<div class="bg-gray-100 px-6 py-3 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-800">{{ category }}</h2>
<span class="text-sm text-gray-500">
{{ creds | selectattr('configured') | list | length }} / {{ creds | length }} configured
</span>
</div>
<!-- Credentials Table -->
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-56">
Credential
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-32">
Status
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-24">
Source
</th>
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-20">
Action
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{% for cred in creds %}
<tr class="{% if not cred.configured %}bg-red-50{% else %}hover:bg-gray-50{% endif %}">
<!-- Key name -->
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-1">
<code class="text-sm font-mono text-gray-800">{{ cred.key }}</code>
{% if cred.restart_required %}
<span title="Restart required after rotating this credential">
<i class="fas fa-rotate text-orange-400 text-xs"></i>
</span>
{% endif %}
</div>
</td>
<!-- Description -->
<td class="px-6 py-4 text-sm text-gray-600">
{{ cred.description }}
</td>
<!-- Status badge -->
<td class="px-6 py-4 whitespace-nowrap">
{% if cred.configured %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800">
<i class="fas fa-check mr-1"></i> Configured
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">
<i class="fas fa-circle-exclamation mr-1"></i> Missing
</span>
{% endif %}
</td>
<!-- Source badge -->
<td class="px-6 py-4 whitespace-nowrap">
{% if cred.source == 'db' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="Stored in database (encrypted)">
<i class="fas fa-database mr-1 text-xs"></i> DB
</span>
{% elif cred.source == 'env' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" title="From environment variable">
<i class="fas fa-terminal mr-1 text-xs"></i> ENV
</span>
{% else %}
<span class="text-gray-400 text-xs"></span>
{% endif %}
</td>
<!-- Quick-edit link -->
<td class="px-6 py-4 whitespace-nowrap text-center">
<a href="/settings#{{ cred.key }}"
title="Edit in Settings"
class="inline-flex items-center justify-center w-7 h-7 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
>
<i class="fas fa-pen-to-square text-sm"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
<!-- Footer actions -->
<div class="flex flex-col sm:flex-row gap-3 mt-6">
<a href="/settings"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
>
<i class="fas fa-cog"></i> Manage Settings
</a>
<a href="/api/settings/credentials"
target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 text-sm font-medium"
>
<i class="fas fa-code"></i> Raw JSON (API)
</a>
</div>
</div>
{% endblock %}