Continue frontend building
This commit is contained in:
File diff suppressed because one or more lines are too long
50
packages/sshecret-frontend/package-lock.json
generated
50
packages/sshecret-frontend/package-lock.json
generated
@ -11,7 +11,10 @@
|
||||
"@shoelace-style/shoelace": "^2.20.1",
|
||||
"@shoelace-style/vue-sl-model": "^1.0.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tiaohsun/vue-datatable-tailwind": "^2.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"datatables.net-dt": "^2.3.2",
|
||||
"datatables.net-vue3": "^3.0.4",
|
||||
"flowbite": "^3.1.2",
|
||||
"flowbite-vue": "^0.2.1",
|
||||
"is-cidr": "^5.1.1",
|
||||
@ -2280,6 +2283,16 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiaohsun/vue-datatable-tailwind": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiaohsun/vue-datatable-tailwind/-/vue-datatable-tailwind-2.2.0.tgz",
|
||||
"integrity": "sha512-9LZ1uUZ5goJCAj/8Ba2sbrkEdth/PWsWrYE5AWFD2uk8+Y+ljKTQPC8q3OKTWdJu+4pYBvtuCmqWv1Py1SlhqA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^4.1.0",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz",
|
||||
@ -3953,6 +3966,37 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/datatables.net": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.2.tgz",
|
||||
"integrity": "sha512-31TzwIQM0+pr2ZOEOEH6dsHd/WSAl5GDDGPezOHPI3mM2NK4lcDyOoG8xXeWmSbVfbi852LNK5C84fpp4Q+qxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jquery": ">=1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/datatables.net-dt": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.3.2.tgz",
|
||||
"integrity": "sha512-2heAMe0AVqJ8nt9wE35ArLtL1zP7N7qnhM3u52bLcJfi4NStxyBsHScVggTHVQPj7r9O+qirAxox1Xcr+Bm4Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"datatables.net": "2.3.2",
|
||||
"jquery": ">=1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/datatables.net-vue3": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/datatables.net-vue3/-/datatables.net-vue3-3.0.4.tgz",
|
||||
"integrity": "sha512-m7b7+zdH07d6J+Dz97KNs2PMezxZk+RgB4Or9i0J/SAXA/kaBRpQLTpr9Pyg8fh66/r4FQxnIkHhPTbW78Ep5A==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"datatables.net": "^2",
|
||||
"vue": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@ -5747,6 +5791,12 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
|
||||
|
||||
@ -18,7 +18,10 @@
|
||||
"@shoelace-style/shoelace": "^2.20.1",
|
||||
"@shoelace-style/vue-sl-model": "^1.0.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tiaohsun/vue-datatable-tailwind": "^2.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"datatables.net-dt": "^2.3.2",
|
||||
"datatables.net-vue3": "^3.0.4",
|
||||
"flowbite": "^3.1.2",
|
||||
"flowbite-vue": "^0.2.1",
|
||||
"is-cidr": "^5.1.1",
|
||||
|
||||
@ -1,8 +1,21 @@
|
||||
<template>
|
||||
<sl-alert
|
||||
v-for="alert in alerts.alerts"
|
||||
:key="alert.id"
|
||||
:variant="alert.type"
|
||||
open
|
||||
closable
|
||||
:duration="['warning', 'error'].includes(alert.type) ? Infinity : 3000"
|
||||
@sl-after-hide="alerts.removeAlert(alert.id)"
|
||||
>{{ alert.message }}</sl-alert
|
||||
>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthTokenStore } from '@/store/auth'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
</script>
|
||||
|
||||
16
packages/sshecret-frontend/src/api/password.ts
Normal file
16
packages/sshecret-frontend/src/api/password.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export function generateRandomPassword(length: number = 16): string {
|
||||
const lower = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const numbers = '0123456789'
|
||||
const special = '!@#$%^&*()-_=+[]{};:,.<>?'
|
||||
|
||||
const allChars = lower + upper + numbers + special
|
||||
|
||||
let password = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
const index = Math.floor(Math.random() * allChars.length)
|
||||
password += allChars[index]
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
13
packages/sshecret-frontend/src/api/typeguards.ts
Normal file
13
packages/sshecret-frontend/src/api/typeguards.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// Typeguards
|
||||
|
||||
import { OPERATIONS } from '@/api/types'
|
||||
import { SUBSYSTEM } from '@/api/types'
|
||||
import type { Operation, SubSystem } from '@/client'
|
||||
|
||||
export function isOperation(value: string): value is Operation {
|
||||
return (OPERATIONS as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
export function isSubSystem(value: string): value is SubSystem {
|
||||
return (SUBSYSTEM as readonly string[]).includes(value)
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import type { SubSystem, Operation } from "@/client";
|
||||
export type PageRequest = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
@ -6,9 +7,67 @@ export type PageRequest = {
|
||||
export enum SshecretObjectType {
|
||||
Client = "Client",
|
||||
ClientSecret = "ClientSecret",
|
||||
SecretGroup = "SecretGroup",
|
||||
}
|
||||
|
||||
export type SshecretObject = {
|
||||
objectType: SshecretObjectType,
|
||||
id: string,
|
||||
param?: string,
|
||||
}
|
||||
|
||||
export type AuditFilter = {
|
||||
/**
|
||||
* Subsystem
|
||||
*/
|
||||
subsystem?: SubSystem | null;
|
||||
/**
|
||||
* Operation
|
||||
*/
|
||||
operation?: Operation | null;
|
||||
/**
|
||||
* Client Id
|
||||
*/
|
||||
client_id?: string | null;
|
||||
/**
|
||||
* Client Name
|
||||
*/
|
||||
client_name?: string | null;
|
||||
/**
|
||||
* Secret Id
|
||||
*/
|
||||
secret_id?: string | null;
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name?: string | null;
|
||||
/**
|
||||
* Origin
|
||||
*/
|
||||
origin?: string | null;
|
||||
/**
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* Limit
|
||||
*/
|
||||
limit?: number;
|
||||
|
||||
}
|
||||
|
||||
export const OPERATIONS = [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
'deny',
|
||||
'permit',
|
||||
'login',
|
||||
'none',
|
||||
] as const
|
||||
|
||||
export const SUBSYSTEM = [
|
||||
'admin',
|
||||
'sshd',
|
||||
'backend',
|
||||
] as const
|
||||
|
||||
55
packages/sshecret-frontend/src/assets/custom.css
Normal file
55
packages/sshecret-frontend/src/assets/custom.css
Normal file
@ -0,0 +1,55 @@
|
||||
sl-input,
|
||||
sl-select,
|
||||
sl-checkbox {
|
||||
display: block;
|
||||
margin-bottom: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
sl-input[data-user-invalid]::part(base),
|
||||
sl-select[data-user-invalid]::part(combobox),
|
||||
sl-checkbox[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
[data-user-invalid]::part(form-control-label),
|
||||
[data-user-invalid]::part(form-control-help-text),
|
||||
sl-checkbox[data-user-invalid]::part(label) {
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
sl-checkbox[data-user-invalid]::part(control) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
sl-input:focus-within[data-user-invalid]::part(base),
|
||||
sl-select:focus-within[data-user-invalid]::part(combobox),
|
||||
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 */
|
||||
sl-input[data-user-valid]::part(base),
|
||||
sl-select[data-user-valid]::part(combobox),
|
||||
sl-checkbox[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
[data-user-valid]::part(form-control-label),
|
||||
[data-user-valid]::part(form-control-help-text),
|
||||
sl-checkbox[data-user-valid]::part(label) {
|
||||
color: var(--sl-color-success-700);
|
||||
}
|
||||
|
||||
sl-checkbox[data-user-valid]::part(control) {
|
||||
background-color: var(--sl-color-success-600);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
sl-input:focus-within[data-user-valid]::part(base),
|
||||
sl-select:focus-within[data-user-valid]::part(combobox),
|
||||
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);
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
@import './base.css';
|
||||
@import './custom.css'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { type Options as ClientOptions, type TDataShape, type Client, urlSearchParamsBodySerializer } from './client';
|
||||
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, 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, DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupNameGetData, GetSecretGroupApiV1SecretsGroupsGroupNameGetResponses, GetSecretGroupApiV1SecretsGroupsGroupNameGetErrors, RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteData, RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteResponses, RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteErrors, MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostData, MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostResponses, MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostErrors, MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostData, MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostResponses, MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostErrors, MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteData, MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteResponses, MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteErrors } 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, 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';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@ -31,6 +31,24 @@ export class SshecretAdmin {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Audit Log
|
||||
* Query audit log.
|
||||
*/
|
||||
public static getAuditLogApiV1AuditGet<ThrowOnError extends boolean = false>(options?: Options<GetAuditLogApiV1AuditGetData, ThrowOnError>) {
|
||||
return (options?.client ?? _heyApiClient).get<GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
scheme: 'bearer',
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/audit/',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Login For Access Token
|
||||
* Login user and generate token.
|
||||
@ -426,8 +444,8 @@ export class SshecretAdmin {
|
||||
* Entries within the group will be moved to the root.
|
||||
* This also includes nested entries further down from the group.
|
||||
*/
|
||||
public static deleteSecretGroupApiV1SecretsGroupsGroupNameDelete<ThrowOnError extends boolean = false>(options: Options<DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).delete<DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteErrors, ThrowOnError>({
|
||||
public static deleteSecretGroupApiV1SecretsGroupsGroupPathDelete<ThrowOnError extends boolean = false>(options: Options<DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).delete<DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
@ -435,7 +453,7 @@ export class SshecretAdmin {
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/groups/{group_name}/',
|
||||
url: '/api/v1/secrets/groups/{group_path}/',
|
||||
...options
|
||||
});
|
||||
}
|
||||
@ -444,8 +462,8 @@ export class SshecretAdmin {
|
||||
* Get Secret Group
|
||||
* Get a specific secret group.
|
||||
*/
|
||||
public static getSecretGroupApiV1SecretsGroupsGroupNameGet<ThrowOnError extends boolean = false>(options: Options<GetSecretGroupApiV1SecretsGroupsGroupNameGetData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).get<GetSecretGroupApiV1SecretsGroupsGroupNameGetResponses, GetSecretGroupApiV1SecretsGroupsGroupNameGetErrors, ThrowOnError>({
|
||||
public static getSecretGroupApiV1SecretsGroupsGroupPathGet<ThrowOnError extends boolean = false>(options: Options<GetSecretGroupApiV1SecretsGroupsGroupPathGetData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).get<GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
@ -453,19 +471,17 @@ export class SshecretAdmin {
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/groups/{group_name}/',
|
||||
url: '/api/v1/secrets/groups/{group_path}/',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Secret From Group
|
||||
* Remove a secret from a group.
|
||||
*
|
||||
* Secret will be moved to the root group.
|
||||
* Update Secret Group
|
||||
* Update a secret group.
|
||||
*/
|
||||
public static removeSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDelete<ThrowOnError extends boolean = false>(options: Options<RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).delete<RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteResponses, RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteErrors, ThrowOnError>({
|
||||
public static updateSecretGroupApiV1SecretsGroupsGroupPathPut<ThrowOnError extends boolean = false>(options: Options<UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).put<UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
@ -473,17 +489,39 @@ export class SshecretAdmin {
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/groups/{group_name}/{secret_name}',
|
||||
url: '/api/v1/secrets/groups/{group_path}/',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Group Id
|
||||
* Remove a group by ID.
|
||||
*/
|
||||
public static deleteGroupIdApiV1SecretsGroupIdDelete<ThrowOnError extends boolean = false>(options: Options<DeleteGroupIdApiV1SecretsGroupIdDeleteData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).delete<DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
scheme: 'bearer',
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/group/{id}',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Secret To Group
|
||||
* Move a secret to a group.
|
||||
* Assign Secret Group
|
||||
* Assign a secret to a group or root.
|
||||
*/
|
||||
public static moveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePost<ThrowOnError extends boolean = false>(options: Options<MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).post<MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostResponses, MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostErrors, ThrowOnError>({
|
||||
public static assignSecretGroupApiV1SecretsSetGroupPost<ThrowOnError extends boolean = false>(options: Options<AssignSecretGroupApiV1SecretsSetGroupPostData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).post<AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
@ -491,8 +529,12 @@ export class SshecretAdmin {
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/groups/{group_name}/{secret_name}',
|
||||
...options
|
||||
url: '/api/v1/secrets/set-group',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -500,8 +542,8 @@ export class SshecretAdmin {
|
||||
* Move Group
|
||||
* Move a group.
|
||||
*/
|
||||
public static moveGroupApiV1SecretsGroupGroupNameParentParentNamePost<ThrowOnError extends boolean = false>(options: Options<MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).post<MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostResponses, MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostErrors, ThrowOnError>({
|
||||
public static moveGroupApiV1SecretsMoveGroupGroupNamePost<ThrowOnError extends boolean = false>(options: Options<MoveGroupApiV1SecretsMoveGroupGroupNamePostData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).post<MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
@ -509,26 +551,12 @@ export class SshecretAdmin {
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/group/{group_name}/parent/{parent_name}',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Group To Root
|
||||
* Move a group to the root.
|
||||
*/
|
||||
public static moveGroupToRootApiV1SecretsGroupGroupNameParentDelete<ThrowOnError extends boolean = false>(options: Options<MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteData, ThrowOnError>) {
|
||||
return (options.client ?? _heyApiClient).delete<MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteResponses, MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteErrors, ThrowOnError>({
|
||||
responseType: 'json',
|
||||
security: [
|
||||
{
|
||||
scheme: 'bearer',
|
||||
type: 'http'
|
||||
}
|
||||
],
|
||||
url: '/api/v1/secrets/group/{group_name}/parent/',
|
||||
...options
|
||||
url: '/api/v1/secrets/move-group/{group_name}',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,71 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
/**
|
||||
* AuditListResult
|
||||
* Class to return when listing audit entries.
|
||||
*/
|
||||
export type AuditListResult = {
|
||||
/**
|
||||
* Results
|
||||
*/
|
||||
results: Array<AuditLog>;
|
||||
/**
|
||||
* Total
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* Remaining
|
||||
*/
|
||||
remaining: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* AuditLog
|
||||
* Implementation of the backend class AuditView.
|
||||
*/
|
||||
export type AuditLog = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id?: string | null;
|
||||
subsystem: SubSystem;
|
||||
operation: Operation;
|
||||
/**
|
||||
* Client Id
|
||||
*/
|
||||
client_id?: string | null;
|
||||
/**
|
||||
* Client Name
|
||||
*/
|
||||
client_name?: string | null;
|
||||
/**
|
||||
* Secret Id
|
||||
*/
|
||||
secret_id?: string | null;
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name?: string | null;
|
||||
/**
|
||||
* Data
|
||||
*/
|
||||
data?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
/**
|
||||
* Message
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Origin
|
||||
*/
|
||||
origin?: string | null;
|
||||
/**
|
||||
* Timestamp
|
||||
*/
|
||||
timestamp?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* AutoGenerateOpts
|
||||
* Option to auto-generate a password.
|
||||
@ -162,6 +228,10 @@ export type ClientReference = {
|
||||
* Client secrets grouped.
|
||||
*/
|
||||
export type ClientSecretGroup = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Group Name
|
||||
*/
|
||||
@ -174,7 +244,7 @@ export type ClientSecretGroup = {
|
||||
* Description
|
||||
*/
|
||||
description?: string | null;
|
||||
parent_group?: ClientSecretGroup | null;
|
||||
parent_group?: GroupReference | null;
|
||||
/**
|
||||
* Children
|
||||
*/
|
||||
@ -200,6 +270,35 @@ export type ClientSecretGroupList = {
|
||||
groups?: Array<ClientSecretGroup>;
|
||||
};
|
||||
|
||||
/**
|
||||
* GroupPath
|
||||
* Path to a group.
|
||||
*/
|
||||
export type GroupPath = {
|
||||
/**
|
||||
* Path
|
||||
*/
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* GroupReference
|
||||
* Reference to a group.
|
||||
*
|
||||
* This will be used for references to parent groups to avoid circular
|
||||
* references.
|
||||
*/
|
||||
export type GroupReference = {
|
||||
/**
|
||||
* Group Name
|
||||
*/
|
||||
group_name: string;
|
||||
/**
|
||||
* Path
|
||||
*/
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* HTTPValidationError
|
||||
*/
|
||||
@ -210,6 +309,12 @@ export type HttpValidationError = {
|
||||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Operation
|
||||
* Various operations for the audit logging module.
|
||||
*/
|
||||
export type Operation = 'create' | 'read' | 'update' | 'delete' | 'deny' | 'permit' | 'login' | 'none';
|
||||
|
||||
/**
|
||||
* RefreshTokenForm
|
||||
* The refresh token form data.
|
||||
@ -263,12 +368,33 @@ export type SecretCreate = {
|
||||
* Assign the secret to a list of clients.
|
||||
*/
|
||||
clients?: Array<string> | null;
|
||||
/**
|
||||
* Client Distinguisher
|
||||
*/
|
||||
client_distinguisher?: 'id' | 'name';
|
||||
/**
|
||||
* Group
|
||||
*/
|
||||
group?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* SecretGroupAssign
|
||||
* Model for assigning secrets to a group.
|
||||
*
|
||||
* If group is None, then it will be placed in the root.
|
||||
*/
|
||||
export type SecretGroupAssign = {
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name: string;
|
||||
/**
|
||||
* Group Path
|
||||
*/
|
||||
group_path: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* SecretGroupCreate
|
||||
* Create model for creating secret groups.
|
||||
@ -288,6 +414,25 @@ export type SecretGroupCreate = {
|
||||
parent_group?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* SecretGroupUdate
|
||||
* Update model for updating secret groups.
|
||||
*/
|
||||
export type SecretGroupUdate = {
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Parent Group
|
||||
*/
|
||||
parent_group?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* SecretListView
|
||||
* Model containing a list of all available secrets.
|
||||
@ -342,6 +487,12 @@ export type SecretView = {
|
||||
clients?: Array<ClientReference>;
|
||||
};
|
||||
|
||||
/**
|
||||
* SubSystem
|
||||
* Available subsystems.
|
||||
*/
|
||||
export type SubSystem = 'admin' | 'sshd' | 'backend';
|
||||
|
||||
/**
|
||||
* Token
|
||||
*/
|
||||
@ -433,6 +584,68 @@ export type GetHealthHealthGetResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetAuditLogApiV1AuditGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Subsystem
|
||||
*/
|
||||
subsystem?: SubSystem | null;
|
||||
/**
|
||||
* Operation
|
||||
*/
|
||||
operation?: Operation | null;
|
||||
/**
|
||||
* Client Id
|
||||
*/
|
||||
client_id?: string | null;
|
||||
/**
|
||||
* Client Name
|
||||
*/
|
||||
client_name?: string | null;
|
||||
/**
|
||||
* Secret Id
|
||||
*/
|
||||
secret_id?: string | null;
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name?: string | null;
|
||||
/**
|
||||
* Origin
|
||||
*/
|
||||
origin?: string | null;
|
||||
/**
|
||||
* Offset
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* Limit
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
url: '/api/v1/audit/';
|
||||
};
|
||||
|
||||
export type GetAuditLogApiV1AuditGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetAuditLogApiV1AuditGetError = GetAuditLogApiV1AuditGetErrors[keyof GetAuditLogApiV1AuditGetErrors];
|
||||
|
||||
export type GetAuditLogApiV1AuditGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: AuditListResult;
|
||||
};
|
||||
|
||||
export type GetAuditLogApiV1AuditGetResponse = GetAuditLogApiV1AuditGetResponses[keyof GetAuditLogApiV1AuditGetResponses];
|
||||
|
||||
export type LoginForAccessTokenApiV1TokenPostData = {
|
||||
body: BodyLoginForAccessTokenApiV1TokenPost;
|
||||
path?: never;
|
||||
@ -604,7 +817,8 @@ export type DeleteClientApiV1ClientsIdDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
@ -632,7 +846,8 @@ export type GetClientApiV1ClientsIdGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
@ -662,7 +877,8 @@ export type UpdateClientApiV1ClientsIdPutData = {
|
||||
body: ClientCreate;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
@ -692,7 +908,8 @@ export type DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
@ -724,7 +941,8 @@ export type AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
@ -786,7 +1004,8 @@ export type UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData = {
|
||||
body: UpdateKeyModel;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
* Client identifier
|
||||
* Identifier of path, may include the prefix 'id:' or 'name:'
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
@ -946,6 +1165,10 @@ export type GetSecretGroupsApiV1SecretsGroupsGetData = {
|
||||
* Filter Regex
|
||||
*/
|
||||
filter_regex?: string | null;
|
||||
/**
|
||||
* Flat
|
||||
*/
|
||||
flat?: boolean;
|
||||
};
|
||||
url: '/api/v1/secrets/groups/';
|
||||
};
|
||||
@ -993,162 +1216,147 @@ export type AddSecretGroupApiV1SecretsGroupsPostResponses = {
|
||||
|
||||
export type AddSecretGroupApiV1SecretsGroupsPostResponse = AddSecretGroupApiV1SecretsGroupsPostResponses[keyof AddSecretGroupApiV1SecretsGroupsPostResponses];
|
||||
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteData = {
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
* Group Path
|
||||
*/
|
||||
group_name: string;
|
||||
group_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/groups/{group_name}/';
|
||||
url: '/api/v1/secrets/groups/{group_path}/';
|
||||
};
|
||||
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteErrors = {
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteError = DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteErrors[keyof DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteErrors];
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteError = DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors[keyof DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors];
|
||||
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupNameDeleteResponses = {
|
||||
export type DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupNameGetData = {
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupPathGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
* Group Path
|
||||
*/
|
||||
group_name: string;
|
||||
group_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/groups/{group_name}/';
|
||||
url: '/api/v1/secrets/groups/{group_path}/';
|
||||
};
|
||||
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupNameGetErrors = {
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupNameGetError = GetSecretGroupApiV1SecretsGroupsGroupNameGetErrors[keyof GetSecretGroupApiV1SecretsGroupsGroupNameGetErrors];
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupPathGetError = GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors[keyof GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors];
|
||||
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupNameGetResponses = {
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: ClientSecretGroup;
|
||||
};
|
||||
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupNameGetResponse = GetSecretGroupApiV1SecretsGroupsGroupNameGetResponses[keyof GetSecretGroupApiV1SecretsGroupsGroupNameGetResponses];
|
||||
export type GetSecretGroupApiV1SecretsGroupsGroupPathGetResponse = GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses[keyof GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses];
|
||||
|
||||
export type RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteData = {
|
||||
body?: never;
|
||||
export type UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData = {
|
||||
body: SecretGroupUdate;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
* Group Path
|
||||
*/
|
||||
group_name: string;
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name: string;
|
||||
group_path: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/groups/{group_name}/{secret_name}';
|
||||
url: '/api/v1/secrets/groups/{group_path}/';
|
||||
};
|
||||
|
||||
export type RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteErrors = {
|
||||
export type UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteError = RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteErrors[keyof RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteErrors];
|
||||
export type UpdateSecretGroupApiV1SecretsGroupsGroupPathPutError = UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors[keyof UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors];
|
||||
|
||||
export type RemoveSecretFromGroupApiV1SecretsGroupsGroupNameSecretNameDeleteResponses = {
|
||||
export type UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: ClientSecretGroup;
|
||||
};
|
||||
|
||||
export type UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponse = UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses[keyof UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses];
|
||||
|
||||
export type DeleteGroupIdApiV1SecretsGroupIdDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/group/{id}';
|
||||
};
|
||||
|
||||
export type DeleteGroupIdApiV1SecretsGroupIdDeleteErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteGroupIdApiV1SecretsGroupIdDeleteError = DeleteGroupIdApiV1SecretsGroupIdDeleteErrors[keyof DeleteGroupIdApiV1SecretsGroupIdDeleteErrors];
|
||||
|
||||
export type DeleteGroupIdApiV1SecretsGroupIdDeleteResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
*/
|
||||
group_name: string;
|
||||
/**
|
||||
* Secret Name
|
||||
*/
|
||||
secret_name: string;
|
||||
};
|
||||
export type AssignSecretGroupApiV1SecretsSetGroupPostData = {
|
||||
body: SecretGroupAssign;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/groups/{group_name}/{secret_name}';
|
||||
url: '/api/v1/secrets/set-group';
|
||||
};
|
||||
|
||||
export type MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostErrors = {
|
||||
export type AssignSecretGroupApiV1SecretsSetGroupPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostError = MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostErrors[keyof MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostErrors];
|
||||
export type AssignSecretGroupApiV1SecretsSetGroupPostError = AssignSecretGroupApiV1SecretsSetGroupPostErrors[keyof AssignSecretGroupApiV1SecretsSetGroupPostErrors];
|
||||
|
||||
export type MoveSecretToGroupApiV1SecretsGroupsGroupNameSecretNamePostResponses = {
|
||||
export type AssignSecretGroupApiV1SecretsSetGroupPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
*/
|
||||
group_name: string;
|
||||
/**
|
||||
* Parent Name
|
||||
*/
|
||||
parent_name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/group/{group_name}/parent/{parent_name}';
|
||||
};
|
||||
|
||||
export type MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostError = MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostErrors[keyof MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostErrors];
|
||||
|
||||
export type MoveGroupApiV1SecretsGroupGroupNameParentParentNamePostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteData = {
|
||||
body?: never;
|
||||
export type MoveGroupApiV1SecretsMoveGroupGroupNamePostData = {
|
||||
body: GroupPath;
|
||||
path: {
|
||||
/**
|
||||
* Group Name
|
||||
@ -1156,19 +1364,19 @@ export type MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteData = {
|
||||
group_name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/secrets/group/{group_name}/parent/';
|
||||
url: '/api/v1/secrets/move-group/{group_name}';
|
||||
};
|
||||
|
||||
export type MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteErrors = {
|
||||
export type MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteError = MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteErrors[keyof MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteErrors];
|
||||
export type MoveGroupApiV1SecretsMoveGroupGroupNamePostError = MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors[keyof MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors];
|
||||
|
||||
export type MoveGroupToRootApiV1SecretsGroupGroupNameParentDeleteResponses = {
|
||||
export type MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
|
||||
120
packages/sshecret-frontend/src/components/audit/AuditFilters.vue
Normal file
120
packages/sshecret-frontend/src/components/audit/AuditFilters.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Audit log</h1>
|
||||
<div class="mt-4">
|
||||
<form @submit.prevent="applyFilter">
|
||||
<div class="py-2 border-b border-gray-100">
|
||||
<sl-select
|
||||
size="small"
|
||||
label="Filter client name"
|
||||
:value="filterForm.clientName"
|
||||
clearable
|
||||
@sl-change="filterForm.clientName = $event.target.value"
|
||||
>
|
||||
<sl-option>Select client...</sl-option>
|
||||
<template v-if="clientNames">
|
||||
<sl-option :value="name" v-for="name in clientNames">{{ name }}</sl-option>
|
||||
</template>
|
||||
</sl-select>
|
||||
</div>
|
||||
|
||||
<div class="py-2 border-b border-gray-100">
|
||||
<sl-select
|
||||
size="small"
|
||||
label="Filter secret name"
|
||||
:value="filterForm.secretName"
|
||||
clearable
|
||||
@sl-change="filterForm.secretName = $event.target.value"
|
||||
>
|
||||
<sl-option>Select secret...</sl-option>
|
||||
<template v-if="secretNames">
|
||||
<sl-option :value="name" v-for="name in secretNames">{{ name }}</sl-option>
|
||||
</template>
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="py-2 border-b border-gray-100">
|
||||
<sl-select
|
||||
size="small"
|
||||
label="Filter operation"
|
||||
:value="filterForm.operation"
|
||||
clearable
|
||||
@sl-change="filterForm.operation = $event.target.value"
|
||||
>
|
||||
<sl-option>Select operation type...</sl-option>
|
||||
<sl-option :value="name" v-for="name in OPERATIONS">{{ name }}</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="py-2 border-b border-gray-100">
|
||||
<sl-select
|
||||
size="small"
|
||||
label="Filter subsystem"
|
||||
:value="filterForm.operation"
|
||||
clearable
|
||||
@sl-change="filterForm.subSystem = $event.target.value"
|
||||
>
|
||||
<sl-option>Select subsystem</sl-option>
|
||||
<sl-option :value="name" v-for="name in SUBSYSTEM">{{ name }}</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="py-2 border-b border-gray-100">
|
||||
<sl-input
|
||||
size="small"
|
||||
label="Filter origin"
|
||||
:value="filterForm.origin"
|
||||
clearable
|
||||
@input="filterForm.origin = $event.target.value"
|
||||
@sl-clear="filterForm.origin = null"
|
||||
></sl-input>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<sl-button size="small" type="reset" @click="auditFilterState.reset">
|
||||
<sl-icon slot="prefix" name="x-circle"></sl-icon>
|
||||
Reset
|
||||
</sl-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import { SUBSYSTEM, OPERATIONS } from '@/api/types'
|
||||
const auditFilterState = useAuditFilterState()
|
||||
|
||||
const clientNames = ref<string[]>()
|
||||
const secretNames = ref<string[]>()
|
||||
|
||||
const filterForm = reactive({
|
||||
subSystem: null,
|
||||
operation: null,
|
||||
clientName: null,
|
||||
secretName: null,
|
||||
origin: null,
|
||||
})
|
||||
|
||||
async function fetchClientNames() {
|
||||
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
||||
if (response.data) {
|
||||
clientNames.value = response.data.map((x) => x.name)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSecretNames() {
|
||||
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
||||
if (response.data) {
|
||||
secretNames.value = response.data.map((x) => x.name)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNames() {
|
||||
await fetchClientNames()
|
||||
await fetchSecretNames()
|
||||
}
|
||||
|
||||
watch(filterForm, (newValue, oldValue) => {
|
||||
console.log('New value', newValue, 'oldValue', oldValue)
|
||||
auditFilterState.setValues(newValue)
|
||||
})
|
||||
|
||||
onMounted(fetchNames)
|
||||
</script>
|
||||
241
packages/sshecret-frontend/src/components/audit/AuditTable.vue
Normal file
241
packages/sshecret-frontend/src/components/audit/AuditTable.vue
Normal file
@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<table v-if="auditEntries" class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Timestamp
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Subsystem
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Operation
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Client
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Secret
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Message
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||
>
|
||||
Origin
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
<template v-for="entry in auditEntries" :key="entry.id">
|
||||
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<td>
|
||||
<sl-icon-button
|
||||
name="chevron-right"
|
||||
@click="toggle(entry.id)"
|
||||
:class="{ 'rotate-90': isExpanded(entry.id) }"
|
||||
></sl-icon-button>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.timestamp }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.subsystem }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.operation }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<abbr :title="entry.client_id" v-if="entry.client_name">{{ entry.client_name }}</abbr>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<abbr :title="entry.secret_id" v-if="entry.secret_name">{{ entry.secret_name }}</abbr>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
{{ entry.message }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
{{ entry.origin }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(entry.id)" class="auditRow">
|
||||
<td></td>
|
||||
<td colspan="8">
|
||||
<dl
|
||||
class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 px-2 py-2"
|
||||
>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">ID</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.id }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Subsystem</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.subsystem }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Timestamp</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.timestamp }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Operation</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.operation }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client ID</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.client_id }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client Name</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.client_name }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret ID</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.secret_id }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret Name</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.secret_name }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Message</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.message }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Origin</dt>
|
||||
<dd class="text-xs font-semibold">{{ entry.origin }}</dd>
|
||||
</div>
|
||||
<template v-if="entry.data">
|
||||
<template v-for="(value, key) in entry.data">
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">{{ key }}</dt>
|
||||
<dd class="text-xs font-semibold">{{ value }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
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">
|
||||
Showing
|
||||
<span class="font-semibold text-gray-900 dark:text-white" v-if="totalEntries < lastResult">
|
||||
{{ 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>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex space-x-1">
|
||||
<PageNumbers
|
||||
@next="nextPage"
|
||||
@previous="prevPage"
|
||||
@goto="goToPage"
|
||||
:pageNum="pageNum"
|
||||
:totalPages="totalPages"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive, onMounted, watch, toRef } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { usePagination } from '@/composables/usePagination'
|
||||
import { SshecretAdmin, GetAuditLogApiV1AuditGetData } from '@/client'
|
||||
import type { AuditListResult } from '@/client'
|
||||
import type { AuditFilter } from '@/api/types'
|
||||
import PageNumbers from '@/components/common/PageNumbers.vue'
|
||||
|
||||
const props = defineProps<{ auditFilter: AuditFilter }>()
|
||||
|
||||
const auditFilter = toRef(() => props.auditFilter)
|
||||
console.log(auditFilter.value)
|
||||
const auditList = ref<AuditListResult>([])
|
||||
const auditEntries = computed(() => auditList.value?.results)
|
||||
const totalEntries = computed(() => auditList.value?.total)
|
||||
|
||||
const perPage = props.auditFilter.limit ?? 25
|
||||
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
|
||||
usePagination(totalEntries, perPage)
|
||||
|
||||
const queryInput = computed<GetAuditLogApiV1AuditGetData['query']>(() => {
|
||||
return {
|
||||
...props.auditFilter,
|
||||
offset: offset.value,
|
||||
limit: perPage,
|
||||
}
|
||||
})
|
||||
|
||||
async function loadLogs() {
|
||||
const response = await SshecretAdmin.getAuditLogApiV1AuditGet({
|
||||
query: queryInput.value,
|
||||
})
|
||||
auditList.value = response.data
|
||||
}
|
||||
|
||||
const expanded = ref(new Set<string>())
|
||||
|
||||
function toggle(id: string) {
|
||||
if (expanded.value.has(id)) {
|
||||
expanded.value.delete(id)
|
||||
} else {
|
||||
expanded.value.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
function isExpanded(id: string) {
|
||||
return expanded.value.has(id)
|
||||
}
|
||||
|
||||
watch([offset, pageNum, auditFilter], loadLogs)
|
||||
onMounted(loadLogs)
|
||||
</script>
|
||||
<style>
|
||||
tr.auditRow {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
tr.auditRow:nth-child(even) {
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
</style>
|
||||
@ -83,7 +83,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="events"> </sl-tab-panel>
|
||||
<sl-tab-panel name="events">
|
||||
<AuditTable :auditFilter="auditFilter" />
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
|
||||
<sl-drawer label="Edit Client" :open="updateDrawerOpen" @sl-hide="updateDrawerOpen = false">
|
||||
@ -102,18 +104,8 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { Client, ClientCreate } from '@/client/types.gen'
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
|
||||
import '@shoelace-style/shoelace/dist/components/divider/divider.js'
|
||||
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
|
||||
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu/menu.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu-label/menu-label.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js'
|
||||
|
||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||
import ClientForm from '@/components/clients/ClientForm.vue'
|
||||
const props = defineProps<{ client: Client }>()
|
||||
const emit = defineEmits<{
|
||||
@ -132,6 +124,11 @@ async function updateClient(data: ClientCreate) {
|
||||
updateDrawerOpen.value = false
|
||||
}
|
||||
|
||||
const auditFilter = {
|
||||
client_name: props.client.name,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
async function deleteClient() {
|
||||
showConfirm.value = false
|
||||
emit('deleted', localClient.value.id)
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
const props = defineProps<{
|
||||
parentId: string
|
||||
name: string
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import type { ClientReference } from '@/client'
|
||||
|
||||
const clients = ref<ClientReference[]>([])
|
||||
const props = defineProps<{ modelValue: string[] }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', data: string[]): void }>()
|
||||
|
||||
async function getClients() {
|
||||
// Get just names and IDs of the clients
|
||||
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
||||
if (response.data) {
|
||||
clients.value = response.data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(getClients)
|
||||
</script>
|
||||
<template>
|
||||
<sl-select
|
||||
label="Select clients"
|
||||
:value="props.modelValue"
|
||||
multiple
|
||||
clearable
|
||||
@sl-change="emit('update:modelValue', $event.target.value)"
|
||||
id="selectClientDialog"
|
||||
>
|
||||
<template v-for="client in clients">
|
||||
<sl-option :value="client.id">{{ client.name }}</sl-option>
|
||||
</template>
|
||||
</sl-select>
|
||||
</template>
|
||||
@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<div id="client-tree-items" class="flowbite-init-target flex flex-col h-full min-h-0">
|
||||
<sl-tree-item v-bind:id="itemId" data-type="client" :data-client-id="id">
|
||||
<sl-icon name="person-fill-lock"> </sl-icon>
|
||||
<span class="px-2">{{ name }}</span>
|
||||
<slot />
|
||||
</sl-tree-item>
|
||||
</div>
|
||||
<sl-tree-item v-bind:id="itemId" data-type="client" :data-client-id="id">
|
||||
<sl-icon name="person-fill-lock"> </sl-icon>
|
||||
<span class="px-2">{{ name }}</span>
|
||||
<slot />
|
||||
</sl-tree-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
|
||||
19
packages/sshecret-frontend/src/components/common/Drawer.vue
Normal file
19
packages/sshecret-frontend/src/components/common/Drawer.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<sl-drawer ref="drawerRef" :open="open" @sl-hide="handleHide" :label="label">
|
||||
<slot />
|
||||
</sl-drawer>
|
||||
</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 drawerRef = ref<HTMLSlDrawerElement>()
|
||||
|
||||
const handleHide = useBubbleSafeHandler(drawerRef, () => {
|
||||
emit('hide')
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<sl-button-group label="pagination">
|
||||
<sl-button :size="size" pill :disabled="pageNum <= 1" @click="emit('previous')">
|
||||
<slot name="previous">
|
||||
<sl-icon name="chevron-compact-left" slot="prefix"></sl-icon>
|
||||
Prev
|
||||
</slot>
|
||||
</sl-button>
|
||||
<template v-if="totalPages > 10">
|
||||
<template v-for="n in 5">
|
||||
<sl-button v-if="n === pageNum" :size="size" disabled pill variant="primary">
|
||||
{{ n }}
|
||||
</sl-button>
|
||||
<sl-button v-else @click="emit('goto', n)" pill :size="size">{{ n }}</sl-button>
|
||||
</template>
|
||||
<sl-button :size="size" disabled pill v-if="pageNum > 6 && pageNum < totalPages">
|
||||
<sl-icon name="three-dots"></sl-icon>
|
||||
</sl-button>
|
||||
<sl-button
|
||||
v-if="pageNum > 5 && pageNum < totalPages"
|
||||
:size="size"
|
||||
disabled
|
||||
pill
|
||||
variant="primary"
|
||||
>
|
||||
{{ pageNum }}
|
||||
</sl-button>
|
||||
|
||||
<sl-button :size="size" disabled pill>
|
||||
<sl-icon name="three-dots"></sl-icon>
|
||||
</sl-button>
|
||||
|
||||
<sl-button v-if="pageNum < totalPages" @click="emit('goto', totalPages)" pill :size="size">{{
|
||||
totalPages
|
||||
}}</sl-button>
|
||||
<sl-button v-if="pageNum === totalPages" :size="size" disabled pill variant="primary">
|
||||
{{ totalPages }}
|
||||
</sl-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="n in totalPages">
|
||||
<sl-button v-if="n === pageNum" :size="size" disabled pill variant="primary">
|
||||
{{ n }}
|
||||
</sl-button>
|
||||
<sl-button v-else @click="emit('goto', n)" pill :size="size">{{ n }}</sl-button>
|
||||
</template>
|
||||
</template>
|
||||
<sl-button :disabled="pageNum >= totalPages" @click="emit('next')" :size="size" pill>
|
||||
<slot name="next">
|
||||
<sl-icon name="chevron-compact-right" slot="suffix"></sl-icon>
|
||||
Next
|
||||
</slot>
|
||||
</sl-button>
|
||||
</sl-button-group>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type PageFun = (...args: never[]) => void
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
pageNum: number
|
||||
totalPages: number
|
||||
size?: string
|
||||
}>(),
|
||||
{
|
||||
size: 'small',
|
||||
},
|
||||
)
|
||||
const emit = defineEmits<{ (e: 'next'): void; (e: 'previous'): void; (e: 'goto', number): void }>()
|
||||
</script>
|
||||
@ -54,3 +54,9 @@ function logout() {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
sl-avatar {
|
||||
--size: 24pt;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<form @submit.prevent="createGroup">
|
||||
<sl-input
|
||||
label="Group name"
|
||||
required
|
||||
autocomplete="off"
|
||||
:help-text="helpText"
|
||||
:value="groupName"
|
||||
@blur="validateInput"
|
||||
@input="groupName = $event.target.value"
|
||||
ref="nameField"
|
||||
>
|
||||
<sl-tag slot="prefix">{{ parentPrefix }}</sl-tag>
|
||||
</sl-input>
|
||||
<br />
|
||||
<sl-button variant="primary" type="submit">Add group</sl-button>
|
||||
<sl-button variant="default" @click="emit('cancel')">Cancel</sl-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
|
||||
const props = defineProps<{ parent }>()
|
||||
const groupName = ref<string>('')
|
||||
const targetPath = computed(() => `${parentPath.value}/${groupName.value}`)
|
||||
const parentPath = toRef(() => props.parent)
|
||||
const parentPrefix = computed(() => parentPath.value + '/')
|
||||
const helpText = computed(() => {
|
||||
if (!parentPath.value) {
|
||||
return 'Name of the group to create. It will be created in the root.'
|
||||
}
|
||||
return 'Name of the group to create below the selected group.'
|
||||
})
|
||||
const emit = defineEmits<{ (e: 'submit', data: string): void; (e: 'cancel'): void }>()
|
||||
|
||||
const nameField = ref<HTMLElement>()
|
||||
|
||||
function validateInput(): boolean {
|
||||
if (groupName.value.includes('/')) {
|
||||
nameField.value?.setCustomValidity('Group name cannot contain /')
|
||||
}
|
||||
nameField.value?.reportValidity()
|
||||
const validity = nameField.validity
|
||||
return validity?.valid ?? true
|
||||
}
|
||||
|
||||
function createGroup() {
|
||||
if (validateInput()) {
|
||||
const newPath = `${parentPath.value}/${groupName.value}`
|
||||
emit('submit', newPath)
|
||||
} else {
|
||||
console.log('Something fishy')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,17 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<sl-select
|
||||
label="Select clients"
|
||||
:value="selectedClients"
|
||||
multiple
|
||||
clearable
|
||||
@sl-change="selectedClients = $event.target.value"
|
||||
id="selectClientDialog"
|
||||
>
|
||||
<template v-for="client in clients">
|
||||
<sl-option :value="client.id">{{ client.name }}</sl-option>
|
||||
</template>
|
||||
</sl-select>
|
||||
<ClientSelectDropdown v-model="selectedClients" />
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<sl-button
|
||||
@ -31,29 +20,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
import '@shoelace-style/shoelace/dist/components/option/option.js'
|
||||
import '@shoelace-style/shoelace/dist/components/select/select.js'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import type { ClientReference } from '@/client'
|
||||
import ClientSelectDropdown from '@/components/clients/ClientSelectDropdown.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'addClients', data: string[]): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const clients = ref<ClientReference[]>([])
|
||||
const selectedClients = ref<string[]>([])
|
||||
|
||||
async function getClients() {
|
||||
// Get just names and IDs of the clients
|
||||
const response = await SshecretAdmin.getClientsTerseApiV1ClientsTerseGet()
|
||||
if (response.data) {
|
||||
clients.value = response.data
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
emit('cancel')
|
||||
}
|
||||
@ -61,6 +37,4 @@ function cancelAdd() {
|
||||
function submitAdd() {
|
||||
emit('addClients', selectedClients.value)
|
||||
}
|
||||
|
||||
onMounted(getClients)
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="flex justify-end px-4">
|
||||
<sl-dropdown>
|
||||
<div slot="trigger">
|
||||
<sl-icon-button name="three-dots"></sl-icon-button>
|
||||
</div>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="edit" @click="showMove = true">Move Group</sl-menu-item>
|
||||
<sl-menu-item value="delete">
|
||||
<span class="text-red-600" @click="showConfirm = true">Delete group</span>
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
<div id="group_details">
|
||||
<div class="w-full p-2">
|
||||
<div class="px-4 sm:px-0">
|
||||
<h3 class="text-base/7 font-semibold text-gray-900 dark:text-gray-50">
|
||||
Group {{ group.group_name }}
|
||||
</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm/6 text-gray-500 dark:text-gray-100">{{ group.path }}</p>
|
||||
</div>
|
||||
<div class="mt-6 border-t border-gray-100">
|
||||
<dl class="divide-y divide-gray-100">
|
||||
<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">
|
||||
Entries in this group
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300">
|
||||
{{ group.entries.length }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sl-dialog label="Move group" :open="showMove" @sl-request-close.prevent="" no-header>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-lg">New path</h2>
|
||||
</div>
|
||||
<GroupMoveTree :self="group.path" @selected="moveGroup" @cancel="hideMove" :key="moveKey" />
|
||||
</sl-dialog>
|
||||
<sl-dialog label="Are you sure?" :open="showConfirm" @sl-request-close.prevent="" no-header>
|
||||
Are you sure you want to delete this client?
|
||||
<div slot="footer">
|
||||
<sl-button variant="default" @click="showConfirm = false" class="mr-2">Cancel</sl-button>
|
||||
<sl-button variant="danger" @click="deleteGroup">Delete</sl-button>
|
||||
</div>
|
||||
</sl-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, toRef } from 'vue'
|
||||
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import GroupMoveTree from '@/components/secrets/GroupMoveTree.vue'
|
||||
|
||||
const treeState = useTreeState()
|
||||
|
||||
const props = defineProps<{ group: ClientSecretGroup }>()
|
||||
|
||||
const showMove = ref(false)
|
||||
|
||||
const moveKey = ref(0)
|
||||
|
||||
function hideMove() {
|
||||
showMove.value = false
|
||||
moveKey.value += 1
|
||||
}
|
||||
|
||||
async function deleteGroup() {
|
||||
await SshecretAdmin.deleteSecretGroupApiV1SecretsGroupsGroupPathDelete({
|
||||
path: {
|
||||
group_path: props.group.path,
|
||||
},
|
||||
})
|
||||
treeState.bumpGroupRevision()
|
||||
treeState.unselect()
|
||||
}
|
||||
|
||||
const showConfirm = ref<boolean>(false)
|
||||
|
||||
const group = toRef(() => props.group)
|
||||
async function moveGroup(path: string) {
|
||||
console.log('Moving group ', props.group.path, 'destination ', path)
|
||||
await SshecretAdmin.moveGroupApiV1SecretsMoveGroupGroupNamePost({
|
||||
path: {
|
||||
group_name: props.group.path,
|
||||
},
|
||||
body: {
|
||||
path: path,
|
||||
},
|
||||
})
|
||||
treeState.bumpGroupRevision()
|
||||
showMove.value = false
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<template v-if="groups">
|
||||
<sl-select
|
||||
placeholder="Target parent"
|
||||
hoist
|
||||
:value="selectedPath"
|
||||
@sl-change="selectedPath = $event.target.value"
|
||||
>
|
||||
<sl-option value="/">/</sl-option>
|
||||
<sl-option v-for="group in groups" :key="group.path" :value="group.path">{{
|
||||
group.path
|
||||
}}</sl-option>
|
||||
</sl-select>
|
||||
</template>
|
||||
<sl-button variant="success" outline @click="selectPath" class="mr-2"> Move</sl-button>
|
||||
<sl-button variant="default" outline @click="emit('cancel')"> Cancel</sl-button>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import { ref, onMounted, toRef } from 'vue'
|
||||
|
||||
const props = defineProps<{ self: string }>()
|
||||
const groups = ref<ClientSecretGroup[]>([])
|
||||
|
||||
const ownPath = toRef(() => props.self)
|
||||
const emit = defineEmits<{ (e: 'selected', data: string): void; (e: 'cancel'): void }>()
|
||||
|
||||
const selectedPath = ref()
|
||||
|
||||
async function getGroups() {
|
||||
selectedPath.value = props.self
|
||||
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet({
|
||||
query: { flat: true },
|
||||
})
|
||||
if (response.data) {
|
||||
groups.value = response.data.groups
|
||||
}
|
||||
}
|
||||
|
||||
function selectPath() {
|
||||
emit('selected', selectedPath.value)
|
||||
}
|
||||
|
||||
onMounted(getGroups)
|
||||
</script>
|
||||
@ -48,9 +48,9 @@
|
||||
<sl-tag
|
||||
class="mr-2"
|
||||
removable
|
||||
@sl-remove="removeClient(client.id, $event)"
|
||||
v-for="client in clients"
|
||||
:key="client.id"
|
||||
@sl-remove="removeClient(client.id)"
|
||||
>
|
||||
{{ client.name }}
|
||||
</sl-tag>
|
||||
@ -82,9 +82,14 @@
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 mb-2">
|
||||
<sl-button variant="warning" outline :disabled="!secretChanged"
|
||||
>Update</sl-button
|
||||
<sl-button
|
||||
variant="warning"
|
||||
outline
|
||||
@click="updateSecret"
|
||||
:disabled="!secretChanged"
|
||||
>
|
||||
Update
|
||||
</sl-button>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
@ -144,7 +149,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="events"> </sl-tab-panel>
|
||||
<sl-tab-panel name="events">
|
||||
<AuditTable :auditFilter="auditFilter" />
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
<sl-drawer
|
||||
id="addDialogDrawer"
|
||||
@ -162,15 +169,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/details/details.js'
|
||||
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
|
||||
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tag/tag.js'
|
||||
|
||||
import type { SecretView } from '@/client/types.gen.ts'
|
||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||
|
||||
import AddSecretToClients from '@/components/secrets/AddSecretToClients.vue'
|
||||
|
||||
@ -186,7 +186,7 @@ const secret = ref<Secret>(props.secret)
|
||||
|
||||
const clients = computed(() => props.secret?.clients ?? [])
|
||||
|
||||
const secretValue = ref<string | null>({ ...props.secret?.secret })
|
||||
const secretValue = ref<string | null>(secret.value?.secret)
|
||||
|
||||
const addDialog = ref<boolean>(false)
|
||||
|
||||
@ -197,6 +197,11 @@ const secretChanged = computed(() => {
|
||||
return secretValue.value !== secret.value.secret
|
||||
})
|
||||
|
||||
const auditFilter = {
|
||||
secret_name: props.secret.name,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
function handleHide(event) {
|
||||
const targetId = event.target.id
|
||||
if (targetId === 'addDialogDrawer') {
|
||||
@ -212,7 +217,10 @@ function addSecretToClients(clientIds: string[]) {
|
||||
clientIds.forEach((clientId) => emit('addClient', clientId))
|
||||
}
|
||||
|
||||
function removeClient(clientId: string) {
|
||||
function removeClient(clientId: string, event: any) {
|
||||
console.log(event)
|
||||
event.target.classList.add('hidden')
|
||||
console.log(event.target.parentNode)
|
||||
emit('removeClient', clientId)
|
||||
}
|
||||
</script>
|
||||
|
||||
121
packages/sshecret-frontend/src/components/secrets/SecretForm.vue
Normal file
121
packages/sshecret-frontend/src/components/secrets/SecretForm.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitCreateSecret" ref="createSecretForm">
|
||||
<sl-input
|
||||
label="Name"
|
||||
required
|
||||
autocomplete="off"
|
||||
help-text="Name of the secret"
|
||||
:value="secretName"
|
||||
@input="secretName = $event.target.value"
|
||||
ref="nameField"
|
||||
></sl-input>
|
||||
<template v-if="group">
|
||||
<sl-input
|
||||
disabled
|
||||
:value="group"
|
||||
label="Group"
|
||||
help-text="Path of the group the secret will be created in."
|
||||
></sl-input>
|
||||
<br />
|
||||
</template>
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="grow w-full mr-2">
|
||||
<sl-range
|
||||
min="8"
|
||||
max="64"
|
||||
@sl-change="secretLength = $event.target.value"
|
||||
:value="secretLength"
|
||||
label="Length"
|
||||
></sl-range>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<sl-button outline @click="generatePassword">Auto-generate</sl-button>
|
||||
</div>
|
||||
</div>
|
||||
<sl-input
|
||||
label="Secret"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="off"
|
||||
help-text="Secret value"
|
||||
@input="secretValue = $event.target.value"
|
||||
:value="secretValue"
|
||||
ref="secretField"
|
||||
password-toggle
|
||||
></sl-input>
|
||||
<label for="clients" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>Clients</label
|
||||
>
|
||||
<ClientSelectDropdown v-model="selectedClients" />
|
||||
<div slot="footer">
|
||||
<sl-button size="medium" variant="success" outline @click="submitCreateSecret" class="mr-2">
|
||||
<sl-icon slot="prefix" name="person-plus"></sl-icon>
|
||||
Submit
|
||||
</sl-button>
|
||||
<sl-button size="medium" outline @click="cancelCreateSecret">Cancel</sl-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SecretCreate } from '@/client'
|
||||
import { ref } from 'vue'
|
||||
import { generateRandomPassword } from '@/api/password'
|
||||
import AddSecretsToClients from '@/components/secrets/AddSecretToClients.vue'
|
||||
import ClientSelectDropdown from '@/components/clients/ClientSelectDropdown.vue'
|
||||
|
||||
const props = defineProps<{ group?: string }>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'submit', data: SecretCreate): void; (e: 'cancel'): void }>()
|
||||
|
||||
const secretName = ref<string>()
|
||||
const createSecretForm = ref<HTMLFormElement>()
|
||||
const nameField = ref<HTMLSlInputElement>()
|
||||
const secretField = ref<HTMLSlInputElement>()
|
||||
const secretValue = ref()
|
||||
const secretLength = ref(8)
|
||||
const autoGenerate = ref(false)
|
||||
const selectedClients = ref<string[]>()
|
||||
|
||||
function setFieldValidation(field: Ref<HTMLSlInputElement>, errorMessage: string = '') {
|
||||
// Set validation on a field
|
||||
field.value?.setCustomValidity(errorMessage)
|
||||
field.value?.reportValidity()
|
||||
}
|
||||
|
||||
function generatePassword() {
|
||||
const password = generateRandomPassword(secretLength.value)
|
||||
secretValue.value = password
|
||||
}
|
||||
|
||||
function validateName() {
|
||||
nameField.value.reportValidity()
|
||||
}
|
||||
|
||||
function validateSecret() {
|
||||
secretField.value.reportValidity()
|
||||
}
|
||||
|
||||
function cancelCreateSecret() {
|
||||
createSecretForm.value.reset()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function submitCreateSecret() {
|
||||
validateName()
|
||||
validateSecret()
|
||||
|
||||
if (createSecretForm.value?.checkValidity()) {
|
||||
console.log('SelectedClients: ', selectedClients.value)
|
||||
const secretGroup = props.group ?? null
|
||||
const secretCreate: SecretCreate = {
|
||||
value: secretValue.value,
|
||||
name: secretName.value,
|
||||
clients: [...selectedClients.value],
|
||||
client_distinguisher: 'id',
|
||||
group: secretGroup,
|
||||
}
|
||||
emit('submit', secretCreate)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<SecretGroupTreeItem :path="group.path" :name="group.group_name">
|
||||
<template v-if="group.children && group.children.length > 0">
|
||||
<template v-for="child in group.children" :key="child.id">
|
||||
<SecretGroup :key="child.id" :group="child" v-if="group.path !== except" />
|
||||
</template>
|
||||
</template>
|
||||
<template v-for="entry in group.entries" :key="entry.name">
|
||||
<SecretGroupTreeEntry :name="entry.name" :groupPath="groupPath" />
|
||||
</template>
|
||||
</SecretGroupTreeItem>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { toRef } from 'vue'
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import SecretGroup from '@/components/secrets/SecretGroup.vue'
|
||||
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
|
||||
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
|
||||
|
||||
const props = defineProps<{ group: SecretGroup; except?: string }>()
|
||||
|
||||
const groupPath = toRef(() => props.group.path)
|
||||
</script>
|
||||
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<sl-tree-item :id="itemId" data-type="secret" :data-name="name" :data-group-path="groupPath">
|
||||
<sl-icon name="file-lock2"> </sl-icon>
|
||||
<span class="px-2">{{ name }}</span>
|
||||
</sl-tree-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRef } from 'vue'
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
groupPath: string
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const groupPath = toRef(() => props.path)
|
||||
const itemId = computed(() => `secret-${props.name}`)
|
||||
</script>
|
||||
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<sl-tree-item
|
||||
class="secret-group-list-item"
|
||||
v-bind:id="itemId"
|
||||
data-type="group"
|
||||
:data-group-path="groupPath"
|
||||
>
|
||||
<sl-icon name="folder"></sl-icon>
|
||||
<span class="px-2">{{ name }}</span>
|
||||
<slot />
|
||||
</sl-tree-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRef } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
path: string
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const groupPath = toRef(() => props.path)
|
||||
const itemId = computed(() => `${name}-${props.path.replace('/', '-')}`)
|
||||
</script>
|
||||
@ -0,0 +1,16 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Creates a safe event handler that only fires when
|
||||
* the event.target matches your element.
|
||||
*/
|
||||
export function useBubbleSafeHandler<T extends HTMLElement>(
|
||||
elementRef: Ref<T | undefined>,
|
||||
handler: (event: Event) => void
|
||||
): (event: Event) => void {
|
||||
return (event: Event) => {
|
||||
if (!elementRef.value) return
|
||||
if (event.target !== elementRef.value) return
|
||||
handler(event)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,36 @@ setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/')
|
||||
import ShoelaceModelDirective from '@shoelace-style/vue-sl-model'
|
||||
|
||||
// End of shoelace import
|
||||
// Import shoelace components
|
||||
import '@shoelace-style/shoelace/dist/components/alert/alert.js'
|
||||
import '@shoelace-style/shoelace/dist/components/animation/animation.js'
|
||||
import '@shoelace-style/shoelace/dist/components/avatar/avatar.js'
|
||||
import '@shoelace-style/shoelace/dist/components/button-group/button-group.js'
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/checkbox/checkbox.js'
|
||||
import '@shoelace-style/shoelace/dist/components/details/details.js'
|
||||
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js'
|
||||
import '@shoelace-style/shoelace/dist/components/divider/divider.js'
|
||||
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
|
||||
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu-label/menu-label.js'
|
||||
import '@shoelace-style/shoelace/dist/components/menu/menu.js'
|
||||
import '@shoelace-style/shoelace/dist/components/option/option.js'
|
||||
import '@shoelace-style/shoelace/dist/components/range/range.js'
|
||||
import '@shoelace-style/shoelace/dist/components/select/select.js'
|
||||
import '@shoelace-style/shoelace/dist/components/skeleton/skeleton.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tag/tag.js'
|
||||
import '@shoelace-style/shoelace/dist/components/textarea/textarea.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
@ -15,6 +45,8 @@ import '@/api/interceptors'
|
||||
import { useAuthTokenStore } from '@/store/auth'
|
||||
import { client } from '@/client/client.gen'
|
||||
|
||||
|
||||
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import LoginPage from '@/views/LoginPage.vue'
|
||||
import WorkspaceView from '@/views/WorkspaceView.vue'
|
||||
import AuditView from '@/views/audit/AuditView.vue'
|
||||
import { useAuthTokenStore } from '@/store/auth'
|
||||
|
||||
|
||||
@ -12,6 +13,9 @@ const routes = [
|
||||
name: 'clientList',
|
||||
component: WorkspaceView,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/audit', name: 'audit', component: AuditView, meta: { requiresAuth: true },
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
26
packages/sshecret-frontend/src/store/useAlertsStore.ts
Normal file
26
packages/sshecret-frontend/src/store/useAlertsStore.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// stores/useAlertsStore.ts
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface Alert {
|
||||
id: number
|
||||
message: string
|
||||
type: 'info' | 'success' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
export const useAlertsStore = defineStore('alerts', {
|
||||
state: () => ({
|
||||
alerts: [] as Alert[],
|
||||
}),
|
||||
actions: {
|
||||
showAlert(message: string, type: Alert['type'] = 'info') {
|
||||
this.alerts.push({
|
||||
id: Date.now(),
|
||||
message,
|
||||
type,
|
||||
})
|
||||
},
|
||||
removeAlert(id: number) {
|
||||
this.alerts = this.alerts.filter(a => a.id !== id)
|
||||
},
|
||||
},
|
||||
})
|
||||
74
packages/sshecret-frontend/src/store/useAuditFilterState.ts
Normal file
74
packages/sshecret-frontend/src/store/useAuditFilterState.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { SubSystem, Operation } from "@/client";
|
||||
import { isOperation, isSubSystem } from '@/api/typeguards';
|
||||
|
||||
export interface ViewAuditFilters {
|
||||
subSystem: SubSystem | null
|
||||
operation: Operation | null
|
||||
clientName: string | null
|
||||
secretName: string | null
|
||||
origin: string | null
|
||||
}
|
||||
|
||||
function withNullValue(value: string | null): string | null {
|
||||
if (value === '') {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function subSystemOrNull(value: string | null): SubSystem | null {
|
||||
if (!value) {
|
||||
return null
|
||||
} else if (isSubSystem(value)) {
|
||||
return value
|
||||
}
|
||||
throw "Invalid subsystem value"
|
||||
}
|
||||
|
||||
function operationOrNull(value: string | null): Operation | null {
|
||||
if (!value) {
|
||||
return null
|
||||
} else if (isOperation(value)) {
|
||||
return value
|
||||
}
|
||||
throw "Invalid operation value"
|
||||
}
|
||||
|
||||
|
||||
export const useAuditFilterState = defineStore('auditFilterState', {
|
||||
state: () => ({
|
||||
subSystem: null as SubSystem | null,
|
||||
operation: null as Operation | null,
|
||||
clientName: null as string | null,
|
||||
secretName: null as string | null,
|
||||
origin: null as string | null,
|
||||
}),
|
||||
actions: {
|
||||
setValues(items: ViewAuditFilters) {
|
||||
this.subSystem = subSystemOrNull(items['subSystem'])
|
||||
this.operation = operationOrNull(items['operation'])
|
||||
this.clientName = withNullValue(items['clientName'])
|
||||
this.secretName = withNullValue(items['secretName'])
|
||||
this.origin = withNullValue(items['origin'])
|
||||
},
|
||||
reset() {
|
||||
this.subSystem = null
|
||||
this.operation = null
|
||||
this.clientName = null
|
||||
this.secretName = null
|
||||
this.origin = null
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
getFilter: (state) => {
|
||||
return {
|
||||
subsystem: state.subSystem,
|
||||
operation: state.operation,
|
||||
client_name: state.clientName,
|
||||
secret_name: state.secretName,
|
||||
origin: state.origin,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -2,13 +2,19 @@ import { defineStore } from 'pinia'
|
||||
import { SshecretObjectType } from '@/api/types'
|
||||
import type { SshecretObject } from '@/api/types'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import type { ClientQueryResult, Client, SecretView } from '@/client'
|
||||
import type { ClientQueryResult, Client, SecretView, ClientSecretGroupList, SecretListView, ClientSecretGroup } from '@/client'
|
||||
|
||||
export const useTreeState = defineStore('treeState', {
|
||||
state: () => ({
|
||||
selected: null as SshecretObject | null,
|
||||
auditFilter: null as SshecretObject | null,
|
||||
parent: null as SshecretObject | null,
|
||||
clients: null as ClientQueryResult | null,
|
||||
secrets: [] as SecretListView[],
|
||||
secretGroups: null as ClientSecretGroupList | null,
|
||||
secretGroupRevision: 0,
|
||||
clientRevision: 0,
|
||||
showAudit: false as boolean,
|
||||
}),
|
||||
actions: {
|
||||
selectClient(id: string) {
|
||||
@ -20,6 +26,9 @@ export const useTreeState = defineStore('treeState', {
|
||||
this.parent = { objectType: SshecretObjectType.Client, id: parent }
|
||||
}
|
||||
},
|
||||
selectGroup(path: string) {
|
||||
this.selected = { objectType: SshecretObjectType.SecretGroup, id: path }
|
||||
},
|
||||
unselect() {
|
||||
this.selected = null
|
||||
},
|
||||
@ -57,6 +66,23 @@ export const useTreeState = defineStore('treeState', {
|
||||
return 0
|
||||
|
||||
},
|
||||
async getSecretNames(): Promise<boolean> {
|
||||
// Get all secret names.
|
||||
const response = await SshecretAdmin.getSecretNamesApiV1SecretsGet()
|
||||
if (response.data) {
|
||||
this.secrets = response.data
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
async getSecretGroups(): Promise<boolean> {
|
||||
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet()
|
||||
if (response.data) {
|
||||
this.secretGroups = response.data
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
async getClient(id: string | null = null): Promise<Client> {
|
||||
if (!id && this.selected?.objectType === SshecretObjectType.Client) {
|
||||
id = this.selected.id
|
||||
@ -85,7 +111,14 @@ export const useTreeState = defineStore('treeState', {
|
||||
}
|
||||
throw "Secret not found"
|
||||
},
|
||||
async getSelected(): Promise<Client | SecretView | null> {
|
||||
async getGroup(path: string): Promise<ClientSecretGroup> {
|
||||
const response = await SshecretAdmin.getSecretGroupApiV1SecretsGroupsGroupPathGet({ path: { group_path: path } })
|
||||
if (response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw "Group not found"
|
||||
},
|
||||
async getSelected(): Promise<Client | SecretView | ClientSecretGroup | null> {
|
||||
if (!this.selected) {
|
||||
return null
|
||||
}
|
||||
@ -93,6 +126,8 @@ export const useTreeState = defineStore('treeState', {
|
||||
return await this.getClient(this.selected.id)
|
||||
} else if (this.selected.objectType === SshecretObjectType.ClientSecret) {
|
||||
return await this.getSecret(this.selected.id)
|
||||
} else if (this.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return await this.getGroup(this.selected.id)
|
||||
}
|
||||
else {
|
||||
throw "Invalid object"
|
||||
@ -113,7 +148,13 @@ export const useTreeState = defineStore('treeState', {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
bumpGroupRevision() {
|
||||
this.secretGroupRevision += 1
|
||||
},
|
||||
bumpClientRevision() {
|
||||
this.clientRevision += 1
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
clientSelected() {
|
||||
@ -133,6 +174,12 @@ export const useTreeState = defineStore('treeState', {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
currentGroupPath(): string {
|
||||
if (this.selected && this.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return this.selected.id
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,30 +1,93 @@
|
||||
<template>
|
||||
<MasterDetail>
|
||||
<template #master>
|
||||
<ClientTreeList />
|
||||
<sl-tab-group
|
||||
id="sideTabs"
|
||||
class="flex flex-col flex-1 h-full overflow-hidden master-pane-tabs"
|
||||
@sl-tab-show="tabSelected($event)"
|
||||
>
|
||||
<sl-tab slot="nav" panel="clients">Clients</sl-tab>
|
||||
<sl-tab slot="nav" panel="secrets">Secrets</sl-tab>
|
||||
<sl-tab slot="nav" panel="audit">Audit</sl-tab>
|
||||
<sl-tab-panel name="clients">
|
||||
<ClientTreeList />
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="secrets">
|
||||
<SecretTreeList />
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="audit">
|
||||
<AuditFilters />
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
</template>
|
||||
<template #detail 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"
|
||||
/>
|
||||
<template #detail v-if="showAudit">
|
||||
<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 { ref } from 'vue'
|
||||
import AuditView from '@/views/audit/AuditView.vue'
|
||||
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||
import ClientTreeList from '@/views/Clients/ClientTreeList.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 { SshecretObjectType } from '@/api/types'
|
||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
|
||||
const treeState = useTreeState()
|
||||
|
||||
const auditFilterState = useAuditFilterState()
|
||||
|
||||
const showAudit = ref<{ boolean }>()
|
||||
|
||||
function tabSelected(tab) {
|
||||
const tabName = tab.detail.name
|
||||
if (tabName == 'audit') {
|
||||
console.log('Showing audit')
|
||||
treeState.showAudit = true
|
||||
showAudit.value = true
|
||||
} else {
|
||||
treeState.showAudit = false
|
||||
showAudit.value = false
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
22
packages/sshecret-frontend/src/views/audit/AuditView.vue
Normal file
22
packages/sshecret-frontend/src/views/audit/AuditView.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<template v-if="loaded">
|
||||
<AuditTable :auditFilter="auditFilter" />
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||
import type { AuditFilter } from '@/api/types'
|
||||
import type { GetAuditLogApiV1AuditGetData } from '@/client'
|
||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||
const auditFilterState = useAuditFilterState()
|
||||
const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({})
|
||||
|
||||
watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter))
|
||||
const loaded = ref<{ boolean }>()
|
||||
|
||||
onMounted(() => {
|
||||
loaded.value = true
|
||||
auditFilter.value = auditFilterState.getFilter
|
||||
})
|
||||
</script>
|
||||
@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-2xl mb-4">Clients</h1>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="client in clients" :key="client.id">
|
||||
<strong>{{ client.name }}</strong>
|
||||
<span v-if="client.description">({{ client.description }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
|
||||
|
||||
import { SshecretAdmin } from '@/client/sdk.gen'
|
||||
|
||||
import MasterDetail from '@/components/layout/MasterDetail.vue'
|
||||
|
||||
const clients = ref<any[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await SshecretAdmin.getClientsApiV1ClientsGet()
|
||||
clients.value = response.data
|
||||
})
|
||||
</script>
|
||||
@ -33,7 +33,7 @@
|
||||
</sl-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div id="client-tree-items" class="flex flex-col h-full min-h-0">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<sl-tree class="w-full" @sl-selection-change="itemSelected" v-if="treeState.clients">
|
||||
<template v-for="client in treeState.clients.clients">
|
||||
@ -61,74 +61,21 @@
|
||||
</span>
|
||||
<div class="inline-flex mt-2 xs:mt-0">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="flex items-center -space-x-px h-8 text-sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
:disabled="pageNum <= 1"
|
||||
@click="prevPage"
|
||||
>
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg
|
||||
class="w-2.5 h-2.5 rtl:rotate-180"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 6 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 1 1 5l4 4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
<li v-for="n in totalPages">
|
||||
<button
|
||||
class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
|
||||
disabled
|
||||
v-if="n === pageNum"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
@click="pageNum = n"
|
||||
v-else
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
@click="nextPage"
|
||||
:disabled="pageNum >= totalPages"
|
||||
>
|
||||
<span class="sr-only">Next</span>
|
||||
<svg
|
||||
class="w-2.5 h-2.5 rtl:rotate-180"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 6 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m1 9 4-4-4-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<PageNumbers
|
||||
@next="nextPage"
|
||||
@previous="prevPage"
|
||||
@goto="goToPage"
|
||||
:pageNum="pageNum"
|
||||
:totalPages="totalPages"
|
||||
>
|
||||
<template #previous>
|
||||
<sl-icon name="chevron-left" slot="prefix"></sl-icon>
|
||||
</template>
|
||||
|
||||
<template #next>
|
||||
<sl-icon name="chevron-right" slot="prefix"></sl-icon>
|
||||
</template>
|
||||
</PageNumbers>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,13 +91,6 @@
|
||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
|
||||
|
||||
import { usePagination } from '@/composables/usePagination'
|
||||
|
||||
import { SshecretAdmin } from '@/client/sdk.gen'
|
||||
@ -159,16 +99,16 @@ import type { Client, ClientCreate } from '@/client/types.gen'
|
||||
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
|
||||
import MasterDetail from '@/components/layout/MasterDetail.vue'
|
||||
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
|
||||
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
|
||||
import ClientForm from '@/components/clients/ClientForm.vue'
|
||||
import PageNumbers from '@/components/common/PageNumbers.vue'
|
||||
|
||||
import { useDebounce } from '@/composables/useDebounce'
|
||||
const treeState = useTreeState()
|
||||
|
||||
const clientsPerPage = 20
|
||||
const totalClients = computed(() => treeState.clients?.total_clients)
|
||||
const totalClients = computed(() => treeState.clients?.total_results)
|
||||
|
||||
const clients = computed(() => treeState.clients.clients)
|
||||
const selectedClient = ref<Client | null>(null)
|
||||
@ -181,16 +121,12 @@ const clientQuery = ref('')
|
||||
|
||||
const debouncedQuery = useDebounce(clientQuery, 300)
|
||||
|
||||
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage } = usePagination(
|
||||
totalClients,
|
||||
clientsPerPage,
|
||||
)
|
||||
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
|
||||
usePagination(totalClients, clientsPerPage)
|
||||
|
||||
async function loadClients() {
|
||||
if (clientQuery.value) {
|
||||
console.log('Search term: ', clientQuery.value)
|
||||
await treeState.queryClients(clientQuery.value, offset.value, clientsPerPage)
|
||||
console.log(`Got ${fetchedClients} results`)
|
||||
} else {
|
||||
await treeState.loadClients(offset.value, clientsPerPage)
|
||||
}
|
||||
@ -210,7 +146,7 @@ function itemSelected(event: Event) {
|
||||
} else {
|
||||
const el = event.detail.selection[0] as HTMLElement
|
||||
const childType = el.dataset.type
|
||||
if (childType == 'client') {
|
||||
if (childType === 'client') {
|
||||
const clientId = el.dataset.clientId
|
||||
treeState.selectClient(clientId)
|
||||
} else if (childType == 'secret') {
|
||||
|
||||
@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<MasterDetail>
|
||||
<template #master>
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div class="tree-header mb-2 grid grid-cols-2 place-content-between">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Client List</h1>
|
||||
<div class="flex">
|
||||
<div class="flex w-full justify-end">
|
||||
<sl-icon-button
|
||||
name="plus-square"
|
||||
label="Add Client"
|
||||
@click="createDrawerOpen = !createDrawerOpen"
|
||||
></sl-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label
|
||||
for="client-search"
|
||||
name="client-search"
|
||||
class="mb-2 text-xs font-medium text-gray-900 sr-only dark:text-white"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
<sl-input type="search" size="small" placeholder="search" clearable>
|
||||
<template v-slot:prefix>
|
||||
<sl-icon name="search" ></sl-icon>
|
||||
</template>
|
||||
</sl-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<sl-tree class="w-full" @sl-selection-change="itemSelected">
|
||||
<template v-for="client in clients">
|
||||
<ClientTreeItem :id="client.id" :name="client.name" :selected="false">
|
||||
<template v-for="secret in client.secrets">
|
||||
<ClientSecretTreeItem :parent_id="client.id" :name="secret" />
|
||||
</template>
|
||||
</ClientTreeItem>
|
||||
</template>
|
||||
</sl-tree>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 mt-4 pt-2 border-t border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
v-if="totalPages > 1"
|
||||
>
|
||||
<div class="mt-4 text-center flex items-center flex-col">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-400">
|
||||
Showing
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ firstResult }}</span>
|
||||
to
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ lastResult }}</span>
|
||||
of
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ totalClients }}</span>
|
||||
Entries
|
||||
</span>
|
||||
<div class="inline-flex mt-2 xs:mt-0">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="flex items-center -space-x-px h-8 text-sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
:disabled="pageNum <= 1"
|
||||
@click="prevPage"
|
||||
>
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg
|
||||
class="w-2.5 h-2.5 rtl:rotate-180"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 6 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 1 1 5l4 4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
<li v-for="n in totalPages">
|
||||
<button
|
||||
class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
|
||||
disabled
|
||||
v-if="n === pageNum"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
@click="pageNum = n"
|
||||
v-else
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
@click="nextPage"
|
||||
:disabled="pageNum >= totalPages"
|
||||
>
|
||||
<span class="sr-only">Next</span>
|
||||
<svg
|
||||
class="w-2.5 h-2.5 rtl:rotate-180"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 6 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m1 9 4-4-4-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #detail v-if="treeState.selected">
|
||||
<ClientDetail
|
||||
:client="treeState.client"
|
||||
:key="treeState.client.id"
|
||||
v-if="treeState.item_type == 'client'"
|
||||
@update="updateClient"
|
||||
@deleted="clientDeleted"
|
||||
/>
|
||||
<SecretDetail
|
||||
:secret_name="treeState.secret_name"
|
||||
:key="treeState.secret_name"
|
||||
v-if="treeState.item_type == 'secret'"
|
||||
/>
|
||||
</template>
|
||||
</MasterDetail>
|
||||
<sl-drawer label="Create Client" :open="createDrawerOpen" @sl-hide="createDrawerOpen = false">
|
||||
<ClientForm @submit="createClient" @cancel="createDrawerOpen = false" :key="createFormKey" />
|
||||
</sl-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
|
||||
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js'
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
|
||||
|
||||
import { usePagination } from '@/composables/usePagination'
|
||||
|
||||
import { SshecretAdmin } from '@/client/sdk.gen'
|
||||
|
||||
import type { Client, ClientCreate } from '@/client/types.gen'
|
||||
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
|
||||
import MasterDetail from '@/components/layout/MasterDetail.vue'
|
||||
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
|
||||
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
|
||||
import ClientDetail from '@/components/clients/ClientDetail.vue'
|
||||
import SecretDetail from '@/components/secrets/SecretDetail.vue'
|
||||
import ClientForm from '@/components/clients/ClientForm.vue'
|
||||
|
||||
const clientsPerPage = 20
|
||||
const totalClients = ref<number>(0)
|
||||
|
||||
const clients = ref<Client[]>([])
|
||||
interface TreeState {
|
||||
selected: boolean
|
||||
item_type?: string
|
||||
secret_name?: string
|
||||
client?: Client
|
||||
}
|
||||
const treeState = ref<TreeState>({ selected: false })
|
||||
const selectedClient = ref<Client | null>(null)
|
||||
const selectedSecret = ref<string | null>(null)
|
||||
|
||||
const createFormKey = ref<number>(0)
|
||||
const createDrawerOpen = ref<boolean>(false)
|
||||
|
||||
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage } = usePagination(
|
||||
totalClients,
|
||||
20,
|
||||
)
|
||||
|
||||
async function loadClients() {
|
||||
const response = await SshecretAdmin.queryClientsApiV1QueryClientsGet({
|
||||
query: {
|
||||
offset: offset.value,
|
||||
limit: clientsPerPage,
|
||||
},
|
||||
})
|
||||
clients.value = response.data.clients
|
||||
totalClients.value = response.data.total_results
|
||||
}
|
||||
|
||||
function updateClient(updated: Client) {
|
||||
const index = clients.value.findIndex((c) => c.name === updated.name)
|
||||
console.log(`UpdateClient fired: ${updated.name} => ${index}`)
|
||||
if (index >= 0) {
|
||||
clients.value[index] = updated
|
||||
}
|
||||
}
|
||||
|
||||
function itemSelected(event: Event) {
|
||||
if (event.detail.selection.length == 0) {
|
||||
treeState.value.selected = false
|
||||
treeState.value.client = null
|
||||
treeState.value.secret_name = null
|
||||
treeState.value.item_type = null
|
||||
} else {
|
||||
console.log('Something else was selected')
|
||||
treeState.value.selected = true
|
||||
|
||||
const el = event.detail.selection[0] as HTMLElement
|
||||
const childType = el.dataset.type
|
||||
if (childType == 'client') {
|
||||
const clientId = el.dataset.clientId
|
||||
console.log(`Selected client ${clientId}`)
|
||||
const targetClient = clients.value.find((client) => client.id === clientId)
|
||||
treeState.value.item_type = 'client'
|
||||
treeState.value.client = targetClient
|
||||
treeState.value.secret_name = null
|
||||
} else if (childType == 'secret') {
|
||||
const secretName = el.dataset.name
|
||||
treeState.value.item_type = 'secret'
|
||||
treeState.value.secret_name = secretName
|
||||
treeState.value.client = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createClient(data: ClientCreate) {
|
||||
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
|
||||
console.log(response.data)
|
||||
clients.value.unshift(response.data)
|
||||
totalClients.value += 1
|
||||
createDrawerOpen.value = false
|
||||
createFormKey.value += 1
|
||||
treeState.value.selected = true
|
||||
treeState.value.item_type = 'secret'
|
||||
treeState.value.client = response.data
|
||||
}
|
||||
|
||||
async function clientDeleted(id: string) {
|
||||
const index = clients.value.findIndex((c) => c.id === id)
|
||||
console.log(`Client Deleted event received: ID: ${id} => ${index}`)
|
||||
if (index >= 0) {
|
||||
clients.value.splice(index, 1)
|
||||
treeState.value.selected = false
|
||||
treeState.value.item_type = null
|
||||
treeState.value.client = null
|
||||
await loadClients()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadClients)
|
||||
watch([offset, pageNum], loadClients)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
sl-input::part(prefix) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
sl-input::part(base) {
|
||||
background-color: var(--color-gray-50);
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<navbar>
|
||||
<Navbar>
|
||||
<button
|
||||
id="sidebar-toggle"
|
||||
aria-expanded="true"
|
||||
@ -10,28 +10,26 @@
|
||||
>
|
||||
<sl-icon name="list" class="text-xl"></sl-icon>
|
||||
</button>
|
||||
</navbar>
|
||||
</Navbar>
|
||||
|
||||
<main id="content" class="flex-1 overflow-y-auto">
|
||||
<div>
|
||||
<div class="flex h-[calc(100vh-3.5rem)] overflow-hidden">
|
||||
<aside
|
||||
id="master-pane"
|
||||
:class="[
|
||||
'flex flex-col overflow-hidden lg:block lg:w-80 w-full shrink-0 border-r bg-white border-gray-200 p-4 dark:bg-gray-800 dark:border-gray-700',
|
||||
{ hidden: masterHidden },
|
||||
]"
|
||||
>
|
||||
<slot name="master" />
|
||||
</aside>
|
||||
<section id="detail-pane" class="flex-1 flex overflow-y-auto bg-white p-4 dark:bg-gray-800">
|
||||
<div class="flex flex-col w-full">
|
||||
<slot name="detail">
|
||||
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="flex h-[calc(100vh-3.5rem)] overflow-hidden">
|
||||
<aside
|
||||
id="master-pane"
|
||||
:class="[
|
||||
'flex flex-col overflow-hidden lg:block lg:w-80 w-full shrink-0 border-r bg-white border-gray-200 p-4 dark:bg-gray-800 dark:border-gray-700',
|
||||
{ hidden: masterHidden },
|
||||
]"
|
||||
>
|
||||
<slot name="master" />
|
||||
</aside>
|
||||
<section id="detail-pane" class="flex-1 flex overflow-y-auto bg-white p-4 dark:bg-gray-800">
|
||||
<div class="flex flex-col w-full">
|
||||
<slot name="detail">
|
||||
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -40,6 +40,7 @@ async function updateSecretValue(value: string) {
|
||||
})
|
||||
if (props.parentId) {
|
||||
await treeState.refreshClient(props.parentId)
|
||||
await loadSecret()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<GroupDetail :group="group" v-if="group" />
|
||||
<ClientSkeleton v-else />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import ClientSkeleton from '@/components/clients/ClientSkeleton.vue'
|
||||
import GroupDetail from '@/components/secrets/GroupDetail.vue'
|
||||
|
||||
const treeState = useTreeState()
|
||||
const props = defineProps<{ groupPath: string }>()
|
||||
|
||||
const group = ref<ClientSecretGroup>()
|
||||
|
||||
async function loadGroup() {
|
||||
if (!props.groupPath) return
|
||||
group.value = await treeState.getGroup(props.groupPath)
|
||||
}
|
||||
onMounted(loadGroup)
|
||||
|
||||
watch(
|
||||
() => props.groupPath,
|
||||
() => loadGroup(),
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
194
packages/sshecret-frontend/src/views/secrets/SecretTreeList.vue
Normal file
194
packages/sshecret-frontend/src/views/secrets/SecretTreeList.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div class="tree-header mb-2 grid grid-cols-2 place-content-between">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Secrets</h1>
|
||||
<div class="flex">
|
||||
<div class="flex w-full justify-end">
|
||||
<sl-dropdown>
|
||||
<sl-icon-button slot="trigger" name="plus-square" label="Add Secret"></sl-icon-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item @click="createSecretDrawer = true">Create secret</sl-menu-item>
|
||||
<sl-menu-item @click="createGroupDrawer = true">
|
||||
<span v-if="currentPath">Create subgroup</span>
|
||||
<span v-else>Create group</span>
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<!-- The search would have gone here... -->
|
||||
|
||||
</div>
|
||||
<div id="secret-tree-items" class="flex flex-col h-full min-h-0 col-span-full">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<sl-tree class="w-full" @sl-selection-change="itemSelected" v-if="treeState.secretGroups">
|
||||
<template v-if="ungrouped">
|
||||
<SecretGroupTreeItem path="ungrouped" name="Ungrouped" groupPath="ungrouped">
|
||||
<template v-for="entry in ungrouped">
|
||||
<SecretGroupTreeEntry :name="entry.name" groupPath="ungrouped" />
|
||||
</template>
|
||||
</SecretGroupTreeItem>
|
||||
</template>
|
||||
<template v-if="secretGroups">
|
||||
<SecretGroup v-for="group in secretGroups" :group="group" />
|
||||
</template>
|
||||
</sl-tree>
|
||||
</div>
|
||||
<!-- pagination would go here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer label="Create Group" :open="createGroupDrawer" @hide="createGroupDrawer = false">
|
||||
<AddGroup
|
||||
:parent="currentPath"
|
||||
@submit="createGroup"
|
||||
@cancel="cancelCreateGroup"
|
||||
:key="drawerKey"
|
||||
/>
|
||||
</Drawer>
|
||||
<Drawer label="Create Secret" :open="createSecretDrawer" @hide="createSecretDrawer = false">
|
||||
<SecretForm
|
||||
:key="createDrawerKey"
|
||||
:group="createSecretParent"
|
||||
@submit="createSecret"
|
||||
@cancel="createSecretDrawer = false"
|
||||
/>
|
||||
</Drawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { SshecretObject } from '@/api/types'
|
||||
import type { SecretCreate } from '@/client'
|
||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import { SshecretObjectType } from '@/api/types'
|
||||
import SecretGroup from '@/components/secrets/SecretGroup.vue'
|
||||
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
|
||||
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
|
||||
import AddGroup from '@/components/secrets/AddGroup.vue'
|
||||
import SecretForm from '@/components/secrets/SecretForm.vue'
|
||||
import Drawer from '@/components/common/Drawer.vue'
|
||||
|
||||
const treeState = useTreeState()
|
||||
const alerts = useAlertsStore()
|
||||
|
||||
const ungrouped = computed(() => treeState.secretGroups.ungrouped)
|
||||
const secretGroups = computed(() => treeState.secretGroups.groups)
|
||||
|
||||
const groupSelected = computed(() => {
|
||||
if (!treeState.selected) {
|
||||
return false
|
||||
}
|
||||
if (treesState.selected.objectType === SshecretObject.SecretGroup) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const drawerKey = ref(0)
|
||||
const createDrawerKey = ref(0)
|
||||
const createGroupDrawer = ref<boolean>(false)
|
||||
const createSecretDrawer = ref<boolean>(false)
|
||||
|
||||
function cancelCreateGroup() {
|
||||
createGroupDrawer.value = false
|
||||
drawerKey.value += 1
|
||||
}
|
||||
|
||||
const currentPath = computed(() => {
|
||||
if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return treeState.selected.id
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const createSecretParent = computed(() => {
|
||||
if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return treeState.selected.id
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
await treeState.getSecretGroups()
|
||||
}
|
||||
|
||||
const drawerKeyName = computed(() => `${currentPath.value}_${drawerKey.value}`)
|
||||
|
||||
async function itemSelected(event: Event) {
|
||||
if (event.detail.selection.length == 0) {
|
||||
treeState.unselect()
|
||||
} else {
|
||||
const el = event.detail.selection[0] as HTMLElement
|
||||
const childType = el.dataset.type
|
||||
if (childType === 'secret') {
|
||||
const secretName = el.dataset.name
|
||||
treeState.selectSecret(secretName, null)
|
||||
} else if (childType === 'group') {
|
||||
const groupPath = el.dataset.groupPath
|
||||
if (groupPath === 'ungrouped') {
|
||||
treeState.unselect()
|
||||
} else {
|
||||
treeState.selectGroup(groupPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createGroup(path: string) {
|
||||
// Create a group
|
||||
console.log('Submit called')
|
||||
const response = await SshecretAdmin.addSecretGroupApiV1SecretsGroupsPost({
|
||||
body: {
|
||||
name: path,
|
||||
},
|
||||
})
|
||||
if (response.status === 200) {
|
||||
console.log('Success. Group created.')
|
||||
alerts.showAlert('Group created', 'success')
|
||||
createGroupDrawer.value = false
|
||||
drawerKey.value += 1
|
||||
|
||||
await loadGroups()
|
||||
} else {
|
||||
console.error(response)
|
||||
alerts.showAlert('Group creation failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function createSecret(secretCreate: SecretCreate) {
|
||||
console.log('Creating secret')
|
||||
const response = await SshecretAdmin.addSecretApiV1SecretsPost({
|
||||
body: secretCreate,
|
||||
})
|
||||
if (response.status == 200) {
|
||||
alerts.showAlert('Secret created', 'success')
|
||||
// We can close the drawer now.
|
||||
createSecretDrawer.value = false
|
||||
createDrawerKey.value += 1
|
||||
|
||||
await loadGroups()
|
||||
// Also update all the clients affected
|
||||
for (const clientId in secretCreate.clients) {
|
||||
console.log('Refreshing client: ', clientId)
|
||||
await treeState.refreshClient(clientId)
|
||||
}
|
||||
|
||||
treeState.selectSecret(secretCreate.name)
|
||||
} else {
|
||||
console.log(response)
|
||||
alerts.showAlert('Secret creation failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => treeState.secretGroupRevision,
|
||||
() => {
|
||||
treeState.getSecretGroups()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(loadGroups)
|
||||
</script>
|
||||
Reference in New Issue
Block a user