const express = require('express') const multer = require('multer') const crypto = require('crypto') const fs = require('fs') const contentDisposition = require('content-disposition') const bot = require('./bot') const db = require('../include/db') const app = express.Router() const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10_000_000 // 10mb } }) function hasAccess(project_id, user_id) { return !!db .prepare(`select 1 from projects where id = :project_id and is_archived <> 1`) .pluck(true) .get({project_id}) && !!db .prepare(` select 1 from chat_users where user_id = :user_id and chat_id in (select id from chats where project_id = :project_id) and not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1) `) .pluck(true) .get({project_id, user_id}) } const sessions = {} app.use((req, res, next) => { if (req.path == '/auth') return next() const sid = req.query.sid || req.cookies.sid req.session = sessions[sid] if (!req.session) throw Error('ACCESS_DENIED::401') res.locals.user_id = req.session.user_id next() }) app.post('/auth', (req, res, next) => { db .prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`) .safeIntegers(true) .run(res.locals) const user_id = db .prepare(`select id from users where telegram_id = :telegram_id`) .safeIntegers(true) .pluck(true) .get(res.locals) const sid = crypto.randomBytes(64).toString('hex') req.session = sessions[sid] = {sid, user_id} res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/miniapp`]) res.locals.user_id = user_id res.status(200).json({success: true}) }) app.get('/project', (req, res, next) => { const rows = db .prepare(` select p.id, p.name, p.description, p.logo, p.is_logo_bg, company_id, c.name customer_name, c.upload_chat_id <> 0 has_upload from projects p inner join customers c on p.customer_id = c.id where p.id in ( select project_id from chats where id in (select chat_id from chat_users where user_id = :user_id) ) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1) and p.is_archived <> 1 `) .all(res.locals) rows.forEach(row => { row.is_logo_bg = Boolean(row.is_logo_bg) }) res.status(200).json({success: true, data: rows}) }) app.use('/project/:pid(\\d+)/*', (req, res, next) => { res.locals.project_id = parseInt(req.params.pid) if (!hasAccess(res.locals.project_id, res.locals.user_id)) throw Error('ACCESS_DENIED::401') const row = db .prepare('select customer_id, company_id from projects where id = :project_id') .get(res.locals) res.locals.customer_id = row.customer_id res.locals.customer_company_id = row.company_id next() }) function getUserCompanyId(user_id, project_id) { return db .prepare(` select company_id from company_users where user_id = :user_id and company_id in (select id from companies where project_id = :project_id) `) .pluck(true) .get({ user_id, project_id }) } app.get('/project/:pid(\\d+)/user', (req, res, next) => { const users = db .prepare(` with actuals (user_id) as ( select distinct user_id from chat_users where chat_id in (select id from chats where project_id = :project_id) and chat_id in (select chat_id from chat_users where user_id = :user_id) ), contributors (user_id) as ( select created_by from tasks where project_id = :project_id union select assigned_to from tasks where project_id = :project_id union select created_by from meetings where project_id = :project_id union select published_by from files where project_id = :project_id ), members (user_id, is_leave) as ( select user_id, 0 is_leave from actuals union all select user_id, 1 is_leave from contributors where user_id not in (select user_id from actuals) ) select u.id, u.telegram_id, u.username, u.firstname, u.lastname, u.photo, ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked, (select company_id from company_users where user_id = u.id and company_id in (select id from companies where project_id = :project_id)) company_id, m.is_leave from users u inner join members m on u.id = m.user_id left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id `) .safeIntegers(true) .all(res.locals) res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id) // Список компаний, которые НЕ ВИДНЫ компании пользователя на проекте const hidden = db .prepare(` select company_id from company_mappings where project_id = :project_id except select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id`) .pluck(true) .all(res.locals) users .filter(user => user.company_id) .filter(user => hidden.indexOf(user.company_id) != -1) .forEach(user => user.company_id = res.locals.customer_company_id) res.status(200).json({success: true, data: users}) }) app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => { const chatIds = db .prepare(`select id from chats where project_id = :project_id`) .all(res.locals) .map(e => e.id) const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) for (const chatId of chatIds) { await bot.reloadChatUsers(chatId) await sleep(1000) } res.status(200).json({success: true}) }) app.get('/project/:pid(\\d+)/company', (req, res, next) => { res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id) const rows = db .prepare(` select id, name, address, email, phone, site, description from companies where project_id = :project_id and ( id = :company_id or id in (select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id) or id not in (select company_id from company_mappings where project_id = :project_id) or (select :customer_company_id) ) `) .all(res.locals) res.status(200).json({success: true, data: rows}) }) // CHAT app.get('/project/:pid(\\d+)/chat', (req, res, next) => { const rows = db .prepare(` select id, name, invite_link, description, telegram_id, owner_id, user_count, logo, (select json_group_array(user_id) from chat_users where chat_id = c.id) users, (select count(1) from tasks where project_id = :project_id and chat_id = c.id) task_count from chats c where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id) `) .safeIntegers(true) .all(res.locals) rows.forEach(row => { row.users = JSON.parse(row.users) }) res.status(200).json({success: true, data: rows}) }) // TASK function getTask(id, user_id) { const row = db .prepare(` select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id, (select json_group_array(user_id) from task_users where task_id = t.id) observers, (select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files from tasks t where t.id = :id `) .get({id}) if (!row) throw Error('NOT_FOUND::404') row.close_file_ids = JSON.parse(row.close_file_ids) row.observers = JSON.parse(row.observers) row.files = JSON.parse(row.files) row.is_editable = row.created_by == user_id || row.assigned_to == user_id return row } app.get('/project/:pid(\\d+)/task', (req, res, next) => { const rows = db .prepare(` select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id, (select json_group_array(user_id) from task_users where task_id = t.id) observers, (select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files from tasks t where project_id = :project_id and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id) or exists(select 1 from chat_users where chat_id = t.chat_id)) `) .all(res.locals) rows.forEach(row => { row.close_file_ids = JSON.parse(row.close_file_ids) row.observers = JSON.parse(row.observers) row.files = JSON.parse(row.files) }) res.status(200).json({success: true, data: rows}) }) app.post('/project/:pid(\\d+)/task', (req, res, next) => { res.locals.name = req.body?.name res.locals.status = parseInt(req.body?.status) res.locals.priority = parseInt(req.body?.priority) res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined res.locals.create_date = Math.floor(Date.now() / 1000) res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to)) throw Error('INCORRECT_ASSIGNED_TO::400') if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals)) throw Error('INCORRECT_CHAT_ID::400') const id = db .prepare(` insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date) values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date) returning id `) .pluck(true) .get(res.locals) const task = getTask(id, res.locals.user_id) res.status(200).json({success: true, data: task}) }) app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => { res.locals.task_id = req.params.tid const task = db .prepare(` select created_by, assigned_to from tasks t where id = :task_id and project_id = :project_id and ( created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = :task_id and user_id = :user_id) or exists(select 1 from chat_users where chat_id = t.chat_id)) `) .get(res.locals) if (!task) throw Error('NOT_FOUND::404') res.locals.is_author = task.created_by == res.locals.user_id res.locals.is_assigned = task.assigned_to == res.locals.user_id next() }) app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { const task = getTask(req.params.tid, res.locals.user_id) res.status(200).json({success: true, data: task}) }) app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { if (!res.locals.is_author && !res.locals.is_assigned) throw Error('ACCESS_DENIED::401') res.locals.id = res.locals.task_id res.locals.name = req.body?.name res.locals.status = parseInt(req.body?.status) res.locals.priority = parseInt(req.body?.priority) res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals)) throw Error('INCORRECT_CHAT_ID::400') const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_file_ids', 'chat_id'] : ['status', 'time_spent', 'close_comment', 'json_close_file_ids'] const info = db .prepareUpdate('tasks', columns, res.locals, ['id', 'project_id']) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') const task = getTask(res.locals.task_id, res.locals.user_id) res.status(200).json({success: true, data: task}) }) app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { if (!res.locals.is_author) throw Error('ACCESS_DENIED::401') const info = db .prepare(`delete from tasks where id = :task_id and project_id = :project_id and created_by = :user_id`) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: {id: res.locals.task_id}}) }) app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => { if (!res.locals.is_author && !res.locals.is_assigned) throw Error('ACCESS_DENIED::401') const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : [] // Проверка, что выбранные пользователи имеют доступ к проекту let rows = db .prepare(` select user_id from chat_users where chat_id in (select id from chats where project_id = :project_id) `) .pluck(true) .all(res.locals) if (user_ids.some(user_id => !rows.contains(user_id))) throw Error('INACCESSABLE_USER::400') res.locals.json_ids = JSON.stringify(user_ids) db .prepare(` delete from task_users where task_id = :task_id; insert into task_users (task_id, user_id) select :task_id, value from json_each(:json_ids) `) .run(res.locals) res.status(200).json({success: true}) }) // MEETINGS function getMeeting(id, user_id) { const row = db .prepare(` select id, name, description, place, created_by, meet_date, chat_id, is_cancel, (select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants, (select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files from meetings m where m.id = :id `) .get({id}) if (!row) throw Error('NOT_FOUND::404') row.participants = JSON.parse(row.participants) row.files = JSON.parse(row.files) row.is_editable = row.created_by == user_id return row } app.get('/project/:pid(\\d+)/meeting', (req, res, next) => { const rows = db .prepare(` select id, name, description, place, created_by, meet_date, duration, chat_id, is_cancel, (select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants, (select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files, created_by = :user_id is_editable from meetings m where project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id)) `) .all(res.locals) rows.forEach(row => { row.participants = JSON.parse(row.participants) row.files = JSON.parse(row.files) row.is_editable = Boolean(row.is_editable) }) res.status(200).json({success: true, data: rows}) }) app.post('/project/:pid(\\d+)/meeting', (req, res, next) => { res.locals.name = req.body?.name res.locals.description = req.body?.description res.locals.place = req.body?.place res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined res.locals.duration = req.body?.duration ? parseInt(req.body?.duration) : undefined res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals)) throw Error('INCORRECT_CHAT_ID::400') const id = db .prepare(` insert into meetings (project_id, name, description, place, created_by, meet_date, duration) values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration) returning id `) .pluck(true) .get(res.locals) const meeting = getMeeting(id, res.locals.user_id) res.status(200).json({success: true, data: meeting}) }) app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => { res.locals.meeting_id = req.params.mid const meeting = db .prepare(` select created_by from meetings where id = :meeting_id and project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = :meeting_id and user_id = :user_id)) `) .get(res.locals) if (!meeting) throw Error('NOT_FOUND::404') res.locals.is_author = meeting.created_by == res.locals.user_id next() }) app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { const meeting = getMeeting(req.params.mid, res.locals.user_id) res.status(200).json({success: true, data: meeting}) }) app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { if (!res.locals.is_author) throw Error('ACCESS_DENIED::401') res.locals.id = res.locals.meeting_id res.locals.name = req.body?.name res.locals.description = req.body?.description res.locals.place = req.body?.place res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined res.locals.is_cancel = +!!req.body?.is_cancel if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals)) throw Error('INCORRECT_CHAT_ID::400') const info = db .prepareUpdate('meetings', ['name', 'description', 'place', 'meet_date', 'chat_id', 'is_cancel'], res.locals, ['id', 'project_id']) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') const meeting = getMeeting(res.locals.meeting_id, res.locals.user_id) res.status(200).json({success: true, data: meeting}) }) app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { if (!res.locals.is_author) throw Error('ACCESS_DENIED::401') const info = db .prepare(`delete from meetings where id = :meeting_id and project_id = :project_id and created_by = :user_id`) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: {id: res.locals.meeting_id}}) }) app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (req, res, next) => { if (!res.locals.is_author) throw Error('ACCESS_DENIED::401') const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : [] // Проверка, что выбранные пользователи имеют доступ к проекту let rows = db .prepare(` select user_id from chat_users where chat_id in (select id from chats where project_id = :project_id) `) .pluck(true) .all(res.locals) if (user_ids.some(user_id => rows.indexOf(user_id)) == -1) throw Error('INACCESSABLE_USER::400') db .prepare(`delete from meeting_users where meeting_id = :meeting_id`) .run(res.locals) res.locals.json_ids = JSON.stringify(user_ids) db .prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`) .run(res.locals) res.status(200).json({ success: true, data: user_ids }) }) // FILES app.get('/project/:pid(\\d+)/file', (req, res, next) => { // 1. Из групп, которые есть в проекте и в которых участвует пользователь // 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель // 3. Из встреч на проекте, где пользователь создатель или участник const rows = db .prepare(` select f.id, f.chat_id, c.telegram_id telegram_chat_id, f.message_id, f.filename, f.mime, f.caption, f.size, f.published_by, f.published, f.parent_id, f.parent_type from files f left join chats c on f.chat_id = c.id and f.parent_type = 0 where f.project_id = :project_id and ( chat_id in (select chat_id from chat_users where user_id = :user_id) or parent_type = 1 and parent_id in ( select id from tasks t where project_id = :project_id and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id)) ) or parent_type = 2 and parent_id in ( select id from meetings m where project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id)) ) ) `) .safeIntegers(true) .all(res.locals) res.status(200).json({success: true, data: rows}) }) app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => { res.locals.parent_id = req.params.id res.locals.parent_type = req.params.type == 'task' ? 1 : 2 const chat_id = db .prepare(` select coalesce(chat_id, (select upload_chat_id from customers where id = :customer_id)) from ${req.params.type}s where id = :parent_id and project_id = :project_id`) .pluck(true) .get(res.locals) if (!chat_id) throw Error('EMPTY_DESTINATION::500') const file_ids = [] for (const file of req.files) { if (file.size == 0) continue const filedata = { project_id: req.params.pid, chat_id, filename: file.originalname, mime: file.mimetype, data: file.buffer, size: file.size, published_by: res.locals.user_id, pablished: Math.floor(Date.now() / 1000), parent_type: res.locals.parent_type, parent_id: req.params.id } const file_id = await bot.sendFile(filedata) if (file_id) file_ids.push(file_id) } if (file_ids.length == 0) throw Error('EMPTY_UPLOAD::500') const files = db .prepare(`select id, chat_id, message_id, filename, mime, size, published_by, published from files where id in (` + file_ids.join(',') + `)`) .all() res.status(200).json({success: true, data: files}) }) app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => { res.locals.file_id = req.params.fid const file = db .prepare(`select * from files where id = :file_id and project_id = :project_id`) .get(res.locals) if (!file) throw Error('NOT_FOUND::404') if (file.parent_type == 0) { res.locals.chat_id = file.chat_id const row = db .prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`) .get(res.locals) if (row) { res.locals.can_download = true } } else { res.locals.parent_id = file.parent_id const parent = file.parent_type == 1 ? 'task' : 'meeting' const row = db .prepare(` select 1 from ${parent}s where id = :parent_id and project_id = :project_id or exists(select 1 from ${parent}_users where ${parent}_id = :parent_id and user_id = :user_id) `) .get(res.locals) if (row) { res.locals.can_download = true res.locals.can_delete = file.published_by == res.locals.user_id } } next() }) app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => { if (!res.locals.can_download) throw Error('NOT_FOUND::404') const file = await bot.downloadFile(res.locals.project_id, res.locals.file_id) res.writeHead(200, { 'Content-Length': file.size, 'Content-Type': file.mime, 'Content-Disposition': contentDisposition(file.filename) }) res.end(file.data) }) app.delete('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => { if (!res.locals.can_delete) throw Error('NOT_FOUND::404') const info = db .prepare(`delete from files where id = :id and project_id = :project_id`) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: {id: res.locals.file_id}}) }) app.get('/settings', (req, res, next) => { const row = db .prepare(`select coalesce(json_settings, '{}') from users where id = :user_id`) .pluck(true) .get(res.locals) res.status(200).json({success: true, data: JSON.parse(row)}) }) app.put('/settings', (req, res, next) => { res.locals.json_settings = JSON.stringify(req.body || {}) const row = db .prepare(`update users set json_settings = :json_settings where id = :user_id`) .run(res.locals) res.status(200).json({success: true}) }) module.exports = app