diff --git a/app/components/FlowEditor.vue b/app/components/FlowEditor.vue
index df93453..07e8983 100644
--- a/app/components/FlowEditor.vue
+++ b/app/components/FlowEditor.vue
@@ -23,7 +23,19 @@
+
+
+
+ 👤 Личный профиль
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ saveMessage.text }}
+
+
+
+
+
+
+
+ 👥 Мои Группы
+
+
+ Вы не состоите ни в одной группе.
+
+
+
+
+
{{ g.name }}
+
{{ g.description }}
+
+
+ {{ g.membershipRole }}
+
+
+
+
+
@@ -72,10 +132,59 @@ definePageMeta({
middleware: 'auth'
})
+const { user } = useAuth()
const { isSupported, subscription, requestPermission, subscribe: pushSubscribe } = usePush()
const { achievements, loadAchievements } = useAchievements()
const permissionGranted = ref(false)
+const saving = ref(false)
+const saveMessage = ref(null)
+const userGroups = ref([])
+
+const profileForm = reactive({
+ displayName: '',
+ username: '',
+ bio: '',
+ avatar: ''
+})
+
+const loadProfile = async () => {
+ try {
+ const data = await $fetch(`/api/users/${user.value.id}/profile`, {
+ headers: { Authorization: `Bearer ${useAuth().token.value}` }
+ })
+const groupsData = await $fetch('/api/user/groups', {
+ headers: { Authorization: `Bearer ${useAuth().token.value}` }
+ })
+ userGroups.value = groupsData.groups || []
+
+ profileForm.displayName = data.profile.displayName || ''
+ profileForm.username = data.profile.user.username || ''
+ profileForm.bio = data.profile.bio || ''
+ profileForm.avatar = data.profile.avatar || ''
+ } catch (e) {
+ console.error('Failed to load profile', e)
+ }
+}
+
+const saveProfile = async () => {
+ saving.value = true
+ saveMessage.value = null
+ try {
+ await $fetch('/api/user/profile', {
+ method: 'PUT',
+ body: profileForm,
+ headers: { Authorization: `Bearer ${useAuth().token.value}` }
+ })
+ saveMessage.value = { type: 'success', text: 'Профиль обновлен успешно!' }
+ // Update local user data if needed
+ user.value.username = profileForm.username
+ } catch (e) {
+ saveMessage.value = { type: 'error', text: e.data?.message || 'Ошибка при сохранении' }
+ } finally {
+ saving.value = false
+ }
+}
const enablePush = async () => {
const granted = await requestPermission()
@@ -91,6 +200,9 @@ const subscribe = async () => {
onMounted(async () => {
permissionGranted.value = Notification.permission === 'granted'
- await loadAchievements()
+ await Promise.all([
+ loadAchievements(),
+ loadProfile()
+ ])
})
diff --git a/app/pages/tasks.vue b/app/pages/tasks.vue
index 46b1e04..887736d 100644
--- a/app/pages/tasks.vue
+++ b/app/pages/tasks.vue
@@ -5,6 +5,56 @@
📋 Мои Задачи
+
+
+
+
+
+
+
+
+
Создать новую задачу
+
+
@@ -92,6 +142,16 @@ const columnsData = reactive({
completed: []
})
+const showCreateModal = ref(false)
+const creating = ref(false)
+const availableCanvases = ref([])
+const newTask = reactive({
+ title: '',
+ canvasId: '',
+ priority: 'medium',
+ state: 'todo'
+})
+
const columns = [
{ id: 'todo', title: 'К выполнению', icon: '📋', color: 'text-slate-400' },
{ id: 'in_progress', title: 'В работе', icon: '⏳', color: 'text-blue-400' },
@@ -125,6 +185,36 @@ const fetchTasks = async () => {
}
}
+const fetchCanvases = async () => {
+ try {
+ const res = await $fetch('/api/canvas')
+ availableCanvases.value = res.canvases || []
+ if (availableCanvases.value.length > 0) {
+ newTask.canvasId = availableCanvases.value[0].id
+ }
+ } catch (e) {
+ console.error('Failed to load canvases', e)
+ }
+}
+
+const createTask = async () => {
+ creating.value = true
+ try {
+ await $fetch('/api/tasks/create', {
+ method: 'POST',
+ body: newTask,
+ headers: { Authorization: `Bearer ${auth.token.value}` }
+ })
+ showCreateModal.value = false
+ newTask.title = ''
+ await fetchTasks() // Refresh list
+ } catch (e) {
+ alert('Ошибка при создании задачи: ' + (e.data?.message || e.message))
+ } finally {
+ creating.value = false
+ }
+}
+
const onTaskMove = async (evt, newStatus) => {
if (evt.added) {
const task = evt.added.element
@@ -197,7 +287,10 @@ onMounted(async () => {
if (!auth.user.value) {
await auth.init()
}
- await fetchTasks()
+ await Promise.all([
+ fetchTasks(),
+ fetchCanvases()
+ ])
})
diff --git a/app/public/manifest.json b/app/public/manifest.json
new file mode 100644
index 0000000..078708d
--- /dev/null
+++ b/app/public/manifest.json
@@ -0,0 +1,34 @@
+{
+ "id": "/",
+ "name": "XFlow",
+ "short_name": "XFlow",
+ "description": "XFlow Obsidian Canvas PWA",
+ "lang": "ru",
+ "categories": ["productivity", "business"],
+ "theme_color": "#0f172a",
+ "background_color": "#0f172a",
+ "display": "standalone",
+ "orientation": "portrait",
+ "start_url": "/",
+ "scope": "/",
+ "icons": [
+ {
+ "src": "/pwa-192x192.svg",
+ "sizes": "192x192",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/pwa-512x512.svg",
+ "sizes": "512x512",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/maskable-icon-512x512.svg",
+ "sizes": "512x512",
+ "type": "image/svg+xml",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/app/server/api/admin/stats.get.ts b/app/server/api/admin/stats.get.ts
index 71e8ee1..3108427 100644
--- a/app/server/api/admin/stats.get.ts
+++ b/app/server/api/admin/stats.get.ts
@@ -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
}
}
-})
+})
diff --git a/app/server/api/auth/google.post.ts b/app/server/api/auth/google.post.ts
index a92bab3..e490661 100644
--- a/app/server/api/auth/google.post.ts
+++ b/app/server/api/auth/google.post.ts
@@ -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
})
diff --git a/app/server/api/groups/[id]/add-member.post.ts b/app/server/api/groups/[id]/add-member.post.ts
new file mode 100644
index 0000000..386b51f
--- /dev/null
+++ b/app/server/api/groups/[id]/add-member.post.ts
@@ -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' })
+ }
+})
diff --git a/app/server/api/groups/[id]/index.get.ts b/app/server/api/groups/[id]/index.get.ts
new file mode 100644
index 0000000..0380aa9
--- /dev/null
+++ b/app/server/api/groups/[id]/index.get.ts
@@ -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 }
+})
diff --git a/app/server/api/groups/index.get.ts b/app/server/api/groups/index.get.ts
index a73bbd2..63dd98e 100644
--- a/app/server/api/groups/index.get.ts
+++ b/app/server/api/groups/index.get.ts
@@ -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 } }
diff --git a/app/server/api/tasks/create.post.ts b/app/server/api/tasks/create.post.ts
new file mode 100644
index 0000000..523ec0f
--- /dev/null
+++ b/app/server/api/tasks/create.post.ts
@@ -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 }
+})
diff --git a/app/server/api/tasks/index.get.ts b/app/server/api/tasks/index.get.ts
new file mode 100644
index 0000000..deec488
--- /dev/null
+++ b/app/server/api/tasks/index.get.ts
@@ -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 }
+})
diff --git a/app/server/api/tasks/update-status.post.ts b/app/server/api/tasks/update-status.post.ts
index e9f3a07..91a6f24 100644
--- a/app/server/api/tasks/update-status.post.ts
+++ b/app/server/api/tasks/update-status.post.ts
@@ -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 }
})
diff --git a/app/server/api/user/profile.put.ts b/app/server/api/user/profile.put.ts
new file mode 100644
index 0000000..6e9e975
--- /dev/null
+++ b/app/server/api/user/profile.put.ts
@@ -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 }
+})
diff --git a/app/server/api/user/stats.get.ts b/app/server/api/user/stats.get.ts
new file mode 100644
index 0000000..74a4805
--- /dev/null
+++ b/app/server/api/user/stats.get.ts
@@ -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
+ }
+ }
+})
diff --git a/app/server/api/users/[id]/activity.get.ts b/app/server/api/users/[id]/activity.get.ts
index 4fd9f62..c437b1f 100644
--- a/app/server/api/users/[id]/activity.get.ts
+++ b/app/server/api/users/[id]/activity.get.ts
@@ -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 }
})
diff --git a/app/server/api/users/[id]/canvases.get.ts b/app/server/api/users/[id]/canvases.get.ts
index 23b113f..d0866b3 100644
--- a/app/server/api/users/[id]/canvases.get.ts
+++ b/app/server/api/users/[id]/canvases.get.ts
@@ -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)
diff --git a/app/server/api/users/[id]/follow-status.get.ts b/app/server/api/users/[id]/follow-status.get.ts
new file mode 100644
index 0000000..564378e
--- /dev/null
+++ b/app/server/api/users/[id]/follow-status.get.ts
@@ -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 }
+ }
+})
diff --git a/app/server/api/users/[id]/follow.post.ts b/app/server/api/users/[id]/follow.post.ts
new file mode 100644
index 0000000..f6f48b6
--- /dev/null
+++ b/app/server/api/users/[id]/follow.post.ts
@@ -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 }
+})
diff --git a/app/server/api/users/[id]/profile.get.ts b/app/server/api/users/[id]/profile.get.ts
index cc27332..c7f26a0 100644
--- a/app/server/api/users/[id]/profile.get.ts
+++ b/app/server/api/users/[id]/profile.get.ts
@@ -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 {
diff --git a/app/server/utils/auth.ts b/app/server/utils/auth.ts
index 21b8fe0..089da3b 100644
--- a/app/server/utils/auth.ts
+++ b/app/server/utils/auth.ts
@@ -1,25 +1,36 @@
import jwt from 'jsonwebtoken'
import { H3Event, createError } from 'h3'
-const JWT_SECRET = process.env.NUXT_JWT_SECRET || 'your-super-secret-jwt-key'
-
export interface AuthUser {
id: string
email: string
username: string
+ displayName?: string
role?: string
}
export function generateToken(user: AuthUser): string {
+ const config = useRuntimeConfig()
+ const secret = config.jwtSecret || 'your-super-secret-jwt-key'
+
return jwt.sign(
- { id: user.id, email: user.email, username: user.username, role: user.role || 'user' },
- JWT_SECRET,
+ {
+ id: user.id,
+ email: user.email,
+ username: user.username,
+ displayName: user.displayName,
+ role: user.role || 'user'
+ },
+ secret,
{ expiresIn: '7d' }
)
}
export function verifyToken(token: string): AuthUser {
- return jwt.verify(token, JWT_SECRET) as AuthUser
+ const config = useRuntimeConfig()
+ const secret = config.jwtSecret || 'your-super-secret-jwt-key'
+
+ return jwt.verify(token, secret) as AuthUser
}
export function getAuthUser(event: H3Event): AuthUser | null {
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 3e922dc..2ed86bb 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -8,52 +8,32 @@ export default defineNuxtConfig({
'@vueuse/nuxt'
],
+ experimental: {
+ appManifest: false
+ },
+
css: ['~/assets/css/main.css'],
tailwindcss: {
cssPath: '~/assets/css/main.css'
},
- pwa: {
- registerType: 'autoUpdate',
- workbox: {
- globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
- },
- client: {
- installPrompt: true,
- },
- manifest: {
- name: 'XFlow',
- short_name: 'XFlow',
- description: 'XFlow Obsidian Canvas PWA',
- theme_color: '#0f172a',
- background_color: '#0f172a',
- display: 'standalone',
- orientation: 'portrait',
- scope: '/',
- start_url: '/',
- icons: [
- {
- src: 'pwa-192x192.svg',
- sizes: '192x192',
- type: 'image/svg+xml'
- },
- {
- src: 'pwa-512x512.svg',
- sizes: '512x512',
- type: 'image/svg+xml',
- purpose: 'any'
- },
- {
- src: 'maskable-icon-512x512.svg',
- sizes: '512x512',
- type: 'image/svg+xml',
- purpose: 'maskable'
- }
+ app: {
+ head: {
+ title: 'XFlow',
+ link: [
+ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
+ { rel: 'manifest', href: '/manifest.json' }
]
- },
+ }
+ },
+
+ pwa: {
+ injectRegister: 'script',
+ registerType: 'autoUpdate',
devOptions: {
- enabled: false
+ enabled: false,
+ suppressWarnings: true
}
},
diff --git a/prisma.config.ts b/prisma.config.ts
index f96141d..1c6ce5c 100644
--- a/prisma.config.ts
+++ b/prisma.config.ts
@@ -1,11 +1,12 @@
import { defineConfig } from "prisma/config";
+// Призма 7 иногда требует явной загрузки .env для некоторых команд
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
- url: process.env["DATABASE_URL"],
+ url: process.env.DATABASE_URL || "postgresql://admin:secure_master_pass@localhost:5432/xflow_crm_db",
},
});
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7bb2650..df0ff5c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -8,20 +8,19 @@ datasource db {
}
model User {
- id String @id @default(cuid())
- email String @unique
- username String @unique
- password String
- role String @default("user")
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- profile Profile?
- canvases Canvas[]
- groupMembers GroupMember[]
+ id String @id @default(cuid())
+ email String @unique
+ username String @unique
+ password String
+ role String @default("user")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
activities Activity[]
- followers Follow[] @relation("following")
- following Follow[] @relation("follower")
+ canvases Canvas[]
+ following Follow[] @relation("follower")
+ followers Follow[] @relation("following")
+ groupMembers GroupMember[]
+ profile Profile?
}
model Profile {
@@ -34,8 +33,7 @@ model Profile {
followerCount Int @default(0)
followingCount Int @default(0)
joinedAt DateTime @default(now())
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Canvas {
@@ -48,22 +46,22 @@ model Canvas {
edges Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
-
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
- group Group? @relation(fields: [groupId], references: [id], onDelete: SetNull)
+ group Group? @relation(fields: [groupId], references: [id])
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
}
model Group {
- id String @id @default(cuid())
+ id String @id @default(cuid())
name String
- description String @default("")
- isPublic Boolean @default(true)
+ description String @default("")
+ isPublic Boolean @default(true)
ownerId String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- members GroupMember[]
- canvases Canvas[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ controlType String? @default("REMOTE")
+ canvases Canvas[]
+ members GroupMember[]
+ Kiosk Kiosk[]
}
model GroupMember {
@@ -72,9 +70,8 @@ model GroupMember {
groupId String
role String @default("member")
joinedAt DateTime @default(now())
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
+ group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, groupId])
}
@@ -87,8 +84,7 @@ model Activity {
targetType String
timestamp DateTime @default(now())
metadata String?
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Follow {
@@ -96,9 +92,19 @@ model Follow {
followerId String
followingId String
since DateTime @default(now())
-
- follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
- following User @relation("following", fields: [followingId], references: [id], onDelete: Cascade)
+ follower User @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
+ following User @relation("following", fields: [followingId], references: [id], onDelete: Cascade)
@@unique([followerId, followingId])
}
+
+model Kiosk {
+ id String @id
+ name String
+ location String?
+ groupId String
+ status String @default("offline")
+ lastSeen DateTime @default(now())
+ createdAt DateTime @default(now())
+ Group Group @relation(fields: [groupId], references: [id])
+}
diff --git a/scripts/fix_user.cjs b/scripts/fix_user.cjs
new file mode 100644
index 0000000..12ad748
--- /dev/null
+++ b/scripts/fix_user.cjs
@@ -0,0 +1,47 @@
+const { PrismaClient } = require('@prisma/client')
+const { PrismaPg } = require('@prisma/adapter-pg')
+const { Pool } = require('pg')
+
+const connectionString = "postgresql://admin:secure_master_pass@localhost:5432/xflow_crm_db"
+const pool = new Pool({ connectionString })
+const adapter = new PrismaPg(pool)
+const prisma = new PrismaClient({ adapter })
+
+async function main() {
+ const admins = ["ormix16@gmail.com", "almalexyz@gmail.com"]
+
+ for (const email of admins) {
+ const username = email.split('@')[0]
+ console.log(`🔧 Setting prefix as username for: ${email}...`)
+
+ const user = await prisma.user.upsert({
+ where: { email },
+ update: {
+ username: username,
+ role: 'admin'
+ },
+ create: {
+ id: `admin-init-${Math.random().toString(36).slice(2, 7)}`,
+ email: email,
+ username: username,
+ password: 'managed-auth',
+ role: 'admin',
+ profile: {
+ create: {
+ displayName: username,
+ avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`
+ }
+ }
+ }
+ })
+
+ console.log(`✅ ${email} updated. URL will be: /profile/${user.username}`)
+ }
+}
+
+main()
+ .catch(console.error)
+ .finally(async () => {
+ await prisma.$disconnect()
+ await pool.end()
+ })