feat: implement group member role management (Admin, Editor, Viewer)
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members Section -->
|
<!-- 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">
|
<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>
|
<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>
|
<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>
|
<div>
|
||||||
<div class="text-slate-200 font-medium">{{ m.user.username }}</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>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="m.role !== 'admin'" @click="removeMember(m.userId)" class="text-slate-600 hover:text-red-400 transition text-xl">×</button>
|
<button v-if="m.role !== 'admin'" @click="removeMember(m.userId)" class="text-slate-600 hover:text-red-400 transition text-xl">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 -->
|
<!-- 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 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">
|
<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) => {
|
const removeMember = async (userId) => {
|
||||||
if (!confirm('Удалить участника из группы?')) return
|
if (!confirm('Удалить участника из группы?')) return
|
||||||
try {
|
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 () => {
|
const fetchGroup = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await $fetch(`/api/groups/${route.params.id}`, {
|
const data = await $fetch(`/api/groups/${route.params.id}`, {
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ export default defineEventHandler(async (event) => {
|
|||||||
members: {
|
members: {
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: { id: true, username: true, email: true }
|
||||||
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' })
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user