Dashboard and error handling
This commit is contained in:
File diff suppressed because one or more lines are too long
31
packages/sshecret-frontend/src/api/assertSdkResponseOk.ts
Normal file
31
packages/sshecret-frontend/src/api/assertSdkResponseOk.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// /src/utils/assertSdkResponseOk.ts
|
||||||
|
import type { AxiosResponse, AxiosError } from 'axios'
|
||||||
|
import { ApiError, ValidationError, NotFoundError } from '@/api/errors'
|
||||||
|
import { isHttpValidationError } from './typeguards';
|
||||||
|
|
||||||
|
export type SDKResponse<T> =
|
||||||
|
| (AxiosResponse<T> & { error: undefined })
|
||||||
|
| (AxiosError<any> & { data: undefined; error: any })
|
||||||
|
|
||||||
|
export function assertSdkResponseOk<T>(response: SDKResponse<T>): T {
|
||||||
|
if ('error' in response && response.error) {
|
||||||
|
const status = response.status ?? 500
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
throw new NotFoundError("Not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 422 && isHttpValidationError(response.error)) {
|
||||||
|
throw new ValidationError(response.error.detail ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = response.error.detail ?? 'Unknown error'
|
||||||
|
throw new ApiError(`API error: ${errorMessage}`, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('data' in response && response.data) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError('Invalid response shape', 500)
|
||||||
|
}
|
||||||
28
packages/sshecret-frontend/src/api/errors.ts
Normal file
28
packages/sshecret-frontend/src/api/errors.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
public status: number
|
||||||
|
|
||||||
|
constructor(message: string, status: number) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ApiError'
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends ApiError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, 404)
|
||||||
|
this.name = "NotFoundError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends ApiError {
|
||||||
|
public errors: any[]
|
||||||
|
|
||||||
|
constructor(errors: any[], message: string = 'Validation failed') {
|
||||||
|
super(message, 422)
|
||||||
|
this.name = 'ValidationError'
|
||||||
|
this.errors = errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { client } from '@/client/client.gen'
|
import { client } from '@/client/client.gen'
|
||||||
import { useAuthTokenStore } from '@/store/auth.ts'
|
import { useAuthTokenStore } from '@/store/auth.ts'
|
||||||
|
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
|
||||||
client.instance.interceptors.response.use(
|
client.instance.interceptors.response.use(
|
||||||
response => response,
|
response => response,
|
||||||
@ -11,6 +13,7 @@ client.instance.interceptors.response.use(
|
|||||||
if (originalRequest.url.includes("/refresh")) {
|
if (originalRequest.url.includes("/refresh")) {
|
||||||
const auth = useAuthTokenStore()
|
const auth = useAuthTokenStore()
|
||||||
auth.logout()
|
auth.logout()
|
||||||
|
router.push({ name: 'login' })
|
||||||
return Promise.reject("Refresh failed - logged out")
|
return Promise.reject("Refresh failed - logged out")
|
||||||
}
|
}
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
@ -27,6 +30,6 @@ client.instance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
return Promise.reject("Could not refresh token")
|
return Promise.reject("Could not refresh token")
|
||||||
}
|
}
|
||||||
return Promise.reject("Could not refresh token")
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { OPERATIONS } from '@/api/types'
|
import { OPERATIONS } from '@/api/types'
|
||||||
import { SUBSYSTEM } from '@/api/types'
|
import { SUBSYSTEM } from '@/api/types'
|
||||||
import type { Operation, SubSystem } from '@/client'
|
import type { HttpValidationError, Operation, SubSystem } from '@/client'
|
||||||
|
|
||||||
export function isOperation(value: string): value is Operation {
|
export function isOperation(value: string): value is Operation {
|
||||||
return (OPERATIONS as readonly string[]).includes(value)
|
return (OPERATIONS as readonly string[]).includes(value)
|
||||||
@ -11,3 +11,7 @@ export function isOperation(value: string): value is Operation {
|
|||||||
export function isSubSystem(value: string): value is SubSystem {
|
export function isSubSystem(value: string): value is SubSystem {
|
||||||
return (SUBSYSTEM as readonly string[]).includes(value)
|
return (SUBSYSTEM as readonly string[]).includes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isHttpValidationError(error: unknown): error is HttpValidationError {
|
||||||
|
return !!(error && typeof error === 'object' && 'detail' in error)
|
||||||
|
}
|
||||||
|
|||||||
@ -53,3 +53,12 @@ sl-checkbox:focus-within[data-user-valid]::part(control) {
|
|||||||
border-color: var(--sl-color-success-600);
|
border-color: var(--sl-color-success-600);
|
||||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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%;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { type Options as ClientOptions, type TDataShape, type Client, urlSearchParamsBodySerializer } from './client';
|
import { type Options as ClientOptions, type TDataShape, type Client, urlSearchParamsBodySerializer } from './client';
|
||||||
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
|
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSystemStatsApiV1StatsGetData, GetSystemStatsApiV1StatsGetResponses, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
|
||||||
import { client as _heyApiClient } from './client.gen';
|
import { client as _heyApiClient } from './client.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||||
@ -300,6 +300,24 @@ export class SshecretAdmin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get System Stats
|
||||||
|
* Get system stats.
|
||||||
|
*/
|
||||||
|
public static getSystemStatsApiV1StatsGet<ThrowOnError extends boolean = false>(options?: Options<GetSystemStatsApiV1StatsGetData, ThrowOnError>) {
|
||||||
|
return (options?.client ?? _heyApiClient).get<GetSystemStatsApiV1StatsGetResponses, unknown, ThrowOnError>({
|
||||||
|
responseType: 'json',
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
scheme: 'bearer',
|
||||||
|
type: 'http'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
url: '/api/v1/stats',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Secret Names
|
* Get Secret Names
|
||||||
* Get Secret Names.
|
* Get Secret Names.
|
||||||
|
|||||||
@ -477,10 +477,7 @@ export type SecretView = {
|
|||||||
* Secret
|
* Secret
|
||||||
*/
|
*/
|
||||||
secret: string | null;
|
secret: string | null;
|
||||||
/**
|
group?: GroupReference | null;
|
||||||
* Group
|
|
||||||
*/
|
|
||||||
group?: string | null;
|
|
||||||
/**
|
/**
|
||||||
* Clients
|
* Clients
|
||||||
*/
|
*/
|
||||||
@ -493,6 +490,25 @@ export type SecretView = {
|
|||||||
*/
|
*/
|
||||||
export type SubSystem = 'admin' | 'sshd' | 'backend';
|
export type SubSystem = 'admin' | 'sshd' | 'backend';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemStats
|
||||||
|
* Generic system stats.
|
||||||
|
*/
|
||||||
|
export type SystemStats = {
|
||||||
|
/**
|
||||||
|
* Clients
|
||||||
|
*/
|
||||||
|
clients: number;
|
||||||
|
/**
|
||||||
|
* Secrets
|
||||||
|
*/
|
||||||
|
secrets: number;
|
||||||
|
/**
|
||||||
|
* Audit Events
|
||||||
|
*/
|
||||||
|
audit_events: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token
|
* Token
|
||||||
*/
|
*/
|
||||||
@ -1031,6 +1047,22 @@ export type UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses = {
|
|||||||
|
|
||||||
export type UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponse = UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses[keyof UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses];
|
export type UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponse = UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses[keyof UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses];
|
||||||
|
|
||||||
|
export type GetSystemStatsApiV1StatsGetData = {
|
||||||
|
body?: never;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/api/v1/stats';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSystemStatsApiV1StatsGetResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: SystemStats;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSystemStatsApiV1StatsGetResponse = GetSystemStatsApiV1StatsGetResponses[keyof GetSystemStatsApiV1StatsGetResponses];
|
||||||
|
|
||||||
export type GetSecretNamesApiV1SecretsGetData = {
|
export type GetSecretNamesApiV1SecretsGetData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@ -79,6 +79,8 @@ 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'
|
||||||
|
|
||||||
const auditFilterState = useAuditFilterState()
|
const auditFilterState = useAuditFilterState()
|
||||||
|
|
||||||
const clientNames = ref<string[]>()
|
const clientNames = ref<string[]>()
|
||||||
@ -94,16 +96,14 @@ const filterForm = reactive({
|
|||||||
|
|
||||||
async function fetchClientNames() {
|
async function fetchClientNames() {
|
||||||
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
||||||
if (response.data) {
|
const responseData = assertSdkResponseOk(response)
|
||||||
clientNames.value = response.data.map((x) => x.name)
|
clientNames.value = responseData.map((x) => x.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSecretNames() {
|
async function fetchSecretNames() {
|
||||||
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
||||||
if (response.data) {
|
const responseData = assertSdkResponseOk(response)
|
||||||
secretNames.value = response.data.map((x) => x.name)
|
secretNames.value = responseData.map((x) => x.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNames() {
|
async function fetchNames() {
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white dark:bg-gray-800">
|
<tbody class="bg-white dark:bg-gray-800">
|
||||||
<template v-for="row in 25">
|
<template v-for="row in amount">
|
||||||
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
<td class="w-[1rem]">
|
<td class="w-[1rem]">
|
||||||
<sl-skeleton class="iconSkeleton"></sl-skeleton>
|
<sl-skeleton class="iconSkeleton"></sl-skeleton>
|
||||||
@ -84,6 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
const props = defineProps<{ amount: number }>()
|
||||||
|
|
||||||
|
const amount = toRef(() => props.amount ?? 25)
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
tr.auditRow {
|
tr.auditRow {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
|
|||||||
@ -147,43 +147,45 @@
|
|||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div
|
<template v-if="shouldPaginate">
|
||||||
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
|
<div
|
||||||
v-if="totalPages > 0"
|
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
|
||||||
>
|
v-if="totalPages > 0"
|
||||||
<div class="flex items-center mb-4 sm:mb-0">
|
>
|
||||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
<div class="flex items-center mb-4 sm:mb-0">
|
||||||
Showing
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
<span
|
Showing
|
||||||
class="font-semibold text-gray-900 dark:text-white"
|
<span
|
||||||
v-if="totalEntries < lastResult"
|
class="font-semibold text-gray-900 dark:text-white"
|
||||||
>
|
v-if="totalEntries < lastResult"
|
||||||
{{ firstResult }}-{{ TotalEntries }}
|
>
|
||||||
|
{{ firstResult }}-{{ TotalEntries }}
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white" v-else>
|
||||||
|
{{ firstResult }}-{{ lastResult }}
|
||||||
|
</span>
|
||||||
|
of
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ totalEntries }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-gray-900 dark:text-white" v-else>
|
</div>
|
||||||
{{ firstResult }}-{{ lastResult }}
|
<div class="flex items-center space-x-3">
|
||||||
</span>
|
<div class="flex space-x-1">
|
||||||
of
|
<PageNumbers
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">
|
@next="nextPage"
|
||||||
{{ totalEntries }}
|
@previous="prevPage"
|
||||||
</span>
|
@goto="goToPage"
|
||||||
</span>
|
:pageNum="pageNum"
|
||||||
</div>
|
:totalPages="totalPages"
|
||||||
<div class="flex items-center space-x-3">
|
/>
|
||||||
<div class="flex space-x-1">
|
</div>
|
||||||
<PageNumbers
|
|
||||||
@next="nextPage"
|
|
||||||
@previous="prevPage"
|
|
||||||
@goto="goToPage"
|
|
||||||
:pageNum="pageNum"
|
|
||||||
:totalPages="totalPages"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<AuditSkeleton />
|
<AuditSkeleton :amount="perPage" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -193,11 +195,18 @@ import { usePagination } from '@/composables/usePagination'
|
|||||||
import { SshecretAdmin, GetAuditLogApiV1AuditGetData } from '@/client'
|
import { SshecretAdmin, GetAuditLogApiV1AuditGetData } from '@/client'
|
||||||
import type { AuditListResult } from '@/client'
|
import type { AuditListResult } from '@/client'
|
||||||
import type { AuditFilter } from '@/api/types'
|
import type { AuditFilter } from '@/api/types'
|
||||||
|
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'
|
||||||
|
|
||||||
const props = defineProps<{ auditFilter: AuditFilter }>()
|
interface Props {
|
||||||
|
auditFilter: AuditFilter
|
||||||
|
paginate?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const shouldPaginate = toRef(() => props.paginate ?? true)
|
||||||
const auditFilter = toRef(() => props.auditFilter)
|
const auditFilter = toRef(() => props.auditFilter)
|
||||||
console.log(auditFilter.value)
|
console.log(auditFilter.value)
|
||||||
const auditList = ref<AuditListResult>([])
|
const auditList = ref<AuditListResult>([])
|
||||||
@ -220,7 +229,8 @@ async function loadLogs() {
|
|||||||
const response = await SshecretAdmin.getAuditLogApiV1AuditGet({
|
const response = await SshecretAdmin.getAuditLogApiV1AuditGet({
|
||||||
query: queryInput.value,
|
query: queryInput.value,
|
||||||
})
|
})
|
||||||
auditList.value = response.data
|
const responseData = assertSdkResponseOk(response)
|
||||||
|
auditList.value = responseData
|
||||||
}
|
}
|
||||||
|
|
||||||
const expanded = ref(new Set<string>())
|
const expanded = ref(new Set<string>())
|
||||||
|
|||||||
@ -89,7 +89,13 @@
|
|||||||
</sl-tab-group>
|
</sl-tab-group>
|
||||||
|
|
||||||
<sl-drawer label="Edit Client" :open="updateDrawerOpen" @sl-hide="updateDrawerOpen = false">
|
<sl-drawer label="Edit Client" :open="updateDrawerOpen" @sl-hide="updateDrawerOpen = false">
|
||||||
<ClientForm @submit="updateClient" @cancel="updateDrawerOpen = false" :client="client" />
|
<ClientForm
|
||||||
|
@submit="updateClient"
|
||||||
|
@cancel="updateDrawerOpen = false"
|
||||||
|
@clearErrors="clearFormErrors"
|
||||||
|
:client="client"
|
||||||
|
:errors="formErrors"
|
||||||
|
/>
|
||||||
</sl-drawer>
|
</sl-drawer>
|
||||||
<sl-dialog label="Are you sure?" :open="showConfirm">
|
<sl-dialog label="Are you sure?" :open="showConfirm">
|
||||||
Are you sure you want to delete this client?
|
Are you sure you want to delete this client?
|
||||||
@ -107,7 +113,15 @@ 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<{ client: Client }>()
|
const props = defineProps({
|
||||||
|
client: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
updateErrors: {
|
||||||
|
type: Array,
|
||||||
|
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
|
||||||
@ -115,6 +129,12 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const localClient = ref({ ...props.client })
|
const localClient = ref({ ...props.client })
|
||||||
|
|
||||||
|
const formErrors = ref([])
|
||||||
|
|
||||||
|
function clearFormErrors() {
|
||||||
|
formErrors.value = []
|
||||||
|
}
|
||||||
|
|
||||||
const updateDrawerOpen = ref<boolean>(false)
|
const updateDrawerOpen = ref<boolean>(false)
|
||||||
const showConfirm = ref<boolean>(false)
|
const showConfirm = ref<boolean>(false)
|
||||||
const clientSecretCount = computed(() => localClient.value.secrets.length)
|
const clientSecretCount = computed(() => localClient.value.secrets.length)
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
:disabled="isEdit"
|
:disabled="isEdit"
|
||||||
:value="name"
|
:value="name"
|
||||||
@blur="checkName"
|
@blur="checkName"
|
||||||
@input="name = $event.target.value"
|
@sl-input="name = $event.target.value"
|
||||||
|
@input="emit('clearErrors')"
|
||||||
ref="nameField"
|
ref="nameField"
|
||||||
></sl-input>
|
></sl-input>
|
||||||
<br />
|
<br />
|
||||||
@ -79,15 +80,22 @@ 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('')
|
// This key is only here during testing.
|
||||||
|
const publicKey = ref(
|
||||||
|
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC737Yj7mbuBLDNbAuNGqhFF4Cvzd/ROq/QeQX0QIcPyZOoUtpXc7R/JIrdL6DXkPYXpN/IrUFoSeJQjV9Le+ewVxYELUPVhF0/nQhpBNE1Rjx2PRtJlfmywG5VRStgPQ+DSTDtgm4L0wPpnJiH3udkq/JFMHEYrVAF40QqNmR7AqYo1ZfEFk8YcQGb/S29JxWigq0qoJyufFENmSGNmabjqPAWJEf/oshMPaxwlDfTdmjeUWkPtsm10gi98XCwtnVCAVYZdVKeLSNpQCKUYVYWlycpahNczaITY9lehcMtux79uXTk2d4difra1Q4guw8oorUp1eRn/Al0BPeRb7x9WdgRs8wVY1kPD2796CTAQMkeBrOzGxwzwWhTf1XOuHG/wB5O2QSbcC6aMW9KAFmcCF+AOMb8Mv2Y5D7l/gbp938qTyZJ8ivP1/fy/88CWr+mrv5yP4HOZmNCyC9nMlAvrS/Kkg0tFU+NHFkDsmWpT3oar+VvGzkImEF6ip6Mzk8= testkey',
|
||||||
|
)
|
||||||
|
|
||||||
const nameField = ref<HTMLSlInputElement>()
|
const nameField = ref<HTMLSlInputElement>()
|
||||||
const sourceField = ref<HTMLSlInputElement>()
|
const sourceField = ref<HTMLSlInputElement>()
|
||||||
const publicKeyField = ref<HTMLSlInputElement>()
|
const publicKeyField = ref<HTMLSlInputElement>()
|
||||||
const clientCreateForm = ref<HTMLElement>()
|
const clientCreateForm = ref<HTMLElement>()
|
||||||
|
|
||||||
const props = defineProps<{ client?: Client | null }>()
|
const props = defineProps<{ client?: Client | null; errors: any[] | null }>()
|
||||||
const emit = defineEmits<{ (e: 'submit', data: ClientCreate): void; (e: 'cancel'): void }>()
|
const emit = defineEmits<{
|
||||||
|
(e: 'submit', data: ClientCreate): void
|
||||||
|
(e: 'cancel'): void
|
||||||
|
(e: 'clearErrors'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const isEdit = computed(() => !!props.client)
|
const isEdit = computed(() => !!props.client)
|
||||||
|
|
||||||
@ -138,6 +146,7 @@ function removePolicy(index: number) {
|
|||||||
|
|
||||||
function checkName() {
|
function checkName() {
|
||||||
nameField.value.reportValidity()
|
nameField.value.reportValidity()
|
||||||
|
emit('clearErrors')
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePublicKey() {
|
function validatePublicKey() {
|
||||||
@ -160,6 +169,19 @@ function validatePublicKey() {
|
|||||||
setFieldValidation(publicKeyField, '')
|
setFieldValidation(publicKeyField, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.errors,
|
||||||
|
(errors) => {
|
||||||
|
const nameErrors = errors.filter((e) => e.loc.includes('name'))
|
||||||
|
if (nameErrors.length > 0) {
|
||||||
|
console.log(nameErrors)
|
||||||
|
setFieldValidation(nameField, nameErrors[0].msg)
|
||||||
|
} else {
|
||||||
|
setFieldValidation(nameField, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
if (clientCreateForm.value?.checkValidity()) {
|
if (clientCreateForm.value?.checkValidity()) {
|
||||||
let clientDescription: string | null = null
|
let clientDescription: string | null = null
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
|
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk'
|
||||||
import type { ClientReference } from '@/client'
|
import type { ClientReference } from '@/client'
|
||||||
|
|
||||||
const clients = ref<ClientReference[]>([])
|
const clients = ref<ClientReference[]>([])
|
||||||
@ -11,9 +12,9 @@ const emit = defineEmits<{ (e: 'update:modelValue', data: string[]): void }>()
|
|||||||
async function getClients() {
|
async function getClients() {
|
||||||
// Get just names and IDs of the clients
|
// Get just names and IDs of the clients
|
||||||
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
||||||
if (response.data) {
|
|
||||||
clients.value = response.data
|
const responseData = assertSdkResponseOk(response)
|
||||||
}
|
clients.value = responseData
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(getClients)
|
onMounted(getClients)
|
||||||
|
|||||||
@ -66,11 +66,7 @@ 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 '@shoelace-style/shoelace/dist/components/button/button.js'
|
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk'
|
||||||
import '@shoelace-style/shoelace/dist/components/input/input.js'
|
|
||||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tag/tag.js'
|
|
||||||
import '@shoelace-style/shoelace/dist/components/textarea/textarea.js'
|
|
||||||
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'
|
||||||
|
|
||||||
@ -152,65 +148,7 @@ async function submitForm() {
|
|||||||
sources: [...policies.value],
|
sources: [...policies.value],
|
||||||
}
|
}
|
||||||
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: clientCreate })
|
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: clientCreate })
|
||||||
console.log(response.data)
|
assertSdkResponseOk(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.client-form sl-input,
|
|
||||||
.client-form sl-select,
|
|
||||||
.client-form sl-checkbox {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: var(--sl-spacing-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* user invalid styles */
|
|
||||||
.client-form sl-input[data-user-invalid]::part(base),
|
|
||||||
.client-form sl-select[data-user-invalid]::part(combobox),
|
|
||||||
.client-form sl-checkbox[data-user-invalid]::part(control) {
|
|
||||||
border-color: var(--sl-color-danger-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form [data-user-invalid]::part(form-control-label),
|
|
||||||
.client-form [data-user-invalid]::part(form-control-help-text),
|
|
||||||
.client-form sl-checkbox[data-user-invalid]::part(label) {
|
|
||||||
color: var(--sl-color-danger-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form sl-checkbox[data-user-invalid]::part(control) {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form sl-input:focus-within[data-user-invalid]::part(base),
|
|
||||||
.client-form sl-select:focus-within[data-user-invalid]::part(combobox),
|
|
||||||
.client-form sl-checkbox:focus-within[data-user-invalid]::part(control) {
|
|
||||||
border-color: var(--sl-color-danger-600);
|
|
||||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* User valid styles */
|
|
||||||
.client-form sl-input[data-user-valid]::part(base),
|
|
||||||
.client-form sl-select[data-user-valid]::part(combobox),
|
|
||||||
.client-form sl-checkbox[data-user-valid]::part(control) {
|
|
||||||
border-color: var(--sl-color-success-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form [data-user-valid]::part(form-control-label),
|
|
||||||
.client-form [data-user-valid]::part(form-control-help-text),
|
|
||||||
.client-form sl-checkbox[data-user-valid]::part(label) {
|
|
||||||
color: var(--sl-color-success-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form sl-checkbox[data-user-valid]::part(control) {
|
|
||||||
background-color: var(--sl-color-success-600);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-form sl-input:focus-within[data-user-valid]::part(base),
|
|
||||||
.client-form sl-select:focus-within[data-user-valid]::part(combobox),
|
|
||||||
.client-form sl-checkbox:focus-within[data-user-valid]::part(control) {
|
|
||||||
border-color: var(--sl-color-success-600);
|
|
||||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
22
packages/sshecret-frontend/src/components/common/Dialog.vue
Normal file
22
packages/sshecret-frontend/src/components/common/Dialog.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<sl-dialog ref="dialogRef" :open="open" @sl-hide="handleHide" :label="label">
|
||||||
|
<slot />
|
||||||
|
<div slot="footer">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</sl-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useBubbleSafeHandler } from '@/composables/useBubbleSafeHandler'
|
||||||
|
|
||||||
|
const props = defineProps<{ label: string; open: boolean }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'hide'): void }>()
|
||||||
|
const dialogRef = ref<HTMLSlDrawerElement>()
|
||||||
|
|
||||||
|
const handleHide = useBubbleSafeHandler(dialogRef, () => {
|
||||||
|
emit('hide')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -9,17 +9,23 @@
|
|||||||
<sl-tab slot="nav" panel="audit" :active="selectedTab === 'audit'">Audit</sl-tab>
|
<sl-tab slot="nav" panel="audit" :active="selectedTab === 'audit'">Audit</sl-tab>
|
||||||
<sl-tab-panel name="clients">
|
<sl-tab-panel name="clients">
|
||||||
<slot name="clients">
|
<slot name="clients">
|
||||||
<ClientTreeList />
|
<template v-if="selectedTab === 'clients'">
|
||||||
|
<ClientTreeList />
|
||||||
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</sl-tab-panel>
|
</sl-tab-panel>
|
||||||
<sl-tab-panel name="secrets">
|
<sl-tab-panel name="secrets">
|
||||||
<slot name="secrets">
|
<slot name="secrets">
|
||||||
<SecretTreeList />
|
<template v-if="selectedTab === 'secrets'">
|
||||||
|
<SecretTreeList />
|
||||||
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</sl-tab-panel>
|
</sl-tab-panel>
|
||||||
<sl-tab-panel name="audit">
|
<sl-tab-panel name="audit">
|
||||||
<slot name="audit">
|
<slot name="audit">
|
||||||
<AuditFilters />
|
<template v-if="selectedTab === 'audit'">
|
||||||
|
<AuditFilters />
|
||||||
|
</template>
|
||||||
</slot>
|
</slot>
|
||||||
</sl-tab-panel>
|
</sl-tab-panel>
|
||||||
</sl-tab-group>
|
</sl-tab-group>
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col justify-center items-center px-6 xl:px-0 dark:bg-gray-900">
|
||||||
|
<div class="text-center xl:max-w-4xl">
|
||||||
|
<h1
|
||||||
|
class="mb-3 text-2xl font-bold leading-tight text-gray-900 sm:text-4xl lg:text-5xl dark:text-white"
|
||||||
|
>
|
||||||
|
Not found
|
||||||
|
</h1>
|
||||||
|
<p class="mb-5 text-base font-normal text-gray-500 md:text-lg dark:text-gray-400">
|
||||||
|
Oops! Looks like you followed a link to an item that does not exist anymore. The item may
|
||||||
|
have been deleted.
|
||||||
|
</p>
|
||||||
|
<sl-button @click="router.go(-1)">
|
||||||
|
<sl-icon name="arrow-left" slot="prefix"></sl-icon>
|
||||||
|
Back
|
||||||
|
</sl-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
</script>
|
||||||
@ -1,23 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-end px-4">
|
<div class="flex justify-end px-4">
|
||||||
<button
|
<sl-dropdown>
|
||||||
id="secret-menu-button"
|
<div slot="trigger">
|
||||||
class="inline-block text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-1.5"
|
<sl-icon-button name="three-dots"></sl-icon-button>
|
||||||
type="button"
|
</div>
|
||||||
>
|
<sl-menu>
|
||||||
<span class="sr-only">Open dropdown</span>
|
<sl-menu-item value="delete">
|
||||||
<svg
|
<span class="text-red-600" @click="showConfirm = true">Delete secret</span>
|
||||||
class="w-5 h-5"
|
</sl-menu-item>
|
||||||
aria-hidden="true"
|
</sl-menu>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</sl-dropdown>
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 16 3"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm6.041 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM14 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<sl-tab-group>
|
<sl-tab-group>
|
||||||
<sl-tab slot="nav" panel="secret_data">Secret Data</sl-tab>
|
<sl-tab slot="nav" panel="secret_data">Secret Data</sl-tab>
|
||||||
@ -100,6 +92,21 @@
|
|||||||
Secret is not managed, and can only decrypted by the client
|
Secret is not managed, and can only decrypted by the client
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">Group</dt>
|
||||||
|
<dd
|
||||||
|
class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300"
|
||||||
|
v-if="secret.group"
|
||||||
|
>
|
||||||
|
{{ secret.group.path }}
|
||||||
|
<div class="mt-2 float-right">
|
||||||
|
<sl-button size="medium" variant="default" outline>
|
||||||
|
<sl-icon slot="prefix" name="box-arrow-in-right"></sl-icon>
|
||||||
|
Move
|
||||||
|
</sl-button>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -165,6 +172,13 @@
|
|||||||
v-if="addDialog"
|
v-if="addDialog"
|
||||||
/>
|
/>
|
||||||
</sl-drawer>
|
</sl-drawer>
|
||||||
|
<Dialog label="Are you sure?" :open="showConfirm" @hide="showConfirm = false">
|
||||||
|
Are you sure you want to delete this secret?
|
||||||
|
<template #footer>
|
||||||
|
<sl-button variant="default" @click="showConfirm = false" class="mr-2">Cancel</sl-button>
|
||||||
|
<sl-button variant="danger" @click="deleteSecret">Delete</sl-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -173,6 +187,7 @@ import type { SecretView } from '@/client/types.gen.ts'
|
|||||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||||
|
|
||||||
import AddSecretToClients from '@/components/secrets/AddSecretToClients.vue'
|
import AddSecretToClients from '@/components/secrets/AddSecretToClients.vue'
|
||||||
|
import Dialog from '@/components/common/Dialog.vue'
|
||||||
|
|
||||||
const props = defineProps<{ secret: SecretView }>()
|
const props = defineProps<{ secret: SecretView }>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -190,6 +205,8 @@ const secretValue = ref<string | null>(secret.value?.secret)
|
|||||||
|
|
||||||
const addDialog = ref<boolean>(false)
|
const addDialog = ref<boolean>(false)
|
||||||
|
|
||||||
|
const showConfirm = ref<boolean>(false)
|
||||||
|
|
||||||
const secretChanged = computed(() => {
|
const secretChanged = computed(() => {
|
||||||
if (!secretValue.value) {
|
if (!secretValue.value) {
|
||||||
return false
|
return false
|
||||||
@ -223,4 +240,8 @@ function removeClient(clientId: string, event: any) {
|
|||||||
console.log(event.target.parentNode)
|
console.log(event.target.parentNode)
|
||||||
emit('removeClient', clientId)
|
emit('removeClient', clientId)
|
||||||
}
|
}
|
||||||
|
function deleteSecret() {
|
||||||
|
showConfirm.value = false
|
||||||
|
emit('delete')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -108,10 +108,14 @@ function submitCreateSecret() {
|
|||||||
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 = []
|
||||||
|
if (Array.isArray(selectedClients.value)) {
|
||||||
|
secretClients = [...selectedClients.value]
|
||||||
|
}
|
||||||
const secretCreate: SecretCreate = {
|
const secretCreate: SecretCreate = {
|
||||||
value: secretValue.value,
|
value: secretValue.value,
|
||||||
name: secretName.value,
|
name: secretName.value,
|
||||||
clients: [...selectedClients.value],
|
clients: secretClients,
|
||||||
client_distinguisher: 'id',
|
client_distinguisher: 'id',
|
||||||
group: secretGroup,
|
group: secretGroup,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import LoginPage from '@/views/LoginPage.vue'
|
import LoginPage from '@/views/LoginPage.vue'
|
||||||
import WorkspaceView from '@/views/WorkspaceView.vue'
|
import Dashboard from '@/views/Dashboard.vue'
|
||||||
import AuditPage from '@/views/audit/AuditPage.vue'
|
import AuditPage from '@/views/audit/AuditPage.vue'
|
||||||
import ClientPage from '@/views/clients/ClientPage.vue'
|
import ClientPage from '@/views/clients/ClientPage.vue'
|
||||||
import SecretPage from '@/views/secrets/SecretPage.vue'
|
import SecretPage from '@/views/secrets/SecretPage.vue'
|
||||||
@ -17,9 +17,11 @@ const routes = [
|
|||||||
{ path: '/login', name: 'login', component: LoginPage },
|
{ path: '/login', name: 'login', component: LoginPage },
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'clientList',
|
component: ClientPage,
|
||||||
component: WorkspaceView,
|
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
|
children: [{
|
||||||
|
path: '', component: Dashboard, name: 'dashboard', meta: { requiresAuth: true },
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/audit', name: 'audit', component: AuditPage, meta: { requiresAuth: true }
|
path: '/audit', name: 'audit', component: AuditPage, meta: { requiresAuth: true }
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SshecretObjectType } from '@/api/types'
|
|||||||
import type { SshecretObject } from '@/api/types'
|
import type { SshecretObject } from '@/api/types'
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
import type { ClientQueryResult, Client, SecretView, ClientSecretGroupList, SecretListView, ClientSecretGroup } from '@/client'
|
import type { ClientQueryResult, Client, SecretView, ClientSecretGroupList, SecretListView, ClientSecretGroup } from '@/client'
|
||||||
|
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
|
||||||
|
|
||||||
export const useTreeState = defineStore('treeState', {
|
export const useTreeState = defineStore('treeState', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@ -55,12 +56,9 @@ export const useTreeState = defineStore('treeState', {
|
|||||||
limit,
|
limit,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (response.data) {
|
const clientData = assertSdkResponseOk(response)
|
||||||
this.clients = response.data
|
this.clients = clientData
|
||||||
return response.data.total_results
|
return clientData.total_results
|
||||||
}
|
|
||||||
this.clients = null
|
|
||||||
return 0
|
|
||||||
},
|
},
|
||||||
async queryClients(query: string, offset: number = 0, limit: number = 100): Promise<number> {
|
async queryClients(query: string, offset: number = 0, limit: number = 100): Promise<number> {
|
||||||
// Query or search. Result is the number of hits.
|
// Query or search. Result is the number of hits.
|
||||||
@ -74,30 +72,21 @@ export const useTreeState = defineStore('treeState', {
|
|||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (response.data) {
|
|
||||||
this.clients = response.data
|
|
||||||
return response.data.total_results
|
|
||||||
}
|
|
||||||
this.clients = null
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
const clientData = assertSdkResponseOk(response)
|
||||||
|
this.clients = clientData
|
||||||
|
return clientData.total_results
|
||||||
},
|
},
|
||||||
async getSecretNames(): Promise<boolean> {
|
async getSecretNames(): Promise<boolean> {
|
||||||
// Get all secret names.
|
// Get all secret names.
|
||||||
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
||||||
if (response.data) {
|
this.secrets = assertSdkResponseOk(response)
|
||||||
this.secrets = response.data
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
async getSecretGroups(): Promise<boolean> {
|
async getSecretGroups(): Promise<boolean> {
|
||||||
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet()
|
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet()
|
||||||
if (response.data) {
|
this.secretGroups = assertSdkResponseOk(response)
|
||||||
this.secretGroups = response.data
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
async getClient(id: string | null = null): Promise<Client> {
|
async getClient(id: string | null = null): Promise<Client> {
|
||||||
if (!id && this.selected?.objectType === SshecretObjectType.Client) {
|
if (!id && this.selected?.objectType === SshecretObjectType.Client) {
|
||||||
@ -115,24 +104,15 @@ export const useTreeState = defineStore('treeState', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const response = await SshecretAdmin.getClientApiV1ClientsIdGet({ path: { id: id } })
|
const response = await SshecretAdmin.getClientApiV1ClientsIdGet({ path: { id: id } })
|
||||||
if (response.data) {
|
return assertSdkResponseOk(response)
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
throw "Client not found"
|
|
||||||
},
|
},
|
||||||
async getSecret(name: string): Promise<SecretView> {
|
async getSecret(name: string): Promise<SecretView> {
|
||||||
const response = await SshecretAdmin.getSecretApiV1SecretsNameGet({ path: { name: name } })
|
const response = await SshecretAdmin.getSecretApiV1SecretsNameGet({ path: { name: name } })
|
||||||
if (response.data) {
|
return assertSdkResponseOk(response)
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
throw "Secret not found"
|
|
||||||
},
|
},
|
||||||
async getGroup(path: string): Promise<ClientSecretGroup> {
|
async getGroup(path: string): Promise<ClientSecretGroup> {
|
||||||
const response = await SshecretAdmin.getSecretGroupApiV1SecretsGroupsGroupPathGet({ path: { group_path: path } })
|
const response = await SshecretAdmin.getSecretGroupApiV1SecretsGroupsGroupPathGet({ path: { group_path: path } })
|
||||||
if (response.data) {
|
return assertSdkResponseOk(response)
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
throw "Group not found"
|
|
||||||
},
|
},
|
||||||
async getSelected(): Promise<Client | SecretView | ClientSecretGroup | null> {
|
async getSelected(): Promise<Client | SecretView | ClientSecretGroup | null> {
|
||||||
if (!this.selected) {
|
if (!this.selected) {
|
||||||
@ -156,11 +136,9 @@ export const useTreeState = defineStore('treeState', {
|
|||||||
if (clientIndex >= 0) {
|
if (clientIndex >= 0) {
|
||||||
console.log("found client at index: ", clientIndex, this.clients.clients[clientIndex])
|
console.log("found client at index: ", clientIndex, this.clients.clients[clientIndex])
|
||||||
const response = await SshecretAdmin.getClientApiV1ClientsIdGet({ path: { id: id } })
|
const response = await SshecretAdmin.getClientApiV1ClientsIdGet({ path: { id: id } })
|
||||||
if (response.data) {
|
const newClient = assertSdkResponseOk(response)
|
||||||
const newClient = response.data
|
this.clients.clients = this.clients.clients.map(c => c.id === id ? newClient : c)
|
||||||
this.clients.clients = this.clients.clients.map(c => c.id === id ? newClient : c)
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
113
packages/sshecret-frontend/src/views/Dashboard.vue
Normal file
113
packages/sshecret-frontend/src/views/Dashboard.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-4 pt-6">
|
||||||
|
<div class="grid w-full grid-cols-1 gap-4 mt-4 xl:grid-cols-2 2xl:grid-cols-3">
|
||||||
|
<div
|
||||||
|
class="items-center justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800"
|
||||||
|
id="dashboard-stats-panel"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<h3 class="text-base text-gray-500 dark:text-gray-400">Stats</h3>
|
||||||
|
<dl
|
||||||
|
class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700"
|
||||||
|
v-if="stats"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 text-xs dark:text-gray-400">Clients</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-client-count">
|
||||||
|
{{ stats.clients }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col py-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secrets</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-secret-count">
|
||||||
|
{{ stats.secrets }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col py-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Audit Events</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-audit-count">
|
||||||
|
{{ stats.audit_events }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 text-xs dark:text-gray-400">
|
||||||
|
<sl-skeleton effect="pulse" class="stat-skeleton"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-client-count">
|
||||||
|
<sl-skeleton class="stat-val-skeleton" effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 text-xs dark:text-gray-400">
|
||||||
|
<sl-skeleton effect="pulse" class="stat-skeleton"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-client-count">
|
||||||
|
<sl-skeleton class="stat-val-skeleton" effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 text-xs dark:text-gray-400">
|
||||||
|
<sl-skeleton effect="pulse" class="stat-skeleton"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="text-lg font-semibold" id="stats-client-count">
|
||||||
|
<sl-skeleton class="stat-val-skeleton" effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="items-center 2xl: col-span-2 xl:col-span-2 justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Last Login Events</h3>
|
||||||
|
<AuditTable :auditFilter="loginAuditFilter" :paginate="false"></AuditTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="items-center 2xl:col-span-3 xl:col-span-3 justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Last Audit Events</h3>
|
||||||
|
<AuditTable :auditFilter="auditFilter" :paginate="false"></AuditTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import type { SystemStats } from '@/client'
|
||||||
|
import type { AuditFilter } from '@/api/types'
|
||||||
|
import { SshecretAdmin } from '@/client'
|
||||||
|
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||||
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
|
|
||||||
|
const loginAuditFilter = {
|
||||||
|
operation: 'login',
|
||||||
|
limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditFilter = { limit: 10 }
|
||||||
|
|
||||||
|
const stats = ref<SystemStats>({})
|
||||||
|
|
||||||
|
const alerts = useAlertsStore()
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
const response = await SshecretAdmin.getSystemStatsApiV1StatsGet()
|
||||||
|
if (response.data) {
|
||||||
|
stats.value = response.data
|
||||||
|
} else {
|
||||||
|
alerts.showAlert('Unable to fetch stats', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadStats)
|
||||||
|
</script>
|
||||||
@ -7,7 +7,12 @@ import ClientDetailView from '@/views/clients/ClientDetailView.vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useTreeState } from '@/store/useTreeState'
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
|
||||||
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
interface Props {
|
||||||
|
id: string
|
||||||
|
parentId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const clientId = toRef(() => props.id)
|
const clientId = toRef(() => props.id)
|
||||||
const parentId = toRef(() => props.parentId)
|
const parentId = toRef(() => props.parentId)
|
||||||
|
|||||||
@ -11,25 +11,30 @@
|
|||||||
|
|
||||||
<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 { 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 } 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'
|
||||||
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
|
|
||||||
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
interface Props {
|
||||||
|
id: string
|
||||||
|
parentId?: string
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
const clientId = toRef(() => props.id)
|
const clientId = toRef(() => props.id)
|
||||||
|
|
||||||
const client = ref<Client>()
|
const client = ref<Client>()
|
||||||
|
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'clientDeleted', data: string): void }>()
|
const emit = defineEmits<{ (e: 'clientDeleted', data: string): void }>()
|
||||||
|
const alerts = useAlertsStore()
|
||||||
|
|
||||||
|
const updateErrors = ref([])
|
||||||
|
|
||||||
async function loadClient() {
|
async function loadClient() {
|
||||||
console.log('loadClient called: ', props.id)
|
|
||||||
if (!props.id) return
|
if (!props.id) return
|
||||||
client.value = await treeState.getClient(props.id)
|
client.value = await treeState.getClient(props.id)
|
||||||
}
|
}
|
||||||
@ -46,12 +51,27 @@ async function deleteClient(deleteId: string) {
|
|||||||
emit('clientDeleted', deleteId)
|
emit('clientDeleted', deleteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearUpdateErrors() {
|
||||||
|
updateErrors.value = []
|
||||||
|
}
|
||||||
|
|
||||||
async function updateClient(updated: ClientCreate) {
|
async function updateClient(updated: ClientCreate) {
|
||||||
const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({
|
const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({
|
||||||
path: { id: idKey(localClient.value.id) },
|
path: { id: idKey(localClient.value.id) },
|
||||||
body: data,
|
body: data,
|
||||||
})
|
})
|
||||||
client.value = response.data
|
try {
|
||||||
|
const responseData = assertSdkResponseOk(response)
|
||||||
|
client.value = responseData
|
||||||
|
clearUpdateErrors()
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ValidationError) {
|
||||||
|
updateErrors.value = err.errors
|
||||||
|
} else {
|
||||||
|
const errorMessage = err.message ?? 'Unknown error'
|
||||||
|
alerts.showAlert(`Error from backend: ${errorMessage}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadClient)
|
onMounted(loadClient)
|
||||||
|
|||||||
@ -88,7 +88,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<sl-drawer label="Create Client" :open="createDrawerOpen" @sl-hide="createDrawerOpen = false">
|
<sl-drawer label="Create Client" :open="createDrawerOpen" @sl-hide="createDrawerOpen = false">
|
||||||
<ClientForm @submit="createClient" @cancel="createDrawerOpen = false" :key="createFormKey" />
|
<ClientForm
|
||||||
|
@submit="createClient"
|
||||||
|
@cancel="createDrawerOpen = false"
|
||||||
|
@clearErrors="clearCreateErrors"
|
||||||
|
:key="createFormKey"
|
||||||
|
:errors="createErrors"
|
||||||
|
/>
|
||||||
</sl-drawer>
|
</sl-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -101,6 +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 { 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'
|
||||||
@ -110,6 +118,7 @@ import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
|
|||||||
import ClientForm from '@/components/clients/ClientForm.vue'
|
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 { useDebounce } from '@/composables/useDebounce'
|
import { useDebounce } from '@/composables/useDebounce'
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
@ -121,6 +130,8 @@ 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 createFormKey = ref<number>(0)
|
const createFormKey = ref<number>(0)
|
||||||
const createDrawerOpen = ref<boolean>(false)
|
const createDrawerOpen = ref<boolean>(false)
|
||||||
|
|
||||||
@ -135,6 +146,7 @@ const router = useRouter()
|
|||||||
const clientQuery = toRef(() => props.loadClient)
|
const clientQuery = toRef(() => props.loadClient)
|
||||||
|
|
||||||
const debouncedQuery = useDebounce(clientQuery, 300)
|
const debouncedQuery = useDebounce(clientQuery, 300)
|
||||||
|
const alerts = useAlertsStore()
|
||||||
|
|
||||||
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
|
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
|
||||||
usePagination(totalClients, clientsPerPage)
|
usePagination(totalClients, clientsPerPage)
|
||||||
@ -173,12 +185,28 @@ function itemSelected(event: Event) {
|
|||||||
|
|
||||||
async function createClient(data: ClientCreate) {
|
async function createClient(data: ClientCreate) {
|
||||||
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
|
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
|
||||||
clients.value.unshift(response.data)
|
try {
|
||||||
totalClients.value += 1
|
const responseData = assertSdkResponseOk(response)
|
||||||
createDrawerOpen.value = false
|
clients.value.unshift(responseData)
|
||||||
createFormKey.value += 1
|
totalClients.value += 1
|
||||||
treeState.selectClient(response.data.id)
|
createDrawerOpen.value = false
|
||||||
router.push({ name: 'Client', params: { id: response.data.id } })
|
createFormKey.value += 1
|
||||||
|
treeState.selectClient(responseData.id)
|
||||||
|
router.push({ name: 'Client', params: { id: responseData.id } })
|
||||||
|
createErrors.value = []
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ValidationError) {
|
||||||
|
createErrors.value = err.errors
|
||||||
|
} else {
|
||||||
|
const errorMessage = err.message ?? 'Unknown error'
|
||||||
|
alerts.showAlert(`Error from backend: ${errorMessage}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCreateErrors() {
|
||||||
|
// Clear any errors from the create form.
|
||||||
|
createErrors.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clientDeleted(id: string) {
|
async function clientDeleted(id: string) {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<SecretSkeleton v-if="loading" />
|
||||||
|
<NotFound v-if="notfound" />
|
||||||
<SecretDetail
|
<SecretDetail
|
||||||
:secret="secret"
|
:secret="secret"
|
||||||
@update="updateSecretValue"
|
@update="updateSecretValue"
|
||||||
@ -8,26 +10,43 @@
|
|||||||
@removeClient="removeClientSecret"
|
@removeClient="removeClientSecret"
|
||||||
v-if="secret"
|
v-if="secret"
|
||||||
/>
|
/>
|
||||||
<SecretSkeleton v-else />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import SecretDetail from '@/components/secrets/SecretDetail.vue'
|
import SecretDetail from '@/components/secrets/SecretDetail.vue'
|
||||||
import SecretSkeleton from '@/components/secrets/SecretSkeleton.vue'
|
import SecretSkeleton from '@/components/secrets/SecretSkeleton.vue'
|
||||||
|
import NotFound from '@/components/common/NotFound.vue'
|
||||||
|
import { ApiError, NotFoundError, ValidationError } from '@/api/errors'
|
||||||
import { useTreeState } from '@/store/useTreeState'
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
|
|
||||||
import type { SecretView } from '@/client/types.gen.ts'
|
import type { SecretView } from '@/client/types.gen.ts'
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
|
|
||||||
|
const alerts = useAlertsStore()
|
||||||
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
||||||
const secret = ref<SecretView>()
|
const secret = ref<SecretView>()
|
||||||
|
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
|
|
||||||
|
const notfound = ref(false)
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
async function loadSecret() {
|
async function loadSecret() {
|
||||||
if (!props.id) return
|
if (!props.id) return
|
||||||
secret.value = await treeState.getSecret(props.id)
|
loading.value = true
|
||||||
|
try {
|
||||||
|
secret.value = await treeState.getSecret(props.id)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NotFoundError) {
|
||||||
|
notfound.value = true
|
||||||
|
} else {
|
||||||
|
alerts.showAlert(`Error from backend: ${err.message}`, 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSecretValue(value: string) {
|
async function updateSecretValue(value: string) {
|
||||||
@ -53,6 +72,8 @@ async function deleteSecret(clients: string[]) {
|
|||||||
for (const clientId in clients) {
|
for (const clientId in clients) {
|
||||||
await treeState.refreshClient(clientId)
|
await treeState.refreshClient(clientId)
|
||||||
}
|
}
|
||||||
|
await treeState.getSecretGroups()
|
||||||
|
treeState.bumpGroupRevision()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user