const crypto = require('crypto') const express = require('express') const db = require('../include/db') const bot = require('./bot') const fs = require('fs') const app = express.Router() const sessions = {} const cache = { // email -> code register: {}, recovery: {}, 'change-password': {}, 'change-email': {}, 'change-email2': {} } function checkEmail(email){ return String(email) .toLowerCase() .match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) } function sendEmail(email, subject, message) { console.log(`${email} --> ${subject}: ${message}`) } function checkPassword(password) { return password.length >= 8 } app.use((req, res, next) => { const public = [ '/auth/email', '/auth/telegram', '/auth/email/register', '/auth/email/recovery', '/auth/logout' ] if (public.includes(req.path)) return next() const asid = req.query.asid || req.cookies.asid req.session = sessions[asid] if (!req.session) throw Error('ACCESS_DENIED::401') res.locals.customer_id = req.session.customer_id next() }) // AUTH function createSession(req, res, customer_id) { if (!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`]) } app.post('/auth/email', (req, res, next) => { res.locals.email = req.body?.email 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 `) .pluck(true) .get(res.locals) if (!customer_id) throw Error('AUTH_ERROR::401') createSession(req, res, customer_id) res.status(200).json({success: true}) }) app.post('/auth/telegram', (req, res, next) => { let customer_id = db .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`) .pluck(true) .get(res.locals) createSession(req, res, customer_id) res.status(200).json({success: true}) }) /* Регистрация нового клиента выполняется за ТРИ последовательных вызова 1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код. 2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК. 3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК. */ app.post('/auth/email/register', (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 stepNo = email && !code ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1 if (stepNo == -1) throw Error('BAD_STEP::400') if (stepNo == 1) { if (!checkEmail(email)) throw Error('INCORRECT_EMAIL::400') const customer_id = db .prepare('select id from customers where email = :email') .pluck(true) .get({email}) if (customer_id) throw Error('USED_EMAIL::400') const code = Math.random().toString().substr(2, 4) cache.register[email] = code sendEmail(email, 'REGISTER', `${email} => ${code}`) } if (stepNo == 2) { if (cache.register[email] != code) throw Error('INCORRECT_CODE::400') } if (stepNo == 3) { if (!checkPassword(password)) throw Error('INCORRECT_PASSWORD::400') db .prepare('insert into customers (email, password) values (:email, :password)') .run({email, password}) delete cache.register[email] } res.status(200).json({success: true}) }) /* Смена email выполняется за ЧЕТЫРЕ последовательных вызовов 1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код. 2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК. 3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email. 4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет 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 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 if (stepNo == -1) throw Error('BAD_STEP::400') if (stepNo == 1) { const code = Math.random().toString().substr(2, 4) cache['change-email'][email] = code sendEmail(email, 'CHANGE-EMAIL', `${email} => ${code}`) } if (stepNo == 2) { if (cache['change-email'][email] != code) throw Error('INCORRECT_CODE::400') } if (stepNo == 3) { if (!checkEmail(email2)) throw Error('INCORRECT_EMAIL::400') const code2 = Math.random().toString().substr(2, 4) cache['change-email2'][email2] = code2 sendEmail(email2, 'CHANGE-EMAIL2', `${email2} => ${code2}`) } if (stepNo == 4) { if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2) throw Error('INCORRECT_CODE::400') const info = db .prepare('update customers set email = :email where id = :customer_id') .run(res.locals) if (info.changes == 0) throw Error('BAD_REQUEST::400') delete cache['change-email'][email] delete cache['change-email2'][email2] } res.status(200).json({success: true}) }) /* Смена пароля/восстановление доступа выполняется за ТРИ последовательных вызова 1. Отравляется пустой закпрос для смены запоса и email, в случае восстановления доступа. Сервер на email отправляет код. 2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК. 3. Отправляется email + код из письма + новый пароль. Сервер изменяет пароль и возвращает ОК. */ 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 action = req.params.action const email = action == 'change-password' ? db .prepare('select email from customers where id = :customer_id') .pluck(true) .get(res.locals) : String(req.body.email ?? '').trim() const stepNo = action == 'change-password' ? (!code && !password ? 1 : code && !password ? 2 : code && password ? 3 : -1) : (!email && !code && !password ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1) if (stepNo == -1) throw Error('BAD_STEP::400') if (stepNo == 1) { if (!checkEmail(email)) throw Error('INCORRECT_EMAIL::400') const code = Math.random().toString().substr(2, 4) cache[action][email] = code sendEmail(email, action.toUpperCase(), `${email} => ${code}`) } if (stepNo == 2) { if (cache[action][email] != code) throw Error('INCORRECT_CODE::400') } if (stepNo == 3) { if (cache[action][email] != code) throw Error('INCORRECT_CODE::400') if (!checkPassword(password)) throw Error('INCORRECT_PASSWORD::400') const info = db .prepare('update customers set password = :password where email = :email') .run({ email, password }) if (info.changes == 0) throw Error('BAD_REQUEST::400') delete cache[action][email] } res.status(200).json({success: true}) }) app.get('/auth/logout', (req, res, next) => { if (req.session?.asid) delete sessions[req.session.asid] res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`]) res.status(200).json({success: true}) }) // CUSTOMER app.get('/customer/profile', (req, res, next) => { const row = db .prepare(` select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_chat_id from customers where id = :customer_id `) .get(res.locals) if (row?.upload_chat_id) { row.upload_chat = db .prepare(`select id, name, telegram_id from chats where id = :chat_id and project_id is null`) .safeIntegers(true) .get({ chat_id: row.upload_chat_id}) delete row.upload_chat_id } for (const key in row) { if (key.startsWith('json_')) { row[key.substr(5)] = JSON.parse(row[key]) delete row[key] } } res.status(200).json({success: true, data: row}) }) app.put('/customer/profile', (req, res, next) => { if (req.body.company instanceof Object) req.body.json_company = JSON.stringify(req.body.company) const info = db .prepareUpdate( 'customers', ['name', 'password', 'json_company'], req.body, ['id']) .run(Object.assign({}, req.body, {id: req.session.customer_id})) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true}) }) app.get('/customer/settings', (req, res, next) => { const row = db .prepare(`select coalesce(json_settings, '{}') from customers where id = :customer_id`) .pluck(true) .get(res.locals) res.status(200).json({success: true, data: JSON.parse(row)}) }) app.put('/customer/settings', (req, res, next) => { res.locals.json_settings = JSON.stringify(req.body || {}) db .prepare(`update customers set json_settings = :json_settings where id = :customer_id`) .run(res.locals) res.status(200).json({success: true}) }) // PROJECT app.get('/project', (req, res, next) => { const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' const rows = db .prepare(` select id, name, description, logo, is_logo_bg, 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} order by name `) .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.post('/project', (req, res, next) => { res.locals.name = req.body?.name res.locals.description = req.body?.description res.locals.logo = req.body?.logo const id = db .prepare(` insert into projects (customer_id, name, description, logo) values (:customer_id, :name, :description, :logo) 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`) .pluck(true) .all(res.locals) for (const chatId of chatIds) { await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.') } 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) const row = db .prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_archived <> 1') .get(res.locals) if (!row) throw Error('ACCESS_DENIED::401') next() }) // USER app.get('/project/:pid(\\d+)/user', (req, res, next) => { const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' const rows = 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 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') res.status(200).json({success: true, data: where ? rows[0] : rows}) }) app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`) }) 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.role = req.body?.role res.locals.department = req.body?.department res.locals.is_blocked = req.body?.is_blocked const info = db .prepareUpdate('user_details', ['fullname', 'role', 'department', 'is_blocked'], res.locals, ['user_id', 'project_id'] ) .all(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.status(200).json({success: true}) }) app.get('/project/:pid(\\d+)/token', (req, res, next) => { res.locals.time = Math.floor(Date.now() / 1000) const key = db .prepare('select generate_key(id, :time) from projects where id = :project_id and customer_id = :customer_id') .pluck(true) .get(res.locals) if (!key) throw Error('NOT_FOUND::404') 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 .prepare(` select id, name, email, phone, description, logo, (select json_chat_array(user_id) from company_users where company_id = c.id) users from companies c where project_id = :project_id ${where} order by name `) .all(res.locals) rows.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}) }) app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`) }) app.post('/project/:pid(\\d+)/company', (req, res, next) => { res.locals.name = req.body?.name 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}`) }) 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.email = req.body?.email res.locals.phone = req.body?.phone res.locals.site = req.body?.site res.locals.description = req.body?.description const info = db .prepareUpdate( 'companies', ['name', 'email', 'phone', 'site', 'description'], res.locals, ['id', 'project_id']) .run(res.locals) if (info.changes == 0) throw Error('NOT_FOUND::404') res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`) }) app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { res.locals.company_id = parseInt(req.params.cid) const info = db .prepare(`delete from companies where id = :company_id and project_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}) }) app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { res.locals.company_id = parseInt(req.params.cid) // Проверка, что есть доступ к компании const row = db .prepare('select 1 from companies where id = :company_id and project_id = :project_id') .get(res.locals) if (!row) throw Error('NOT_FOUND::404') 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) // .raw? .get(res.locals) if (user_ids.some(user_id => !rows.contains(user_id))) throw Error('INACCESSABLE_MEMBER::400') // Проверка, что пользователи не участвуют в других компаниях на проекте rows = db .prepare(` select user_id from company_users where company_id in (select id from companies where id <> :company_id and project_id = :project_id) `) .pluck(true) // .raw? .get(res.locals) 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) db .prepare(` insert into company_users (company_id, user_id) select :company_id, value from json_each(:json_ids) `) .run(res.locals, {json_ids: JSON.stringify(user_ids)}) res.status(200).json({success: true}) }) module.exports = app