Fix type errors

This commit is contained in:
2025-07-17 20:47:03 +02:00
parent 1362d0a289
commit 1156bc315e
43 changed files with 372 additions and 323 deletions

View File

@ -189,7 +189,9 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
) )
path = f"/auth_cb#access_token={access_token}&refresh_token={refresh_token}" path = f"/auth_cb#access_token={access_token}&refresh_token={refresh_token}"
callback_url = os.path.join(dependencies.settings.frontend_url, path) callback_url = os.path.join("admin", path)
if dependencies.settings.frontend_test_url:
callback_url = os.path.join(dependencies.settings.frontend_test_url, path)
origin = "UNKNOWN" origin = "UNKNOWN"
if request.client: if request.client:
origin = request.client.host origin = request.client.host

View File

@ -4,12 +4,14 @@
# #
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
import logging import logging
import pathlib
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -29,6 +31,28 @@ from .settings import AdminServerSettings
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def valid_frontend_directory(frontend_dir: pathlib.Path) -> bool:
"""Validate frontend dir."""
if not frontend_dir.exists():
return False
if not frontend_dir.is_dir():
return False
if (frontend_dir / "index.html").exists():
return True
return False
def setup_frontend(app: FastAPI, settings: AdminServerSettings) -> None:
"""Setup frontend."""
if not settings.frontend_dir:
return
if not valid_frontend_directory(settings.frontend_dir):
LOG.error("Error: Not a valid frontend directory: %s", settings.frontend_dir)
return
frontend = StaticFiles(directory=settings.frontend_dir)
app.mount("/admin", frontend, name="frontend")
def create_admin_app( def create_admin_app(
settings: AdminServerSettings, settings: AdminServerSettings,
create_db: bool = False, create_db: bool = False,
@ -104,5 +128,6 @@ def create_admin_app(
dependencies = BaseDependencies(settings, get_db_session, get_async_session) dependencies = BaseDependencies(settings, get_db_session, get_async_session)
app.include_router(api.create_api_router(dependencies)) app.include_router(api.create_api_router(dependencies))
setup_frontend(app, settings)
return app return app

View File

@ -40,7 +40,8 @@ class AdminServerSettings(BaseSettings):
password_manager_directory: Path | None = None password_manager_directory: Path | None = None
oidc: OidcSettings | None = None oidc: OidcSettings | None = None
frontend_origin: str = Field(default="*") frontend_origin: str = Field(default="*")
frontend_url: str frontend_test_url: str | None = Field(default=None)
frontend_dir: Path | None = None
@property @property
def admin_db(self) -> URL: def admin_db(self) -> URL:

View File

@ -1,9 +1,12 @@
import type { Ref } from 'vue' import type { Ref } from 'vue'
import SlInput from '@shoelace-style/shoelace/dist/components/input/input.js' //import SlInput from '@shoelace-style/shoelace/dist/components/input/input.js'
export function setFieldValidation(field: Ref<SlInput>, errorMessage: string = '') { export function setFieldValidation(field: Ref<any>, errorMessage: string = '') {
// Set validation on a field // Set validation on a field
if (!field.value) {
return
}
field.value?.setCustomValidity(errorMessage) field.value?.setCustomValidity(errorMessage)
field.value?.reportValidity() field.value?.reportValidity()
} }

View File

@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -1,7 +1,7 @@
<template> <template>
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Audit log</h1> <h1 class="text-lg font-semibold text-gray-900 dark:text-white">Audit log</h1>
<div class="mt-4"> <div class="mt-4">
<form @submit.prevent="applyFilter"> <form @submit.prevent="">
<div class="py-2 border-b border-gray-100"> <div class="py-2 border-b border-gray-100">
<sl-select <sl-select
size="small" size="small"
@ -79,7 +79,7 @@ import { ref, reactive, onMounted, watch } from 'vue'
import { useAuditFilterState } from '@/store/useAuditFilterState' import { useAuditFilterState } from '@/store/useAuditFilterState'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
import { SUBSYSTEM, OPERATIONS } from '@/api/types' import { SUBSYSTEM, OPERATIONS } from '@/api/types'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
const auditFilterState = useAuditFilterState() const auditFilterState = useAuditFilterState()

View File

@ -86,7 +86,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { toRef } from 'vue' import { toRef } from 'vue'
const props = defineProps<{ amount: number }>() interface Props {
amount?: number
}
const props = defineProps<Props>()
const amount = toRef(() => props.amount ?? 25) const amount = toRef(() => props.amount ?? 25)
</script> </script>

View File

@ -55,7 +55,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="bg-white dark:bg-gray-800"> <tbody class="bg-white dark:bg-gray-800">
<template v-for="entry in auditEntries" :key="entry.id"> <template v-for="entry in auditEntries" :key="entry.id" v-if="auditEntries">
<tr class="audit-table-row auditRow hover:bg-gray-100 dark:hover:bg-gray-700"> <tr class="audit-table-row auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="audit-col-chevron"> <td class="audit-col-chevron">
<sl-icon-button <sl-icon-button
@ -84,14 +84,14 @@
<td <td
class="audit-col-client p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white" class="audit-col-client p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white"
> >
<abbr :title="entry.client_id" v-if="entry.client_name">{{ <abbr :title="entry.client_id" v-if="entry.client_name && entry.client_id">{{
entry.client_name entry.client_name
}}</abbr> }}</abbr>
</td> </td>
<td <td
class="audit-col-secret p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white" class="audit-col-secret p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white"
> >
<abbr :title="entry.secret_id" v-if="entry.secret_name">{{ <abbr :title="entry.secret_id" v-if="entry.secret_name && entry.secret_id">{{
entry.secret_name entry.secret_name
}}</abbr> }}</abbr>
</td> </td>
@ -179,7 +179,7 @@
class="font-semibold text-gray-900 dark:text-white" class="font-semibold text-gray-900 dark:text-white"
v-if="totalEntries < lastResult" v-if="totalEntries < lastResult"
> >
{{ firstResult }}-{{ TotalEntries }} {{ firstResult }}-{{ totalEntries }}
</span> </span>
<span class="font-semibold text-gray-900 dark:text-white" v-else> <span class="font-semibold text-gray-900 dark:text-white" v-else>
{{ firstResult }}-{{ lastResult }} {{ firstResult }}-{{ lastResult }}
@ -210,17 +210,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, reactive, onMounted, watch, toRef } from 'vue' import { computed, ref, reactive, onMounted, watch, toRef } from 'vue'
import type { ComputedRef } from 'vue'
import { usePagination } from '@/composables/usePagination' import { usePagination } from '@/composables/usePagination'
import { SshecretAdmin, GetAuditLogApiV1AuditGetData } from '@/client' import { SshecretAdmin } from '@/client'
import type { AuditListResult } from '@/client' import type { AuditListResult, AuditLog, GetAuditLogApiV1AuditGetData } from '@/client'
import type { AuditFilter } from '@/api/types' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk'
import PageNumbers from '@/components/common/PageNumbers.vue' import PageNumbers from '@/components/common/PageNumbers.vue'
import AuditSkeleton from '@/components/audit/AuditSkeleton.vue' import AuditSkeleton from '@/components/audit/AuditSkeleton.vue'
interface Props { interface Props {
auditFilter: AuditFilter auditFilter: GetAuditLogApiV1AuditGetData['query']
paginate?: boolean paginate?: boolean
} }
@ -228,19 +226,25 @@ const props = defineProps<Props>()
const shouldPaginate = toRef(() => props.paginate ?? true) const shouldPaginate = toRef(() => props.paginate ?? true)
const auditFilter = toRef(() => props.auditFilter) const auditFilter = toRef(() => props.auditFilter)
const auditList = ref<AuditListResult>([]) const auditList = ref<AuditListResult>()
const auditEntries = computed(() => auditList.value?.results) const auditEntries = computed<AuditLog[]>(() => (auditList.value ? auditList.value.results : []))
const totalEntries = computed(() => auditList.value?.total) const totalEntries = computed<number>(() => auditList.value?.total ?? 0)
const perPage = props.auditFilter.limit ?? 25 const perPage = computed<number>(() => {
if (props.auditFilter) {
return props.auditFilter.limit ?? 25
} else {
return 25
}
})
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } = const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
usePagination(totalEntries, perPage) usePagination(totalEntries.value, perPage.value)
const queryInput = computed<GetAuditLogApiV1AuditGetData['query']>(() => { const queryInput = computed<GetAuditLogApiV1AuditGetData['query']>(() => {
return { return {
...props.auditFilter, ...props.auditFilter,
offset: offset.value, offset: offset.value,
limit: perPage, limit: perPage.value,
} }
}) })
@ -254,7 +258,10 @@ async function loadLogs() {
const expanded = ref(new Set<string>()) const expanded = ref(new Set<string>())
function toggle(id: string) { function toggle(id: string | null | undefined) {
if (!id) {
return
}
if (expanded.value.has(id)) { if (expanded.value.has(id)) {
expanded.value.delete(id) expanded.value.delete(id)
} else { } else {
@ -262,8 +269,11 @@ function toggle(id: string) {
} }
} }
function isExpanded(id: string) { function isExpanded(id: any) {
if (id) {
return expanded.value.has(id) return expanded.value.has(id)
}
return false
} }
watch([offset, pageNum, auditFilter], loadLogs) watch([offset, pageNum, auditFilter], loadLogs)

View File

@ -46,6 +46,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'
import { ref } from 'vue' import { ref } from 'vue'
import { useAlertsStore } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
@ -55,19 +56,23 @@ import { ApiError, ValidationError } from '@/api/errors'
import { setFieldValidation } from '@/api/validation' import { setFieldValidation } from '@/api/validation'
const currentPassword = ref<string>('') const currentPassword = ref<string>('')
const newPassword = ref<string>('') const newPassword = ref<string>('')
const newPasswordConfirm = ref<string>('') const newPasswordConfirm = ref<string>('')
const passwordChangeForm = ref<HTMLFormElement>() const passwordChangeForm = ref<HTMLFormElement>()
const currentPasswordField = ref<SlInput>() const currentPasswordField = ref<SlInput>()
const newPasswordField = ref<SlInput>() const newPasswordField = ref<SlInput>()
const newPasswordFieldConfirm = ref<Slinput>() const newPasswordFieldConfirm = ref<SlInput>()
const alerts = useAlertsStore() const alerts = useAlertsStore()
const emit = defineEmits<{ (e: 'changed'): void; (e: 'cancel'): void }>() const emit = defineEmits<{ (e: 'changed'): void; (e: 'cancel'): void }>()
function checkCurrent() { function checkCurrent() {
if (!currentPasswordField.value) {
return
}
setFieldValidation(currentPasswordField) setFieldValidation(currentPasswordField)
currentPasswordField.value.reportValidity() currentPasswordField.value.reportValidity()
} }
@ -105,8 +110,11 @@ async function changePassword() {
setFieldValidation(newPasswordFieldConfirm, 'Passwords do not match match') setFieldValidation(newPasswordFieldConfirm, 'Passwords do not match match')
} else if (err instanceof ApiError && err.message.includes('Invalid current password')) { } else if (err instanceof ApiError && err.message.includes('Invalid current password')) {
setFieldValidation(currentPasswordField, 'Invalid current password') setFieldValidation(currentPasswordField, 'Invalid current password')
} else { } else if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Error changing password') alerts.showAlert(err.message, 'error', 'Error changing password')
} else {
console.error(err)
alerts.showAlert('Error changing password', 'error')
} }
} }
} }

View File

@ -12,9 +12,12 @@ function getTokens() {
const params = new URLSearchParams(hash) const params = new URLSearchParams(hash)
const accessToken = params.get('access_token') const accessToken = params.get('access_token')
const refreshToken = params.get('refresh_token') const refreshToken = params.get('refresh_token')
if (accessToken && refreshToken) {
auth.setToken(accessToken, refreshToken) auth.setToken(accessToken, refreshToken)
router.push({ name: 'dashboard' }) router.push({ name: 'dashboard' })
} else {
router.push({ name: 'login' })
}
} }
onMounted(getTokens) onMounted(getTokens)

View File

@ -107,32 +107,30 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, toRef, watch, withDefaults } from 'vue'
import type { Client, ClientCreate } from '@/client/types.gen' import type { Client, ClientCreate } from '@/client/types.gen'
import AuditTable from '@/components/audit/AuditTable.vue' import AuditTable from '@/components/audit/AuditTable.vue'
import ClientForm from '@/components/clients/ClientForm.vue' import ClientForm from '@/components/clients/ClientForm.vue'
const props = defineProps({ interface Props {
client: { client: Client
type: Object, updateErrors?: any[]
}, }
updateErrors: {
type: Array, const props = withDefaults(defineProps<Props>(), { updateErrors: () => [] })
detault: [],
},
})
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update', data: ClientCreate): void (e: 'update', data: ClientCreate): void
(e: 'deleted', data: string): void (e: 'deleted', data: string): void
(e: 'clearErrors'): void
}>() }>()
const localClient = ref({ ...props.client }) const localClient = ref({ ...props.client })
const formErrors = ref([]) const formErrors = toRef(() => props.updateErrors)
function clearFormErrors() { function clearFormErrors() {
formErrors.value = [] emit('clearErrors')
} }
const updateDrawerOpen = ref<boolean>(false) const updateDrawerOpen = ref<boolean>(false)

View File

@ -8,7 +8,7 @@
help-text="Name of the client, usually the hostname" help-text="Name of the client, usually the hostname"
:disabled="isEdit" :disabled="isEdit"
:value="name" :value="name"
@blur="checkName" @blur="checkName()"
@sl-input="name = $event.target.value" @sl-input="name = $event.target.value"
@input="emit('clearErrors')" @input="emit('clearErrors')"
ref="nameField" ref="nameField"
@ -68,10 +68,11 @@ import { computed, ref, watch } from 'vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { isIP } from 'is-ip' import { isIP } from 'is-ip'
import isCidr from 'is-cidr' import isCidr from 'is-cidr'
import type SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'
import { setFieldValidation } from '@/api/validation' import { setFieldValidation } from '@/api/validation'
import type { ClientCreate } from '@/client/types.gen' import type { ClientCreate, Client } from '@/client'
const name = ref('') const name = ref('')
const description = ref('') const description = ref('')
@ -79,10 +80,10 @@ const sourcePrefix = ref('')
const policies = ref(['0.0.0.0/0', '::/0']) const policies = ref(['0.0.0.0/0', '::/0'])
const publicKey = ref('') const publicKey = ref('')
const nameField = ref<HTMLSlInputElement>() const nameField = ref<SlInput>()
const sourceField = ref<HTMLSlInputElement>() const sourceField = ref<SlInput>()
const publicKeyField = ref<HTMLSlInputElement>() const publicKeyField = ref<SlInput>()
const clientCreateForm = ref<HTMLElement>() const clientCreateForm = ref<HTMLFormElement>()
interface Props { interface Props {
client?: Client client?: Client
@ -137,7 +138,9 @@ function removePolicy(index: number) {
} }
function checkName() { function checkName() {
if (nameField.value) {
nameField.value.reportValidity() nameField.value.reportValidity()
}
emit('clearErrors') emit('clearErrors')
} }
@ -170,23 +173,26 @@ function resetValidation() {
watch( watch(
() => props.errors, () => props.errors,
(errors) => { (errors) => {
if (!errors) {
return
}
resetValidation() resetValidation()
const nameErrors = errors.filter((e) => e.loc.includes('name')) const nameErrors = errors.filter((e) => e.loc.includes('name'))
const sourceErrors = errors.filter((e) => e.loc.includes('source')) const sourceErrors = errors.filter((e) => e.loc.includes('source'))
const publicKeyError = errors.filter((e) => e.loc.includes('public_key')) const publicKeyErrors = errors.filter((e) => e.loc.includes('public_key'))
if (nameErrors.length > 0) { if (nameErrors.length > 0) {
setFieldValidation(nameField, nameErrors[0].msg) setFieldValidation(nameField, nameErrors[0].msg)
} }
if (sourceErrors.length > 0) { if (sourceErrors.length > 0) {
setFieldValidation(sourceField, sourceErrors[0].msg) setFieldValidation(sourceField, sourceErrors[0].msg)
} }
if (publicKeyError.length > 0) { if (publicKeyErrors.length > 0) {
setFieldValidation(publicKeyField, publicKeyErrors[0].msg) setFieldValidation(publicKeyField, publicKeyErrors[0].msg)
} }
}, },
) )
async function submitForm() { function submitForm() {
if (clientCreateForm.value?.checkValidity()) { if (clientCreateForm.value?.checkValidity()) {
let clientDescription: string | null = null let clientDescription: string | null = null
if (description.value) { if (description.value) {

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import type { ClientReference } from '@/client' import type { ClientReference } from '@/client'
const clients = ref<ClientReference[]>([]) const clients = ref<ClientReference[]>([])

View File

@ -60,27 +60,30 @@
</form> </form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { isIP } from 'is-ip' import { isIP } from 'is-ip'
import isCidr from 'is-cidr' import isCidr from 'is-cidr'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import type { ClientCreate } from '@/client/types.gen' import type { ClientCreate } from '@/client/types.gen'
import { SshecretAdmin } from '@/client/sdk.gen' import { SshecretAdmin } from '@/client/sdk.gen'
import { setFieldValidation } from '@/api/validation' import { setFieldValidation } from '@/api/validation'
import type SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'
const name = ref('') const name = ref('')
const description = ref('') const description = ref('')
const sourcePrefix = ref('') const sourcePrefix = ref('')
const policies = ref(['0.0.0.0/0', '::/0']) const policies = ref(['0.0.0.0/0', '::/0'])
const publicKey = ref([]) const publicKey = ref('')
const nameField = ref<HTMLSlInputElement>() const nameField = ref<SlInput>()
const sourceField = ref<HTMLSlInputElement>() const sourceField = ref<SlInput>()
const publicKeyField = ref<HTMLSlInputElement>() const publicKeyField = ref<SlInput>()
const clientCreateForm = ref<HTMLElement>() const clientCreateForm = ref<HTMLFormElement>()
function addPolicy() { function addPolicy() {
if (!sourcePrefix.value) { if (!sourcePrefix.value) {
@ -107,11 +110,14 @@ function removePolicy(index: number) {
} }
function checkName() { function checkName() {
if (nameField.value) {
nameField.value.reportValidity() nameField.value.reportValidity()
}
} }
function validatePublicKey() { function validatePublicKey() {
const pubkey = publicKey.value const pubkey: string = publicKey.value
if (pubkey) {
const defaultError = 'Invalid public key. Must be a valid ssh-rsa key.' const defaultError = 'Invalid public key. Must be a valid ssh-rsa key.'
if (!pubkey.startsWith('ssh-rsa ')) { if (!pubkey.startsWith('ssh-rsa ')) {
setFieldValidation(publicKeyField, defaultError) setFieldValidation(publicKeyField, defaultError)
@ -128,6 +134,7 @@ function validatePublicKey() {
} }
setFieldValidation(publicKeyField, '') setFieldValidation(publicKeyField, '')
}
} }
async function submitForm() { async function submitForm() {

View File

@ -14,16 +14,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useAlertsStore, Alert } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
import type { Alert } from '@/store/useAlertsStore'
import type SlAlert from '@shoelace-style/shoelace/dist/components/alert/alert.component.js'
const alerts = useAlertsStore() const alerts = useAlertsStore()
const props = defineProps<{ alert: Alert }>() const props = defineProps<{ alert: Alert }>()
const alertElement = ref<HTMLSlAlertElement>() const alertElement = ref<SlAlert>()
function showToast() { function showToast() {
if (alertElement.value) {
alertElement.value.toast() alertElement.value.toast()
}
} }
onMounted(showToast) onMounted(showToast)

View File

@ -10,11 +10,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useBubbleSafeHandler } from '@/composables/useBubbleSafeHandler' import { useBubbleSafeHandler } from '@/composables/useBubbleSafeHandler'
import type SlDialog from '@shoelace-style/shoelace/dist/components/dialog/dialog.component.js'
const props = defineProps<{ label: string; open: boolean }>() const props = defineProps<{ label: string; open: boolean }>()
const emit = defineEmits<{ (e: 'hide'): void }>() const emit = defineEmits<{ (e: 'hide'): void }>()
const dialogRef = ref<HTMLSlDrawerElement>() const dialogRef = ref<SlDialog>()
const handleHide = useBubbleSafeHandler(dialogRef, () => { const handleHide = useBubbleSafeHandler(dialogRef, () => {
emit('hide') emit('hide')

View File

@ -7,11 +7,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useBubbleSafeHandler } from '@/composables/useBubbleSafeHandler' import { useBubbleSafeHandler } from '@/composables/useBubbleSafeHandler'
import type SlDrawer from '@shoelace-style/shoelace/dist/components/drawer/drawer.component.js'
const props = defineProps<{ label: string; open: boolean }>() const props = defineProps<{ label: string; open: boolean }>()
const emit = defineEmits<{ (e: 'hide'): void }>() const emit = defineEmits<{ (e: 'hide'): void }>()
const drawerRef = ref<HTMLSlDrawerElement>() const drawerRef = ref<SlDrawer>()
const handleHide = useBubbleSafeHandler(drawerRef, () => { const handleHide = useBubbleSafeHandler(drawerRef, () => {
emit('hide') emit('hide')

View File

@ -1,109 +0,0 @@
<template>
<MasterDetail>
<template #master>
<MasterTabs :selectedTab="selectedTab" @change="tabSelected" />
</template>
<template #detail v-if="showAudit || selectedTab === 'audit'">
<AuditView />
</template>
<template #detail v-else>
<template v-if="treeState.selected">
<ClientDetailView
v-if="treeState.selected.objectType === SshecretObjectType.Client"
:clientId="treeState.selected.id"
:key="treeState.selected.id"
/>
<SecretDetailView
v-else-if="treeState.selected.objectType === SshecretObjectType.ClientSecret"
:secretName="treeState.selected.id"
:parentId="null"
:key="treeState.selected.id"
/>
<SecretGroupDetailView
v-else-if="
treeState.selected.objectType === SshecretObjectType.SecretGroup &&
treeState.selected.id != 'ungrouped'
"
:groupPath="treeState.selected.id"
:key="treeState.selected.id"
/>
</template>
</template>
</MasterDetail>
</template>
<script setup lang="ts">
import type { PageState } from '@/api/types'
import { computed, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import MasterTabs from '@/components/common/MasterTabs.vue'
import MasterDetail from '@/views/layout/MasterDetail.vue'
import AuditView from '@/views/audit/AuditView.vue'
import ClientTreeList from '@/views/clients/ClientTreeList.vue'
import SecretTreeList from '@/views/secrets/SecretTreeList.vue'
import ClientDetailView from '@/views/clients/ClientDetailView.vue'
import SecretDetailView from '@/views/secrets/SecretDetailView.vue'
import SecretGroupDetailView from '@/views/secrets/SecretGroupDetailView.vue'
import AuditFilters from '@/components/audit/AuditFilters.vue'
import GenericDetail from '@/components/common/GenericDetail.vue'
import { SshecretObjectType } from '@/api/types'
import { useAuditFilterState } from '@/store/useAuditFilterState'
import { useTreeState } from '@/store/useTreeState'
import { useAlertsStore } from '@/store/useAlertsStore'
const router = useRouter()
const alerts = useAlertsStore()
const treeState = useTreeState()
const auditFilterState = useAuditFilterState()
const showAudit = ref<{ boolean }>()
const props = defineProps<{ loadPage: PageState }>()
const selectedTab = computed(() => props.loadPage?.activePane)
const selectedClientName = ref()
async function loadObjectSelection() {
if (!props.loadPage) {
return
}
if (props.loadPage.activePane === 'audit') {
treeState.showAudit = true
showAudit.value = true
}
if (props.loadPage.activePane === 'clients' && props.loadPage.selectedObject) {
try {
await treeState.loadClientName(props.loadPage.selectedObject)
selectedClientName.value = props.loadPage.selectedObject
} catch (e) {
// We need to figure out how to generate a 404 here
alerts.showAlert('Could not find the object', 'error')
}
}
}
function tabSelected(tabName) {
router.push({ name: tabName })
if (tabName == 'audit') {
treeState.showAudit = true
showAudit.value = true
} else {
treeState.showAudit = false
showAudit.value = false
}
}
onMounted(loadObjectSelection)
</script>
<style>
sl-tab-group.master-pane-tabs::part(base),
sl-tab-group.master-pane-tabs::part(body),
sl-tab-group.master-pane-tabs sl-tab-panel::part(base),
sl-tab-group.master-pane-tabs sl-tab-panel::part(body),
sl-tab-group.master-pane-tabs sl-tab-panel {
height: 100%;
}
</style>

View File

@ -66,5 +66,9 @@ const props = withDefaults(
size: 'small', size: 'small',
}, },
) )
const emit = defineEmits<{ (e: 'next'): void; (e: 'previous'): void; (e: 'goto', number): void }>() const emit = defineEmits<{
(e: 'next'): void
(e: 'previous'): void
(e: 'goto', data: number): void
}>()
</script> </script>

View File

@ -20,8 +20,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, toRef } from 'vue' import { computed, ref, toRef } from 'vue'
import type SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'
const props = defineProps<{ parent }>() const props = defineProps<{ parent: string }>()
const groupName = ref<string>('') const groupName = ref<string>('')
const targetPath = computed(() => `${parentPath.value}/${groupName.value}`) const targetPath = computed(() => `${parentPath.value}/${groupName.value}`)
const parentPath = toRef(() => props.parent) const parentPath = toRef(() => props.parent)
@ -34,14 +35,15 @@ const helpText = computed(() => {
}) })
const emit = defineEmits<{ (e: 'submit', data: string): void; (e: 'cancel'): void }>() const emit = defineEmits<{ (e: 'submit', data: string): void; (e: 'cancel'): void }>()
const nameField = ref<HTMLElement>() const nameField = ref<SlInput>()
function validateInput(): boolean { function validateInput(): boolean {
if (!nameField.value) return true
if (groupName.value.includes('/')) { if (groupName.value.includes('/')) {
nameField.value?.setCustomValidity('Group name cannot contain /') nameField.value?.setCustomValidity('Group name cannot contain /')
} }
nameField.value?.reportValidity() nameField.value.reportValidity()
const validity = nameField.validity const validity = nameField.value.validity
return validity?.valid ?? true return validity?.valid ?? true
} }

View File

@ -26,9 +26,19 @@
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200"> <dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">
Entries in this group Entries in this group
</dt> </dt>
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300"> <dd
class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300"
v-if="group.entries"
>
{{ group.entries.length }} {{ group.entries.length }}
</dd> </dd>
<dd
class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300"
v-else
>
0
</dd>
</div> </div>
</dl> </dl>
</div> </div>

View File

@ -22,6 +22,7 @@ import type { ClientSecretGroup } from '@/client'
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { useAlertsStore } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
import { ApiError } from '@/api/errors'
const props = defineProps<{ self: string }>() const props = defineProps<{ self: string }>()
const groups = ref<ClientSecretGroup[]>([]) const groups = ref<ClientSecretGroup[]>([])
@ -31,6 +32,8 @@ const emit = defineEmits<{ (e: 'selected', data: string): void; (e: 'cancel'): v
const selectedPath = ref() const selectedPath = ref()
const alerts = useAlertsStore()
async function getGroups() { async function getGroups() {
selectedPath.value = props.self selectedPath.value = props.self
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet({ const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet({
@ -38,9 +41,16 @@ async function getGroups() {
}) })
try { try {
const responseData = assertSdkResponseOk(response) const responseData = assertSdkResponseOk(response)
if (responseData.groups) {
groups.value = responseData.groups groups.value = responseData.groups
}
} catch (err) { } catch (err) {
alerts.showAlert(err.message, 'error', 'Could not fetch groups from the backend') if (err instanceof ApiError) {
const errorMessage = err.message ?? 'Unknown error'
alerts.showAlert(errorMessage, 'error', 'Could not fetch groups from the backend')
} else {
alerts.showAlert('Could not fetch groups from the backend', 'error')
}
} }
} }

View File

@ -20,6 +20,7 @@ import type { ClientSecretGroup } from '@/client'
import { ref, onMounted, toRef } from 'vue' import { ref, onMounted, toRef } from 'vue'
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { useAlertsStore } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
import { ApiError } from '@/api/errors'
interface Props { interface Props {
existingPath?: string existingPath?: string
@ -40,13 +41,19 @@ async function getGroups() {
}) })
try { try {
const responseData = assertSdkResponseOk(response) const responseData = assertSdkResponseOk(response)
if (responseData.groups) {
if (props.existingPath) { if (props.existingPath) {
groups.value = responseData.groups.filter((entry) => entry.path !== props.existingPath) groups.value = responseData.groups.filter((entry) => entry.path !== props.existingPath)
} else { } else {
groups.value = responseData.groups groups.value = responseData.groups
} }
}
} catch (err) { } catch (err) {
alerts.showAlert(err.message, 'error', 'Could not fetch groups from the backend') let errorMessage = 'Unknown error'
if (err instanceof ApiError && err.message) {
errorMessage = err.message
}
alerts.showAlert(errorMessage, 'error', 'Could not fetch groups from the backend')
} }
} }

View File

@ -22,12 +22,6 @@
<h3 class="text-base/7 font-semibold text-gray-900 dark:text-gray-50"> <h3 class="text-base/7 font-semibold text-gray-900 dark:text-gray-50">
{{ secret.name }} {{ secret.name }}
</h3> </h3>
<p
class="mt-1 max-w-2xl text-sm/6 text-gray-500 dark:text-gray-100"
v-if="secret?.description"
>
{{ secret?.description }}
</p>
</div> </div>
<div class="mt-6 border-t border-gray-100" v-if="secret"> <div class="mt-6 border-t border-gray-100" v-if="secret">
<dl class="divide-y divide-gray-100"> <dl class="divide-y divide-gray-100">
@ -219,7 +213,7 @@ const emit = defineEmits<{
(e: 'moveGroup', data: string): void (e: 'moveGroup', data: string): void
}>() }>()
const secret = ref<Secret>(props.secret) const secret = ref<SecretView>(props.secret)
const clients = computed(() => props.secret?.clients ?? []) const clients = computed(() => props.secret?.clients ?? [])
const secretValue = ref<string | null>(secret.value?.secret) const secretValue = ref<string | null>(secret.value?.secret)
const addDialog = ref<boolean>(false) const addDialog = ref<boolean>(false)
@ -238,9 +232,13 @@ const auditFilter = {
limit: 10, limit: 10,
} }
function handleHide(event) { function handleHide(event: CustomEvent<Record<PropertyKey, never>>) {
const targetId = event.target.id if (!event.target) {
if (targetId === 'addDialogDrawer') { return
}
const target = event.target as HTMLDivElement
const targetId = target.id
if (targetId && targetId === 'addDialogDrawer') {
addDialog.value = false addDialog.value = false
} }
} }
@ -253,7 +251,9 @@ function moveGroup(path: string) {
const existingGroupPath = computed(() => secret.value.group?.path) const existingGroupPath = computed(() => secret.value.group?.path)
function updateSecret() { function updateSecret() {
if (secretValue.value) {
emit('update', secretValue.value) emit('update', secretValue.value)
}
} }
function addSecretToClients(clientIds: string[]) { function addSecretToClients(clientIds: string[]) {
addDialog.value = false addDialog.value = false

View File

@ -47,7 +47,9 @@
<label for="clients" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" <label for="clients" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Clients</label >Clients</label
> >
<template v-if="selectedClients">
<ClientSelectDropdown v-model="selectedClients" /> <ClientSelectDropdown v-model="selectedClients" />
</template>
<div slot="footer"> <div slot="footer">
<sl-button size="medium" variant="success" outline @click="submitCreateSecret" class="mr-2"> <sl-button size="medium" variant="success" outline @click="submitCreateSecret" class="mr-2">
<sl-icon slot="prefix" name="person-plus"></sl-icon> <sl-icon slot="prefix" name="person-plus"></sl-icon>
@ -65,6 +67,7 @@ import { generateRandomPassword } from '@/api/password'
import AddSecretsToClients from '@/components/secrets/AddSecretToClients.vue' import AddSecretsToClients from '@/components/secrets/AddSecretToClients.vue'
import ClientSelectDropdown from '@/components/clients/ClientSelectDropdown.vue' import ClientSelectDropdown from '@/components/clients/ClientSelectDropdown.vue'
import { setFieldValidation } from '@/api/validation' import { setFieldValidation } from '@/api/validation'
import type SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'
interface Props { interface Props {
group?: string group?: string
@ -81,8 +84,8 @@ const emit = defineEmits<{
const secretName = ref<string>() const secretName = ref<string>()
const createSecretForm = ref<HTMLFormElement>() const createSecretForm = ref<HTMLFormElement>()
const nameField = ref<HTMLSlInputElement>() const nameField = ref<SlInput>()
const secretField = ref<HTMLSlInputElement>() const secretField = ref<SlInput>()
const secretValue = ref() const secretValue = ref()
const secretLength = ref(8) const secretLength = ref(8)
const autoGenerate = ref(false) const autoGenerate = ref(false)
@ -94,15 +97,23 @@ function generatePassword() {
} }
function validateName() { function validateName() {
if (!nameField.value) {
return
}
nameField.value.reportValidity() nameField.value.reportValidity()
} }
function validateSecret() { function validateSecret() {
if (!secretField.value) {
return
}
secretField.value.reportValidity() secretField.value.reportValidity()
} }
function cancelCreateSecret() { function cancelCreateSecret() {
if (createSecretForm.value) {
createSecretForm.value.reset() createSecretForm.value.reset()
}
emit('cancel') emit('cancel')
} }
@ -114,10 +125,12 @@ function submitCreateSecret() {
validateName() validateName()
validateSecret() validateSecret()
if (!secretName.value) return
if (createSecretForm.value?.checkValidity()) { if (createSecretForm.value?.checkValidity()) {
console.log('SelectedClients: ', selectedClients.value) console.log('SelectedClients: ', selectedClients.value)
const secretGroup = props.group ?? null const secretGroup = props.group ?? null
let secretClients = [] let secretClients: string[] = []
if (Array.isArray(selectedClients.value)) { if (Array.isArray(selectedClients.value)) {
secretClients = [...selectedClients.value] secretClients = [...selectedClients.value]
} }
@ -135,11 +148,13 @@ function submitCreateSecret() {
watch( watch(
() => props.errors, () => props.errors,
(errors) => { (errors) => {
if (errors) {
resetValidation() resetValidation()
const nameErrors = errors.filter((e) => e.loc.includes('name')) const nameErrors = errors.filter((e) => e.loc.includes('name'))
if (nameErrors.length > 0) { if (nameErrors.length > 0) {
setFieldValidation(nameField, nameErrors[0].msg) setFieldValidation(nameField, nameErrors[0].msg)
} }
}
}, },
) )
</script> </script>

View File

@ -17,7 +17,7 @@ import SecretGroup from '@/components/secrets/SecretGroup.vue'
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue' import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue' import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
const props = defineProps<{ group: SecretGroup; except?: string }>() const props = defineProps<{ group: ClientSecretGroup; except?: string }>()
const groupPath = toRef(() => props.group.path) const groupPath = toRef(() => props.group.path)
</script> </script>

View File

@ -13,6 +13,6 @@ const props = defineProps<{
selected?: boolean selected?: boolean
}>() }>()
const groupPath = toRef(() => props.path) const groupPath = toRef(() => props.groupPath)
const itemId = computed(() => `secret-${props.name}`) const itemId = computed(() => `secret-${props.name}`)
</script> </script>

View File

@ -2,7 +2,7 @@ import './assets/main.css'
// Added for shoelace. // Added for shoelace.
// See note on CDB usage: https://shoelace.style/frameworks/vue // See note on CDB usage: https://shoelace.style/frameworks/vue
import '@shoelace-style/shoelace/dist/themes/light.css' import '@shoelace-style/shoelace/dist/themes/light.css'
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path' import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js'
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/') setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/')
@ -51,7 +51,8 @@ import { client } from '@/client/client.gen'
import router from './router' import router from './router'
import App from './App.vue' import App from './App.vue'
const baseURL = import.meta.env.SSHECRET_FRONTEND_API_BASE_URL const baseURL = import.meta.env.DEV ? import.meta.env.SSHECRET_FRONTEND_API_BASE_URL : "/"
//const baseURL = import.meta.env.SSHECRET_FRONTEND_API_BASE_URL
client.setConfig({ baseURL }) client.setConfig({ baseURL })

View File

@ -68,7 +68,7 @@ const routes = [
name: 'Group', name: 'Group',
path: '/group/:groupPath(.*)*', path: '/group/:groupPath(.*)*',
component: SecretGroupDetailView, component: SecretGroupDetailView,
props: route => ({ props: (route: any) => ({
groupPath: Array.isArray(route.params.groupPath) ? reassemblePath(route.params.groupPath) : route.params.groupPath || '' groupPath: Array.isArray(route.params.groupPath) ? reassemblePath(route.params.groupPath) : route.params.groupPath || ''
}), }),
meta: { requiresAuth: true }, meta: { requiresAuth: true },

View File

@ -23,6 +23,9 @@ export const useAuthTokenStore = defineStore('authtoken', {
async login(username: string, password: string): Promise<boolean> { async login(username: string, password: string): Promise<boolean> {
try { try {
const response = await SshecretAdmin.loginForAccessTokenApiV1TokenPost({ body: { username, password } }) const response = await SshecretAdmin.loginForAccessTokenApiV1TokenPost({ body: { username, password } })
if (!response.data) {
return false
}
const tokenData: Token = response.data const tokenData: Token = response.data
const accessToken = tokenData.access_token const accessToken = tokenData.access_token
const refreshToken = tokenData.refresh_token const refreshToken = tokenData.refresh_token
@ -53,6 +56,9 @@ export const useAuthTokenStore = defineStore('authtoken', {
try { try {
console.log("Refreshing token") console.log("Refreshing token")
const response = await SshecretAdmin.refreshTokenApiV1RefreshPost({ body: { grant_type: "refresh_token", refresh_token: this.refreshToken } }) const response = await SshecretAdmin.refreshTokenApiV1RefreshPost({ body: { grant_type: "refresh_token", refresh_token: this.refreshToken } })
if (!response.data) {
return false
}
const tokenData: Token = response.data const tokenData: Token = response.data
const accessToken = tokenData.access_token const accessToken = tokenData.access_token
const refreshToken = tokenData.refresh_token const refreshToken = tokenData.refresh_token

View File

@ -1,7 +1,7 @@
// stores/useAlertsStore.ts // stores/useAlertsStore.ts
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
interface Alert { export interface Alert {
id: number id: number
message: string message: string
title?: string title?: string

View File

@ -86,17 +86,19 @@ import { ref, onMounted } from 'vue'
import type { SystemStats } from '@/client' import type { SystemStats } from '@/client'
import type { AuditFilter } from '@/api/types' import type { AuditFilter } from '@/api/types'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
import type { Operation } from '@/client'
import AuditTable from '@/components/audit/AuditTable.vue' import AuditTable from '@/components/audit/AuditTable.vue'
import { useAlertsStore } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
const loginOperation: Operation = 'login'
const loginAuditFilter = { const loginAuditFilter = {
operation: 'login', operation: loginOperation,
limit: 10, limit: 10,
} }
const auditFilter = { limit: 10 } const auditFilter = { limit: 10 }
const stats = ref<SystemStats>({}) const stats = ref<SystemStats>()
const alerts = useAlertsStore() const alerts = useAlertsStore()

View File

@ -1,6 +0,0 @@
<template>
<MasterDetailWorkspace />
</template>
<script setup lang="ts">
import MasterDetailWorkspace from '@/components/common/MasterDetailWorkspace.vue'
</script>

View File

@ -24,6 +24,4 @@ function tabSelected(tabName: string) {
router.push({ name: tabName }) router.push({ name: tabName })
} }
} }
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
</script> </script>

View File

@ -17,7 +17,7 @@ const auditFilterState = useAuditFilterState()
const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({}) const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({})
watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter)) watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter))
const loaded = ref<{ boolean }>(false) const loaded = ref<boolean>(false)
onMounted(() => { onMounted(() => {
auditFilter.value = auditFilterState.getFilter auditFilter.value = auditFilterState.getFilter

View File

@ -11,11 +11,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, toRef, watch, onMounted } from 'vue' import { ref, toRef, watch, onMounted } from 'vue'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { ValidationError } from '@/api/errors' import { ApiError, ValidationError } from '@/api/errors'
import ClientSkeleton from '@/components/clients/ClientSkeleton.vue' import ClientSkeleton from '@/components/clients/ClientSkeleton.vue'
import ClientDetail from '@/components/clients/ClientDetail.vue' import ClientDetail from '@/components/clients/ClientDetail.vue'
import type { ClientCreate } from '@/client' import type { ClientCreate, Client } from '@/client'
import { idKey } from '@/api/paths' import { idKey } from '@/api/paths'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
import { useTreeState } from '@/store/useTreeState' import { useTreeState } from '@/store/useTreeState'
@ -32,7 +32,7 @@ const treeState = useTreeState()
const emit = defineEmits<{ (e: 'clientDeleted', data: string): void }>() const emit = defineEmits<{ (e: 'clientDeleted', data: string): void }>()
const alerts = useAlertsStore() const alerts = useAlertsStore()
const updateErrors = ref([]) const updateErrors = ref<any[]>([])
async function loadClient() { async function loadClient() {
if (!props.id) return if (!props.id) return
@ -56,9 +56,10 @@ function clearUpdateErrors() {
} }
async function updateClient(updated: ClientCreate) { async function updateClient(updated: ClientCreate) {
if (!client.value) return
const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({ const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({
path: { id: idKey(localClient.value.id) }, path: { id: idKey(client.value.id) },
body: data, body: updated,
}) })
try { try {
const responseData = assertSdkResponseOk(response) const responseData = assertSdkResponseOk(response)
@ -67,9 +68,12 @@ async function updateClient(updated: ClientCreate) {
} catch (err) { } catch (err) {
if (err instanceof ValidationError) { if (err instanceof ValidationError) {
updateErrors.value = err.errors updateErrors.value = err.errors
} else { } else if (err instanceof ApiError) {
const errorMessage = err.message ?? 'Unknown error' const errorMessage = err.message ?? 'Unknown error'
alerts.showAlert(`Error from backend: ${errorMessage}`, 'error') alerts.showAlert(`Error from backend: ${errorMessage}`, 'error')
} else {
console.error(err)
alerts.showAlert('Unexpected error from backend', 'error')
} }
} }
} }

View File

@ -4,7 +4,7 @@
<MasterTabs selectedTab="clients" @change="tabSelected" /> <MasterTabs selectedTab="clients" @change="tabSelected" />
</template> </template>
<template #detail> <template #detail>
<RouterView :key="routeKey" /> <RouterView :key="route.path" />
</template> </template>
</MasterDetail> </MasterDetail>
</template> </template>
@ -24,6 +24,4 @@ function tabSelected(tabName: string) {
router.push({ name: tabName }) router.push({ name: tabName })
} }
} }
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
</script> </script>

View File

@ -107,8 +107,8 @@ import { usePagination } from '@/composables/usePagination'
import { SshecretAdmin } from '@/client/sdk.gen' import { SshecretAdmin } from '@/client/sdk.gen'
import type { Client, ClientCreate } from '@/client/types.gen' import type { Client, ClientCreate } from '@/client/types.gen'
import { ValidationError } from '@/api/errors' import { ApiError, ValidationError } from '@/api/errors'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { useTreeState } from '@/store/useTreeState' import { useTreeState } from '@/store/useTreeState'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
@ -119,31 +119,26 @@ import ClientForm from '@/components/clients/ClientForm.vue'
import PageNumbers from '@/components/common/PageNumbers.vue' import PageNumbers from '@/components/common/PageNumbers.vue'
import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue' import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue'
import { useAlertsStore } from '@/store/useAlertsStore' import { useAlertsStore } from '@/store/useAlertsStore'
import type SlTreeItem from '@shoelace-style/shoelace/dist/components/tree-item/tree-item.component.js'
import { useDebounce } from '@/composables/useDebounce' import { useDebounce } from '@/composables/useDebounce'
const treeState = useTreeState() const treeState = useTreeState()
const clientsPerPage = 20 const clientsPerPage = 20
const totalClients = computed(() => treeState.clients?.total_results) const totalClients = computed<number>(() => treeState.clients?.total_results ?? 0)
const clients = computed(() => treeState.clients.clients) const clients = computed(() => treeState.clients?.clients)
const selectedClient = ref<Client | null>(null) const selectedClient = ref<Client | null>(null)
const selectedSecret = ref<string | null>(null) const selectedSecret = ref<string | null>(null)
const createErrors = ref([]) const createErrors = ref<any[]>([])
const createFormKey = ref<number>(0) const createFormKey = ref<number>(0)
const createDrawerOpen = ref<boolean>(false) const createDrawerOpen = ref<boolean>(false)
const props = defineProps({
loadClient: {
type: String,
default: '',
},
})
const router = useRouter() const router = useRouter()
const clientQuery = toRef(() => props.loadClient) const clientQuery = ref<string>()
const debouncedQuery = useDebounce(clientQuery, 300) const debouncedQuery = useDebounce(clientQuery, 300)
const alerts = useAlertsStore() const alerts = useAlertsStore()
@ -160,12 +155,16 @@ async function loadClients() {
} }
function updateClient(updated: Client) { function updateClient(updated: Client) {
if (!clients.value) {
return
}
const index = clients.value.findIndex((c) => c.name === updated.name) const index = clients.value.findIndex((c) => c.name === updated.name)
if (index >= 0) { if (index >= 0) {
clients.value[index] = updated clients.value[index] = updated
} }
} }
function itemSelected(event: Event) {
function itemSelected(event: CustomEvent<{ selection: SlTreeItem[] }>) {
if (event.detail.selection) { if (event.detail.selection) {
const el = event.detail.selection[0] as HTMLElement const el = event.detail.selection[0] as HTMLElement
const childType = el.dataset.type const childType = el.dataset.type
@ -186,8 +185,12 @@ async function createClient(data: ClientCreate) {
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data }) const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
try { try {
const responseData = assertSdkResponseOk(response) const responseData = assertSdkResponseOk(response)
if (clients.value) {
clients.value.unshift(responseData) clients.value.unshift(responseData)
totalClients.value += 1 }
if (treeState.clients) {
treeState.clients.total_results += 1
}
createDrawerOpen.value = false createDrawerOpen.value = false
createFormKey.value += 1 createFormKey.value += 1
treeState.selectClient(responseData.id) treeState.selectClient(responseData.id)
@ -196,9 +199,12 @@ async function createClient(data: ClientCreate) {
} catch (err) { } catch (err) {
if (err instanceof ValidationError) { if (err instanceof ValidationError) {
createErrors.value = err.errors createErrors.value = err.errors
} else { } else if (err instanceof ApiError) {
const errorMessage = err.message ?? 'Unknown error' const errorMessage = err.message ?? 'Unknown error'
alerts.showAlert(`Error from backend: ${errorMessage}`, 'error') alerts.showAlert(`Error from backend: ${errorMessage}`, 'error')
} else {
console.error(err)
alerts.showAlert('Error communicating with backend', 'error', 'Unexpected Error')
} }
} }
} }
@ -209,6 +215,7 @@ function clearCreateErrors() {
} }
async function clientDeleted(id: string) { async function clientDeleted(id: string) {
if (!clients.value) return
const index = clients.value.findIndex((c) => c.id === id) const index = clients.value.findIndex((c) => c.id === id)
if (index >= 0) { if (index >= 0) {
clients.value.splice(index, 1) clients.value.splice(index, 1)

View File

@ -25,7 +25,7 @@ import { useAlertsStore } from '@/store/useAlertsStore'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { SecretView } from '@/client/types.gen.ts' import type { SecretView } from '@/client'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
const alerts = useAlertsStore() const alerts = useAlertsStore()
@ -46,8 +46,11 @@ async function loadSecret() {
} catch (err) { } catch (err) {
if (err instanceof NotFoundError) { if (err instanceof NotFoundError) {
notfound.value = true notfound.value = true
} else { } else if (err instanceof ApiError) {
alerts.showAlert(`Error from backend: ${err.message}`, 'error') alerts.showAlert(`Error from backend: ${err.message}`, 'error')
} else {
console.error(err)
alerts.showAlert('Error communicating with backend', 'error')
} }
} finally { } finally {
loading.value = false loading.value = false
@ -56,7 +59,9 @@ async function loadSecret() {
async function updateSecretValue(value: string) { async function updateSecretValue(value: string) {
// Update a secret value // Update a secret value
await SshecretAdmin.updateSecretApiV1SecretsNamePut({ if (!props.id) return
try {
const response = await SshecretAdmin.updateSecretApiV1SecretsNamePut({
path: { path: {
name: props.id, name: props.id,
}, },
@ -64,13 +69,23 @@ async function updateSecretValue(value: string) {
value: value, value: value,
}, },
}) })
assertSdkResponseOk(response)
} catch (err) {
if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Error updating secret')
} else {
console.error(err)
alerts.showAlert('Unexpected Error', 'error', 'Error updating secret')
}
}
if (props.parentId) { if (props.parentId) {
await treeState.refreshClient(props.parentId) await treeState.refreshClient(props.parentId)
await loadSecret() await loadSecret()
} }
} }
async function deleteSecret(clients: string[]) { async function deleteSecret() {
// Delete the whole secret // Delete the whole secret
if (props.id) { if (props.id) {
const response = await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({ const response = await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({
@ -78,14 +93,16 @@ async function deleteSecret(clients: string[]) {
}) })
try { try {
assertSdkResponseOk(response) assertSdkResponseOk(response)
for (const clientId in clients) {
await treeState.refreshClient(clientId)
}
await treeState.getSecretGroups() await treeState.getSecretGroups()
treeState.bumpGroupRevision() treeState.bumpGroupRevision()
router.go(-1) router.go(-1)
} catch (err) { } catch (err) {
if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Error deleting secret') alerts.showAlert(err.message, 'error', 'Error deleting secret')
} else {
console.error(err)
alerts.showAlert('Unexpected Error', 'error', 'Error deleting secret')
}
} }
} }
} }
@ -103,7 +120,12 @@ async function addSecretToClient(clientId: string) {
await treeState.refreshClient(clientId) await treeState.refreshClient(clientId)
await loadSecret() await loadSecret()
} catch (err) { } catch (err) {
if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Failed to add secret to client') alerts.showAlert(err.message, 'error', 'Failed to add secret to client')
} else {
console.error(err)
alerts.showAlert('Unexpected error', 'error', 'Failed to add secret to client')
}
} }
} }
} }
@ -122,13 +144,21 @@ async function removeClientSecret(clientId: string) {
await treeState.refreshClient(clientId) await treeState.refreshClient(clientId)
await loadSecret() await loadSecret()
} catch (err) { } catch (err) {
if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Failed to remove secret from client') alerts.showAlert(err.message, 'error', 'Failed to remove secret from client')
} else {
console.error(err)
alerts.showAlert('Unexpected error', 'error', 'Failed to remove secret from client')
}
} }
} }
} }
async function moveGroup(path) { async function moveGroup(path: string) {
// Move secret to a group. // Move secret to a group.
if (!secret.value) {
return
}
const data = { const data = {
secret_name: secret.value.name, secret_name: secret.value.name,
group_path: path, group_path: path,
@ -141,7 +171,12 @@ async function moveGroup(path) {
treeState.bumpGroupRevision() treeState.bumpGroupRevision()
alerts.showAlert('Secret moved', 'success') alerts.showAlert('Secret moved', 'success')
} catch (err) { } catch (err) {
if (err instanceof ApiError) {
alerts.showAlert(err.message, 'error', 'Failed to move secret to group') alerts.showAlert(err.message, 'error', 'Failed to move secret to group')
} else {
console.error(err)
alerts.showAlert('Unexpected error', 'error', 'Failed to move secret to group')
}
} }
} }
onMounted(loadSecret) onMounted(loadSecret)

View File

@ -27,6 +27,4 @@ function tabSelected(tabName: string) {
router.push({ name: tabName }) router.push({ name: tabName })
} }
} }
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
</script> </script>

View File

@ -73,8 +73,8 @@ import { useAlertsStore } from '@/store/useAlertsStore'
import { SshecretAdmin } from '@/client' import { SshecretAdmin } from '@/client'
import { SshecretObjectType } from '@/api/types' import { SshecretObjectType } from '@/api/types'
import { splitPath } from '@/api/paths' import { splitPath } from '@/api/paths'
import { ValidationError } from '@/api/errors' import { ApiError, ValidationError } from '@/api/errors'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk' import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import SecretGroup from '@/components/secrets/SecretGroup.vue' import SecretGroup from '@/components/secrets/SecretGroup.vue'
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue' import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue' import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
@ -82,19 +82,20 @@ import AddGroup from '@/components/secrets/AddGroup.vue'
import SecretForm from '@/components/secrets/SecretForm.vue' import SecretForm from '@/components/secrets/SecretForm.vue'
import Drawer from '@/components/common/Drawer.vue' import Drawer from '@/components/common/Drawer.vue'
import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue' import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue'
import type SlTreeItem from '@shoelace-style/shoelace/dist/components/tree-item/tree-item.component.js'
const router = useRouter() const router = useRouter()
const treeState = useTreeState() const treeState = useTreeState()
const alerts = useAlertsStore() const alerts = useAlertsStore()
const ungrouped = computed(() => treeState.secretGroups.ungrouped) const ungrouped = computed(() => treeState.secretGroups?.ungrouped)
const secretGroups = computed(() => treeState.secretGroups.groups) const secretGroups = computed(() => treeState.secretGroups?.groups)
const groupSelected = computed(() => { const groupSelected = computed(() => {
if (!treeState.selected) { if (!treeState.selected) {
return false return false
} }
if (treesState.selected.objectType === SshecretObject.SecretGroup) { if (treeState.selected.objectType === SshecretObjectType.SecretGroup) {
return true return true
} }
return false return false
@ -105,7 +106,7 @@ const createDrawerKey = ref(0)
const createGroupDrawer = ref<boolean>(false) const createGroupDrawer = ref<boolean>(false)
const createSecretDrawer = ref<boolean>(false) const createSecretDrawer = ref<boolean>(false)
const createErrors = ref([]) const createErrors = ref<any[]>([])
function cancelCreateGroup() { function cancelCreateGroup() {
createGroupDrawer.value = false createGroupDrawer.value = false
@ -123,7 +124,6 @@ const createSecretParent = computed(() => {
if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) { if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) {
return treeState.selected.id return treeState.selected.id
} }
return null
}) })
async function loadGroups() { async function loadGroups() {
@ -132,7 +132,7 @@ async function loadGroups() {
const drawerKeyName = computed(() => `${currentPath.value}_${drawerKey.value}`) const drawerKeyName = computed(() => `${currentPath.value}_${drawerKey.value}`)
async function itemSelected(event: Event) { function itemSelected(event: CustomEvent<{ selection: SlTreeItem[] }>) {
if (event.detail.selection.length == 0) { if (event.detail.selection.length == 0) {
treeState.unselect() treeState.unselect()
} else { } else {
@ -140,13 +140,15 @@ async function itemSelected(event: Event) {
const childType = el.dataset.type const childType = el.dataset.type
if (childType === 'secret') { if (childType === 'secret') {
const secretName = el.dataset.name const secretName = el.dataset.name
treeState.selectSecret(secretName, null) if (secretName) {
treeState.selectSecret(secretName)
router.push({ name: 'Secret', params: { id: secretName } }) router.push({ name: 'Secret', params: { id: secretName } })
}
} else if (childType === 'group') { } else if (childType === 'group') {
const groupPath = el.dataset.groupPath const groupPath = el.dataset.groupPath
if (groupPath === 'ungrouped') { if (groupPath === 'ungrouped') {
treeState.unselect() treeState.unselect()
} else { } else if (groupPath) {
const groupPathElements = splitPath(groupPath) const groupPathElements = splitPath(groupPath)
treeState.selectGroup(groupPath) treeState.selectGroup(groupPath)
router.push({ name: 'Group', params: { groupPath: groupPathElements } }) router.push({ name: 'Group', params: { groupPath: groupPathElements } })
@ -195,9 +197,11 @@ async function createSecret(secretCreate: SecretCreate) {
} catch (err) { } catch (err) {
if (err instanceof ValidationError) { if (err instanceof ValidationError) {
createErrors.value = err.errors createErrors.value = err.errors
} else { } else if (err instanceof ApiError) {
const errorMessage = err.message ?? 'Unknown error' const errorMessage = err.message ?? 'Unknown error'
alerts.showAlert(`Error from backend: ${errorMessage}`, 'error') alerts.showAlert(`Error from backend: ${errorMessage}`, 'error')
} else {
alerts.showAlert(`Error from backend: ${err}`, 'error')
} }
} }
} }

View File

@ -5,10 +5,12 @@ import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
/** @type {import('vite').UserConfig} */ /** @type {import('vite').UserConfig} */
export default defineConfig({ export default defineConfig({
envPrefix: "SSHECRET_FRONTEND_", envPrefix: "SSHECRET_FRONTEND_",
base: process.env.NODE_ENV === "production" ? "/admin/" : "/",
plugins: [ plugins: [
vue({ vue({
template: { template: {