feat: stabilize admin dashboard, refactor group management, implement PWA, and fix routing
This commit is contained in:
@@ -1,28 +1,47 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { users, canvases, pushSubscriptions } from '~/server/utils/db'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
console.log(`[Admin API] Request from user: ${user.email}, Role: ${user.role}`)
|
||||
|
||||
if (user.role !== 'admin') {
|
||||
throw createError({ statusCode: 403, message: 'Admin access required' })
|
||||
}
|
||||
|
||||
const usersList = Array.from(users.values())
|
||||
const canvasesList = Array.from(canvases.values()).map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
nodeCount: c.nodes.length,
|
||||
updatedAt: c.updatedAt
|
||||
}))
|
||||
// 1. Получаем список всех пользователей с их профилями
|
||||
const usersList = await prisma.user.findMany({
|
||||
include: { profile: true }
|
||||
})
|
||||
|
||||
// 2. Получаем список всех канвасов
|
||||
const canvasesList = await prisma.canvas.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nodes: true,
|
||||
updatedAt: true,
|
||||
ownerId: true
|
||||
}
|
||||
})
|
||||
|
||||
// 3. Считаем общее кол-во пушей (если есть таблица)
|
||||
let pushCount = 0
|
||||
try {
|
||||
pushCount = await prisma.pushSubscription.count()
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
users: usersList,
|
||||
canvases: canvasesList,
|
||||
canvases: canvasesList.map(c => ({
|
||||
...c,
|
||||
nodeCount: (c.nodes as any[])?.length || 0
|
||||
})),
|
||||
stats: {
|
||||
users: usersList.length,
|
||||
canvases: canvasesList.length,
|
||||
nodes: canvasesList.reduce((sum, c) => sum + c.nodeCount, 0),
|
||||
pushes: pushSubscriptions.size
|
||||
nodes: canvasesList.reduce((sum, c) => sum + ((c.nodes as any[])?.length || 0), 0),
|
||||
pushes: pushCount
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,12 +34,15 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const fbUser: FirebaseUser = verifyRes.users[0]
|
||||
const email = fbUser.email
|
||||
const username = fbUser.displayName || email.split('@')[0]
|
||||
const username = email.split('@')[0]
|
||||
const userId = fbUser.localId
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: { username },
|
||||
update: {
|
||||
username
|
||||
// Мы не затираем профиль при каждом входе, но нам нужно его получить
|
||||
},
|
||||
create: {
|
||||
id: userId,
|
||||
email,
|
||||
@@ -48,10 +51,13 @@ export default defineEventHandler(async (event) => {
|
||||
role: 'user',
|
||||
profile: {
|
||||
create: {
|
||||
displayName: username,
|
||||
displayName: fbUser.displayName || username,
|
||||
avatar: fbUser.photoUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}`
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -59,6 +65,7 @@ export default defineEventHandler(async (event) => {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
displayName: user.profile?.displayName || user.username,
|
||||
role: user.role
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 { email, role = 'member' } = body
|
||||
|
||||
if (!groupId || !email) {
|
||||
throw createError({ statusCode: 400, message: 'Group ID and Email are required' })
|
||||
}
|
||||
|
||||
// 1. Проверяем, является ли текущий пользователь админом группы или глобальным админом
|
||||
const group = await prisma.group.findUnique({
|
||||
where: { id: groupId }
|
||||
})
|
||||
|
||||
if (!group || (group.ownerId !== adminUser.id && adminUser.role !== 'admin')) {
|
||||
throw createError({ statusCode: 403, message: 'Only group owners or admins can add members' })
|
||||
}
|
||||
|
||||
// 2. Ищем пользователя по email
|
||||
const userToAdd = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
|
||||
if (!userToAdd) {
|
||||
throw createError({ statusCode: 404, message: 'User with this email not found in system' })
|
||||
}
|
||||
|
||||
// 3. Добавляем в группу
|
||||
try {
|
||||
const membership = await prisma.groupMember.upsert({
|
||||
where: {
|
||||
userId_groupId: {
|
||||
userId: userToAdd.id,
|
||||
groupId: groupId
|
||||
}
|
||||
},
|
||||
update: { role },
|
||||
create: {
|
||||
userId: userToAdd.id,
|
||||
groupId: groupId,
|
||||
role
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, membership }
|
||||
} catch (e) {
|
||||
throw createError({ statusCode: 500, message: 'Failed to add member to group' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const id = getRouterParam(event, 'id')
|
||||
|
||||
const group = await prisma.group.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!group) {
|
||||
throw createError({ statusCode: 404, message: 'Group not found' })
|
||||
}
|
||||
|
||||
// Проверяем, имеет ли пользователь право видеть детали группы
|
||||
const isMember = group.members.some(m => m.userId === user.id)
|
||||
if (!group.isPublic && !isMember && user.role !== 'admin') {
|
||||
throw createError({ statusCode: 403, message: 'Access denied' })
|
||||
}
|
||||
|
||||
return { group }
|
||||
})
|
||||
@@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
|
||||
const allGroups = await prisma.group.findMany({
|
||||
where: { isPublic: true },
|
||||
where: user.role === 'admin' ? {} : { isPublic: true },
|
||||
include: {
|
||||
members: { include: { user: { select: { id: true, username: true } } } },
|
||||
_count: { select: { members: true, canvases: true } }
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const body = await readBody(event)
|
||||
const { canvasId, title, priority, state } = body
|
||||
|
||||
if (!canvasId || !title) {
|
||||
throw createError({ statusCode: 400, message: 'Canvas ID and Title are required' })
|
||||
}
|
||||
|
||||
// 1. Получаем канвас
|
||||
const canvas = await prisma.canvas.findUnique({
|
||||
where: { id: canvasId }
|
||||
})
|
||||
|
||||
if (!canvas) {
|
||||
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||
}
|
||||
|
||||
// 2. Проверяем права
|
||||
const isOwner = canvas.ownerId === user.id
|
||||
let isMember = false
|
||||
if (canvas.groupId) {
|
||||
const membership = await prisma.groupMember.findFirst({
|
||||
where: { userId: user.id, groupId: canvas.groupId }
|
||||
})
|
||||
isMember = !!membership
|
||||
}
|
||||
|
||||
if (!isOwner && !isMember && user.role !== 'admin') {
|
||||
throw createError({ statusCode: 403, message: 'No permission to add tasks to this canvas' })
|
||||
}
|
||||
|
||||
// 3. Создаем новый узел задачи
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
|
||||
// Генерируем позицию (просто в центре или скраю)
|
||||
const lastNode = nodes[nodes.length - 1]
|
||||
const x = lastNode?.position?.x || 100
|
||||
const y = (lastNode?.position?.y || 100) + 100
|
||||
|
||||
const newTaskNode = {
|
||||
id: `task-${Date.now()}`,
|
||||
type: 'task',
|
||||
position: { x, y },
|
||||
data: {
|
||||
text: `# ${title}`,
|
||||
priority: priority || 'medium',
|
||||
state: state || 'todo',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
// Дублируем поля для удобства фильтрации на бэкенде
|
||||
priority: priority || 'medium',
|
||||
state: state || 'todo',
|
||||
label: title
|
||||
}
|
||||
|
||||
nodes.push(newTaskNode)
|
||||
|
||||
// 4. Сохраняем в БД
|
||||
await prisma.canvas.update({
|
||||
where: { id: canvasId },
|
||||
data: { nodes }
|
||||
})
|
||||
|
||||
return { success: true, task: newTaskNode }
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
|
||||
// 1. Получаем группы пользователя
|
||||
const userGroups = await prisma.groupMember.findMany({
|
||||
where: { userId: user.id },
|
||||
select: { groupId: true }
|
||||
})
|
||||
const groupIds = userGroups.map(g => g.groupId)
|
||||
|
||||
// 2. Получаем все доступные канвасы
|
||||
const canvases = await prisma.canvas.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId: user.id },
|
||||
{ groupId: { in: groupIds } },
|
||||
{ isPublic: true }
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nodes: true
|
||||
}
|
||||
})
|
||||
|
||||
// 3. Собираем задачи из всех канвасов
|
||||
const allTasks: any[] = []
|
||||
|
||||
for (const canvas of canvases) {
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const taskNodes = nodes.filter(node => node.type === 'task')
|
||||
|
||||
taskNodes.forEach(task => {
|
||||
allTasks.push({
|
||||
...task,
|
||||
canvasId: canvas.id,
|
||||
canvasName: canvas.name
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Сортируем: сначала высокий приоритет, потом дата
|
||||
allTasks.sort((a, b) => {
|
||||
const pPriority = { high: 3, medium: 2, low: 1 }
|
||||
const prioA = pPriority[a.priority || a.data?.priority] || 0
|
||||
const prioB = pPriority[b.priority || b.data?.priority] || 0
|
||||
return prioB - prioA
|
||||
})
|
||||
|
||||
return { tasks: allTasks }
|
||||
})
|
||||
@@ -7,10 +7,10 @@ export default defineEventHandler(async (event) => {
|
||||
const { taskId, canvasId, newState } = body
|
||||
|
||||
if (!taskId || !canvasId || !newState) {
|
||||
throw createError({ statusCode: 400, message: 'taskId, canvasId and newState are required' })
|
||||
throw createError({ statusCode: 400, message: 'Missing parameters' })
|
||||
}
|
||||
|
||||
// 1. Найти холст
|
||||
// 1. Получаем канвас
|
||||
const canvas = await prisma.canvas.findUnique({
|
||||
where: { id: canvasId }
|
||||
})
|
||||
@@ -19,7 +19,21 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||
}
|
||||
|
||||
// 2. Найти и обновить узел в JSON
|
||||
// 2. Проверяем права на редактирование
|
||||
const isOwner = canvas.ownerId === user.id
|
||||
let isMember = false
|
||||
if (canvas.groupId) {
|
||||
const membership = await prisma.groupMember.findFirst({
|
||||
where: { userId: user.id, groupId: canvas.groupId }
|
||||
})
|
||||
isMember = !!membership
|
||||
}
|
||||
|
||||
if (!isOwner && !isMember && user.role !== 'admin') {
|
||||
throw createError({ statusCode: 403, message: 'No permission to update this task' })
|
||||
}
|
||||
|
||||
// 3. Обновляем узел внутри массива nodes
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const nodeIdx = nodes.findIndex(n => n.id === taskId)
|
||||
|
||||
@@ -29,18 +43,15 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// Обновляем состояние
|
||||
nodes[nodeIdx].state = newState
|
||||
|
||||
// Также обновляем дату изменения, если нужно
|
||||
nodes[nodeIdx].updatedAt = new Date().toISOString()
|
||||
if (nodes[nodeIdx].data) {
|
||||
nodes[nodeIdx].data.state = newState
|
||||
}
|
||||
|
||||
// 3. Сохранить изменения в БД
|
||||
// 4. Сохраняем обратно в БД
|
||||
await prisma.canvas.update({
|
||||
where: { id: canvasId },
|
||||
data: { nodes }
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Task ${taskId} moved to ${newState}`
|
||||
}
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const body = await readBody(event)
|
||||
const { displayName, bio, avatar } = body
|
||||
|
||||
// Обновляем данные в таблице Profile
|
||||
const updatedProfile = await prisma.profile.update({
|
||||
where: { userId: user.id },
|
||||
data: {
|
||||
...(displayName !== undefined && { displayName }),
|
||||
...(bio !== undefined && { bio }),
|
||||
...(avatar !== undefined && { avatar })
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, profile: updatedProfile }
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
|
||||
// 1. Считаем канвасы (личные и где участник)
|
||||
const canvasCount = await prisma.canvas.count({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId: user.id },
|
||||
{ groupId: { not: null } } // Тут можно усложнить, но для начала так
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Считаем задачи во всех доступных канвасах
|
||||
const canvases = await prisma.canvas.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ ownerId: user.id },
|
||||
{ groupId: { not: null } }
|
||||
]
|
||||
},
|
||||
select: { nodes: true }
|
||||
})
|
||||
|
||||
let totalTasks = 0
|
||||
let completedTasks = 0
|
||||
|
||||
for (const canvas of canvases) {
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const tasks = nodes.filter(n => n.type === 'task')
|
||||
totalTasks += tasks.length
|
||||
completedTasks += tasks.filter(t => t.data?.state === 'completed' || t.state === 'completed').length
|
||||
}
|
||||
|
||||
// 3. Ачивки (пока возвращаем 0, так как модели нет в схеме)
|
||||
const achievementCount = 0
|
||||
|
||||
return {
|
||||
stats: {
|
||||
canvases: canvasCount,
|
||||
tasks: {
|
||||
total: totalTasks,
|
||||
completed: completedTasks
|
||||
},
|
||||
achievements: achievementCount
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -8,10 +8,23 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
// Get user's recent activities
|
||||
const userActivities = Array.from(activities.values())
|
||||
.filter(activity => activity.userId === userId)
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 50) // Limit to 50 most recent
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ id: userId }, { username: userId }]
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, message: 'User not found' })
|
||||
}
|
||||
|
||||
// Get user's activities from database or memory
|
||||
const userActivities = await prisma.activity.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { timestamp: 'desc' },
|
||||
take: 20
|
||||
})
|
||||
// Limit to 50 most recent
|
||||
|
||||
return { activities: userActivities }
|
||||
})
|
||||
|
||||
@@ -7,6 +7,12 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, message: 'User ID is required' })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ id: userId }, { username: userId }]
|
||||
}
|
||||
})
|
||||
|
||||
// Get user's group memberships
|
||||
const userGroupIds = Array.from(userGroups.values())
|
||||
.filter(membership => membership.userId === userId)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const currentUser = requireAuth(event)
|
||||
const targetUserIdOrUsername = getRouterParam(event, 'id')
|
||||
|
||||
if (!targetUserIdOrUsername) {
|
||||
throw createError({ statusCode: 400, message: 'User ID is required' })
|
||||
}
|
||||
|
||||
// Находим целевого пользователя (по ID или username)
|
||||
const targetUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: targetUserIdOrUsername },
|
||||
{ username: targetUserIdOrUsername }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (!targetUser) {
|
||||
throw createError({ statusCode: 404, message: 'Target user not found' })
|
||||
}
|
||||
|
||||
try {
|
||||
const follow = await prisma.follows.findUnique({
|
||||
where: {
|
||||
followerId_followingId: {
|
||||
followerId: currentUser.id,
|
||||
followingId: targetUser.id
|
||||
}
|
||||
}
|
||||
})
|
||||
return { isFollowing: !!follow }
|
||||
} catch (e) {
|
||||
return { isFollowing: false }
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const currentUser = requireAuth(event)
|
||||
const targetIdOrUsername = getRouterParam(event, 'id')
|
||||
|
||||
const targetUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: targetIdOrUsername },
|
||||
{ username: targetIdOrUsername }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (!targetUser) {
|
||||
throw createError({ statusCode: 404, message: 'User not found' })
|
||||
}
|
||||
|
||||
if (targetUser.id === currentUser.id) {
|
||||
throw createError({ statusCode: 400, message: 'You cannot follow yourself' })
|
||||
}
|
||||
|
||||
await prisma.follows.upsert({
|
||||
where: {
|
||||
followerId_followingId: {
|
||||
followerId: currentUser.id,
|
||||
followingId: targetUser.id
|
||||
}
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
followerId: currentUser.id,
|
||||
followingId: targetUser.id
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
@@ -7,27 +7,52 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, message: 'User ID is required' })
|
||||
}
|
||||
|
||||
const profile = await prisma.profile.findUnique({
|
||||
where: { userId },
|
||||
let profile = await prisma.profile.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: userId },
|
||||
{ user: { username: userId } }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
canvases: true,
|
||||
followers: true,
|
||||
following: true
|
||||
}
|
||||
select: { canvases: true, followers: true, following: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If profile doesn't exist but user does, create it
|
||||
if (!profile) {
|
||||
// If profile doesn't exist but user might exist in Firebase,
|
||||
// we could potentially create it here or return 404
|
||||
throw createError({ statusCode: 404, message: 'Profile not found' })
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ id: userId }, { username: userId }]
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 404, message: 'User not found' })
|
||||
}
|
||||
|
||||
profile = await prisma.profile.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
displayName: user.username,
|
||||
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.id}`
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: {
|
||||
_count: {
|
||||
select: { canvases: true, followers: true, following: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user