This permanently removes all secrets and versions. Audit records remain.
diff --git a/frontend/src/api.js b/frontend/src/api.js index 606bf20..e2a075a 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -43,5 +43,9 @@ exportData: () => request("/api/v1/export", { method: "POST" }), importData: (payload) => request("/api/v1/import", { method: "POST", body: JSON.stringify(payload) }), - deleteAccountData: () => request("/api/v1/account-data", { method: "DELETE" }) + deleteAccountData: () => request("/api/v1/account-data", { method: "DELETE" }), + adminUsers: (params = {}) => { + const query = new URLSearchParams(params); + return request(`/api/v1/admin/users?${query}`); + } }; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 3d790a2..9d2850b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -323,6 +323,39 @@ max-width: 840px; } +.admin-stack { + max-width: 1120px; +} + +.admin-grid { + display: grid; + grid-template-columns: minmax(260px, 1fr) 120px 120px minmax(160px, 220px); + gap: 12px; + align-items: center; + padding: 11px 10px; + border-bottom: 1px solid #30384f; +} + +.admin-grid > span { + min-width: 0; + display: grid; + gap: 4px; +} + +.admin-grid strong, +.admin-grid small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-grid-head { + color: #9ba7cb; + font-size: 12px; + text-transform: uppercase; +} + .checks { display: flex; gap: 14px; @@ -379,7 +412,8 @@ .field-row, .secret-row, - .audit-row { + .audit-row, + .admin-grid { grid-template-columns: 1fr; } diff --git a/gnexus_creds/auth.py b/gnexus_creds/auth.py index b2810f7..c958230 100644 --- a/gnexus_creds/auth.py +++ b/gnexus_creds/auth.py @@ -8,7 +8,6 @@ from gnexus_creds.db import get_db from gnexus_creds.errors import AppError from gnexus_creds.models import User -from gnexus_creds.schemas import Scope from gnexus_creds.services import ( Actor, authenticate_api_token, @@ -58,7 +57,7 @@ async def require_admin(actor: Actor = Depends(actor_from_request)) -> Actor: if actor.user.system_role != "admin": - actor.require(Scope.admin) + raise AppError("forbidden", "Admin role required.", status_code=403) return actor diff --git a/tests/test_api.py b/tests/test_api.py index 53fc6ab..98c4059 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -155,3 +155,20 @@ assert {field["name"]: field["value"] for field in reveal_response.json()["fields"]}[ "password" ] == "secret" + + +@pytest.mark.anyio +async def test_admin_users_requires_admin_role(app, actor): + actor.user.system_role = "user" + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/api/v1/admin/users") + assert response.status_code == 403 + + +@pytest.mark.anyio +async def test_admin_users_lists_users_for_admin(app): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/api/v1/admin/users") + assert response.status_code == 200 + assert response.json()["total"] == 1 + assert response.json()["items"][0]["email"] == "user@example.test"