This commit is contained in:
2025-05-04 22:22:20 +03:00
parent cda54b1e95
commit ebd77a3e66
54 changed files with 1194 additions and 2580 deletions

View File

@@ -1,24 +1,45 @@
const crypto = require('crypto')
const express = require('express')
const multer = require('multer')
const db = require('../include/db')
const bot = require('./bot')
const fs = require('fs')
const cookieParser = require('cookie-parser')
const app = express.Router()
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 1_000_000 // 1mb
}
})
const sessions = {}
const emailCache = {} // key = email, value = code
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) => {
if (req.path == '/auth/email' || req.path == '/auth/telegram' || req.path == '/auth/register' || req.path == '/auth/logout')
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
@@ -59,10 +80,10 @@ app.post('/auth/email', (req, res, next) => {
app.post('/auth/telegram', (req, res, next) => {
let customer_id = db
.prepare(`select id from customers where is_blocked = 0 and telegram_id = :telegram_id`)
.prepare(`select id from customers where telegram_id = :telegram_id`)
.pluck(true)
.get(res.locals) || db
.prepare(`replace into customers (telegram_id, is_blocked) values (:telegram_id, 0) returning id`)
.prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`)
.pluck(true)
.get(res.locals)
@@ -70,73 +91,198 @@ app.post('/auth/telegram', (req, res, next) => {
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})
})
app.post('/auth/register', (req, res, next) => {
/*
Регистрация нового клиента выполняется за ТРИ последовательных вызова
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()
if (email) {
const validateEmail = email => 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,}))$/)
if (!validateEmail(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')
}
if (email && !code) {
const code = Math.random().toString().substr(2, 4)
emailCache[email] = code
// To-Do: send email
console.log(`${email} => ${code}`)
}
if (email && code && !password) {
if (emailCache[email] != code)
throw Error('INCORRECT_CODE::400')
}
if (email && code && password) {
if (password.length < 8)
throw Error('INCORRECT_PASSWORD::400')
db
.prepare('insert into customers (email, password, is_blocked) values (:email, :password, 0)')
.run({email, password})
}
res.status(200).json({success: true})
})
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_group_id
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_group_id) {
row.upload_group = db
.prepare(`select id, name, telegram_id from groups where id = :group_id and project_id is null`)
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({ group_id: row.upload_group_id})
delete row.upload_group_id
.get({ chat_id: row.upload_chat_id})
delete row.upload_chat_id
}
for (const key in row) {
@@ -167,15 +313,36 @@ app.put('/customer/profile', (req, res, next) => {
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
from projects
where customer_id = :customer_id ${where} and is_deleted <> 1
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)
@@ -204,7 +371,7 @@ app.post('/project', (req, res, next) => {
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
res.redirect(req.baseUrl + `/project?id=${id}`)
})
app.put('/project/:pid(\\d+)', (req, res, next) => {
@@ -212,11 +379,12 @@ app.put('/project/:pid(\\d+)', (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 = req.body?.is_logo_bg
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo'],
['name', 'description', 'logo', 'is_logo_bg'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
@@ -224,39 +392,41 @@ app.put('/project/:pid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
app.delete('/project/:pid(\\d+)', async (req, res, next) => {
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 id_deleted = 1 where id = :id and customer_id = :customer_id')
.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('NOT_FOUND::404')
throw Error('BAD_REQUEST::400')
const groupIds = db
.prepare(`select id from groups where project_id = :id`)
const chatIds = db
.prepare(`select id from chats where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const groupId of groupIds) {
await bot.sendMessage(groupId, 'Проект удален')
await bot.leaveGroup(groupId)
for (const chatId of chatIds) {
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
db.prepare(`updates groups set project_id = null where id in (${ groupIds.join(', ')})`).run()
res.status(200).json({success: true})
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_deleted <> 1')
.prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_archived <> 1')
.get(res.locals)
if (!row)
@@ -277,8 +447,8 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id in (
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
) ${where}
`)
.safeIntegers(true)
@@ -337,7 +507,7 @@ app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const rows = db
.prepare(`
select id, name, email, phone, description, logo,
(select json_group_array(user_id) from company_users where company_id = c.id) users
(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
@@ -373,7 +543,7 @@ app.post('/project/:pid(\\d+)/company', (req, res, next) => {
.pluck(res.locals)
.get(res.locals)
res.status(200).json({success: true, data: id})
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${id}`)
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
@@ -395,7 +565,7 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
})
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
@@ -411,13 +581,13 @@ app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/group', (req, res, next) => {
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 groups
from chats
where project_id = :project_id ${where}
`)
.all(res.locals)
@@ -428,20 +598,20 @@ app.get('/project/:pid(\\d+)/group', (req, res, next) => {
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.uid}`)
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+)/group/:gid(\\d+)', async (req, res, next) => {
res.locals.group_id = parseInt(req.params.gid)
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 groups set project_id = null where id = :group_id and project_id = :project_id`)
.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.group_id, 'Группа удалена из проекта')
await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта')
res.status(200).json({success: true})
})
@@ -463,8 +633,8 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
let rows = db
.prepare(`
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true) // .raw?
.get(res.locals)