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 group_users where user_id = :user_id and group_id in (select id from groups 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) and not exists(select 1 from projects where id = :project_id and is_deleted = 1) `) .get({project_id, user_id}) } const sessions = {} app.use((req, res, next) => { if (req.path == '/user/login') 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('/user/login', (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=/`]) res.locals.user_id = user_id res.status(200).json({success: true}) }) app.get('/project', (req, res, next) => { const where = req.query.id ? ' and p.id = ' + parseInt(req.query.id) : '' const rows = db .prepare(` select p.id, p.name, p.description, p.logo, c.name customer_name, c.upload_group_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 groups where id in (select group_id from group_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) ${where} and is_deleted <> 1 `) .all(res.locals) if (where && rows.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: where ? rows[0] : rows}) }) app.get('/project/:pid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project?id=${req.params.pid}`) }) 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 from projects where id = :project_id') .get(res.locals) res.locals.customer_id = row.customer_id next() }) app.get('/project/:pid(\\d+)/user', (req, res, next) => { const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : '' const users = db .prepare(` with actuals (user_id) as ( select distinct user_id from group_users where group_id in (select id from groups where project_id = :project_id) and group_id in (select group_id from group_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 documents 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, u.json_phone_projects, ud.fullname, 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 where 1 = 1 ${where} `) .all(res.locals) const companies = db .prepare('select id, name, email, phone, site, description from companies where project_id = :project_id') .all(res.locals) .reduce((companies, row) => { companies[row.id] = row return companies }, {}) const mappings = {} const company_id = users.find(m => m.id == res.locals.user_id).company_id if (company_id) { res.locals.company_id = company_id db .prepare('select show_as_id, show_to_id from company_mappings where project_id = :project_id and company_id = :company_id') .all(res.locals) .forEach(row => mappings[row.show_to_id] = row.show_to_id) } users.forEach(m => { m.company = companies[mappings[m.company_id] || m.company_id] delete m.company_id }) users.forEach(m => { const isHide = JSON.parse(m.json_phone_projects || []).indexOf(res.locals.project_id) == -1 if (isHide) delete m.phone delete m.json_phone_projects }) if (where && users.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: where ? users[0] : users}) }) app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => { const groupIds = db .prepare(`select id from groups where project_id = :project_id`) .all(res.locals) .map(e => e.id) const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) for (const groupId of groupIds) { await bot.reloadGroupUsers(groupId) await sleep(1000) } res.status(200).json({success: true}) }) app.get('/project/:pid(\\d+)/group', (req, res, next) => { const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' const rows = db .prepare(` select id, name, telegram_id from groups where project_id = :project_id and id in (select group_id from group_users where user_id = :user_id) ${where} `) .all(res.locals) if (where && rows.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: where ? rows[0] : rows}) }) app.get('/project/:pid(\\d+)/group/:gid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.gid}`) }) // TASK app.get('/project/:pid(\\d+)/task', (req, res, next) => { const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : '' const rows = db .prepare(` select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date, (select json_group_array(user_id) from task_users where task_id = t.id) observers, (select json_group_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments 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)) ${where} `) .all(res.locals) rows.forEach(row => { row.observers = JSON.parse(row.observers) row.attachments = JSON.parse(row.attachments) }) if (where && rows.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: where ? rows[0] : rows}) }) app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project/${req.params.pid}/task?id=${req.params.tid}`) }) 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 if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to)) throw Error('INCORRECT_ASSIGNED_TO::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) res.status(200).json({success: true, data: id}) }) 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 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)) `) .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.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 const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent'] : ['status', 'time_spent'] const info = db .prepareUpdate('tasks', columns, res.locals, ['id', 'project_id']) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true}) }) 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}) }) 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 group_users where group_id in (select id from groups 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 app.get('/project/:pid(\\d+)/meeting', (req, res, next) => { const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : '' const rows = db .prepare(` select id, name, description, created_by, meet_date, (select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants, (select json_group_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments 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)) ${where} `) .all(res.locals) rows.forEach(row => { row.participants = JSON.parse(row.participants) row.attachments = JSON.parse(row.attachments) }) if (where && rows.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: where ? rows[0] : rows}) }) app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project/${req.params.pid}/meeting?id=${req.params.mid}`) }) app.post('/project/:pid(\\d+)/meeting', (req, res, next) => { res.locals.name = req.body?.name res.locals.description = req.body?.description res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined const id = db .prepare(` insert into meetings (project_id, name, description, created_by, meet_date) values (:project_id, :name, :description, :user_id, :meet_date) returning id `) .pluck(true) .get(res.locals) res.status(200).json({success: true, data: id}) }) 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.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.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined const info = db .prepareUpdate('meetings', ['name', 'description', 'meet_date'], res.locals, ['id', 'project_id']) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true}) }) 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}) }) app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (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 group_users where group_id in (select id from groups where project_id = :project_id) `) .pluck(true) // .raw? .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}) }) // DOCUMENTS app.get('/project/:pid(\\d+)/document', (req, res, next) => { const ids = String(req.query.id).split(',').map(e => parseInt(e)).filter(e => e > 0) const where = ids.length > 0 ? ' and id in (' + ids.join(', ') + ')' : '' // Документы // 1. Из групп, которые есть в проекте и в которых участвует пользователь // 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель // 3. Из встреч на проекте, где пользователь создатель или участник // To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId const rows = db .prepare(` select id, origin_group_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type from documents d where project_id = :project_id ${where} and ( origin_group_id in (select group_id from group_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)) ) ) `) .all(res.locals) if (where && rows.length == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows}) }) app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => { const parentType = req.params.type == 'task' ? 1 : 2 const parentId = req.params.id const ids = [] for (const file of req.files) { const id = await bot.uploadDocument(res.locals.project_id, file.originalname, file.mimetype, file.buffer, parentType, parentId, res.locals.user_id) ids.push(id) } res.redirect(req.baseUrl + `/project/${req.params.pid}/document?id=` + ids.join(',')) }) app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => { res.locals.document_id = req.params.did const doc = db .prepare(`select * from documents where id = :document_id and project_id = :project_id`) .get(res.locals) if (!doc) throw Error('NOT_FOUND::404') if (doc.parent_type == 0) { res.locals.group_id = doc.group_id const row = db .prepare(`select 1 from group_users where group_id = :group_id and user_id = :user_id`) .get(res.locals) if (row) { res.locals.can_download = true } } else { res.locals.parent_id = doc.parent_id const parent = doc.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 = doc.published_by == res.locals.user_id } } next() }) app.get('/project/:pid(\\d+)/document/:did(\\d+)', async (req, res, next) => { if (!res.locals.can_download) throw Error('NOT_FOUND::404') const file = await bot.downloadDocument(res.locals.project_id, res.locals.document_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+)/document/:id(\\d+)', (req, res, next) => { if (!res.locals.can_delete) throw Error('NOT_FOUND::404') const doc = db .prepare(`delete from documents where id = :id and project_id = :project_id`) .run(res.locals) res.status(200).json({success: true}) }) 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