Deletions, group moves and validation
This commit is contained in:
@ -1,21 +1,10 @@
|
||||
<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
|
||||
>
|
||||
<ShowAlerts />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthTokenStore } from '@/store/auth'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
import ShowAlerts from '@/components/common/ShowAlerts.vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
</script>
|
||||
|
||||
@ -208,7 +208,6 @@ const props = defineProps<Props>()
|
||||
|
||||
const shouldPaginate = toRef(() => props.paginate ?? true)
|
||||
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)
|
||||
|
||||
@ -80,10 +80,7 @@ const name = ref('')
|
||||
const description = ref('')
|
||||
const sourcePrefix = ref('')
|
||||
const policies = ref(['0.0.0.0/0', '::/0'])
|
||||
// This key is only here during testing.
|
||||
const publicKey = ref(
|
||||
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC737Yj7mbuBLDNbAuNGqhFF4Cvzd/ROq/QeQX0QIcPyZOoUtpXc7R/JIrdL6DXkPYXpN/IrUFoSeJQjV9Le+ewVxYELUPVhF0/nQhpBNE1Rjx2PRtJlfmywG5VRStgPQ+DSTDtgm4L0wPpnJiH3udkq/JFMHEYrVAF40QqNmR7AqYo1ZfEFk8YcQGb/S29JxWigq0qoJyufFENmSGNmabjqPAWJEf/oshMPaxwlDfTdmjeUWkPtsm10gi98XCwtnVCAVYZdVKeLSNpQCKUYVYWlycpahNczaITY9lehcMtux79uXTk2d4difra1Q4guw8oorUp1eRn/Al0BPeRb7x9WdgRs8wVY1kPD2796CTAQMkeBrOzGxwzwWhTf1XOuHG/wB5O2QSbcC6aMW9KAFmcCF+AOMb8Mv2Y5D7l/gbp938qTyZJ8ivP1/fy/88CWr+mrv5yP4HOZmNCyC9nMlAvrS/Kkg0tFU+NHFkDsmWpT3oar+VvGzkImEF6ip6Mzk8= testkey',
|
||||
)
|
||||
const publicKey = ref('')
|
||||
|
||||
const nameField = ref<HTMLSlInputElement>()
|
||||
const sourceField = ref<HTMLSlInputElement>()
|
||||
@ -169,15 +166,27 @@ function validatePublicKey() {
|
||||
setFieldValidation(publicKeyField, '')
|
||||
}
|
||||
|
||||
function resetValidation() {
|
||||
setFieldValidation(nameField, '')
|
||||
setFieldValidation(sourceField, '')
|
||||
setFieldValidation(publicKeyField, '')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.errors,
|
||||
(errors) => {
|
||||
resetValidation()
|
||||
const nameErrors = errors.filter((e) => e.loc.includes('name'))
|
||||
const sourceErrors = errors.filter((e) => e.loc.includes('source'))
|
||||
const publicKeyError = errors.filter((e) => e.loc.includes('public_key'))
|
||||
if (nameErrors.length > 0) {
|
||||
console.log(nameErrors)
|
||||
setFieldValidation(nameField, nameErrors[0].msg)
|
||||
} else {
|
||||
setFieldValidation(nameField, '')
|
||||
}
|
||||
if (sourceErrors.length > 0) {
|
||||
setFieldValidation(sourceField, sourceErrors[0].msg)
|
||||
}
|
||||
if (publicKeyError.length > 0) {
|
||||
setFieldValidation(publicKeyField, publicKeyErrors[0].msg)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<sl-alert
|
||||
:variant="alert.type"
|
||||
closable
|
||||
:duration="['warning', 'error', 'danger'].includes(alert.type) ? Infinity : 3000"
|
||||
ref="alertElement"
|
||||
@sl-after-hide="alerts.removeAlert(alert.id)"
|
||||
>
|
||||
<sl-icon slot="icon" :name="alert.icon"></sl-icon>
|
||||
<strong class="alert-title">{{ alert.title }}</strong
|
||||
><br />
|
||||
{{ alert.message }}
|
||||
</sl-alert>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAlertsStore, Alert } from '@/store/useAlertsStore'
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
|
||||
const props = defineProps<{ alert: Alert }>()
|
||||
|
||||
const alertElement = ref<HTMLSlAlertElement>()
|
||||
|
||||
function showToast() {
|
||||
alertElement.value.toast()
|
||||
}
|
||||
|
||||
onMounted(showToast)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.alert-title::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<template v-for="alert in alerts.alerts" :key="alert.id">
|
||||
<AlertToast :alert="alert" />
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
import AlertToast from '@/components/common/AlertToast.vue'
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
</script>
|
||||
@ -16,9 +16,12 @@
|
||||
<sl-button variant="default" outline @click="emit('cancel')"> Cancel</sl-button>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, toRef } from 'vue'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import { ref, onMounted, toRef } from 'vue'
|
||||
|
||||
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
|
||||
const props = defineProps<{ self: string }>()
|
||||
const groups = ref<ClientSecretGroup[]>([])
|
||||
@ -33,8 +36,11 @@ async function getGroups() {
|
||||
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet({
|
||||
query: { flat: true },
|
||||
})
|
||||
if (response.data) {
|
||||
groups.value = response.data.groups
|
||||
try {
|
||||
const responseData = assertSdkResponseOk(response)
|
||||
groups.value = responseData.groups
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Could not fetch groups from the backend')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<template v-if="groups">
|
||||
<sl-select
|
||||
placeholder="Target parent"
|
||||
hoist
|
||||
:value="selectedPath"
|
||||
@sl-change="selectedPath = $event.target.value"
|
||||
>
|
||||
<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'
|
||||
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
|
||||
interface Props {
|
||||
existingPath?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const groups = ref<ClientSecretGroup[]>([])
|
||||
|
||||
const emit = defineEmits<{ (e: 'selected', data: string): void; (e: 'cancel'): void }>()
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
|
||||
const selectedPath = ref('')
|
||||
|
||||
async function getGroups() {
|
||||
const response = await SshecretAdmin.getSecretGroupsApiV1SecretsGroupsGet({
|
||||
query: { flat: true },
|
||||
})
|
||||
try {
|
||||
const responseData = assertSdkResponseOk(response)
|
||||
if (props.existingPath) {
|
||||
groups.value = responseData.groups.filter((entry) => entry.path !== props.existingPath)
|
||||
} else {
|
||||
groups.value = responseData.groups
|
||||
}
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Could not fetch groups from the backend')
|
||||
}
|
||||
}
|
||||
|
||||
function selectPath() {
|
||||
emit('selected', selectedPath.value)
|
||||
}
|
||||
|
||||
onMounted(getGroups)
|
||||
</script>
|
||||
@ -100,12 +100,25 @@
|
||||
>
|
||||
{{ secret.group.path }}
|
||||
<div class="mt-2 float-right">
|
||||
<sl-button size="medium" variant="default" outline>
|
||||
<sl-button size="medium" variant="default" outline @click="showMove = true">
|
||||
<sl-icon slot="prefix" name="box-arrow-in-right"></sl-icon>
|
||||
Move
|
||||
</sl-button>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dd
|
||||
class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300"
|
||||
v-else
|
||||
>
|
||||
Not in any group
|
||||
<div class="mt-2 float-right">
|
||||
<sl-button size="medium" variant="success" outline @click="showMove = true">
|
||||
<sl-icon slot="prefix" name="folder-plus"></sl-icon>
|
||||
Add to group
|
||||
</sl-button>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
@ -179,6 +192,13 @@
|
||||
<sl-button variant="danger" @click="deleteSecret">Delete</sl-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog label="Move secret to group" :open="showMove" @hide="showMove = false">
|
||||
<MoveSecretGroup
|
||||
:existingPath="existingGroupPath"
|
||||
@selected="moveGroup"
|
||||
@cancel="showMove = false"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -188,6 +208,7 @@ import AuditTable from '@/components/audit/AuditTable.vue'
|
||||
|
||||
import AddSecretToClients from '@/components/secrets/AddSecretToClients.vue'
|
||||
import Dialog from '@/components/common/Dialog.vue'
|
||||
import MoveSecretGroup from '@/components/secrets/MoveSecretGroup.vue'
|
||||
|
||||
const props = defineProps<{ secret: SecretView }>()
|
||||
const emit = defineEmits<{
|
||||
@ -195,17 +216,15 @@ const emit = defineEmits<{
|
||||
(e: 'delete'): void
|
||||
(e: 'addClient', data: string): void
|
||||
(e: 'removeClient', data: string): void
|
||||
(e: 'moveGroup', data: string): void
|
||||
}>()
|
||||
|
||||
const secret = ref<Secret>(props.secret)
|
||||
|
||||
const clients = computed(() => props.secret?.clients ?? [])
|
||||
|
||||
const secretValue = ref<string | null>(secret.value?.secret)
|
||||
|
||||
const addDialog = ref<boolean>(false)
|
||||
|
||||
const showConfirm = ref<boolean>(false)
|
||||
const showMove = ref<boolean>(false)
|
||||
|
||||
const secretChanged = computed(() => {
|
||||
if (!secretValue.value) {
|
||||
@ -226,6 +245,13 @@ function handleHide(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function moveGroup(path: string) {
|
||||
emit('moveGroup', path)
|
||||
showMove.value = false
|
||||
}
|
||||
|
||||
const existingGroupPath = computed(() => secret.value.group?.path)
|
||||
|
||||
function updateSecret() {
|
||||
emit('update', secretValue.value)
|
||||
}
|
||||
|
||||
@ -4,7 +4,17 @@ import { defineStore } from 'pinia'
|
||||
interface Alert {
|
||||
id: number
|
||||
message: string
|
||||
type: 'info' | 'success' | 'warning' | 'error'
|
||||
title?: string
|
||||
type: 'info' | 'success' | 'warning' | 'error' | 'danger'
|
||||
icon: string
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
'info': 'info-circle',
|
||||
'success': 'check2-circle',
|
||||
'warning': 'exclamation-triangle',
|
||||
'error': 'exclamation-octagon',
|
||||
'danger': 'exclamation-octagon',
|
||||
}
|
||||
|
||||
export const useAlertsStore = defineStore('alerts', {
|
||||
@ -12,11 +22,24 @@ export const useAlertsStore = defineStore('alerts', {
|
||||
alerts: [] as Alert[],
|
||||
}),
|
||||
actions: {
|
||||
showAlert(message: string, type: Alert['type'] = 'info') {
|
||||
showAlert(message: string, type: Alert['type'] = 'info', title?: string) {
|
||||
if (type === 'error') {
|
||||
type = 'danger'
|
||||
}
|
||||
if (!title) {
|
||||
if (type === 'danger') {
|
||||
title = 'Error'
|
||||
} else {
|
||||
title = type
|
||||
}
|
||||
}
|
||||
const icon = iconMap[type]
|
||||
this.alerts.push({
|
||||
id: Date.now(),
|
||||
message,
|
||||
title,
|
||||
type,
|
||||
icon,
|
||||
})
|
||||
},
|
||||
removeAlert(id: number) {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
@delete="deleteSecret"
|
||||
@addClient="addSecretToClient"
|
||||
@removeClient="removeClientSecret"
|
||||
@moveGroup="moveGroup"
|
||||
v-if="secret"
|
||||
/>
|
||||
</div>
|
||||
@ -17,16 +18,20 @@ import { ref, watch, onMounted } from 'vue'
|
||||
import SecretDetail from '@/components/secrets/SecretDetail.vue'
|
||||
import SecretSkeleton from '@/components/secrets/SecretSkeleton.vue'
|
||||
import NotFound from '@/components/common/NotFound.vue'
|
||||
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
|
||||
import { ApiError, NotFoundError, ValidationError } from '@/api/errors'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { SecretView } from '@/client/types.gen.ts'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
|
||||
const alerts = useAlertsStore()
|
||||
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
||||
const secret = ref<SecretView>()
|
||||
const router = useRouter()
|
||||
|
||||
const treeState = useTreeState()
|
||||
|
||||
@ -68,38 +73,75 @@ async function updateSecretValue(value: string) {
|
||||
async function deleteSecret(clients: string[]) {
|
||||
// Delete the whole secret
|
||||
if (props.id) {
|
||||
await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({ path: { name: props.id } })
|
||||
for (const clientId in clients) {
|
||||
await treeState.refreshClient(clientId)
|
||||
const response = await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({
|
||||
path: { name: props.id },
|
||||
})
|
||||
try {
|
||||
assertSdkResponseOk(response)
|
||||
for (const clientId in clients) {
|
||||
await treeState.refreshClient(clientId)
|
||||
}
|
||||
await treeState.getSecretGroups()
|
||||
treeState.bumpGroupRevision()
|
||||
router.go(-1)
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Error deleting secret')
|
||||
}
|
||||
await treeState.getSecretGroups()
|
||||
treeState.bumpGroupRevision()
|
||||
}
|
||||
}
|
||||
|
||||
async function addSecretToClient(clientId: string) {
|
||||
if (props.id) {
|
||||
await SshecretAdmin.addSecretToClientApiV1ClientsIdSecretsSecretNamePut({
|
||||
const response = await SshecretAdmin.addSecretToClientApiV1ClientsIdSecretsSecretNamePut({
|
||||
path: {
|
||||
id: clientId,
|
||||
secret_name: props.id,
|
||||
},
|
||||
})
|
||||
await treeState.refreshClient(clientId)
|
||||
await loadSecret()
|
||||
try {
|
||||
assertSdkResponseOk(response)
|
||||
await treeState.refreshClient(clientId)
|
||||
await loadSecret()
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Failed to add secret to client')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeClientSecret(clientId: string) {
|
||||
if (props.id) {
|
||||
await SshecretAdmin.deleteSecretFromClientApiV1ClientsIdSecretsSecretNameDelete({
|
||||
path: {
|
||||
id: clientId,
|
||||
secret_name: props.id,
|
||||
},
|
||||
})
|
||||
await treeState.refreshClient(clientId)
|
||||
const response =
|
||||
await SshecretAdmin.deleteSecretFromClientApiV1ClientsIdSecretsSecretNameDelete({
|
||||
path: {
|
||||
id: clientId,
|
||||
secret_name: props.id,
|
||||
},
|
||||
})
|
||||
try {
|
||||
assertSdkResponseOk(response)
|
||||
await treeState.refreshClient(clientId)
|
||||
await loadSecret()
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Failed to remove secret from client')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function moveGroup(path) {
|
||||
// Move secret to a group.
|
||||
const data = {
|
||||
secret_name: secret.value.name,
|
||||
group_path: path,
|
||||
}
|
||||
const response = await SshecretAdmin.assignSecretGroupApiV1SecretsSetGroupPost({ body: data })
|
||||
try {
|
||||
assertSdkResponseOk(response)
|
||||
await loadSecret()
|
||||
await treeState.getSecretGroups()
|
||||
treeState.bumpGroupRevision()
|
||||
alerts.showAlert('Secret moved', 'success')
|
||||
} catch (err) {
|
||||
alerts.showAlert(err.message, 'error', 'Failed to move secret to group')
|
||||
}
|
||||
}
|
||||
onMounted(loadSecret)
|
||||
|
||||
Reference in New Issue
Block a user