Первый коммит для gitea

This commit is contained in:
2025-07-24 16:26:18 +03:00
parent 34baeb40e3
commit 4c7f79bb7f
35 changed files with 168 additions and 4957 deletions

View File

@@ -1,96 +0,0 @@
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const crypto = require('crypto')
const fs = require('fs')
const util = require('util')
const bot = require('./apps/bot')
const app = express()
app.use(bodyParser.json())
app.use(cookieParser())
BigInt.prototype.toJSON = function () {
return Number(this)
}
app.use((req, res, next) => {
if(!(req.body instanceof Object))
return next()
const escapeHtml = str => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
Object
.keys(req.body || {})
.filter(key => typeof(req.body[key]) == 'string' && key != 'password')
.map(key => req.body[key] = escapeHtml(req.body[key]))
next()
})
// cors
app.use((req, res, next) => {
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Accept,Accept-Language,Content-Language,Content-Type,Authorization,Cookie,X-Requested-With,Origin,Host',
'Access-Control-Allow-Credentials': true
})
return req.method == 'OPTIONS' ? res.status(200).json({success: true}) : next()
})
app.post('(/api/admin/customer/login|/api/miniapp/user/login)', (req, res, next) => {
const data = Object.assign({}, req.query)
delete data.hash
const hash = req.query?.hash
const BOT_TOKEN = '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join("\n")
const secretKey = crypto.createHmac("sha256", "WebAppData").update(BOT_TOKEN).digest()
const hmac = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex")
const timeDiff = Date.now() / 1000 - data.auth_date
if (hmac !== req.query.hash) // || timeDiff > 10)
throw Error('ACCESS_DENIED::401')
const user = JSON.parse(req.query.user)
res.locals.telegram_id = user.id
res.locals.start_param = req.query.start_param
if (!res.locals.telegram_id)
throw Error('ACCESS_DENIED::500')
next()
})
app.use('/api/admin', require('./apps/admin'))
app.use('/api/miniapp', require('./apps/miniapp'))
app.use((err, req, res, next) => {
console.error(`Error for ${req.path}: ${err}`)
let message, code
//if (err.code == 'SQLITE_ERROR' || err.code == 'SQLITE_CONSTRAINT_CHECK') {
// message = 'DATABASE_ERROR'
//code = err.code == 'SQLITE_CONSTRAINT_CHECK' ? 400 : 500
//} else {
[message, code = 500] = err.message.split('::')
//}
res.status(res.statusCode == 200 ? 500 : res.statusCode).json({success: false, error: { message, code}})
})
app.use(express.static('public'))
const PORT = process.env.PORT || 3000
app.listen(PORT, async () => {
console.log(`Listening at port ${PORT}`)
bot.start(
process.env.API_ID || 26746106,
process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5',
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
)
})

View File

@@ -1,766 +0,0 @@
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: {},
upgrade: {},
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 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})
})
/*
Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
*/
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)
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[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 (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' :
'update customers set email = :email, password = :password where id = :id'
db.prepare(query).run({email, password, id: res.locals.customer_id})
delete cache[action][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, generate_key(-id, :time) upload_token
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)
else
delete req.body?.json_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
function getProject(id, customer_id) {
const row = 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 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, 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)
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})
})
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
.prepare(`
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)
const data = getProject(id, res.locals.customer_id)
res.status(200).json({success: true, data})
})
app.use ('(/project/:pid(\\d+)/*|/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()
})
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})
})
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) => {
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 ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
// 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.email, ud.phone, ud.role, ud.department, ud.is_blocked,
cu.company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
left join company_users cu on u.id = cu.user_id
where id = :id
`)
.safeIntegers(true)
.all({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
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,
cu.company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
left join company_users cu on u.id = cu.user_id
where id in (
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
)
`)
.safeIntegers(true)
.all(res.locals)
data.forEach(row => {
row.is_blocked = Boolean(row.is_blocked)
})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
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 = 'is_blocked' in req.body ? +req.body.is_blocked : undefined
const info = db
.prepareUpsert('user_details',
['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'],
res.locals,
['user_id', 'project_id']
)
.run(res.locals)
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) => {
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
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 c.id = :id and project_id = :project_id
order by name
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
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 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)
data.forEach(row => row.users = JSON.parse(row.users || '[]'))
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
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, address, email, phone, site, description, logo)
values (:project_id, :name, :address, :email, :phone, :site, :description, :logo)
returning id
`)
.pluck(true)
.get(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', 'address', 'email', 'phone', 'site', 'description', 'logo'],
res.locals,
['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
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 = 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, data: {id: req.params.cid}})
})
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})
})
// CHATS
function getChat(id, project_id) {
const row = db
.prepare(`
select id, name, telegram_id, is_channel, description, logo, user_count, bot_can_ban
from chats c
where c.id = :id and project_id = :project_id
`)
.all({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, 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) => {
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, 'Чат удален из проекта')
res.status(200).json({success: true, data: {id: req.params.gid}})
})
module.exports = app

View File

@@ -1,627 +0,0 @@
const db = require('../include/db')
const { Api, TelegramClient } = require('telegram')
const { StringSession } = require('telegram/sessions')
const { Button } = require('telegram/tl/custom/button')
const { CustomFile } = require('telegram/client/uploads')
let session
let client
let BOT_ID
const BOT_NAME = 'ready_or_not_2025_bot'
function registerUser (telegramId) {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
.run({ telegram_id: telegramId })
return db
.prepare(`select id from users where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({ telegram_id: telegramId })
}
function updateUser (userId, data) {
const info = db
.prepare(`
update users set firstname = :firstname, lastname = :lastname, username = :username,
access_hash = :access_hash, language_code = :language_code where id = :user_id
`)
.safeIntegers(true)
.run({
user_id: userId,
firstname: data.firstName,
lastname: data.lastName,
username: data.username,
access_hash: data.accessHash.value,
language_code: data.langCode
})
return info.changes == 1
}
async function updateUserPhoto (userId, data) {
const photoId = data.photo?.photoId?.value
if (!photoId)
return
const tgUserId = db
.prepare(`select telegram_id from users where id = :user_id and coalesce(photo_id, 0) <> :photo_id`)
.safeIntegers(true)
.pluck(true)
.get({user_id: userId, photo_id: photoId })
if (!tgUserId)
return
const photo = await client.invoke(new Api.photos.GetUserPhotos({
userId: new Api.PeerUser({ userId: tgUserId }),
maxId: photoId,
offset: -1,
limit: 1,
}));
const file = await client.downloadFile(new Api.InputPhotoFileLocation({
id: photoId,
accessHash: photo.photos[0]?.accessHash,
fileReference: Buffer.from('random'),
thumbSize: 'a',
}, {}))
db
.prepare(`update users set photo_id = :photo_id, photo = :photo where id = :user_id`)
.safeIntegers(true)
.run({ user_id: userId, photo_id: photoId, photo: 'data:image/jpg;base64,' + file.toString('base64') })
}
async function registerChat (telegramId, isChannel) {
const chat = db
.prepare(`select id, name, is_channel, access_hash from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.get({telegram_id: telegramId})
if (chat && chat.access_hash && chat.is_channel == isChannel && chat.name)
return chat.id
const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId }
const tgChat = await client.getEntity( isChannel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
)
const chatId = db
.prepare(`replace into chats (telegram_id, is_channel, access_hash, name) values (:telegram_id, :is_channel, :access_hash, :name) returning id`)
.safeIntegers(true)
.pluck(true)
.get({
telegram_id: telegramId,
is_channel: +isChannel,
access_hash: tgChat.accessHash.value,
name: tgChat.title
})
await updateChat(chatId)
return chatId
}
async function updateChat (chatId) {
const chat = db
.prepare(`select id, telegram_id, access_hash, is_channel from chats where id = :id`)
.safeIntegers(true)
.get({id: chatId})
const peer = chat.is_channel ?
new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
const data = chat.is_channel ?
await client.invoke(new Api.channels.GetFullChannel({ channel: peer })) :
await client.invoke(new Api.messages.GetFullChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }))
const file = data?.fullChat?.chatPhoto ? await client.downloadFile(new Api.InputPeerPhotoFileLocation({ peer, photoId: data.fullChat.chatPhoto?.id }, {})) : null
logo = file ? 'data:image/jpg;base64,' + file.toString('base64') : null
db
.prepare(`update chats set description = :description, logo = :logo, user_count = :user_count, last_update_time = :last_update_time where id = :id`)
.safeIntegers(true)
.run({
id: chatId,
description: data.fullChat.about,
logo,
user_count: data.fullChat.participantsCount - (data.users || []).filter(user => user.bot).length,
last_update_time: Math.floor(Date.now() / 1000)
})
}
async function attachChat(chatId, projectId) {
const chat = db
.prepare(`update chats set project_id = :project_id where id = :chat_id returning telegram_id, access_hash, is_channel`)
.safeIntegers(true)
.get({ chat_id: chatId, project_id: projectId })
if (!chat.telegram_id)
return console.error('Can\'t attach chat: ' + chatId + ' to project: ' + projectId)
const peer = chat.is_channel ?
new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
const message = db
.prepare(`select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id`)
.pluck(true)
.get({project_id: projectId})
const resultBtn = await client.sendMessage(peer, {
message,
buttons: client.buildReplyMarkup([[Button.url('Открыть проект', `https://t.me/${BOT_NAME}/userapp?startapp=` + projectId)]])
})
await client.invoke(new Api.messages.UpdatePinnedMessage({
peer,
id: resultBtn.id,
unpin: false
}))
}
async function reloadChatUsers(chatId, onlyReset) {
db
.prepare(`delete from chat_users where chat_id = :chat_id`)
.run({ chat_id: chatId })
if (onlyReset)
return
const chat = db
.prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!chat)
return
const tgChatId = chat.telegram_id
const isChannel = chat.is_channel
const accessHash = chat.access_hash
const result = isChannel ?
await client.invoke(new Api.channels.GetParticipants({
channel: new Api.PeerChannel({ channelId: tgChatId, accessHash }),
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset: 0
})) : await client.invoke(new Api.messages.GetFullChat({ chatId: tgChatId, accessHash }))
const users = result.users.filter(user => !user.bot)
for (const user of users) {
const userId = registerUser(user.id.value, user)
if (updateUser(userId, user)) {
await updateUserPhoto (userId, user)
db
.prepare(`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`)
.run({ chat_id: chatId, user_id: userId })
}
}
db
.prepare(`update chats set user_count = (select count(1) from chat_users where chat_id = :chat_id) where id = :chat_id`)
.run({ chat_id: chatId})
}
async function registerUpload(data) {
if (!data.projectId || !data.media)
return console.error ('registerUpload: ' + (data.projectId ? 'media' : 'project id') + ' is missing')
const customer_id = db
.prepare(`select customer_id from projects where project_id = :project_id`)
.pluck(true)
.get({project_id: data.projectId})
if (!customer_id)
return console.error ('registerUpload: The customer is not found for project: ' + data.projectId)
const chat = db
.prepare(
`select id, telegram_id, project_id, is_channel, access_hash from chats
where id = (select upload_chat_id from customers where id = :customer_id`)
.safeIntegers(true)
.get({ customer_id })
if (!chat || !chat.telegram_id || chat.id == data.originchatId)
return console.error ('registerUpload: The upload chat is not set for customer: ' + customer_id)
const peer = chat.is_channel ?
new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.PeerChat({ chatlId: chat.telegram_id, accessHash: chat.access_hash })
let resultId = 0
try {
const result = await client.invoke(new Api.messages.SendMedia({
peer,
media: data.media,
message: data.caption || '',
background: true,
silent: true
}))
const update = result.updates.find(u =>
(u.className == 'UpdateNewMessage' || u.className == 'UpdateNewChannelMessage') &&
u.message.className == 'Message' &&
(u.message.peerId.channelId?.value == chat.telegram_id || u.message.peerId.chatId?.value == chat.telegram_id) &&
u.message.media)
const udoc = update?.message?.media?.document
if (udoc) {
resultId = db
.prepare(`
insert into files (project_id, origin_chat_id, origin_message_id, chat_id, message_id,
file_id, access_hash, filename, mime, caption, size, published_by, published, parent_type, parent_id)
values (:project_id, :origin_chat_id, :origin_message_id, :chat_id, :message_id,
:file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :published, :parent_type, :parent_id)
returning id
`)
.safeIntegers(true)
.pluck(true)
.get({
project_id: data.projectId,
origin_chat_id: data.originchatId,
origin_message_id: data.originMessageId,
chat_id: chat.id,
message_id: update.message.id,
file_id: udoc.id.value,
filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName,
access_hash: udoc.accessHash.value,
mime: udoc.mimeType,
caption: data.caption,
size: udoc.size.value,
published_by: data.publishedBy,
published: data.published,
parent_type: data.parentType,
parent_id: data.parentId
})
}
} catch (err) {
//fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n')
console.error('registerUpload: ' + err.message)
}
return resultId
}
async function onNewServiceMessage (msg, isChannel) {
const action = msg.action || {}
const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chatId = await registerChat(tgChatId, isChannel)
// Сhat rename
if (action.className == 'MessageActionChatEditTitle') {
const info = db
.prepare(`
update chats
set name = :name, is_channel = :is_channel, last_update_time = :last_update_time
where telegram_id = :telegram_id
`)
.safeIntegers(true)
.run({
name: action.title,
is_channel: +isChannel,
last_update_time: Math.floor (Date.now() / 1000),
telegram_id: tgChatId
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t update a chat title: ' + tgChatId)
}
// Chat to Channel
if (action.className == 'MessageActionChatMigrateTo') {
const info = db
.prepare(`
update chats
set telegram_id = :new_telegram_id, name = :name, is_channel = 1, last_update_time = :last_update_time
where telegram_id = :old_telegram_id
`)
.safeIntegers(true)
.run({
name: action.title,
last_update_time: Date.now() / 1000,
old_telegram_id: tgChatId,
new_telegram_id: action.channelId.value
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t apply a chat migration to channel: ' + tgChatId)
}
// User/s un/register
if (action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChatDeleteUser' ||
action.className == 'MessageActionChannelAddUser' || action.className == 'MessageActionChannelDeleteUser'
) {
const tgUserIds = [action.user, action.users, action.userId].flat().filter(Boolean).map(e => BigInt(e.value))
const isAdd = action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChannelAddUser'
if (tgUserIds.indexOf(BOT_ID) == -1) {
// Add/remove non-bot users
for (const tgUserId of tgUserIds) {
const userId = registerUser(tgUserId)
if (isAdd) {
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
updateUser(userId, user)
await updateUserPhoto (userId, user)
} catch (err) {
console.error(msg.className + ', ' + userId + ': ' + err.message)
}
}
const query = isAdd ?
`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)` :
`delete from chat_users where chat_id = :chat_id and user_id = :user_id`
db
.prepare(query)
.run({ chat_id: chatId, user_id: userId })
}
}
}
}
async function onNewMessage (msg, isChannel) {
const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chatId = await registerChat(tgChatId, isChannel)
// Document is detected
if (msg.media?.document) {
const doc = msg.media.document
const projectId = db
.prepare(`select project_id from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({telegram_id: tgChatId})
const media = new Api.InputMediaDocument({
id: new Api.InputDocument({
id: doc.id.value,
accessHash: doc.accessHash.value,
fileReference: doc.fileReference
})
})
await registerUpload({
projectId,
media,
caption: msg.message,
originchatId: chatId,
originMessageId: msg.id,
parentType: 0,
publishedBy: registerUser (msg.fromId?.userId?.value),
published: msg.date
})
}
if (msg.message?.startsWith(`/start@${BOT_NAME} KEY-`) || msg.message?.startsWith('KEY-')) {
const rows = db
.prepare(`
select 1 from chats where id = :chat_id and project_id is not null
union all
select 1 from customers where upload_chat_id = :chat_id
`)
.all({ chat_id: chatId })
if (rows.length)
return await sendMessage(chatId, 'Чат уже используется')
const rawkey = msg.message.substr(msg.message?.indexOf('KEY-'))
const [_, time64, key] = rawkey.split('-')
const now = Math.floor(Date.now() / 1000)
const time = Buffer.from(time64, 'base64')
if (now - 3600 >= time && time >= now)
return await sendMessage(chatId, 'Время действия ключа для привязки истекло')
const row = db
.prepare(`
select (select id from projects where generate_key(id, :time) = :rawkey) project_id,
(select id from customers where generate_key(-id, :time) = :rawkey) customer_id
`)
.get({ rawkey, time })
if (row.project_id) {
await attachChat(chatId, row.project_id)
await reloadChatUsers(chatId)
}
if (row.customer_id) {
const info = db
.prepare(`update customers set upload_chat_id = :chat_id where id = :customer_id`)
.safeIntegers(true)
.run({ customer_id: row.customer_id, chat_id: chatId })
if (info.changes == 0)
console.error('Can\'t set upload chat: ' + chatId + ' to customer: ' + row.customer_id)
}
}
}
async function onNewUserMessage (msg) {
if (msg.message == '/start' && msg.peerId?.className == 'PeerUser') {
const tgUserId = msg.peerId?.userId?.value
const userId = registerUser(tgUserId)
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
updateUser(userId, user)
await updateUserPhoto (userId, user)
const appButton = new Api.KeyboardButtonWebView({
text: "Open Mini-App", // Текст на кнопке
url: "https://h5sj0gpz-3000.euw.devtunnels.ms/", // URL вашего Mini-App (HTTPS!)
});
const inputPeer = new Api.InputPeerUser({userId: tgUserId, accessHash: user.accessHash.value})
await client.sendMessage(inputPeer, {
message: 'Сообщение от бота',
buttons: client.buildReplyMarkup([
[Button.url('Админка', `https://t.me/${BOT_NAME}/userapp?startapp=admin`)],
[Button.url('Пользователь', `https://t.me/${BOT_NAME}/userapp?startapp=user`)],
[appButton]
])
})
} catch (err) {
console.error(msg.className + ', ' + userId + ': ' + err.message)
}
}
}
async function onUpdatePaticipant (update, isChannel) {
const tgChatId = isChannel ? update.channelId?.value : update.chatlId?.value
if (!tgChatId || update.userId?.value != BOT_ID)
return
const chatId = await registerChat (tgChatId, isChannel)
const isBan = update.prevParticipant && !update.newParticipant
const isAdd = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant
if (isBan || isAdd)
await reloadChatUsers(chatId, isBan)
if (isBan) {
db
.prepare(`update chats set project_id = null where id = :chat_id`)
.run({chat_id: chatId})
}
const botCanBan = update.newParticipant?.adminRights?.banUsers || 0
db
.prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`)
.run({chat_id: chatId, bot_can_ban: +botCanBan})
}
async function uploadFile(projectId, fileName, mime, data, parentType, parentId, publishedBy) {
const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 })
const media = new Api.InputMediaUploadedDocument({
file,
mimeType: mime,
attributes: [new Api.DocumentAttributeFilename({ fileName })]
})
return await registerUpload({
projectId,
media,
parentType,
parentId,
publishedBy,
published: Math.floor(Date.now() / 1000)
})
}
async function downloadFile(projectId, fileId) {
const file = db
.prepare(`
select file_id, access_hash, '' thumbSize, filename, mime
from files where id = :id and project_id = :project_id
`)
.safeIntegers(true)
.get({project_id: projectId, id: fileId})
if (!file)
return false
const result = await client.downloadFile(new Api.InputDocumentFileLocation({
id: file.file_id,
accessHash: file.access_hash,
fileReference: Buffer.from(file.filename),
thumbSize: ''
}, {}))
return {
filename: file.filename,
mime: file.mime,
size: result.length,
data: result
}
}
async function sendMessage (chatId, message) {
const chat = db
.prepare(`select telegram_id, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!chat)
return
const entity = chat.is_channel ? { channelId: chat.telegram_id } : { chatId: chat.telegram_id }
const inputPeer = await client.getEntity( chat.is_channel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
)
await client.sendMessage(inputPeer, {message})
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
await delay(1000)
}
async function leaveChat (chatId) {
const chat = db
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!chat)
return
if (chat.is_channel) {
const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }))
await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer }))
} else {
await client.invoke(new Api.messages.DeleteChatUser({ chatId: chat.telegram_id, userId: this.id, accessHash: chat.access_hash }))
}
}
async function start (apiId, apiHash, botAuthToken, sid) {
BOT_ID = BigInt(botAuthToken.split(':')[0])
session= new StringSession(sid || '')
client = new TelegramClient(session, apiId, apiHash, {})
client.addEventHandler(async (update) => {
if (update.className == 'UpdateConnectionState')
return
try {
// console.log(update)
if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') {
const msg = update?.message
const isChannel = update.className == 'UpdateNewChannelMessage'
if (!msg)
return
const result = msg.peerId?.className == 'PeerUser' ? await onNewUserMessage(msg) :
msg.className == 'MessageService' ? await onNewServiceMessage(msg, isChannel) :
await onNewMessage(msg, isChannel)
}
if (update.className == 'UpdateChatParticipant' || update.className == 'UpdateChannelParticipant')
await onUpdatePaticipant(update, update.className == 'UpdateChannelParticipant')
} catch (err) {
console.error(err)
}
})
await client.start({botAuthToken})
}
module.exports = { start, uploadFile, downloadFile, reloadChatUsers, sendMessage }

View File

@@ -1,643 +0,0 @@
const express = require('express')
const multer = require('multer')
const crypto = require('crypto')
const fs = require('fs')
const contentDisposition = require('content-disposition')
const bot = require('./bot')
const db = require('../include/db')
const app = express.Router()
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10_000_000 // 10mb
}
})
function hasAccess(project_id, user_id) {
return !!db
.prepare(`
select 1
from group_users
where user_id = :user_id and
group_id in (select id from groups where project_id = :project_id) and
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1) and
not exists(select 1 from projects where id = :project_id and is_deleted = 1)
`)
.get({project_id, user_id})
}
const sessions = {}
app.use((req, res, next) => {
if (req.path == '/user/login')
return next()
const sid = req.query.sid || req.cookies.sid
req.session = sessions[sid]
if (!req.session)
throw Error('ACCESS_DENIED::401')
res.locals.user_id = req.session.user_id
next()
})
app.post('/user/login', (req, res, next) => {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
.run(res.locals)
const user_id = db
.prepare(`select id from users where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get(res.locals)
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, user_id}
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/`])
res.locals.user_id = user_id
res.status(200).json({success: true})
})
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and p.id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select p.id, p.name, p.description, p.logo,
c.name customer_name, c.upload_group_id <> 0 has_upload
from projects p
inner join customers c on p.customer_id = c.id
where p.id in (
select project_id
from groups
where id in (select group_id from group_users where user_id = :user_id)
) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1)
${where} and is_deleted <> 1
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
app.use('/project/:pid(\\d+)/*', (req, res, next) => {
res.locals.project_id = parseInt(req.params.pid)
if (!hasAccess(res.locals.project_id, res.locals.user_id))
throw Error('ACCESS_DENIED::401')
const row = db
.prepare('select customer_id from projects where id = :project_id')
.get(res.locals)
res.locals.customer_id = row.customer_id
next()
})
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : ''
const users = db
.prepare(`
with actuals (user_id) as (
select distinct user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
and group_id in (select group_id from group_users where user_id = :user_id)
),
contributors (user_id) as (
select created_by from tasks where project_id = :project_id
union
select assigned_to from tasks where project_id = :project_id
union
select created_by from meetings where project_id = :project_id
union
select published_by from documents where project_id = :project_id
),
members (user_id, is_leave) as (
select user_id, 0 is_leave from actuals
union all
select user_id, 1 is_leave from contributors where user_id not in (select user_id from actuals)
)
select u.id,
u.telegram_id,
u.username,
u.firstname,
u.lastname,
u.photo,
u.json_phone_projects,
ud.fullname,
ud.role,
ud.department,
ud.is_blocked,
(select company_id
from company_users
where user_id = u.id and
company_id in (select id from companies where project_id = :project_id)) company_id,
m.is_leave
from users u
inner join members m on u.id = m.user_id
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
where 1 = 1 ${where}
`)
.all(res.locals)
const companies = db
.prepare('select id, name, email, phone, site, description from companies where project_id = :project_id')
.all(res.locals)
.reduce((companies, row) => {
companies[row.id] = row
return companies
}, {})
const mappings = {}
const company_id = users.find(m => m.id == res.locals.user_id).company_id
if (company_id) {
res.locals.company_id = company_id
db
.prepare('select show_as_id, show_to_id from company_mappings where project_id = :project_id and company_id = :company_id')
.all(res.locals)
.forEach(row => mappings[row.show_to_id] = row.show_to_id)
}
users.forEach(m => {
m.company = companies[mappings[m.company_id] || m.company_id]
delete m.company_id
})
users.forEach(m => {
const isHide = JSON.parse(m.json_phone_projects || []).indexOf(res.locals.project_id) == -1
if (isHide)
delete m.phone
delete m.json_phone_projects
})
if (where && users.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? users[0] : users})
})
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
const groupIds = db
.prepare(`select id from groups where project_id = :project_id`)
.all(res.locals)
.map(e => e.id)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
for (const groupId of groupIds) {
await bot.reloadGroupUsers(groupId)
await sleep(1000)
}
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/group', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, telegram_id
from groups
where project_id = :project_id and id in (select group_id from group_users where user_id = :user_id)
${where}
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)/group/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.gid}`)
})
// TASK
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments
from tasks t
where project_id = :project_id and
(created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id))
${where}
`)
.all(res.locals)
rows.forEach(row => {
row.observers = JSON.parse(row.observers)
row.attachments = JSON.parse(row.attachments)
})
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/task?id=${req.params.tid}`)
})
app.post('/project/:pid(\\d+)/task', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.create_date = Math.floor(Date.now() / 1000)
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
throw Error('INCORRECT_ASSIGNED_TO::400')
const id = db
.prepare(`
insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date)
values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
})
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
res.locals.task_id = req.params.tid
const task = db
.prepare(`
select created_by, assigned_to
from tasks
where id = :task_id and project_id = :project_id
and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = :task_id and user_id = :user_id))
`)
.get(res.locals)
if (!task)
throw Error('NOT_FOUND::404')
res.locals.is_author = task.created_by == res.locals.user_id
res.locals.is_assigned = task.assigned_to == res.locals.user_id
next()
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (!res.locals.is_author && !res.locals.is_assigned)
throw Error('ACCESS_DENIED::401')
res.locals.id = res.locals.task_id
res.locals.name = req.body?.name
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent'] : ['status', 'time_spent']
const info = db
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const info = db
.prepare(`delete from tasks where id = :task_id and project_id = :project_id and created_by = :user_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
if (!res.locals.is_author && !res.locals.is_assigned)
throw Error('ACCESS_DENIED::401')
const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : []
// Проверка, что выбранные пользователи имеют доступ к проекту
let rows = db
.prepare(`
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
`)
.pluck(true)
.all(res.locals)
if (user_ids.some(user_id => !rows.contains(user_id)))
throw Error('INACCESSABLE_USER::400')
res.locals.json_ids = JSON.stringify(user_ids)
db
.prepare(`
delete from task_users where task_id = :task_id;
insert into task_users (task_id, user_id) select :task_id, value from json_each(:json_ids)
`)
.run(res.locals)
res.status(200).json({success: true})
})
// MEETINGS
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, description, created_by, meet_date,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments
from meetings m
where project_id = :project_id and
(created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id))
${where}
`)
.all(res.locals)
rows.forEach(row => {
row.participants = JSON.parse(row.participants)
row.attachments = JSON.parse(row.attachments)
})
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/meeting?id=${req.params.mid}`)
})
app.post('/project/:pid(\\d+)/meeting', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
const id = db
.prepare(`
insert into meetings (project_id, name, description, created_by, meet_date)
values (:project_id, :name, :description, :user_id, :meet_date)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
})
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
res.locals.meeting_id = req.params.mid
const meeting = db
.prepare(`
select created_by
from meetings
where id = :meeting_id and project_id = :project_id
and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = :meeting_id and user_id = :user_id))
`)
.get(res.locals)
if (!meeting)
throw Error('NOT_FOUND::404')
res.locals.is_author = meeting.created_by == res.locals.user_id
next()
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
res.locals.id = res.locals.meeting_id
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
const info = db
.prepareUpdate('meetings', ['name', 'description', 'meet_date'], res.locals, ['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const info = db
.prepare(`delete from meetings where id = :meeting_id and project_id = :project_id and created_by = :user_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : []
// Проверка, что выбранные пользователи имеют доступ к проекту
let rows = db
.prepare(`
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
`)
.pluck(true) // .raw?
.all(res.locals)
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
throw Error('INACCESSABLE_USER::400')
db
.prepare(`delete from meeting_users where meeting_id = :meeting_id`)
.run(res.locals)
res.locals.json_ids = JSON.stringify(user_ids)
db
.prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`)
.run(res.locals)
res.status(200).json({success: true})
})
// DOCUMENTS
app.get('/project/:pid(\\d+)/document', (req, res, next) => {
const ids = String(req.query.id).split(',').map(e => parseInt(e)).filter(e => e > 0)
const where = ids.length > 0 ? ' and id in (' + ids.join(', ') + ')' : ''
// Документы
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
// 3. Из встреч на проекте, где пользователь создатель или участник
// To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId
const rows = db
.prepare(`
select id, origin_group_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type
from documents d
where project_id = :project_id ${where} and (
origin_group_id in (select group_id from group_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
select id
from tasks t
where project_id = :project_id and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id))
)
or
parent_type = 2 and parent_id in (
select id
from meetings m
where project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id))
)
)
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows})
})
app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => {
const parentType = req.params.type == 'task' ? 1 : 2
const parentId = req.params.id
const ids = []
for (const file of req.files) {
const id = await bot.uploadDocument(res.locals.project_id, file.originalname, file.mimetype, file.buffer, parentType, parentId, res.locals.user_id)
ids.push(id)
}
res.redirect(req.baseUrl + `/project/${req.params.pid}/document?id=` + ids.join(','))
})
app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
res.locals.document_id = req.params.did
const doc = db
.prepare(`select * from documents where id = :document_id and project_id = :project_id`)
.get(res.locals)
if (!doc)
throw Error('NOT_FOUND::404')
if (doc.parent_type == 0) {
res.locals.group_id = doc.group_id
const row = db
.prepare(`select 1 from group_users where group_id = :group_id and user_id = :user_id`)
.get(res.locals)
if (row) {
res.locals.can_download = true
}
} else {
res.locals.parent_id = doc.parent_id
const parent = doc.parent_type == 1 ? 'task' : 'meeting'
const row = db
.prepare(`
select 1
from ${parent}s
where id = :parent_id and project_id = :project_id or
exists(select 1 from ${parent}_users where ${parent}_id = :parent_id and user_id = :user_id)
`)
.get(res.locals)
if (row) {
res.locals.can_download = true
res.locals.can_delete = doc.published_by == res.locals.user_id
}
}
next()
})
app.get('/project/:pid(\\d+)/document/:did(\\d+)', async (req, res, next) => {
if (!res.locals.can_download)
throw Error('NOT_FOUND::404')
const file = await bot.downloadDocument(res.locals.project_id, res.locals.document_id)
res.writeHead(200, {
'Content-Length': file.size,
'Content-Type': file.mime,
'Content-Disposition': contentDisposition(file.filename)
})
res.end(file.data)
})
app.delete('/project/:pid(\\d+)/document/:id(\\d+)', (req, res, next) => {
if (!res.locals.can_delete)
throw Error('NOT_FOUND::404')
const doc = db
.prepare(`delete from documents where id = :id and project_id = :project_id`)
.run(res.locals)
res.status(200).json({success: true})
})
app.get('/settings', (req, res, next) => {
const row = db
.prepare(`select coalesce(json_settings, '{}') from users where id = :user_id`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: JSON.parse(row)})
})
app.put('/settings', (req, res, next) => {
res.locals.json_settings = JSON.stringify(req.body || {})
const row = db
.prepare(`update users set json_settings = :json_settings where id = :user_id`)
.run(res.locals)
res.status(200).json({success: true})
})
module.exports = app

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,171 +0,0 @@
pragma foreign_keys = off;
create table if not exists customers (
id integer primary key autoincrement,
name text check(name is null or trim(name) <> '' and length(name) < 256),
email text check(email is null or trim(email) <> '' and length(email) < 128),
password text check(password is null or length(password) > 7 and length(password) < 64),
telegram_id integer,
plan integer,
json_balance text default '{}',
is_blocked integer default 0,
json_company text default '{}',
upload_chat_id integer,
json_backup_server text default '{}',
json_backup_params text default '{}',
json_settings text default '{}'
) strict;
create table if not exists projects (
id integer primary key autoincrement,
customer_id integer references customers(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 256),
description text check(description is null or length(description) < 4096),
logo text,
is_logo_bg integer default 0,
is_archived integer default 0
) strict;
create table if not exists chats (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text,
telegram_id integer,
description text,
logo text,
access_hash integer,
is_channel integer check(is_channel in (0, 1)) default 0,
bot_can_ban integer default 0,
owner_id integer references users(id) on delete set null,
user_count integer,
last_update_time integer
);
create unique index if not exists idx_chats_telegram_id on chats (telegram_id);
create table if not exists users (
id integer primary key autoincrement,
telegram_id integer,
access_hash integer,
firstname text check(firstname is null or length(firstname) < 256),
lastname text check(lastname is null or length(lastname) < 256),
username text check(username is null or length(username) < 256),
photo_id integer,
photo text,
language_code text,
json_settings text default '{}'
) strict;
create unique index if not exists idx_users_telegram_id on users (telegram_id);
create table if not exists user_details (
user_id integer references users(id) on delete cascade,
project_id integer references projects(id) on delete cascade,
fullname text check(fullname is null or length(fullname) < 256),
email text check(email is null or length(email) < 256),
phone text check(phone is null or length(phone) < 256),
role text check(role is null or length(role) < 256),
department text check(department is null or length(department) < 256),
is_blocked integer check(is_blocked in (0, 1)) default 0,
primary key (user_id, project_id)
) strict;
create table if not exists tasks (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 4096),
created_by integer references users(id) on delete set null,
assigned_to integer references users(id) on delete set null,
closed_by integer references users(id) on delete set null,
priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0,
status integer check(status >= 1 and status <= 10) default 1,
time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 44640), -- one month
create_date integer,
plan_date integer,
close_date integer
) strict;
create table if not exists meetings (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 4096),
description text check(description is null or length(description) < 4096),
created_by integer references users(id) on delete set null,
meet_date integer
) strict;
create table if not exists files (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
origin_chat_id integer references chats(id) on delete set null,
origin_message_id integer,
chat_id integer references chats(id) on delete set null,
message_id integer,
file_id integer,
access_hash integer,
filename text not null check(length(filename) < 256),
mime text check(mime is null or length(mime) < 128),
caption text check(caption is null or length(caption) < 4096),
size integer,
published_by integer references users(id) on delete set null,
published integer,
parent_type integer check(parent_type in (0, 1, 2)) default 0,
parent_id integer,
backup_state integer default 0
) strict;
create table if not exists companies (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(length(name) < 4096),
address text check(address is null or length(address) < 512),
email text check(email is null or length(email) < 128),
phone text check(phone is null or length(phone) < 128),
site text check(site is null or length(site) < 128),
description text check(description is null or length(description) < 4096),
logo text
) strict;
create table if not exists company_mappings (
project_id integer references projects(id) on delete cascade,
company_id integer references companies(id) on delete cascade,
show_as_id integer references companies(id) on delete cascade,
show_to_id integer references companies(id) on delete cascade
) strict;
create table if not exists task_users (
task_id integer references tasks(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (task_id, user_id)
) without rowid;
create table if not exists meeting_users (
meeting_id integer references meetings(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (meeting_id, user_id)
) without rowid;
create table if not exists company_users (
company_id integer references companies(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (company_id, user_id)
) without rowid;
create table if not exists chat_users (
chat_id integer references chats(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (chat_id, user_id)
) without rowid;
pragma foreign_keys = on;
create trigger if not exists trg_chats_update after update on chats
when NEW.project_id is null
begin
delete from chat_users where chat_id = NEW.id;
end;

View File

@@ -1,57 +0,0 @@
const fs = require('fs')
const crypto = require('crypto')
const sqlite3 = require('better-sqlite3')
const db = sqlite3(`./data/db.sqlite`)
db.pragma('journal_mode = WAL')
db.exec(fs.readFileSync('./data/init.sql', 'utf8'))
/*
db.exec(`attach database './data/backup.sqlite' as backup`)
db.function('backup', (tblname, ...values) => {
db
.prepare(`insert into backup.${tblname} select ` + values.map(e => '?').join(', '))
.run(values)
})
const backupQuery = db
.prepare(`
select group_concat(tbl || char(13) || trg, char(13) || char(13)) from (
select 'create table if not exists backup.' || t.name || ' (' || group_concat(c.name || ' ' || c.type, ', ') || ', time integer);' tbl,
'create trigger if not exists trg_' || t.name || '_delete after delete on ' || t.name || ' for each row begin ' ||
' select backup (' || t.name || ',' || group_concat('OLD.' || c.name, ', ') || ', strftime(''%s'', ''now'')); end;' trg
from sqlite_master t left join pragma_table_xinfo c on t.tbl_name = c.arg and c.schema = 'main'
where t.sql is not null and t.type = 'table' and t.name <> 'sqlite_sequence'
group by t.type, t.name order by t.type, t.name)
`)
.pluck(true)
.get()
db.exec(backupQuery)
*/
db.function('generate_key', (id, time) => {
return [
'KEY',
Buffer.from(time + '').toString('base64'),
crypto.createHash('md5').update(`sa${time}-${id}lt`).digest('hex')
].join('-')
})
process.on('exit', () => db.close())
process.on('SIGHUP', () => process.exit(128 + 1))
process.on('SIGINT', () => process.exit(128 + 2))
process.on('SIGTERM', () => process.exit(128 + 15))
db.prepareUpdate = function (table, columns, data, where) {
const dataColumns = columns.filter(col => col in data)
if (dataColumns.length == 0)
throw Error('SQLite Error: No data to update')
return db.prepare(`update "${table}" ` +
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
}
module.exports = db

1990
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "telegram-bot",
"version": "1.0.0",
"main": "app.js",
"directories": {
"doc": "docs"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"better-sqlite3": "^11.8.0",
"body-parser": "^1.20.3",
"content-disposition": "^0.5.4",
"cookie-parser": "^1.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"telegram": "^2.26.16"
}
}

View File

@@ -1,337 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="MobileOptimized" content="176" />
<meta name="HandheldFriendly" content="True" />
<meta name="robots" content="noindex,nofollow" />
<script src="https://telegram.org/js/telegram-web-app.js?56"></script>
<title></title>
<style>
*[hidden] {display: none;}
.block {border: 1px black solid; padding:10px; margin-bottom: 10px;}
#settings input {width: 45%;}
#delete {color:red; cursor: pointer; margin-left: 10px;}
</style>
</head>
<body>
<div id = "adminapp" hidden>
<b>Админка</b>
<div id = "settings" class = "block">
<b>Настройки</b><br>
<input id = "name" type = "text" placeholder = "Name">
<div>
<button id = "upload-group" href = "">Не задано</button>
<button id = "upload-group-selector" href = "" admin>Задать</button>
</div>
<b>Company</b>
<div id = "company">
<input id = "company-name" type = "text" placeholder = "Name">
<input id = "company-phone" type = "text" placeholder = "Phone">
<input id = "company-site" type = "text" placeholder = "Site">
<input id = "company-description" type = "text" placeholder = "Description">
</div>
<button id = "update-profile">Update</button>
</div>
<div id = "projects" class = "block">
<b>Список проектов</b><button id = "add-project">Добавить/Обновить</button>
<br>
<input id = "project-id" placeholder = "Id">
<input id = "project-name" placeholder = "Name">
<input id = "project-description" placeholder = "Description">
<br>
<div id = "project-list">Loading...</div>
</div>
<div id = "groups" class = "block">
<b>Список групп</b><button id = "add-group" href="" admin>Добавить</button>
<br>
<div id = "group-list">Проект не выбран</div>
<button id = "share" href = "">Отправить ключ подключения</button>
</div>
<div id = "users" class = "block">
<b>Список пользователей</b>
<br>
<div id = "user-list">Проект не выбран</div>
</div>
<div id = "companies" class = "block">
<b>Список компаний</b><button id = "add-company">Добавить/Обновить</button>
<br>
<input id = "company-id" placeholder = "Id">
<input id = "company-name" placeholder = "Name">
<input id = "company-description" placeholder = "Description">
<br>
<div id = "company-list">Проект не выбран</div>
</div>
</div>
<div id = "miniapp" style = "min-height: 20px; border: 1px black solid; padding:10px; word-break: break-all;" hidden>
Пользов
</div>
<script type="text/javascript">
const $miniapp = document.getElementById('miniapp')
const $adminapp = document.getElementById('adminapp')
$adminapp.querySelector('#settings #update-profile').addEventListener('click', async function (event) {
const $e = this.parentNode
const data = {
name: $e.querySelector('#name').value,
company: {
name: $e.querySelector('#company-name').value,
phone: $e.querySelector('#company-phone').value,
site: $e.querySelector('#company-site').value,
description: $e.querySelector('#company-description').value
}
}
const res = await fetch('/api/admin/customer/profile', {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json())
console.log(res)
})
$adminapp.querySelector('#projects #project-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const res = await fetch('/api/admin/project/' + id, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelector('#projects #project-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.parentNode.id != 'project-list')
return
$adminapp.querySelector('#projects #project-id').value = $e.id
$adminapp.querySelector('#projects #project-name').value = $e.getAttribute('name')
$adminapp.querySelector('#projects #project-description').value = $e.getAttribute('title')
$adminapp.querySelector('#groups #add-group').setAttribute('href', 'https://t.me/ready_or_not_2025_bot?startgroup=' + $e.id)
reloadGroups($e.id)
reloadUsers($e.id)
reloadCompanies($e.id)
const reqKey = await fetch('/api/admin/project/' + $e.id + '/token').then(res => res.json())
const url = 'https://t.me/share/url?url=' + reqKey.data
$adminapp.querySelector('#groups #share').setAttribute('href', url)
})
$adminapp.querySelector('#companies #company-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const project_id = $adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const res = await fetch(`/api/admin/project/${project_id}/company/${id}`, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelector('#companies #company-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.parentNode.id != 'company-list')
return
$adminapp.querySelector('#companies #company-id').value = $e.id
$adminapp.querySelector('#companies #company-name').value = $e.getAttribute('name')
$adminapp.querySelector('#companies #company-description').value = $e.getAttribute('title')
})
$adminapp.querySelector('#groups #group-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const project_id = $adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const res = await fetch(`/api/admin/project/${project_id}/group/${id}`, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelectorAll('button[href]')
.forEach($e => {
$e.addEventListener('click', async function (event) {
// &admin=change_info+delete_messages+restrict_members+invite_users+pin_messages+promote_members
let url = this.getAttribute('href')
if ($e.hasAttribute('admin'))
url += '&admin=change_info+pin_messages'
window.Telegram.WebApp.openTelegramLink(url)
})
})
async function reloadProjects() {
const reqProjects = await fetch('/api/admin/project').then(res => res.json())
const projects = reqProjects.data
const $projectList = $adminapp.querySelector('#projects #project-list')
$projectList.innerHTML = projects.map(e => `<div id = "${e.id}" title = "${e.description}" name = "${e.name}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadGroups(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/group').then(res => res.json())
const groups = req.data
const $groupList = $adminapp.querySelector('#groups #group-list')
$groupList.setAttribute('project-id', projectId)
$groupList.innerHTML = groups.map(e => `<div id = "${e.id}" name = "${e.name}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadCompanies(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/company').then(res => res.json())
const companies = req.data
const $companyList = $adminapp.querySelector('#companies #company-list')
$companyList.setAttribute('project-id', projectId)
$companyList.innerHTML = companies.map(e => `<div id = "${e.id}" name = "${e.name}" title = "${e.description}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadUsers(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/user').then(res => res.json())
const users = req.data
const $userList = $adminapp.querySelector('#users #user-list')
$userList.setAttribute('user-id', projectId)
$userList.innerHTML = users.map(e => `<div id = "${e.id}">${e.firstname} ${e.lastname}</div>`).join('') || 'Пусто'
}
$adminapp.querySelector('#projects #add-project').addEventListener('click', async function (event) {
const $e = this.parentNode
const id = +$e.querySelector('#project-id').value
const data = {
name: $e.querySelector('#project-name').value,
description: $e.querySelector('#project-description').value
}
const url = id ? '/api/admin/project/' + id : '/api/admin/project'
const method = id ? 'PUT' : 'POST'
await fetch(url, {
method,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(() => {
reloadProjects()
$e.querySelector('#project-id').value = ''
$e.querySelector('#project-name').value = ''
$e.querySelector('#project-description').value = ''
}).catch(alert)
})
$adminapp.querySelector('#companies #add-company').addEventListener('click', async function (event) {
const $e = this.parentNode
const project_id = +$adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const id = +$e.querySelector('#company-id').value
const data = {
name: $e.querySelector('#company-name').value,
description: $e.querySelector('#company-description').value
}
const url = '/api/admin/project/' + project_id + '/company' + (id ? '/' + id : '')
const method = id ? 'PUT' : 'POST'
await fetch(url, {
method,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(() => {
reloadCompanies(project_id)
$e.querySelector('#company-id').value = ''
$e.querySelector('#company-name').value = ''
$e.querySelector('#company-description').value = ''
}).catch(alert)
})
try {
(async () => {
const startParams = (Telegram.WebApp.initDataUnsafe.start_param || '').split('_')
const isAdmin = startParams[0] == 'admin'
const $app = isAdmin ? $adminapp : $miniapp
$app.hidden = false
const login_url = isAdmin ? '/api/admin/customer/login?' : '/api/miniapp/user/login?'
const login = await fetch(login_url + Telegram.WebApp.initData, {method: 'POST'}).then(res => res.json())
console.log(login)
if (isAdmin) {
const reqProfile = await fetch('/api/admin/customer/profile').then(res => res.json())
const profile = reqProfile.data
$adminapp.querySelector('#settings #name').value = profile.name || ''
$adminapp.querySelector('#settings #company-name').value = profile.company?.name || ''
$adminapp.querySelector('#settings #company-phone').value = profile.company?.phone || ''
$adminapp.querySelector('#settings #company-site').value = profile.company?.site || ''
$adminapp.querySelector('#settings #company-description').value = profile.company?.description || ''
$adminapp.querySelector('#settings #upload-group-selector').setAttribute('href', 'https://t.me/ready_or_not_2025_bot?startgroup=-' + profile.id)
const $uploadGroup = $adminapp.querySelector('#settings #upload-group')
if (profile.upload_group) {
$uploadGroup.innerHTML = profile.upload_group.name || 'Ошибка'
$uploadGroup.setAttribute('href', 'https://t.me/c/' + profile.upload_group.telegram_id)
} else {
$uploadGroup.innerHTML = 'Не задано'
$uploadGroup.setAttribute('href', '')
}
await reloadProjects()
} else {
// Пользовательский
if (startParams[1])
alert('Группа на проекте ' + startParams[1])
}
})()
} catch (err) {
alert(err)
}
</script>
</body>
</html>

View File

@@ -1,65 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
.block {margin-bottom: 10px}
#projects > div {cursor: pointer}
</style>
<div class = "block">
<label for = "user">USER</label>
<select id = "user"></select>
</div>
<div class = "block">
<div id = "project-title">ПРОЕКТЫ</div>
<div id = "projects">EMPTY</div>
</div>
<div class = "block">
<div id = "member-title">УЧАСТНИКИ</div>
<div id = "members"></div>
</div>
<script type="text/javascript">
(async function () {
try {
const $user = document.getElementById('user')
const $projects = document.getElementById('projects')
const $members = document.getElementById('members')
let members = {}
$user.addEventListener('change', async () => {
const user_id = $user.value
const projects = await fetch('/api/miniapp/project?user_id=' + user_id).then(res => res.json())
$projects.innerHTML = projects.data.map(e =>
`<div id = "${e.id}" title = '${JSON.stringify(e)}'>${e.name} - ${e.description}</div>`
).join('')
$members.innerHTML = ''
})
$projects.addEventListener('click', async (evt) => {
if (evt.target.parentNode != $projects)
return
const _members = await fetch('/api/miniapp/project/' + evt.target.id + '/member?user_id=' + $user.value).then(res => res.json())
members = _members.data
$members.innerHTML = members.map(e =>
`<div id = "${e.id}" title = '${JSON.stringify(e)}' is-blocked = '${e.is_blocked}'>${e.id} - ${e.telegram_name}</div>`
).join('')
})
const user_ids = await fetch('/api/miniapp/user').then(res => res.json())
$user.innerHTML = user_ids.data.map(id => `<option value = '${id}'>${id}</option>`).join('')
$user.dispatchEvent(new Event('change'))
} catch (err) {
console.error(err)
alert(err.message)
}
})();
</script>
</body>
</html>

Binary file not shown.

7
package-lock.json generated
View File

@@ -12,7 +12,6 @@
"@quasar/cli": "^2.5.0", "@quasar/cli": "^2.5.0",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.2.1", "axios": "^1.2.1",
"dayjs": "^1.11.13",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",
@@ -4594,12 +4593,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",

View File

@@ -17,7 +17,6 @@
"@quasar/cli": "^2.5.0", "@quasar/cli": "^2.5.0",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.2.1", "axios": "^1.2.1",
"dayjs": "^1.11.13",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",

View File

@@ -60,40 +60,11 @@ export default defineConfig((ctx) => {
// extendTsConfig (tsConfig) {} // extendTsConfig (tsConfig) {}
}, },
vueRouterMode: 'history', // available values: 'hash', 'history' vueRouterMode: 'history',
// vueDevtools: true, // Должно быть true
// devtool: 'source-map', // Для лучшей отладки
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
// from deepseek
vite: { vite: {
plugins: [ plugins: [
['@intlify/unplugin-vue-i18n/vite', { ['@intlify/unplugin-vue-i18n/vite', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
// you need to set `runtimeOnly: false`
// runtimeOnly: false,
ssr: ctx.modeName === 'ssr', ssr: ctx.modeName === 'ssr',
// you need to set i18n resource including paths ! // you need to set i18n resource including paths !
include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ] include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ]
}], }],
@@ -105,8 +76,12 @@ export default defineConfig((ctx) => {
useFlatConfig: true useFlatConfig: true
} }
}, { server: false }] }, { server: false }]
] ],
},
// Конфигурация сборки Rollup
build: {
}
}
}, },
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver

View File

@@ -7,7 +7,6 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from 'stores/auth' import { useAuthStore } from 'stores/auth'
import { useSettingsStore } from 'stores/settings' import { useSettingsStore } from 'stores/settings'
import { useProjectsStore } from 'stores/projects'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import type { WebApp } from '@twa-dev/types' import type { WebApp } from '@twa-dev/types'
@@ -51,7 +50,6 @@
const authStore = useAuthStore() const authStore = useAuthStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const projectsStore = useProjectsStore()
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null) const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
if (tg.initDataUnsafe.start_param) { if (tg.initDataUnsafe.start_param) {
@@ -59,9 +57,8 @@
} }
onMounted(async () => { onMounted(async () => {
console.log('app mount')
try { try {
if (startRouteInfo.value) projectsStore.setStartRouteInfo(startRouteInfo.value) if (startRouteInfo.value) authStore.setStartRouteInfo(startRouteInfo.value)
if (!authStore.isInit) await authStore.init(tg) if (!authStore.isInit) await authStore.init(tg)
if (!settingsStore.isInit) await settingsStore.init() if (!settingsStore.isInit) await settingsStore.init()
} catch { } catch {

View File

@@ -1,34 +1,40 @@
import { defineBoot } from '#q-app/wrappers' import { defineBoot } from '#q-app/wrappers'
import axios, { type AxiosInstance } from 'axios' import axios, { type AxiosError } from 'axios'
declare module 'vue' { class ServerError extends Error {
interface ComponentCustomProperties { constructor(
$axios: AxiosInstance; public code: string,
$api: AxiosInstance; message: string
) {
super(message)
this.name = 'ServerError'
} }
} }
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({ const api = axios.create({
baseURL: '/api/miniapp', baseURL: '/api/miniapp',
withCredentials: true // Важно для работы с cookies withCredentials: true
}) })
api.interceptors.response.use(
response => response,
async (error: AxiosError<{ error?: { code: string; message: string } }>) => {
const errorData = error.response?.data?.error || {
code: 'ZERO',
message: error.message || 'Unknown error'
}
const serverError = new ServerError(
errorData.code,
errorData.message
)
return Promise.reject(serverError)
}
)
export default defineBoot(({ app }) => { export default defineBoot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api app.config.globalProperties.$api = api
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
}) })
export { api } export { api, ServerError }

View File

@@ -49,7 +49,6 @@
] ]
async function goPersonInfo () { async function goPersonInfo () {
console.log('update')
await router.push({ name: 'person_info' }) await router.push({ name: 'person_info' })
} }
</script> </script>

View File

@@ -296,7 +296,6 @@
return Object.values(validations).every(Boolean) return Object.values(validations).every(Boolean)
}) })
const initialMeeting = ref({} as MeetingParams) const initialMeeting = ref({} as MeetingParams)
onMounted(() => { onMounted(() => {

View File

@@ -17,7 +17,7 @@
accept="image/*" accept="image/*"
/> />
<q-icon <q-icon
v-if="modelValue === '' || modelValue === undefined" v-if="modelValue === '' || modelValue === undefined || modelValue === null"
name="mdi-camera-plus-outline" name="mdi-camera-plus-outline"
class="absolute-full fit text-grey-4" class="absolute-full fit text-grey-4"
:style="{ fontSize: String(iconsize) + 'px'}" :style="{ fontSize: String(iconsize) + 'px'}"

View File

@@ -1,59 +1,67 @@
<template> <template>
<q-dialog <q-dialog v-model="modelValue">
v-model="modelValue" <q-card
class="q-pa-none q-ma-none w100 no-scroll"
align="center"
> >
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
<q-card-section> <q-card-section>
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/> <q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
</q-card-section> </q-card-section>
<q-card-section <q-card-section
class="text-h6 text-bold q-pt-none wrap no-scroll" class="wrap no-scroll q-gutter-y-lg q-pt-none "
style="overflow-wrap: break-word" style="overflow-wrap: break-word"
> >
{{ $t(title)}} <div class="text-h6 text-bold ">
{{ $t(title) }}
</div>
<div v-if="message1">
{{ $t(message1) }}
</div>
<div v-if="message2">
{{ $t(message2) }}
</div>
</q-card-section> </q-card-section>
<q-card-section v-if="message1">
{{ $t(message1)}} <q-card-section>
</q-card-section> <div class="flex column w100 q-mt-lg q-px-sm">
<q-card-section v-if="message2"> <div class="flex q-gutter-md">
{{ $t(message2)}} <div class="col-grow" v-if="auxBtnLabel">
</q-card-section>
<q-card-actions align="center" vertical>
<div class="flex no-wrap w100 justify-center q-gutter-x-md">
<q-btn <q-btn
v-if="auxBtnLabel"
:label="$t(auxBtnLabel)" :label="$t(auxBtnLabel)"
outline outline
color="grey" color="grey"
class="w100"
v-close-popup v-close-popup
rounded rounded
class="w50"
@click="emit('clickAuxBtn')" @click="emit('clickAuxBtn')"
/> />
</div>
<div class="col-grow">
<q-btn <q-btn
:label="$t(mainBtnLabel)" :label="$t(mainBtnLabel)"
:color="color" :color="color"
class="w100"
v-close-popup v-close-popup
rounded rounded
:class="auxBtnLabel ? 'w50' : 'w80'"
@click="emit('clickMainBtn')" @click="emit('clickMainBtn')"
/> />
</div> </div>
</div>
<q-btn <q-btn
class="w80 q-mt-md q-mb-sm" flat class="w100 q-mt-md q-mb-sm" flat
v-close-popup rounded v-close-popup rounded
no-caps
@click="emit('close')" @click="emit('close')"
> >
<div class="flex items-center"> <div class="flex items-center">
{{$t('close')}}
<q-icon name="close"/> <q-icon name="close"/>
{{$t('close')}}
</div> </div>
</q-btn> </q-btn>
</q-card-actions> </div>
</q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
@@ -81,5 +89,5 @@
</script> </script>
<style> <style scoped>
</style> </style>

View File

@@ -8,6 +8,7 @@
rounded color="primary" rounded color="primary"
class="w100 q-mt-md q-mb-xs" class="w100 q-mt-md q-mb-xs"
@click = "emit('update', newFiles)" @click = "emit('update', newFiles)"
:disable="!(isFormValid && (isDirty(initialTask, modelValue) || newFiles.length !== 0))"
> >
{{ $t(btnText) }} {{ $t(btnText) }}
</q-btn> </q-btn>
@@ -275,6 +276,7 @@
import { useUsersStore } from 'stores/users' import { useUsersStore } from 'stores/users'
import { useFilesStore } from 'stores/files' import { useFilesStore } from 'stores/files'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isDirty } from 'helpers/helpers'
import { date } from 'quasar' import { date } from 'quasar'
const { t } = useI18n() const { t } = useI18n()
const filesStore = useFilesStore() const filesStore = useFilesStore()

View File

@@ -36,7 +36,7 @@ body {
} }
.main-content { .main-content {
max-width: 600px; max-width: var(--body-width);
margin: 0 auto; margin: 0 auto;
} }

View File

@@ -4,4 +4,4 @@ import ruRU from './ru-RU'
export default { export default {
'en-US': enUS, 'en-US': enUS,
'ru-RU': ruRU 'ru-RU': ruRU
}; }

File diff suppressed because one or more lines are too long

View File

@@ -122,7 +122,6 @@
} }
function messageUser () { function messageUser () {
console.log((!user.value ? '' : (user.value.username)))
const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? undefined)) const telegramUrl = 'https://t.me/' + (!user.value ? '' : (user.value.username ?? undefined))
if (tg?.platform !== 'unknown') { if (tg?.platform !== 'unknown') {

View File

@@ -31,18 +31,17 @@
:img="project.logo" :img="project.logo"
:name="project.name" :name="project.name"
type="rounded" type="rounded"
size="lg"
/> />
<div class="flex column text-white fit"> <div class="flex column text-white fit">
<div <div
class="text-h6 q-pl-sm" class="text-h6 q-pl-sm text-field"
style="max-width: -webkit-fill-available; white-space: pre-line"
> >
{{ project.name }} {{ project.name }}
</div> </div>
<div <div
class="text-caption q-pl-sm" class="text-caption q-pl-sm text-field"
style="max-width: -webkit-fill-available; white-space: pre-line"
> >
{{ project.description }} {{ project.description }}
</div> </div>
@@ -101,6 +100,7 @@
:img="item.logo" :img="item.logo"
:name="item.name" :name="item.name"
type="rounded" type="rounded"
size="lg"
/> />
</q-item-section> </q-item-section>
<q-item-section :class="item.id === currentProjectId ? 'text-primary !important' : ''"> <q-item-section :class="item.id === currentProjectId ? 'text-primary !important' : ''">
@@ -137,7 +137,8 @@
const currentProjectId = computed(() => projectsStore.currentProjectId) const currentProjectId = computed(() => projectsStore.currentProjectId)
const projects = projectsStore.getProjects const projects = projectsStore.getProjects
const project = computed(() => { const project = computed(() => {
const currentProject = currentProjectId.value && projectsStore.projectById(currentProjectId.value) const currentProject =
currentProjectId.value && projectsStore.projectById(currentProjectId.value)
return currentProject return currentProject
? { ? {
@@ -176,9 +177,14 @@
</script> </script>
<style scoped> <style scoped>
.fix-width-scroll :deep(.q-scrollarea__content){ .text-field {
max-width: -webkit-fill-available;
white-space: pre-line;
}
.fix-width-scroll :deep(.q-scrollarea__content){
width: 100% width: 100%
} }
</style> </style>

View File

@@ -83,6 +83,7 @@
</span> </span>
</q-btn> </q-btn>
</div> </div>
<q-list separator>
<template v-for="item in displayTasks" :key="item.id"> <template v-for="item in displayTasks" :key="item.id">
<q-slide-item <q-slide-item
@right="handleSlideRight($event, item.id)" @right="handleSlideRight($event, item.id)"
@@ -113,6 +114,7 @@
</q-item> </q-item>
</q-slide-item> </q-slide-item>
</template> </template>
</q-list>
</pn-scroll-list> </pn-scroll-list>
<q-page-sticky <q-page-sticky
position="bottom-right" position="bottom-right"

View File

@@ -7,6 +7,7 @@ import {
} from 'vue-router' } from 'vue-router'
import routes from './routes' import routes from './routes'
import { useProjectsStore } from 'stores/projects' import { useProjectsStore } from 'stores/projects'
import { useAuthStore } from 'stores/auth'
export default defineRouter(function (/* { store, ssrContext } */) { export default defineRouter(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
@@ -28,12 +29,13 @@ export default defineRouter(function (/* { store, ssrContext } */) {
if (to.name === 'settings') return if (to.name === 'settings') return
if (to.name === '404') return if (to.name === '404') return
const authStore = useAuthStore()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
console.log('router mount', projectsStore.startRouteInfo)
if (projectsStore.startRouteInfo && to.path === '/') {
const { id, taskId, meetingId } = projectsStore.startRouteInfo if (authStore.startRouteInfo && to.path === '/') {
projectsStore.setStartRouteInfo(null) const { id, taskId, meetingId } = authStore.startRouteInfo
authStore.setStartRouteInfo(null)
if (!projectsStore.isInit) await projectsStore.init() if (!projectsStore.isInit) await projectsStore.init()
const project = projectsStore.projectById(id) const project = projectsStore.projectById(id)

View File

@@ -13,9 +13,17 @@ export const useAuthStore = defineStore('auth', () => {
isInit.value = true isInit.value = true
} }
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
function setStartRouteInfo (info: { id: number; taskId?: number; meetingId?: number } | null) {
startRouteInfo.value = info
}
return { return {
isInit, isInit,
telegramUserData, telegramUserData,
startRouteInfo,
setStartRouteInfo,
init init
} }
}) })

View File

@@ -71,12 +71,6 @@ export const useProjectsStore = defineStore('projects', () => {
const getProjects = computed(() => projects.value) const getProjects = computed(() => projects.value)
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
function setStartRouteInfo (info: { id: number; taskId?: number; meetingId?: number } | null) {
startRouteInfo.value = info
}
watch (currentProjectId, async (newId) => { watch (currentProjectId, async (newId) => {
if (newId) await initStores(); else resetStores() if (newId) await initStores(); else resetStores()
}, { flush: 'sync' }) }, { flush: 'sync' })
@@ -91,8 +85,6 @@ export const useProjectsStore = defineStore('projects', () => {
setCurrentProjectId, setCurrentProjectId,
initStores, initStores,
resetStores, resetStores,
getProjects, getProjects
startRouteInfo,
setStartRouteInfo
} }
}) })

View File

@@ -11,6 +11,7 @@ interface TaskParams {
chat_id: number | null chat_id: number | null
close_files: number[] close_files: number[]
close_comment: string close_comment: string
[key: string]: unknown
} }
interface Task extends TaskParams { interface Task extends TaskParams {