feat: implement group member role management (Admin, Editor, Viewer)

This commit is contained in:
2026-05-03 21:20:37 +05:00
parent f0b5883084
commit ace515f18e
3 changed files with 110 additions and 7 deletions
+65 -2
View File
@@ -16,7 +16,7 @@
</div>
<!-- Members Section -->
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden">
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden mb-8">
<div class="p-6 border-b border-slate-800 flex items-center justify-between">
<h2 class="text-xl font-bold text-white">Участники ({{ members.length }})</h2>
<button @click="showAddMemberModal = true" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-bold transition">+ Добавить</button>
@@ -29,13 +29,49 @@
</div>
<div>
<div class="text-slate-200 font-medium">{{ m.user.username }}</div>
<div class="text-xs text-slate-500">{{ m.role }}</div>
<div class="text-xs text-slate-500">
<select
v-if="group.ownerId === auth.user.value?.id || auth.user.value?.role === 'admin'"
:value="m.role"
@change="(e) => updateMemberRole(m.userId, e.target.value)"
class="bg-transparent border-none p-0 focus:ring-0 text-blue-400 cursor-pointer outline-none"
:disabled="m.userId === group.ownerId"
>
<option value="admin">Админ</option>
<option value="editor">Редактор</option>
<option value="viewer">Зритель</option>
</select>
<span v-else>{{ m.role }}</span>
</div>
</div>
</div>
<button v-if="m.role !== 'admin'" @click="removeMember(m.userId)" class="text-slate-600 hover:text-red-400 transition text-xl">&times;</button>
</div>
</div>
</div>
<!-- Canvases Section -->
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden">
<div class="p-6 border-b border-slate-800 flex items-center justify-between">
<h2 class="text-xl font-bold text-white">Карты группы ({{ group.canvases?.length || 0 }})</h2>
<NuxtLink to="/canvases" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm font-bold transition">Управлять в Проектах</NuxtLink>
</div>
<div class="divide-y divide-slate-800">
<div v-for="c in group.canvases" :key="c.id" class="p-4 flex items-center justify-between hover:bg-slate-800/30 transition">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-900/20 rounded-lg flex items-center justify-center text-xl">🗺️</div>
<div>
<div class="text-slate-200 font-medium">{{ c.name }}</div>
<div class="text-[10px] text-slate-500 uppercase tracking-wider">Обновлена: {{ new Date(c.updatedAt).toLocaleDateString() }}</div>
</div>
</div>
<button @click="unassignCanvas(c.id)" class="text-slate-600 hover:text-red-400 transition text-sm font-bold">Отвязать</button>
</div>
<div v-if="!group.canvases?.length" class="p-8 text-center text-slate-500 italic">
В этой группе пока нет карт.
</div>
</div>
</div>
<!-- Add Member Modal -->
<div v-if="showAddMemberModal" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4" @click.self="showAddMemberModal = false">
<div class="bg-slate-900 border border-slate-700 rounded-3xl p-8 w-full max-w-md shadow-2xl">
@@ -139,6 +175,19 @@ const addMember = async (userId) => {
}
}
const updateMemberRole = async (userId, role) => {
try {
await $fetch(`/api/groups/${route.params.id}/members`, {
method: 'PATCH',
body: { userId, role },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
await fetchGroup()
} catch (e) {
alert('Ошибка при смене роли')
}
}
const removeMember = async (userId) => {
if (!confirm('Удалить участника из группы?')) return
try {
@@ -153,6 +202,20 @@ const removeMember = async (userId) => {
}
}
const unassignCanvas = async (canvasId) => {
if (!confirm('Отвязать карту от группы? Она станет личной картой владельца.')) return
try {
await $fetch(`/api/canvases/${canvasId}`, {
method: 'PATCH',
body: { groupId: null },
headers: { Authorization: `Bearer ${auth.token.value}` }
})
await fetchGroup()
} catch (e) {
alert('Ошибка при отвязке карты')
}
}
const fetchGroup = async () => {
try {
const data = await $fetch(`/api/groups/${route.params.id}`, {
+4 -5
View File
@@ -11,13 +11,12 @@ export default defineEventHandler(async (event) => {
members: {
include: {
user: {
select: {
id: true,
username: true,
email: true
}
select: { id: true, username: true, email: true }
}
}
},
canvases: {
select: { id: true, name: true, updatedAt: true }
}
}
})
@@ -0,0 +1,41 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const adminUser = requireAuth(event)
const groupId = getRouterParam(event, 'id')
const body = await readBody(event)
const { userId, role } = body
if (!groupId || !userId || !role) {
throw createError({ statusCode: 400, message: 'Missing required fields' })
}
// 1. Проверяем права: только владелец группы или глобальный админ может менять роли
const group = await prisma.group.findUnique({
where: { id: groupId }
})
if (!group || (group.ownerId !== adminUser.id && adminUser.role !== 'admin')) {
throw createError({ statusCode: 403, message: 'Permission denied' })
}
// 2. Нельзя менять роль самому владельцу (он всегда админ)
if (userId === group.ownerId) {
throw createError({ statusCode: 400, message: 'Cannot change owner role' })
}
// 3. Обновляем роль
try {
const updated = await prisma.groupMember.update({
where: {
userId_groupId: { userId, groupId }
},
data: { role }
})
return { success: true, updated }
} catch (e) {
throw createError({ statusCode: 500, message: 'Failed to update member role' })
}
})