diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fc71bcb..2efd4d1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -30,13 +30,18 @@ const selected = ref(null); const revealed = ref(null); const versions = ref([]); +const revealedVersion = ref(null); const auditEvents = ref([]); +const auditMode = ref("all"); const tokens = ref([]); const newToken = ref(null); const showDeleteConfirm = ref(false); const editing = ref(false); const editFields = ref(false); const pendingDeleteSecret = ref(null); +const importFile = ref(null); +const importPreview = ref(null); +const importError = ref(""); const form = reactive({ title: "", @@ -128,6 +133,7 @@ function selectSecret(secret) { selected.value = secret; revealed.value = null; + revealedVersion.value = null; editing.value = false; editFields.value = false; fillEditForm(secret); @@ -247,12 +253,22 @@ async function loadVersions(secret) { selected.value = secret; activeTab.value = "history"; + revealedVersion.value = null; versions.value = await api.versions(secret.id); } +async function revealVersion(version) { + if (!selected.value) return; + revealedVersion.value = await api.revealVersion(selected.value.id, version.id); +} + async function loadAudit() { activeTab.value = "audit"; - auditEvents.value = (await api.audit()).items; + if (auditMode.value === "secret" && selected.value) { + auditEvents.value = (await api.secretAudit(selected.value.id)).items; + } else { + auditEvents.value = (await api.audit()).items; + } } async function loadTokens() { @@ -291,6 +307,32 @@ URL.revokeObjectURL(url); } +async function handleImportFile(event) { + importError.value = ""; + importPreview.value = null; + const file = event.target.files?.[0]; + importFile.value = file || null; + if (!file) return; + try { + const text = await file.text(); + const payload = JSON.parse(text); + if (payload.format !== "gnexus-creds-export" || payload.version !== 1) { + throw new Error("Unsupported import file"); + } + importPreview.value = payload; + } catch (err) { + importError.value = err.message; + } +} + +async function importData() { + if (!importPreview.value) return; + await api.importData(importPreview.value); + importFile.value = null; + importPreview.value = null; + await loadSecrets(); +} + async function deleteAllData() { await api.deleteAccountData(); showDeleteConfirm.value = false; @@ -504,11 +546,33 @@
-

Version history

-

{{ selected?.title || "Select a secret first" }}

+
+

Version history

+ Refresh +
+

{{ selected?.title || "Select a secret first from the Secrets tab" }}

- Version {{ version.version_number }} - {{ version.created_at }} + + Version {{ version.version_number }} + {{ version.created_at }} + + + {{ version.fields.length }} fields + Reveal + +
+
+
+

Version {{ revealedVersion.version_number }}

+
+
+ + {{ field.name }} + {{ field.encrypted ? "encrypted" : "plain" }} + + {{ field.value }} + Copy +
@@ -517,11 +581,27 @@

Audit

+ + All + + + Selected secret + Refresh
-
- {{ event.action }} - {{ event.channel }} · {{ event.created_at }} +
+ + {{ event.action }} + {{ event.channel }} · {{ event.created_at }} + + {{ JSON.stringify(event.metadata) }}
@@ -560,6 +640,22 @@

Settings

+
+

Import JSON

+ +

{{ importError }}

+

+ {{ importPreview.secrets?.length || 0 }} secrets ready to import from + {{ importFile?.name }} +

+ + Import selected file + +
Export decrypted JSON Delete all data diff --git a/frontend/src/api.js b/frontend/src/api.js index 951353c..606bf20 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -32,7 +32,10 @@ deleteSecret: (id) => request(`/api/v1/secrets/${id}`, { method: "DELETE" }), revealSecret: (id) => request(`/api/v1/secrets/${id}/reveal`, { method: "POST" }), versions: (id) => request(`/api/v1/secrets/${id}/versions`), + revealVersion: (id, versionId) => + request(`/api/v1/secrets/${id}/versions/${versionId}/reveal`, { method: "POST" }), audit: () => request("/api/v1/audit-events"), + secretAudit: (id) => request(`/api/v1/secrets/${id}/audit-events`), tokens: () => request("/api/v1/api-tokens"), createToken: (payload) => request("/api/v1/api-tokens", { method: "POST", body: JSON.stringify(payload) }), diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2ad7f21..3d790a2 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -27,6 +27,14 @@ font: inherit; } +input[type="file"] { + width: 100%; + padding: 10px; + border: 1px solid #536184; + background: #11121c; + color: #d6ddff; +} + .creds-app { min-height: 100vh; width: min(1480px, calc(100vw - 48px)); @@ -178,13 +186,18 @@ white-space: nowrap; } +.inline-title { + padding: 10px; +} + .list-panel { max-height: calc(100vh - 220px); overflow: auto; } .secret-row, -.field-row { +.field-row, +.audit-row { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -210,6 +223,7 @@ .secret-row span, .field-row span, +.audit-row span, .row-meta { min-width: 0; display: grid; @@ -263,6 +277,18 @@ white-space: nowrap; } +.audit-row { + grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1fr); +} + +.audit-row code { + min-width: 0; + overflow: auto; + padding: 6px 8px; + background: #11121c; + white-space: nowrap; +} + .revealed { margin-top: 4px; border: 1px solid #536184; @@ -352,7 +378,8 @@ } .field-row, - .secret-row { + .secret-row, + .audit-row { grid-template-columns: 1fr; }