Fix type errors
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,9 +269,12 @@ 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)
|
||||||
onMounted(loadLogs)
|
onMounted(loadLogs)
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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[]>([])
|
||||||
|
|||||||
@ -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)
|
||||||
@ -129,6 +135,7 @@ function validatePublicKey() {
|
|||||||
|
|
||||||
setFieldValidation(publicKeyField, '')
|
setFieldValidation(publicKeyField, '')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
if (clientCreateForm.value?.checkValidity()) {
|
if (clientCreateForm.value?.checkValidity()) {
|
||||||
|
|||||||
@ -14,17 +14,21 @@
|
|||||||
</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)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,8 +251,10 @@ 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
|
||||||
clientIds.forEach((clientId) => emit('addClient', clientId))
|
clientIds.forEach((clientId) => emit('addClient', clientId))
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<MasterDetailWorkspace />
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import MasterDetailWorkspace from '@/components/common/MasterDetailWorkspace.vue'
|
|
||||||
</script>
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user