change theme, linter, formatter
This commit is contained in:
parent
823e377e4b
commit
e2375fbba9
@ -12,6 +12,7 @@ module.exports = {
|
|||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'@vue/eslint-config-typescript',
|
'@vue/eslint-config-typescript',
|
||||||
|
'prettier',
|
||||||
],
|
],
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs', '*.config.js'],
|
ignorePatterns: ['dist', '.eslintrc.cjs', '*.config.js'],
|
||||||
parser: 'vue-eslint-parser',
|
parser: 'vue-eslint-parser',
|
||||||
|
|||||||
32
.prettierignore
Normal file
32
.prettierignore
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
CHANGELOG.md
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# Config files that should maintain their format
|
||||||
|
.prettierrc
|
||||||
|
.eslintrc*
|
||||||
|
tsconfig*.json
|
||||||
|
vite.config.ts
|
||||||
|
tailwind.config.js
|
||||||
|
postcss.config.js
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
tags
|
||||||
@ -1,14 +1,15 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en" data-theme="business">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vault-icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vault-icon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Browser Vault GUI</title>
|
<title>Browser Vault GUI</title>
|
||||||
|
<!-- Material Design Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "vue-tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext .vue,.ts --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint . --ext .vue,.ts --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.15"
|
"vue": "^3.4.15"
|
||||||
@ -20,8 +22,10 @@
|
|||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"daisyui": "^4.4.24",
|
"daisyui": "^4.4.24",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"eslint-plugin-vue": "^9.19.2",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.8",
|
"vite": "^5.0.8",
|
||||||
|
|||||||
@ -33,12 +33,18 @@ importers:
|
|||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.55.0
|
specifier: ^8.55.0
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: ^10.1.8
|
||||||
|
version: 10.1.8(eslint@8.57.1)
|
||||||
eslint-plugin-vue:
|
eslint-plugin-vue:
|
||||||
specifier: ^9.19.2
|
specifier: ^9.19.2
|
||||||
version: 9.33.0(eslint@8.57.1)
|
version: 9.33.0(eslint@8.57.1)
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.33
|
specifier: ^8.4.33
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.6.2
|
||||||
|
version: 3.6.2
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.1
|
specifier: ^3.4.1
|
||||||
version: 3.4.18
|
version: 3.4.18
|
||||||
@ -720,6 +726,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8:
|
||||||
|
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7.0.0'
|
||||||
|
|
||||||
eslint-plugin-vue@9.33.0:
|
eslint-plugin-vue@9.33.0:
|
||||||
resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==}
|
resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==}
|
||||||
engines: {node: ^14.17.0 || >=16.0.0}
|
engines: {node: ^14.17.0 || >=16.0.0}
|
||||||
@ -1134,6 +1146,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prettier@3.6.2:
|
||||||
|
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1980,6 +1997,10 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8(eslint@8.57.1):
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.57.1
|
||||||
|
|
||||||
eslint-plugin-vue@9.33.0(eslint@8.57.1):
|
eslint-plugin-vue@9.33.0(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
|
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
|
||||||
@ -2391,6 +2412,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier@3.6.2: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|||||||
39
src/App.vue
39
src/App.vue
@ -18,11 +18,15 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Save servers to localStorage whenever they change
|
// Save servers to localStorage whenever they change
|
||||||
watch(servers, (newServers) => {
|
watch(
|
||||||
|
servers,
|
||||||
|
newServers => {
|
||||||
if (newServers.length > 0) {
|
if (newServers.length > 0) {
|
||||||
localStorage.setItem('vaultServers', JSON.stringify(newServers))
|
localStorage.setItem('vaultServers', JSON.stringify(newServers))
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const handleAddServer = (server: VaultServer) => {
|
const handleAddServer = (server: VaultServer) => {
|
||||||
servers.value = [...servers.value, server]
|
servers.value = [...servers.value, server]
|
||||||
@ -47,10 +51,7 @@ const handleLogin = async (credentials: VaultCredentials, shouldSaveCredentials:
|
|||||||
try {
|
try {
|
||||||
// Verify login and get mount points
|
// Verify login and get mount points
|
||||||
const { vaultApi } = await import('./services/vaultApi')
|
const { vaultApi } = await import('./services/vaultApi')
|
||||||
const mountPoints = await vaultApi.verifyLoginAndGetMounts(
|
const mountPoints = await vaultApi.verifyLoginAndGetMounts(selectedServer.value, credentials)
|
||||||
selectedServer.value,
|
|
||||||
credentials
|
|
||||||
)
|
|
||||||
|
|
||||||
activeConnection.value = {
|
activeConnection.value = {
|
||||||
server: selectedServer.value,
|
server: selectedServer.value,
|
||||||
@ -79,10 +80,7 @@ const handleLogin = async (credentials: VaultCredentials, shouldSaveCredentials:
|
|||||||
console.log(`✓ Logged in successfully. Found ${mountPoints.length} KV mount point(s).`)
|
console.log(`✓ Logged in successfully. Found ${mountPoints.length} KV mount point(s).`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error)
|
console.error('Login failed:', error)
|
||||||
alert(
|
alert(`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}\n\n` + 'Please check your credentials and server configuration.')
|
||||||
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}\n\n` +
|
|
||||||
'Please check your credentials and server configuration.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +94,10 @@ const handleLogout = () => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="bg-gradient-to-r from-primary to-secondary text-primary-content shadow-lg">
|
<header class="bg-gradient-to-r from-primary to-secondary text-primary-content shadow-lg">
|
||||||
<div class="container mx-auto px-4 py-6">
|
<div class="container mx-auto px-4 py-6">
|
||||||
<h1 class="text-4xl font-bold mb-2">🔐 Browser Vault GUI</h1>
|
<h1 class="text-4xl font-bold mb-2 flex items-center gap-3">
|
||||||
|
<i class="mdi mdi-shield-lock text-primary-content" />
|
||||||
|
Browser Vault GUI
|
||||||
|
</h1>
|
||||||
<p class="text-lg opacity-90">Alternative frontend for HashiCorp Vault</p>
|
<p class="text-lg opacity-90">Alternative frontend for HashiCorp Vault</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -117,27 +118,17 @@ const handleLogout = () => {
|
|||||||
|
|
||||||
<!-- Login Form -->
|
<!-- Login Form -->
|
||||||
<div v-if="selectedServer">
|
<div v-if="selectedServer">
|
||||||
<LoginForm
|
<LoginForm :server="selectedServer" @login="handleLogin" />
|
||||||
:server="selectedServer"
|
|
||||||
@login="handleLogin"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dashboard -->
|
<!-- Dashboard -->
|
||||||
<Dashboard
|
<Dashboard v-else :connection="activeConnection" @logout="handleLogout" />
|
||||||
v-else
|
|
||||||
:connection="activeConnection"
|
|
||||||
@logout="handleLogout"
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-base-300 border-t border-base-content/10">
|
<footer class="bg-base-300 border-t border-base-content/10">
|
||||||
<div class="container mx-auto px-4 py-4 text-center text-sm opacity-70">
|
<div class="container mx-auto px-4 py-4 text-center text-sm opacity-70">Browser Vault GUI - An alternative frontend for HashiCorp Vault</div>
|
||||||
Browser Vault GUI - An alternative frontend for HashiCorp Vault
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -47,11 +47,7 @@ const handleReadSecret = async (path?: string) => {
|
|||||||
secretData.value = null
|
secretData.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await vaultApi.readSecret(
|
const data = await vaultApi.readSecret(props.connection.server, props.connection.credentials, pathToRead)
|
||||||
props.connection.server,
|
|
||||||
props.connection.credentials,
|
|
||||||
pathToRead
|
|
||||||
)
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
secretData.value = data
|
secretData.value = data
|
||||||
@ -144,31 +140,28 @@ const handleViewSecret = () => {
|
|||||||
<div class="flex flex-col md:flex-row justify-between gap-4">
|
<div class="flex flex-col md:flex-row justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold">Connected to {{ connection.server.name }}</h2>
|
<h2 class="text-2xl font-bold">Connected to {{ connection.server.name }}</h2>
|
||||||
<p class="text-sm font-mono opacity-70">{{ connection.server.url }}</p>
|
<p class="text-sm font-mono opacity-70">
|
||||||
|
{{ connection.server.url }}
|
||||||
|
</p>
|
||||||
<p class="text-sm opacity-60 mt-1">
|
<p class="text-sm opacity-60 mt-1">
|
||||||
Authenticated via {{ connection.credentials.authMethod }}
|
Authenticated via {{ connection.credentials.authMethod }}
|
||||||
<span v-if="connection.lastConnected" class="italic">
|
<span v-if="connection.lastConnected" class="italic">
|
||||||
• Connected at {{ connection.lastConnected.toLocaleTimeString() }}
|
• Connected at
|
||||||
|
{{ connection.lastConnected.toLocaleTimeString() }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<button
|
<button class="btn btn-primary btn-sm" @click="showSearch = !showSearch">
|
||||||
class="btn btn-primary btn-sm"
|
<i :class="showSearch ? 'mdi mdi-magnify-close' : 'mdi mdi-magnify'" class="mr-2" />
|
||||||
@click="showSearch = !showSearch"
|
{{ showSearch ? 'Hide Search' : 'Show Search' }}
|
||||||
>
|
|
||||||
{{ showSearch ? 'Hide Search' : '🔍 Show Search' }}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-sm" @click="showSettings = true">
|
||||||
class="btn btn-sm"
|
<i class="mdi mdi-cog mr-2" />
|
||||||
@click="showSettings = true"
|
Settings
|
||||||
>
|
|
||||||
⚙️ Settings
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-error btn-sm" @click="emit('logout')">
|
||||||
class="btn btn-error btn-sm"
|
<i class="mdi mdi-logout mr-2" />
|
||||||
@click="emit('logout')"
|
|
||||||
>
|
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -195,19 +188,9 @@ const handleViewSecret = () => {
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Mount Point</span>
|
<span class="label-text">Mount Point</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select v-model="selectedMountPoint" class="select select-bordered w-full" :disabled="isLoading">
|
||||||
v-model="selectedMountPoint"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
<option value="">Select a mount point...</option>
|
<option value="">Select a mount point...</option>
|
||||||
<option
|
<option v-for="mount in connection.mountPoints" :key="mount.path" :value="mount.path">{{ mount.path }}/ ({{ mount.type }} v2)</option>
|
||||||
v-for="mount in connection.mountPoints"
|
|
||||||
:key="mount.path"
|
|
||||||
:value="mount.path"
|
|
||||||
>
|
|
||||||
{{ mount.path }}/ ({{ mount.type }} v2)
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -217,9 +200,7 @@ const handleViewSecret = () => {
|
|||||||
<span class="label-text">Secret Path</span>
|
<span class="label-text">Secret Path</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="join w-full">
|
<div class="join w-full">
|
||||||
<span class="join-item bg-base-300 px-3 py-2 text-sm font-mono border border-base-300">
|
<span class="join-item bg-base-300 px-3 py-2 text-sm font-mono border border-base-300"> {{ selectedMountPoint || 'mount' }}/ </span>
|
||||||
{{ selectedMountPoint || 'mount' }}/
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
v-model="secretPath"
|
v-model="secretPath"
|
||||||
type="text"
|
type="text"
|
||||||
@ -228,34 +209,34 @@ const handleViewSecret = () => {
|
|||||||
:disabled="isLoading || !selectedMountPoint"
|
:disabled="isLoading || !selectedMountPoint"
|
||||||
@keypress="handleKeyPress"
|
@keypress="handleKeyPress"
|
||||||
/>
|
/>
|
||||||
<button
|
<button class="btn btn-primary join-item" :disabled="!selectedMountPoint || !secretPath" @click="handleViewSecret()">
|
||||||
class="btn btn-primary join-item"
|
<i class="mdi mdi-eye mr-2" />
|
||||||
:disabled="!selectedMountPoint || !secretPath"
|
|
||||||
@click="handleViewSecret()"
|
|
||||||
>
|
|
||||||
View Secret
|
View Secret
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt">
|
<span class="label-text-alt">
|
||||||
Full path: {{ selectedMountPoint ? `${selectedMountPoint}/${secretPath || 'path'}` : 'Select mount point first' }}
|
Full path:
|
||||||
|
{{ selectedMountPoint ? `${selectedMountPoint}/${secretPath || 'path'}` : 'Select mount point first' }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Removed inline secret display - now using modal -->
|
|
||||||
|
|
||||||
<!-- Info Box -->
|
<!-- Info Box -->
|
||||||
<div v-if="!showSearch" class="alert alert-info mt-6">
|
<div v-if="!showSearch" class="alert alert-info mt-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<h4 class="font-bold">Browse Secrets</h4>
|
<h4 class="font-bold">Browse Secrets</h4>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
<li>Select a mount point from the detected KV secret engines</li>
|
<li>Select a mount point from the detected KV secret engines</li>
|
||||||
<li>Enter the secret path (without the mount point prefix)</li>
|
<li>Enter the secret path (without the mount point prefix)</li>
|
||||||
<li>Example: Mount <code class="bg-base-200 px-1 rounded">secret</code> + Path <code class="bg-base-200 px-1 rounded">data/myapp/config</code></li>
|
<li>
|
||||||
|
Example: Mount
|
||||||
|
<code class="bg-base-200 px-1 rounded">secret</code> + Path
|
||||||
|
<code class="bg-base-200 px-1 rounded">data/myapp/config</code>
|
||||||
|
</li>
|
||||||
<li>Use Search (shown above) to find secrets across all mount points</li>
|
<li>Use Search (shown above) to find secrets across all mount points</li>
|
||||||
<li><strong>Security:</strong> Secret data is never cached - always fetched fresh</li>
|
<li><strong>Security:</strong> Secret data is never cached - always fetched fresh</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -265,23 +246,35 @@ const handleViewSecret = () => {
|
|||||||
<!-- API Info -->
|
<!-- API Info -->
|
||||||
<div class="alert mt-4">
|
<div class="alert mt-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<h4 class="font-semibold">Security & Caching</h4>
|
<h4 class="font-semibold">Security & Caching</h4>
|
||||||
<p class="mt-1">🔒 <strong>Secret data is NEVER cached</strong> - always fetched fresh for security.</p>
|
<p class="mt-1 flex items-center gap-2">
|
||||||
<p class="mt-1">📂 Directory listings are cached to improve search performance.</p>
|
<i class="mdi mdi-lock text-success" />
|
||||||
<p class="mt-1">🔑 All requests include the <code class="bg-base-200 px-1 rounded">X-Vault-Token</code> header for authentication.</p>
|
<strong>Secret data is NEVER cached</strong> - always fetched fresh for security.
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-folder text-info" />
|
||||||
|
Directory listings are cached to improve search performance.
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-key text-warning" />
|
||||||
|
All requests include the
|
||||||
|
<code class="bg-base-200 px-1 rounded">X-Vault-Token</code> header for authentication.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<Settings
|
<Settings v-if="showSettings" @close="showSettings = false" />
|
||||||
v-if="showSettings"
|
|
||||||
@close="showSettings = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Secret Viewer Modal -->
|
<!-- Secret Viewer Modal -->
|
||||||
<SecretModal
|
<SecretModal
|
||||||
@ -293,4 +286,3 @@ const handleViewSecret = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -42,10 +42,14 @@ const loadCredentialsFromServer = (server: VaultServer) => {
|
|||||||
loadCredentialsFromServer(props.server)
|
loadCredentialsFromServer(props.server)
|
||||||
|
|
||||||
// Watch for server changes and reload credentials
|
// Watch for server changes and reload credentials
|
||||||
watch(() => props.server, (newServer) => {
|
watch(
|
||||||
|
() => props.server,
|
||||||
|
newServer => {
|
||||||
loadCredentialsFromServer(newServer)
|
loadCredentialsFromServer(newServer)
|
||||||
showSecurityWarning.value = false // Close any open warning modal
|
showSecurityWarning.value = false // Close any open warning modal
|
||||||
}, { immediate: false })
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// Show warning if user is trying to save credentials for the first time
|
// Show warning if user is trying to save credentials for the first time
|
||||||
@ -95,20 +99,19 @@ const cancelSaveCredentials = () => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="card-title text-2xl mb-2">Connect to {{ server.name }}</h2>
|
<h2 class="card-title text-2xl mb-2">Connect to {{ server.name }}</h2>
|
||||||
<p class="text-sm font-mono opacity-70">{{ server.url }}</p>
|
<p class="text-sm font-mono opacity-70">
|
||||||
|
{{ server.url }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<!-- Auth Method -->
|
<!-- Auth Method -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Authentication Method</span>
|
<span class="label-text">Authentication Method</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select v-model="authMethod" class="select select-bordered w-full">
|
||||||
v-model="authMethod"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
>
|
|
||||||
<option value="token">Token</option>
|
<option value="token">Token</option>
|
||||||
<option value="userpass">Username & Password</option>
|
<option value="userpass">Username & Password</option>
|
||||||
<option value="ldap">LDAP</option>
|
<option value="ldap">LDAP</option>
|
||||||
@ -167,29 +170,21 @@ const cancelSaveCredentials = () => {
|
|||||||
<!-- Save Credentials Option -->
|
<!-- Save Credentials Option -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label cursor-pointer justify-start gap-3">
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
<input
|
<input v-model="saveCredentials" type="checkbox" class="checkbox checkbox-warning" />
|
||||||
v-model="saveCredentials"
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-warning"
|
|
||||||
/>
|
|
||||||
<span class="label-text">
|
<span class="label-text">
|
||||||
<span class="font-semibold text-warning">⚠️ Save credentials locally</span>
|
<span class="font-semibold text-warning flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-alert text-warning" />
|
||||||
|
Save credentials locally
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt text-warning">
|
<span class="label-text-alt text-warning"> Not recommended! Credentials will be stored in plain text in localStorage </span>
|
||||||
Not recommended! Credentials will be stored in plain text in localStorage
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<button
|
<button type="submit" class="btn btn-primary w-full" :class="{ loading: isLoading }" :disabled="isLoading">
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary w-full"
|
|
||||||
:class="{ 'loading': isLoading }"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
{{ isLoading ? 'Connecting...' : 'Connect' }}
|
{{ isLoading ? 'Connecting...' : 'Connect' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -197,11 +192,19 @@ const cancelSaveCredentials = () => {
|
|||||||
<!-- Security Warning Modal -->
|
<!-- Security Warning Modal -->
|
||||||
<div v-if="showSecurityWarning" class="modal modal-open">
|
<div v-if="showSecurityWarning" class="modal modal-open">
|
||||||
<div class="modal-box border-2 border-error">
|
<div class="modal-box border-2 border-error">
|
||||||
<h3 class="font-bold text-lg text-error mb-4">⚠️ Security Warning</h3>
|
<h3 class="font-bold text-lg text-error mb-4 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-alert-circle text-error" />
|
||||||
|
Security Warning
|
||||||
|
</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-semibold">This is NOT recommended for security reasons!</span>
|
<span class="font-semibold">This is NOT recommended for security reasons!</span>
|
||||||
</div>
|
</div>
|
||||||
@ -209,7 +212,10 @@ const cancelSaveCredentials = () => {
|
|||||||
<div class="text-sm space-y-2">
|
<div class="text-sm space-y-2">
|
||||||
<p class="font-semibold">If you save credentials:</p>
|
<p class="font-semibold">If you save credentials:</p>
|
||||||
<ul class="list-disc list-inside space-y-1 ml-2">
|
<ul class="list-disc list-inside space-y-1 ml-2">
|
||||||
<li>Your token/password will be stored in <strong>plain text</strong></li>
|
<li>
|
||||||
|
Your token/password will be stored in
|
||||||
|
<strong>plain text</strong>
|
||||||
|
</li>
|
||||||
<li>Anyone with access to your browser can read them</li>
|
<li>Anyone with access to your browser can read them</li>
|
||||||
<li>Browser extensions can access localStorage</li>
|
<li>Browser extensions can access localStorage</li>
|
||||||
<li>If your computer is compromised, credentials are exposed</li>
|
<li>If your computer is compromised, credentials are exposed</li>
|
||||||
@ -226,28 +232,18 @@ const cancelSaveCredentials = () => {
|
|||||||
|
|
||||||
<div class="bg-base-300 p-3 rounded text-xs">
|
<div class="bg-base-300 p-3 rounded text-xs">
|
||||||
<p class="font-mono">
|
<p class="font-mono">
|
||||||
<strong>Better alternatives:</strong><br>
|
<strong>Better alternatives:</strong><br />
|
||||||
• Use short-lived tokens<br>
|
• Use short-lived tokens<br />
|
||||||
• Re-login each session<br>
|
• Re-login each session<br />
|
||||||
• Use a password manager<br>
|
• Use a password manager<br />
|
||||||
• Enable auto-logout timeout
|
• Enable auto-logout timeout
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button
|
<button class="btn btn-ghost" @click="cancelSaveCredentials">Cancel</button>
|
||||||
class="btn btn-ghost"
|
<button class="btn btn-error" @click="confirmSaveCredentials">I Understand the Risks - Save Anyway</button>
|
||||||
@click="cancelSaveCredentials"
|
|
||||||
>
|
|
||||||
Cancel - Don't Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-error"
|
|
||||||
@click="confirmSaveCredentials"
|
|
||||||
>
|
|
||||||
I Understand the Risks - Save Anyway
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -255,14 +251,19 @@ const cancelSaveCredentials = () => {
|
|||||||
<!-- Security Notice -->
|
<!-- Security Notice -->
|
||||||
<div class="alert mt-4">
|
<div class="alert mt-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-xs">
|
<div class="text-xs">
|
||||||
<p class="font-semibold">⚠️ Security Notice:</p>
|
<p class="font-semibold flex items-center gap-2">
|
||||||
<p>This application connects directly to your Vault server. Credentials are not stored permanently and are only kept in memory during your session.</p>
|
<i class="mdi mdi-shield-alert text-warning" />
|
||||||
|
Security Notice:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This application connects directly to your Vault server. Credentials are not stored permanently and are only kept in memory during your
|
||||||
|
session.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -45,16 +45,17 @@ const handleSearch = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Always search across all mount points
|
// Always search across all mount points
|
||||||
const searchResults = await vaultApi.searchAllMounts(
|
const searchResults = await vaultApi.searchAllMounts(props.server, props.credentials, props.mountPoints!, searchTerm.value)
|
||||||
props.server,
|
|
||||||
props.credentials,
|
|
||||||
props.mountPoints!,
|
|
||||||
searchTerm.value
|
|
||||||
)
|
|
||||||
|
|
||||||
const endTime = performance.now()
|
const endTime = performance.now()
|
||||||
searchTime.value = endTime - startTime
|
searchTime.value = endTime - startTime
|
||||||
results.value = searchResults
|
results.value = searchResults
|
||||||
|
|
||||||
|
if (results.value.filter(r => !r.isDirectory).length == 1) {
|
||||||
|
emit('selectPath', results.value.filter(r => !r.isDirectory)[0].path)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(results.value.length)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search error:', error)
|
console.error('Search error:', error)
|
||||||
alert('Search failed. Check console for details.')
|
alert('Search failed. Check console for details.')
|
||||||
@ -73,24 +74,19 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-xl font-bold mb-4">🔍 Search Paths</h3>
|
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-magnify text-primary" />
|
||||||
|
Search Paths
|
||||||
|
</h3>
|
||||||
|
|
||||||
<!-- Search Controls -->
|
<!-- Search Controls -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Search Info -->
|
<!-- Search Info -->
|
||||||
<div class="alert alert-info">
|
<div v-if="!mountPointsAvailable" class="alert alert-info">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm">
|
<p class="text-sm text-error">No mount points detected - logout and login again to refresh</p>
|
||||||
<p class="font-semibold">🌐 Searching across all mount points</p>
|
|
||||||
<p v-if="mountPointsAvailable">
|
|
||||||
Found {{ mountPoints?.length }} KV mount point(s): {{ mountPoints?.map(m => m.path).join(', ') }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-error">
|
|
||||||
No mount points detected - logout and login again to refresh
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Term -->
|
<!-- Search Term -->
|
||||||
@ -107,35 +103,31 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
:disabled="isSearching"
|
:disabled="isSearching"
|
||||||
@keypress="handleKeyPress"
|
@keypress="handleKeyPress"
|
||||||
/>
|
/>
|
||||||
<button
|
<button class="btn btn-primary join-item" :class="{ loading: isSearching }" :disabled="isSearching" @click="handleSearch">
|
||||||
class="btn btn-primary join-item"
|
<i v-if="!isSearching" class="mdi mdi-magnify mr-2" />
|
||||||
:class="{ 'loading': isSearching }"
|
|
||||||
:disabled="isSearching"
|
|
||||||
@click="handleSearch"
|
|
||||||
>
|
|
||||||
{{ isSearching ? 'Searching...' : 'Search' }}
|
{{ isSearching ? 'Searching...' : 'Search' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Progress -->
|
|
||||||
<div v-if="isSearching" class="alert mt-4">
|
|
||||||
<span class="loading loading-spinner"></span>
|
|
||||||
<span>Searching recursively... This may take a moment.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Stats -->
|
<!-- Search Stats -->
|
||||||
<div v-if="searchTime !== null" class="alert alert-success mt-4">
|
<div v-if="searchTime !== null" class="alert alert-success mt-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
Found <strong>{{ results.length }}</strong> result{{ results.length !== 1 ? 's' : '' }}
|
Found <strong>{{ results.length }}</strong> result{{ results.length !== 1 ? 's' : '' }} in
|
||||||
in <strong>{{ (searchTime / 1000).toFixed(2) }}s</strong>
|
<strong>{{ (searchTime / 1000).toFixed(2) }}s</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Progress -->
|
||||||
|
<div v-if="isSearching" class="alert mt-4">
|
||||||
|
<span class="loading loading-spinner" />
|
||||||
|
<span>Searching recursively... This may take a moment.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search Results -->
|
<!-- Search Results -->
|
||||||
<div v-if="results.length > 0" class="mt-4">
|
<div v-if="results.length > 0" class="mt-4">
|
||||||
<h4 class="font-semibold mb-3">Search Results</h4>
|
<h4 class="font-semibold mb-3">Search Results</h4>
|
||||||
@ -148,19 +140,19 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
@click="!result.isDirectory && emit('selectPath', result.path)"
|
@click="!result.isDirectory && emit('selectPath', result.path)"
|
||||||
>
|
>
|
||||||
<div class="card-body p-3 flex flex-row items-center gap-3">
|
<div class="card-body p-3 flex flex-row items-center gap-3">
|
||||||
<span class="text-2xl">{{ result.isDirectory ? '📁' : '📄' }}</span>
|
<i :class="result.isDirectory ? 'mdi mdi-folder text-warning' : 'mdi mdi-file-document text-info'" class="text-xl" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-mono text-sm break-all">{{ result.path }}</p>
|
<p class="font-mono text-sm break-all">
|
||||||
<p v-if="result.mountPoint" class="text-xs opacity-60 italic">
|
{{ result.path }}
|
||||||
📌 {{ result.mountPoint }}
|
</p>
|
||||||
|
<p v-if="result.mountPoint" class="text-xs opacity-60 italic flex items-center gap-1">
|
||||||
|
<i class="mdi mdi-pin text-xs" />
|
||||||
|
{{ result.mountPoint }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-sm">Depth: {{ result.depth }}</span>
|
<span class="badge badge-sm">Depth: {{ result.depth }}</span>
|
||||||
<button
|
<button v-if="!result.isDirectory" class="btn btn-primary btn-xs" @click.stop="emit('selectPath', result.path)">
|
||||||
v-if="!result.isDirectory"
|
<i class="mdi mdi-eye mr-1" />
|
||||||
class="btn btn-primary btn-xs"
|
|
||||||
@click.stop="emit('selectPath', result.path)"
|
|
||||||
>
|
|
||||||
View
|
View
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -171,7 +163,12 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
<!-- No Results -->
|
<!-- No Results -->
|
||||||
<div v-if="!isSearching && results.length === 0 && searchTime !== null" class="alert mt-4">
|
<div v-if="!isSearching && results.length === 0 && searchTime !== null" class="alert mt-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<p>No results found for "{{ searchTerm }}" across all mount points</p>
|
<p>No results found for "{{ searchTerm }}" across all mount points</p>
|
||||||
@ -180,17 +177,22 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Tips -->
|
<!-- Search Tips -->
|
||||||
<div class="alert alert-info mt-4">
|
<div class="alert mt-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<h4 class="font-bold">ℹ️ Search Tips</h4>
|
<h4 class="font-bold flex items-center gap-2">
|
||||||
|
<i class="mdi mdi-information text-info" />
|
||||||
|
Search Tips
|
||||||
|
</h4>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
<li>Search is case-insensitive and matches partial paths</li>
|
<li>Search is case-insensitive and matches partial paths. Search is cached to improve performance</li>
|
||||||
<li>Searches across all detected KV secret engines automatically</li>
|
<li>
|
||||||
<li>Directory listings are cached to improve performance</li>
|
Directories are marked with
|
||||||
<li>Directories are marked with 📁, secrets with 📄</li>
|
<i class="mdi mdi-folder text-warning" />, secrets with
|
||||||
|
<i class="mdi mdi-file-document text-info" />
|
||||||
|
</li>
|
||||||
<li>Click "View" on secrets to open detailed modal with metadata</li>
|
<li>Click "View" on secrets to open detailed modal with metadata</li>
|
||||||
<li>Maximum search depth and results can be configured in settings</li>
|
<li>Maximum search depth and results can be configured in settings</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -199,4 +201,3 @@ const handleKeyPress = (event: KeyboardEvent) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,49 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import type { VaultServer, VaultCredentials } from "../types";
|
import type { VaultServer, VaultCredentials } from '../types'
|
||||||
import { vaultApi, VaultError } from "../services/vaultApi";
|
import { vaultApi, VaultError } from '../services/vaultApi'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
server: VaultServer;
|
server: VaultServer
|
||||||
credentials: VaultCredentials;
|
credentials: VaultCredentials
|
||||||
secretPath: string;
|
secretPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: [];
|
close: []
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const secretData = ref<Record<string, unknown> | null>(null);
|
const secretData = ref<Record<string, unknown> | null>(null)
|
||||||
const secretMetadata = ref<any>(null);
|
const secretVersion = ref<number>(null)
|
||||||
const secretVersions = ref<any[]>([]);
|
const secretMetadata = ref<any>(null)
|
||||||
const isLoading = ref(false);
|
const secretVersions = ref<any[]>([])
|
||||||
const error = ref<string | null>(null);
|
const isLoading = ref(false)
|
||||||
const activeTab = ref<"current" | "json" | "metadata" | "versions">("current");
|
const error = ref<string | null>(null)
|
||||||
const visibleValues = ref<Record<string, boolean>>({});
|
const activeTab = ref<'current' | 'json' | 'metadata' | 'versions'>('current')
|
||||||
|
const visibleValues = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSecret();
|
loadSecret()
|
||||||
});
|
})
|
||||||
|
|
||||||
|
const isLatestVersion = computed<boolean>(() => {
|
||||||
|
if (!secretVersion.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!secretMetadata.value.current_version) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return secretMetadata.value.current_version == secretVersion.value
|
||||||
|
})
|
||||||
|
|
||||||
const loadSecret = async () => {
|
const loadSecret = async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
error.value = null;
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load current secret data
|
// Load current secret data
|
||||||
const response = await vaultApi.readSecret(
|
const response = await vaultApi.readSecret(props.server, props.credentials, props.secretPath)
|
||||||
props.server,
|
|
||||||
props.credentials,
|
|
||||||
props.secretPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Secret response structure:", response);
|
|
||||||
|
|
||||||
// For KV v2, the response includes both data and metadata
|
// For KV v2, the response includes both data and metadata
|
||||||
if (response && typeof response === "object") {
|
if (response && typeof response === 'object') {
|
||||||
// Extract secret data (usually under 'data' key)
|
// Extract secret data (usually under 'data' key)
|
||||||
secretData.value = response.data || response;
|
secretData.value = response.data || response
|
||||||
|
|
||||||
// Extract metadata if present in the response
|
// Extract metadata if present in the response
|
||||||
if (response.metadata) {
|
if (response.metadata) {
|
||||||
@ -56,60 +61,57 @@ const loadSecret = async () => {
|
|||||||
destroyed: response.metadata.destroyed,
|
destroyed: response.metadata.destroyed,
|
||||||
deletion_time: response.metadata.deletion_time,
|
deletion_time: response.metadata.deletion_time,
|
||||||
custom_metadata: response.metadata.custom_metadata,
|
custom_metadata: response.metadata.custom_metadata,
|
||||||
};
|
}
|
||||||
|
|
||||||
|
secretVersion.value = response.metadata.version
|
||||||
|
|
||||||
// Create a single version entry from the current metadata
|
// Create a single version entry from the current metadata
|
||||||
if (response.metadata.version) {
|
if (response.metadata.version) {
|
||||||
secretVersions.value = [
|
secretVersions.value = [
|
||||||
{
|
{
|
||||||
version: response.metadata.version,
|
version: response.metadata.version,
|
||||||
created_time: new Date(
|
created_time: new Date(response.metadata.created_time).toLocaleString(),
|
||||||
response.metadata.created_time,
|
|
||||||
).toLocaleString(),
|
|
||||||
destroyed: response.metadata.destroyed,
|
destroyed: response.metadata.destroyed,
|
||||||
deletion_time: response.metadata.deletion_time,
|
deletion_time: response.metadata.deletion_time,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
secretData.value = response;
|
secretData.value = response
|
||||||
|
secretVersion.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load full metadata and version history from metadata endpoint
|
// Try to load full metadata and version history from metadata endpoint
|
||||||
await loadMetadataAndVersions();
|
await loadMetadataAndVersions()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading secret:", err);
|
console.error('Error loading secret:', err)
|
||||||
if (err instanceof VaultError) {
|
if (err instanceof VaultError) {
|
||||||
error.value = `${err.message} (HTTP ${err.statusCode || "Unknown"})`;
|
error.value = `${err.message} (HTTP ${err.statusCode || 'Unknown'})`
|
||||||
if (err.errors && err.errors.length > 0) {
|
if (err.errors && err.errors.length > 0) {
|
||||||
error.value += `\n\nDetails:\n${err.errors.join("\n")}`;
|
error.value += `\n\nDetails:\n${err.errors.join('\n')}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = err instanceof Error ? err.message : "Unknown error";
|
error.value = err instanceof Error ? err.message : 'Unknown error'
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const loadMetadataAndVersions = async () => {
|
const loadMetadataAndVersions = async () => {
|
||||||
try {
|
try {
|
||||||
// Use the dedicated readSecretMetadata method from VaultApi
|
// Use the dedicated readSecretMetadata method from VaultApi
|
||||||
const fullMetadata = await vaultApi.readSecretMetadata(
|
const fullMetadata = await vaultApi.readSecretMetadata(props.server, props.credentials, props.secretPath)
|
||||||
props.server,
|
|
||||||
props.credentials,
|
|
||||||
props.secretPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fullMetadata) {
|
if (fullMetadata) {
|
||||||
console.log("Full metadata response:", fullMetadata);
|
console.log('Full metadata response:', fullMetadata)
|
||||||
|
|
||||||
// Merge with existing metadata or replace it
|
// Merge with existing metadata or replace it
|
||||||
secretMetadata.value = {
|
secretMetadata.value = {
|
||||||
...secretMetadata.value, // Keep any metadata from the secret response
|
...secretMetadata.value, // Keep any metadata from the secret response
|
||||||
...fullMetadata, // Override with full metadata
|
...fullMetadata, // Override with full metadata
|
||||||
};
|
}
|
||||||
|
|
||||||
// Extract complete version history from full metadata
|
// Extract complete version history from full metadata
|
||||||
if (fullMetadata.versions) {
|
if (fullMetadata.versions) {
|
||||||
@ -119,97 +121,81 @@ const loadMetadataAndVersions = async () => {
|
|||||||
...versionData,
|
...versionData,
|
||||||
created_time: new Date(versionData.created_time).toLocaleString(),
|
created_time: new Date(versionData.created_time).toLocaleString(),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.version - a.version); // Latest first
|
.sort((a, b) => b.version - a.version) // Latest first
|
||||||
} else if (secretMetadata.value?.current_version) {
|
} else if (secretMetadata.value?.current_version) {
|
||||||
// Fallback: if no versions array but we have current version info
|
// Fallback: if no versions array but we have current version info
|
||||||
secretVersions.value = [
|
secretVersions.value = [
|
||||||
{
|
{
|
||||||
version: secretMetadata.value.current_version,
|
version: secretMetadata.value.current_version,
|
||||||
created_time: secretMetadata.value.created_time
|
created_time: secretMetadata.value.created_time ? new Date(secretMetadata.value.created_time).toLocaleString() : 'Unknown',
|
||||||
? new Date(secretMetadata.value.created_time).toLocaleString()
|
|
||||||
: "Unknown",
|
|
||||||
destroyed: secretMetadata.value.destroyed || false,
|
destroyed: secretMetadata.value.destroyed || false,
|
||||||
deletion_time: secretMetadata.value.deletion_time,
|
deletion_time: secretMetadata.value.deletion_time,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn('Could not load full metadata (using basic metadata from secret response):', err)
|
||||||
"Could not load full metadata (using basic metadata from secret response):",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
// If we can't load full metadata, we'll use what we extracted from the secret response
|
// If we can't load full metadata, we'll use what we extracted from the secret response
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const loadVersion = async (version: number) => {
|
const loadVersion = async (version: number) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true
|
||||||
error.value = null;
|
error.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// For KV v2, append ?version=X to get specific version
|
// For KV v2, append ?version=X to get specific version
|
||||||
const versionPath = `${props.secretPath}?version=${version}`;
|
const versionPath = `${props.secretPath}?version=${version}`
|
||||||
const data = await vaultApi.readSecret(
|
const data = await vaultApi.readSecret(props.server, props.credentials, versionPath)
|
||||||
props.server,
|
secretData.value = data
|
||||||
props.credentials,
|
secretVersion.value = version
|
||||||
versionPath,
|
activeTab.value = 'current'
|
||||||
);
|
|
||||||
secretData.value = data;
|
|
||||||
activeTab.value = "current";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading version:", err);
|
console.error('Error loading version:', err)
|
||||||
error.value = err instanceof Error ? err.message : "Unknown error";
|
error.value = err instanceof Error ? err.message : 'Unknown error'
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
|
||||||
if (bytes === 0) return "0 Bytes";
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text)
|
||||||
// Could add a toast notification here
|
// Could add a toast notification here
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to copy:", err);
|
console.error('Failed to copy:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const toggleValueVisibility = (key: string) => {
|
const toggleValueVisibility = (key: string) => {
|
||||||
visibleValues.value[key] = !visibleValues.value[key];
|
visibleValues.value[key] = !visibleValues.value[key]
|
||||||
};
|
}
|
||||||
|
|
||||||
const isValueVisible = (key: string): boolean => {
|
const isValueVisible = (key: string): boolean => {
|
||||||
return visibleValues.value[key] || false;
|
return visibleValues.value[key] || false
|
||||||
};
|
}
|
||||||
|
|
||||||
const maskValue = (value: string): string => {
|
const maskValue = (value: string): string => {
|
||||||
return "•".repeat(Math.min(value.length, 12));
|
return '•'.repeat(Math.min(value.length, 12))
|
||||||
};
|
}
|
||||||
|
|
||||||
const getDisplayValue = (key: string, value: unknown): string => {
|
const getDisplayValue = (key: string, value: unknown): string => {
|
||||||
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
||||||
return isValueVisible(key) ? stringValue : maskValue(stringValue);
|
return isValueVisible(key) ? stringValue : maskValue(stringValue)
|
||||||
};
|
}
|
||||||
|
|
||||||
const toggleAllValues = () => {
|
const toggleAllValues = () => {
|
||||||
if (!secretData.value) return;
|
if (!secretData.value) return
|
||||||
|
|
||||||
// Check if any values are currently visible
|
// Check if any values are currently visible
|
||||||
const hasVisibleValues = Object.values(visibleValues.value).some((v) => v);
|
const hasVisibleValues = Object.values(visibleValues.value).some(v => v)
|
||||||
|
|
||||||
// Set all keys to the opposite state
|
// Set all keys to the opposite state
|
||||||
Object.keys(secretData.value).forEach((key) => {
|
Object.keys(secretData.value).forEach(key => {
|
||||||
visibleValues.value[key] = !hasVisibleValues;
|
visibleValues.value[key] = !hasVisibleValues
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -219,23 +205,23 @@ const toggleAllValues = () => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-start mb-4 flex-shrink-0">
|
<div class="flex justify-between items-start mb-4 flex-shrink-0">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h2 class="text-xl font-bold truncate">🔐 Secret Viewer</h2>
|
<h2 class="text-xl font-bold truncate flex items-center gap-2">
|
||||||
<p class="text-sm font-mono opacity-70 truncate mt-1">
|
<i class="mdi mdi-lock text-primary" />
|
||||||
{{ secretPath }}
|
{{ secretPath }}
|
||||||
</p>
|
<span v-if="isLatestVersion" class="badge badge-success">Latest</span>
|
||||||
|
<span v-else class="badge badge-warning">Version: {{ secretVersion }}</span>
|
||||||
|
</h2>
|
||||||
|
<div v-if="!isLatestVersion" class="alert alert-warning mt-4">This is not the latest version of this secret!</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="btn btn-sm btn-circle btn-ghost ml-4" @click="emit('close')">
|
||||||
class="btn btn-sm btn-circle btn-ghost ml-4"
|
<i class="mdi mdi-close" />
|
||||||
@click="emit('close')"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg" />
|
||||||
<p class="mt-4">Loading secret...</p>
|
<p class="mt-4">Loading secret...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -243,12 +229,7 @@ const toggleAllValues = () => {
|
|||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div v-else-if="error" class="flex-1">
|
<div v-else-if="error" class="flex-1">
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<svg
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
@ -267,35 +248,21 @@ const toggleAllValues = () => {
|
|||||||
<div v-else class="flex-1 flex flex-col overflow-hidden">
|
<div v-else class="flex-1 flex flex-col overflow-hidden">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-bordered mb-4 flex-shrink-0">
|
<div class="tabs tabs-bordered mb-4 flex-shrink-0">
|
||||||
<button
|
<button class="tab" :class="{ 'tab-active': activeTab === 'current' }" @click="activeTab = 'current'">
|
||||||
class="tab"
|
<i class="mdi mdi-table mr-2" />
|
||||||
:class="{ 'tab-active': activeTab === 'current' }"
|
Current Data
|
||||||
@click="activeTab = 'current'"
|
|
||||||
>
|
|
||||||
📄 Current Data
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="tab" :class="{ 'tab-active': activeTab === 'json' }" @click="activeTab = 'json'">
|
||||||
class="tab"
|
<i class="mdi mdi-code-json mr-2" />
|
||||||
:class="{ 'tab-active': activeTab === 'json' }"
|
JSON Data
|
||||||
@click="activeTab = 'json'"
|
|
||||||
>
|
|
||||||
📋 JSON Data
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button v-if="secretMetadata" class="tab" :class="{ 'tab-active': activeTab === 'metadata' }" @click="activeTab = 'metadata'">
|
||||||
v-if="secretMetadata"
|
<i class="mdi mdi-information mr-2" />
|
||||||
class="tab"
|
Metadata
|
||||||
:class="{ 'tab-active': activeTab === 'metadata' }"
|
|
||||||
@click="activeTab = 'metadata'"
|
|
||||||
>
|
|
||||||
ℹ️ Metadata
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button v-if="secretVersions.length > 0" class="tab" :class="{ 'tab-active': activeTab === 'versions' }" @click="activeTab = 'versions'">
|
||||||
v-if="secretVersions.length > 0"
|
<i class="mdi mdi-history mr-2" />
|
||||||
class="tab"
|
Versions ({{ secretVersions.length }})
|
||||||
:class="{ 'tab-active': activeTab === 'versions' }"
|
|
||||||
@click="activeTab = 'versions'"
|
|
||||||
>
|
|
||||||
🕒 Versions ({{ secretVersions.length }})
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -307,19 +274,13 @@ const toggleAllValues = () => {
|
|||||||
<h3 class="font-semibold">Secret Data</h3>
|
<h3 class="font-semibold">Secret Data</h3>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="btn btn-sm btn-outline" @click="toggleAllValues">
|
<button class="btn btn-sm btn-outline" @click="toggleAllValues">
|
||||||
{{
|
<i :class="Object.values(visibleValues).some(v => v) ? 'mdi mdi-eye-off' : 'mdi mdi-eye'" class="mr-2" />
|
||||||
Object.values(visibleValues).some((v) => v)
|
{{ Object.values(visibleValues).some(v => v) ? 'Hide All' : 'Show All' }}
|
||||||
? "🙈 Hide All"
|
|
||||||
: "👁️ Show All"
|
|
||||||
}}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<div
|
<div v-if="secretData && Object.keys(secretData).length > 0" class="overflow-x-auto">
|
||||||
v-if="secretData && Object.keys(secretData).length > 0"
|
|
||||||
class="overflow-x-auto"
|
|
||||||
>
|
|
||||||
<table class="table table-zebra w-full">
|
<table class="table table-zebra w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -329,39 +290,28 @@ const toggleAllValues = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="[key, value] in Object.entries(secretData)" :key="key">
|
||||||
v-for="[key, value] in Object.entries(secretData)"
|
<td class="font-mono font-semibold">
|
||||||
:key="key"
|
{{ key }}
|
||||||
>
|
</td>
|
||||||
<td class="font-mono font-semibold">{{ key }}</td>
|
|
||||||
<td class="font-mono text-sm">
|
<td class="font-mono text-sm">
|
||||||
<span class="select-all">{{
|
<span class="select-all">{{ getDisplayValue(key, value) }}</span>
|
||||||
getDisplayValue(key, value)
|
|
||||||
}}</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-ghost"
|
class="btn btn-ghost"
|
||||||
:title="
|
:title="isValueVisible(key) ? 'Hide value' : 'Show value'"
|
||||||
isValueVisible(key) ? 'Hide value' : 'Show value'
|
|
||||||
"
|
|
||||||
@click="toggleValueVisibility(key)"
|
@click="toggleValueVisibility(key)"
|
||||||
>
|
>
|
||||||
{{ isValueVisible(key) ? "🙈" : "👁️" }}
|
<i :class="isValueVisible(key) ? 'mdi mdi-eye-off' : 'mdi mdi-eye'" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-ghost"
|
class="btn btn-ghost"
|
||||||
title="Copy value"
|
title="Copy value"
|
||||||
@click="
|
@click="copyToClipboard(typeof value === 'string' ? value : JSON.stringify(value))"
|
||||||
copyToClipboard(
|
|
||||||
typeof value === 'string'
|
|
||||||
? value
|
|
||||||
: JSON.stringify(value),
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
📋
|
<i class="mdi mdi-content-copy" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -369,10 +319,7 @@ const toggleAllValues = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="flex items-center justify-center h-full text-base-content/60">
|
||||||
v-else
|
|
||||||
class="flex items-center justify-center h-full text-base-content/60"
|
|
||||||
>
|
|
||||||
<p>No secret data available</p>
|
<p>No secret data available</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -382,26 +329,18 @@ const toggleAllValues = () => {
|
|||||||
<div v-else-if="activeTab === 'json'" class="h-full flex flex-col">
|
<div v-else-if="activeTab === 'json'" class="h-full flex flex-col">
|
||||||
<div class="flex justify-between items-center mb-3 flex-shrink-0">
|
<div class="flex justify-between items-center mb-3 flex-shrink-0">
|
||||||
<h3 class="font-semibold">JSON Data</h3>
|
<h3 class="font-semibold">JSON Data</h3>
|
||||||
<button
|
<button class="btn btn-sm btn-outline" @click="copyToClipboard(JSON.stringify(secretData, null, 2))">
|
||||||
class="btn btn-sm btn-outline"
|
<i class="mdi mdi-content-copy mr-2" />
|
||||||
@click="copyToClipboard(JSON.stringify(secretData, null, 2))"
|
Copy JSON
|
||||||
>
|
|
||||||
📋 Copy JSON
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<pre
|
<pre class="bg-base-300 p-4 rounded-lg text-sm h-full overflow-auto">{{ JSON.stringify(secretData, null, 2) }}</pre>
|
||||||
class="bg-base-300 p-4 rounded-lg text-sm h-full overflow-auto"
|
|
||||||
>{{ JSON.stringify(secretData, null, 2) }}</pre
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metadata Tab -->
|
<!-- Metadata Tab -->
|
||||||
<div
|
<div v-else-if="activeTab === 'metadata' && secretMetadata" class="h-full flex flex-col">
|
||||||
v-else-if="activeTab === 'metadata' && secretMetadata"
|
|
||||||
class="h-full flex flex-col"
|
|
||||||
>
|
|
||||||
<h3 class="font-semibold mb-3 flex-shrink-0">Secret Metadata</h3>
|
<h3 class="font-semibold mb-3 flex-shrink-0">Secret Metadata</h3>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
@ -411,35 +350,23 @@ const toggleAllValues = () => {
|
|||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<strong>Current Version:</strong>
|
<strong>Current Version:</strong>
|
||||||
{{ secretMetadata.current_version || "N/A" }}
|
{{ secretMetadata.current_version || 'N/A' }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Max Versions:</strong>
|
<strong>Max Versions:</strong>
|
||||||
{{ secretMetadata.max_versions || "N/A" }}
|
{{ secretMetadata.max_versions || 'N/A' }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Oldest Version:</strong>
|
<strong>Oldest Version:</strong>
|
||||||
{{ secretMetadata.oldest_version || "N/A" }}
|
{{ secretMetadata.oldest_version || 'N/A' }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Created:</strong>
|
<strong>Created:</strong>
|
||||||
{{
|
{{ secretMetadata.created_time ? new Date(secretMetadata.created_time).toLocaleString() : 'N/A' }}
|
||||||
secretMetadata.created_time
|
|
||||||
? new Date(
|
|
||||||
secretMetadata.created_time,
|
|
||||||
).toLocaleString()
|
|
||||||
: "N/A"
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Updated:</strong>
|
<strong>Updated:</strong>
|
||||||
{{
|
{{ secretMetadata.updated_time ? new Date(secretMetadata.updated_time).toLocaleString() : 'N/A' }}
|
||||||
secretMetadata.updated_time
|
|
||||||
? new Date(
|
|
||||||
secretMetadata.updated_time,
|
|
||||||
).toLocaleString()
|
|
||||||
: "N/A"
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -451,21 +378,15 @@ const toggleAllValues = () => {
|
|||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<strong>Destroyed:</strong>
|
<strong>Destroyed:</strong>
|
||||||
{{ secretMetadata.destroyed ? "Yes" : "No" }}
|
{{ secretMetadata.destroyed ? 'Yes' : 'No' }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Delete Version After:</strong>
|
<strong>Delete Version After:</strong>
|
||||||
{{ secretMetadata.delete_version_after || "Never" }}
|
{{ secretMetadata.delete_version_after || 'Never' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="secretMetadata.custom_metadata">
|
<div v-if="secretMetadata.custom_metadata">
|
||||||
<strong>Custom Metadata:</strong>
|
<strong>Custom Metadata:</strong>
|
||||||
<pre class="text-xs mt-1 bg-base-300 p-2 rounded">{{
|
<pre class="text-xs mt-1 bg-base-300 p-2 rounded">{{ JSON.stringify(secretMetadata.custom_metadata, null, 2) }}</pre>
|
||||||
JSON.stringify(
|
|
||||||
secretMetadata.custom_metadata,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
}}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -475,64 +396,32 @@ const toggleAllValues = () => {
|
|||||||
<div class="card bg-base-200">
|
<div class="card bg-base-200">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<h4 class="font-semibold text-sm mb-2">Raw Metadata</h4>
|
<h4 class="font-semibold text-sm mb-2">Raw Metadata</h4>
|
||||||
<pre class="bg-base-300 p-4 rounded text-xs overflow-auto">{{
|
<pre class="bg-base-300 p-4 rounded text-xs overflow-auto">{{ JSON.stringify(secretMetadata, null, 2) }}</pre>
|
||||||
JSON.stringify(secretMetadata, null, 2)
|
|
||||||
}}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Versions Tab -->
|
<!-- Versions Tab -->
|
||||||
<div
|
<div v-else-if="activeTab === 'versions'" class="h-full flex flex-col">
|
||||||
v-else-if="activeTab === 'versions'"
|
|
||||||
class="h-full flex flex-col"
|
|
||||||
>
|
|
||||||
<h3 class="font-semibold mb-3 flex-shrink-0">Version History</h3>
|
<h3 class="font-semibold mb-3 flex-shrink-0">Version History</h3>
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="version in secretVersions" :key="version.version" class="card bg-base-200 hover:bg-base-300 transition-colors">
|
||||||
v-for="version in secretVersions"
|
<div class="card-body p-4 flex flex-row items-center justify-between">
|
||||||
:key="version.version"
|
|
||||||
class="card bg-base-200 hover:bg-base-300 transition-colors"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="card-body p-4 flex flex-row items-center justify-between"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<span class="badge badge-primary"
|
<span class="badge badge-primary">v{{ version.version }}</span>
|
||||||
>v{{ version.version }}</span
|
<span v-if="version.version === secretMetadata?.current_version" class="badge badge-success">Current</span>
|
||||||
>
|
<span v-if="version.destroyed" class="badge badge-error">Destroyed</span>
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
version.version === secretMetadata?.current_version
|
|
||||||
"
|
|
||||||
class="badge badge-success"
|
|
||||||
>Current</span
|
|
||||||
>
|
|
||||||
<span v-if="version.destroyed" class="badge badge-error"
|
|
||||||
>Destroyed</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm opacity-70">
|
<p class="text-sm opacity-70">Created: {{ version.created_time }}</p>
|
||||||
Created: {{ version.created_time }}
|
<p v-if="version.deletion_time" class="text-sm opacity-70">
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-if="version.deletion_time"
|
|
||||||
class="text-sm opacity-70"
|
|
||||||
>
|
|
||||||
Deleted:
|
Deleted:
|
||||||
{{ new Date(version.deletion_time).toLocaleString() }}
|
{{ new Date(version.deletion_time).toLocaleString() }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button v-if="!version.destroyed" class="btn btn-sm btn-primary" @click="loadVersion(version.version)">View Version</button>
|
||||||
v-if="!version.destroyed"
|
|
||||||
class="btn btn-sm btn-primary"
|
|
||||||
@click="loadVersion(version.version)"
|
|
||||||
>
|
|
||||||
View Version
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -544,8 +433,8 @@ const toggleAllValues = () => {
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="modal-action flex-shrink-0">
|
<div class="modal-action flex-shrink-0">
|
||||||
<div class="flex-1 text-xs opacity-70">
|
<div class="flex-1 text-xs opacity-70">
|
||||||
<p>🔒 Secret data is never cached - always fetched fresh</p>
|
<p><i class="mdi mdi-lock mr-1" />Secret data is never cached - always fetched fresh</p>
|
||||||
<p>📊 KV v2: Metadata and version history available</p>
|
<p><i class="mdi mdi-chart-line mr-1" />KV v2: Metadata and version history available</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" @click="emit('close')">Close</button>
|
<button class="btn" @click="emit('close')">Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,60 +49,39 @@ const handleRemove = (serverId: string, serverName: string) => {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="card-title text-2xl">Vault Servers</h2>
|
<h2 class="card-title text-2xl">Vault Servers</h2>
|
||||||
<button
|
<button class="btn btn-primary btn-sm" @click="showAddForm = !showAddForm">
|
||||||
class="btn btn-primary btn-sm"
|
<i :class="showAddForm ? 'mdi mdi-close' : 'mdi mdi-plus'" class="mr-2" />
|
||||||
@click="showAddForm = !showAddForm"
|
{{ showAddForm ? 'Cancel' : 'Add Server' }}
|
||||||
>
|
|
||||||
{{ showAddForm ? 'Cancel' : '+ Add Server' }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Server Form -->
|
<!-- Add Server Form -->
|
||||||
<div v-if="showAddForm" class="bg-base-200 p-4 rounded-lg mb-4">
|
<div v-if="showAddForm" class="bg-base-200 p-4 rounded-lg mb-4">
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-4">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Server Name *</span>
|
<span class="label-text">Server Name *</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model="newServer.name" type="text" placeholder="Production Vault" class="input input-bordered w-full" required />
|
||||||
v-model="newServer.name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Production Vault"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Server URL *</span>
|
<span class="label-text">Server URL *</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model="newServer.url" type="url" placeholder="https://vault.example.com" class="input input-bordered w-full" required />
|
||||||
v-model="newServer.url"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://vault.example.com"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Description</span>
|
<span class="label-text">Description</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model="newServer.description" type="text" placeholder="Optional description" class="input input-bordered w-full" />
|
||||||
v-model="newServer.description"
|
|
||||||
type="text"
|
|
||||||
placeholder="Optional description"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KV v2 is enforced - no version selection needed -->
|
<!-- KV v2 is enforced - no version selection needed -->
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success w-full">
|
<button type="submit" class="btn btn-success w-full">Add Server</button>
|
||||||
Add Server
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -122,28 +101,27 @@ const handleRemove = (serverId: string, serverName: string) => {
|
|||||||
>
|
>
|
||||||
<div class="card-body p-4 flex flex-row justify-between items-center">
|
<div class="card-body p-4 flex flex-row justify-between items-center">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="font-bold text-lg">{{ server.name }}</h3>
|
<h3 class="font-bold text-lg">
|
||||||
<p class="text-sm font-mono opacity-70 mt-1">{{ server.url }}</p>
|
{{ server.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm font-mono opacity-70 mt-1">
|
||||||
|
{{ server.url }}
|
||||||
|
</p>
|
||||||
<p v-if="server.description" class="text-sm italic opacity-60 mt-1">
|
<p v-if="server.description" class="text-sm italic opacity-60 mt-1">
|
||||||
{{ server.description }}
|
{{ server.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 flex gap-2 flex-wrap">
|
<div class="mt-2 flex gap-2 flex-wrap">
|
||||||
<span class="badge badge-sm badge-outline">KV v2</span>
|
<span class="badge badge-sm badge-outline">KV v2</span>
|
||||||
<span v-if="server.savedCredentials" class="badge badge-sm badge-warning">
|
<span v-if="server.savedCredentials" class="badge badge-sm badge-warning">
|
||||||
🔓 Saved Credentials
|
<i class="mdi mdi-lock-open mr-1" />
|
||||||
|
Saved Credentials
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="btn btn-error btn-sm" @click.stop="handleRemove(server.id, server.name)">Remove</button>
|
||||||
class="btn btn-error btn-sm"
|
|
||||||
@click.stop="handleRemove(server.id, server.name)"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -56,19 +56,16 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Modal Overlay -->
|
<!-- Modal Overlay -->
|
||||||
<div
|
<div class="modal modal-open" @click.self="emit('close')">
|
||||||
class="modal modal-open"
|
|
||||||
@click.self="emit('close')"
|
|
||||||
>
|
|
||||||
<div class="modal-box max-w-3xl max-h-[90vh] overflow-y-auto">
|
<div class="modal-box max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="text-2xl font-bold">⚙️ Settings</h2>
|
<h2 class="text-2xl font-bold flex items-center gap-2">
|
||||||
<button
|
<i class="mdi mdi-cog text-primary" />
|
||||||
class="btn btn-sm btn-circle btn-ghost"
|
Settings
|
||||||
@click="emit('close')"
|
</h2>
|
||||||
>
|
<button class="btn btn-sm btn-circle btn-ghost" @click="emit('close')">
|
||||||
✕
|
<i class="mdi mdi-close" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -79,11 +76,7 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
<!-- Enable Cache -->
|
<!-- Enable Cache -->
|
||||||
<div class="form-control mb-4">
|
<div class="form-control mb-4">
|
||||||
<label class="label cursor-pointer justify-start gap-3">
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
<input
|
<input v-model="config.cache.enabled" type="checkbox" class="checkbox checkbox-primary" />
|
||||||
v-model="config.cache.enabled"
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-primary"
|
|
||||||
/>
|
|
||||||
<span class="label-text">Enable cache</span>
|
<span class="label-text">Enable cache</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="text-sm opacity-70 ml-8">Cache API responses to reduce load on Vault server</p>
|
<p class="text-sm opacity-70 ml-8">Cache API responses to reduce load on Vault server</p>
|
||||||
@ -94,13 +87,7 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Maximum cache size (MB)</span>
|
<span class="label-text">Maximum cache size (MB)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model.number="config.cache.maxSizeMB" type="number" min="1" max="100" class="input input-bordered w-full" />
|
||||||
v-model.number="config.cache.maxSizeMB"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt">Maximum size of cached data in megabytes</span>
|
<span class="label-text-alt">Maximum size of cached data in megabytes</span>
|
||||||
</label>
|
</label>
|
||||||
@ -132,27 +119,30 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p class="opacity-70">Total Size:</p>
|
<p class="opacity-70">Total Size:</p>
|
||||||
<p class="font-mono">{{ formatBytes(cacheStats.totalSize) }}</p>
|
<p class="font-mono">
|
||||||
|
{{ formatBytes(cacheStats.totalSize) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="opacity-70">Entry Count:</p>
|
<p class="opacity-70">Entry Count:</p>
|
||||||
<p class="font-mono">{{ cacheStats.entryCount }}</p>
|
<p class="font-mono">
|
||||||
|
{{ cacheStats.entryCount }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="opacity-70">Oldest Entry:</p>
|
<p class="opacity-70">Oldest Entry:</p>
|
||||||
<p class="font-mono text-xs">{{ formatDate(cacheStats.oldestEntry) }}</p>
|
<p class="font-mono text-xs">
|
||||||
|
{{ formatDate(cacheStats.oldestEntry) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="opacity-70">Newest Entry:</p>
|
<p class="opacity-70">Newest Entry:</p>
|
||||||
<p class="font-mono text-xs">{{ formatDate(cacheStats.newestEntry) }}</p>
|
<p class="font-mono text-xs">
|
||||||
|
{{ formatDate(cacheStats.newestEntry) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="btn btn-error btn-sm mt-4" @click="handleClearCache">Clear Cache</button>
|
||||||
class="btn btn-error btn-sm mt-4"
|
|
||||||
@click="handleClearCache"
|
|
||||||
>
|
|
||||||
Clear Cache
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -166,13 +156,7 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Maximum search depth</span>
|
<span class="label-text">Maximum search depth</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model.number="config.search.maxDepth" type="number" min="1" max="50" class="input input-bordered w-full" />
|
||||||
v-model.number="config.search.maxDepth"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="50"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt">Maximum recursion depth for path searches</span>
|
<span class="label-text-alt">Maximum recursion depth for path searches</span>
|
||||||
</label>
|
</label>
|
||||||
@ -183,13 +167,7 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Maximum search results</span>
|
<span class="label-text">Maximum search results</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input v-model.number="config.search.maxResults" type="number" min="10" max="10000" class="input input-bordered w-full" />
|
||||||
v-model.number="config.search.maxResults"
|
|
||||||
type="number"
|
|
||||||
min="10"
|
|
||||||
max="10000"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text-alt">Maximum number of results to return from a search</span>
|
<span class="label-text-alt">Maximum number of results to return from a search</span>
|
||||||
</label>
|
</label>
|
||||||
@ -198,20 +176,9 @@ const formatDate = (timestamp: number | null): string => {
|
|||||||
|
|
||||||
<!-- Footer Actions -->
|
<!-- Footer Actions -->
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button
|
<button class="btn" @click="emit('close')">Cancel</button>
|
||||||
class="btn"
|
<button class="btn btn-success" @click="handleSave">Save Settings</button>
|
||||||
@click="emit('close')"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-success"
|
|
||||||
@click="handleSave"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
// Application configuration
|
// Application configuration
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
cache: {
|
cache: {
|
||||||
maxSizeMB: number; // Maximum cache size in megabytes
|
maxSizeMB: number // Maximum cache size in megabytes
|
||||||
maxAge: number; // Maximum age of cache entries in milliseconds
|
maxAge: number // Maximum age of cache entries in milliseconds
|
||||||
enabled: boolean;
|
enabled: boolean
|
||||||
};
|
}
|
||||||
search: {
|
search: {
|
||||||
maxDepth: number; // Maximum recursion depth for path search
|
maxDepth: number // Maximum recursion depth for path search
|
||||||
maxResults: number; // Maximum number of results to return
|
maxResults: number // Maximum number of results to return
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@ -22,27 +22,26 @@ export const defaultConfig: AppConfig = {
|
|||||||
maxDepth: 10,
|
maxDepth: 10,
|
||||||
maxResults: 1000,
|
maxResults: 1000,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
// Load configuration from localStorage
|
// Load configuration from localStorage
|
||||||
export function loadConfig(): AppConfig {
|
export function loadConfig(): AppConfig {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('vaultGuiConfig');
|
const saved = localStorage.getItem('vaultGuiConfig')
|
||||||
if (saved) {
|
if (saved) {
|
||||||
return { ...defaultConfig, ...JSON.parse(saved) };
|
return { ...defaultConfig, ...JSON.parse(saved) }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load config:', error);
|
console.error('Failed to load config:', error)
|
||||||
}
|
}
|
||||||
return defaultConfig;
|
return defaultConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save configuration to localStorage
|
// Save configuration to localStorage
|
||||||
export function saveConfig(config: AppConfig): void {
|
export function saveConfig(config: AppConfig): void {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('vaultGuiConfig', JSON.stringify(config));
|
localStorage.setItem('vaultGuiConfig', JSON.stringify(config))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save config:', error);
|
console.error('Failed to save config:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@ -5,4 +5,3 @@ declare module '*.vue' {
|
|||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,4 +3,3 @@ import './style.css'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { VaultServer, VaultCredentials, MountPoint } from '../types';
|
import { VaultServer, VaultCredentials, MountPoint } from '../types'
|
||||||
import { vaultCache } from '../utils/cache';
|
import { vaultCache } from '../utils/cache'
|
||||||
import { loadConfig } from '../config';
|
import { loadConfig } from '../config'
|
||||||
import { VaultClient, VaultError } from './vaultClient';
|
import { VaultClient, VaultError } from './vaultClient'
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
path: string;
|
path: string
|
||||||
isDirectory: boolean;
|
isDirectory: boolean
|
||||||
depth: number;
|
depth: number
|
||||||
mountPoint?: string;
|
mountPoint?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,97 +20,82 @@ class VaultApiService {
|
|||||||
/**
|
/**
|
||||||
* Create a VaultClient instance for the given server and credentials
|
* Create a VaultClient instance for the given server and credentials
|
||||||
*/
|
*/
|
||||||
private createClient(
|
private createClient(server: VaultServer, credentials: VaultCredentials): VaultClient {
|
||||||
server: VaultServer,
|
|
||||||
credentials: VaultCredentials
|
|
||||||
): VaultClient {
|
|
||||||
return new VaultClient({
|
return new VaultClient({
|
||||||
server,
|
server,
|
||||||
credentials,
|
credentials,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
kvVersion: 2, // KV v2 is enforced
|
kvVersion: 2, // KV v2 is enforced
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a cache key for a given operation
|
* Generate a cache key for a given operation
|
||||||
*/
|
*/
|
||||||
private getCacheKey(
|
private getCacheKey(server: VaultServer, path: string, operation: string): string {
|
||||||
server: VaultServer,
|
return `${server.id}:${operation}:${path}`
|
||||||
path: string,
|
|
||||||
operation: string
|
|
||||||
): string {
|
|
||||||
return `${server.id}:${operation}:${path}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List secrets at a given path with caching
|
* List secrets at a given path with caching
|
||||||
*/
|
*/
|
||||||
async listSecrets(
|
async listSecrets(server: VaultServer, credentials: VaultCredentials, path: string): Promise<string[]> {
|
||||||
server: VaultServer,
|
const cacheKey = this.getCacheKey(server, path, 'list')
|
||||||
credentials: VaultCredentials,
|
|
||||||
path: string
|
|
||||||
): Promise<string[]> {
|
|
||||||
const cacheKey = this.getCacheKey(server, path, 'list');
|
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cached = vaultCache.get<string[]>(cacheKey);
|
const cached = vaultCache.get<string[]>(cacheKey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`✓ Cache hit for list: ${path}`);
|
console.log(`✓ Cache hit for list: ${path}`)
|
||||||
return cached;
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`⚡ API call for list: ${path}`);
|
console.log(`⚡ API call for list: ${path}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
const keys = await client.list(path);
|
const keys = await client.list(path)
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
vaultCache.set(cacheKey, keys);
|
vaultCache.set(cacheKey, keys)
|
||||||
|
|
||||||
return keys;
|
return keys
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error(`Vault error listing ${path}:`, error.message);
|
console.error(`Vault error listing ${path}:`, error.message)
|
||||||
if (error.errors) {
|
if (error.errors) {
|
||||||
console.error('Details:', error.errors);
|
console.error('Details:', error.errors)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error listing secrets at ${path}:`, error);
|
console.error(`Error listing secrets at ${path}:`, error)
|
||||||
}
|
}
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a secret from Vault (NO CACHING - secrets are never cached for security)
|
* Read a secret from Vault (NO CACHING - secrets are never cached for security)
|
||||||
*/
|
*/
|
||||||
async readSecret(
|
async readSecret(server: VaultServer, credentials: VaultCredentials, path: string): Promise<Record<string, unknown> | null> {
|
||||||
server: VaultServer,
|
console.log(`⚡ API call for read (no cache): ${path}`)
|
||||||
credentials: VaultCredentials,
|
|
||||||
path: string
|
|
||||||
): Promise<Record<string, unknown> | null> {
|
|
||||||
console.log(`⚡ API call for read (no cache): ${path}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
const secretData = await client.read<Record<string, unknown>>(path);
|
const secretData = await client.read<Record<string, unknown>>(path)
|
||||||
|
|
||||||
// SECURITY: Never cache secret data - always fetch fresh
|
// SECURITY: Never cache secret data - always fetch fresh
|
||||||
return secretData;
|
return secretData
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error(`Vault error reading ${path}:`, error.message);
|
console.error(`Vault error reading ${path}:`, error.message)
|
||||||
if (error.errors) {
|
if (error.errors) {
|
||||||
console.error('Details:', error.errors);
|
console.error('Details:', error.errors)
|
||||||
}
|
}
|
||||||
// Re-throw to let the caller handle it
|
// Re-throw to let the caller handle it
|
||||||
throw error;
|
throw error
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error reading secret at ${path}:`, error);
|
console.error(`Error reading secret at ${path}:`, error)
|
||||||
throw new VaultError('Failed to read secret');
|
throw new VaultError('Failed to read secret')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,29 +103,25 @@ class VaultApiService {
|
|||||||
/**
|
/**
|
||||||
* Read metadata for a secret (KV v2 only)
|
* Read metadata for a secret (KV v2 only)
|
||||||
*/
|
*/
|
||||||
async readSecretMetadata(
|
async readSecretMetadata(server: VaultServer, credentials: VaultCredentials, path: string): Promise<any> {
|
||||||
server: VaultServer,
|
console.log(`⚡ API call for metadata (no cache): ${path}`)
|
||||||
credentials: VaultCredentials,
|
|
||||||
path: string
|
|
||||||
): Promise<any> {
|
|
||||||
console.log(`⚡ API call for metadata (no cache): ${path}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
const metadata = await client.readMetadata(path);
|
const metadata = await client.readMetadata(path)
|
||||||
|
|
||||||
return metadata;
|
return metadata
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error(`Vault error reading metadata ${path}:`, error.message);
|
console.error(`Vault error reading metadata ${path}:`, error.message)
|
||||||
if (error.errors) {
|
if (error.errors) {
|
||||||
console.error('Details:', error.errors);
|
console.error('Details:', error.errors)
|
||||||
}
|
}
|
||||||
// Re-throw to let the caller handle it
|
// Re-throw to let the caller handle it
|
||||||
throw error;
|
throw error
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error reading metadata at ${path}:`, error);
|
console.error(`Error reading metadata at ${path}:`, error)
|
||||||
throw new VaultError('Failed to read metadata');
|
throw new VaultError('Failed to read metadata')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,30 +129,25 @@ class VaultApiService {
|
|||||||
/**
|
/**
|
||||||
* Write a secret to Vault (no caching)
|
* Write a secret to Vault (no caching)
|
||||||
*/
|
*/
|
||||||
async writeSecret(
|
async writeSecret(server: VaultServer, credentials: VaultCredentials, path: string, data: Record<string, unknown>): Promise<void> {
|
||||||
server: VaultServer,
|
console.log(`⚡ API call for write: ${path}`)
|
||||||
credentials: VaultCredentials,
|
|
||||||
path: string,
|
|
||||||
data: Record<string, unknown>
|
|
||||||
): Promise<void> {
|
|
||||||
console.log(`⚡ API call for write: ${path}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
await client.write(path, data);
|
await client.write(path, data)
|
||||||
|
|
||||||
// Invalidate cache for this path
|
// Invalidate cache for this path
|
||||||
const cacheKey = this.getCacheKey(server, path, 'read');
|
const cacheKey = this.getCacheKey(server, path, 'read')
|
||||||
vaultCache.delete(cacheKey);
|
vaultCache.delete(cacheKey)
|
||||||
|
|
||||||
console.log(`✓ Secret written successfully: ${path}`);
|
console.log(`✓ Secret written successfully: ${path}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error(`Vault error writing ${path}:`, error.message);
|
console.error(`Vault error writing ${path}:`, error.message)
|
||||||
throw error;
|
throw error
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error writing secret at ${path}:`, error);
|
console.error(`Error writing secret at ${path}:`, error)
|
||||||
throw new VaultError('Failed to write secret');
|
throw new VaultError('Failed to write secret')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,29 +155,25 @@ class VaultApiService {
|
|||||||
/**
|
/**
|
||||||
* Delete a secret from Vault (no caching)
|
* Delete a secret from Vault (no caching)
|
||||||
*/
|
*/
|
||||||
async deleteSecret(
|
async deleteSecret(server: VaultServer, credentials: VaultCredentials, path: string): Promise<void> {
|
||||||
server: VaultServer,
|
console.log(`⚡ API call for delete: ${path}`)
|
||||||
credentials: VaultCredentials,
|
|
||||||
path: string
|
|
||||||
): Promise<void> {
|
|
||||||
console.log(`⚡ API call for delete: ${path}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
await client.delete(path);
|
await client.delete(path)
|
||||||
|
|
||||||
// Invalidate cache for this path
|
// Invalidate cache for this path
|
||||||
const cacheKey = this.getCacheKey(server, path, 'read');
|
const cacheKey = this.getCacheKey(server, path, 'read')
|
||||||
vaultCache.delete(cacheKey);
|
vaultCache.delete(cacheKey)
|
||||||
|
|
||||||
console.log(`✓ Secret deleted successfully: ${path}`);
|
console.log(`✓ Secret deleted successfully: ${path}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error(`Vault error deleting ${path}:`, error.message);
|
console.error(`Vault error deleting ${path}:`, error.message)
|
||||||
throw error;
|
throw error
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error deleting secret at ${path}:`, error);
|
console.error(`Error deleting secret at ${path}:`, error)
|
||||||
throw new VaultError('Failed to delete secret');
|
throw new VaultError('Failed to delete secret')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,20 +181,17 @@ class VaultApiService {
|
|||||||
/**
|
/**
|
||||||
* Verify login and get available mount points
|
* Verify login and get available mount points
|
||||||
*/
|
*/
|
||||||
async verifyLoginAndGetMounts(
|
async verifyLoginAndGetMounts(server: VaultServer, credentials: VaultCredentials): Promise<MountPoint[]> {
|
||||||
server: VaultServer,
|
console.log('⚡ Verifying login and fetching mount points...')
|
||||||
credentials: VaultCredentials
|
|
||||||
): Promise<MountPoint[]> {
|
|
||||||
console.log('⚡ Verifying login and fetching mount points...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = this.createClient(server, credentials);
|
const client = this.createClient(server, credentials)
|
||||||
const mounts = await client.listMounts();
|
const mounts = await client.listMounts()
|
||||||
|
|
||||||
console.log('📋 Raw mount points from API:', mounts);
|
console.log('📋 Raw mount points from API:', mounts)
|
||||||
|
|
||||||
// Convert to array and filter for KV secret engines
|
// Convert to array and filter for KV secret engines
|
||||||
const mountPoints: MountPoint[] = [];
|
const mountPoints: MountPoint[] = []
|
||||||
|
|
||||||
for (const [path, mount] of Object.entries(mounts)) {
|
for (const [path, mount] of Object.entries(mounts)) {
|
||||||
// Only include KV secret engines
|
// Only include KV secret engines
|
||||||
@ -234,18 +203,21 @@ class VaultApiService {
|
|||||||
accessor: mount.accessor,
|
accessor: mount.accessor,
|
||||||
config: mount.config,
|
config: mount.config,
|
||||||
options: mount.options || {},
|
options: mount.options || {},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ Found ${mountPoints.length} KV mount point(s):`, mountPoints.map(m => `${m.path} (v${m.options?.version || '1'})`));
|
console.log(
|
||||||
return mountPoints;
|
`✓ Found ${mountPoints.length} KV mount point(s):`,
|
||||||
|
mountPoints.map(m => `${m.path} (v${m.options?.version || '1'})`)
|
||||||
|
)
|
||||||
|
return mountPoints
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
console.error('✗ Login verification failed:', error.message);
|
console.error('✗ Login verification failed:', error.message)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
throw new VaultError('Failed to verify login');
|
throw new VaultError('Failed to verify login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,23 +232,23 @@ class VaultApiService {
|
|||||||
currentDepth: number = 0,
|
currentDepth: number = 0,
|
||||||
mountPoint?: string
|
mountPoint?: string
|
||||||
): Promise<SearchResult[]> {
|
): Promise<SearchResult[]> {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
|
|
||||||
// Check depth limit
|
// Check depth limit
|
||||||
if (currentDepth >= config.search.maxDepth) {
|
if (currentDepth >= config.search.maxDepth) {
|
||||||
console.warn(`⚠ Max depth ${config.search.maxDepth} reached at ${basePath}`);
|
console.warn(`⚠ Max depth ${config.search.maxDepth} reached at ${basePath}`)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: SearchResult[] = [];
|
const results: SearchResult[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// List items at current path
|
// List items at current path
|
||||||
const items = await this.listSecrets(server, credentials, basePath);
|
const items = await this.listSecrets(server, credentials, basePath)
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const fullPath = basePath ? `${basePath}${item}` : item;
|
const fullPath = basePath ? `${basePath}${item}` : item
|
||||||
const isDirectory = item.endsWith('/');
|
const isDirectory = item.endsWith('/')
|
||||||
|
|
||||||
// Check if this path matches the search term
|
// Check if this path matches the search term
|
||||||
if (fullPath.toLowerCase().includes(searchTerm.toLowerCase())) {
|
if (fullPath.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||||
@ -285,88 +257,65 @@ class VaultApiService {
|
|||||||
isDirectory,
|
isDirectory,
|
||||||
depth: currentDepth,
|
depth: currentDepth,
|
||||||
mountPoint,
|
mountPoint,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Stop if we've reached max results
|
// Stop if we've reached max results
|
||||||
if (results.length >= config.search.maxResults) {
|
if (results.length >= config.search.maxResults) {
|
||||||
console.warn(
|
console.warn(`⚠ Max results ${config.search.maxResults} reached`)
|
||||||
`⚠ Max results ${config.search.maxResults} reached`
|
return results
|
||||||
);
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a directory, recursively search it
|
// If it's a directory, recursively search it
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
const subResults = await this.searchPaths(
|
const subResults = await this.searchPaths(server, credentials, fullPath, searchTerm, currentDepth + 1, mountPoint)
|
||||||
server,
|
results.push(...subResults)
|
||||||
credentials,
|
|
||||||
fullPath,
|
|
||||||
searchTerm,
|
|
||||||
currentDepth + 1,
|
|
||||||
mountPoint
|
|
||||||
);
|
|
||||||
results.push(...subResults);
|
|
||||||
|
|
||||||
// Stop if we've reached max results
|
// Stop if we've reached max results
|
||||||
if (results.length >= config.search.maxResults) {
|
if (results.length >= config.search.maxResults) {
|
||||||
console.warn(
|
console.warn(`⚠ Max results ${config.search.maxResults} reached`)
|
||||||
`⚠ Max results ${config.search.maxResults} reached`
|
return results.slice(0, config.search.maxResults)
|
||||||
);
|
|
||||||
return results.slice(0, config.search.maxResults);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error searching path ${basePath}:`, error);
|
console.error(`Error searching path ${basePath}:`, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search across all mount points
|
* Search across all mount points
|
||||||
*/
|
*/
|
||||||
async searchAllMounts(
|
async searchAllMounts(server: VaultServer, credentials: VaultCredentials, mountPoints: MountPoint[], searchTerm: string): Promise<SearchResult[]> {
|
||||||
server: VaultServer,
|
console.log(`🔍 Searching across ${mountPoints.length} mount point(s)...`)
|
||||||
credentials: VaultCredentials,
|
|
||||||
mountPoints: MountPoint[],
|
|
||||||
searchTerm: string
|
|
||||||
): Promise<SearchResult[]> {
|
|
||||||
console.log(`🔍 Searching across ${mountPoints.length} mount point(s)...`);
|
|
||||||
|
|
||||||
const allResults: SearchResult[] = [];
|
const allResults: SearchResult[] = []
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
|
|
||||||
for (const mount of mountPoints) {
|
for (const mount of mountPoints) {
|
||||||
console.log(` → Searching in ${mount.path}/`);
|
console.log(` → Searching in ${mount.path}/`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Search this mount point (KV v2 enforced)
|
// Search this mount point (KV v2 enforced)
|
||||||
const results = await this.searchPaths(
|
const results = await this.searchPaths(server, credentials, `${mount.path}/`, searchTerm, 0, mount.path)
|
||||||
server,
|
|
||||||
credentials,
|
|
||||||
`${mount.path}/`,
|
|
||||||
searchTerm,
|
|
||||||
0,
|
|
||||||
mount.path
|
|
||||||
);
|
|
||||||
|
|
||||||
allResults.push(...results);
|
allResults.push(...results)
|
||||||
|
|
||||||
// Stop if we've hit the global max results
|
// Stop if we've hit the global max results
|
||||||
if (allResults.length >= config.search.maxResults) {
|
if (allResults.length >= config.search.maxResults) {
|
||||||
console.warn(`⚠ Max results ${config.search.maxResults} reached`);
|
console.warn(`⚠ Max results ${config.search.maxResults} reached`)
|
||||||
return allResults.slice(0, config.search.maxResults);
|
return allResults.slice(0, config.search.maxResults)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(` ✗ Error searching ${mount.path}:`, error);
|
console.error(` ✗ Error searching ${mount.path}:`, error)
|
||||||
// Continue with other mount points even if one fails
|
// Continue with other mount points even if one fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ Found ${allResults.length} total result(s) across all mounts`);
|
console.log(`✓ Found ${allResults.length} total result(s) across all mounts`)
|
||||||
return allResults;
|
return allResults
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -376,72 +325,58 @@ class VaultApiService {
|
|||||||
try {
|
try {
|
||||||
const client = this.createClient(server, {
|
const client = this.createClient(server, {
|
||||||
serverId: server.id,
|
serverId: server.id,
|
||||||
authMethod: 'token'
|
authMethod: 'token',
|
||||||
});
|
})
|
||||||
const health = await client.health();
|
const health = await client.health()
|
||||||
console.log('✓ Vault server health:', health);
|
console.log('✓ Vault server health:', health)
|
||||||
return health.initialized && !health.sealed;
|
return health.initialized && !health.sealed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('✗ Failed to connect to Vault:', error);
|
console.error('✗ Failed to connect to Vault:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate with username/password
|
* Authenticate with username/password
|
||||||
*/
|
*/
|
||||||
async loginUserpass(
|
async loginUserpass(server: VaultServer, username: string, password: string): Promise<string> {
|
||||||
server: VaultServer,
|
|
||||||
username: string,
|
|
||||||
password: string
|
|
||||||
): Promise<string> {
|
|
||||||
const client = this.createClient(server, {
|
const client = this.createClient(server, {
|
||||||
serverId: server.id,
|
serverId: server.id,
|
||||||
authMethod: 'userpass',
|
authMethod: 'userpass',
|
||||||
});
|
})
|
||||||
return await client.loginUserpass(username, password);
|
return await client.loginUserpass(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate with LDAP
|
* Authenticate with LDAP
|
||||||
*/
|
*/
|
||||||
async loginLdap(
|
async loginLdap(server: VaultServer, username: string, password: string): Promise<string> {
|
||||||
server: VaultServer,
|
|
||||||
username: string,
|
|
||||||
password: string
|
|
||||||
): Promise<string> {
|
|
||||||
const client = this.createClient(server, {
|
const client = this.createClient(server, {
|
||||||
serverId: server.id,
|
serverId: server.id,
|
||||||
authMethod: 'ldap',
|
authMethod: 'ldap',
|
||||||
});
|
})
|
||||||
return await client.loginLdap(username, password);
|
return await client.loginLdap(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current token information
|
* Get current token information
|
||||||
*/
|
*/
|
||||||
async getTokenInfo(
|
async getTokenInfo(server: VaultServer, credentials: VaultCredentials): Promise<unknown> {
|
||||||
server: VaultServer,
|
const client = this.createClient(server, credentials)
|
||||||
credentials: VaultCredentials
|
return await client.tokenLookupSelf()
|
||||||
): Promise<unknown> {
|
|
||||||
const client = this.createClient(server, credentials);
|
|
||||||
return await client.tokenLookupSelf();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke current token (logout)
|
* Revoke current token (logout)
|
||||||
*/
|
*/
|
||||||
async logout(
|
async logout(server: VaultServer, credentials: VaultCredentials): Promise<void> {
|
||||||
server: VaultServer,
|
const client = this.createClient(server, credentials)
|
||||||
credentials: VaultCredentials
|
await client.tokenRevokeSelf()
|
||||||
): Promise<void> {
|
|
||||||
const client = this.createClient(server, credentials);
|
|
||||||
await client.tokenRevokeSelf();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const vaultApi = new VaultApiService();
|
export const vaultApi = new VaultApiService()
|
||||||
|
|
||||||
// Export VaultError for error handling
|
// Export VaultError for error handling
|
||||||
export { VaultError };
|
export { VaultError }
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { VaultServer, VaultCredentials } from '../types';
|
import { VaultServer, VaultCredentials } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for VaultClient
|
* Configuration options for VaultClient
|
||||||
*/
|
*/
|
||||||
export interface VaultClientOptions {
|
export interface VaultClientOptions {
|
||||||
server: VaultServer;
|
server: VaultServer
|
||||||
credentials: VaultCredentials;
|
credentials: VaultCredentials
|
||||||
timeout?: number;
|
timeout?: number
|
||||||
retries?: number;
|
retries?: number
|
||||||
kvVersion?: 1 | 2; // KV secret engine version
|
kvVersion?: 1 | 2 // KV secret engine version
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,8 +20,8 @@ export class VaultError extends Error {
|
|||||||
public statusCode?: number,
|
public statusCode?: number,
|
||||||
public errors?: string[]
|
public errors?: string[]
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message)
|
||||||
this.name = 'VaultError';
|
this.name = 'VaultError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,18 +33,18 @@ export class VaultError extends Error {
|
|||||||
* Supports both KV v1 and KV v2 secret engines.
|
* Supports both KV v1 and KV v2 secret engines.
|
||||||
*/
|
*/
|
||||||
export class VaultClient {
|
export class VaultClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string
|
||||||
private token?: string;
|
private token?: string
|
||||||
private timeout: number;
|
private timeout: number
|
||||||
private retries: number;
|
private retries: number
|
||||||
private kvVersion: 1 | 2;
|
private kvVersion: 1 | 2
|
||||||
|
|
||||||
constructor(options: VaultClientOptions) {
|
constructor(options: VaultClientOptions) {
|
||||||
this.baseUrl = options.server.url.replace(/\/$/, ''); // Remove trailing slash
|
this.baseUrl = options.server.url.replace(/\/$/, '') // Remove trailing slash
|
||||||
this.token = options.credentials.token;
|
this.token = options.credentials.token
|
||||||
this.timeout = options.timeout || 30000; // 30 seconds default
|
this.timeout = options.timeout || 30000 // 30 seconds default
|
||||||
this.retries = options.retries || 2;
|
this.retries = options.retries || 2
|
||||||
this.kvVersion = options.kvVersion || 2; // Default to KV v2 (most common)
|
this.kvVersion = options.kvVersion || 2 // Default to KV v2 (most common)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,126 +52,111 @@ export class VaultClient {
|
|||||||
* KV v2 uses /data/ for reads/writes and /metadata/ for lists
|
* KV v2 uses /data/ for reads/writes and /metadata/ for lists
|
||||||
*/
|
*/
|
||||||
private transformPath(path: string, operation: 'data' | 'metadata' | 'none' = 'none'): string {
|
private transformPath(path: string, operation: 'data' | 'metadata' | 'none' = 'none'): string {
|
||||||
const normalized = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
const normalized = path.replace(/^\/+/, '').replace(/\/+$/, '')
|
||||||
|
|
||||||
if (this.kvVersion === 1) {
|
if (this.kvVersion === 1) {
|
||||||
return normalized;
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
// KV v2 path transformation
|
// KV v2 path transformation
|
||||||
// Check if path already has /data/ or /metadata/
|
// Check if path already has /data/ or /metadata/
|
||||||
if (normalized.includes('/data/') || normalized.includes('/metadata/')) {
|
if (normalized.includes('/data/') || normalized.includes('/metadata/')) {
|
||||||
return normalized;
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
// For KV v2, transform the path
|
// For KV v2, transform the path
|
||||||
const parts = normalized.split('/');
|
const parts = normalized.split('/')
|
||||||
const mount = parts[0]; // e.g., "secret"
|
const mount = parts[0] // e.g., "secret"
|
||||||
const rest = parts.slice(1).join('/');
|
const rest = parts.slice(1).join('/')
|
||||||
|
|
||||||
if (operation === 'data') {
|
if (operation === 'data') {
|
||||||
return `${mount}/data/${rest}`;
|
return `${mount}/data/${rest}`
|
||||||
} else if (operation === 'metadata') {
|
} else if (operation === 'metadata') {
|
||||||
return `${mount}/metadata/${rest}`;
|
return `${mount}/metadata/${rest}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an HTTP request to the Vault API
|
* Make an HTTP request to the Vault API
|
||||||
*/
|
*/
|
||||||
private async request<T>(
|
private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
path: string,
|
const url = `${this.baseUrl}/v1/${path.replace(/^\//, '')}`
|
||||||
options: RequestInit = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const url = `${this.baseUrl}/v1/${path.replace(/^\//, '')}`;
|
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options.headers,
|
...options.headers,
|
||||||
};
|
}
|
||||||
|
|
||||||
// Add authentication token if available
|
// Add authentication token if available
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
headers['X-Vault-Token'] = this.token;
|
headers['X-Vault-Token'] = this.token
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
})
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
// Handle non-OK responses
|
// Handle non-OK responses
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorData: { errors?: string[] } = {};
|
let errorData: { errors?: string[] } = {}
|
||||||
try {
|
try {
|
||||||
errorData = await response.json();
|
errorData = await response.json()
|
||||||
} catch {
|
} catch {
|
||||||
// Response might not be JSON
|
// Response might not be JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new VaultError(
|
throw new VaultError(`Vault API error: ${response.statusText}`, response.status, errorData.errors)
|
||||||
`Vault API error: ${response.statusText}`,
|
|
||||||
response.status,
|
|
||||||
errorData.errors
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle empty responses (e.g., 204 No Content)
|
// Handle empty responses (e.g., 204 No Content)
|
||||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||||
return null as T;
|
return null as T
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (error instanceof VaultError) {
|
if (error instanceof VaultError) {
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
throw new VaultError('Request timeout');
|
throw new VaultError('Request timeout')
|
||||||
}
|
}
|
||||||
throw new VaultError(`Network error: ${error.message}`);
|
throw new VaultError(`Network error: ${error.message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new VaultError('Unknown error occurred');
|
throw new VaultError('Unknown error occurred')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request with automatic retries
|
* Make a request with automatic retries
|
||||||
*/
|
*/
|
||||||
private async requestWithRetry<T>(
|
private async requestWithRetry<T>(path: string, options: RequestInit = {}, attempt = 0): Promise<T> {
|
||||||
path: string,
|
|
||||||
options: RequestInit = {},
|
|
||||||
attempt = 0
|
|
||||||
): Promise<T> {
|
|
||||||
try {
|
try {
|
||||||
return await this.request<T>(path, options);
|
return await this.request<T>(path, options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only retry on network errors, not on 4xx client errors
|
// Only retry on network errors, not on 4xx client errors
|
||||||
if (
|
if (attempt < this.retries && error instanceof VaultError && (!error.statusCode || error.statusCode >= 500)) {
|
||||||
attempt < this.retries &&
|
|
||||||
error instanceof VaultError &&
|
|
||||||
(!error.statusCode || error.statusCode >= 500)
|
|
||||||
) {
|
|
||||||
// Exponential backoff
|
// Exponential backoff
|
||||||
const delay = Math.pow(2, attempt) * 1000;
|
const delay = Math.pow(2, attempt) * 1000
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
return this.requestWithRetry<T>(path, options, attempt + 1);
|
return this.requestWithRetry<T>(path, options, attempt + 1)
|
||||||
}
|
}
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,16 +167,13 @@ export class VaultClient {
|
|||||||
* For KV v1, this uses the path directly
|
* For KV v1, this uses the path directly
|
||||||
*/
|
*/
|
||||||
async list(path: string): Promise<string[]> {
|
async list(path: string): Promise<string[]> {
|
||||||
const normalizedPath = this.transformPath(path, 'metadata');
|
const normalizedPath = this.transformPath(path, 'metadata')
|
||||||
|
|
||||||
// Ensure path ends with / for LIST operations
|
// Ensure path ends with / for LIST operations
|
||||||
const listPath = normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`;
|
const listPath = normalizedPath.endsWith('/') ? normalizedPath : `${normalizedPath}/`
|
||||||
|
|
||||||
const response = await this.requestWithRetry<{ data: { keys: string[] } }>(
|
const response = await this.requestWithRetry<{ data: { keys: string[] } }>(`${listPath}?list=true`, { method: 'LIST' })
|
||||||
`${listPath}?list=true`,
|
return response?.data?.keys || []
|
||||||
{ method: 'LIST' }
|
|
||||||
);
|
|
||||||
return response?.data?.keys || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,21 +183,18 @@ export class VaultClient {
|
|||||||
* For KV v1, this uses the path directly
|
* For KV v1, this uses the path directly
|
||||||
*/
|
*/
|
||||||
async read<T = Record<string, unknown>>(path: string): Promise<T | null> {
|
async read<T = Record<string, unknown>>(path: string): Promise<T | null> {
|
||||||
const normalizedPath = this.transformPath(path, 'data');
|
const normalizedPath = this.transformPath(path, 'data')
|
||||||
|
|
||||||
if (this.kvVersion === 2) {
|
if (this.kvVersion === 2) {
|
||||||
// KV v2 returns { data: { data: {...}, metadata: {...} } }
|
// KV v2 returns { data: { data: {...}, metadata: {...} } }
|
||||||
const response = await this.requestWithRetry<{
|
const response = await this.requestWithRetry<{
|
||||||
data: { data: T; metadata?: unknown };
|
data: { data: T; metadata?: unknown }
|
||||||
}>(normalizedPath, { method: 'GET' });
|
}>(normalizedPath, { method: 'GET' })
|
||||||
return response?.data?.data || null;
|
return response?.data?.data || null
|
||||||
} else {
|
} else {
|
||||||
// KV v1 returns { data: {...} }
|
// KV v1 returns { data: {...} }
|
||||||
const response = await this.requestWithRetry<{ data: T }>(
|
const response = await this.requestWithRetry<{ data: T }>(normalizedPath, { method: 'GET' })
|
||||||
normalizedPath,
|
return response?.data || null
|
||||||
{ method: 'GET' }
|
|
||||||
);
|
|
||||||
return response?.data || null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,18 +204,15 @@ export class VaultClient {
|
|||||||
* For KV v2, this uses the /data/ endpoint
|
* For KV v2, this uses the /data/ endpoint
|
||||||
* For KV v1, this uses the path directly
|
* For KV v1, this uses the path directly
|
||||||
*/
|
*/
|
||||||
async write<T = Record<string, unknown>>(
|
async write<T = Record<string, unknown>>(path: string, data: T): Promise<void> {
|
||||||
path: string,
|
const normalizedPath = this.transformPath(path, 'data')
|
||||||
data: T
|
|
||||||
): Promise<void> {
|
|
||||||
const normalizedPath = this.transformPath(path, 'data');
|
|
||||||
|
|
||||||
const body = this.kvVersion === 2 ? { data } : data;
|
const body = this.kvVersion === 2 ? { data } : data
|
||||||
|
|
||||||
await this.requestWithRetry<void>(normalizedPath, {
|
await this.requestWithRetry<void>(normalizedPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,10 +222,10 @@ export class VaultClient {
|
|||||||
* For KV v1, this uses the path directly (hard delete)
|
* For KV v1, this uses the path directly (hard delete)
|
||||||
*/
|
*/
|
||||||
async delete(path: string): Promise<void> {
|
async delete(path: string): Promise<void> {
|
||||||
const normalizedPath = this.transformPath(path, 'data');
|
const normalizedPath = this.transformPath(path, 'data')
|
||||||
await this.requestWithRetry<void>(normalizedPath, {
|
await this.requestWithRetry<void>(normalizedPath, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -257,51 +233,57 @@ export class VaultClient {
|
|||||||
* Returns version history, created time, etc.
|
* Returns version history, created time, etc.
|
||||||
*/
|
*/
|
||||||
async readMetadata(path: string): Promise<{
|
async readMetadata(path: string): Promise<{
|
||||||
versions: Record<string, {
|
versions: Record<
|
||||||
created_time: string;
|
string,
|
||||||
deletion_time: string;
|
{
|
||||||
destroyed: boolean;
|
created_time: string
|
||||||
}>;
|
deletion_time: string
|
||||||
current_version: number;
|
destroyed: boolean
|
||||||
oldest_version: number;
|
}
|
||||||
created_time: string;
|
>
|
||||||
updated_time: string;
|
current_version: number
|
||||||
|
oldest_version: number
|
||||||
|
created_time: string
|
||||||
|
updated_time: string
|
||||||
} | null> {
|
} | null> {
|
||||||
if (this.kvVersion !== 2) {
|
if (this.kvVersion !== 2) {
|
||||||
throw new VaultError('Metadata is only available in KV v2');
|
throw new VaultError('Metadata is only available in KV v2')
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedPath = this.transformPath(path, 'metadata');
|
const normalizedPath = this.transformPath(path, 'metadata')
|
||||||
const response = await this.requestWithRetry<{
|
const response = await this.requestWithRetry<{
|
||||||
data: {
|
data: {
|
||||||
versions: Record<string, {
|
versions: Record<
|
||||||
created_time: string;
|
string,
|
||||||
deletion_time: string;
|
{
|
||||||
destroyed: boolean;
|
created_time: string
|
||||||
}>;
|
deletion_time: string
|
||||||
current_version: number;
|
destroyed: boolean
|
||||||
oldest_version: number;
|
}
|
||||||
created_time: string;
|
>
|
||||||
updated_time: string;
|
current_version: number
|
||||||
};
|
oldest_version: number
|
||||||
}>(normalizedPath, { method: 'GET' });
|
created_time: string
|
||||||
|
updated_time: string
|
||||||
|
}
|
||||||
|
}>(normalizedPath, { method: 'GET' })
|
||||||
|
|
||||||
return response?.data || null;
|
return response?.data || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get health status of Vault server
|
* Get health status of Vault server
|
||||||
*/
|
*/
|
||||||
async health(): Promise<{
|
async health(): Promise<{
|
||||||
initialized: boolean;
|
initialized: boolean
|
||||||
sealed: boolean;
|
sealed: boolean
|
||||||
standby: boolean;
|
standby: boolean
|
||||||
version: string;
|
version: string
|
||||||
}> {
|
}> {
|
||||||
// Health endpoint doesn't require authentication
|
// Health endpoint doesn't require authentication
|
||||||
const url = `${this.baseUrl}/v1/sys/health`;
|
const url = `${this.baseUrl}/v1/sys/health`
|
||||||
const response = await fetch(url);
|
const response = await fetch(url)
|
||||||
return response.json();
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -309,14 +291,14 @@ export class VaultClient {
|
|||||||
*/
|
*/
|
||||||
async loginUserpass(username: string, password: string): Promise<string> {
|
async loginUserpass(username: string, password: string): Promise<string> {
|
||||||
const response = await this.request<{
|
const response = await this.request<{
|
||||||
auth: { client_token: string };
|
auth: { client_token: string }
|
||||||
}>('auth/userpass/login/' + username, {
|
}>('auth/userpass/login/' + username, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ password }),
|
body: JSON.stringify({ password }),
|
||||||
});
|
})
|
||||||
|
|
||||||
this.token = response.auth.client_token;
|
this.token = response.auth.client_token
|
||||||
return this.token;
|
return this.token
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -324,14 +306,14 @@ export class VaultClient {
|
|||||||
*/
|
*/
|
||||||
async loginLdap(username: string, password: string): Promise<string> {
|
async loginLdap(username: string, password: string): Promise<string> {
|
||||||
const response = await this.request<{
|
const response = await this.request<{
|
||||||
auth: { client_token: string };
|
auth: { client_token: string }
|
||||||
}>('auth/ldap/login/' + username, {
|
}>('auth/ldap/login/' + username, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ password }),
|
body: JSON.stringify({ password }),
|
||||||
});
|
})
|
||||||
|
|
||||||
this.token = response.auth.client_token;
|
this.token = response.auth.client_token
|
||||||
return this.token;
|
return this.token
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -339,28 +321,28 @@ export class VaultClient {
|
|||||||
*/
|
*/
|
||||||
async tokenLookupSelf(): Promise<{
|
async tokenLookupSelf(): Promise<{
|
||||||
data: {
|
data: {
|
||||||
accessor: string;
|
accessor: string
|
||||||
creation_time: number;
|
creation_time: number
|
||||||
creation_ttl: number;
|
creation_ttl: number
|
||||||
display_name: string;
|
display_name: string
|
||||||
entity_id: string;
|
entity_id: string
|
||||||
expire_time: string | null;
|
expire_time: string | null
|
||||||
explicit_max_ttl: number;
|
explicit_max_ttl: number
|
||||||
id: string;
|
id: string
|
||||||
issue_time: string;
|
issue_time: string
|
||||||
meta: Record<string, string>;
|
meta: Record<string, string>
|
||||||
num_uses: number;
|
num_uses: number
|
||||||
orphan: boolean;
|
orphan: boolean
|
||||||
path: string;
|
path: string
|
||||||
policies: string[];
|
policies: string[]
|
||||||
renewable: boolean;
|
renewable: boolean
|
||||||
ttl: number;
|
ttl: number
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
}> {
|
}> {
|
||||||
return this.requestWithRetry('auth/token/lookup-self', {
|
return this.requestWithRetry('auth/token/lookup-self', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -369,8 +351,8 @@ export class VaultClient {
|
|||||||
async tokenRevokeSelf(): Promise<void> {
|
async tokenRevokeSelf(): Promise<void> {
|
||||||
await this.requestWithRetry<void>('auth/token/revoke-self', {
|
await this.requestWithRetry<void>('auth/token/revoke-self', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
})
|
||||||
this.token = undefined;
|
this.token = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,48 +361,48 @@ export class VaultClient {
|
|||||||
*/
|
*/
|
||||||
async listMounts(): Promise<{
|
async listMounts(): Promise<{
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
type: string;
|
type: string
|
||||||
description: string;
|
description: string
|
||||||
accessor: string;
|
accessor: string
|
||||||
config: {
|
config: {
|
||||||
default_lease_ttl: number;
|
default_lease_ttl: number
|
||||||
max_lease_ttl: number;
|
max_lease_ttl: number
|
||||||
};
|
}
|
||||||
options: {
|
options: {
|
||||||
version?: string;
|
version?: string
|
||||||
} | null;
|
} | null
|
||||||
};
|
}
|
||||||
}> {
|
}> {
|
||||||
const response = await this.requestWithRetry<{
|
const response = await this.requestWithRetry<{
|
||||||
data: {
|
data: {
|
||||||
auth?: {
|
auth?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
type: string;
|
type: string
|
||||||
description: string;
|
description: string
|
||||||
accessor: string;
|
accessor: string
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>
|
||||||
options: Record<string, unknown> | null;
|
options: Record<string, unknown> | null
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
secret?: {
|
secret?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
type: string;
|
type: string
|
||||||
description: string;
|
description: string
|
||||||
accessor: string;
|
accessor: string
|
||||||
config: {
|
config: {
|
||||||
default_lease_ttl: number;
|
default_lease_ttl: number
|
||||||
max_lease_ttl: number;
|
max_lease_ttl: number
|
||||||
};
|
}
|
||||||
options: {
|
options: {
|
||||||
version?: string;
|
version?: string
|
||||||
} | null;
|
} | null
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
}>('sys/internal/ui/mounts', { method: 'GET' });
|
}>('sys/internal/ui/mounts', { method: 'GET' })
|
||||||
|
|
||||||
// Return only the secret engines (not auth methods)
|
// Return only the secret engines (not auth methods)
|
||||||
return response?.data?.secret || {};
|
return response?.data?.secret || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -430,16 +412,16 @@ export class VaultClient {
|
|||||||
try {
|
try {
|
||||||
const response = await this.requestWithRetry<{
|
const response = await this.requestWithRetry<{
|
||||||
data: {
|
data: {
|
||||||
options: { version?: string };
|
options: { version?: string }
|
||||||
type: string;
|
type: string
|
||||||
};
|
}
|
||||||
}>(`sys/internal/ui/mounts/${mountPath}`, { method: 'GET' });
|
}>(`sys/internal/ui/mounts/${mountPath}`, { method: 'GET' })
|
||||||
|
|
||||||
const version = response?.data?.options?.version;
|
const version = response?.data?.options?.version
|
||||||
return version === '2' ? 2 : 1;
|
return version === '2' ? 2 : 1
|
||||||
} catch {
|
} catch {
|
||||||
// If detection fails, assume v2 (most common)
|
// If detection fails, assume v2 (most common)
|
||||||
return 2;
|
return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/types.ts
67
src/types.ts
@ -1,50 +1,51 @@
|
|||||||
export interface VaultServer {
|
export interface VaultServer {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
url: string;
|
url: string
|
||||||
description?: string;
|
description?: string
|
||||||
// KV v2 is enforced - no version selection needed
|
// KV v2 is enforced - no version selection needed
|
||||||
savedCredentials?: VaultCredentials; // Optional saved credentials (WARNING: stored in localStorage)
|
savedCredentials?: VaultCredentials // Optional saved credentials (WARNING: stored in localStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultCredentials {
|
export interface VaultCredentials {
|
||||||
serverId: string;
|
serverId: string
|
||||||
token?: string;
|
token?: string
|
||||||
username?: string;
|
username?: string
|
||||||
password?: string;
|
password?: string
|
||||||
authMethod: 'token' | 'userpass' | 'ldap';
|
authMethod: 'token' | 'userpass' | 'ldap'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MountPoint {
|
export interface MountPoint {
|
||||||
path: string;
|
path: string
|
||||||
type: string;
|
type: string
|
||||||
description: string;
|
description: string
|
||||||
accessor: string;
|
accessor: string
|
||||||
config: {
|
config: {
|
||||||
default_lease_ttl: number;
|
default_lease_ttl: number
|
||||||
max_lease_ttl: number;
|
max_lease_ttl: number
|
||||||
};
|
}
|
||||||
options: {
|
options:
|
||||||
version?: string;
|
| {
|
||||||
} | Record<string, never>;
|
version?: string
|
||||||
|
}
|
||||||
|
| Record<string, never>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultConnection {
|
export interface VaultConnection {
|
||||||
server: VaultServer;
|
server: VaultServer
|
||||||
credentials: VaultCredentials;
|
credentials: VaultCredentials
|
||||||
isConnected: boolean;
|
isConnected: boolean
|
||||||
lastConnected?: Date;
|
lastConnected?: Date
|
||||||
mountPoints?: MountPoint[];
|
mountPoints?: MountPoint[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultSecret {
|
export interface VaultSecret {
|
||||||
path: string;
|
path: string
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>
|
||||||
metadata?: {
|
metadata?: {
|
||||||
created_time: string;
|
created_time: string
|
||||||
deletion_time: string;
|
deletion_time: string
|
||||||
destroyed: boolean;
|
destroyed: boolean
|
||||||
version: number;
|
version: number
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,166 +1,166 @@
|
|||||||
import { loadConfig } from '../config';
|
import { loadConfig } from '../config'
|
||||||
|
|
||||||
export interface CacheEntry<T> {
|
export interface CacheEntry<T> {
|
||||||
data: T;
|
data: T
|
||||||
timestamp: number;
|
timestamp: number
|
||||||
size: number; // Size in bytes
|
size: number // Size in bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheStats {
|
export interface CacheStats {
|
||||||
totalSize: number; // Total size in bytes
|
totalSize: number // Total size in bytes
|
||||||
entryCount: number;
|
entryCount: number
|
||||||
oldestEntry: number | null;
|
oldestEntry: number | null
|
||||||
newestEntry: number | null;
|
newestEntry: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
class VaultCache {
|
class VaultCache {
|
||||||
private readonly CACHE_KEY = 'vaultApiCache';
|
private readonly CACHE_KEY = 'vaultApiCache'
|
||||||
private cache: Map<string, CacheEntry<unknown>>;
|
private cache: Map<string, CacheEntry<unknown>>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = this.loadFromStorage();
|
this.cache = this.loadFromStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadFromStorage(): Map<string, CacheEntry<unknown>> {
|
private loadFromStorage(): Map<string, CacheEntry<unknown>> {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(this.CACHE_KEY);
|
const stored = localStorage.getItem(this.CACHE_KEY)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored)
|
||||||
return new Map(Object.entries(parsed));
|
return new Map(Object.entries(parsed))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load cache from storage:', error);
|
console.error('Failed to load cache from storage:', error)
|
||||||
}
|
}
|
||||||
return new Map();
|
return new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveToStorage(): void {
|
private saveToStorage(): void {
|
||||||
try {
|
try {
|
||||||
const obj = Object.fromEntries(this.cache);
|
const obj = Object.fromEntries(this.cache)
|
||||||
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj));
|
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save cache to storage:', error);
|
console.error('Failed to save cache to storage:', error)
|
||||||
// If quota exceeded, clear old entries and retry
|
// If quota exceeded, clear old entries and retry
|
||||||
this.evictOldEntries(0.5); // Remove 50% of entries
|
this.evictOldEntries(0.5) // Remove 50% of entries
|
||||||
try {
|
try {
|
||||||
const obj = Object.fromEntries(this.cache);
|
const obj = Object.fromEntries(this.cache)
|
||||||
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj));
|
localStorage.setItem(this.CACHE_KEY, JSON.stringify(obj))
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
console.error('Failed to save cache after cleanup:', retryError);
|
console.error('Failed to save cache after cleanup:', retryError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateSize(data: unknown): number {
|
private calculateSize(data: unknown): number {
|
||||||
// Rough estimation of size in bytes
|
// Rough estimation of size in bytes
|
||||||
return new Blob([JSON.stringify(data)]).size;
|
return new Blob([JSON.stringify(data)]).size
|
||||||
}
|
}
|
||||||
|
|
||||||
private evictOldEntries(fraction: number): void {
|
private evictOldEntries(fraction: number): void {
|
||||||
const entries = Array.from(this.cache.entries());
|
const entries = Array.from(this.cache.entries())
|
||||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
entries.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||||
const toRemove = Math.floor(entries.length * fraction);
|
const toRemove = Math.floor(entries.length * fraction)
|
||||||
for (let i = 0; i < toRemove; i++) {
|
for (let i = 0; i < toRemove; i++) {
|
||||||
this.cache.delete(entries[i][0]);
|
this.cache.delete(entries[i][0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enforceSizeLimit(): void {
|
private enforceSizeLimit(): void {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
if (!config.cache.enabled) return;
|
if (!config.cache.enabled) return
|
||||||
|
|
||||||
const maxBytes = config.cache.maxSizeMB * 1024 * 1024;
|
const maxBytes = config.cache.maxSizeMB * 1024 * 1024
|
||||||
let totalSize = 0;
|
let totalSize = 0
|
||||||
|
|
||||||
// Calculate total size
|
// Calculate total size
|
||||||
for (const entry of this.cache.values()) {
|
for (const entry of this.cache.values()) {
|
||||||
totalSize += entry.size;
|
totalSize += entry.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// If over limit, remove oldest entries
|
// If over limit, remove oldest entries
|
||||||
if (totalSize > maxBytes) {
|
if (totalSize > maxBytes) {
|
||||||
const entries = Array.from(this.cache.entries());
|
const entries = Array.from(this.cache.entries())
|
||||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
entries.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||||
|
|
||||||
for (const [key, entry] of entries) {
|
for (const [key, entry] of entries) {
|
||||||
if (totalSize <= maxBytes * 0.8) break; // Remove until 80% of limit
|
if (totalSize <= maxBytes * 0.8) break // Remove until 80% of limit
|
||||||
totalSize -= entry.size;
|
totalSize -= entry.size
|
||||||
this.cache.delete(key);
|
this.cache.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(key: string): T | null {
|
get<T>(key: string): T | null {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
if (!config.cache.enabled) return null;
|
if (!config.cache.enabled) return null
|
||||||
|
|
||||||
const entry = this.cache.get(key) as CacheEntry<T> | undefined;
|
const entry = this.cache.get(key) as CacheEntry<T> | undefined
|
||||||
if (!entry) return null;
|
if (!entry) return null
|
||||||
|
|
||||||
// Check if entry is expired
|
// Check if entry is expired
|
||||||
const age = Date.now() - entry.timestamp;
|
const age = Date.now() - entry.timestamp
|
||||||
if (age > config.cache.maxAge) {
|
if (age > config.cache.maxAge) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry.data;
|
return entry.data
|
||||||
}
|
}
|
||||||
|
|
||||||
set<T>(key: string, data: T): void {
|
set<T>(key: string, data: T): void {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
if (!config.cache.enabled) return;
|
if (!config.cache.enabled) return
|
||||||
|
|
||||||
const size = this.calculateSize(data);
|
const size = this.calculateSize(data)
|
||||||
const entry: CacheEntry<T> = {
|
const entry: CacheEntry<T> = {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
size,
|
size,
|
||||||
};
|
}
|
||||||
|
|
||||||
this.cache.set(key, entry as CacheEntry<unknown>);
|
this.cache.set(key, entry as CacheEntry<unknown>)
|
||||||
this.enforceSizeLimit();
|
this.enforceSizeLimit()
|
||||||
this.saveToStorage();
|
this.saveToStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: string): boolean {
|
has(key: string): boolean {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
if (!config.cache.enabled) return false;
|
if (!config.cache.enabled) return false
|
||||||
|
|
||||||
const entry = this.cache.get(key);
|
const entry = this.cache.get(key)
|
||||||
if (!entry) return false;
|
if (!entry) return false
|
||||||
|
|
||||||
const age = Date.now() - entry.timestamp;
|
const age = Date.now() - entry.timestamp
|
||||||
if (age > config.cache.maxAge) {
|
if (age > config.cache.maxAge) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(key: string): void {
|
delete(key: string): void {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key)
|
||||||
this.saveToStorage();
|
this.saveToStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.cache.clear();
|
this.cache.clear()
|
||||||
this.saveToStorage();
|
this.saveToStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats(): CacheStats {
|
getStats(): CacheStats {
|
||||||
let totalSize = 0;
|
let totalSize = 0
|
||||||
let oldestEntry: number | null = null;
|
let oldestEntry: number | null = null
|
||||||
let newestEntry: number | null = null;
|
let newestEntry: number | null = null
|
||||||
|
|
||||||
for (const entry of this.cache.values()) {
|
for (const entry of this.cache.values()) {
|
||||||
totalSize += entry.size;
|
totalSize += entry.size
|
||||||
if (oldestEntry === null || entry.timestamp < oldestEntry) {
|
if (oldestEntry === null || entry.timestamp < oldestEntry) {
|
||||||
oldestEntry = entry.timestamp;
|
oldestEntry = entry.timestamp
|
||||||
}
|
}
|
||||||
if (newestEntry === null || entry.timestamp > newestEntry) {
|
if (newestEntry === null || entry.timestamp > newestEntry) {
|
||||||
newestEntry = entry.timestamp;
|
newestEntry = entry.timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,34 +169,33 @@ class VaultCache {
|
|||||||
entryCount: this.cache.size,
|
entryCount: this.cache.size,
|
||||||
oldestEntry,
|
oldestEntry,
|
||||||
newestEntry,
|
newestEntry,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up expired entries
|
// Clean up expired entries
|
||||||
cleanup(): void {
|
cleanup(): void {
|
||||||
const config = loadConfig();
|
const config = loadConfig()
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
const keysToDelete: string[] = [];
|
const keysToDelete: string[] = []
|
||||||
|
|
||||||
for (const [key, entry] of this.cache.entries()) {
|
for (const [key, entry] of this.cache.entries()) {
|
||||||
if (now - entry.timestamp > config.cache.maxAge) {
|
if (now - entry.timestamp > config.cache.maxAge) {
|
||||||
keysToDelete.push(key);
|
keysToDelete.push(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keysToDelete) {
|
for (const key of keysToDelete) {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keysToDelete.length > 0) {
|
if (keysToDelete.length > 0) {
|
||||||
this.saveToStorage();
|
this.saveToStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
export const vaultCache = new VaultCache();
|
export const vaultCache = new VaultCache()
|
||||||
|
|
||||||
// Cleanup expired entries on page load
|
// Cleanup expired entries on page load
|
||||||
vaultCache.cleanup();
|
vaultCache.cleanup()
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,8 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [require("daisyui")],
|
plugins: [require("daisyui")],
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: ["dark", "light"],
|
themes: ["business"],
|
||||||
darkTheme: "dark",
|
darkTheme: "business",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user