before delete 3software

This commit is contained in:
2025-06-29 18:55:59 +03:00
parent ebd77a3e66
commit b51a472738
147 changed files with 257326 additions and 3151 deletions

View File

@@ -1,15 +1,27 @@
const crypto = require('crypto')
const express = require('express')
const WebSocket = require('ws')
const db = require('../include/db')
const log = require('../include/log')
const eventBus = require('../include/eventbus')
const bot = require('./bot')
const fs = require('fs')
const app = express.Router()
const sessions = {}
function registerWS(sid, ws) {
const session = sessions[sid]
if (session)
session.ws = ws
return !!session
}
const cache = {
// email -> code
register: {},
upgrade: {},
recovery: {},
'change-password': {},
'change-email': {},
@@ -42,8 +54,8 @@ app.use((req, res, next) => {
if (public.includes(req.path))
return next()
const asid = req.query.asid || req.cookies.asid
req.session = sessions[asid]
const sid = req.query.sid || req.cookies.sid
req.session = sessions[sid]
if (!req.session)
throw Error('ACCESS_DENIED::401')
@@ -57,9 +69,9 @@ function createSession(req, res, customer_id) {
throw Error('AUTH_ERROR::500')
res.locals.customer_id = customer_id
const asid = crypto.randomBytes(64).toString('hex')
req.session = sessions[asid] = {asid, customer_id }
res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`])
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, customer_id }
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/admin`])
}
app.post('/auth/email', (req, res, next) => {
@@ -67,7 +79,7 @@ app.post('/auth/email', (req, res, next) => {
res.locals.password = req.body?.password
const customer_id = db
.prepare(`select id from customers where is_blocked = 0 and email = :email and password is not null and password = :password `)
.prepare(`select id from customers where email = :email and password is not null and password = :password `)
.pluck(true)
.get(res.locals)
@@ -75,7 +87,7 @@ app.post('/auth/email', (req, res, next) => {
throw Error('AUTH_ERROR::401')
createSession(req, res, customer_id)
res.status(200).json({success: true})
res.status(200).json({success: true })
})
app.post('/auth/telegram', (req, res, next) => {
@@ -83,24 +95,25 @@ app.post('/auth/telegram', (req, res, next) => {
.prepare(`select id from customers where telegram_id = :telegram_id`)
.pluck(true)
.get(res.locals) || db
.prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`)
.prepare(`insert into customers (telegram_id) values (:telegram_id) returning id`)
.pluck(true)
.get(res.locals)
createSession(req, res, customer_id)
res.status(200).json({success: true})
res.status(200).json({ success: true })
})
/*
Регистрация нового клиента выполняется за ТРИ последовательных вызова
Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
*/
app.post('/auth/email/register', (req, res, next) => {
app.post('/auth/email/:action(register|upgrade)', (req, res, next) => {
const email = String(req.body.email ?? '').trim()
const code = String(req.body.code ?? '').trim()
const password = String(req.body.password ?? '').trim()
const action = req.params.action
const stepNo = email && !code ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1
if (stepNo == -1)
@@ -119,12 +132,12 @@ app.post('/auth/email/register', (req, res, next) => {
throw Error('USED_EMAIL::400')
const code = Math.random().toString().substr(2, 4)
cache.register[email] = code
sendEmail(email, 'REGISTER', `${email} => ${code}`)
cache[action][email] = code
sendEmail(email, action.toUpperCase(), `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache.register[email] != code)
if (cache[action][email] != code)
throw Error('INCORRECT_CODE::400')
}
@@ -132,11 +145,11 @@ app.post('/auth/email/register', (req, res, next) => {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
db
.prepare('insert into customers (email, password) values (:email, :password)')
.run({email, password})
const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' :
'update customers set email = :email, password = :password, telegram_id = null where id = :id'
db.prepare(query).run({email, password, id: res.locals.customer_id})
delete cache.register[email]
delete cache[action][email]
}
res.status(200).json({success: true})
@@ -144,23 +157,25 @@ app.post('/auth/email/register', (req, res, next) => {
/*
Смена email выполняется за ЧЕТЫРЕ последовательных вызовов
Смена email выполняется за ПЯТЬ последовательных вызовов
1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код.
2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email.
4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет email пользователя на новый и возвращает ОК.
4. Отправлются оба кода и новый email. Если коды проходят проверку, то сервер отвечает ОК.
5. Отправлются оба кода, новые email и password. Если они проходят проверку, то сервер меняет email и пароль пользователя на новый и возвращает ОК.
*/
app.post('/auth/email/change-email', (req, res, next) => {
const email2 = String(req.body.email ?? '').trim()
const code = String(req.body.code ?? '').trim()
const code2 = String(req.body.code2 ?? '').trim()
const password = String(req.body.password ?? '').trim()
const email = db
.prepare('select email from customers where id = :customer_id')
.pluck(true)
.get(res.locals)
const stepNo = !code ? 1 : code && !email ? 2 : code && email && !code2 ? 3 : code && email && code2 ? 4 : -1
const stepNo = !code ? 1 : code && !email2 ? 2 : code && email2 && !code2 ? 3 : code && email2 && code2 && !password ? 4 : code && email2 && code2 && password ? 5 : -1
if (stepNo == -1)
throw Error('BAD_STEP::400')
@@ -187,9 +202,17 @@ app.post('/auth/email/change-email', (req, res, next) => {
if (stepNo == 4) {
if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 5) {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
res.locals.email = email2
res.locals.password = password
const info = db
.prepare('update customers set email = :email where id = :customer_id')
.prepare('update customers set email = :email, password = :password where id = :customer_id')
.run(res.locals)
if (info.changes == 0)
@@ -210,7 +233,7 @@ app.post('/auth/email/change-email', (req, res, next) => {
*/
app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
const code = String(req.body.code ?? '').trim()
const password = String(req.body.password)
const password = String(req.body.password ?? '').trim()
const action = req.params.action
const email = action == 'change-password' ? db
@@ -260,18 +283,21 @@ app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
})
app.get('/auth/logout', (req, res, next) => {
if (req.session?.asid)
delete sessions[req.session.asid]
if (req.session?.sid)
delete sessions[req.session.sid]
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`])
res.setHeader('Set-Cookie', [`sid=; expired; httpOnly;path=/api/admin`])
res.status(200).json({success: true})
})
// CUSTOMER
app.get('/customer/profile', (req, res, next) => {
res.locals.time = Math.floor(Date.now() / 1000)
const row = db
.prepare(`
select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_chat_id
select id, name, email, plan,
coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company,
upload_chat_id, generate_key(-id, :time) upload_token
from customers
where id = :customer_id
`)
@@ -298,6 +324,8 @@ app.get('/customer/profile', (req, res, next) => {
app.put('/customer/profile', (req, res, next) => {
if (req.body.company instanceof Object)
req.body.json_company = JSON.stringify(req.body.company)
else
delete req.body?.json_company
const info = db
.prepareUpdate(
@@ -333,96 +361,86 @@ app.put('/customer/settings', (req, res, next) => {
})
// PROJECT
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
function getProject(id, customer_id) {
const row = db
.prepare(`
select id, name, description, logo, is_logo_bg, is_archived,
select id, name, description, logo, is_logo_bg, company_id, is_archived,
(select count(*) from chats where project_id = p.id) chat_count,
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
from projects p
where customer_id = :customer_id ${where}
where customer_id = :customer_id and p.id = :id
order by name
`)
.get({id, customer_id})
if (!row)
throw Error('NOT_FOUND::404')
row.is_archived = Boolean(row.is_archived)
row.is_logo_bg = Boolean(row.is_logo_bg)
return row
}
app.get('/project', (req, res, next) => {
const data = db
.prepare(`
select id, name, description, logo, is_logo_bg, company_id, is_archived,
(select count(*) from chats where project_id = p.id) chat_count,
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
from projects p
where customer_id = :customer_id
order by name
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
data.forEach(row => {
row.is_archived = Boolean(row.is_archived)
row.is_logo_bg = Boolean(row.is_logo_bg)
})
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}`)
res.status(200).json({success: true, data})
})
app.post('/project', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
const id = db
res.locals.project_id = db
.prepare(`
insert into projects (customer_id, name, description, logo)
values (:customer_id, :name, :description, :logo)
insert into projects (customer_id, name, description, logo, is_logo_bg)
values (:customer_id, :name, :description, :logo, :is_logo_bg)
returning id
`)
.pluck(true)
.get(res.locals)
res.redirect(req.baseUrl + `/project?id=${id}`)
})
app.put('/project/:pid(\\d+)', (req, res, next) => {
res.locals.id = req.params.pid
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
res.locals.is_logo_bg = req.body?.is_logo_bg
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo', 'is_logo_bg'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => {
res.locals.id = req.params.pid
res.locals.is_archived = +(req.params.action == 'archive')
const info = db
.prepare(`
update projects
set is_archived = :is_archived
where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived
`)
.run(res.locals)
if (info.changes == 0)
throw Error('BAD_REQUEST::400')
const chatIds = db
.prepare(`select id from chats where project_id = :id`)
const json_company = db
.prepare(`select coalesce(json_company, '{}') from customers where id = :customer_id`)
.pluck(true)
.all(res.locals)
.get(res.locals)
for (const chatId of chatIds) {
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
res.locals.company_id = addCompany(Object.assign({
name: 'My Company',
address: null,
email: null,
phone: null,
site: null,
description: null,
logo: null
}, JSON.parse(json_company), {project_id: res.locals.project_id}))
db
.prepare(`update projects set company_id = :company_id where id = :project_id`)
.run(res.locals)
const data = getProject(res.locals.project_id, res.locals.customer_id)
res.status(200).json({success: true, data})
})
app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
app.use ('(/project/:pid(\\d+)/*|/project/:pid(\\d+))', (req, res, next) => {
res.locals.project_id = parseInt(req.params.pid)
const row = db
@@ -435,55 +453,138 @@ app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
next()
})
// USER
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
app.get('/project/:pid(\\d+)', (req, res, next) => {
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
const rows = db
app.put('/project/:pid(\\d+)', (req, res, next) => {
res.locals.id = req.params.pid
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo', 'is_logo_bg'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => {
try {
res.locals.id = req.params.pid
res.locals.is_archived = +(req.params.action == 'archive')
const info = db
.prepare(`
update projects
set is_archived = :is_archived
where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived
`)
.run(res.locals)
if (info.changes == 0)
throw Error('BAD_REQUEST::400')
const chat_ids = db
.prepare(`select id from chats where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const chat_id of chat_ids) {
await bot.sendMessage(chat_id, res.locals.is_archived ? 'ON_PROJECT_ARCHIVE' : 'ON_PROJECT_RESTORE')
}
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
} catch (err) {
next(err)
}
})
// USER
function getUser(id, project_id) {
const row = db
.prepare(`
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
ud.fullname, ud.role, ud.department, ud.is_blocked
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
(select company_id from company_users where user_id = :id) company_id,
(select json_group_array(chat_id) from chat_users where user_id = :id and chat_id in (select id from chats where project_id = :project_id)) chats
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id = :id
`)
.safeIntegers(true)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.chats = JSON.parse(row.chats || '[]')
row.is_blocked = Boolean(row.is_blocked)
return row
}
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const data = db
.prepare(`
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, 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) company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id in (
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
) ${where}
)
`)
.safeIntegers(true)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
data.forEach(row => {
row.is_blocked = Boolean(row.is_blocked)
})
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`)
const data = getUser(req.params.uid, req.params.pid)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
res.locals.user_id = parseInt(req.params.uid)
res.locals.fullname = req.body?.fullname
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.role = req.body?.role
res.locals.department = req.body?.department
res.locals.is_blocked = req.body?.is_blocked
res.locals.is_blocked = 'is_blocked' in req.body ? +req.body.is_blocked : undefined
const info = db
.prepareUpdate('user_details',
['fullname', 'role', 'department', 'is_blocked'],
.prepareUpsert('user_details',
['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'],
res.locals,
['user_id', 'project_id']
)
.all(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
.run(res.locals)
res.status(200).json({success: true})
const data = getUser(req.params.uid, req.params.pid)
res.status(200).json({ success: true, data })
})
app.get('/project/:pid(\\d+)/token', (req, res, next) => {
@@ -497,67 +598,92 @@ app.get('/project/:pid(\\d+)/token', (req, res, next) => {
if (!key)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: key})
res.status(200).json({ success: true, data: key })
})
// COMPANY
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
function addCompany(data) {
return db
.prepare(`
select id, name, email, phone, description, logo,
(select json_chat_array(user_id) from company_users where company_id = c.id) users
insert into companies (project_id, name, address, email, phone, site, description, logo)
values (:project_id, :name, :address, :email, :phone, :site, :description, :logo)
returning id
`)
.pluck(true)
.get(data)
}
function getCompany(id, project_id) {
const row = db
.prepare(`
select id, name, address, email, phone, site, description, logo,
(select json_group_array(user_id) from company_users where company_id = c.id) users
from companies c
where project_id = :project_id ${where}
where c.id = :id and project_id = :project_id
order by name
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.users = JSON.parse(row.users || '[]')
return row
}
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const data = db
.prepare(`
select id, name, address, email, phone, site, description, logo,
(select company_id = c.id from projects where id = :project_id) is_own,
(select json_group_array(user_id) from company_users where company_id = c.id) users
from companies c
where project_id = :project_id
order by name
`)
.all(res.locals)
rows.forEach(row => row.users = JSON.parse(row.users || '[]'))
data.forEach(row => {
row.users = JSON.parse(row.users || '[]')
})
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
const data = getCompany(req.params.cid, req.params.pid)
res.status(200).json({success: true, data})
})
app.post('/project/:pid(\\d+)/company', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.address = req.body?.address
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.site = req.body?.site
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const id = db
.prepare(`
insert into companies (project_id, name, email, phone, site, description, logo)
values (:project_id, :name, :email, :phone, :site, :description, :logo)
returning id
`)
.pluck(res.locals)
.get(res.locals)
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${id}`)
const id = addCompany(res.locals)
const data = getCompany(id, req.params.pid)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.locals.id = parseInt(req.params.cid)
res.locals.name = req.body?.name
res.locals.address = req.body?.address
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.site = req.body?.site
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const info = db
.prepareUpdate(
'companies',
['name', 'email', 'phone', 'site', 'description'],
['name', 'address', 'email', 'phone', 'site', 'description', 'logo'],
res.locals,
['id', 'project_id'])
.run(res.locals)
@@ -565,55 +691,24 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
const data = getCompany(req.params.cid, req.params.pid)
res.status(200).json({success: true, data})
})
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.locals.company_id = parseInt(req.params.cid)
res.locals.company_id = req.params.cid
const info = db
.prepare(`delete from companies where id = :company_id and project_id = :project_id`)
.prepare(`
delete from companies
where id = :company_id and project_id = :project_id and
not exists(select company_id from projects where id = :project_id)`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, telegram_id, is_channel, user_count, bot_can_ban
from chats
where project_id = :project_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+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.uid}`)
})
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
res.locals.chat_id = parseInt(req.params.gid)
const info = db
.prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: req.params.cid}})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
@@ -655,7 +750,6 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
if (user_ids.some(user_id => !rows.contains(user_id)))
throw Error('USED_MEMBER::400')
db
.prepare(`delete from company_users where company_id = :company_id`)
.run(res.locals)
@@ -670,4 +764,140 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
res.status(200).json({success: true})
})
module.exports = app
app.get('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
const data = db
.prepare(`
select company_id, json_group_array(show_to_id) show_to_ids
from company_mappings
where project_id = :project_id and company_id <> show_to_id
`)
.all(res.locals)
data.forEach(row => {
row.show_to_ids = JSON.parse(row.show_to_ids || '[]')
})
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
if(!(req.body instanceof Array))
throw Error('ARRAY_REQUIRED::500')
db
.prepare(`delete from company_mappings where project_id = :project_id`)
.run(res.locals)
req.body
.filter(row => Number.isInteger(row.company_id) && row.show_to_ids instanceof Array && row.show_to_ids.every(id => Number.isInteger(id)))
.forEach(row => {
row.show_to_ids.push(row.company_id)
const json_ids = row.show_to_ids.join(', ')
const check = db
.prepare(`select count(1) from companies where project_id = :project_id and id in (${json_ids}) `)
.get(res.locals)
if (check.count)
return log.error (Error('IGNORE: ' + JSON.stringify(row)))
const locals = {
project_ids: res.locals.project_id,
company_id: row.company_id,
json_ids
}
db
.prepare(`
insert into company_mappings (project_id, company_id, show_to_id) values
select :project_ids, :company_id, value from json_each(:json_ids)
`)
.run(locals)
})
res.status(200).json({ success: true })
})
// CHATS
function getChat(id, project_id) {
const row = db
.prepare(`
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
from chats
where id = :id and project_id = :project_id
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.is_channel = Boolean(row.is_channel)
row.bot_can_ban = Boolean(row.bot_can_ban)
return row
}
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const data = db
.prepare(`
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
from chats
where project_id = :project_id
`)
.all(res.locals)
data.forEach(row => {
row.is_channel = Boolean(row.is_channel)
row.bot_can_ban = Boolean(row.bot_can_ban)
})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
const data = getChat(req.params.gid, req.params.pid)
res.status(200).json({ success: true, data })
})
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
try {
res.locals.chat_id = req.params.gid
const info = db
.prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
await bot.sendMessage(res.locals.chat_id, 'ON_CHAT_REMOVE')
res.status(200).json({success: true, data: {id: req.params.gid}})
} catch (err){
next(err)
}
})
eventBus.on('chat-attached', chat => {
const customer_id = db
.prepare(`select customer_id from projects where id = :project_id`)
.pluck(true)
.get(chat)
if (!customer_id)
return
const msg = {
event: 'chat-attached',
entity: 'chat',
id: chat.id,
data: getChat(chat.id, chat.project_id)
}
Object.values(sessions)
.filter(s => s.customer_id == customer_id && s.ws?.readyState === WebSocket.OPEN)
.map(s => s.ws)
.forEach(ws => ws.send(JSON.stringify(msg)))
})
module.exports = { router: app, registerWS }

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
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 log = require('../include/log')
const eventBus = require('../include/eventbus')
const app = express.Router()
const upload = multer({
@@ -15,23 +16,47 @@ const upload = multer({
}
})
const sessions = {}
function registerWS(sid, ws) {
const session = sessions[sid]
if (session)
session.ws = ws
return !!session
}
function hasAccess(project_id, user_id) {
return !!db
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) and
not exists(select 1 from projects where id = :project_id and is_deleted = 1)
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 = {}
function getUserInfo (user_id, project_id) {
const user = db
.prepare(`
select u.telegram_id, coalesce(ud.fullname, coalesce(u.lastname, '') || ' ' || coalesce(u.firstname, '')) name
from users u left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where u.id = :user_id
`)
.safeIntegers(true)
.get({ user_id, project_id })
return user
}
app.use((req, res, next) => {
if (req.path == '/user/login')
if (req.path == '/auth')
return next()
const sid = req.query.sid || req.cookies.sid
@@ -43,8 +68,7 @@ app.use((req, res, next) => {
next()
})
app.post('/user/login', (req, res, next) => {
app.post('/auth', (req, res, next) => {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
@@ -58,18 +82,16 @@ app.post('/user/login', (req, res, next) => {
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, user_id}
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/`])
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/miniapp`])
res.locals.user_id = user_id
res.status(200).json({success: true})
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,
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
@@ -78,18 +100,15 @@ app.get('/project', (req, res, next) => {
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)
${where} and is_deleted <> 1
and p.is_archived <> 1
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
rows.forEach(row => {
row.is_logo_bg = Boolean(row.is_logo_bg)
})
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}`)
res.status(200).json({success: true, data: rows})
})
app.use('/project/:pid(\\d+)/*', (req, res, next) => {
@@ -99,17 +118,27 @@ app.use('/project/:pid(\\d+)/*', (req, res, next) => {
throw Error('ACCESS_DENIED::401')
const row = db
.prepare('select customer_id from projects where id = :project_id')
.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()
})
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : ''
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 (
@@ -125,7 +154,7 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
union
select created_by from meetings where project_id = :project_id
union
select published_by from documents where project_id = :project_id
select published_by from files where project_id = :project_id
),
members (user_id, is_leave) as (
select user_id, 0 is_leave from actuals
@@ -138,8 +167,9 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
u.firstname,
u.lastname,
u.photo,
u.json_phone_projects,
ud.fullname,
ud.email,
ud.phone,
ud.role,
ud.department,
ud.is_blocked,
@@ -150,138 +180,189 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
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}
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
`)
.safeIntegers(true)
.all(res.locals)
const companies = db
.prepare('select id, name, email, phone, site, description from companies where project_id = :project_id')
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)
.reduce((companies, row) => {
companies[row.id] = row
return companies
}, {})
users
.filter(user => user.company_id)
.filter(user => hidden.indexOf(user.company_id) != -1)
.forEach(user => user.company_id = res.locals.customer_company_id)
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})
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)
try {
const chat_ids = 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))
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
for (const chatId of chatIds) {
await bot.reloadGroupUsers(chatId)
await sleep(1000)
}
for (const chat_id of chat_ids) {
await bot.reloadChatUsers(chat_id)
await sleep(1000)
}
res.status(200).json({success: true})
res.status(200).json({success: true})
} catch (err) {
next(err)
}
})
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
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, telegram_id
from chats
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
${where}
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)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data: rows})
})
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.gid}`)
// 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
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : ''
function getTask(id, user_id) {
const row = db
.prepare(`
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
close_date, close_comment, coalesce(json_close_files, '[]') close_files, 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_files = JSON.parse(row.close_files)
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,
(select json_chat_array(user_id) from task_users where task_id = t.id) observers,
(select json_chat_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
close_date, close_comment, coalesce(json_close_files, '[]') close_files, 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))
${where}
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_files = JSON.parse(row.close_files)
row.observers = JSON.parse(row.observers)
row.attachments = JSON.parse(row.attachments)
row.files = JSON.parse(row.files)
})
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data: 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', async (req, res, next) => {
try {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
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')
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')
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 id = db
.prepare(`
insert into tasks (project_id, name, description, created_by, assigned_to, priority, status, create_date, plan_date, chat_id)
values (:project_id, :name, :description, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date, :chat_id)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
const task = getTask(id, res.locals.user_id)
if (res.locals.chat_id) {
const creator = getUserInfo(task.created_by, task.project_id)
const assignee = getUserInfo(task.assigned_to, task.project_id)
const args = {
PRIORITY: '$TASK_PRIORITY_' + (task.priority || '0'),
NAME: task.name,
PLAN_DATE: new Date(task.plan_date * 1000).toLocaleString('ru'),
CREATOR: creator.name,
CREATOR_ID: creator.telegram_id,
ASSIGNEE: assignee.name,
ASSIGNEE_ID: assignee.telegram_id,
URL: bot.USER_APP + '/?startapp=p' + res.locals.project_id + 't' + task.id
}
message_id = await bot.sendMessage(res.locals.chat_id, 'TASK_MESSAGE', args)
db
.prepare(`update tasks set message_id = :message_id where id = :id`)
.run({ id, message_id})
}
res.status(200).json({success: true, data: task})
} catch (err) {
next (err)
}
})
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
@@ -290,9 +371,12 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
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))
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)
@@ -305,18 +389,43 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
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.description = req.body?.description
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.close_comment = req.body?.close_comment
res.locals.json_close_files = null
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'] : ['status', 'time_spent']
if (req.body?.close_files instanceof Array) {
const file_ids = db
.prepare(`select id from files where parent_type = 1 and parent_id = :task_id`)
.pluck(true)
.all(res.locals)
const close_files = req.body.close_files
.map(id => parseInt(id))
.filter(id => file_ids.indexOf(id) != -1)
res.json_close_files = JSON.stringify(close_files)
}
const columns = res.locals.is_author ? ['name', 'description', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_files', '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)
@@ -324,7 +433,8 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
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) => {
@@ -338,7 +448,7 @@ app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: res.locals.task_id}})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
@@ -373,51 +483,82 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
})
// MEETINGS
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : ''
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, created_by, meet_date,
(select json_chat_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_chat_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments
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))
${where}
`)
.all(res.locals)
rows.forEach(row => {
row.participants = JSON.parse(row.participants)
row.attachments = JSON.parse(row.attachments)
row.files = JSON.parse(row.files)
row.is_editable = Boolean(row.is_editable)
})
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data: 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', async (req, res, next) => {
try {
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')
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, place, created_by, meet_date, duration, chat_id)
values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration, :chat_id)
returning id
`)
.pluck(true)
.get(res.locals)
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})
const meeting = getMeeting(id, res.locals.user_id)
if (res.locals.chat_id) {
const url = bot.USER_APP + '/?startapp=p' + res.locals.project_id + 'm' + meeting.id
message_id = await bot.sendMessage(res.locals.chat_id, 'MEETING_MESSAGE', meeting)
db
.prepare(`update meetings set message_id = :message_id where id = :id`)
.run({ id, message_id})
}
res.status(200).json({success: true, data: meeting})
} catch (err) {
next(err)
}
})
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
@@ -440,6 +581,11 @@ app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
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')
@@ -447,16 +593,23 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
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', 'meet_date'], res.locals, ['id', 'project_id'])
.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')
res.status(200).json({success: true})
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) => {
@@ -470,10 +623,10 @@ app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: res.locals.meeting_id}})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next) => {
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
@@ -486,7 +639,7 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true) // .raw?
.pluck(true)
.all(res.locals)
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
@@ -501,72 +654,123 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
.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})
res.status(200).json({ success: true, data: user_ids })
})
// 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(', ') + ')' : ''
// Документы
// FILES
app.get('/project/:pid(\\d+)/file', (req, res, next) => {
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
// 3. Из встреч на проекте, где пользователь создатель или участник
// To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId
const rows = db
.prepare(`
select id, origin_chat_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_chat_id in (select chat_id from chat_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
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 (
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))
)
)
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)
.safeIntegers(true)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
const upload_ids = db
.prepare(`select upload_chat_id from customers where upload_chat_id is not null`)
.pluck(true)
.all()
.reduce((res, e) => { res[e] = true; return res}, {})
res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows})
rows
.filter (row => upload_ids[row.chat_id])
.forEach(row => {
row.chat_id = null
row.message_id = null
})
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) => {
const parentType = req.params.type == 'task' ? 1 : 2
const parentId = req.params.id
try {
res.locals.parent_id = req.params.id
res.locals.parent_type = req.params.type == 'task' ? 1 : 2
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)
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
}
try {
const file_id = await bot.sendFile(filedata)
if (file_id)
file_ids.push(file_id)
} catch (err) {
log.error(err)
}
}
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})
} catch (err) {
next(err)
}
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
app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
res.locals.file_id = req.params.fid
const doc = db
.prepare(`select * from documents where id = :document_id and project_id = :project_id`)
const file = db
.prepare(`select * from files where id = :file_id and project_id = :project_id`)
.get(res.locals)
if (!doc)
if (!file)
throw Error('NOT_FOUND::404')
if (doc.parent_type == 0) {
res.locals.chat_id = doc.chat_id
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)
@@ -575,8 +779,8 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
res.locals.can_download = true
}
} else {
res.locals.parent_id = doc.parent_id
const parent = doc.parent_type == 1 ? 'task' : 'meeting'
res.locals.parent_id = file.parent_id
const parent = file.parent_type == 1 ? 'task' : 'meeting'
const row = db
.prepare(`
@@ -589,36 +793,43 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
if (row) {
res.locals.can_download = true
res.locals.can_delete = doc.published_by == res.locals.user_id
res.locals.can_delete = file.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')
app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => {
try {
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)
})
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)
res.end(file.data)
} catch (err) {
next(err)
}
})
app.delete('/project/:pid(\\d+)/document/:id(\\d+)', (req, res, next) => {
app.delete('/project/:pid(\\d+)/file/:fid(\\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`)
const info = db
.prepare(`delete from files where id = :id and project_id = :project_id`)
.run(res.locals)
res.status(200).json({success: true})
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) => {
@@ -640,4 +851,28 @@ app.put('/settings', (req, res, next) => {
res.status(200).json({success: true})
})
module.exports = app
/*
eventBus.on('data', evt => {
if (evt.)
const msg = {
event: evt.event,
entity: evt.entity,
id: evt.id,
source: 'miniapp'
}
const users = {}
if (evt.entity == 'project') {
}
if (evt.entity == 'task') {
msg.data = getTask(evt.id, null)
}
if (evt.entity == 'meeting') {
msg.data = getMeeting(evt.id, null)
}
})
*/
module.exports = { router: app, registerWS }