diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ecd1d28..645dd39 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -298,17 +298,33 @@ } function routeToTab(pathname = window.location.pathname) { + if (pathname.startsWith("/secrets/")) { + return "secrets"; + } return routeTabs[pathname] || (pathname === "/" ? "secrets" : null); } +function routeSecretId(pathname = window.location.pathname) { + const match = pathname.match(/^\/secrets\/([^/]+)$/); + return match ? decodeURIComponent(match[1]) : null; +} + +function setRoute(path, replace = false, state = {}) { + if (window.location.pathname === path) return; + const method = replace ? "replaceState" : "pushState"; + window.history[method](state, "", path); +} + function setRouteForTab(tab, replace = false) { const path = tabRoutes[tab] || tabRoutes.secrets; - if (window.location.pathname === path) return; - const method = replace ? "replaceState" : "pushState"; - window.history[method]({ tab }, "", path); + setRoute(path, replace, { tab }); } -function syncTabFromRoute(replace = false) { +function setRouteForSecret(secretId, replace = false) { + setRoute(`/secrets/${encodeURIComponent(secretId)}`, replace, { tab: "secrets", secretId }); +} + +async function syncTabFromRoute(replace = false) { const routedTab = routeToTab(); const nextTab = routedTab && canUseTab(routedTab) ? routedTab : "secrets"; if (activeTab.value !== nextTab) { @@ -318,10 +334,24 @@ if (!routedTab || routedTab !== nextTab || window.location.pathname === "/") { setRouteForTab(nextTab, replace); } + if (nextTab === "secrets") { + const secretId = routeSecretId(); + if (secretId) { + try { + await openSecretById(secretId, replace); + } catch { + selected.value = null; + secretsView.value = "list"; + setRouteForTab("secrets", true); + } + } else { + secretsView.value = "list"; + } + } } function onPopState() { - syncTabFromRoute(true); + void syncTabFromRoute(true); } function serializeFields(fields) { @@ -354,10 +384,25 @@ editing.value = false; editFields.value = false; fillEditForm(secret); + setRouteForSecret(secret.id); +} + +async function openSecretById(secretId, replace = false) { + const cached = secrets.value.find((secret) => secret.id === secretId); + const secret = cached || (await api.getSecret(secretId)); + selected.value = secret; + secretsView.value = "detail"; + revealed.value = null; + revealedVersion.value = null; + editing.value = false; + editFields.value = false; + fillEditForm(secret); + setRouteForSecret(secret.id, replace); } function showSecretsList() { secretsView.value = "list"; + setRouteForTab("secrets"); } function openEditMetadata() { @@ -620,9 +665,9 @@ onMounted(async () => { try { me.value = await api.me(); - syncTabFromRoute(true); - window.addEventListener("popstate", onPopState); await loadSecrets(); + await syncTabFromRoute(true); + window.addEventListener("popstate", onPopState); } catch { window.location.href = "/auth/login"; } @@ -640,6 +685,9 @@ if (suppressNextRouteSync) { suppressNextRouteSync = false; } else { + if (tab === "secrets") { + secretsView.value = "list"; + } setRouteForTab(tab); } if (tab === "admin" && me.value?.role === "admin" && !adminUsers.value.length) { diff --git a/frontend/src/api.js b/frontend/src/api.js index c0b0122..9320c5e 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -29,6 +29,7 @@ }, createSecret: (payload) => request("/api/v1/secrets", { method: "POST", body: JSON.stringify(payload) }), + getSecret: (id) => request(`/api/v1/secrets/${id}`), updateSecret: (id, payload) => request(`/api/v1/secrets/${id}`, { method: "PATCH", body: JSON.stringify(payload) }), deleteSecret: (id) => request(`/api/v1/secrets/${id}`, { method: "DELETE" }),