refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types
This commit is contained in:
1 parent
5d6f378fce
commit
b39a41be88
124 files changed
+8816
-523
No files matched your search
@@ -19,16 +19,24 @@ const emit = defineEmits<{
|
||||
deleted: [role: RoleItem]
|
||||
gone: [role: RoleItem]
|
||||
duplicate: [role: RoleItem]
|
||||
conflict: [role: RoleItem]
|
||||
refreshed: [roles: RoleItem[]]
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
|
||||
const { reassignRole, deleteRole } = useRoles()
|
||||
const { reassignRole, deleteRole, listRoles } = useRoles()
|
||||
|
||||
const target = ref<number | undefined>(undefined)
|
||||
const pending = ref(false)
|
||||
const notice = ref<DirectoryNotice | null>(null)
|
||||
const members = ref(props.role.members)
|
||||
const missingTargets = ref<number[]>([])
|
||||
const gainedMembers = ref(false)
|
||||
|
||||
const targets = computed(() =>
|
||||
props.roles.filter((item) => !missingTargets.value.includes(item.id)),
|
||||
)
|
||||
|
||||
const stage = computed<'builtin' | 'blocked' | 'confirm'>(() => {
|
||||
if (props.role.built_in) {
|
||||
@@ -37,9 +45,14 @@ const stage = computed<'builtin' | 'blocked' | 'confirm'>(() => {
|
||||
return members.value > 0 ? 'blocked' : 'confirm'
|
||||
})
|
||||
|
||||
const deleteTop = computed(() => (notice.value ? 25 : 35))
|
||||
|
||||
const title = computed(() => t('Delete {0}?', [props.role.name]))
|
||||
|
||||
const description = computed(() => {
|
||||
if (gainedMembers.value) {
|
||||
return ''
|
||||
}
|
||||
switch (stage.value) {
|
||||
case 'builtin':
|
||||
return t("Built-in roles can't be deleted.")
|
||||
@@ -58,7 +71,9 @@ const confirmLabel = computed(() => {
|
||||
return t('Duplicate role')
|
||||
}
|
||||
return stage.value === 'blocked'
|
||||
? t('Move {0} members and delete role', [members.value])
|
||||
? members.value === 1
|
||||
? t('Move 1 member and delete role')
|
||||
: t('Move {0} members and delete role', [members.value])
|
||||
: t('Delete role')
|
||||
})
|
||||
|
||||
@@ -66,14 +81,65 @@ const pendingLabel = computed(() =>
|
||||
stage.value === 'blocked' ? t('Moving and deleting…') : t('Deleting…'),
|
||||
)
|
||||
|
||||
function showRefusal(error: unknown) {
|
||||
if (!(error instanceof ApiError)) {
|
||||
notice.value = { description: t("Couldn't delete the role. Try again in a moment.") }
|
||||
const GENERIC_FAILURE = "Couldn't delete the role. Try again in a moment."
|
||||
|
||||
async function liveMemberCount() {
|
||||
try {
|
||||
const roles = await listRoles()
|
||||
const live = roles.find((item) => item.id === props.role.id)
|
||||
return Math.max(live?.members ?? 0, 1)
|
||||
} catch {
|
||||
return Math.max(members.value, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function showGainedMembers() {
|
||||
const count = await liveMemberCount()
|
||||
members.value = count
|
||||
gainedMembers.value = true
|
||||
target.value = undefined
|
||||
notice.value = {
|
||||
tone: 'warning',
|
||||
title: t('Role still has members'),
|
||||
description:
|
||||
count === 1
|
||||
? t('{0} now has 1 member. Move them to another role first.', [props.role.name])
|
||||
: t('{0} now has {1} members. Move them to another role first.', [
|
||||
props.role.name,
|
||||
count,
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
async function showTargetGone() {
|
||||
const missing = props.roles.find((item) => item.id === target.value)
|
||||
if (missing) {
|
||||
missingTargets.value = [...missingTargets.value, missing.id]
|
||||
}
|
||||
notice.value = {
|
||||
title: t('{0} no longer exists', [missing?.name ?? '']),
|
||||
description: t(
|
||||
'It was deleted somewhere else. Choose another role to move the members to.',
|
||||
),
|
||||
}
|
||||
target.value = undefined
|
||||
try {
|
||||
emit('refreshed', await listRoles())
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_has_members') {
|
||||
members.value = Math.max(members.value, 1)
|
||||
target.value = undefined
|
||||
}
|
||||
|
||||
function showMoveFailure() {
|
||||
notice.value = {
|
||||
title: t("Couldn't move the members"),
|
||||
description: t('Nothing was changed. Try again in a moment.'),
|
||||
}
|
||||
}
|
||||
|
||||
function showRefusal(error: unknown) {
|
||||
if (!(error instanceof ApiError)) {
|
||||
notice.value = { description: t(GENERIC_FAILURE) }
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_not_found') {
|
||||
@@ -81,13 +147,43 @@ function showRefusal(error: unknown) {
|
||||
open.value = false
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_conflict') {
|
||||
emit('conflict', props.role)
|
||||
open.value = false
|
||||
return
|
||||
}
|
||||
const copy = error.code === undefined ? undefined : refusalCopy(error.code, error.permission)
|
||||
notice.value =
|
||||
copy === undefined
|
||||
? { description: t("Couldn't delete the role. Try again in a moment.") }
|
||||
? { description: t(GENERIC_FAILURE) }
|
||||
: { title: copy.title, description: copy.description ?? '' }
|
||||
}
|
||||
|
||||
async function showMoveRefusal(error: unknown) {
|
||||
if (!(error instanceof ApiError)) {
|
||||
showMoveFailure()
|
||||
return
|
||||
}
|
||||
if (error.code === 'target_role_not_found') {
|
||||
await showTargetGone()
|
||||
return
|
||||
}
|
||||
if (error.code === 'invalid_reassign') {
|
||||
target.value = undefined
|
||||
showRefusal(error)
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_conflict' || error.code === 'role_not_found') {
|
||||
showRefusal(error)
|
||||
return
|
||||
}
|
||||
if (error.code === undefined || refusalCopy(error.code, error.permission) === undefined) {
|
||||
showMoveFailure()
|
||||
return
|
||||
}
|
||||
showRefusal(error)
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
if (stage.value === 'builtin') {
|
||||
emit('duplicate', props.role)
|
||||
@@ -101,13 +197,22 @@ async function onConfirm() {
|
||||
notice.value = null
|
||||
try {
|
||||
if (stage.value === 'blocked' && target.value !== undefined) {
|
||||
await reassignRole(props.role.id, target.value)
|
||||
try {
|
||||
await reassignRole(props.role.id, target.value)
|
||||
} catch (error) {
|
||||
await showMoveRefusal(error)
|
||||
return
|
||||
}
|
||||
members.value = 0
|
||||
}
|
||||
await deleteRole(props.role.id)
|
||||
emit('deleted', props.role)
|
||||
open.value = false
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.code === 'role_has_members') {
|
||||
await showGainedMembers()
|
||||
return
|
||||
}
|
||||
showRefusal(error)
|
||||
} finally {
|
||||
pending.value = false
|
||||
@@ -117,6 +222,7 @@ async function onConfirm() {
|
||||
|
||||
<template>
|
||||
<DirectoryDialog
|
||||
closable
|
||||
v-model:open="open"
|
||||
:title="title"
|
||||
:description="description"
|
||||
@@ -126,17 +232,19 @@ async function onConfirm() {
|
||||
:tone="stage === 'builtin' ? 'neutral' : 'danger'"
|
||||
:pending="pending"
|
||||
:notice="notice"
|
||||
:top="deleteTop"
|
||||
:confirm-disabled="stage === 'blocked' && target === undefined"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<template v-if="stage === 'blocked'">
|
||||
<UFormField
|
||||
:label="t('Move members to')"
|
||||
required
|
||||
:help="t('They keep their API keys. Keys follow the new role.')"
|
||||
>
|
||||
<RolePicker
|
||||
v-model:selected="target"
|
||||
:roles="roles"
|
||||
:roles="targets"
|
||||
:source-id="role.id"
|
||||
:can-assign="canAssign"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user