feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI

This commit is contained in:
XFlow Deploy
2026-09-28 08:57:32 +05:00
parent edcd30c829
commit 0837b3f612
204 changed files with 24616 additions and 39 deletions
@@ -0,0 +1,18 @@
import { requireAuth } from '~/server/utils/auth'
import { getUserAchievements, achievements } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
// Calculate stats
const userAchievements = getUserAchievements(user.id)
const totalCompleted = userAchievements.filter((a: any) => a.id?.startsWith('completed-')).length
return {
achievements: userAchievements,
stats: {
total: userAchievements.length,
completedTasks: totalCompleted
}
}
})
+47
View File
@@ -0,0 +1,47 @@
import { requireAuth } from '~/server/utils/auth'
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' })
}
// 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.map(c => ({
...c,
nodeCount: (c.nodes as any[])?.length || 0
})),
stats: {
users: usersList.length,
canvases: canvasesList.length,
nodes: canvasesList.reduce((sum, c) => sum + ((c.nodes as any[])?.length || 0), 0),
pushes: pushCount
}
}
})
+170
View File
@@ -0,0 +1,170 @@
import { requireAuth } from '~/server/utils/auth'
import { generateAIResponse } from '~/server/utils/ai'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { action, text, model } = body
if (!action || !text) {
throw createError({ statusCode: 400, message: 'Action and text are required' })
}
console.log(`[AI Coach] action=${action} userId=${user.id}`)
try {
if (action === 'smart') {
const systemPrompt = `You are an expert executive coach. Your job is to transform any vague goal or statement into a high-quality, fully detailed S.M.A.R.T. goal. Keep your response in Russian.
Use Markdown headers for each element: Specific (Конкретная), Measurable (Измеримая), Achievable (Достижимая), Relevant (Значимая), Time-bound (Ограниченная во времени).
For Measurable, include a clear checklist with markdown checkboxes (- [ ]) for metrics. At the end, write a brief, inspiring summary.`
const prompt = `Пожалуйста, преобразуй следующую цель в формат S.M.A.R.T.
Исходная цель: "${text}"`
const result = await generateAIResponse(prompt, systemPrompt, model)
return { success: true, result }
} else if (action === 'grow') {
const systemPrompt = `You are a professional business coach. Your job is to perform a structured coaching analysis of the user's problem or goal using the G.R.O.W. framework. Keep all content in Russian.
You must return the response strictly as a JSON object with the following keys:
{
"goal": "Markdown text. Description of the reformulated Goal (G) starting with # 🎯 Цель (Goal).\\n\\nAnswer these questions:\\n- Какой именно цели я хочу достичь?\\n- Какой результат я хочу получить?\\n- Почему достижение этой цели важно?",
"options": "Markdown text. Options & Obstacles (O) starting with # 🚧 Варианты (Options).\\n\\nAnswer these questions:\\n- Что именно я могу изменить?\\n- Какие ресурсы мне нужны?\\n- Кто мне может помочь?\\n- А что еще?\\nInclude a checkbox list (- [ ]).",
"reality": "Markdown text. Description of the current Situation/Reality (R) starting with # 🔍 Реальность (Reality).\\n\\nAnswer these questions:\\n- Какова текущая ситуация?\\n- Какие препятствия я вижу?\\n- Что мне мешает начать?\\n- Какие у меня уже есть ресурсы?",
"wayForward": "Markdown text. Action Plan / Way Forward (W) starting with # 🏁 План действий (Way Forward).\\n\\nAnswer these questions:\\n- Какие самые важные действия необходимо предпринять?\\n- Что я сделаю в первую очередь? Какие сроки?\\nInclude a checkbox list (- [ ])."
}
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
const prompt = `Проведи GROW-коучинг анализ для следующей проблемы/цели: "${text}"`
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
// Robust JSON extraction
let jsonText = aiResponse.trim()
if (jsonText.startsWith('```json')) {
jsonText = jsonText.substring(7)
} else if (jsonText.startsWith('```')) {
jsonText = jsonText.substring(3)
}
if (jsonText.endsWith('```')) {
jsonText = jsonText.substring(0, jsonText.length - 3)
}
jsonText = jsonText.trim()
try {
const parsed = JSON.parse(jsonText)
return { success: true, result: parsed }
} catch (parseErr) {
console.error('Failed to parse AI G.R.O.W. JSON response:', aiResponse)
// Fallback: if JSON parsing fails, wrap the text into fields
return {
success: true,
result: {
goal: `# 🎯 Цель (Goal)\n\n${text}`,
reality: `# 🔍 Реальность (Reality)\n\nНе удалось автоматически разбить ответ AI на JSON. Вот оригинальный ответ:\n\n${aiResponse}`,
options: `# 🚧 Варианты\n\n- Рассмотреть альтернативные пути решения вручную`,
wayForward: `# 🏁 План действий\n\n- [ ] Детализировать шаги вручную`
}
}
}
} else if (action === 'dilts') {
const systemPrompt = `You are a professional life and business coach. Your job is to perform a structured analysis of the user's goal or situation using Robert Dilts' Logical Levels (Пирамида логических уровней Дилтса). Keep all content in Russian.
You must return the response strictly as a JSON object with the following keys:
{
"mission": "Markdown text starting with # 🌟 Миссия (Mission).\\n\\nAnswer these questions:\\n- Ради кого?\\n- Кому еще это важно?\\n(Уровень БЫТЬ)",
"identity": "Markdown text starting with # 👤 Идентичность (Identity).\\n\\nAnswer these questions:\\n- Кто я?",
"values": "Markdown text starting with # 💎 Ценности (Values).\\n\\nAnswer these questions:\\n- Почему это важно?",
"skills": "Markdown text starting with # 🧠 Навыки (Skills).\\n\\nAnswer these questions:\\n- Как? Каким образом?",
"actions": "Markdown text starting with # 🎬 Действия (Actions).\\n\\nAnswer these questions:\\n- Что делать? (Уровень ДЕЛАТЬ)",
"environment": "Markdown text starting with # 🌍 Окружение (Environment).\\n\\nAnswer these questions:\\n- Где? Когда? С кем? (Уровень ИМЕТЬ)"
}
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
const prompt = `Проведи коучинговый анализ по логическим уровням Дилтса для следующего запроса: "${text}"`
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
// Robust JSON extraction
let jsonText = aiResponse.trim()
if (jsonText.startsWith('```json')) {
jsonText = jsonText.substring(7)
} else if (jsonText.startsWith('```')) {
jsonText = jsonText.substring(3)
}
if (jsonText.endsWith('```')) {
jsonText = jsonText.substring(0, jsonText.length - 3)
}
jsonText = jsonText.trim()
try {
const parsed = JSON.parse(jsonText)
return { success: true, result: parsed }
} catch (parseErr) {
console.error('Failed to parse AI Dilts JSON response:', aiResponse)
// Fallback
return {
success: true,
result: {
mission: `# 🌟 Миссия (Mission)\n\nРади кого? Кому еще это важно? (Уровень БЫТЬ)\n\n${text}`,
identity: `# 👤 Идентичность (Identity)\n\nКто я?`,
values: `# 💎 Ценности (Values)\n\nПочему это важно?`,
skills: `# 🧠 Навыки (Skills)\n\nКак? Каким образом?`,
actions: `# 🎬 Действия (Actions)\n\nЧто делать? (Уровень ДЕЛАТЬ)\n\nНе удалось автоматически разбить ответ AI на JSON:\n\n${aiResponse}`,
environment: `# 🌍 Окружение (Environment)\n\nГде? Когда? С кем? (Уровень ИМЕТЬ)`
}
}
}
} else if (action === 'eisenhower') {
const systemPrompt = `You are a professional productivity expert. Your job is to analyze the user's tasks or list of ideas and prioritize them using the Eisenhower Matrix (Матрица Эйзенхауэра). Keep all content in Russian.
You must return the response strictly as a JSON object with the following keys:
{
"q1": "Markdown text starting with # 🔥 Важно и Срочно (Do First).\\n\\nIdentify tasks that are both urgent and important. Focus on crises, deadlines, and crucial problems.",
"q2": "Markdown text starting with # ⚡ Важно, но Несрочно (Schedule).\\n\\nIdentify tasks that are important but not urgent. Focus on long-term planning, personal growth, and relationships.",
"q3": "Markdown text starting with # 🧊 Неважно, но Срочно (Delegate).\\n\\nIdentify tasks that are urgent but not important. Focus on interruptions, minor meetings, and tasks that can be delegated.",
"q4": "Markdown text starting with # 💤 Неважно и Несрочно (Eliminate).\\n\\nIdentify tasks that are neither urgent nor important. Focus on time-wasting activities, distractions, and low-value tasks."
}
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
const prompt = `Распредели следующие задачи/цели по квадрантам Матрицы Эйзенхауэра: "${text}"`
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
// Robust JSON extraction
let jsonText = aiResponse.trim()
if (jsonText.startsWith('```json')) {
jsonText = jsonText.substring(7)
} else if (jsonText.startsWith('```')) {
jsonText = jsonText.substring(3)
}
if (jsonText.endsWith('```')) {
jsonText = jsonText.substring(0, jsonText.length - 3)
}
jsonText = jsonText.trim()
try {
const parsed = JSON.parse(jsonText)
return { success: true, result: parsed }
} catch (parseErr) {
console.error('Failed to parse AI Eisenhower JSON response:', aiResponse)
// Fallback
return {
success: true,
result: {
q1: `# 🔥 Важно и Срочно (Do First)\n\n- [ ] Срочные приоритетные задачи\n\n${text}`,
q2: `# ⚡ Важно, но Несрочно (Schedule)\n\n- [ ] Задачи планирования`,
q3: `# 🧊 Неважно, но Срочно (Delegate)\n\n- [ ] Задачи для делегирования\n\nНе удалось разобрать ответ ИИ:\n\n${aiResponse}`,
q4: `# 💤 Неважно и Несрочно (Eliminate)\n\n- [ ] Задачи, которые можно отложить`
}
}
}
} else {
throw createError({ statusCode: 400, message: `Unsupported action: ${action}` })
}
} catch (err: any) {
console.error('[AI Coach Error]:', err)
throw createError({
statusCode: 500,
message: err.message || 'AI coach generation failed'
})
}
})
+93
View File
@@ -0,0 +1,93 @@
import { generateToken } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
interface FirebaseUser {
localId: string
email: string
displayName?: string
photoUrl?: string
}
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { idToken } = body
if (!idToken) {
throw createError({ statusCode: 400, message: 'Firebase ID token required' })
}
const config = useRuntimeConfig()
const apiKey = config.firebaseApiKey || config.public.firebaseApiKey
if (!apiKey) {
throw createError({ statusCode: 500, message: 'Firebase API key not configured' })
}
console.log('[Auth] Received idToken, length:', idToken?.length || 0)
console.log('[Auth] Using API key (masked):', apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : 'EMPTY')
let verifyRes: any
try {
verifyRes = await $fetch<any>(
`https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=${apiKey}`,
{ method: 'POST', body: { idToken } }
)
} catch (err: any) {
console.error('[Auth] Firebase token verification failed:', err?.data || err?.message || err)
throw createError({ statusCode: 401, message: 'Invalid Firebase token' })
}
if (!verifyRes?.users?.[0]) {
throw createError({ statusCode: 401, message: 'Invalid Firebase token' })
}
const fbUser: FirebaseUser = verifyRes.users[0]
const email = fbUser.email
// Очищаем username: заменяем пробелы на дефисы и убираем спецсимволы
const username = email.split('@')[0].toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-_]/g, '')
const userId = fbUser.localId
const user = await prisma.user.upsert({
where: { email },
update: {
username
// Мы не затираем профиль при каждом входе, но нам нужно его получить
},
create: {
id: userId,
email,
username,
password: 'firebase-managed',
role: 'user',
profile: {
create: {
displayName: fbUser.displayName || username,
avatar: fbUser.photoUrl || `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}`
}
}
},
include: {
profile: true
}
})
const token = generateToken({
id: user.id,
email: user.email,
username: user.username,
displayName: user.profile?.displayName || user.username,
role: user.role
})
return {
message: 'Google sign-in successful',
token,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
photoUrl: fbUser.photoUrl
}
}
})
+17
View File
@@ -0,0 +1,17 @@
import { verifyToken } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const authHeader = getHeader(event, 'authorization')
const token = authHeader?.split(' ')[1]
if (!token) {
return { valid: false, error: 'No token provided' }
}
try {
const decoded = verifyToken(token)
return { valid: true, user: decoded }
} catch {
return { valid: false, error: 'Invalid token' }
}
})
+34
View File
@@ -0,0 +1,34 @@
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')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
const canvas = await prisma.canvas.findUnique({ where: { id } })
if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' })
// 1. Проверка прав
let canDelete = canvas.ownerId === user.id || user.role === 'admin'
if (!canDelete && canvas.groupId) {
const membership = await prisma.groupMember.findUnique({
where: {
userId_groupId: { userId: user.id, groupId: canvas.groupId }
}
})
// Только админ группы может удалять чужие карты внутри группы
if (membership && membership.role === 'admin') {
canDelete = true
}
}
if (!canDelete) {
throw createError({ statusCode: 403, message: 'Удалять карты может только владелец или администратор группы.' })
}
await prisma.canvas.delete({ where: { id } })
return { success: true, message: 'Canvas deleted' }
})
+31
View File
@@ -0,0 +1,31 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
const canvas = await prisma.canvas.findUnique({ where: { id } })
if (!canvas) throw createError({ statusCode: 404, message: 'Canvas not found' })
let user = { id: null, role: 'guest' } as any
try {
user = requireAuth(event)
} catch (e) {}
// Определение роли
let userRole = 'viewer'
if (user.id === canvas.ownerId) userRole = 'owner'
else if (user.role === 'admin') userRole = 'admin'
else if (canvas.groupId) {
const membership = user.id ? await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id as string, groupId: canvas.groupId } }
}) : null
if (membership) userRole = membership.role
else if (!canvas.isPublic) throw createError({ statusCode: 403, message: 'Access denied' })
} else if (!canvas.isPublic) {
throw createError({ statusCode: 403, message: 'Access denied' })
}
return { canvas, role: userRole }
})
+46
View File
@@ -0,0 +1,46 @@
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')
if (!id) throw createError({ statusCode: 400, message: 'Canvas ID is required' })
const body = await readBody(event)
const existing = await prisma.canvas.findUnique({ where: { id } })
if (!existing) throw createError({ statusCode: 404, message: 'Canvas not found' })
// 1. Проверка прав
let canEdit = existing.ownerId === user.id || user.role === 'admin' || existing.isPublic
if (!canEdit && existing.groupId) {
const membership = await prisma.groupMember.findUnique({
where: {
userId_groupId: { userId: user.id, groupId: existing.groupId }
}
})
// Редакторы и админы группы могут править
if (membership && (membership.role === 'admin' || membership.role === 'editor')) {
canEdit = true
}
}
if (!canEdit) {
throw createError({ statusCode: 403, message: 'У вас недостаточно прав для редактирования этой карты' })
}
const { name, nodes, edges, isPublic, groupId } = body
const canvas = await prisma.canvas.update({
where: { id },
data: {
...(name !== undefined && { name }),
...(nodes !== undefined && { nodes }),
...(edges !== undefined && { edges }),
...(isPublic !== undefined && { isPublic }),
...(groupId !== undefined && { groupId: groupId || null })
}
})
return { canvas }
})
@@ -0,0 +1,46 @@
import { requireAuth } from '~/server/utils/auth'
import { canvases } from '~/server/utils/db'
import type { CanvasData } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({ statusCode: 400, message: 'Canvas ID is required' })
}
const original = canvases.get(id)
if (!original) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
// Check permissions
if (original.ownerId !== user.id && !original.isPublic) {
throw createError({ statusCode: 403, message: 'Access denied' })
}
const newId = `canvas-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const duplicate: CanvasData = {
...original,
id: newId,
name: `${original.name} (Copy)`,
ownerId: user.id,
groupId: undefined, // Duplicates are personal
isPublic: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
nodes: original.nodes.map(node => ({
...node,
id: `${node.id}-copy-${Date.now()}`
})),
edges: original.edges.map(edge => ({
...edge,
id: `${edge.id}-copy-${Date.now()}`
}))
}
canvases.set(newId, duplicate)
return { canvas: duplicate }
})
+46
View File
@@ -0,0 +1,46 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const userGroups = await prisma.groupMember.findMany({
where: { userId: user.id },
select: { groupId: true }
})
const groupIds = userGroups.map(m => m.groupId)
const canvasList = await prisma.canvas.findMany({
where: {
OR: [
{ ownerId: user.id },
{ isPublic: true },
{ groupId: { in: groupIds } }
]
},
select: {
id: true,
name: true,
ownerId: true,
groupId: true,
isPublic: true,
createdAt: true,
updatedAt: true,
nodes: true
},
orderBy: { updatedAt: 'desc' }
})
const totalCount = await prisma.canvas.count()
const canvases = canvasList.map(c => ({
...c,
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
nodes: undefined
}))
return {
canvases,
totalCount
}
})
+32
View File
@@ -0,0 +1,32 @@
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 { name = 'New Canvas', nodes = [], edges = [], isPublic = false, groupId } = body
if (groupId) {
const membership = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (!membership) {
throw createError({ statusCode: 403, message: 'Not a member of this group' })
}
}
const id = `canvas-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const canvas = await prisma.canvas.create({
data: {
id,
name,
nodes,
edges,
isPublic,
ownerId: user.id,
groupId: groupId || null
}
})
return { canvas }
})
@@ -0,0 +1,33 @@
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 body = await readBody(event)
// Проверяем права: только владелец может менять настройки доступа
const canvas = await prisma.canvas.findUnique({
where: { id }
})
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
if (canvas.ownerId !== user.id && user.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Only owner can change settings' })
}
// Обновляем поля
const updatedCanvas = await prisma.canvas.update({
where: { id },
data: {
name: body.name !== undefined ? body.name : undefined,
isPublic: body.isPublic !== undefined ? body.isPublic : undefined,
groupId: body.groupId !== undefined ? body.groupId : undefined,
}
})
return { success: true, canvas: updatedCanvas }
})
+36
View File
@@ -0,0 +1,36 @@
import { requireAuth } from '~/server/utils/auth'
import { saveCanvasToGitea } from '~/server/utils/giteaApi'
import { canvases } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { canvasId, autoSync = true } = body
if (!canvasId) {
throw createError({ statusCode: 400, message: 'Canvas ID is required' })
}
// Check if canvas exists and user has access
const canvas = canvases.get(canvasId)
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
// Check permissions
if (canvas.ownerId !== user.id && !canvas.isPublic) {
throw createError({ statusCode: 403, message: 'Access denied' })
}
try {
const ok = await saveCanvasToGitea(canvasId, canvas, `Auto-sync: ${canvas.name}`)
return {
success: ok,
message: ok ? 'Canvas synchronized with Gitea' : 'Gitea sync failed',
autoSyncEnabled: autoSync
}
} catch (error) {
console.error('Failed to sync canvas to Gitea:', error)
throw createError({ statusCode: 500, message: 'Failed to sync canvas to Gitea' })
}
})
+68
View File
@@ -0,0 +1,68 @@
import { requireAuth } from '~/server/utils/auth'
import { saveCanvasToGitea, loadCanvasFromGitea } from '~/server/utils/giteaApi'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { canvasId, action, commitMessage } = body
if (!canvasId || !action) {
throw createError({ statusCode: 400, message: 'Canvas ID and action are required' })
}
try {
switch (action) {
case 'init':
case 'sync': {
const canvas = await prisma.canvas.findUnique({ where: { id: canvasId } })
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
if (canvas.ownerId !== user.id && !canvas.isPublic) {
throw createError({ statusCode: 403, message: 'Access denied' })
}
const opts = { username: user.username, groupId: canvas.groupId, isPublic: canvas.isPublic }
const ok = await saveCanvasToGitea(canvasId, canvas, opts, commitMessage)
return {
success: ok,
message: ok
? `Canvas ${canvasId} saved to Gitea`
: `Failed to save canvas ${canvasId} to Gitea`
}
}
case 'load': {
const existing = await prisma.canvas.findUnique({ where: { id: canvasId } })
const opts = existing ? { username: user.username, groupId: existing.groupId, isPublic: existing.isPublic } : { username: user.username }
const gitCanvas = await loadCanvasFromGitea(canvasId, opts)
if (!gitCanvas) {
return { success: false, message: 'Canvas not found in Gitea', needsInit: true }
}
if (existing) {
await prisma.canvas.update({
where: { id: canvasId },
data: {
nodes: gitCanvas.nodes || [],
edges: gitCanvas.edges || []
}
})
}
return { success: true, canvas: gitCanvas, message: `Canvas ${canvasId} loaded from Gitea` }
}
default:
throw createError({ statusCode: 400, message: 'Invalid action' })
}
} catch (error: any) {
const details = error.data || error.message || error
console.error('Gitea sync error details:', details)
throw createError({
statusCode: 500,
message: typeof details === 'string' ? details : JSON.stringify(details)
})
}
})
@@ -0,0 +1,47 @@
import { requireAuth } from '~/server/utils/auth'
import { saveCanvasToGitea } from '~/server/utils/giteaApi'
import { canvases, userGroups } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { canvasId, type } = body
if (!canvasId || !type) {
throw createError({ statusCode: 400, message: 'Canvas ID and repository type are required' })
}
// Check if canvas exists and user has access
const canvas = canvases.get(canvasId)
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
if (canvas.ownerId !== user.id && !canvas.isPublic) {
// Check group access
if (canvas.groupId) {
const membership = Array.from(userGroups.values()).find(
m => m.userId === user.id && m.groupId === canvas.groupId
)
if (!membership) {
throw createError({ statusCode: 403, message: 'Access denied' })
}
} else {
throw createError({ statusCode: 403, message: 'Access denied' })
}
}
try {
// Initial save to Gitea creates the canvas file in the repo
const ok = await saveCanvasToGitea(canvasId, canvas, `Init canvas ${canvasId}`)
return {
success: ok,
message: ok
? `Canvas ${canvasId} initialized in Gitea`
: `Failed to initialize canvas ${canvasId} in Gitea`
}
} catch (error) {
console.error('Failed to initialize canvas in Gitea:', error)
throw createError({ statusCode: 500, message: 'Failed to initialize canvas in Gitea' })
}
})
@@ -0,0 +1,54 @@
import { requireAuth } from '~/server/utils/auth'
import { resolveRepo } from '~/server/utils/giteaApi'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const canvasId = getRouterParam(event, 'canvasId')
if (!canvasId) {
throw createError({ statusCode: 400, message: 'Canvas ID is required' })
}
const canvas = await prisma.canvas.findUnique({ where: { id: canvasId } })
const opts = canvas
? { username: user.username, groupId: canvas.groupId, isPublic: canvas.isPublic }
: { username: user.username }
const { owner, repo } = resolveRepo(opts)
const config = useRuntimeConfig()
const { giteaUrl: url, giteaToken: token } = config
try {
const res = await $fetch<any>(`${url}/api/v1/repos/${owner}/${repo}`, {
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/json'
}
})
return {
success: true,
repository: {
id: res.id,
name: res.name,
owner: res.owner.login,
url: res.html_url,
cloneUrl: res.clone_url,
isPrivate: res.private,
type: 'personal',
ownerId: user.id,
createdAt: res.created_at,
updatedAt: res.updated_at
}
}
} catch (error: any) {
if (error.response?.status === 404) {
return { success: false, repository: null, message: 'Repository not found' }
}
console.error('Failed to fetch repository:', error)
throw createError({ statusCode: 500, message: 'Failed to fetch repository details from Gitea' })
}
})
+23
View File
@@ -0,0 +1,23 @@
import { requireAuth } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { canvasId, action, commitMessage } = body
if (!canvasId || !action) {
throw createError({ statusCode: 400, message: 'Canvas ID and action are required' })
}
try {
// Git sync logic will be implemented here
// For now, return success response
return {
success: true,
message: `Git ${action} completed for canvas ${canvasId}`,
commitMessage: commitMessage || `Auto-sync canvas ${canvasId}`
}
} catch (error) {
throw createError({ statusCode: 500, message: 'Git sync failed' })
}
})
+45
View File
@@ -0,0 +1,45 @@
// Test endpoint: GET /api/git/test
// Verifies Gitea connection and repo access
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const { giteaUrl, giteaToken, giteaOrg, giteaMindmapRepo } = config
if (!giteaUrl || !giteaToken) {
return { success: false, message: 'GITEA_URL or GITEA_TOKEN not set in .env' }
}
try {
// Test 1: check repo exists
const repo = await $fetch<any>(`${giteaUrl}/api/v1/repos/${giteaOrg}/${giteaMindmapRepo}`, {
headers: { 'Authorization': `token ${giteaToken}` }
})
// Test 2: check/create canvases folder by listing contents
let folderExists = false
try {
await $fetch(`${giteaUrl}/api/v1/repos/${giteaOrg}/${giteaMindmapRepo}/contents/canvases`, {
headers: { 'Authorization': `token ${giteaToken}` }
})
folderExists = true
} catch {
folderExists = false
}
return {
success: true,
repo: repo.full_name,
private: repo.private,
folderExists,
message: folderExists
? '✅ Gitea connected, canvases/ folder exists'
: '✅ Gitea connected, canvases/ folder will be created on first save'
}
} catch (e: any) {
return {
success: false,
message: `❌ Gitea connection failed: ${e?.message || e}`,
hint: 'Check GITEA_URL, GITEA_TOKEN, GITEA_ORG, GITEA_MINDMAP_REPO in .env'
}
}
})
+17
View File
@@ -0,0 +1,17 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const groupId = getRouterParam(event, 'id')!
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (group.ownerId !== user.id && user.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Only owner can delete group' })
}
await prisma.group.delete({ where: { id: groupId } })
return { success: true }
})
@@ -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, userId, role = 'member' } = body
if (!groupId || (!email && !userId)) {
throw createError({ statusCode: 400, message: 'Group ID and either Email or User ID 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. Находим пользователя
let userToAdd = null
if (userId) {
userToAdd = await prisma.user.findUnique({ where: { id: userId } })
} else if (email) {
userToAdd = await prisma.user.findUnique({ where: { email } })
}
if (!userToAdd) {
throw createError({ statusCode: 404, message: 'User not found' })
}
// 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,35 @@
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 }
}
}
},
canvases: {
select: { id: true, name: true, updatedAt: 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 }
})
@@ -0,0 +1,22 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const groupId = getRouterParam(event, 'id')!
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (!group.isPublic) throw createError({ statusCode: 403, message: 'Group is private' })
const existing = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (existing) return { message: 'Already a member' }
await prisma.groupMember.create({
data: { userId: user.id, groupId, role: 'member' }
})
return { success: true, message: `Joined group "${group.name}"` }
})
@@ -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 groupId = getRouterParam(event, 'id')!
const group = await prisma.group.findUnique({ where: { id: groupId } })
if (!group) throw createError({ statusCode: 404, message: 'Group not found' })
if (group.ownerId === user.id) throw createError({ statusCode: 400, message: 'Owner cannot leave group. Delete it instead.' })
const membership = await prisma.groupMember.findUnique({
where: { userId_groupId: { userId: user.id, groupId } }
})
if (!membership) throw createError({ statusCode: 400, message: 'Not a member' })
await prisma.groupMember.delete({ where: { id: membership.id } })
return { success: true, message: `Left group "${group.name}"` }
})
@@ -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' })
}
})
+30
View File
@@ -0,0 +1,30 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const allGroups = await prisma.group.findMany({
where: user.role === 'admin' ? {} : { isPublic: true },
include: {
members: { include: { user: { select: { id: true, username: true } } } },
_count: { select: { members: true, canvases: true } }
},
orderBy: { createdAt: 'desc' }
})
return {
groups: allGroups.map(g => ({
id: g.id,
name: g.name,
description: g.description,
isPublic: g.isPublic,
ownerId: g.ownerId,
createdAt: g.createdAt,
updatedAt: g.updatedAt,
memberCount: g._count.members,
canvasCount: g._count.canvases,
isMember: g.members.some(m => m.userId === user.id)
}))
}
})
+29
View File
@@ -0,0 +1,29 @@
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 { name, description = '', isPublic = true } = body
if (!name?.trim()) {
throw createError({ statusCode: 400, message: 'Group name is required' })
}
const group = await prisma.group.create({
data: {
name: name.trim(),
description,
isPublic,
ownerId: user.id,
members: {
create: {
userId: user.id,
role: 'admin'
}
}
}
})
return { group }
})
+26
View File
@@ -0,0 +1,26 @@
import { requireAuth } from '~/server/utils/auth'
import { nodes, addAchievement } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const existing = nodes.get(id || '') || { id, state: 'pending', assigned_to: user.id }
// Update state
const updated = { ...existing, ...body, updatedAt: new Date().toISOString() }
nodes.set(id || '', updated)
// Gamification: achievement on completion
if (body.state === 'completed' && existing.state !== 'completed') {
addAchievement(user.id, {
id: `completed-${id}`,
title: 'Задача выполнена',
description: `Нода ${id} завершена`,
icon: '✅'
})
}
return { node: updated }
})
@@ -0,0 +1,105 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
import { generateAIResponse } from '~/server/utils/ai'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const nodeId = getRouterParam(event, 'id')
const body = await readBody(event)
const { canvasId, context } = body
console.log(`[Execute] nodeId=${nodeId} canvasId=${canvasId} userId=${user.id}`)
if (!nodeId || !canvasId) {
throw createError({ statusCode: 400, message: 'Node ID and Canvas ID are required' })
}
// 1. Найти холст и убедиться в правах доступа
const canvas = await prisma.canvas.findUnique({
where: { id: canvasId }
})
console.log(`[Execute] Canvas found: ${!!canvas}, nodes count: ${(canvas?.nodes as any[])?.length}`)
if (!canvas) {
throw createError({ statusCode: 404, message: 'Canvas not found' })
}
// Проверка прав (владелец или админ)
if (canvas.ownerId !== user.id && user.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Access denied' })
}
// 2. Найти конкретный узел в JSON холста
const nodes = (canvas.nodes as any[]) || []
const nodeIdx = nodes.findIndex(n => n.id === nodeId)
console.log(`[Execute] Node found at index: ${nodeIdx}`)
if (nodeIdx === -1) {
throw createError({ statusCode: 404, message: `Node ${nodeId} not found in canvas ${canvasId}` })
}
const node = nodes[nodeIdx]
console.log(`[Execute] Node type: ${node.type}`)
try {
let result = ''
if (node.type === 'ai') {
// Формируем промпт с учетом контекста от соседей
let fullPrompt = node.prompt || node.text || 'Analyze the provided context.'
if (context && context.length > 0) {
const contextText = context.map((c: any) => `[Node ${c.id}]: ${c.text}`).join('\n\n')
fullPrompt = `CONTEXT INFORMATION:\n${contextText}\n\nUSER REQUEST/INSTRUCTION:\n${fullPrompt}`
}
// Модель можно задать прямо в узле: node.model = 'qwen_coder' | 'llama' | 'gemma' | 'laguna' | 'nemotron'
const model = node.model || undefined
console.log(`[AI Execute] model=${model || 'default'} nodeId=${nodeId}`)
result = await generateAIResponse(fullPrompt, 'You are a helpful assistant.', model)
// Сохраняем результат в текст узла
nodes[nodeIdx].text = result
} else if (node.type === 'webhook') {
const url = node.url
const method = node.method || 'GET'
if (!url) throw new Error('Webhook URL is not configured')
console.log(`[Webhook Execute] Calling ${method} ${url}...`)
const response = await $fetch<any>(url, {
method,
...(method === 'POST' ? { body: { context, timestamp: new Date() } } : {})
})
result = typeof response === 'string' ? response : JSON.stringify(response, null, 2)
nodes[nodeIdx].text = `### Response from ${url}\n\n\`\`\`json\n${result}\n\`\`\``
} else {
throw createError({ statusCode: 400, message: `Execution not supported for type: ${node.type}` })
}
// 3. Сохраняем обновленный холст
await prisma.canvas.update({
where: { id: canvasId },
data: { nodes }
})
return {
success: true,
result,
message: 'Node executed and canvas updated'
}
} catch (error: any) {
console.error('Node execution error:', error)
throw createError({
statusCode: 500,
message: error.message || 'Execution failed'
})
}
})
+14
View File
@@ -0,0 +1,14 @@
import { requireAuth } from '~/server/utils/auth'
import { nodes } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
// RBAC: filter nodes by assigned_to or role
const allNodes = Array.from(nodes.values())
const filtered = user.role === 'admin'
? allNodes
: allNodes.filter((n: any) => n.assigned_to === user.id || n.assigned_to === 'all')
return { nodes: filtered }
})
+21
View File
@@ -0,0 +1,21 @@
import { requireRole } from '~/server/utils/auth'
import { pushSubscriptions } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
requireRole(event, 'admin')
const body = await readBody(event)
const { title, message, userId } = body
const targets = userId
? [pushSubscriptions.get(userId)].filter(Boolean)
: Array.from(pushSubscriptions.values())
// In production, use web-push library with VAPID keys
const results = targets.map((sub: any) => ({
userId: sub.userId,
sent: true,
notification: { title, message, timestamp: new Date().toISOString() }
}))
return { sent: results.length, results }
})
@@ -0,0 +1,16 @@
import { requireAuth } from '~/server/utils/auth'
import { pushSubscriptions } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const body = await readBody(event)
const { subscription } = body
if (!subscription?.endpoint) {
throw createError({ statusCode: 400, message: 'Invalid subscription' })
}
pushSubscriptions.set(user.id, { userId: user.id, subscription, createdAt: new Date().toISOString() })
return { success: true }
})
+56
View File
@@ -0,0 +1,56 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const userGroups = await prisma.groupMember.findMany({
where: { userId: user.id },
select: { groupId: true }
})
const groupIds = userGroups.map(m => m.groupId)
// Get all canvases user has access to
const canvasList = await prisma.canvas.findMany({
where: {
OR: [
{ ownerId: user.id },
{ isPublic: true },
{ groupId: { in: groupIds } }
]
},
select: {
id: true,
name: true,
nodes: true
}
})
// Extract tasks from all canvases
const allTasks = []
for (const canvas of canvasList) {
const nodes = Array.isArray(canvas.nodes) ? canvas.nodes :
(typeof canvas.nodes === 'string' ? JSON.parse(canvas.nodes) : [])
if (!Array.isArray(nodes)) continue
const canvasTasks = nodes.filter(n => {
const type = n.type || n.data?.type
if (type !== 'task') return false
// Temporary: Show ALL tasks regardless of assignee so you can see them!
return true
})
for (const t of canvasTasks) {
allTasks.push({
...t,
canvasId: canvas.id,
canvasName: canvas.name
})
}
}
return { tasks: allTasks }
})
+69
View File
@@ -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 }
})
+55
View File
@@ -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 }
})
@@ -0,0 +1,57 @@
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 { taskId, canvasId, newState } = body
if (!taskId || !canvasId || !newState) {
throw createError({ statusCode: 400, message: 'Missing parameters' })
}
// 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 update this task' })
}
// 3. Обновляем узел внутри массива nodes
const nodes = (canvas.nodes as any[]) || []
const nodeIdx = nodes.findIndex(n => n.id === taskId)
if (nodeIdx === -1) {
throw createError({ statusCode: 404, message: 'Task node not found in canvas' })
}
// Обновляем состояние
nodes[nodeIdx].state = newState
if (nodes[nodeIdx].data) {
nodes[nodeIdx].data.state = newState
}
// 4. Сохраняем обратно в БД
await prisma.canvas.update({
where: { id: canvasId },
data: { nodes }
})
return { success: true }
})
+21
View File
@@ -0,0 +1,21 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
// Находим группы, где пользователь либо владелец, либо участник
const groups = await prisma.group.findMany({
where: {
OR: [
{ ownerId: user.id },
{ members: { some: { userId: user.id } } }
]
},
orderBy: {
name: 'asc'
}
})
return { groups }
})
+20
View File
@@ -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 }
})
+53
View File
@@ -0,0 +1,53 @@
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
if (Array.isArray(nodes)) {
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
}
}
})
@@ -0,0 +1,30 @@
import { activities } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'id')
if (!userId) {
throw createError({ statusCode: 400, message: 'User ID is required' })
}
// Get user's recent activities
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 }
})
@@ -0,0 +1,42 @@
import { canvases, userGroups } from '~/server/utils/db'
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'id')
if (!userId) {
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)
.map(membership => membership.groupId)
// Get canvases owned by user or shared with user's groups
const userCanvases = Array.from(canvases.values()).filter(canvas => {
// User's own canvases
if (canvas.ownerId === userId) return true
// Group canvases
if (canvas.groupId && userGroupIds.includes(canvas.groupId)) return true
return false
})
const canvasList = userCanvases.map(canvas => ({
id: canvas.id,
name: canvas.name,
ownerId: canvas.ownerId,
createdAt: canvas.createdAt,
updatedAt: canvas.updatedAt,
isPublic: canvas.isPublic,
groupId: canvas.groupId,
nodeCount: canvas.nodes.length
}))
return { canvases: canvasList }
})
@@ -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 }
})
@@ -0,0 +1,66 @@
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'id')
if (!userId) {
throw createError({ statusCode: 400, message: 'User ID is required' })
}
let profile = await prisma.profile.findFirst({
where: {
OR: [
{ userId: userId },
{ user: { username: userId } }
]
},
include: {
user: {
include: {
_count: {
select: { canvases: true, followers: true, following: true }
}
}
}
}
})
// If profile doesn't exist but user does, create it
if (!profile) {
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 {
profile: {
...profile,
canvasCount: profile.user._count.canvases,
followerCount: profile.user._count.followers,
followingCount: profile.user._count.following
}
}
})
+32
View File
@@ -0,0 +1,32 @@
import { defineEventHandler } from 'h3'
import { prisma } from '~/server/utils/prisma'
import { requireAuth } from '~/server/utils/auth'
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
if (!user) return []
const users = await prisma.user.findMany({
select: {
id: true,
username: true,
email: true,
role: true,
profile: {
select: {
displayName: true,
avatar: true
}
}
}
})
return users.map(u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role,
displayName: u.profile?.displayName || u.username,
avatar: u.profile?.avatar
}))
})
+28
View File
@@ -0,0 +1,28 @@
import { requireAuth } from '~/server/utils/auth'
import { prisma } from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
requireAuth(event)
const query = getQuery(event).q as string
if (!query || query.length < 2) {
return { users: [] }
}
const users = await prisma.user.findMany({
where: {
OR: [
{ username: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } }
]
},
select: {
id: true,
username: true,
email: true
},
take: 10
})
return { users }
})
+18
View File
@@ -0,0 +1,18 @@
import { getListsUseCase } from '../../../modules/notes-lists';
import { getAuthUser } from '../../../utils/auth';
export default defineEventHandler(async (event) => {
const user = getAuthUser(event);
const ownerId = user?.id || (getQuery(event).userId as string) || 'default-user';
const category = getQuery(event).category as string | undefined;
const result = await getListsUseCase.execute({ ownerId, category });
if (result.isFailure) {
throw createError({ statusCode: 400, message: result.getError().message });
}
return {
success: true,
lists: result.getValue().map(l => l.toJSON())
};
});
@@ -0,0 +1,35 @@
import { addListItemUseCase } from '../../../modules/notes-lists';
import { getAuthUser } from '../../../utils/auth';
export default defineEventHandler(async (event) => {
const user = getAuthUser(event);
const body = await readBody(event);
const ownerId = user?.id || body.ownerId || 'default-user';
const { title, listId, listTitle, category, description, priority, metadata } = body;
if (!title) {
throw createError({ statusCode: 400, message: 'Item title is required' });
}
const result = await addListItemUseCase.execute({
title,
listId,
listTitle,
category,
description,
priority,
metadata,
ownerId
});
if (result.isFailure) {
const error = result.getError();
throw createError({ statusCode: error.statusCode || 400, message: error.message });
}
return {
success: true,
item: result.getValue().toJSON()
};
});
@@ -0,0 +1,29 @@
import { toggleListItemUseCase } from '../../../../modules/notes-lists';
import { getAuthUser } from '../../../../utils/auth';
export default defineEventHandler(async (event) => {
const itemId = getRouterParam(event, 'id');
if (!itemId) {
throw createError({ statusCode: 400, message: 'Item ID is required' });
}
const user = getAuthUser(event);
const body = await readBody(event).catch(() => ({}));
const ownerId = user?.id || body.ownerId || 'default-user';
const result = await toggleListItemUseCase.execute({
itemId,
status: body.status,
ownerId
});
if (result.isFailure) {
const error = result.getError();
throw createError({ statusCode: error.statusCode || 400, message: error.message });
}
return {
success: true,
item: result.getValue().toJSON()
};
});