mirror of
https://github.com/rustfs/console.git
synced 2026-08-31 01:37:52 +08:00
feat: Associate user policies with grouping policies. #30
This commit is contained in:
@@ -83,9 +83,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePolicies } from '@/composables/usePolicies'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { listGroup, updateGroupMembers } = useGroups()
|
||||
const { listPolicies, getPolicyByUserName, setUserOrGroupPolicy } = usePolicies()
|
||||
const message = useMessage()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -215,6 +217,26 @@ const changeMembers = async () => {
|
||||
),
|
||||
])
|
||||
|
||||
// After updating groups, update user policies to include inherited policies from groups
|
||||
const updatedUserPolicies = await getPolicyByUserName(props.user.accessKey)
|
||||
// Get all policy names from the combined policy document
|
||||
const policyNames =
|
||||
updatedUserPolicies?.Statement?.map((statement: { Sid?: string; Origin?: { policyName?: string } }) => {
|
||||
// This is a simplified approach - in reality, we need to extract policy names
|
||||
// For now, we'll assume the backend handles policy resolution correctly
|
||||
return statement.Origin?.policyName || statement.Sid || ''
|
||||
}).filter(Boolean) || []
|
||||
|
||||
// Remove duplicate policy names
|
||||
const uniquePolicyNames = Array.from(new Set(policyNames))
|
||||
|
||||
// Update user policies to include both direct and inherited policies
|
||||
await setUserOrGroupPolicy({
|
||||
policyName: uniquePolicyNames,
|
||||
userOrGroup: props.user.accessKey,
|
||||
isGroup: false,
|
||||
})
|
||||
|
||||
message.success(t('Update Success'))
|
||||
editStatus.value = false
|
||||
groupSelectorOpen.value = false
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
:key="option.value"
|
||||
:value="option.label"
|
||||
@select="() => togglePolicy(option.value)"
|
||||
:class="{
|
||||
'opacity-70 cursor-not-allowed': editForm.groupInheritedPolicies.includes(option.value),
|
||||
}"
|
||||
:disabled="editForm.groupInheritedPolicies.includes(option.value)"
|
||||
>
|
||||
<Icon
|
||||
name="ri:check-line"
|
||||
@@ -112,6 +116,11 @@
|
||||
:class="editForm.policies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<span>{{ option.label }}</span>
|
||||
<span
|
||||
v-if="editForm.groupInheritedPolicies.includes(option.value)"
|
||||
class="ml-2 text-xs text-muted-foreground opacity-70"
|
||||
>({{ t('Inherited from group') }})</span
|
||||
>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
@@ -147,13 +156,15 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useNuxtApp } from '#app'
|
||||
import Modal from '~/components/modal.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { createUser } = useUsers()
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
const { listGroup, updateGroupMembers } = useGroups()
|
||||
const { listGroup, updateGroupMembers, getGroup } = useGroups()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search'): void
|
||||
@@ -167,6 +178,8 @@ const editForm = reactive({
|
||||
secretKey: '',
|
||||
groups: [] as string[],
|
||||
policies: [] as string[],
|
||||
// 存储从分组继承的策略,这些策略不可取消
|
||||
groupInheritedPolicies: [] as string[],
|
||||
})
|
||||
|
||||
const errors = reactive({
|
||||
@@ -200,6 +213,7 @@ const resetForm = () => {
|
||||
editForm.secretKey = ''
|
||||
editForm.groups = []
|
||||
editForm.policies = []
|
||||
editForm.groupInheritedPolicies = []
|
||||
errors.accessKey = ''
|
||||
errors.secretKey = ''
|
||||
groupSelectorOpen.value = false
|
||||
@@ -284,6 +298,43 @@ const submitForm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分组的策略
|
||||
const getGroupPolicies = async (groupName: string) => {
|
||||
try {
|
||||
const groupInfo: { policy?: string } = await getGroup(groupName)
|
||||
return groupInfo.policy ? groupInfo.policy.split(',') : []
|
||||
} catch (error) {
|
||||
console.error('获取分组策略失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分组继承的策略
|
||||
const updateGroupInheritedPolicies = async () => {
|
||||
// 清空当前的分组继承策略
|
||||
editForm.groupInheritedPolicies = []
|
||||
|
||||
// 获取所有选中分组的策略
|
||||
if (editForm.groups.length > 0) {
|
||||
const promises = editForm.groups.map(group => getGroupPolicies(group))
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
// 合并所有分组的策略并去重
|
||||
const inheritedPolicies = Array.from(new Set(results.flat()))
|
||||
editForm.groupInheritedPolicies = inheritedPolicies
|
||||
}
|
||||
|
||||
// 更新策略列表,确保分组继承的策略被选中且不可取消
|
||||
updateSelectedPolicies()
|
||||
}
|
||||
|
||||
// 更新选中的策略,确保分组继承的策略被选中
|
||||
const updateSelectedPolicies = () => {
|
||||
// 合并用户手动选择的策略和分组继承的策略
|
||||
const allPolicies = Array.from(new Set([...editForm.policies, ...editForm.groupInheritedPolicies]))
|
||||
editForm.policies = allPolicies
|
||||
}
|
||||
|
||||
const getPoliciesList = async () => {
|
||||
const res = await listPolicies()
|
||||
policiesList.value = Object.keys(res ?? {})
|
||||
@@ -302,15 +353,25 @@ const getGroupsList = async () => {
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleGroup = (value: string) => {
|
||||
const toggleGroup = async (value: string) => {
|
||||
if (editForm.groups.includes(value)) {
|
||||
// 移除分组
|
||||
editForm.groups = editForm.groups.filter(item => item !== value)
|
||||
} else {
|
||||
// 添加分组
|
||||
editForm.groups = [...editForm.groups, value]
|
||||
}
|
||||
|
||||
// 更新分组继承的策略
|
||||
await updateGroupInheritedPolicies()
|
||||
}
|
||||
|
||||
const togglePolicy = (value: string) => {
|
||||
// 如果是分组继承的策略,不能取消选中
|
||||
if (editForm.groupInheritedPolicies.includes(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (editForm.policies.includes(value)) {
|
||||
editForm.policies = editForm.policies.filter(item => item !== value)
|
||||
} else {
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
:key="option.value"
|
||||
:value="option.label"
|
||||
@select="() => togglePolicy(option.value)"
|
||||
:disabled="inheritedPolicies.includes(option.value) && selectedPolicies.includes(option.value)"
|
||||
class="group"
|
||||
>
|
||||
<Icon
|
||||
name="ri:check-line"
|
||||
@@ -46,6 +48,12 @@
|
||||
:class="selectedPolicies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<span>{{ option.label }}</span>
|
||||
<span
|
||||
v-if="inheritedPolicies.includes(option.value)"
|
||||
class="ml-2 text-xs text-muted-foreground opacity-70 transition-opacity"
|
||||
>
|
||||
{{ t('(Inherited from group)') }}
|
||||
</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
@@ -83,11 +91,39 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGroups } from '@/composables/useGroups'
|
||||
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
const { listPolicies, setUserOrGroupPolicy, getPolicyByUserName } = usePolicies()
|
||||
const { listGroup } = useGroups()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
// 存储用户继承的策略
|
||||
const inheritedPolicies = ref<string[]>([])
|
||||
|
||||
// 获取用户继承的策略
|
||||
const loadInheritedPolicies = async () => {
|
||||
if (!props.user?.accessKey) return
|
||||
try {
|
||||
const userPolicies = await getPolicyByUserName(props.user.accessKey)
|
||||
if (userPolicies?.Statement) {
|
||||
// 提取来自组的继承策略
|
||||
const inheritedPolicyNames = new Set<string>()
|
||||
userPolicies.Statement.forEach((statement: any) => {
|
||||
if (statement.Origin?.type === 'group' || statement.Origin?.type === 'both') {
|
||||
// 如果策略来自组或同时来自组和直接分配,将其添加到继承策略列表
|
||||
if (statement.Origin?.policyName) {
|
||||
inheritedPolicyNames.add(statement.Origin.policyName)
|
||||
}
|
||||
}
|
||||
})
|
||||
inheritedPolicies.value = Array.from(inheritedPolicyNames)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load inherited policies:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
user: {
|
||||
accessKey: string
|
||||
@@ -153,9 +189,10 @@ const loadPolicies = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPolicies()
|
||||
onMounted(async () => {
|
||||
await loadPolicies()
|
||||
selectedPolicies.value = [...currentPolicies.value]
|
||||
await loadInheritedPolicies()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -168,6 +205,12 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(editStatus, async val => {
|
||||
if (val) {
|
||||
await loadInheritedPolicies()
|
||||
}
|
||||
})
|
||||
|
||||
const startEditing = () => {
|
||||
selectedPolicies.value = [...currentPolicies.value]
|
||||
policySelectorOpen.value = false
|
||||
@@ -182,9 +225,17 @@ const cancelEditing = () => {
|
||||
|
||||
const togglePolicy = (value: string) => {
|
||||
if (selectedPolicies.value.includes(value)) {
|
||||
// 如果是继承的策略,禁止删除
|
||||
if (inheritedPolicies.value.includes(value)) {
|
||||
// 不再显示弹出消息,而是通过UI禁用删除功能
|
||||
return
|
||||
}
|
||||
selectedPolicies.value = selectedPolicies.value.filter(item => item !== value)
|
||||
} else {
|
||||
selectedPolicies.value = [...selectedPolicies.value, value]
|
||||
// 确保添加的策略不重复
|
||||
if (!selectedPolicies.value.includes(value)) {
|
||||
selectedPolicies.value = [...selectedPolicies.value, value]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+59
-20
@@ -60,6 +60,22 @@ export const usePolicies = () => {
|
||||
* @returns Result
|
||||
*/
|
||||
const setPolicyMultiple = async (data: any) => {
|
||||
// Add basic deduplication for policy names if applicable
|
||||
// This handles common batch policy assignment patterns
|
||||
if (data.policyName && Array.isArray(data.policyName)) {
|
||||
data.policyName = Array.from(new Set(data.policyName))
|
||||
}
|
||||
// Handle potential different structure where policies might be in a 'policies' array
|
||||
if (data.policies && Array.isArray(data.policies)) {
|
||||
// If policies array contains objects with policyName, deduplicate based on policyName
|
||||
const policyNameMap = new Map()
|
||||
data.policies.forEach((policy: any) => {
|
||||
if (policy.policyName && !policyNameMap.has(policy.policyName)) {
|
||||
policyNameMap.set(policy.policyName, policy)
|
||||
}
|
||||
})
|
||||
data.policies = Array.from(policyNameMap.values())
|
||||
}
|
||||
return await $api.put(`/set-policy-multi`, data)
|
||||
}
|
||||
|
||||
@@ -69,6 +85,10 @@ export const usePolicies = () => {
|
||||
* @returns Result
|
||||
*/
|
||||
const setUserOrGroupPolicy = async (data: any) => {
|
||||
// Ensure policy names are unique before sending to API
|
||||
if (data.policyName && Array.isArray(data.policyName)) {
|
||||
data.policyName = Array.from(new Set(data.policyName))
|
||||
}
|
||||
return await $api.put(`/set-user-or-group-policy`, {}, { params: data })
|
||||
}
|
||||
|
||||
@@ -80,37 +100,56 @@ export const usePolicies = () => {
|
||||
const getPolicyByUserName = async (userName: string) => {
|
||||
// Get user policy groups
|
||||
const userInfo = await $api.get(`/user-info?accessKey=${userName}`)
|
||||
const policyName = userInfo?.policyName?.split(',') || []
|
||||
const directPolicyNames = userInfo?.policyName?.split(',') || []
|
||||
|
||||
// Get user's group memberships
|
||||
const memberOf = userInfo?.memberOf
|
||||
const memberOf = userInfo?.memberOf || []
|
||||
// Get group policies
|
||||
if (memberOf && memberOf.length > 0) {
|
||||
const promises = memberOf.map(async (element: string) => {
|
||||
const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(element)}`)
|
||||
const groupPolicyName: string[] = groupInfo.policy ? groupInfo.policy.split(',') : []
|
||||
return groupPolicyName
|
||||
})
|
||||
const results = await Promise.all(promises)
|
||||
results.forEach(policyNames => {
|
||||
policyName.push(...policyNames)
|
||||
const groupPoliciesMap: Record<string, string[]> = {}
|
||||
if (memberOf.length > 0) {
|
||||
const promises = memberOf.map(async (groupName: string) => {
|
||||
const groupInfo: { policy?: string } = await $api.get(`/group?group=${encodeURIComponent(groupName)}`)
|
||||
const groupPolicyNames: string[] = groupInfo.policy ? groupInfo.policy.split(',') : []
|
||||
if (groupPolicyNames.length > 0) {
|
||||
groupPoliciesMap[groupName] = groupPolicyNames
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
let uniquePolicyName: string[] = []
|
||||
if (policyName.length) {
|
||||
uniquePolicyName = Array.from(new Set(policyName))
|
||||
}
|
||||
// Collect all unique policy names
|
||||
const allPolicyNames = new Set<string>()
|
||||
directPolicyNames.forEach((policyName: string) => allPolicyNames.add(policyName))
|
||||
Object.values(groupPoliciesMap).forEach((policyNames: string[]) => {
|
||||
policyNames.forEach((policyName: string) => allPolicyNames.add(policyName))
|
||||
})
|
||||
|
||||
let policyStatement: any = []
|
||||
// Get all remaining policy documents
|
||||
if (uniquePolicyName.length) {
|
||||
const policyPromises = uniquePolicyName.map(async (element: any) => {
|
||||
const policyInfo = await getPolicy(element)
|
||||
// Get all policy documents
|
||||
if (allPolicyNames.size > 0) {
|
||||
const policyPromises = Array.from(allPolicyNames).map(async (policyName: string) => {
|
||||
const policyInfo = await getPolicy(policyName)
|
||||
// Format policy
|
||||
let policyRes = JSON.parse(policyInfo.policy)
|
||||
if (policyRes?.Statement) {
|
||||
// Add origin information to each statement
|
||||
policyRes.Statement = policyRes.Statement.map((statement: any) => {
|
||||
// Check if this policy is direct or inherited from groups
|
||||
const isDirect = directPolicyNames.includes(policyName)
|
||||
const groupOrigins = Object.entries(groupPoliciesMap)
|
||||
.filter(([_, policies]) => policies.includes(policyName))
|
||||
.map(([groupName]) => groupName)
|
||||
|
||||
return {
|
||||
...statement,
|
||||
Sid: statement.Sid || policyName, // Use policy name as Sid if not provided
|
||||
Origin: {
|
||||
type: isDirect && groupOrigins.length > 0 ? 'both' : isDirect ? 'direct' : 'group',
|
||||
groups: groupOrigins,
|
||||
policyName: policyName,
|
||||
},
|
||||
}
|
||||
})
|
||||
policyStatement.push(...policyRes.Statement)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Tier hinzufügen",
|
||||
"Add User": "Benutzer hinzufügen",
|
||||
"Add User Group": "Benutzergruppe hinzufügen",
|
||||
"Inherited from group": "Vererbt von Gruppe",
|
||||
"Add failed": "Hinzufügen fehlgeschlagen",
|
||||
"Add group members": "Gruppenmitglieder hinzufügen",
|
||||
"Add replication rules to sync objects across buckets.": "Replikationsregeln hinzufügen, um Objekte zwischen Buckets zu synchronisieren.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Add Tier",
|
||||
"Add User": "Add User",
|
||||
"Add User Group": "Add User Group",
|
||||
"Inherited from group": "Inherited from group",
|
||||
"Add failed": "Add failed",
|
||||
"Add group members": "Add group members",
|
||||
"Add replication rules to sync objects across buckets.": "Add replication rules to sync objects across buckets.",
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"Add Tag": "Añadir Etiqueta",
|
||||
"Add Tier": "Añadir Nivel",
|
||||
"Add User": "Añadir Usuario",
|
||||
"Add User Group": "Añadir Grupo de Usuarios",
|
||||
"Add User Group": "Añadir Grupo de Usuario",
|
||||
"Inherited from group": "Herencia de Grupo",
|
||||
"Add failed": "Error al añadir",
|
||||
"Add group members": "Añadir miembros del grupo",
|
||||
"Add replication rules to sync objects across buckets.": "Añade reglas de replicación para sincronizar objetos entre buckets.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Ajouter un niveau",
|
||||
"Add User": "Ajouter un utilisateur",
|
||||
"Add User Group": "Ajouter un groupe d'utilisateurs",
|
||||
"Inherited from group": "Hérité de groupe",
|
||||
"Add failed": "Échec de l'ajout",
|
||||
"Add group members": "Ajouter des membres au groupe",
|
||||
"Add replication rules to sync objects across buckets.": "Ajoutez des règles de réplication pour synchroniser les objets entre les compartiments.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Aggiungi tier",
|
||||
"Add User": "Aggiungi utente",
|
||||
"Add User Group": "Aggiungi gruppo utenti",
|
||||
"Inherited from group": "Ereditato da gruppo",
|
||||
"Add failed": "Aggiunta non riuscita",
|
||||
"Add group members": "Aggiungi membri del gruppo",
|
||||
"Add replication rules to sync objects across buckets.": "Aggiungi regole di replica per sincronizzare gli oggetti tra i bucket.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "ティアを追加",
|
||||
"Add User": "ユーザーを追加",
|
||||
"Add User Group": "ユーザーグループを追加",
|
||||
"Inherited from group": "グループから継承",
|
||||
"Add failed": "追加に失敗しました",
|
||||
"Add group members": "グループメンバーを追加",
|
||||
"Add replication rules to sync objects across buckets.": "バケット間でオブジェクトを同期するレプリケーションルールを追加します。",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "티어 추가",
|
||||
"Add User": "사용자 추가",
|
||||
"Add User Group": "사용자 그룹 추가",
|
||||
"Inherited from group": "그룹 상속",
|
||||
"Add failed": "추가 실패",
|
||||
"Add group members": "그룹 멤버 추가",
|
||||
"Add replication rules to sync objects across buckets.": "버킷 간 객체를 동기화하는 복제 규칙을 추가합니다.",
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"Add Tag": "Adicionar Tag",
|
||||
"Add Tier": "Adicionar Tier",
|
||||
"Add User": "Adicionar Usuário",
|
||||
"Add User Group": "Adicionar Grupo de Usuários",
|
||||
"Add User Group": "Adicionar Grupo de Usuário",
|
||||
"Inherited from group": "Herança de Grupo",
|
||||
"Add failed": "Falha ao adicionar",
|
||||
"Add group members": "Adicionar membros do grupo",
|
||||
"Add replication rules to sync objects across buckets.": "Adicione regras de replicação para sincronizar objetos entre buckets.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Добавить уровень",
|
||||
"Add User": "Добавить пользователя",
|
||||
"Add User Group": "Добавить группу пользователей",
|
||||
"Inherited from group": "Наследовано от группы",
|
||||
"Add failed": "Ошибка добавления",
|
||||
"Add group members": "Добавить участников группы",
|
||||
"Add replication rules to sync objects across buckets.": "Добавьте правила репликации для синхронизации объектов между бакетами.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "Katman Ekle",
|
||||
"Add User": "Kullanıcı Ekle",
|
||||
"Add User Group": "Kullanıcı Grubu Ekle",
|
||||
"Inherited from group": "Kalıtım Grubu",
|
||||
"Add failed": "Ekleme başarısız",
|
||||
"Add group members": "Grup üyeleri ekle",
|
||||
"Add replication rules to sync objects across buckets.": "Add replication rules to sync objects across buckets.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Add Tier": "添加存储层",
|
||||
"Add User": "新增用户",
|
||||
"Add User Group": "新增用户组",
|
||||
"Inherited from group": "继承自分组",
|
||||
"Add failed": "添加失败",
|
||||
"Add group members": "添加组成员",
|
||||
"Add replication rules to sync objects across buckets.": "添加复制规则以同步存储桶之间的对象。",
|
||||
|
||||
Generated
+29
-9
@@ -38,7 +38,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-vue-next": "^0.487.0",
|
||||
"motion-v": "^1.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-forge": "^1.3.2",
|
||||
"nuxt": "^4.2.0",
|
||||
"pinia": "^2.3.1",
|
||||
"pnpm": "^10.19.0",
|
||||
@@ -67,6 +67,7 @@
|
||||
"@types/aws4": "^1.11.6",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node-forge": "^1.3.12",
|
||||
"@vitalets/google-translate-api": "^9.2.1",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
@@ -80,6 +81,7 @@
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar": "^7.5.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4",
|
||||
@@ -7372,6 +7374,12 @@
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-1.8.2.tgz",
|
||||
"integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
@@ -7837,6 +7845,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitalets/google-translate-api": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vitalets/google-translate-api/-/google-translate-api-9.2.1.tgz",
|
||||
"integrity": "sha512-zlwQWSjXUZhbZQ6qwtIQ7GdYXFQmJ4wYqzcrYJUxtvzQQwUP+uKUb/SRJaBOQuBntjBjzcdcJoLFrpCKUbIkOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-errors": "^1.8.2",
|
||||
"http-errors": "^2.0.0",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
@@ -15077,10 +15099,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.3.tgz",
|
||||
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
@@ -18819,10 +18840,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.1.tgz",
|
||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
||||
"license": "ISC",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.2.tgz",
|
||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
|
||||
Reference in New Issue
Block a user