before delete 3software
@@ -8,4 +8,4 @@ API_ID=26746106
|
|||||||
API_HASH=29e5f83c04e635fa583721473a6003b5
|
API_HASH=29e5f83c04e635fa583721473a6003b5
|
||||||
BOT_TOKEN=7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k
|
BOT_TOKEN=7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k
|
||||||
|
|
||||||
BOT_SID=1AgAOMTQ5LjE1NC4xNjcuNTEBu6zLey/IpmODwaKLyTbkKwFDY28LSFPf4UZpgSKCO4bP5gGtFOGVmNDhsJxhMtUWNzhyOX46GyDliNiZ4FUQdoQ6G93DEN8mYcREljmiCp5JchNyZPmhGxl2GeclPo0tp9T/yXFUyo7PD8YpuykHH/MdWVyZxPp93Pjjpi+E03DKCwD00tEpi2TAGzW/MyQ8HUAUIK45nkJA7dnv8Up7NB9LWJ2z+8Dx81oGdVYyOHBL9qy9722LyKtvLD47KpwINjJyZOdhdBM1W8bhsGE4JkHs6DXFXOzrmMrWaE30z4corikkQoNIDL/tXttv+bJULQbbyGZvskbXuvwkV/NVen0=
|
BOT_SID=1AgAOMTQ5LjE1NC4xNjcuNTEBuwHf5TwB0JAqpBNdICUe76iNT43Yz+Telwrt/sVZeS393zNvu1hxB1vnioPR8TdWOatPNETxHQPeqeNJflSiXr3Kvz3qo9Jlubn1rgtmrIedinniF8uK9eQegzEb3jOYM89HPogRiQ581YcGjJsMUNUNGyqup51xZVrzANxm1E2CagQqhZ1vQVPAaGA89PYtt3GkHw61NH8kuV2BmwypS4v2sXuY44324SdhPpKOAVIvi46GxMT79livgQCwNegL0EzbrekhjE1fBi7W39LpyHisbNPc7U6gvd2YoVgEid95+cNS3rJcE6xzH14yNxNzqdK6uTZPWIjgVA4ciFKPtUA=
|
||||||
BIN
backend/_old/backend_v4.zip
Normal file
BIN
backend/_old/backend_v5.zip
Normal file
BIN
backend/_old/backend_v6.zip
Normal file
BIN
backend/_old/backend_v7.zip
Normal file
1
backend/_old/v8/1.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object]
|
||||||
26
backend/_old/v8/2.log
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Listening at port 3000
|
||||||
|
[33m[2025-05-23T22:15:26.439] [INFO] - [Running gramJS version 2.26.21][0m
|
||||||
|
[33m[2025-05-23T22:15:26.443] [INFO] - [Connecting to 149.154.167.51:80/TCPFull...][0m
|
||||||
|
[33m[2025-05-23T22:15:26.492] [INFO] - [Connection to 149.154.167.51:80/TCPFull complete!][0m
|
||||||
|
[33m[2025-05-23T22:15:26.493] [INFO] - [Using LAYER 198 for initial connect][0m
|
||||||
|
UpdateConnectionState {
|
||||||
|
state: 1,
|
||||||
|
_entities: Map(0) {},
|
||||||
|
_client: {
|
||||||
|
session: StringSession {
|
||||||
|
_serverAddress: '149.154.167.51',
|
||||||
|
_dcId: 2,
|
||||||
|
_port: 443,
|
||||||
|
_takeoutId: undefined,
|
||||||
|
_entities: [Set],
|
||||||
|
_updateStates: {},
|
||||||
|
_key: <Buffer 01 df e5 3c 01 d0 90 2a a4 13 5d 20 25 1e ef a8 8d 4f 8d d8 cf e4 de 97 0a ed fe c5 59 79 2d fd df 33 6f bb 58 71 07 5b e7 8a 83 d1 f1 37 56 39 ab 4f ... 206 more bytes>,
|
||||||
|
_authKey: [AuthKey]
|
||||||
|
},
|
||||||
|
apiId: 26746106,
|
||||||
|
apiHash: '29e5f83c04e635fa583721473a6003b5',
|
||||||
|
testServers: false,
|
||||||
|
networkSocket: [class PromisedNetSockets],
|
||||||
|
useWSS: false
|
||||||
|
}
|
||||||
|
}
|
||||||
79687
backend/_old/v8/3.log
Normal file
2
backend/_old/v8/app.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node --env-file .env app
|
||||||
|
pause
|
||||||
79
backend/_old/v8/app.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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({limit: '10mb'}))
|
||||||
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||||
|
Object
|
||||||
|
.keys(req.body || {})
|
||||||
|
.filter(key => typeof(req.body[key]) == 'string' && key != 'password')
|
||||||
|
.map(key => req.body[key] = escapeHtml(req.body[key]))
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
|
||||||
|
const data = Object.assign({}, req.query)
|
||||||
|
delete data.hash
|
||||||
|
const hash = req.query?.hash
|
||||||
|
|
||||||
|
const BOT_TOKEN = process.env.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}`)
|
||||||
|
console.trace()
|
||||||
|
console.log('\n\n')
|
||||||
|
|
||||||
|
let message, code
|
||||||
|
[message, code = 500] = err.message.split('::')
|
||||||
|
res.status(+code).json({success: false, error: { message, code}})
|
||||||
|
})
|
||||||
|
|
||||||
|
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',
|
||||||
|
process.env.BOT_SID || ''
|
||||||
|
)
|
||||||
|
})
|
||||||
861
backend/_old/v8/apps/admin.js
Normal file
@@ -0,0 +1,861 @@
|
|||||||
|
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(`insert into customers (telegram_id) values (:telegram_id) returning id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
createSession(req, res, customer_id)
|
||||||
|
res.status(200).json({success: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
Регистрация нового клиента/Перевод авторизации с 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, telegram_id = null 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. Если коды проходят проверку, то сервер отвечает ОК.
|
||||||
|
5. Отправлются оба кода, новые email и password. Если они проходят проверку, то сервер меняет email и пароль пользователя на новый и возвращает ОК.
|
||||||
|
*/
|
||||||
|
app.post('/auth/email/change-email', (req, res, next) => {
|
||||||
|
const email2 = String(req.body.email ?? '').trim()
|
||||||
|
const code = String(req.body.code ?? '').trim()
|
||||||
|
const code2 = String(req.body.code2 ?? '').trim()
|
||||||
|
const password = String(req.body.password ?? '').trim()
|
||||||
|
|
||||||
|
const email = db
|
||||||
|
.prepare('select email from customers where id = :customer_id')
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
const stepNo = !code ? 1 : code && !email2 ? 2 : code && email2 && !code2 ? 3 : code && email2 && code2 && !password ? 4 : code && email2 && code2 && password ? 5 : -1
|
||||||
|
if (stepNo == -1)
|
||||||
|
throw Error('BAD_STEP::400')
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepNo == 5) {
|
||||||
|
if (!checkPassword(password))
|
||||||
|
throw Error('INCORRECT_PASSWORD::400')
|
||||||
|
|
||||||
|
res.locals.email = email2
|
||||||
|
res.locals.password = password
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepare('update customers set email = :email, password = :password 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 ?? '').trim()
|
||||||
|
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) => {
|
||||||
|
res.locals.time = Math.floor(Date.now() / 1000)
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, email, plan,
|
||||||
|
coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company,
|
||||||
|
upload_chat_id, 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, company_id, is_archived,
|
||||||
|
(select count(*) from chats where project_id = p.id) chat_count,
|
||||||
|
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
|
||||||
|
from projects p
|
||||||
|
where customer_id = :customer_id and p.id = :id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.get({id, customer_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.is_archived = Boolean(row.is_archived)
|
||||||
|
row.is_logo_bg = Boolean(row.is_logo_bg)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, logo, is_logo_bg, company_id, is_archived,
|
||||||
|
(select count(*) from chats where project_id = p.id) chat_count,
|
||||||
|
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
|
||||||
|
from projects p
|
||||||
|
where customer_id = :customer_id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
res.locals.project_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 json_company = db
|
||||||
|
.prepare(`select coalesce(json_company, '{}') from customers where id = :customer_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
res.locals.company_id = addCompany(Object.assign({
|
||||||
|
name: 'My Company',
|
||||||
|
address: null,
|
||||||
|
email: null,
|
||||||
|
phone: null,
|
||||||
|
site: null,
|
||||||
|
description: null,
|
||||||
|
logo: null
|
||||||
|
}, JSON.parse(json_company), {project_id: res.locals.project_id}))
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`update projects set company_id = :company_id where id = :project_id`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
const data = getProject(res.locals.project_id, res.locals.customer_id)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use ('(/project/:pid(\\d+)/*|/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,
|
||||||
|
(select company_id from company_users where user_id = :id) company_id,
|
||||||
|
(select json_group_array(chat_id) from chat_users where user_id = :id and chat_id in (select id from chats where project_id = :project_id)) chats
|
||||||
|
from users u
|
||||||
|
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
||||||
|
where id = :id
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({id, project_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.chats = JSON.parse(row.chats || '[]')
|
||||||
|
row.is_blocked = Boolean(row.is_blocked)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
|
||||||
|
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
|
||||||
|
(select company_id from company_users where user_id = u.id) company_id
|
||||||
|
from users u
|
||||||
|
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
||||||
|
where id in (
|
||||||
|
select user_id
|
||||||
|
from chat_users
|
||||||
|
where chat_id in (select id from chats where project_id = :project_id)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.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 addCompany(data) {
|
||||||
|
return 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(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCompany(id, project_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, address, email, phone, site, description, logo,
|
||||||
|
(select json_group_array(user_id) from company_users where company_id = c.id) users
|
||||||
|
from companies c
|
||||||
|
where c.id = :id and project_id = :project_id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.get({id, project_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.users = JSON.parse(row.users || '[]')
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, address, email, phone, site, description, logo,
|
||||||
|
(select company_id = c.id from projects where id = :project_id) is_own,
|
||||||
|
(select json_group_array(user_id) from company_users where company_id = c.id) users
|
||||||
|
from companies c
|
||||||
|
where project_id = :project_id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
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 = addCompany(res.locals)
|
||||||
|
const data = getCompany(id, req.params.pid)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
||||||
|
res.locals.id = parseInt(req.params.cid)
|
||||||
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.address = req.body?.address
|
||||||
|
res.locals.email = req.body?.email
|
||||||
|
res.locals.phone = req.body?.phone
|
||||||
|
res.locals.site = req.body?.site
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.logo = req.body?.logo
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepareUpdate(
|
||||||
|
'companies',
|
||||||
|
['name', '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 and
|
||||||
|
not exists(select company_id from projects where id = :project_id)`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
res.status(200).json({success: true, 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})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select company_id, json_group_array(show_to_id) show_to_ids
|
||||||
|
from company_mappings
|
||||||
|
where project_id = :project_id and company_id <> show_to_id
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
row.show_to_ids = JSON.parse(row.show_to_ids || '[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
|
||||||
|
if(!(req.body instanceof Array))
|
||||||
|
throw Error('ARRAY_REQUIRED::500')
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`delete from company_mappings where project_id = :project_id`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
req.body
|
||||||
|
.filter(row => Number.isInteger(row.company_id) && row.show_to_ids instanceof Array && row.show_to_ids.every(id => Number.isInteger(id)))
|
||||||
|
.forEach(row => {
|
||||||
|
row.show_to_ids.push(row.company_id)
|
||||||
|
|
||||||
|
const json_ids = row.show_to_ids.join(', ')
|
||||||
|
const check = db
|
||||||
|
.prepare(`select count(1) from companies where project_id = :project_id and id in (${json_ids}) `)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (check.count)
|
||||||
|
return console.error ('IGNORE: ', row)
|
||||||
|
|
||||||
|
const locals = {
|
||||||
|
project_ids: res.locals.project_id,
|
||||||
|
company_id: row.company_id,
|
||||||
|
json_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`
|
||||||
|
insert into company_mappings (project_id, company_id, show_to_id) values
|
||||||
|
select :project_ids, :company_id, value from json_each(:json_ids)
|
||||||
|
`)
|
||||||
|
.run(locals)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
// CHATS
|
||||||
|
function getChat(id, project_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
|
||||||
|
from chats 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, invite_link, description, logo, user_count, bot_can_ban
|
||||||
|
from chats
|
||||||
|
where project_id = :project_id
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
row.is_channel = Boolean(row.is_channel)
|
||||||
|
row.bot_can_ban = Boolean(row.bot_can_ban)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
|
||||||
|
const data = getChat(req.params.gid, req.params.pid)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
|
||||||
|
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
|
||||||
684
backend/_old/v8/apps/bot.js
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const util = require('util')
|
||||||
|
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 debug (msg) {
|
||||||
|
//console.log ('DEBUG: ', msg)
|
||||||
|
fs.appendFileSync('./debug.log', msg instanceof Object ? util.inspect(msg) : msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`insert or ignore into chats (telegram_id) values (:telegram_id)`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.run({ telegram_id: telegramId })
|
||||||
|
|
||||||
|
const chatId = db
|
||||||
|
.prepare(`update chats set is_channel = :is_channel, access_hash = :access_hash, name = :name where telegram_id = :telegram_id 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 (chat_id) {
|
||||||
|
const chat = db
|
||||||
|
.prepare(`select id, telegram_id, access_hash, is_channel from chats where id = :chat_id`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({ chat_id })
|
||||||
|
|
||||||
|
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 invite_link = :invite_link, description = :description, logo = :logo, user_count = :user_count, last_update_time = :last_update_time
|
||||||
|
where id = :chat_id
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.run({
|
||||||
|
chat_id,
|
||||||
|
invite_link: data.fullChat?.exportedInvite?.link,
|
||||||
|
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(chat_id, project_id) {
|
||||||
|
console.log('attachChat: ', chat_id, project_id)
|
||||||
|
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, project_id })
|
||||||
|
|
||||||
|
if (!chat.telegram_id)
|
||||||
|
return console.error('Can\'t attach chat: ' + chat_id + ' to project: ' + project_id)
|
||||||
|
|
||||||
|
console.log('attachChat: build peer')
|
||||||
|
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 p.name from projects p where id = :project_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ project_id })
|
||||||
|
|
||||||
|
console.log('attachChat: send message')
|
||||||
|
const resultBtn = await client.sendMessage(peer, {
|
||||||
|
message,
|
||||||
|
buttons: client.buildReplyMarkup([[Button.url('Открыть проект', `https://t.me/${BOT_NAME}/userapp?startapp=` + project_id)]])
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('attachChat: pin message')
|
||||||
|
await client.invoke(new Api.messages.UpdatePinnedMessage({
|
||||||
|
peer,
|
||||||
|
id: resultBtn.id,
|
||||||
|
unpin: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadChatUsers(chat_id, onlyReset) {
|
||||||
|
console.log('reloadChatUsers: ', chat_id, onlyReset)
|
||||||
|
db
|
||||||
|
.prepare(`delete from chat_users where chat_id = :chat_id`)
|
||||||
|
.run({ chat_id })
|
||||||
|
|
||||||
|
if (onlyReset)
|
||||||
|
return
|
||||||
|
|
||||||
|
const chat = db
|
||||||
|
.prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`)
|
||||||
|
.get({ chat_id })
|
||||||
|
|
||||||
|
if (!chat)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.log('reloadChatUsers: get user')
|
||||||
|
const result = chat.is_channel ?
|
||||||
|
await client.invoke(new Api.channels.GetParticipants({
|
||||||
|
channel: new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }),
|
||||||
|
filter: new Api.ChannelParticipantsRecent(),
|
||||||
|
limit: 999999,
|
||||||
|
offset: 0
|
||||||
|
})) : await client.invoke(new Api.messages.GetFullChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }))
|
||||||
|
|
||||||
|
console.log('reloadChatUsers: process users')
|
||||||
|
const users = result.users.filter(user => !user.bot)
|
||||||
|
for (const user of users) {
|
||||||
|
|
||||||
|
const user_id = registerUser(user.id.value, user)
|
||||||
|
|
||||||
|
if (updateUser(user_id, user)) {
|
||||||
|
await updateUserPhoto (user_id, user)
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`)
|
||||||
|
.run({ chat_id, user_id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('reloadChatUsers: end of user processing')
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onNewServiceMessage (msg, is_channel) {
|
||||||
|
const action = msg.action || {}
|
||||||
|
const tg_chat_id = is_channel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
|
||||||
|
const chat_id = await registerChat(tg_chat_id, is_channel)
|
||||||
|
|
||||||
|
// С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,
|
||||||
|
last_update_time: Math.floor (Date.now() / 1000),
|
||||||
|
telegram_id: tg_chat_id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
console.error('onNewServiceMessage: Can\'t update a chat title: ' + tg_chat_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: tg_chat_id,
|
||||||
|
new_telegram_id: action.channelId.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
console.error('onNewServiceMessage: Can\'t apply a chat migration to channel: ' + tg_chat_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 user_id = registerUser(tgUserId)
|
||||||
|
|
||||||
|
if (isAdd) {
|
||||||
|
try {
|
||||||
|
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
|
||||||
|
updateUser(user_id, user)
|
||||||
|
await updateUserPhoto (user_id, user)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(msg.className + ', ' + user_id + ': ' + 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, user_id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onNewMessage (msg, is_сhannel) {
|
||||||
|
const telegram_id = is_сhannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
|
||||||
|
const chat_id = await registerChat(telegram_id, is_сhannel)
|
||||||
|
|
||||||
|
console.log(msg)
|
||||||
|
const file = msg.media?.document || msg.media?.photo
|
||||||
|
if (file) {
|
||||||
|
const is_photo = file.className == 'Photo'
|
||||||
|
const tg_user_id = msg.senderId?.value
|
||||||
|
|
||||||
|
const filedata = {
|
||||||
|
chat_id,
|
||||||
|
message_id: msg.id,
|
||||||
|
caption: msg.message,
|
||||||
|
published_by: registerUser(tg_user_id),
|
||||||
|
published: msg.date,
|
||||||
|
parent_type: 0,
|
||||||
|
parent_id: null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_photo) {
|
||||||
|
function formatTime(time) {
|
||||||
|
const date = new Date(time * 1000)
|
||||||
|
const isoString = date.toISOString()
|
||||||
|
const [datePart, timePart] = isoString.split('T')
|
||||||
|
const [year, month, day] = datePart.split('-')
|
||||||
|
const [hours, minutes] = timePart.split(':')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}_${hours}-${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
filedata.filename = 'photo_' + formatTime(msg.date) + '.jpg'
|
||||||
|
filedata.mime = 'image/jpeg'
|
||||||
|
console.log(file.sizes)
|
||||||
|
const s = file.sizes.reduce((prev, e) => (prev.w > e.w) ? prev : e)
|
||||||
|
filedata.size = s.size || s.sizes?.reduce((a, b) => Math.max(a, b))
|
||||||
|
} else {
|
||||||
|
filedata.filename = file.attributes?.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName
|
||||||
|
filedata.mime = file.mimeType
|
||||||
|
filedata.size = doc.size?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileAccess(file_id, telegram_file_id, access_hash) {
|
||||||
|
return db
|
||||||
|
.prepare(`update files set file_id = :telegram_file_id, access_hash = :access_hash where id = :file_id returning id`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ file_id, telegram_file_id, access_hash })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tg_user_id != BOT_ID) {
|
||||||
|
const project_id = db
|
||||||
|
.prepare(`select project_id from chats where telegram_id = :telegram_id`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ telegram_id })
|
||||||
|
|
||||||
|
const customer_id = db
|
||||||
|
.prepare(`select customer_id from projects where id = :project_id`)
|
||||||
|
.get({ project_id })
|
||||||
|
|
||||||
|
if (!project_id || !customer_id)
|
||||||
|
return console.error ('Register document: project/customer is not found: ', file, project_id, customer_id)
|
||||||
|
|
||||||
|
filedata.project_id = project_id
|
||||||
|
filedata.id = registerFile (filedata)
|
||||||
|
} else {
|
||||||
|
filedata = db
|
||||||
|
.prepare(`select * from files where chat_id = :chat_id and filename = :filename`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({ chat_id, filename })
|
||||||
|
|
||||||
|
if (!filedata)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileAccess(filedata.id, file.id?.value, file.accessHash?.value)
|
||||||
|
|
||||||
|
const upload_id = db
|
||||||
|
.prepare(`select upload_chat_id from customers where id = (select customer_id from projects where id = :project_id)`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.pluck(true)
|
||||||
|
.get(filedata)
|
||||||
|
|
||||||
|
if (!upload_id)
|
||||||
|
return console.error ('Upload chat is not set. Backup skipped for ', filedata.id)
|
||||||
|
|
||||||
|
if (upload_id == chat_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
let data = file.buffer
|
||||||
|
if (is_photo) {
|
||||||
|
try {
|
||||||
|
const res = await downloadFile(filedata.project_id, filedata.id)
|
||||||
|
data = res.data
|
||||||
|
} catch (err) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data)
|
||||||
|
return console.error ('No data for ', filedata.id)
|
||||||
|
|
||||||
|
const uploaddata = Object.assign({}, filedata, {
|
||||||
|
chat_id: upload_id,
|
||||||
|
data,
|
||||||
|
published_by: null,
|
||||||
|
parent_type: 3,
|
||||||
|
parent_id: filedata.id
|
||||||
|
})
|
||||||
|
|
||||||
|
sendFile(uploaddata)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
if (rows.length)
|
||||||
|
return await sendMessage(chat_id, 'Чат уже используется')
|
||||||
|
|
||||||
|
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(chat_id, 'Время действия ключа для привязки истекло')
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
console.log ('PROJECT_ID: ', row.project_id)
|
||||||
|
if (row.project_id) {
|
||||||
|
await attachChat(chat_id, row.project_id)
|
||||||
|
await reloadChatUsers(chat_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
console.error('Can\'t set upload chat: ' + chat_id + ' to customer: ' + row.customer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onNewUserMessage (msg) {
|
||||||
|
if (msg.message == '/start' && msg.peerId?.className == 'PeerUser') {
|
||||||
|
const tg_user_id = msg.peerId?.userId?.value
|
||||||
|
const user_id = registerUser(tg_user_id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await client.getEntity(new Api.PeerUser({ userId: tg_user_id }))
|
||||||
|
updateUser(user_id, user)
|
||||||
|
await updateUserPhoto (user_id, 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: tg_user_id, 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 + ', ' + user_id + ': ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUpdatePaticipant (update, is_channel) {
|
||||||
|
const tg_chat_id = is_channel ? update.channelId?.value : update.chatId?.value
|
||||||
|
if (!tg_chat_id || update.userId?.value != BOT_ID)
|
||||||
|
return
|
||||||
|
|
||||||
|
const chat_id = await registerChat (tg_chat_id, is_channel)
|
||||||
|
const is_ban = update.prevParticipant && !update.newParticipant
|
||||||
|
const is_add = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant
|
||||||
|
|
||||||
|
if (is_ban || is_add)
|
||||||
|
await reloadChatUsers(chat_id, is_ban)
|
||||||
|
|
||||||
|
if (is_ban) {
|
||||||
|
//db
|
||||||
|
// .prepare(`update chats set project_id = null where id = :chat_id`)
|
||||||
|
// .run({chat_id: chatId})
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot_can_ban = +update.newParticipant?.adminRights?.banUsers || 0
|
||||||
|
db
|
||||||
|
.prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`)
|
||||||
|
.run({ chat_id, bot_can_ban })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function downloadFile(project_id, file_id) {
|
||||||
|
const file = db
|
||||||
|
.prepare(`
|
||||||
|
select file_id, access_hash, '' thumbSize, filename, mime
|
||||||
|
from files where id = :file_id and project_id = :project_id
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({ project_id, file_id })
|
||||||
|
|
||||||
|
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 (chat_id, message) {
|
||||||
|
const chat = db
|
||||||
|
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
|
||||||
|
.get({ chat_id })
|
||||||
|
|
||||||
|
if (!chat)
|
||||||
|
return
|
||||||
|
|
||||||
|
const entity = chat.is_channel ? { channelId: chat.telegram_id, accessHash: chat.access_hash } : { chatId: chat.telegram_id, accessHash: chat.access_hash }
|
||||||
|
const peer = await client.getEntity( chat.is_channel ?
|
||||||
|
new Api.InputPeerChannel(entity) :
|
||||||
|
new Api.InputPeerChat(entity)
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.sendMessage(peer, {message})
|
||||||
|
|
||||||
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
await delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerFile(filedata) {
|
||||||
|
const file_id = db
|
||||||
|
.prepare(`
|
||||||
|
insert into files (project_id, chat_id, message_id, filename, mime, size, caption, published_by, published, parent_type, parent_id) values
|
||||||
|
(:project_id, :chat_id, :message_id, :filename, :mime, :size, :caption, :published_by, :published, :parent_type, :parent_id)
|
||||||
|
returning id
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(filedata)
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendFile(filedata) {
|
||||||
|
const file_id = registerFile(filedata)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chat = db
|
||||||
|
.prepare(`select id, telegram_id, project_id, is_channel, access_hash from chats where id = :chat_id`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({ chat_id: filedata.chat_id })
|
||||||
|
|
||||||
|
if (!chat)
|
||||||
|
throw Error('CHAT_NOT_FOUND::404')
|
||||||
|
|
||||||
|
if (!chat.telegram_id || !chat.access_hash)
|
||||||
|
throw Error('CHAT_INACCESSABLE::404')
|
||||||
|
|
||||||
|
const peer = chat.is_channel ?
|
||||||
|
new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
|
||||||
|
new Api.PeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
|
||||||
|
|
||||||
|
const file = await client.uploadFile({ file: new CustomFile(filedata.filename, filedata.data.length, '', filedata.data), workers: 1 })
|
||||||
|
const media = new Api.InputMediaUploadedDocument({
|
||||||
|
file,
|
||||||
|
mimeType: filedata.mime,
|
||||||
|
attributes: [new Api.DocumentAttributeFilename({ fileName: filedata.filename })]
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.invoke(new Api.messages.SendMedia({
|
||||||
|
peer,
|
||||||
|
media,
|
||||||
|
message: filedata.caption,
|
||||||
|
background: true,
|
||||||
|
silent: true
|
||||||
|
}))
|
||||||
|
} catch (err) {
|
||||||
|
db.prepare(`delete from files where id = :file_id`).get({ file_id })
|
||||||
|
console.error('SendFile', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveChat (chat_id) {
|
||||||
|
const chat = db
|
||||||
|
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
|
||||||
|
.get({ chat_id })
|
||||||
|
|
||||||
|
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, {})
|
||||||
|
|
||||||
|
if (fs.existsSync('./debug.log'))
|
||||||
|
fs.unlinkSync('./debug.log')
|
||||||
|
|
||||||
|
client.addEventHandler(async (update) => {
|
||||||
|
if (update.className == 'UpdateConnectionState')
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug(update)
|
||||||
|
|
||||||
|
if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') {
|
||||||
|
const msg = update?.message
|
||||||
|
const is_channel = update.className == 'UpdateNewChannelMessage' ? 1 : 0
|
||||||
|
|
||||||
|
if (!msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
const result = msg.peerId?.className == 'PeerUser' ? await onNewUserMessage(msg) :
|
||||||
|
msg.className == 'MessageService' ? await onNewServiceMessage(msg, is_channel) :
|
||||||
|
await onNewMessage(msg, is_channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, downloadFile, reloadChatUsers, sendMessage, sendFile }
|
||||||
|
|
||||||
751
backend/_old/v8/apps/miniapp.js
Normal file
@@ -0,0 +1,751 @@
|
|||||||
|
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 projects where id = :project_id and is_archived <> 1`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({project_id}) &&
|
||||||
|
!!db
|
||||||
|
.prepare(`
|
||||||
|
select 1
|
||||||
|
from chat_users
|
||||||
|
where user_id = :user_id and
|
||||||
|
chat_id in (select id from chats where project_id = :project_id) and
|
||||||
|
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1)
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({project_id, user_id})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = {}
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path == '/auth')
|
||||||
|
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('/auth', (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=/api/miniapp`])
|
||||||
|
res.locals.user_id = user_id
|
||||||
|
|
||||||
|
res.status(200).json({success: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project', (req, res, next) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select p.id, p.name, p.description, p.logo, p.is_logo_bg, company_id,
|
||||||
|
c.name customer_name, c.upload_chat_id <> 0 has_upload
|
||||||
|
from projects p
|
||||||
|
inner join customers c on p.customer_id = c.id
|
||||||
|
where p.id in (
|
||||||
|
select project_id
|
||||||
|
from chats
|
||||||
|
where id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
|
) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1)
|
||||||
|
and p.is_archived <> 1
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.is_logo_bg = Boolean(row.is_logo_bg)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
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, company_id from projects where id = :project_id')
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
res.locals.customer_id = row.customer_id
|
||||||
|
res.locals.customer_company_id = row.company_id
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getUserCompanyId(user_id, project_id) {
|
||||||
|
return db
|
||||||
|
.prepare(`
|
||||||
|
select company_id
|
||||||
|
from company_users
|
||||||
|
where user_id = :user_id and company_id in (select id from companies where project_id = :project_id)
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ user_id, project_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
||||||
|
const users = db
|
||||||
|
.prepare(`
|
||||||
|
with actuals (user_id) as (
|
||||||
|
select distinct user_id
|
||||||
|
from chat_users
|
||||||
|
where chat_id in (select id from chats where project_id = :project_id)
|
||||||
|
and chat_id in (select chat_id from chat_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 files 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,
|
||||||
|
ud.fullname,
|
||||||
|
ud.email,
|
||||||
|
ud.phone,
|
||||||
|
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
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
|
||||||
|
|
||||||
|
// Список компаний, которые НЕ ВИДНЫ компании пользователя на проекте
|
||||||
|
const hidden = db
|
||||||
|
.prepare(`
|
||||||
|
select company_id from company_mappings where project_id = :project_id
|
||||||
|
except
|
||||||
|
select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
users
|
||||||
|
.filter(user => user.company_id)
|
||||||
|
.filter(user => hidden.indexOf(user.company_id) != -1)
|
||||||
|
.forEach(user => user.company_id = res.locals.customer_company_id)
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: users})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
|
||||||
|
const chatIds = db
|
||||||
|
.prepare(`select id from chats where project_id = :project_id`)
|
||||||
|
.all(res.locals)
|
||||||
|
.map(e => e.id)
|
||||||
|
|
||||||
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
for (const chatId of chatIds) {
|
||||||
|
await bot.reloadChatUsers(chatId)
|
||||||
|
await sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({success: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
|
||||||
|
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
|
||||||
|
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, address, email, phone, site, description
|
||||||
|
from companies
|
||||||
|
where project_id = :project_id and (
|
||||||
|
id = :company_id or
|
||||||
|
id in (select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id) or
|
||||||
|
id not in (select company_id from company_mappings where project_id = :project_id) or
|
||||||
|
(select :customer_company_id)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
// CHAT
|
||||||
|
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, invite_link, description, telegram_id, owner_id, user_count, logo,
|
||||||
|
(select json_group_array(user_id) from chat_users where chat_id = c.id) users,
|
||||||
|
(select count(1) from tasks where project_id = :project_id and chat_id = c.id) task_count
|
||||||
|
from chats c
|
||||||
|
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.users = JSON.parse(row.users)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TASK
|
||||||
|
function getTask(id, user_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
|
||||||
|
close_date, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id,
|
||||||
|
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
|
||||||
|
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
|
||||||
|
from tasks t
|
||||||
|
where t.id = :id
|
||||||
|
`)
|
||||||
|
.get({id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.close_file_ids = JSON.parse(row.close_file_ids)
|
||||||
|
row.observers = JSON.parse(row.observers)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = row.created_by == user_id || row.assigned_to == user_id
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
|
||||||
|
close_date, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id,
|
||||||
|
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
|
||||||
|
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
|
||||||
|
from tasks t
|
||||||
|
where project_id = :project_id and (created_by = :user_id or
|
||||||
|
assigned_to = :user_id or
|
||||||
|
exists(select 1 from task_users where task_id = t.id and user_id = :user_id) or
|
||||||
|
exists(select 1 from chat_users where chat_id = t.chat_id))
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.close_file_ids = JSON.parse(row.close_file_ids)
|
||||||
|
row.observers = JSON.parse(row.observers)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
|
||||||
|
throw Error('INCORRECT_ASSIGNED_TO::400')
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
|
const id = db
|
||||||
|
.prepare(`
|
||||||
|
insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date)
|
||||||
|
values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date)
|
||||||
|
returning id
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
const task = getTask(id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 t
|
||||||
|
where id = :task_id and project_id = :project_id and (
|
||||||
|
created_by = :user_id or
|
||||||
|
assigned_to = :user_id or
|
||||||
|
exists(select 1 from task_users where task_id = :task_id and user_id = :user_id) or
|
||||||
|
exists(select 1 from chat_users where chat_id = t.chat_id))
|
||||||
|
`)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
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.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
|
const task = getTask(req.params.tid, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
|
if (!res.locals.is_author && !res.locals.is_assigned)
|
||||||
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
|
res.locals.id = res.locals.task_id
|
||||||
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.status = parseInt(req.body?.status)
|
||||||
|
res.locals.priority = parseInt(req.body?.priority)
|
||||||
|
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
|
||||||
|
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
|
const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_file_ids', 'chat_id'] : ['status', 'time_spent', 'close_comment', 'json_close_file_ids']
|
||||||
|
const info = db
|
||||||
|
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
const task = getTask(res.locals.task_id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
|
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, data: {id: res.locals.task_id}})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 chat_users
|
||||||
|
where chat_id in (select id from chats 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
|
||||||
|
function getMeeting(id, user_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, place, created_by, meet_date, chat_id, is_cancel,
|
||||||
|
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
|
||||||
|
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files
|
||||||
|
from meetings m
|
||||||
|
where m.id = :id
|
||||||
|
`)
|
||||||
|
.get({id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.participants = JSON.parse(row.participants)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = row.created_by == user_id
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, place, created_by, meet_date, duration, chat_id, is_cancel,
|
||||||
|
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
|
||||||
|
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files,
|
||||||
|
created_by = :user_id is_editable
|
||||||
|
from meetings m
|
||||||
|
where project_id = :project_id and
|
||||||
|
(created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id))
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.participants = JSON.parse(row.participants)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = Boolean(row.is_editable)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/project/:pid(\\d+)/meeting', (req, res, next) => {
|
||||||
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.place = req.body?.place
|
||||||
|
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
||||||
|
res.locals.duration = req.body?.duration ? parseInt(req.body?.duration) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
|
const id = db
|
||||||
|
.prepare(`
|
||||||
|
insert into meetings (project_id, name, description, place, created_by, meet_date, duration)
|
||||||
|
values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration)
|
||||||
|
returning id
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
const meeting = getMeeting(id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
|
})
|
||||||
|
|
||||||
|
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.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
|
const meeting = getMeeting(req.params.mid, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
|
if (!res.locals.is_author)
|
||||||
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
|
res.locals.id = res.locals.meeting_id
|
||||||
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.place = req.body?.place
|
||||||
|
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
res.locals.is_cancel = +!!req.body?.is_cancel
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepareUpdate('meetings', ['name', 'description', 'place', 'meet_date', 'chat_id', 'is_cancel'], res.locals, ['id', 'project_id'])
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
const meeting = getMeeting(res.locals.meeting_id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
|
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, data: {id: res.locals.meeting_id}})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (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 chat_users
|
||||||
|
where chat_id in (select id from chats where project_id = :project_id)
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.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, data: user_ids })
|
||||||
|
})
|
||||||
|
|
||||||
|
// FILES
|
||||||
|
app.get('/project/:pid(\\d+)/file', (req, res, next) => {
|
||||||
|
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
|
||||||
|
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
|
||||||
|
// 3. Из встреч на проекте, где пользователь создатель или участник
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select f.id, f.chat_id, c.telegram_id telegram_chat_id, f.message_id, f.filename, f.mime, f.caption, f.size, f.published_by, f.published, f.parent_id, f.parent_type
|
||||||
|
from files f
|
||||||
|
left join chats c on f.chat_id = c.id and f.parent_type = 0
|
||||||
|
where f.project_id = :project_id and (
|
||||||
|
chat_id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
|
or
|
||||||
|
parent_type = 1 and parent_id in (
|
||||||
|
select id
|
||||||
|
from tasks t
|
||||||
|
where project_id = :project_id and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id))
|
||||||
|
)
|
||||||
|
or
|
||||||
|
parent_type = 2 and parent_id in (
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => {
|
||||||
|
res.locals.parent_id = req.params.id
|
||||||
|
res.locals.parent_type = req.params.type == 'task' ? 1 : 2
|
||||||
|
|
||||||
|
const chat_id = db
|
||||||
|
.prepare(`
|
||||||
|
select coalesce(chat_id, (select upload_chat_id from customers where id = :customer_id))
|
||||||
|
from ${req.params.type}s
|
||||||
|
where id = :parent_id and project_id = :project_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (!chat_id)
|
||||||
|
throw Error('EMPTY_DESTINATION::500')
|
||||||
|
|
||||||
|
const file_ids = []
|
||||||
|
for (const file of req.files) {
|
||||||
|
if (file.size == 0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
const filedata = {
|
||||||
|
project_id: req.params.pid,
|
||||||
|
chat_id,
|
||||||
|
filename: file.originalname,
|
||||||
|
mime: file.mimetype,
|
||||||
|
data: file.buffer,
|
||||||
|
size: file.size,
|
||||||
|
published_by: res.locals.user_id,
|
||||||
|
pablished: Math.floor(Date.now() / 1000),
|
||||||
|
parent_type: res.locals.parent_type,
|
||||||
|
parent_id: req.params.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const file_id = await bot.sendFile(filedata)
|
||||||
|
if (file_id)
|
||||||
|
file_ids.push(file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_ids.length == 0)
|
||||||
|
throw Error('EMPTY_UPLOAD::500')
|
||||||
|
|
||||||
|
const files = db
|
||||||
|
.prepare(`select id, chat_id, message_id, filename, mime, size, published_by, published from files where id in (` + file_ids.join(',') + `)`)
|
||||||
|
.all()
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: files})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
|
||||||
|
res.locals.file_id = req.params.fid
|
||||||
|
|
||||||
|
const file = db
|
||||||
|
.prepare(`select * from files where id = :file_id and project_id = :project_id`)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (!file)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
if (file.parent_type == 0) {
|
||||||
|
res.locals.chat_id = file.chat_id
|
||||||
|
const row = db
|
||||||
|
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
res.locals.can_download = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.locals.parent_id = file.parent_id
|
||||||
|
const parent = file.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 = file.published_by == res.locals.user_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => {
|
||||||
|
if (!res.locals.can_download)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
const file = await bot.downloadFile(res.locals.project_id, res.locals.file_id)
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Length': file.size,
|
||||||
|
'Content-Type': file.mime,
|
||||||
|
'Content-Disposition': contentDisposition(file.filename)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.end(file.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
|
||||||
|
if (!res.locals.can_delete)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepare(`delete from files where id = :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: res.locals.file_id}})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
22
backend/_old/v8/changes.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
СТРУКТУРА БАЗЫ
|
||||||
|
1. Переименована таблица documents в files
|
||||||
|
2. В таблицу files добавлено поле published, содержащее дату публикации файла
|
||||||
|
3. В таблице chats
|
||||||
|
* Добавлена колонка owner_id (ссылка на users.id; пусто)
|
||||||
|
* Добавлена колонка description
|
||||||
|
* Переименована колонка logo_url в logo
|
||||||
|
|
||||||
|
|
||||||
|
МИНИАПП
|
||||||
|
1. Операции POST/PUT возвращают объект
|
||||||
|
2. Операция DELETE возвращает data: {id: id-удаленого объекта}
|
||||||
|
3. Роут /project/:id/meeting/:id/participants заменен на /project/:id/meeting/:id/participant
|
||||||
|
4. В роут /project/:pid(\\d+)/file добавлен вывод origin_tg_chat_id для возможности сформировать ссылку на чат/сообщение на клиенте
|
||||||
|
5. Роут /project/:id/task или meeting/:id/attach возвращает описания файлов, а не просто их id
|
||||||
|
|
||||||
|
|
||||||
|
АДМИНКА
|
||||||
|
|
||||||
|
Общее
|
||||||
|
1. Файл /docs/api.xls обновлен, удалены ненужные листы
|
||||||
|
2. В коде document заменен на file
|
||||||
BIN
backend/_old/v8/data/db.sqlite
Normal file
182
backend/_old/v8/data/init.sql
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
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,
|
||||||
|
company_id integer references companies(id) on delete cascade,
|
||||||
|
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,
|
||||||
|
invite_link text,
|
||||||
|
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) < 256),
|
||||||
|
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 < 3 * 44640), -- one month
|
||||||
|
create_date integer,
|
||||||
|
plan_date integer,
|
||||||
|
close_date integer,
|
||||||
|
close_comment text check(close_comment is null or length(close_comment) < 1024),
|
||||||
|
json_close_file_ids text,
|
||||||
|
chat_id integer references chats(id) on delete set null
|
||||||
|
) strict;
|
||||||
|
create index if not exists idx_tasks_project_id on tasks (project_id);
|
||||||
|
|
||||||
|
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) < 256),
|
||||||
|
description text check(description is null or length(description) < 1024),
|
||||||
|
place text check(place is null or length(place) < 4096),
|
||||||
|
created_by integer references users(id) on delete set null,
|
||||||
|
meet_date integer,
|
||||||
|
duration integer,
|
||||||
|
chat_id integer references chats(id) on delete set null,
|
||||||
|
is_cancel integer default 0
|
||||||
|
) strict;
|
||||||
|
create index if not exists idx_meetings_project_id on meetings (project_id);
|
||||||
|
|
||||||
|
create table if not exists files (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
project_id integer references projects(id) on delete cascade,
|
||||||
|
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),
|
||||||
|
size integer,
|
||||||
|
caption text check(caption is null or length(caption) < 2048),
|
||||||
|
published_by integer references users(id) on delete set null,
|
||||||
|
published integer,
|
||||||
|
parent_type integer check(parent_type in (0, 1, 2, 3)) default 0,
|
||||||
|
parent_id integer
|
||||||
|
) strict;
|
||||||
|
create index if not exists idx_files_project_id on files (project_id);
|
||||||
|
|
||||||
|
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 index if not exists idx_companies_project_id on companies (project_id);
|
||||||
|
|
||||||
|
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 index if not exists idx_company_mappings_project_id on company_mappings (project_id);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
167050
backend/_old/v8/debug.log
Normal file
BIN
backend/_old/v8/docs/api.xls
Normal file
46
backend/_old/v8/include/db.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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.pragma('foreign_keys = on')
|
||||||
|
|
||||||
|
db.exec(fs.readFileSync('./data/init.sql', 'utf8'))
|
||||||
|
|
||||||
|
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 '))
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepareUpsert = 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(
|
||||||
|
`insert into "${table}" (` + [...dataColumns, ...where].join(', ') + `) values (:` + [...dataColumns, ...where].join(', :') +
|
||||||
|
`) on conflict (` + where.join(',') + `) do update ` +
|
||||||
|
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
|
||||||
|
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = db
|
||||||
1991
backend/_old/v8/package-lock.json
generated
Normal file
25
backend/_old/v8/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
node --env-file .env app
|
node --env-file .env app
|
||||||
|
pause
|
||||||
@@ -1,20 +1,39 @@
|
|||||||
|
const http = require('http')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const WebSocket = require('ws')
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
|
const cookie = require('cookie')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const fs = require('fs')
|
|
||||||
const util = require('util')
|
|
||||||
const bot = require('./apps/bot')
|
const bot = require('./apps/bot')
|
||||||
|
const log = require('./include/log')
|
||||||
|
|
||||||
const app = express()
|
const adminapp = require('./apps/admin')
|
||||||
|
const miniapp = require('./apps/miniapp')
|
||||||
app.use(bodyParser.json({limit: '10mb'}))
|
|
||||||
app.use(cookieParser())
|
|
||||||
|
|
||||||
BigInt.prototype.toJSON = function () {
|
BigInt.prototype.toJSON = function () {
|
||||||
return Number(this)
|
return Number(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const server = http.createServer(app)
|
||||||
|
const wss = new WebSocket.Server({ server })
|
||||||
|
|
||||||
|
app.use(bodyParser.json({limit: '10mb'}))
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
const end = res.end
|
||||||
|
res.end = function (chunk, encoding) {
|
||||||
|
log.http (req, res, Date.now() - start)
|
||||||
|
end.apply (res, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if(!(req.body instanceof Object))
|
if(!(req.body instanceof Object))
|
||||||
return next()
|
return next()
|
||||||
@@ -31,7 +50,6 @@ app.use((req, res, next) => {
|
|||||||
app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
|
app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
|
||||||
const data = Object.assign({}, req.query)
|
const data = Object.assign({}, req.query)
|
||||||
delete data.hash
|
delete data.hash
|
||||||
const hash = req.query?.hash
|
|
||||||
|
|
||||||
const BOT_TOKEN = process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
|
const BOT_TOKEN = process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
|
||||||
const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join('\n')
|
const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join('\n')
|
||||||
@@ -40,7 +58,7 @@ app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
|
|||||||
|
|
||||||
const timeDiff = Date.now() / 1000 - data.auth_date
|
const timeDiff = Date.now() / 1000 - data.auth_date
|
||||||
|
|
||||||
if (hmac !== req.query.hash) // || timeDiff > 10)
|
if (hmac !== req.query?.hash) // || timeDiff > 10)
|
||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
const user = JSON.parse(req.query.user)
|
const user = JSON.parse(req.query.user)
|
||||||
@@ -53,26 +71,31 @@ app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use('/api/admin', adminapp.router)
|
||||||
app.use('/api/admin', require('./apps/admin'))
|
app.use('/api/miniapp', miniapp.router)
|
||||||
app.use('/api/miniapp', require('./apps/miniapp'))
|
|
||||||
|
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error(`Error for ${req.path}: ${err}`)
|
console.error(`Error for ${req.path}: ${err}`)
|
||||||
console.trace()
|
log.error(err)
|
||||||
console.log('\n\n')
|
|
||||||
|
|
||||||
let message, code
|
let message, code
|
||||||
[message, code = 500] = err.message.split('::')
|
[message, code = 500] = err.message.split('::')
|
||||||
res.status(+code).json({success: false, error: { message, code}})
|
res.status(+code).json({success: false, error: { message, code}})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
wss.on('connection', (ws, req) => {
|
||||||
|
const sid = cookie.parse(req.headers.cookie || '')?.sid
|
||||||
|
if (!miniapp.registerWS(sid, ws) && !adminapp.registerWS(sid, ws))
|
||||||
|
return ws.close(1008, 'Unauthorized')
|
||||||
|
})
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000
|
const PORT = process.env.PORT || 3000
|
||||||
app.listen(PORT, async () => {
|
server.listen(PORT, async () => {
|
||||||
console.log(`Listening at port ${PORT}`)
|
console.log(`Listening at port ${PORT}`)
|
||||||
bot.start(
|
bot.start(
|
||||||
+(process.env.API_ID || 26746106),
|
+(process.env.API_ID || 26746106),
|
||||||
process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5',
|
process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5',
|
||||||
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
|
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k',
|
||||||
|
process.env.BOT_SID || ''
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const WebSocket = require('ws')
|
||||||
const db = require('../include/db')
|
const db = require('../include/db')
|
||||||
|
const log = require('../include/log')
|
||||||
|
const eventBus = require('../include/eventbus')
|
||||||
const bot = require('./bot')
|
const bot = require('./bot')
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
const app = express.Router()
|
const app = express.Router()
|
||||||
|
|
||||||
const sessions = {}
|
const sessions = {}
|
||||||
|
|
||||||
|
function registerWS(sid, ws) {
|
||||||
|
const session = sessions[sid]
|
||||||
|
if (session)
|
||||||
|
session.ws = ws
|
||||||
|
|
||||||
|
return !!session
|
||||||
|
}
|
||||||
|
|
||||||
const cache = {
|
const cache = {
|
||||||
// email -> code
|
// email -> code
|
||||||
register: {},
|
register: {},
|
||||||
|
upgrade: {},
|
||||||
recovery: {},
|
recovery: {},
|
||||||
'change-password': {},
|
'change-password': {},
|
||||||
'change-email': {},
|
'change-email': {},
|
||||||
@@ -42,8 +54,8 @@ app.use((req, res, next) => {
|
|||||||
if (public.includes(req.path))
|
if (public.includes(req.path))
|
||||||
return next()
|
return next()
|
||||||
|
|
||||||
const asid = req.query.asid || req.cookies.asid
|
const sid = req.query.sid || req.cookies.sid
|
||||||
req.session = sessions[asid]
|
req.session = sessions[sid]
|
||||||
if (!req.session)
|
if (!req.session)
|
||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
@@ -57,9 +69,9 @@ function createSession(req, res, customer_id) {
|
|||||||
throw Error('AUTH_ERROR::500')
|
throw Error('AUTH_ERROR::500')
|
||||||
|
|
||||||
res.locals.customer_id = customer_id
|
res.locals.customer_id = customer_id
|
||||||
const asid = crypto.randomBytes(64).toString('hex')
|
const sid = crypto.randomBytes(64).toString('hex')
|
||||||
req.session = sessions[asid] = {asid, customer_id }
|
req.session = sessions[sid] = {sid, customer_id }
|
||||||
res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`])
|
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/admin`])
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/auth/email', (req, res, next) => {
|
app.post('/auth/email', (req, res, next) => {
|
||||||
@@ -67,7 +79,7 @@ app.post('/auth/email', (req, res, next) => {
|
|||||||
res.locals.password = req.body?.password
|
res.locals.password = req.body?.password
|
||||||
|
|
||||||
const customer_id = db
|
const customer_id = db
|
||||||
.prepare(`select id from customers where is_blocked = 0 and email = :email and password is not null and password = :password `)
|
.prepare(`select id from customers where email = :email and password is not null and password = :password `)
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
@@ -75,7 +87,7 @@ app.post('/auth/email', (req, res, next) => {
|
|||||||
throw Error('AUTH_ERROR::401')
|
throw Error('AUTH_ERROR::401')
|
||||||
|
|
||||||
createSession(req, res, customer_id)
|
createSession(req, res, customer_id)
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/auth/telegram', (req, res, next) => {
|
app.post('/auth/telegram', (req, res, next) => {
|
||||||
@@ -83,24 +95,25 @@ app.post('/auth/telegram', (req, res, next) => {
|
|||||||
.prepare(`select id from customers where telegram_id = :telegram_id`)
|
.prepare(`select id from customers where telegram_id = :telegram_id`)
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals) || db
|
.get(res.locals) || db
|
||||||
.prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`)
|
.prepare(`insert into customers (telegram_id) values (:telegram_id) returning id`)
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
createSession(req, res, customer_id)
|
createSession(req, res, customer_id)
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Регистрация нового клиента выполняется за ТРИ последовательных вызова
|
Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова
|
||||||
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
|
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
|
||||||
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
|
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
|
||||||
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
|
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
|
||||||
*/
|
*/
|
||||||
app.post('/auth/email/register', (req, res, next) => {
|
app.post('/auth/email/:action(register|upgrade)', (req, res, next) => {
|
||||||
const email = String(req.body.email ?? '').trim()
|
const email = String(req.body.email ?? '').trim()
|
||||||
const code = String(req.body.code ?? '').trim()
|
const code = String(req.body.code ?? '').trim()
|
||||||
const password = String(req.body.password ?? '').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
|
const stepNo = email && !code ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1
|
||||||
if (stepNo == -1)
|
if (stepNo == -1)
|
||||||
@@ -119,12 +132,12 @@ app.post('/auth/email/register', (req, res, next) => {
|
|||||||
throw Error('USED_EMAIL::400')
|
throw Error('USED_EMAIL::400')
|
||||||
|
|
||||||
const code = Math.random().toString().substr(2, 4)
|
const code = Math.random().toString().substr(2, 4)
|
||||||
cache.register[email] = code
|
cache[action][email] = code
|
||||||
sendEmail(email, 'REGISTER', `${email} => ${code}`)
|
sendEmail(email, action.toUpperCase(), `${email} => ${code}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stepNo == 2) {
|
if (stepNo == 2) {
|
||||||
if (cache.register[email] != code)
|
if (cache[action][email] != code)
|
||||||
throw Error('INCORRECT_CODE::400')
|
throw Error('INCORRECT_CODE::400')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +145,11 @@ app.post('/auth/email/register', (req, res, next) => {
|
|||||||
if (!checkPassword(password))
|
if (!checkPassword(password))
|
||||||
throw Error('INCORRECT_PASSWORD::400')
|
throw Error('INCORRECT_PASSWORD::400')
|
||||||
|
|
||||||
db
|
const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' :
|
||||||
.prepare('insert into customers (email, password) values (:email, :password)')
|
'update customers set email = :email, password = :password, telegram_id = null where id = :id'
|
||||||
.run({email, password})
|
db.prepare(query).run({email, password, id: res.locals.customer_id})
|
||||||
|
|
||||||
delete cache.register[email]
|
delete cache[action][email]
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true})
|
||||||
@@ -144,23 +157,25 @@ app.post('/auth/email/register', (req, res, next) => {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Смена email выполняется за ЧЕТЫРЕ последовательных вызовов
|
Смена email выполняется за ПЯТЬ последовательных вызовов
|
||||||
1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код.
|
1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код.
|
||||||
2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК.
|
2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК.
|
||||||
3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email.
|
3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email.
|
||||||
4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет email пользователя на новый и возвращает ОК.
|
4. Отправлются оба кода и новый email. Если коды проходят проверку, то сервер отвечает ОК.
|
||||||
|
5. Отправлются оба кода, новые email и password. Если они проходят проверку, то сервер меняет email и пароль пользователя на новый и возвращает ОК.
|
||||||
*/
|
*/
|
||||||
app.post('/auth/email/change-email', (req, res, next) => {
|
app.post('/auth/email/change-email', (req, res, next) => {
|
||||||
const email2 = String(req.body.email ?? '').trim()
|
const email2 = String(req.body.email ?? '').trim()
|
||||||
const code = String(req.body.code ?? '').trim()
|
const code = String(req.body.code ?? '').trim()
|
||||||
const code2 = String(req.body.code2 ?? '').trim()
|
const code2 = String(req.body.code2 ?? '').trim()
|
||||||
|
const password = String(req.body.password ?? '').trim()
|
||||||
|
|
||||||
const email = db
|
const email = db
|
||||||
.prepare('select email from customers where id = :customer_id')
|
.prepare('select email from customers where id = :customer_id')
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
const stepNo = !code ? 1 : code && !email ? 2 : code && email && !code2 ? 3 : code && email && code2 ? 4 : -1
|
const stepNo = !code ? 1 : code && !email2 ? 2 : code && email2 && !code2 ? 3 : code && email2 && code2 && !password ? 4 : code && email2 && code2 && password ? 5 : -1
|
||||||
if (stepNo == -1)
|
if (stepNo == -1)
|
||||||
throw Error('BAD_STEP::400')
|
throw Error('BAD_STEP::400')
|
||||||
|
|
||||||
@@ -187,9 +202,17 @@ app.post('/auth/email/change-email', (req, res, next) => {
|
|||||||
if (stepNo == 4) {
|
if (stepNo == 4) {
|
||||||
if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2)
|
if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2)
|
||||||
throw Error('INCORRECT_CODE::400')
|
throw Error('INCORRECT_CODE::400')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepNo == 5) {
|
||||||
|
if (!checkPassword(password))
|
||||||
|
throw Error('INCORRECT_PASSWORD::400')
|
||||||
|
|
||||||
|
res.locals.email = email2
|
||||||
|
res.locals.password = password
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepare('update customers set email = :email where id = :customer_id')
|
.prepare('update customers set email = :email, password = :password where id = :customer_id')
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
@@ -210,7 +233,7 @@ app.post('/auth/email/change-email', (req, res, next) => {
|
|||||||
*/
|
*/
|
||||||
app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
|
app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
|
||||||
const code = String(req.body.code ?? '').trim()
|
const code = String(req.body.code ?? '').trim()
|
||||||
const password = String(req.body.password)
|
const password = String(req.body.password ?? '').trim()
|
||||||
const action = req.params.action
|
const action = req.params.action
|
||||||
|
|
||||||
const email = action == 'change-password' ? db
|
const email = action == 'change-password' ? db
|
||||||
@@ -260,18 +283,21 @@ app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.get('/auth/logout', (req, res, next) => {
|
app.get('/auth/logout', (req, res, next) => {
|
||||||
if (req.session?.asid)
|
if (req.session?.sid)
|
||||||
delete sessions[req.session.asid]
|
delete sessions[req.session.sid]
|
||||||
|
|
||||||
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`])
|
res.setHeader('Set-Cookie', [`sid=; expired; httpOnly;path=/api/admin`])
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true})
|
||||||
})
|
})
|
||||||
|
|
||||||
// CUSTOMER
|
// CUSTOMER
|
||||||
app.get('/customer/profile', (req, res, next) => {
|
app.get('/customer/profile', (req, res, next) => {
|
||||||
|
res.locals.time = Math.floor(Date.now() / 1000)
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_chat_id
|
select id, name, email, plan,
|
||||||
|
coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company,
|
||||||
|
upload_chat_id, generate_key(-id, :time) upload_token
|
||||||
from customers
|
from customers
|
||||||
where id = :customer_id
|
where id = :customer_id
|
||||||
`)
|
`)
|
||||||
@@ -298,6 +324,8 @@ app.get('/customer/profile', (req, res, next) => {
|
|||||||
app.put('/customer/profile', (req, res, next) => {
|
app.put('/customer/profile', (req, res, next) => {
|
||||||
if (req.body.company instanceof Object)
|
if (req.body.company instanceof Object)
|
||||||
req.body.json_company = JSON.stringify(req.body.company)
|
req.body.json_company = JSON.stringify(req.body.company)
|
||||||
|
else
|
||||||
|
delete req.body?.json_company
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepareUpdate(
|
.prepareUpdate(
|
||||||
@@ -333,96 +361,86 @@ app.put('/customer/settings', (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// PROJECT
|
// PROJECT
|
||||||
app.get('/project', (req, res, next) => {
|
function getProject(id, customer_id) {
|
||||||
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
|
const row = db
|
||||||
|
|
||||||
const rows = db
|
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, description, logo, is_logo_bg, is_archived,
|
select id, name, description, logo, is_logo_bg, company_id, is_archived,
|
||||||
(select count(*) from chats where project_id = p.id) chat_count,
|
(select count(*) 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
|
(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
|
from projects p
|
||||||
where customer_id = :customer_id ${where}
|
where customer_id = :customer_id and p.id = :id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.get({id, customer_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.is_archived = Boolean(row.is_archived)
|
||||||
|
row.is_logo_bg = Boolean(row.is_logo_bg)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, logo, is_logo_bg, company_id, is_archived,
|
||||||
|
(select count(*) from chats where project_id = p.id) chat_count,
|
||||||
|
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
|
||||||
|
from projects p
|
||||||
|
where customer_id = :customer_id
|
||||||
order by name
|
order by name
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
data.forEach(row => {
|
||||||
throw Error('NOT_FOUND::404')
|
row.is_archived = Boolean(row.is_archived)
|
||||||
|
row.is_logo_bg = Boolean(row.is_logo_bg)
|
||||||
|
})
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
res.status(200).json({success: true, data})
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)', (req, res, next) => {
|
|
||||||
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/project', (req, res, next) => {
|
app.post('/project', (req, res, next) => {
|
||||||
res.locals.name = req.body?.name
|
res.locals.name = req.body?.name
|
||||||
res.locals.description = req.body?.description
|
res.locals.description = req.body?.description
|
||||||
res.locals.logo = req.body?.logo
|
res.locals.logo = req.body?.logo
|
||||||
|
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
|
||||||
|
|
||||||
const id = db
|
res.locals.project_id = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
insert into projects (customer_id, name, description, logo)
|
insert into projects (customer_id, name, description, logo, is_logo_bg)
|
||||||
values (:customer_id, :name, :description, :logo)
|
values (:customer_id, :name, :description, :logo, :is_logo_bg)
|
||||||
returning id
|
returning id
|
||||||
`)
|
`)
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
res.redirect(req.baseUrl + `/project?id=${id}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)', (req, res, next) => {
|
|
||||||
res.locals.id = req.params.pid
|
|
||||||
res.locals.name = req.body?.name
|
|
||||||
res.locals.description = req.body?.description
|
|
||||||
res.locals.logo = req.body?.logo
|
|
||||||
res.locals.is_logo_bg = req.body?.is_logo_bg
|
|
||||||
|
|
||||||
const info = db
|
|
||||||
.prepareUpdate(
|
|
||||||
'projects',
|
|
||||||
['name', 'description', 'logo', 'is_logo_bg'],
|
|
||||||
res.locals,
|
|
||||||
['id', 'customer_id'])
|
|
||||||
.run(res.locals)
|
|
||||||
|
|
||||||
if (info.changes == 0)
|
const json_company = db
|
||||||
throw Error('NOT_FOUND::404')
|
.prepare(`select coalesce(json_company, '{}') from customers where id = :customer_id`)
|
||||||
|
|
||||||
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => {
|
|
||||||
res.locals.id = req.params.pid
|
|
||||||
res.locals.is_archived = +(req.params.action == 'archive')
|
|
||||||
|
|
||||||
const info = db
|
|
||||||
.prepare(`
|
|
||||||
update projects
|
|
||||||
set is_archived = :is_archived
|
|
||||||
where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived
|
|
||||||
`)
|
|
||||||
.run(res.locals)
|
|
||||||
|
|
||||||
if (info.changes == 0)
|
|
||||||
throw Error('BAD_REQUEST::400')
|
|
||||||
|
|
||||||
const chatIds = db
|
|
||||||
.prepare(`select id from chats where project_id = :id`)
|
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.all(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
for (const chatId of chatIds) {
|
res.locals.company_id = addCompany(Object.assign({
|
||||||
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
|
name: 'My Company',
|
||||||
}
|
address: null,
|
||||||
|
email: null,
|
||||||
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
|
phone: null,
|
||||||
|
site: null,
|
||||||
|
description: null,
|
||||||
|
logo: null
|
||||||
|
}, JSON.parse(json_company), {project_id: res.locals.project_id}))
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`update projects set company_id = :company_id where id = :project_id`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
const data = getProject(res.locals.project_id, res.locals.customer_id)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
|
app.use ('(/project/:pid(\\d+)/*|/project/:pid(\\d+))', (req, res, next) => {
|
||||||
res.locals.project_id = parseInt(req.params.pid)
|
res.locals.project_id = parseInt(req.params.pid)
|
||||||
|
|
||||||
const row = db
|
const row = db
|
||||||
@@ -435,55 +453,138 @@ app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
// USER
|
app.get('/project/:pid(\\d+)', (req, res, next) => {
|
||||||
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
const data = getProject(req.params.pid, res.locals.customer_id)
|
||||||
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
const rows = db
|
app.put('/project/:pid(\\d+)', (req, res, next) => {
|
||||||
|
res.locals.id = req.params.pid
|
||||||
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.logo = req.body?.logo
|
||||||
|
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepareUpdate(
|
||||||
|
'projects',
|
||||||
|
['name', 'description', 'logo', 'is_logo_bg'],
|
||||||
|
res.locals,
|
||||||
|
['id', 'customer_id'])
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
const data = getProject(req.params.pid, res.locals.customer_id)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
res.locals.id = req.params.pid
|
||||||
|
res.locals.is_archived = +(req.params.action == 'archive')
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepare(`
|
||||||
|
update projects
|
||||||
|
set is_archived = :is_archived
|
||||||
|
where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived
|
||||||
|
`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('BAD_REQUEST::400')
|
||||||
|
|
||||||
|
const chat_ids = db
|
||||||
|
.prepare(`select id from chats where project_id = :id`)
|
||||||
|
.pluck(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
for (const chat_id of chat_ids) {
|
||||||
|
await bot.sendMessage(chat_id, res.locals.is_archived ? 'ON_PROJECT_ARCHIVE' : 'ON_PROJECT_RESTORE')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getProject(req.params.pid, res.locals.customer_id)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// USER
|
||||||
|
function getUser(id, project_id) {
|
||||||
|
const row = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
|
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
|
||||||
ud.fullname, ud.role, ud.department, ud.is_blocked
|
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
|
||||||
|
(select company_id from company_users where user_id = :id) company_id,
|
||||||
|
(select json_group_array(chat_id) from chat_users where user_id = :id and chat_id in (select id from chats where project_id = :project_id)) chats
|
||||||
|
from users u
|
||||||
|
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
||||||
|
where id = :id
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({id, project_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.chats = JSON.parse(row.chats || '[]')
|
||||||
|
row.is_blocked = Boolean(row.is_blocked)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
|
||||||
|
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
|
||||||
|
(select company_id from company_users where user_id = u.id) company_id
|
||||||
from users u
|
from users u
|
||||||
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
||||||
where id in (
|
where id in (
|
||||||
select user_id
|
select user_id
|
||||||
from chat_users
|
from chat_users
|
||||||
where chat_id in (select id from chats where project_id = :project_id)
|
where chat_id in (select id from chats where project_id = :project_id)
|
||||||
) ${where}
|
)
|
||||||
`)
|
`)
|
||||||
.safeIntegers(true)
|
.safeIntegers(true)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
data.forEach(row => {
|
||||||
throw Error('NOT_FOUND::404')
|
row.is_blocked = Boolean(row.is_blocked)
|
||||||
|
})
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
res.status(200).json({success: true, data})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
|
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`)
|
const data = getUser(req.params.uid, req.params.pid)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
|
||||||
res.locals.user_id = parseInt(req.params.uid)
|
res.locals.user_id = parseInt(req.params.uid)
|
||||||
|
|
||||||
res.locals.fullname = req.body?.fullname
|
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.role = req.body?.role
|
||||||
res.locals.department = req.body?.department
|
res.locals.department = req.body?.department
|
||||||
res.locals.is_blocked = req.body?.is_blocked
|
res.locals.is_blocked = 'is_blocked' in req.body ? +req.body.is_blocked : undefined
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepareUpdate('user_details',
|
.prepareUpsert('user_details',
|
||||||
['fullname', 'role', 'department', 'is_blocked'],
|
['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'],
|
||||||
res.locals,
|
res.locals,
|
||||||
['user_id', 'project_id']
|
['user_id', 'project_id']
|
||||||
)
|
)
|
||||||
.all(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
if (info.changes == 0)
|
|
||||||
throw Error('NOT_FOUND::404')
|
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
const data = getUser(req.params.uid, req.params.pid)
|
||||||
|
res.status(200).json({ success: true, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/token', (req, res, next) => {
|
app.get('/project/:pid(\\d+)/token', (req, res, next) => {
|
||||||
@@ -497,67 +598,92 @@ app.get('/project/:pid(\\d+)/token', (req, res, next) => {
|
|||||||
if (!key)
|
if (!key)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true, data: key})
|
res.status(200).json({ success: true, data: key })
|
||||||
})
|
})
|
||||||
|
|
||||||
// COMPANY
|
// COMPANY
|
||||||
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
|
function addCompany(data) {
|
||||||
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
|
return db
|
||||||
|
|
||||||
const rows = db
|
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, email, phone, description, logo,
|
insert into companies (project_id, name, address, email, phone, site, description, logo)
|
||||||
(select json_chat_array(user_id) from company_users where company_id = c.id) users
|
values (:project_id, :name, :address, :email, :phone, :site, :description, :logo)
|
||||||
|
returning id
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCompany(id, project_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, address, email, phone, site, description, logo,
|
||||||
|
(select json_group_array(user_id) from company_users where company_id = c.id) users
|
||||||
from companies c
|
from companies c
|
||||||
where project_id = :project_id ${where}
|
where c.id = :id and project_id = :project_id
|
||||||
|
order by name
|
||||||
|
`)
|
||||||
|
.get({id, project_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.users = JSON.parse(row.users || '[]')
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, address, email, phone, site, description, logo,
|
||||||
|
(select company_id = c.id from projects where id = :project_id) is_own,
|
||||||
|
(select json_group_array(user_id) from company_users where company_id = c.id) users
|
||||||
|
from companies c
|
||||||
|
where project_id = :project_id
|
||||||
order by name
|
order by name
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
rows.forEach(row => row.users = JSON.parse(row.users || '[]'))
|
data.forEach(row => {
|
||||||
|
row.users = JSON.parse(row.users || '[]')
|
||||||
|
})
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
res.status(200).json({success: true, data})
|
||||||
throw Error('NOT_FOUND::404')
|
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
|
const data = getCompany(req.params.cid, req.params.pid)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/project/:pid(\\d+)/company', (req, res, next) => {
|
app.post('/project/:pid(\\d+)/company', (req, res, next) => {
|
||||||
res.locals.name = req.body?.name
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.address = req.body?.address
|
||||||
res.locals.email = req.body?.email
|
res.locals.email = req.body?.email
|
||||||
res.locals.phone = req.body?.phone
|
res.locals.phone = req.body?.phone
|
||||||
res.locals.site = req.body?.site
|
res.locals.site = req.body?.site
|
||||||
res.locals.description = req.body?.description
|
res.locals.description = req.body?.description
|
||||||
res.locals.logo = req.body?.logo
|
res.locals.logo = req.body?.logo
|
||||||
|
|
||||||
const id = db
|
const id = addCompany(res.locals)
|
||||||
.prepare(`
|
const data = getCompany(id, req.params.pid)
|
||||||
insert into companies (project_id, name, email, phone, site, description, logo)
|
res.status(200).json({success: true, data})
|
||||||
values (:project_id, :name, :email, :phone, :site, :description, :logo)
|
|
||||||
returning id
|
|
||||||
`)
|
|
||||||
.pluck(res.locals)
|
|
||||||
.get(res.locals)
|
|
||||||
|
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${id}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
||||||
res.locals.id = parseInt(req.params.cid)
|
res.locals.id = parseInt(req.params.cid)
|
||||||
res.locals.name = req.body?.name
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.address = req.body?.address
|
||||||
res.locals.email = req.body?.email
|
res.locals.email = req.body?.email
|
||||||
res.locals.phone = req.body?.phone
|
res.locals.phone = req.body?.phone
|
||||||
res.locals.site = req.body?.site
|
res.locals.site = req.body?.site
|
||||||
res.locals.description = req.body?.description
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.logo = req.body?.logo
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepareUpdate(
|
.prepareUpdate(
|
||||||
'companies',
|
'companies',
|
||||||
['name', 'email', 'phone', 'site', 'description'],
|
['name', 'address', 'email', 'phone', 'site', 'description', 'logo'],
|
||||||
res.locals,
|
res.locals,
|
||||||
['id', 'project_id'])
|
['id', 'project_id'])
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
@@ -565,55 +691,24 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
|||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
|
const data = getCompany(req.params.cid, req.params.pid)
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
|
||||||
res.locals.company_id = parseInt(req.params.cid)
|
res.locals.company_id = req.params.cid
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepare(`delete from companies where id = :company_id and project_id = :project_id`)
|
.prepare(`
|
||||||
|
delete from companies
|
||||||
|
where id = :company_id and project_id = :project_id and
|
||||||
|
not exists(select company_id from projects where id = :project_id)`)
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true, data: {id: req.params.cid}})
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
|
|
||||||
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
|
|
||||||
|
|
||||||
const rows = db
|
|
||||||
.prepare(`
|
|
||||||
select id, name, telegram_id, is_channel, user_count, bot_can_ban
|
|
||||||
from chats
|
|
||||||
where project_id = :project_id ${where}
|
|
||||||
`)
|
|
||||||
.all(res.locals)
|
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
|
||||||
throw Error('NOT_FOUND::404')
|
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
|
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.uid}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
|
|
||||||
res.locals.chat_id = parseInt(req.params.gid)
|
|
||||||
const info = db
|
|
||||||
.prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`)
|
|
||||||
.run(res.locals)
|
|
||||||
|
|
||||||
if (info.changes == 0)
|
|
||||||
throw Error('NOT_FOUND::404')
|
|
||||||
|
|
||||||
await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта')
|
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
|
||||||
@@ -655,7 +750,6 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
|
|||||||
if (user_ids.some(user_id => !rows.contains(user_id)))
|
if (user_ids.some(user_id => !rows.contains(user_id)))
|
||||||
throw Error('USED_MEMBER::400')
|
throw Error('USED_MEMBER::400')
|
||||||
|
|
||||||
|
|
||||||
db
|
db
|
||||||
.prepare(`delete from company_users where company_id = :company_id`)
|
.prepare(`delete from company_users where company_id = :company_id`)
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
@@ -670,4 +764,140 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
|
|||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true})
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = app
|
app.get('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select company_id, json_group_array(show_to_id) show_to_ids
|
||||||
|
from company_mappings
|
||||||
|
where project_id = :project_id and company_id <> show_to_id
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
row.show_to_ids = JSON.parse(row.show_to_ids || '[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
|
||||||
|
if(!(req.body instanceof Array))
|
||||||
|
throw Error('ARRAY_REQUIRED::500')
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`delete from company_mappings where project_id = :project_id`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
req.body
|
||||||
|
.filter(row => Number.isInteger(row.company_id) && row.show_to_ids instanceof Array && row.show_to_ids.every(id => Number.isInteger(id)))
|
||||||
|
.forEach(row => {
|
||||||
|
row.show_to_ids.push(row.company_id)
|
||||||
|
|
||||||
|
const json_ids = row.show_to_ids.join(', ')
|
||||||
|
const check = db
|
||||||
|
.prepare(`select count(1) from companies where project_id = :project_id and id in (${json_ids}) `)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (check.count)
|
||||||
|
return log.error (Error('IGNORE: ' + JSON.stringify(row)))
|
||||||
|
|
||||||
|
const locals = {
|
||||||
|
project_ids: res.locals.project_id,
|
||||||
|
company_id: row.company_id,
|
||||||
|
json_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`
|
||||||
|
insert into company_mappings (project_id, company_id, show_to_id) values
|
||||||
|
select :project_ids, :company_id, value from json_each(:json_ids)
|
||||||
|
`)
|
||||||
|
.run(locals)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// CHATS
|
||||||
|
function getChat(id, project_id) {
|
||||||
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
|
||||||
|
from chats
|
||||||
|
where id = :id and project_id = :project_id
|
||||||
|
`)
|
||||||
|
.get({id, project_id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.is_channel = Boolean(row.is_channel)
|
||||||
|
row.bot_can_ban = Boolean(row.bot_can_ban)
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
|
||||||
|
const data = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
|
||||||
|
from chats
|
||||||
|
where project_id = :project_id
|
||||||
|
`)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
row.is_channel = Boolean(row.is_channel)
|
||||||
|
row.bot_can_ban = Boolean(row.bot_can_ban)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
|
||||||
|
const data = getChat(req.params.gid, req.params.pid)
|
||||||
|
res.status(200).json({ success: true, data })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
res.locals.chat_id = req.params.gid
|
||||||
|
|
||||||
|
const info = db
|
||||||
|
.prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`)
|
||||||
|
.run(res.locals)
|
||||||
|
|
||||||
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
await bot.sendMessage(res.locals.chat_id, 'ON_CHAT_REMOVE')
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: {id: req.params.gid}})
|
||||||
|
} catch (err){
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
eventBus.on('chat-attached', chat => {
|
||||||
|
const customer_id = db
|
||||||
|
.prepare(`select customer_id from projects where id = :project_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(chat)
|
||||||
|
|
||||||
|
if (!customer_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
event: 'chat-attached',
|
||||||
|
entity: 'chat',
|
||||||
|
id: chat.id,
|
||||||
|
data: getChat(chat.id, chat.project_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(sessions)
|
||||||
|
.filter(s => s.customer_id == customer_id && s.ws?.readyState === WebSocket.OPEN)
|
||||||
|
.map(s => s.ws)
|
||||||
|
.forEach(ws => ws.send(JSON.stringify(msg)))
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = { router: app, registerWS }
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const multer = require('multer')
|
const multer = require('multer')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const fs = require('fs')
|
|
||||||
const contentDisposition = require('content-disposition')
|
const contentDisposition = require('content-disposition')
|
||||||
|
|
||||||
const bot = require('./bot')
|
const bot = require('./bot')
|
||||||
const db = require('../include/db')
|
const db = require('../include/db')
|
||||||
|
const log = require('../include/log')
|
||||||
|
const eventBus = require('../include/eventbus')
|
||||||
|
|
||||||
const app = express.Router()
|
const app = express.Router()
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
@@ -15,23 +16,47 @@ const upload = multer({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sessions = {}
|
||||||
|
|
||||||
|
function registerWS(sid, ws) {
|
||||||
|
const session = sessions[sid]
|
||||||
|
if (session)
|
||||||
|
session.ws = ws
|
||||||
|
|
||||||
|
return !!session
|
||||||
|
}
|
||||||
|
|
||||||
function hasAccess(project_id, user_id) {
|
function hasAccess(project_id, user_id) {
|
||||||
return !!db
|
return !!db
|
||||||
|
.prepare(`select 1 from projects where id = :project_id and is_archived <> 1`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({project_id}) &&
|
||||||
|
!!db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select 1
|
select 1
|
||||||
from chat_users
|
from chat_users
|
||||||
where user_id = :user_id and
|
where user_id = :user_id and
|
||||||
chat_id in (select id from chats where project_id = :project_id) and
|
chat_id in (select id from chats where project_id = :project_id) and
|
||||||
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1) and
|
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1)
|
||||||
not exists(select 1 from projects where id = :project_id and is_deleted = 1)
|
|
||||||
`)
|
`)
|
||||||
|
.pluck(true)
|
||||||
.get({project_id, user_id})
|
.get({project_id, user_id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = {}
|
function getUserInfo (user_id, project_id) {
|
||||||
|
const user = db
|
||||||
|
.prepare(`
|
||||||
|
select u.telegram_id, coalesce(ud.fullname, coalesce(u.lastname, '') || ' ' || coalesce(u.firstname, '')) name
|
||||||
|
from users u left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
|
||||||
|
where u.id = :user_id
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.get({ user_id, project_id })
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.path == '/user/login')
|
if (req.path == '/auth')
|
||||||
return next()
|
return next()
|
||||||
|
|
||||||
const sid = req.query.sid || req.cookies.sid
|
const sid = req.query.sid || req.cookies.sid
|
||||||
@@ -43,8 +68,7 @@ app.use((req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post('/auth', (req, res, next) => {
|
||||||
app.post('/user/login', (req, res, next) => {
|
|
||||||
db
|
db
|
||||||
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
|
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
|
||||||
.safeIntegers(true)
|
.safeIntegers(true)
|
||||||
@@ -58,18 +82,16 @@ app.post('/user/login', (req, res, next) => {
|
|||||||
|
|
||||||
const sid = crypto.randomBytes(64).toString('hex')
|
const sid = crypto.randomBytes(64).toString('hex')
|
||||||
req.session = sessions[sid] = {sid, user_id}
|
req.session = sessions[sid] = {sid, user_id}
|
||||||
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/`])
|
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/miniapp`])
|
||||||
res.locals.user_id = user_id
|
res.locals.user_id = user_id
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project', (req, res, next) => {
|
app.get('/project', (req, res, next) => {
|
||||||
const where = req.query.id ? ' and p.id = ' + parseInt(req.query.id) : ''
|
|
||||||
|
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select p.id, p.name, p.description, p.logo,
|
select p.id, p.name, p.description, p.logo, p.is_logo_bg, company_id,
|
||||||
c.name customer_name, c.upload_chat_id <> 0 has_upload
|
c.name customer_name, c.upload_chat_id <> 0 has_upload
|
||||||
from projects p
|
from projects p
|
||||||
inner join customers c on p.customer_id = c.id
|
inner join customers c on p.customer_id = c.id
|
||||||
@@ -78,18 +100,15 @@ app.get('/project', (req, res, next) => {
|
|||||||
from chats
|
from chats
|
||||||
where id in (select chat_id from chat_users where user_id = :user_id)
|
where id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1)
|
) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1)
|
||||||
${where} and is_deleted <> 1
|
and p.is_archived <> 1
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
rows.forEach(row => {
|
||||||
throw Error('NOT_FOUND::404')
|
row.is_logo_bg = Boolean(row.is_logo_bg)
|
||||||
|
})
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
res.status(200).json({success: true, data: 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) => {
|
app.use('/project/:pid(\\d+)/*', (req, res, next) => {
|
||||||
@@ -99,17 +118,27 @@ app.use('/project/:pid(\\d+)/*', (req, res, next) => {
|
|||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
const row = db
|
const row = db
|
||||||
.prepare('select customer_id from projects where id = :project_id')
|
.prepare('select customer_id, company_id from projects where id = :project_id')
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
res.locals.customer_id = row.customer_id
|
res.locals.customer_id = row.customer_id
|
||||||
|
res.locals.customer_company_id = row.company_id
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
function getUserCompanyId(user_id, project_id) {
|
||||||
const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : ''
|
return db
|
||||||
|
.prepare(`
|
||||||
|
select company_id
|
||||||
|
from company_users
|
||||||
|
where user_id = :user_id and company_id in (select id from companies where project_id = :project_id)
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get({ user_id, project_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
||||||
const users = db
|
const users = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
with actuals (user_id) as (
|
with actuals (user_id) as (
|
||||||
@@ -125,7 +154,7 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
|||||||
union
|
union
|
||||||
select created_by from meetings where project_id = :project_id
|
select created_by from meetings where project_id = :project_id
|
||||||
union
|
union
|
||||||
select published_by from documents where project_id = :project_id
|
select published_by from files where project_id = :project_id
|
||||||
),
|
),
|
||||||
members (user_id, is_leave) as (
|
members (user_id, is_leave) as (
|
||||||
select user_id, 0 is_leave from actuals
|
select user_id, 0 is_leave from actuals
|
||||||
@@ -138,8 +167,9 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
|||||||
u.firstname,
|
u.firstname,
|
||||||
u.lastname,
|
u.lastname,
|
||||||
u.photo,
|
u.photo,
|
||||||
u.json_phone_projects,
|
|
||||||
ud.fullname,
|
ud.fullname,
|
||||||
|
ud.email,
|
||||||
|
ud.phone,
|
||||||
ud.role,
|
ud.role,
|
||||||
ud.department,
|
ud.department,
|
||||||
ud.is_blocked,
|
ud.is_blocked,
|
||||||
@@ -150,138 +180,189 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
|
|||||||
m.is_leave
|
m.is_leave
|
||||||
from users u
|
from users u
|
||||||
inner join members m on u.id = m.user_id
|
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
|
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
|
||||||
where 1 = 1 ${where}
|
|
||||||
`)
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
const companies = db
|
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
|
||||||
.prepare('select id, name, email, phone, site, description from companies where project_id = :project_id')
|
|
||||||
|
// Список компаний, которые НЕ ВИДНЫ компании пользователя на проекте
|
||||||
|
const hidden = db
|
||||||
|
.prepare(`
|
||||||
|
select company_id from company_mappings where project_id = :project_id
|
||||||
|
except
|
||||||
|
select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id`)
|
||||||
|
.pluck(true)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
.reduce((companies, row) => {
|
|
||||||
companies[row.id] = row
|
users
|
||||||
return companies
|
.filter(user => user.company_id)
|
||||||
}, {})
|
.filter(user => hidden.indexOf(user.company_id) != -1)
|
||||||
|
.forEach(user => user.company_id = res.locals.customer_company_id)
|
||||||
|
|
||||||
const mappings = {}
|
res.status(200).json({success: true, data: users})
|
||||||
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) => {
|
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
|
||||||
const chatIds = db
|
try {
|
||||||
.prepare(`select id from chats where project_id = :project_id`)
|
const chat_ids = db
|
||||||
.all(res.locals)
|
.prepare(`select id from chats where project_id = :project_id`)
|
||||||
.map(e => e.id)
|
.all(res.locals)
|
||||||
|
.map(e => e.id)
|
||||||
|
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
for (const chatId of chatIds) {
|
for (const chat_id of chat_ids) {
|
||||||
await bot.reloadGroupUsers(chatId)
|
await bot.reloadChatUsers(chat_id)
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true})
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
|
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
|
||||||
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
|
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
|
||||||
|
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, telegram_id
|
select id, name, address, email, phone, site, description
|
||||||
from chats
|
from companies
|
||||||
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
|
where project_id = :project_id and (
|
||||||
${where}
|
id = :company_id or
|
||||||
|
id in (select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id) or
|
||||||
|
id not in (select company_id from company_mappings where project_id = :project_id) or
|
||||||
|
(select :customer_company_id)
|
||||||
|
)
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
res.status(200).json({success: true, data: rows})
|
||||||
throw Error('NOT_FOUND::404')
|
|
||||||
|
|
||||||
res.status(200).json({success: true, data: where ? rows[0] : rows})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
|
// CHAT
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.gid}`)
|
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
|
||||||
|
const rows = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, invite_link, description, telegram_id, owner_id, user_count, logo,
|
||||||
|
(select json_group_array(user_id) from chat_users where chat_id = c.id) users,
|
||||||
|
(select count(1) from tasks where project_id = :project_id and chat_id = c.id) task_count
|
||||||
|
from chats c
|
||||||
|
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
|
`)
|
||||||
|
.safeIntegers(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.users = JSON.parse(row.users)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TASK
|
// TASK
|
||||||
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
|
function getTask(id, user_id) {
|
||||||
const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : ''
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
|
||||||
|
close_date, close_comment, coalesce(json_close_files, '[]') close_files, chat_id,
|
||||||
|
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
|
||||||
|
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
|
||||||
|
from tasks t
|
||||||
|
where t.id = :id
|
||||||
|
`)
|
||||||
|
.get({id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.close_files = JSON.parse(row.close_files)
|
||||||
|
row.observers = JSON.parse(row.observers)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = row.created_by == user_id || row.assigned_to == user_id
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date,
|
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
|
||||||
(select json_chat_array(user_id) from task_users where task_id = t.id) observers,
|
close_date, close_comment, coalesce(json_close_files, '[]') close_files, chat_id,
|
||||||
(select json_chat_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments
|
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
|
||||||
|
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
|
||||||
from tasks t
|
from tasks t
|
||||||
where project_id = :project_id and
|
where project_id = :project_id and (created_by = :user_id or
|
||||||
(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))
|
assigned_to = :user_id or
|
||||||
${where}
|
exists(select 1 from task_users where task_id = t.id and user_id = :user_id) or
|
||||||
|
exists(select 1 from chat_users where chat_id = t.chat_id))
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
|
row.close_files = JSON.parse(row.close_files)
|
||||||
row.observers = JSON.parse(row.observers)
|
row.observers = JSON.parse(row.observers)
|
||||||
row.attachments = JSON.parse(row.attachments)
|
row.files = JSON.parse(row.files)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
res.status(200).json({success: true, data: rows})
|
||||||
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) => {
|
app.post('/project/:pid(\\d+)/task', async (req, res, next) => {
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/task?id=${req.params.tid}`)
|
try {
|
||||||
})
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.status = parseInt(req.body?.status)
|
||||||
|
res.locals.priority = parseInt(req.body?.priority)
|
||||||
|
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
|
||||||
|
res.locals.create_date = Math.floor(Date.now() / 1000)
|
||||||
|
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
|
||||||
|
throw Error('INCORRECT_ASSIGNED_TO::400')
|
||||||
|
|
||||||
app.post('/project/:pid(\\d+)/task', (req, res, next) => {
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
res.locals.name = req.body?.name
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
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
|
const id = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date)
|
insert into tasks (project_id, name, description, created_by, assigned_to, priority, status, create_date, plan_date, chat_id)
|
||||||
values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date)
|
values (:project_id, :name, :description, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date, :chat_id)
|
||||||
returning id
|
returning id
|
||||||
`)
|
`)
|
||||||
.pluck(true)
|
.pluck(true)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
res.status(200).json({success: true, data: id})
|
const task = getTask(id, res.locals.user_id)
|
||||||
|
if (res.locals.chat_id) {
|
||||||
|
const creator = getUserInfo(task.created_by, task.project_id)
|
||||||
|
const assignee = getUserInfo(task.assigned_to, task.project_id)
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
PRIORITY: '$TASK_PRIORITY_' + (task.priority || '0'),
|
||||||
|
NAME: task.name,
|
||||||
|
PLAN_DATE: new Date(task.plan_date * 1000).toLocaleString('ru'),
|
||||||
|
CREATOR: creator.name,
|
||||||
|
CREATOR_ID: creator.telegram_id,
|
||||||
|
ASSIGNEE: assignee.name,
|
||||||
|
ASSIGNEE_ID: assignee.telegram_id,
|
||||||
|
URL: bot.USER_APP + '/?startapp=p' + res.locals.project_id + 't' + task.id
|
||||||
|
}
|
||||||
|
|
||||||
|
message_id = await bot.sendMessage(res.locals.chat_id, 'TASK_MESSAGE', args)
|
||||||
|
db
|
||||||
|
.prepare(`update tasks set message_id = :message_id where id = :id`)
|
||||||
|
.run({ id, message_id})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
|
} catch (err) {
|
||||||
|
next (err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
|
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
|
||||||
@@ -290,9 +371,12 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
|
|||||||
const task = db
|
const task = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select created_by, assigned_to
|
select created_by, assigned_to
|
||||||
from tasks
|
from tasks t
|
||||||
where id = :task_id and project_id = :project_id
|
where id = :task_id and project_id = :project_id and (
|
||||||
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))
|
created_by = :user_id or
|
||||||
|
assigned_to = :user_id or
|
||||||
|
exists(select 1 from task_users where task_id = :task_id and user_id = :user_id) or
|
||||||
|
exists(select 1 from chat_users where chat_id = t.chat_id))
|
||||||
`)
|
`)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
@@ -305,18 +389,43 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
|
const task = getTask(req.params.tid, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
if (!res.locals.is_author && !res.locals.is_assigned)
|
if (!res.locals.is_author && !res.locals.is_assigned)
|
||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
res.locals.id = res.locals.task_id
|
res.locals.id = res.locals.task_id
|
||||||
res.locals.name = req.body?.name
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
res.locals.status = parseInt(req.body?.status)
|
res.locals.status = parseInt(req.body?.status)
|
||||||
res.locals.priority = parseInt(req.body?.priority)
|
res.locals.priority = parseInt(req.body?.priority)
|
||||||
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
|
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
|
||||||
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
|
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
|
||||||
|
res.locals.close_comment = req.body?.close_comment
|
||||||
|
res.locals.json_close_files = null
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent'] : ['status', 'time_spent']
|
if (req.body?.close_files instanceof Array) {
|
||||||
|
const file_ids = db
|
||||||
|
.prepare(`select id from files where parent_type = 1 and parent_id = :task_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
|
const close_files = req.body.close_files
|
||||||
|
.map(id => parseInt(id))
|
||||||
|
.filter(id => file_ids.indexOf(id) != -1)
|
||||||
|
|
||||||
|
res.json_close_files = JSON.stringify(close_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = res.locals.is_author ? ['name', 'description', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_files', 'chat_id'] : ['status', 'time_spent', 'close_comment', 'json_close_file_ids']
|
||||||
const info = db
|
const info = db
|
||||||
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
|
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
@@ -324,7 +433,8 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
|||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
const task = getTask(res.locals.task_id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: task})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
||||||
@@ -338,7 +448,7 @@ app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
|
|||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true, data: {id: res.locals.task_id}})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
|
||||||
@@ -373,51 +483,82 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// MEETINGS
|
// MEETINGS
|
||||||
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
|
function getMeeting(id, user_id) {
|
||||||
const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : ''
|
const row = db
|
||||||
|
.prepare(`
|
||||||
|
select id, name, description, place, created_by, meet_date, chat_id, is_cancel,
|
||||||
|
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
|
||||||
|
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files
|
||||||
|
from meetings m
|
||||||
|
where m.id = :id
|
||||||
|
`)
|
||||||
|
.get({id})
|
||||||
|
|
||||||
|
if (!row)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
row.participants = JSON.parse(row.participants)
|
||||||
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = row.created_by == user_id
|
||||||
|
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, name, description, created_by, meet_date,
|
select id, name, description, place, created_by, meet_date, duration, chat_id, is_cancel,
|
||||||
(select json_chat_array(user_id) from meeting_users where meeting_id = m.id) participants,
|
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
|
||||||
(select json_chat_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments
|
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files,
|
||||||
|
created_by = :user_id is_editable
|
||||||
from meetings m
|
from meetings m
|
||||||
where project_id = :project_id and
|
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))
|
(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)
|
.all(res.locals)
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
row.participants = JSON.parse(row.participants)
|
row.participants = JSON.parse(row.participants)
|
||||||
row.attachments = JSON.parse(row.attachments)
|
row.files = JSON.parse(row.files)
|
||||||
|
row.is_editable = Boolean(row.is_editable)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
res.status(200).json({success: true, data: rows})
|
||||||
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) => {
|
app.post('/project/:pid(\\d+)/meeting', async (req, res, next) => {
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/meeting?id=${req.params.mid}`)
|
try {
|
||||||
})
|
res.locals.name = req.body?.name
|
||||||
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.place = req.body?.place
|
||||||
|
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
||||||
|
res.locals.duration = req.body?.duration ? parseInt(req.body?.duration) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
app.post('/project/:pid(\\d+)/meeting', (req, res, next) => {
|
const id = db
|
||||||
res.locals.name = req.body?.name
|
.prepare(`
|
||||||
res.locals.description = req.body?.description
|
insert into meetings (project_id, name, description, place, created_by, meet_date, duration, chat_id)
|
||||||
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration, :chat_id)
|
||||||
|
returning id
|
||||||
|
`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
const id = db
|
const meeting = getMeeting(id, res.locals.user_id)
|
||||||
.prepare(`
|
if (res.locals.chat_id) {
|
||||||
insert into meetings (project_id, name, description, created_by, meet_date)
|
const url = bot.USER_APP + '/?startapp=p' + res.locals.project_id + 'm' + meeting.id
|
||||||
values (:project_id, :name, :description, :user_id, :meet_date)
|
message_id = await bot.sendMessage(res.locals.chat_id, 'MEETING_MESSAGE', meeting)
|
||||||
returning id
|
db
|
||||||
`)
|
.prepare(`update meetings set message_id = :message_id where id = :id`)
|
||||||
.pluck(true)
|
.run({ id, message_id})
|
||||||
.get(res.locals)
|
}
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
res.status(200).json({success: true, data: id})
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
|
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
|
||||||
@@ -440,6 +581,11 @@ app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
|
const meeting = getMeeting(req.params.mid, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
if (!res.locals.is_author)
|
if (!res.locals.is_author)
|
||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
@@ -447,16 +593,23 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
|||||||
res.locals.id = res.locals.meeting_id
|
res.locals.id = res.locals.meeting_id
|
||||||
res.locals.name = req.body?.name
|
res.locals.name = req.body?.name
|
||||||
res.locals.description = req.body?.description
|
res.locals.description = req.body?.description
|
||||||
|
res.locals.place = req.body?.place
|
||||||
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
|
||||||
|
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
|
||||||
|
res.locals.is_cancel = +!!req.body?.is_cancel
|
||||||
|
|
||||||
|
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
|
||||||
|
throw Error('INCORRECT_CHAT_ID::400')
|
||||||
|
|
||||||
const info = db
|
const info = db
|
||||||
.prepareUpdate('meetings', ['name', 'description', 'meet_date'], res.locals, ['id', 'project_id'])
|
.prepareUpdate('meetings', ['name', 'description', 'place', 'meet_date', 'chat_id', 'is_cancel'], res.locals, ['id', 'project_id'])
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
const meeting = getMeeting(res.locals.meeting_id, res.locals.user_id)
|
||||||
|
res.status(200).json({success: true, data: meeting})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
||||||
@@ -470,10 +623,10 @@ app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
|
|||||||
if (info.changes == 0)
|
if (info.changes == 0)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true, data: {id: res.locals.meeting_id}})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next) => {
|
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (req, res, next) => {
|
||||||
if (!res.locals.is_author)
|
if (!res.locals.is_author)
|
||||||
throw Error('ACCESS_DENIED::401')
|
throw Error('ACCESS_DENIED::401')
|
||||||
|
|
||||||
@@ -486,7 +639,7 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
|
|||||||
from chat_users
|
from chat_users
|
||||||
where chat_id in (select id from chats where project_id = :project_id)
|
where chat_id in (select id from chats where project_id = :project_id)
|
||||||
`)
|
`)
|
||||||
.pluck(true) // .raw?
|
.pluck(true)
|
||||||
.all(res.locals)
|
.all(res.locals)
|
||||||
|
|
||||||
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
|
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
|
||||||
@@ -501,72 +654,123 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
|
|||||||
.prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`)
|
.prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`)
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
res.status(200).json({ success: true, data: user_ids })
|
||||||
})
|
})
|
||||||
|
|
||||||
// DOCUMENTS
|
// FILES
|
||||||
app.get('/project/:pid(\\d+)/document', (req, res, next) => {
|
app.get('/project/:pid(\\d+)/file', (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. Из групп, которые есть в проекте и в которых участвует пользователь
|
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
|
||||||
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
|
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
|
||||||
// 3. Из встреч на проекте, где пользователь создатель или участник
|
// 3. Из встреч на проекте, где пользователь создатель или участник
|
||||||
// To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId
|
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
select id, origin_chat_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type
|
select f.id, f.chat_id, c.telegram_id telegram_chat_id, f.message_id, f.filename, f.mime, f.caption, f.size, f.published_by, f.published, f.parent_id, f.parent_type
|
||||||
from documents d
|
from files f
|
||||||
where project_id = :project_id ${where} and (
|
left join chats c on f.chat_id = c.id and f.parent_type = 0
|
||||||
origin_chat_id in (select chat_id from chat_users where user_id = :user_id)
|
where f.project_id = :project_id and (
|
||||||
or
|
chat_id in (select chat_id from chat_users where user_id = :user_id)
|
||||||
parent_type = 1 and parent_id in (
|
or
|
||||||
|
parent_type = 1 and parent_id in (
|
||||||
select id
|
select id
|
||||||
from tasks t
|
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 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
|
or
|
||||||
parent_type = 2 and parent_id in (
|
parent_type = 2 and parent_id in (
|
||||||
select id
|
select id
|
||||||
from meetings m
|
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 project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
.all(res.locals)
|
.safeIntegers(true)
|
||||||
|
.all(res.locals)
|
||||||
|
|
||||||
if (where && rows.length == 0)
|
const upload_ids = db
|
||||||
throw Error('NOT_FOUND::404')
|
.prepare(`select upload_chat_id from customers where upload_chat_id is not null`)
|
||||||
|
.pluck(true)
|
||||||
|
.all()
|
||||||
|
.reduce((res, e) => { res[e] = true; return res}, {})
|
||||||
|
|
||||||
res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows})
|
rows
|
||||||
|
.filter (row => upload_ids[row.chat_id])
|
||||||
|
.forEach(row => {
|
||||||
|
row.chat_id = null
|
||||||
|
row.message_id = null
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: rows})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => {
|
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
|
try {
|
||||||
const parentId = req.params.id
|
res.locals.parent_id = req.params.id
|
||||||
|
res.locals.parent_type = req.params.type == 'task' ? 1 : 2
|
||||||
|
|
||||||
const ids = []
|
const chat_id = db
|
||||||
for (const file of req.files) {
|
.prepare(`
|
||||||
const id = await bot.uploadDocument(res.locals.project_id, file.originalname, file.mimetype, file.buffer, parentType, parentId, res.locals.user_id)
|
select coalesce(chat_id, (select upload_chat_id from customers where id = :customer_id))
|
||||||
ids.push(id)
|
from ${req.params.type}s
|
||||||
|
where id = :parent_id and project_id = :project_id`)
|
||||||
|
.pluck(true)
|
||||||
|
.get(res.locals)
|
||||||
|
|
||||||
|
if (!chat_id)
|
||||||
|
throw Error('EMPTY_DESTINATION::500')
|
||||||
|
|
||||||
|
const file_ids = []
|
||||||
|
|
||||||
|
for (const file of req.files) {
|
||||||
|
if (file.size == 0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
const filedata = {
|
||||||
|
project_id: req.params.pid,
|
||||||
|
chat_id,
|
||||||
|
filename: file.originalname,
|
||||||
|
mime: file.mimetype,
|
||||||
|
data: file.buffer,
|
||||||
|
size: file.size,
|
||||||
|
published_by: res.locals.user_id,
|
||||||
|
pablished: Math.floor(Date.now() / 1000),
|
||||||
|
parent_type: res.locals.parent_type,
|
||||||
|
parent_id: req.params.id
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file_id = await bot.sendFile(filedata)
|
||||||
|
if (file_id)
|
||||||
|
file_ids.push(file_id)
|
||||||
|
} catch (err) {
|
||||||
|
log.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_ids.length == 0)
|
||||||
|
throw Error('EMPTY_UPLOAD::500')
|
||||||
|
|
||||||
|
const files = db
|
||||||
|
.prepare(`select id, chat_id, message_id, filename, mime, size, published_by, published from files where id in (` + file_ids.join(',') + `)`)
|
||||||
|
.all()
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: files})
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(req.baseUrl + `/project/${req.params.pid}/document?id=` + ids.join(','))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
|
app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
|
||||||
res.locals.document_id = req.params.did
|
res.locals.file_id = req.params.fid
|
||||||
|
|
||||||
const doc = db
|
const file = db
|
||||||
.prepare(`select * from documents where id = :document_id and project_id = :project_id`)
|
.prepare(`select * from files where id = :file_id and project_id = :project_id`)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
|
|
||||||
if (!doc)
|
if (!file)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
if (doc.parent_type == 0) {
|
if (file.parent_type == 0) {
|
||||||
res.locals.chat_id = doc.chat_id
|
res.locals.chat_id = file.chat_id
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
|
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
|
||||||
.get(res.locals)
|
.get(res.locals)
|
||||||
@@ -575,8 +779,8 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
|
|||||||
res.locals.can_download = true
|
res.locals.can_download = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.locals.parent_id = doc.parent_id
|
res.locals.parent_id = file.parent_id
|
||||||
const parent = doc.parent_type == 1 ? 'task' : 'meeting'
|
const parent = file.parent_type == 1 ? 'task' : 'meeting'
|
||||||
|
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
@@ -589,36 +793,43 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
|
|||||||
|
|
||||||
if (row) {
|
if (row) {
|
||||||
res.locals.can_download = true
|
res.locals.can_download = true
|
||||||
res.locals.can_delete = doc.published_by == res.locals.user_id
|
res.locals.can_delete = file.published_by == res.locals.user_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/project/:pid(\\d+)/document/:did(\\d+)', async (req, res, next) => {
|
app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => {
|
||||||
if (!res.locals.can_download)
|
try {
|
||||||
throw Error('NOT_FOUND::404')
|
if (!res.locals.can_download)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
const file = await bot.downloadDocument(res.locals.project_id, res.locals.document_id)
|
const file = await bot.downloadFile(res.locals.project_id, res.locals.file_id)
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Length': file.size,
|
'Content-Length': file.size,
|
||||||
'Content-Type': file.mime,
|
'Content-Type': file.mime,
|
||||||
'Content-Disposition': contentDisposition(file.filename)
|
'Content-Disposition': contentDisposition(file.filename)
|
||||||
})
|
})
|
||||||
|
|
||||||
res.end(file.data)
|
res.end(file.data)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete('/project/:pid(\\d+)/document/:id(\\d+)', (req, res, next) => {
|
app.delete('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
|
||||||
if (!res.locals.can_delete)
|
if (!res.locals.can_delete)
|
||||||
throw Error('NOT_FOUND::404')
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
const doc = db
|
const info = db
|
||||||
.prepare(`delete from documents where id = :id and project_id = :project_id`)
|
.prepare(`delete from files where id = :id and project_id = :project_id`)
|
||||||
.run(res.locals)
|
.run(res.locals)
|
||||||
|
|
||||||
res.status(200).json({success: true})
|
if (info.changes == 0)
|
||||||
|
throw Error('NOT_FOUND::404')
|
||||||
|
|
||||||
|
res.status(200).json({success: true, data: {id: res.locals.file_id}})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/settings', (req, res, next) => {
|
app.get('/settings', (req, res, next) => {
|
||||||
@@ -640,4 +851,28 @@ app.put('/settings', (req, res, next) => {
|
|||||||
res.status(200).json({success: true})
|
res.status(200).json({success: true})
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = app
|
/*
|
||||||
|
eventBus.on('data', evt => {
|
||||||
|
if (evt.)
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
event: evt.event,
|
||||||
|
entity: evt.entity,
|
||||||
|
id: evt.id,
|
||||||
|
source: 'miniapp'
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = {}
|
||||||
|
if (evt.entity == 'project') {
|
||||||
|
|
||||||
|
}
|
||||||
|
if (evt.entity == 'task') {
|
||||||
|
msg.data = getTask(evt.id, null)
|
||||||
|
}
|
||||||
|
if (evt.entity == 'meeting') {
|
||||||
|
msg.data = getMeeting(evt.id, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = { router: app, registerWS }
|
||||||
BIN
backend/data/db.sqlite-shm
Normal file
BIN
backend/data/db.sqlite-wal
Normal file
@@ -23,6 +23,7 @@ create table if not exists projects (
|
|||||||
description text check(description is null or length(description) < 4096),
|
description text check(description is null or length(description) < 4096),
|
||||||
logo text,
|
logo text,
|
||||||
is_logo_bg integer default 0,
|
is_logo_bg integer default 0,
|
||||||
|
company_id integer references companies(id) on delete cascade,
|
||||||
is_archived integer default 0
|
is_archived integer default 0
|
||||||
) strict;
|
) strict;
|
||||||
|
|
||||||
@@ -31,9 +32,13 @@ create table if not exists chats (
|
|||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
name text,
|
name text,
|
||||||
telegram_id integer,
|
telegram_id integer,
|
||||||
|
description text,
|
||||||
|
logo text,
|
||||||
access_hash integer,
|
access_hash integer,
|
||||||
is_channel integer check(is_channel in (0, 1)) default 0,
|
is_channel integer check(is_channel in (0, 1)) default 0,
|
||||||
|
invite_link text,
|
||||||
bot_can_ban integer default 0,
|
bot_can_ban integer default 0,
|
||||||
|
owner_id integer references users(id) on delete set null,
|
||||||
user_count integer,
|
user_count integer,
|
||||||
last_update_time integer
|
last_update_time integer
|
||||||
);
|
);
|
||||||
@@ -43,14 +48,12 @@ create table if not exists users (
|
|||||||
id integer primary key autoincrement,
|
id integer primary key autoincrement,
|
||||||
telegram_id integer,
|
telegram_id integer,
|
||||||
access_hash integer,
|
access_hash integer,
|
||||||
firstname text,
|
firstname text check(firstname is null or length(firstname) < 256),
|
||||||
lastname text,
|
lastname text check(lastname is null or length(lastname) < 256),
|
||||||
username text,
|
username text check(username is null or length(username) < 256),
|
||||||
photo_id integer,
|
photo_id integer,
|
||||||
photo text,
|
photo text,
|
||||||
language_code text,
|
language_code text,
|
||||||
phone text,
|
|
||||||
json_phone_projects text default '[]',
|
|
||||||
json_settings text default '{}'
|
json_settings text default '{}'
|
||||||
) strict;
|
) strict;
|
||||||
create unique index if not exists idx_users_telegram_id on users (telegram_id);
|
create unique index if not exists idx_users_telegram_id on users (telegram_id);
|
||||||
@@ -58,73 +61,89 @@ create unique index if not exists idx_users_telegram_id on users (telegram_id);
|
|||||||
create table if not exists user_details (
|
create table if not exists user_details (
|
||||||
user_id integer references users(id) on delete cascade,
|
user_id integer references users(id) on delete cascade,
|
||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
fullname text,
|
fullname text check(fullname is null or length(fullname) < 256),
|
||||||
role text,
|
email text check(email is null or length(email) < 256),
|
||||||
department text,
|
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,
|
is_blocked integer check(is_blocked in (0, 1)) default 0,
|
||||||
primary key (user_id, project_id)
|
primary key (user_id, project_id)
|
||||||
) strict;
|
) strict;
|
||||||
|
|
||||||
create table if not exists tasks (
|
create table if not exists tasks (
|
||||||
id integer primary key autoincrement,
|
id integer primary key autoincrement,
|
||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
name text not null check(trim(name) <> '' and length(name) < 4096),
|
name text not null check(trim(name) <> '' and length(name) < 256),
|
||||||
|
description text not null check(trim(description) <> '' and length(description) < 4096),
|
||||||
created_by integer references users(id) on delete set null,
|
created_by integer references users(id) on delete set null,
|
||||||
assigned_to 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,
|
closed_by integer references users(id) on delete set null,
|
||||||
priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0,
|
priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0,
|
||||||
status integer check(status >= 1 and status <= 10) default 1,
|
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
|
time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 3 * 44640), -- one month
|
||||||
create_date integer,
|
create_date integer,
|
||||||
plan_date integer,
|
plan_date integer,
|
||||||
close_date integer
|
close_date integer,
|
||||||
|
close_comment text check(close_comment is null or length(close_comment) < 1024),
|
||||||
|
json_close_files text,
|
||||||
|
chat_id integer references chats(id) on delete set null,
|
||||||
|
message_id integer
|
||||||
) strict;
|
) strict;
|
||||||
|
create index if not exists idx_tasks_project_id on tasks (project_id);
|
||||||
|
|
||||||
create table if not exists meetings (
|
create table if not exists meetings (
|
||||||
id integer primary key autoincrement,
|
id integer primary key autoincrement,
|
||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
name text not null check(trim(name) <> '' and length(name) < 4096),
|
name text not null check(trim(name) <> '' and length(name) < 256),
|
||||||
description text check(description is null or length(description) < 4096),
|
description text check(description is null or length(description) < 1024),
|
||||||
|
place text check(place is null or length(place) < 4096),
|
||||||
created_by integer references users(id) on delete set null,
|
created_by integer references users(id) on delete set null,
|
||||||
meet_date integer
|
meet_date integer,
|
||||||
|
duration integer,
|
||||||
|
chat_id integer references chats(id) on delete set null,
|
||||||
|
message_id integer,
|
||||||
|
is_cancel integer default 0
|
||||||
) strict;
|
) strict;
|
||||||
|
create index if not exists idx_meetings_project_id on meetings (project_id);
|
||||||
|
|
||||||
create table if not exists documents (
|
create table if not exists files (
|
||||||
id integer primary key autoincrement,
|
id integer primary key autoincrement,
|
||||||
project_id integer references projects(id) on delete cascade,
|
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,
|
chat_id integer references chats(id) on delete set null,
|
||||||
message_id integer,
|
message_id integer,
|
||||||
file_id integer,
|
file_id integer,
|
||||||
access_hash integer,
|
access_hash integer,
|
||||||
filename text not null check(length(filename) < 256),
|
filename text not null check(length(filename) < 256),
|
||||||
mime text check(mime is null or length(mime) < 128),
|
mime text check(mime is null or length(mime) < 128),
|
||||||
caption text check(caption is null or length(caption) < 4096),
|
|
||||||
size integer,
|
size integer,
|
||||||
|
caption text check(caption is null or length(caption) < 2048),
|
||||||
published_by integer references users(id) on delete set null,
|
published_by integer references users(id) on delete set null,
|
||||||
parent_type integer check(parent_type in (0, 1, 2)) default 0,
|
published integer,
|
||||||
parent_id integer,
|
parent_type integer check(parent_type in (0, 1, 2, 3)) default 0,
|
||||||
backup_state integer default 0
|
parent_id integer
|
||||||
) strict;
|
) strict;
|
||||||
|
create index if not exists idx_files_project_id on files (project_id);
|
||||||
|
|
||||||
create table if not exists companies (
|
create table if not exists companies (
|
||||||
id integer primary key autoincrement,
|
id integer primary key autoincrement,
|
||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
name text not null check(length(name) < 4096),
|
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),
|
email text check(email is null or length(email) < 128),
|
||||||
phone text check(phone is null or length(phone) < 128),
|
phone text check(phone is null or length(phone) < 128),
|
||||||
site text check(site is null or length(site) < 128),
|
site text check(site is null or length(site) < 128),
|
||||||
description text check(description is null or length(description) < 4096),
|
description text check(description is null or length(description) < 4096),
|
||||||
logo text
|
logo text
|
||||||
) strict;
|
) strict;
|
||||||
|
create index if not exists idx_companies_project_id on companies (project_id);
|
||||||
|
|
||||||
create table if not exists company_mappings (
|
create table if not exists company_mappings (
|
||||||
project_id integer references projects(id) on delete cascade,
|
project_id integer references projects(id) on delete cascade,
|
||||||
company_id integer references companies(id) on delete cascade,
|
company_id integer references companies(id) on delete cascade,
|
||||||
show_as_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
|
show_to_id integer references companies(id) on delete cascade
|
||||||
) strict;
|
) strict;
|
||||||
|
create index if not exists idx_company_mappings_project_id on company_mappings (project_id);
|
||||||
|
|
||||||
create table if not exists task_users (
|
create table if not exists task_users (
|
||||||
task_id integer references tasks(id) on delete cascade,
|
task_id integer references tasks(id) on delete cascade,
|
||||||
@@ -153,8 +172,7 @@ create table if not exists chat_users (
|
|||||||
pragma foreign_keys = on;
|
pragma foreign_keys = on;
|
||||||
|
|
||||||
|
|
||||||
create trigger if not exists trg_chats_update after update
|
create trigger if not exists trg_chats_update after update on chats
|
||||||
on chats
|
|
||||||
when NEW.project_id is null
|
when NEW.project_id is null
|
||||||
begin
|
begin
|
||||||
delete from chat_users where chat_id = NEW.id;
|
delete from chat_users where chat_id = NEW.id;
|
||||||
|
|||||||
BIN
backend/data/log.sqlite
Normal file
@@ -1,12 +0,0 @@
|
|||||||
Описание ПО
|
|
||||||
|
|
||||||
Программа состоит из двух частей: бота и приложение miniapp, привязанное к боту. Приложение отображает проекты для пользователя, задачи и встречи на проекте, прикрепленные к ним файлы, а также список тех, с кем пользователь пересекался в группах. Бот отслеживает не только участников группы, но и при наличии админских прав в группе, пересылаемые файлы и сообщения для резервного копирования.
|
|
||||||
|
|
||||||
Термины
|
|
||||||
Клиенты - специальные аккаунты для организаций, регистрируемые в miniapp, позволяющие управлять проектами и контактами на них.
|
|
||||||
Пользователи - пользователи Telegram, участвующие в одной или нескольких группах где есть бот. Пользователи могут заходить в miniapp и
|
|
||||||
|
|
||||||
После добавления бота в группу, группа привязывается к специальному внутреннему клиенту, который имеет только один проект "Вне проектов". Администратор группы может
|
|
||||||
|
|
||||||
После добавления в группу и привязки токеном к аккаунту клиента, бот отслеживает новые сообщения в группе. При обнаружении файла он копируется с специальную группу, указанную клиентом, и регистрирует его в списке файлов, доступных для участников группы. Бот не хранит ни сами файлы, ни сообщения.
|
|
||||||
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
id-телеграма могут быть больше 32-битного int, поэтому лучше использовать свои id + кеш, который позволит быстро конвертировать одни id в другие. Хотя может быть использование BigInt будет достаточно. Большие id могут негативно сказаться на скорости выборки данных и индексировании.
|
|
||||||
|
|
||||||
A: Будут использоваться свои id, а ID Telegram записиываться в отдельные колонки.
|
|
||||||
-----------------------------------------------------
|
|
||||||
По хорошему для каждого customer надо иметь свою базу. Поскольку для отображения данных проекта требуется информация только именно этого customer, то информация может быть локализвана. При этом остается общая база, в которой хранится список пользователей (таблица users) и информация, необходимая для формирования первой сводной страницы, напр. общее число незавершенных задач и календарь совещаний, а также id-клиентов, у которых пользователь участвует/вал в проектах.
|
|
||||||
? Можно ли сделать так, чтобы клиент имел свою установку, но при этом пользователи обращались к нашему боту/miniapp?
|
|
||||||
Если данные запрашивать не напрямую из базы, а по http/https, то каждый узел будет иметь свою базу.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Хотелось бы не хранить сообщения локально. А запрашивать их за день и отправлять их бекап в специальную группу. Бот имеет ограничение 20-30 запросов в секунду, т.е. за минуту бот может получить данные из 1200 чатов.
|
|
||||||
|
|
||||||
A: Не хранить сообщения. Запрашивать их, если клиент установил настройку.
|
|
||||||
-----------------------------------------------------
|
|
||||||
При удалении проекта/задачи/встречи/компании надо также чистить links. Возможно лучше сделать отдельные таблицы для каждой связки, чтобы использовать FK + delete cascade
|
|
||||||
|
|
||||||
A: Операция выполняется DELETE CASCADE автоматически
|
|
||||||
-----------------------------------------------------
|
|
||||||
ФИО пользователей задает customer и ФИО в адресной книге всегда показывается. Телефон задает пользователь и пользователь может указать на каком проекте его показывать, а где нет. По умолчанию "Не показывать"
|
|
||||||
<- может храниться в users.is_show_phone = [project_id1, project_id3]
|
|
||||||
|
|
||||||
A: Хранится в настойках пользователя в таблице users
|
|
||||||
-----------------------------------------------------
|
|
||||||
Если пользователь изменил задачу, то у тех, у кого она есть в миниаппе, должны получить по ней обновление. Вопрос в том, как маякнуть клиенту, что что-то обновилось?
|
|
||||||
Два варианта: клиент должен запрашивать обновления скажем раз в минуту или же использовать WebSocket. Есть вероятность, что для сервера поддерживать кучу WebScoket слишком накладно.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Не делать API, который возвращает одну сущность, а только список.
|
|
||||||
Если надо сущность, то можно /api/project?id=1
|
|
||||||
Или можно использовать redirect с /api/project/:id на /api/project?id=1
|
|
||||||
|
|
||||||
A: Да
|
|
||||||
-----------------------------------------------------
|
|
||||||
Кто заполняет ФИО пользователя? Если админ, тогда возможна ситуация, когда на проекте одного админа человек будет с ФИО, а на проекте другого - нет. Возможно ФИО также должен заполнять сам пользователь. По сути есть некоторая дыра, когда пользователь предоставляет ФИО, телефон и номер телеграма всем желающим,
|
|
||||||
когда можно заманить человека в группу с ботом и узнать его личные данные.
|
|
||||||
|
|
||||||
A: Фамилию задает клиент отдельно на каждом проекте. А также роль и департамент.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Польщователь сам решает показывать ли ему ФИО и/или телефон на
|
|
||||||
проектах конкретного админа системы.
|
|
||||||
|
|
||||||
А: Пользователь решает только про телефон
|
|
||||||
-----------------------------------------------------
|
|
||||||
Добавить документы может любой из участников. Удалять можно только своё или если создатель.
|
|
||||||
|
|
||||||
A: Да.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Если переслать документ боту, то бот спросит к какой задаче прикрепить. Автор в этом случае тот, кто переслал сообщение.
|
|
||||||
|
|
||||||
A: В пересылаем соообщении нет информации о том, откуда оно было получено, соотв. нельзя определить к какому проекту относится пересылаемое напрямую. Как вариант - использовать последний проект, где пользователь отправил сообщение в группе + кнопка Другой проект.
|
|
||||||
Если бот не имеет админских прав, то это будет плохо работать.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Предположим, что бота добавляют в группу А. Что делать, если не все в группе используют бота/miniapp?
|
|
||||||
-----------------------------------------------------
|
|
||||||
Если бот не получает админских прав, то привязать группу токеном не получится, т.к. бот не видит сообщений. Можно сделать, чтобы пользователь отправил комманду /attach<token>. Пока передать параметр в команду невозможно.
|
|
||||||
https://gram.js.org/tl/bots/SetBotCommands
|
|
||||||
|
|
||||||
|
|
||||||
Для отправки пользователям сообщений бот должен знать не только их id, но и accessHash. Аналогично для отправки сообщения в чат или скачивания файла, т.е. надо хранить еще и это поле.
|
|
||||||
Когда пользователь отписывается от бота, то надо обнулять его hash. В адресной книге тех, к кому у бота нет доступа, т.е. они участники группы, но у них нет бота, отображать специальным образом.
|
|
||||||
|
|
||||||
А: если у того, кому назначили задачу, нет бота и следовательно accessHash, то можно отправлять в один из чатов, в котором участвуют создатель и ответственный, возможно отсортировав по времени последнего сообщения от каждого из них. Наблюдатели должны использовать бота.
|
|
||||||
-----------------------------------------------------
|
|
||||||
Возможно дать некоторым пользователям добавлять группы к проекту самостоятельно, путем нажатия кнопки "Добавить группу".
|
|
||||||
-----------------------------------------------------
|
|
||||||
В uTasks поначалу отображали только задачи, связанные с пользователем, потом они добавили крыж "Показывать все задачи" по просьбам пользователей. Почему пользователям это понадобилось? Возможно потому что у них нет механизма наблюдателей.
|
|
||||||
Возможно нужно для контроля начальником => отдельное приложение
|
|
||||||
-----------------------------------------------------
|
|
||||||
Предположим, что бот был в группе, а потом его удалили из нее.
|
|
||||||
Должна ли группа отображаться в списке групп проекта?
|
|
||||||
-----------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
const { Api, TelegramClient } = require('telegram')
|
|
||||||
const { StringSession } = require('telegram/sessions')
|
|
||||||
const session = new StringSession('')
|
|
||||||
|
|
||||||
const apiId = 26746106
|
|
||||||
const apiHash = '29e5f83c04e635fa583721473a6003b5'
|
|
||||||
const BOT_TOKEN = '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k';
|
|
||||||
|
|
||||||
const client = new TelegramClient(session, apiId, apiHash, {})
|
|
||||||
|
|
||||||
(async function run() {
|
|
||||||
await client.start({botAuthToken: BOT_TOKEN})
|
|
||||||
...
|
|
||||||
})()
|
|
||||||
|
|
||||||
Получение информации о себе
|
|
||||||
const result = await client.getEntity("me")
|
|
||||||
|
|
||||||
Группа при создании имеет тип group. Её id выглядит как -4646437202
|
|
||||||
Для получения информации надо отбросить минус
|
|
||||||
const result = await client.getEntity(new Api.PeerChat({ chatId: 4646437202n }))
|
|
||||||
Для получения пользователей и описания
|
|
||||||
const result = await client.invoke(new Api.messages.GetFullChat({chatId: 4646437202n}))
|
|
||||||
|
|
||||||
После включения топиков группа меняется на канал и её id становится вида -1002496664184
|
|
||||||
Для получения информации
|
|
||||||
const result = await client.getEntity(new Api.PeerChannel({ channelId: -1002496664184n }))
|
|
||||||
При этом аттрибут forum = true
|
|
||||||
Если топики отключить, то id и класс уже не меняется, но forum = false
|
|
||||||
Для получения списка пользователей канала
|
|
||||||
const channel = new Api.PeerChannel({ channelId: -1002496664184n })
|
|
||||||
const result = await client.invoke(
|
|
||||||
new Api.channels.GetParticipants({
|
|
||||||
channel,
|
|
||||||
filter: new Api.ChannelParticipantsRecent(),
|
|
||||||
limit: 999999,
|
|
||||||
offset:0 ,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
Скачивание файла из сообщения на диск
|
|
||||||
client.addEventHandler(async (update) => {
|
|
||||||
const msg = update.message
|
|
||||||
if (msg?.className == 'Message') {
|
|
||||||
const buffer = await client.downloadMedia(msg, {})
|
|
||||||
fs.writeFileSync("d:/CODES/Telegram/tmp/file", buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
}, new NewMessage({}));
|
|
||||||
|
|
||||||
Примеры кода
|
|
||||||
Получение списка пользователей в супергруппе
|
|
||||||
https://gist.github.com/waptik/9de410055eac8a60668ce7ac1e5183ac
|
|
||||||
|
|
||||||
Ссылка для выбора группы - https://qna.habr.com/q/1374460
|
|
||||||
|
|
||||||
У пользователя есть Name = First Name + Last Name - обязательно, а также username - опционально для t.me/username
|
|
||||||
|
|
||||||
Выбор куда отправить - ForwardTo - https://core.telegram.org/widgets/share
|
|
||||||
|
|
||||||
Размеры превьюшек - https://core.telegram.org/api/files#stripped-thumbnails
|
|
||||||
|
|
||||||
you can just pass an ID.
|
|
||||||
if channel/superroup => add -100
|
|
||||||
if group add => -
|
|
||||||
if user don't add anything
|
|
||||||
for example for your test group2 it would be
|
|
||||||
client.sendMessage(-463172658,{params})
|
|
||||||
https://github.com/gram-js/gramjs/issues/146#issuecomment-913528553
|
|
||||||
BIN
backend/docs/triggers_v1.xlsx
Normal file
@@ -4,38 +4,15 @@ const sqlite3 = require('better-sqlite3')
|
|||||||
|
|
||||||
const db = sqlite3(`./data/db.sqlite`)
|
const db = sqlite3(`./data/db.sqlite`)
|
||||||
db.pragma('journal_mode = WAL')
|
db.pragma('journal_mode = WAL')
|
||||||
|
db.pragma('foreign_keys = on')
|
||||||
|
|
||||||
db.exec(fs.readFileSync('./data/init.sql', 'utf8'))
|
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) => {
|
db.function('generate_key', (id, time) => {
|
||||||
return [
|
return [
|
||||||
'KEY',
|
'KEY',
|
||||||
Buffer.from(time + '').toString('base64'),
|
Buffer.from(time + '').toString('base64'),
|
||||||
crypto.createHash('md5').update(`sa${time}-${id}lt`).digest('hex')
|
crypto.createHash('md5').update(`sa${time}-${id}lty`).digest('hex')
|
||||||
].join('-')
|
].join('-')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -54,4 +31,16 @@ db.prepareUpdate = function (table, columns, data, where) {
|
|||||||
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
|
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.prepareUpsert = 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(
|
||||||
|
`insert into "${table}" (` + [...dataColumns, ...where].join(', ') + `) values (:` + [...dataColumns, ...where].join(', :') +
|
||||||
|
`) on conflict (` + where.join(',') + `) do update ` +
|
||||||
|
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
|
||||||
|
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = db
|
module.exports = db
|
||||||
5
backend/include/eventbus.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const EventEmitter = require('events')
|
||||||
|
class EventBus extends EventEmitter {}
|
||||||
|
const eventBus = new EventBus()
|
||||||
|
|
||||||
|
module.exports = eventBus
|
||||||
58
backend/include/i18n.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const i18n = {
|
||||||
|
en: {
|
||||||
|
ON_PROJECT_ARCHIVE: 'The project was moved to an rachive. Message tracking has been discontinued.',
|
||||||
|
ON_PROJECT_RESTORE: 'The project has been recovered from the archives.',
|
||||||
|
ON_CHAT_REMOVE: 'The chat was removed from a project.',
|
||||||
|
CHAT_IN_USE: 'Chat is already in use',
|
||||||
|
EXPIRED_KEY: 'The key validity time for binding has expired',
|
||||||
|
OPEN_PROJECT: 'Open project',
|
||||||
|
WELCOME: 'Welcome, strange',
|
||||||
|
TASK_MESSAGE: ':clipboard: New task ${PRIORITY}\n${NAME}\n[${CREATOR}](tg://user?id=${CREATOR_ID}) > [${ASSIGNEE}](tg://user?id=${ASSIGNEE_ID})\nPlan date: ${PLAN_DATE}\n[Open a task page](${URL})',
|
||||||
|
TASK_PRIORITY_0: '',
|
||||||
|
TASK_PRIORITY_1: '',
|
||||||
|
TASK_PRIORITY_2: '(Important)',
|
||||||
|
TASK_PRIORITY_3: '(Critical)',
|
||||||
|
MEETING_MESSAGE: 'Meeting ${name}',
|
||||||
|
ADMIN_APP: 'Administrator Access',
|
||||||
|
USER_APP: 'User Access'
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
ON_PROJECT_ARCHIVE: 'Проект помещен в архив. Отслеживание сообщений прекращено.',
|
||||||
|
ON_PROJECT_RESTORE: 'Проект восстановлен из архива.',
|
||||||
|
ON_CHAT_REMOVE: 'Чат удален из проекта.',
|
||||||
|
CHAT_IN_USE: 'Чат уже используется',
|
||||||
|
EXPIRED_KEY: 'Время действия ключа для привязки истекло',
|
||||||
|
OPEN_PROJECT: 'Открыть проект',
|
||||||
|
WELCOME: 'Добро пожаловать',
|
||||||
|
TASK_MESSAGE: '📋 Новая задача ${PRIORITY}\n${NAME}\n<a href="tg://user?id=${CREATOR_ID}">${CREATOR}</a> > <a href="tg://user?id=${ASSIGNEE_ID}">${ASSIGNEE}</a>\nСрок: ${PLAN_DATE}\n<a href="${URL}">Перейти на страницу задачи</a>',
|
||||||
|
TASK_PRIORITY_0: '',
|
||||||
|
TASK_PRIORITY_1: '',
|
||||||
|
TASK_PRIORITY_2: '(Важно)',
|
||||||
|
TASK_PRIORITY_3: '(Критично)',
|
||||||
|
MEETING_MESSAGE: 'Встреча ${name}',
|
||||||
|
ADMIN_APP: 'Режим администратора',
|
||||||
|
USER_APP: 'Режим пользователя'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLocale = (locale) => !!i18n[locale]
|
||||||
|
function getString (locale, code, args = {}) {
|
||||||
|
const params = {}
|
||||||
|
Object.keys(args instanceof Object ? args : {}).forEach(key => {
|
||||||
|
const value = args[key]
|
||||||
|
params[key] = value[0] == '$' ? getString(locale, value.substring(1)) : value
|
||||||
|
})
|
||||||
|
|
||||||
|
let res = i18n[locale] ? i18n[locale][code] : undefined
|
||||||
|
if (res === undefined)
|
||||||
|
res = i18n.en[code]
|
||||||
|
if (res === undefined)
|
||||||
|
res = code
|
||||||
|
if (res === undefined)
|
||||||
|
res = 'ERROR'
|
||||||
|
|
||||||
|
res = res.replace(/\${(\w+)}/g, (match, key) => params.hasOwnProperty(key) ? params[key] : match)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { hasLocale, getString }
|
||||||
90
backend/include/log.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const sqlite3 = require('better-sqlite3')
|
||||||
|
|
||||||
|
const db = sqlite3(`./data/log.sqlite`)
|
||||||
|
db.pragma('journal_mode = DELETE')
|
||||||
|
db.pragma('synchronous = 0')
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
create table if not exists http (time integer default (strftime('%s','now')), method text, url text, headers text, body text, status text, duration integer);
|
||||||
|
create table if not exists mtproto (time integer default (strftime('%s','now')), message text);
|
||||||
|
create table if not exists error (time integer default (strftime('%s','now')), message text, stack text);
|
||||||
|
`)
|
||||||
|
|
||||||
|
function http (req, res, duration) {
|
||||||
|
const data = {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
status: res.statusCode,
|
||||||
|
duration
|
||||||
|
};
|
||||||
|
|
||||||
|
['headers', 'body'].forEach(key => {
|
||||||
|
const value = req.body[key]
|
||||||
|
try {
|
||||||
|
data[key] = value instanceof Array && value.length < 1_000_000 || value instanceof Object ? JSON.stringify(value) : value?.toString() || null
|
||||||
|
} catch (err) {
|
||||||
|
error(err)
|
||||||
|
data[key] = value?.toString() || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`insert into http (method, url, headers, body, status, duration) values (:method, :url, :headers, :body, :status, :duration)`)
|
||||||
|
.run(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeStringify(obj, indent = 2) {
|
||||||
|
const cache = new Set()
|
||||||
|
|
||||||
|
return JSON.stringify(obj, (key, value) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (cache.has(value))
|
||||||
|
return '[Circular]'
|
||||||
|
|
||||||
|
cache.add(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mtproto (update) {
|
||||||
|
const message = update
|
||||||
|
message.self = message
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`insert into mtproto (message) values (:message)`)
|
||||||
|
.run({
|
||||||
|
message: safeStringify(message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function error (...args) {
|
||||||
|
const stacks = []
|
||||||
|
const message = args.map(arg => {
|
||||||
|
if (arg instanceof Error) {
|
||||||
|
stacks.push(arg.stack)
|
||||||
|
return 'Error: ' + arg.message
|
||||||
|
} else if (arg instanceof Object) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg)
|
||||||
|
} catch (err) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(arg)
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
if (stacks.length == 0)
|
||||||
|
stacks.push(new Error().stack)
|
||||||
|
|
||||||
|
console.error(message)
|
||||||
|
|
||||||
|
db
|
||||||
|
.prepare(`insert into error (message, stack) values (:message, :stack)`)
|
||||||
|
.run({
|
||||||
|
message,
|
||||||
|
stack: stacks.join('\n\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { http, mtproto, error }
|
||||||
94
backend/package-lock.json
generated
@@ -12,12 +12,14 @@
|
|||||||
"better-sqlite3": "^11.8.0",
|
"better-sqlite3": "^11.8.0",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"telegram": "^2.26.16"
|
"telegram": "^2.26.16",
|
||||||
|
"ws": "^8.18.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cryptography/aes": {
|
"node_modules/@cryptography/aes": {
|
||||||
@@ -81,9 +83,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/better-sqlite3": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "11.9.1",
|
"version": "11.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||||
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
|
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -293,12 +295,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie-parser": {
|
"node_modules/cookie-parser": {
|
||||||
@@ -314,6 +316,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-parser/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie-signature": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
@@ -485,9 +496,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
@@ -686,6 +697,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-session/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express-session/node_modules/cookie-signature": {
|
"node_modules/express-session/node_modules/cookie-signature": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
@@ -1098,6 +1118,7 @@
|
|||||||
"version": "1.4.5-lts.2",
|
"version": "1.4.5-lts.2",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||||
|
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
@@ -1301,9 +1322,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -1395,9 +1416,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/real-cancellable-promise": {
|
"node_modules/real-cancellable-promise": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.2.tgz",
|
||||||
"integrity": "sha512-JwhiWJTMMyzFYfpKsiSb8CyQktCi1MZ8ZBn3wXvq28qXDh8Y5dM7RYzgW3r6SV22JTEcof8pRsvDp4GxLmGIxg==",
|
"integrity": "sha512-Qh1RvIGdekUCv/ZkK9IiAkah2/Q++p66KHe6TSgHnx4QSbr5vCo3qDoszqRO1TSH+6h6HI5aDVBVrQCQBGj44Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
@@ -1427,9 +1448,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -1635,9 +1656,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socks": {
|
"node_modules/socks": {
|
||||||
"version": "2.8.4",
|
"version": "2.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
|
||||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "^9.0.5",
|
"ip-address": "^9.0.5",
|
||||||
@@ -1702,9 +1723,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||||
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
|
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^1.1.1",
|
"chownr": "^1.1.1",
|
||||||
@@ -1967,6 +1988,27 @@
|
|||||||
"slide": "^1.1.5"
|
"slide": "^1.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||||
|
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
"better-sqlite3": "^11.8.0",
|
"better-sqlite3": "^11.8.0",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"telegram": "^2.26.16"
|
"telegram": "^2.26.16",
|
||||||
|
"ws": "^8.18.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
backend/sqlite-viewver/sqlite-x.exe
Normal file
47
backend/sqlite-viewver/sqlite-x.ini
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
[sqlite-x]
|
||||||
|
font-size =16
|
||||||
|
header-row = 1 ; 0/1
|
||||||
|
filter-row =0
|
||||||
|
dark-theme =0
|
||||||
|
|
||||||
|
; Below params can be changed only in the ini file
|
||||||
|
font = Arial ;
|
||||||
|
disable-grid-lines = 1 ; 0/1, Disable/Enable grid lines
|
||||||
|
max-column-width = 300 ;
|
||||||
|
cache-size = 2000 ; Cached rows
|
||||||
|
delete-journal = 0 ; 0 - keep, 1 - ask, 2 - delete
|
||||||
|
filter-align = 0 ; -1 - left, 0 - center, 1 - right
|
||||||
|
copy-column = 0 ; Ctrl + C: 0 - copy a current cell, 1 - copy all cells from selected rows/current column
|
||||||
|
; column-delimiter = ; Used when copy rows. Should be one char. Default is TAB
|
||||||
|
editable = 1 ; Allow to edit data in a cell by F2 or Space
|
||||||
|
exit-by-escape = 1 ;
|
||||||
|
open-last-db = 1 ; Open the last database on app start
|
||||||
|
recent=F:\project\projectsNode\backend\data\db.sqlite?
|
||||||
|
|
||||||
|
; Colors
|
||||||
|
; Light theme
|
||||||
|
text-color = 0 ; RGB(0, 0, 0)
|
||||||
|
back-color = 16777215 ; RGB(255, 255, 255)
|
||||||
|
back-color2 = 15790320 ; RGB(240, 240, 240)
|
||||||
|
filter-text-color = 0 ; RGB(0, 0, 0)
|
||||||
|
filter-back-color = 15790320 ; RGB(240, 240, 240)
|
||||||
|
selection-text-color = 16777215 ; RGB(255, 255, 255)
|
||||||
|
selection-back-color = 6956042 ; RGB(10, 36, 106)
|
||||||
|
current-cell-back-color = 10903622 ; RGB(70, 96, 166)
|
||||||
|
; splitter-color = <as button>
|
||||||
|
|
||||||
|
; Dark theme
|
||||||
|
text-color-dark = 14474460 ; RGB(220, 220, 220)
|
||||||
|
back-color-dark = 2105376 ; RGB(32, 32, 32)
|
||||||
|
back-color2-dark = 3421236 ; RGB(52, 52, 52)
|
||||||
|
filter-text-color-dark = 16777215 ; RGB(255, 255, 255)
|
||||||
|
filter-back-color-dark = 3947580 ; RGB(60, 60, 60)
|
||||||
|
selection-text-color-dark = 14474460 ; RGB(220, 220, 220)
|
||||||
|
selection-back-color-dark = 6710856 ; RGB(72, 102, 102)
|
||||||
|
current-cell-back-color-dark = 4079136 ; RGB(32, 62, 62)
|
||||||
|
splitter-position=200
|
||||||
|
position-x=93
|
||||||
|
position-y=151
|
||||||
|
width=1686
|
||||||
|
height=600
|
||||||
|
; splitter-color = <as button>
|
||||||
BIN
i18n-2.xlsm
7
public/3software/license/axios/LICENSE.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Copyright (c) 2014-present Matt Zabriskie & Collaborators
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
21
public/3software/license/better-sqlite3/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Joshua Wise
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
24
public/3software/license/express/LICENSE.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2009-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
Copyright (c) 2013-2014 Roman Shtylman <shtylman+expressjs@gmail.com>
|
||||||
|
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
21
public/3software/license/pinia/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019-present Eduardo San Martin Morote
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
21
public/3software/license/quasar/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present Razvan Stoenescu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
20
public/3software/license/vue-i18n/LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-present kazuya kawaguchi and contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
21
public/3software/license/vue-router/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019-present Eduardo San Martin Morote
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
21
public/3software/license/vue/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
8
public/3software/logo/axios.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="188" height="28" viewBox="0 0 188 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M93.295 3.65206L86.356 9.30495H90.8876V27.68L93.295 25.7411V3.65206Z" fill="#5A29E4"/>
|
||||||
|
<path d="M95.295 24.0997L102.356 18.305H97.6975V0.350052L95.295 2.02275V24.0997Z" fill="#5A29E4"/>
|
||||||
|
<path d="M182.695 6.95295C183.495 7.36895 184.071 7.72095 184.423 8.00895L186.919 3.25695C185.671 2.48895 184.167 1.80095 182.407 1.19295C180.679 0.584955 178.807 0.280952 176.791 0.280952C174.871 0.280952 173.095 0.600952 171.463 1.24095C169.863 1.88095 168.583 2.82495 167.623 4.07295C166.695 5.32095 166.231 6.87295 166.231 8.72895C166.231 10.809 166.887 12.409 168.199 13.529C169.543 14.617 171.591 15.513 174.343 16.217C176.551 16.793 178.327 17.321 179.671 17.801C181.047 18.249 181.735 19.001 181.735 20.057C181.735 21.625 180.263 22.409 177.319 22.409C175.847 22.409 174.455 22.233 173.143 21.881C171.831 21.529 170.679 21.097 169.687 20.585C168.727 20.073 168.039 19.609 167.623 19.193L165.031 24.233C166.695 25.289 168.599 26.121 170.743 26.729C172.887 27.337 175.047 27.641 177.223 27.641C179.111 27.641 180.871 27.385 182.503 26.873C184.135 26.329 185.447 25.465 186.439 24.281C187.463 23.065 187.975 21.4649 187.975 19.4809C187.975 17.8489 187.591 16.537 186.823 15.545C186.087 14.521 185.015 13.705 183.607 13.097C182.231 12.489 180.599 11.945 178.711 11.465C176.567 10.953 174.935 10.489 173.815 10.073C172.727 9.65695 172.183 8.95295 172.183 7.96095C172.183 6.26495 173.687 5.41695 176.695 5.41695C177.815 5.41695 178.903 5.57695 179.959 5.89695C181.015 6.18495 181.927 6.53695 182.695 6.95295Z" fill="#5A29E4"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M132.182 27.497C130.07 27.497 128.15 27.129 126.422 26.393C124.694 25.625 123.205 24.601 121.957 23.321C120.709 22.009 119.749 20.537 119.077 18.905C118.406 17.273 118.069 15.593 118.069 13.865C118.069 12.105 118.421 10.409 119.125 8.77695C119.829 7.14495 120.822 5.70496 122.102 4.45695C123.382 3.17695 124.885 2.16895 126.613 1.43295C128.341 0.696953 130.229 0.328949 132.277 0.328949C134.389 0.328949 136.31 0.728952 138.038 1.52895C139.766 2.29695 141.238 3.33695 142.454 4.64895C143.702 5.92895 144.661 7.38495 145.333 9.01695C146.005 10.649 146.342 12.3129 146.342 14.0089C146.342 15.7689 145.99 17.465 145.286 19.097C144.582 20.697 143.589 22.137 142.309 23.417C141.061 24.665 139.574 25.657 137.846 26.393C136.118 27.129 134.23 27.497 132.182 27.497ZM123.925 13.913C123.925 15.353 124.262 16.729 124.934 18.041C125.605 19.321 126.549 20.361 127.765 21.161C129.013 21.961 130.501 22.361 132.229 22.361C133.989 22.361 135.477 21.945 136.693 21.113C137.91 20.249 138.837 19.177 139.477 17.8969C140.117 16.5849 140.438 15.241 140.438 13.865C140.438 12.425 140.102 11.0649 139.43 9.78495C138.758 8.50495 137.798 7.48095 136.549 6.71295C135.333 5.91295 133.878 5.51295 132.182 5.51295C130.422 5.51295 128.917 5.92895 127.669 6.76095C126.453 7.59295 125.525 8.64896 124.885 9.92896C124.245 11.209 123.925 12.537 123.925 13.913Z" fill="#5A29E4"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 27.305L11.712 0.472954H16.464L28.128 27.305H21.984L19.296 21.017H8.88L6.192 27.305H0ZM14.112 7.52895L10.176 15.977H17.904L14.112 7.52895Z" fill="#5A29E4"/>
|
||||||
|
<path d="M50.8211 0.472954L58.2131 9.97695L65.6051 0.472954H71.8931L61.2851 14.057L71.5571 27.305H65.2691L58.2131 18.185L51.2051 27.305H44.8211L55.1411 14.057L44.4851 0.472954H50.8211Z" fill="#5A29E4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
3
public/3software/logo/express.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="express-logo" viewBox="0 0 1120.322 250" width="90" height="30">
|
||||||
|
<path d="M347.47162 250V4.890464h13.29516v38.55596a50.415241 50.415241 0 0 0 4.33954-5.43506c11.10412-23.03785 34.52487-37.57744 60.09412-37.29026 30.31296-.90408 54.94623 10.31704 69.422 37.29026a119.86915 119.86915 0 0 1 2.89302 109.881816c-13.4866 30.22788-46.79895 45.25672-82.46189 39.73657a66.688515 66.688515 0 0 1-53.22317-35.12049v97.4801zm13.29516-158.403836 2.97812 28.781356c5.25425 32.75927 24.62263 52.11702 55.03132 55.75458a62.540425 62.540425 0 0 0 68.69874-39.73657c11.82737-28.18574 10.95521-60.094116-2.33995-87.620406a61.26409 61.26409 0 0 0-64.54001-35.66294 59.668671 59.668671 0 0 0-53.30827 44.07611 304.40595 304.40595 0 0 0-6.51995 34.39724zm420.10574 33.312346a71.687494 71.687494 0 0 1-70.06017 63.35941c-55.75458 2.80794-81.91945-34.21642-86.07817-76.94174a123.27271 123.27271 0 0 1 10.85948-67.890396 75.729222 75.729222 0 0 1 78.83497-42.26797 72.432023 72.432023 0 0 1 64.26348 55.12705 371.94535 371.94535 0 0 1 6.24341 40.73636H638.50796c-2.71221 38.736776 18.10269 69.879356 47.6073 77.388456 36.74782 9.04071 68.1563-6.88157 79.55823-41.82125 2.5314-8.96625 7.14748-10.23195 15.29475-7.68992zM638.4016 84.629504h130.97326c-.81898-41.26817-26.51586-71.26205-61.37045-71.60241-39.35367-.63816-67.89039 28.15383-69.60281 71.60241zm169.53986 41.183076h12.83781a51.478853 51.478853 0 0 0 30.22787 44.35265 79.026422 79.026422 0 0 0 68.61365-1.80814 30.844768 30.844768 0 0 0 18.10269-30.3236 27.973013 27.973013 0 0 0-18.82595-27.97301c-14.12477-5.25425-29.14298-8.14727-43.53366-12.763346a319.0838 319.0838 0 0 1-43.81021-16.01801c-23.18675-11.31684-24.62263-55.39295 1.62733-69.34755a92.427941 92.427941 0 0 1 88.34367-1.36142c16.95398 9.35979 26.32441 28.26019 23.53775 47.43712h-11.00839c0-.5318-.9998-.99979-.9998-1.54223-1.36142-35.09922-30.86604-46.07571-62.54043-42.99123-9.57251 1.06361-18.64513 3.95664-27.15403 8.23236a27.122123 27.122123 0 0 0-15.74147 27.15404 27.122123 27.122123 0 0 0 18.10269 25.5267c13.82697 5.07343 28.50482 8.32809 42.81041 12.306l34.56741 9.04071a40.842727 40.842727 0 0 1 28.05811 36.843536c2.76539 18.56004-6.70076 36.801-23.44203 45.25672-30.22787 17.10289-80.01558 12.58254-102.1919-9.04071-11.34875-11.41256-17.67724-26.9094-17.54961-42.99123zm306.10774-67.794666h-12.0401c0-1.62733-.6382-3.19084-.819-4.43526a39.353669 39.353669 0 0 0-32.0466-37.83271 79.026422 79.026422 0 0 0-50.7769 2.44631 30.844768 30.844768 0 0 0-22.35715 29.41953 28.398458 28.398458 0 0 0 21.71895 28.60054l55.0313 14.12478a153.05386 153.05386 0 0 1 17.5497 5.33934c17.5496 6.381666 29.462 22.676216 29.9938 41.300076a45.203539 45.203539 0 0 1-27.6539 42.96995 100.72412 100.72412 0 0 1-81.4621.81898 56.477833 56.477833 0 0 1-34.0356-54.85051h11.76356c4.42463 21.32544 19.07054 39.08777 39.16224 47.49031 20.0916 8.40254 43.0337 6.33913 61.3066-5.48824a32.333825 32.333825 0 0 0 17.3794-30.22787 27.973013 27.973013 0 0 0-19.1024-27.7922c-14.1248-5.25425-29.143-8.05155-43.5337-12.763356a320.67922 320.67922 0 0 1-44.0761-15.83719c-22.6337-11.13602-24.44184-54.8505 1.3614-68.79447a91.151606 91.151606 0 0 1 89.7902-.99979 47.330764 47.330764 0 0 1 22.7188 46.43733zM325.59311 183.84329a20.740447 20.740447 0 0 1-25.70752-9.7746l-46.79895-64.72083-6.78585-9.04071-54.30807 73.85727a19.889557 19.889557 0 0 1-24.44182 9.59378l69.96445-93.863816-65.0931-84.81247c9.6576-3.48865 20.42136.29781 25.79261 9.04071l48.50074 65.51854 48.78791-65.26328a19.464112 19.464112 0 0 1 24.261-9.05134l-25.2608 33.51443-34.21642 44.53347a9.040708 9.040708 0 0 0 0 13.48661l65.16755 86.982236zM622.66013 4.177844v12.76335a65.624902 65.624902 0 0 0-69.87935 67.79467v99.564786h-12.94417V4.975554h12.76336v36.74781c15.65637-26.80304 39.82165-36.74781 70.14525-37.47107ZM.021272 88.724414l5.700964-28.15383c15.656379-55.66949 79.473139-78.83497 123.379074-44.35265 25.70751 20.18737 32.1211 48.78792 30.86604 81.01538H15.135208C12.79526 154.79603 54.329335 189.55489 107.45679 171.81383c17.50706-6.38167 30.68522-20.93189 35.02476-39.01331 2.80794-9.04071 7.44529-10.59359 15.93292-7.9771a73.495636 73.495636 0 0 1-35.12049 53.68054 85.089014 85.089014 0 0 1-99.118064-12.66763c-12.93353-14.53959-20.740447-32.91881-22.261413-52.32975 0-3.19084-1.063613-6.16895-1.808142-9.0407Q0 96.414334 0 88.724414Zm15.294751-3.89282H146.28929c-.81898-41.72553-27.15403-71.32587-62.274525-71.60241-39.098402-.53181-67.071415 28.41973-68.794468 71.42159Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
1
public/3software/logo/pinia.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
123
public/3software/logo/vue-i18n.svg
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="256"
|
||||||
|
height="224"
|
||||||
|
viewBox="0 0 67.733332 59.266668"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||||
|
sodipodi:docname="vue-i18n.svg"
|
||||||
|
inkscape:export-filename="/Users/kazupon/Desktop/vue-i18n.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="2"
|
||||||
|
inkscape:cx="65.171196"
|
||||||
|
inkscape:cy="106.17152"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:lockguides="false"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
inkscape:window-height="751"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="1"
|
||||||
|
inkscape:window-maximized="1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid3715" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-237.73331)">
|
||||||
|
<rect
|
||||||
|
style="fill:#42b983;fill-opacity:1;stroke-width:0.44801387"
|
||||||
|
id="rect4524"
|
||||||
|
width="67.73333"
|
||||||
|
height="59.266666"
|
||||||
|
x="0"
|
||||||
|
y="237.73331"
|
||||||
|
ry="6.8791666" />
|
||||||
|
<rect
|
||||||
|
style="fill:#34495e;fill-opacity:1;stroke-width:1.29214942"
|
||||||
|
id="rect4528"
|
||||||
|
width="55.033333"
|
||||||
|
height="45.508335"
|
||||||
|
x="6.3499999"
|
||||||
|
y="244.61247"
|
||||||
|
ry="6.8791666" />
|
||||||
|
<path
|
||||||
|
style="fill:#34495e;fill-opacity:1;stroke:none;stroke-width:5.39703941;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
|
d="m 33.866667,270.54165 v 26.45833 L 6.35,270.54165 Z"
|
||||||
|
id="path4538"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<circle
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.11716342;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
|
id="path4567"
|
||||||
|
cx="34.008522"
|
||||||
|
cy="-267.371"
|
||||||
|
transform="scale(1,-1)"
|
||||||
|
r="15.610168" />
|
||||||
|
<ellipse
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.067662;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
|
id="path4567-7"
|
||||||
|
cx="33.929485"
|
||||||
|
cy="-267.40591"
|
||||||
|
transform="scale(1,-1)"
|
||||||
|
rx="7.4328356"
|
||||||
|
ry="15.634919" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 33.866667,252.02081 v 30.42708 z"
|
||||||
|
id="path4596"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 48.947917,267.89581 H 18.520837 Z"
|
||||||
|
id="path4596-2"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:2.02254486;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 47.625,259.95831 H 19.843753 Z"
|
||||||
|
id="path4596-2-8"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-width:2.02254486;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 47.625,275.30414 H 19.843753 Z"
|
||||||
|
id="path4596-2-8-4"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/3software/logo/vue.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
30
public/doc/Terms_of_use_en.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Software SURVy (the “Tool”) to facilitate your selection CCTV products based on your needs.
|
||||||
|
Before you start using the Tool, we ask you to carefully read through the below terms of use ("Terms of Use") and make sure that you have understood them prior to any use of the Tool. By downloading, installing, activating, accessing or otherwise using the Tool, you agree to be bound by the terms and conditions of the Terms of Use. If you are executing the Terms of Use on behalf of an entity, you represent that you have authority to legally bind that entity. If you do not have such authority or you do not agree to the terms and conditions of the Terms of Use, neither you nor the entity is permitted to and must not download, install, access or use the Tool.
|
||||||
|
The Tool developer ("Developer") - Individual Entrepreneur/Sole Proprietor Martyshkin Alexey Alexandrovich (Russia, Moscow, PSRNSP/Primary State Registration Number of the Sole Proprietor 318774600262084, ITN/Individual Taxpayer Number 366316608346). All rights to software SURVy belong to Developer.
|
||||||
|
By "Developer Representatives" in this document means to the circle of persons involved by Developer for development and support of the Tool.
|
||||||
|
|
||||||
|
TERMS OF USE
|
||||||
|
The Tool is provided for guidance only. The estimates, recommendations and calculation results (collectively the "Deliverables") produced by the use of the Tool are only orientational.
|
||||||
|
DEVELOPER AND/OR ITS REPRESENTATIVES WILL IN NO EVENT BE RESPONSIBLE FOR DAMAGES OF ANY NATURE WHATSOEVER RESULTING FROM THE USE OF, OR RELIANCE UPON, THE TOOL AND THE DELIVERABLES.
|
||||||
|
The Tool for authorization use HttpOnly cookies-files. Also in local memory of your web browser (known as "local storage") stored language settings.
|
||||||
|
|
||||||
|
Consent to Use of Data
|
||||||
|
Your projects will be stored on Developer servers. By agreeing to these terms of use, you accept that your project data will be used by Developer and/or its representatives for internal purposes (such as subsequent improving the Tool).
|
||||||
|
Rest assured that will Developer and/or its representatives intentionally not share or transfer about your projects to anyone.
|
||||||
|
Developer can use email address specified in the account to send notifications about changes to these Terms, request feedback on the use of the Tool and provide technical support.
|
||||||
|
|
||||||
|
Restrictions
|
||||||
|
You may not (and you may not allow anyone else to):
|
||||||
|
(i) reverse engineer, decompile, disassemble or otherwise attempt to derive access to the source code of the Tool, or any part thereof,
|
||||||
|
(ii) misuse the Tool by interfering with its normal operation, or attempting to access it using a method other than through the interfaces and instructions that provide,
|
||||||
|
(iii) submit or upload any data or content that is illegal or violates these Terms of Use,
|
||||||
|
(iv) use the Tool on the territory of Russian Federation for objects of state-owned enterprises/institutions (including companies with state participation), as well as for those objects whose data can be identified as classified information.
|
||||||
|
The data submitted by you by use of the Tool may not exceed 1 GB.
|
||||||
|
Developer reserves the right to delete your data (account and projects) in its sole discretion.
|
||||||
|
You undertake to indemnify and hold Developer and/or its representatives harmless for all damages and losses incurred due to any breach of the undertakings in this Section.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE TOOL AND ANY DELIVERABLES ARE DELIVERED FREE OF CHARGE AND 'AS IS' WITHOUT WARRANTY OF ANY KIND. THE ENTIRE RISK AS TO THE RESULTS AND PERFORMANCE OF THE TOOL AND THE DELIVERABLES IS ASSUMED BY YOU/THE USER. DEVELOPER DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT, OR ANY WARRANTY ARISING OUT OF ANY PROPOSAL, SPECIFICATION OR SAMPLE WITH RESPECT TO THE TOOL AND DELIVERABLES. DEVELOPER AND/OR ITS REPRESENTATIVES SHALL NOT BE LIABLE FOR LOSS OF DATA, LOSS OF PRODUCTION, LOSS OF PROFIT, LOSS OF USE, LOSS OF CONTRACTS OR FOR ANY OTHER CONSEQUENTIAL, ECONOMIC OR INDIRECT LOSS WHATSOEVER IN RESPECT OF DELIVERY, USE OR DISPOSITION OF THE TOOL AND THE DELIVERABLES. DEVELOPER AND/OR ITS REPRESENTATIVES TOTAL LIABILITY FOR ANY AND ALL CLAIMS, DAMAGES AND LIABILITY IN ACCORDANCE WITH THE DELIVERY AND USE OF THE TOOL AND THE DELIVERABLES SHALL NOT EXCEED THE PRICE PAID FOR THE TOOL.
|
||||||
|
|
||||||
|
Governing law and dispute resolution
|
||||||
|
These Terms of Use shall be deemed performed in and shall be construed and governed by the laws of Russian Federation.
|
||||||
30
public/doc/Terms_of_use_ru.txt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Программное обеспечение SURVy (далее – "Инструмент") разработано, чтобы помочь в выборе оборудования систем видеонаблюдения согласно вашим потребностям.
|
||||||
|
Перед началом использования Инструмента просьба внимательно ознакомиться с нижеуказанными условиями использования (далее – Условия использования) и убедиться, что вы понимаете их до начала использования Инструмента. Выполняя загрузку, установку, активацию Инструмента, осуществляя доступ к нему или иное его использование, вы соглашаетесь принять и соблюдать положения настоящих Условий использования. Если вы выполняете Условия использования от имени юридического лица, вы заявляете, что имеете юридические основания связывать обязательствами такое юридическое лицо. Если у вас нет таких полномочий или вы не согласны с положениями настоящих Условий использования, то ни вам, ни такому юридическому лицу не разрешается и не следует загружать, устанавливать Инструмент, осуществлять доступ к нему или использовать его.
|
||||||
|
Разработчиком Инструмента (далее – "Разработчик") является индивидуальный предприниматель Мартышкин Алексей Александрович (Россия, г. Москва, ОГРНИП 318774600262084, ИНН 366316608346). Все права на программу SURVy принадлежат Разработчику.
|
||||||
|
Под "Представителями разработчика" в данном документе подразумевается круг лиц, привлекаемых Разработчиком в рамках разработки и поддержки Инструмента.
|
||||||
|
|
||||||
|
УСЛОВИЯ ИСПОЛЬЗОВАНИЯ
|
||||||
|
Инструмент предоставляется исключительно для оказания помощи в выборе. Оценки, рекомендации, результаты расчетов (совместно именуемые «Результаты работы»), полученные путем использования Инструмента являются ориентировочными.
|
||||||
|
РАЗРАБОТЧИК И/ИЛИ ЕГО ПРЕДСТАВИТЕЛИ В ЛЮБОМ СЛУЧАЕ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА УБЫТКИ ЛЮБОГО РОДА, ПОНЕСЕННЫЕ В РЕЗУЛЬТАТЕ ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТА И РЕЗУЛЬТАТОВ РАБОТЫ ИЛИ ПРИНЯТИЯ РЕШЕНИЙ НА ИХ ОСНОВЕ.
|
||||||
|
Инструмент для авторизации использует только HttpOnly cookie-файлы. Также в локальной памяти вашего веб-браузера (Локальное хранилище) хранятся языковые настройки.
|
||||||
|
|
||||||
|
Согласие на использование данных
|
||||||
|
Ваши проекты будут храниться на серверах Разработчика. Принимая данные условия использования, вы соглашаетесь с тем, что данные ваших проектов будут доступны Разработчику и/или его представителям и могут использоваться для внутренних целей (например, для последующего улучшения Инструмента).
|
||||||
|
Вы можете быть уверены в том, что Разработчик и его представители не будут преднамеренно передавать кому-либо данные о ваших проектах.
|
||||||
|
Разработчик может использовать адрес электронной почты, указанный в учетной записи для отправки уведомлений об изменении данных Условий, запроса получения обратной связи по пользованию Инструментом и оказания технической поддержки.
|
||||||
|
|
||||||
|
Ограничения
|
||||||
|
Вы не вправе (а также не вправе разрешать кому-либо):
|
||||||
|
(i) осуществлять обратную разработку, декомпиляцию, дизассемблировать Инструмент или иным образом получать доступ к его исходному коду или любой его части;
|
||||||
|
(ii) неправильно использовать Инструмент, нарушая его обычный режим работы или пытаясь использовать его не с помощью тех интерфейсов и инструкций, которые не предоставляются;
|
||||||
|
(iii) отправлять или загружать в Инструмент какие-либо данные или содержание, являющиеся незаконными или нарушающие настоящие Условия использования;
|
||||||
|
(iv) использовать Инструмент на территории Российской Федерации для объектов государственных предприятий/учреждений (в том числе компаний с государственным участием), а также тех объектов, данные по которым могут классифицироваться как секретная информация (любого класса).
|
||||||
|
Общий размер данных, отправляемых вами при использовании Инструмента, не может превышать 1 ГБ.
|
||||||
|
Разработчик оставляет за собой право удалить ваши данные (учетную запись и проекты) по своему усмотрению.
|
||||||
|
Вы обязуетесь защитить и оградить Разработчика и его представителей от каких-либо убытков или потерь, понесенных ими в результате нарушения обязательства в настоящем разделе.
|
||||||
|
|
||||||
|
ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ
|
||||||
|
ДАННЫЙ ИНСТРУМЕНТ И РЕЗУЛЬТАТЫ РАБОТЫ ПОСТАВЛЯЮТСЯ БЕЗВОЗМЕЗДНО «КАК ЕСТЬ» БЕЗ ПРЕДОСТАВЛЕНИЯ КАКИХ-ЛИБО ГАРАНТИЙ. ВСЕ РИСКИ В ОТНОШЕНИИ РЕЗУЛЬТАТОВ РАБОТЫ И РАБОТЫ ИНСТРУМЕНТА ПОЛНОСТЬЮ ЛОЖАТСЯ НА ВАС/ПОЛЬЗОВАТЕЛЯ. В РАМКАХ ПРИМЕНИМОГО ЗАКОНОДАТЕЛЬСТВА РАЗРАБОТЧИК ОТКАЗЫВАЕТСЯ ОТ ВСЕХ ГАРАНТИЙНЫХ ОБЯЗАТЕЛЬСТВ, КАК ЯВНЫХ, ТАК И СКРЫТЫХ, ВКЛЮЧАЯ, В ЧИСЛЕ ПРОЧЕГО, СКРЫТЫЕ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, ПРИГОДНОСТЬ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ, СОБЛЮДЕНИЕ ПРАВ ИЛИ ЛЮБЫЕ ГАРАНТИИ, ВЫТЕКАЮЩИЕ ИЗ ЛЮБОГО ПРЕДЛОЖЕНИЯ, СПЕЦИФИКАЦИИ ИЛИ ОБРАЗЦА В ОТНОШЕНИИ РЕЗУЛЬТАТОВ РАБОТЫ И ИНСТРУМЕНТА. РАЗРАБОЧИК И ЕГО ПРЕДСТАВИТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ПОТЕРЮ ДАННЫХ, ПРОИЗВОДСТВЕННЫЕ ПОТЕРИ, ПОТЕРЮ ПРИБЫЛИ, НЕВОЗМОЖНОСТЬ ИСПОЛЬЗОВАНИЯ, НЕЗАКЛЮЧЕНИЕ ДОГОВОРА ИЛИ ЗА КАКИЕ-ЛИБО ДРУГИЕ КОСВЕННЫЕ, ЭКОНОМИЧЕСКИЕ ИЛИ НЕПРЯМЫЕ ПОТЕРИ В РЕЗУЛЬТАТЕ ПОЛУЧЕНИЯ, ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТА ИЛИ РЕЗУЛЬТАТОВ РАБОТЫ ИЛИ РАСПОРЯЖЕНИЯ ИМИ. ОБЩАЯ ОТВЕТСТВЕННОСТЬ РАЗРАБОТЧИКА И ЕГО ПРЕДСТАВИТЕЛЕЙ ПО ЛЮБЫМ ТРЕБОВАНИЯМ, УБЫТКАМИ ИЛИ ВИДАМ ОТВЕТСТВЕННОСТИ ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ И ПОЛУЧЕНИЯ ИНСТРУМЕНТА И РЕЗУЛЬТАТОВ РАБОТЫ НЕ ПРЕВЫШАЕТ ЦЕНЫ, УПЛАЧЕННОЙ ЗА ИНСТРУМЕНТ.
|
||||||
|
|
||||||
|
Регулирующее право и разрешение споров
|
||||||
|
Настоящие Условия использования должны считаться выполненными, подлежат толкованию и регулируются согласно законодательству Российской Федерации.
|
||||||
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -14,10 +14,10 @@ export default defineConfig((ctx) => {
|
|||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
'telegram-boot',
|
|
||||||
'i18n',
|
|
||||||
'axios',
|
|
||||||
'auth-init',
|
'auth-init',
|
||||||
|
'i18n',
|
||||||
|
'telegram-boot',
|
||||||
|
'axios',
|
||||||
'global-components'
|
'global-components'
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -42,6 +42,8 @@ export default defineConfig((ctx) => {
|
|||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||||
build: {
|
build: {
|
||||||
|
publicPath: '/admin',
|
||||||
|
vueRouterBase: '/admin',
|
||||||
target: {
|
target: {
|
||||||
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
|
browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ],
|
||||||
node: 'node20'
|
node: 'node20'
|
||||||
@@ -55,7 +57,8 @@ export default defineConfig((ctx) => {
|
|||||||
|
|
||||||
alias: {
|
alias: {
|
||||||
'composables': path.resolve(__dirname, './src/composables'),
|
'composables': path.resolve(__dirname, './src/composables'),
|
||||||
'types': path.resolve(__dirname, './src/types')
|
'types': path.resolve(__dirname, './src/types'),
|
||||||
|
'helpers': path.resolve(__dirname, './src/helpers')
|
||||||
},
|
},
|
||||||
|
|
||||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
@@ -111,9 +114,17 @@ export default defineConfig((ctx) => {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://localhost:3000',
|
||||||
changeOrigin: true
|
changeOrigin: true,
|
||||||
}
|
ws: true
|
||||||
|
},
|
||||||
|
/* '/ws': {
|
||||||
|
target: 'ws://localhost:3000', // или wss:// для HTTPS
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true, // Ключевая опция для WebSocket
|
||||||
|
rewrite: (path) => path.replace(/^\/ws/, ''), // Опционально
|
||||||
|
} */
|
||||||
},
|
},
|
||||||
|
|
||||||
// https: true,
|
// https: true,
|
||||||
// open: true // opens browser window automatically
|
// open: true // opens browser window automatically
|
||||||
},
|
},
|
||||||
|
|||||||
33
src/App.vue
@@ -3,35 +3,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
import { useI18n } from 'vue-i18n'
|
const settingsStore = useSettingsStore()
|
||||||
const { locale } = useI18n()
|
|
||||||
|
|
||||||
const tg = inject('tg') as WebApp
|
onMounted(async () => {
|
||||||
|
await settingsStore.init()
|
||||||
const getLocale = (): string => {
|
|
||||||
const localeMap = {
|
|
||||||
ru: 'ru-RU',
|
|
||||||
en: 'en-US'
|
|
||||||
} as const satisfies Record<string, string>
|
|
||||||
|
|
||||||
type LocaleCode = keyof typeof localeMap
|
|
||||||
|
|
||||||
const normLocale = (locale?: string): string | undefined => {
|
|
||||||
if (!locale) return undefined
|
|
||||||
const code = locale.split('-')[0] as LocaleCode
|
|
||||||
return localeMap[code] ?? undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const tgLang = tg?.initDataUnsafe?.user?.language_code
|
|
||||||
const normalizedTgLang = normLocale(tgLang)
|
|
||||||
|
|
||||||
return normalizedTgLang ?? normLocale(navigator.language) ?? 'en-US'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
locale.value = getLocale()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import { useAuthStore } from 'stores/auth'
|
|||||||
export default async () => {
|
export default async () => {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
await authStore.initialize()
|
await authStore.initialize()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,29 @@ import pnPageCard from 'components/pnPageCard.vue'
|
|||||||
import pnScrollList from 'components/pnScrollList.vue'
|
import pnScrollList from 'components/pnScrollList.vue'
|
||||||
import pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
import pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
||||||
import pnOverlay from 'components/pnOverlay.vue'
|
import pnOverlay from 'components/pnOverlay.vue'
|
||||||
import pnMagicOverlay from 'components/pnMagicOverlay.vue'
|
import pnSmallDialog from 'components/pnSmallDialog.vue'
|
||||||
import pnImageSelector from 'components/pnImageSelector.vue'
|
import pnImageSelector from 'components/pnImageSelector.vue'
|
||||||
|
import pnShadowScroll from 'components/pnShadowScroll.vue'
|
||||||
|
import pnMagicOverlay from 'components/pnMagicOverlay.vue'
|
||||||
import pnAccountBlockName from 'components/pnAccountBlockName.vue'
|
import pnAccountBlockName from 'components/pnAccountBlockName.vue'
|
||||||
|
import pnOnboardBtn from 'components/pnOnboardBtn.vue'
|
||||||
|
|
||||||
export default boot(async ({ app }) => { // eslint-disable-line
|
const components = {
|
||||||
app.component('pnPageCard', pnPageCard)
|
pnPageCard,
|
||||||
app.component('pnScrollList', pnScrollList)
|
pnScrollList,
|
||||||
app.component('pnAutoAvatar', pnAutoAvatar)
|
pnAutoAvatar,
|
||||||
app.component('pnOverlay', pnOverlay)
|
pnOverlay,
|
||||||
app.component('pnMagicOverlay', pnMagicOverlay)
|
pnImageSelector,
|
||||||
app.component('pnImageSelector', pnImageSelector)
|
pnSmallDialog,
|
||||||
app.component('pnAccountBlockName', pnAccountBlockName)
|
pnShadowScroll,
|
||||||
|
pnMagicOverlay,
|
||||||
|
pnAccountBlockName,
|
||||||
|
pnOnboardBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
export default boot(({ app }) => {
|
||||||
|
Object.entries(components).forEach(([name, component]) => {
|
||||||
|
app.component(name, component)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
function isDirty (
|
|
||||||
obj1: Record<string, unknown> | null | undefined,
|
|
||||||
obj2: Record<string, unknown> | null | undefined
|
|
||||||
): boolean {
|
|
||||||
const actualObj1 = obj1 ?? {}
|
|
||||||
const actualObj2 = obj2 ?? {}
|
|
||||||
|
|
||||||
const filteredObj1 = filterIgnored(actualObj1)
|
|
||||||
const filteredObj2 = filterIgnored(actualObj2)
|
|
||||||
|
|
||||||
const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)])
|
|
||||||
|
|
||||||
for (const key of allKeys) {
|
|
||||||
const hasKey1 = Object.hasOwn(filteredObj1, key)
|
|
||||||
const hasKey2 = Object.hasOwn(filteredObj2, key)
|
|
||||||
|
|
||||||
if (hasKey1 !== hasKey2) return false
|
|
||||||
|
|
||||||
if (hasKey1 && hasKey2) {
|
|
||||||
const val1 = filteredObj1[key]
|
|
||||||
const val2 = filteredObj2[key]
|
|
||||||
|
|
||||||
if (typeof val1 === 'string' && typeof val2 === 'string') {
|
|
||||||
if (val1.trim() !== val2.trim()) return false
|
|
||||||
} else if (val1 !== val2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterIgnored(obj: Record<string, unknown>): Record<string, string | number | boolean> {
|
|
||||||
const filtered: Record<string, string | number | boolean> = {}
|
|
||||||
|
|
||||||
for (const key in obj) {
|
|
||||||
const originalValue = obj[key]
|
|
||||||
|
|
||||||
// Пропускаем значения, которые не string, number или boolean
|
|
||||||
if (
|
|
||||||
typeof originalValue !== 'string' &&
|
|
||||||
typeof originalValue !== 'number' &&
|
|
||||||
typeof originalValue !== 'boolean'
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = originalValue
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = value.trim()
|
|
||||||
if (value === '') continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 0 || value === false) continue
|
|
||||||
|
|
||||||
filtered[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseIntString (s: string | string[] | undefined) :number | null {
|
|
||||||
if (typeof s !== 'string') return null
|
|
||||||
const regex = /^[+-]?\d+$/
|
|
||||||
return regex.test(s) ? Number(s) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
isDirty,
|
|
||||||
parseIntString
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,13 @@
|
|||||||
import { defineBoot } from '#q-app/wrappers'
|
import { defineBoot } from '#q-app/wrappers'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import messages from 'src/i18n'
|
import messages from 'src/i18n'
|
||||||
|
|
||||||
export type MessageLanguages = keyof typeof messages
|
export const i18n = createI18n({
|
||||||
// Type-define 'en-US' as the master schema for the resource
|
legacy: false,
|
||||||
export type MessageSchema = typeof messages['en-US']
|
locale: 'en-US',
|
||||||
|
messages
|
||||||
// See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition
|
})
|
||||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
||||||
declare module 'vue-i18n' {
|
|
||||||
// define the locale messages schema
|
|
||||||
export interface DefineLocaleMessage extends MessageSchema {}
|
|
||||||
|
|
||||||
// define the datetime format schema
|
|
||||||
export interface DefineDateTimeFormat {}
|
|
||||||
|
|
||||||
// define the number format schema
|
|
||||||
export interface DefineNumberFormat {}
|
|
||||||
}
|
|
||||||
/* eslint-enable @typescript-eslint/no-empty-object-type */
|
|
||||||
|
|
||||||
export default defineBoot(({ app }) => {
|
export default defineBoot(({ app }) => {
|
||||||
const i18n = createI18n<{ message: MessageSchema }, MessageLanguages>({
|
|
||||||
locale: 'en-US',
|
|
||||||
legacy: false,
|
|
||||||
messages,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set i18n instance on app
|
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import type { BootParams } from '@quasar/app'
|
import { defineBoot } from '#q-app/wrappers'
|
||||||
|
import type { WebApp } from "@twa-dev/types"
|
||||||
|
|
||||||
export default ({ app }: BootParams) => {
|
declare global {
|
||||||
|
interface Window {
|
||||||
// Инициализация Telegram WebApp
|
Telegram: {
|
||||||
|
WebApp: WebApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineBoot(({ app }) => {
|
||||||
if (window.Telegram?.WebApp) {
|
if (window.Telegram?.WebApp) {
|
||||||
const webApp = window.Telegram.WebApp
|
const webApp = window.Telegram.WebApp
|
||||||
// Помечаем приложение как готовое
|
|
||||||
webApp.ready()
|
webApp.ready()
|
||||||
// window.Telegram.WebApp.requestFullscreen()
|
|
||||||
// Опционально: сохраняем объект в Vue-приложение для глобального доступа
|
|
||||||
// webApp.SettingsButton.isVisible = true
|
|
||||||
// webApp.BackButton.isVisible = true
|
|
||||||
app.config.globalProperties.$tg = webApp
|
app.config.globalProperties.$tg = webApp
|
||||||
// Для TypeScript: объявляем тип для инжекции
|
|
||||||
app.provide('tg', webApp)
|
app.provide('tg', webApp)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
no-error-icon
|
no-error-icon
|
||||||
@focus="($refs.emailInput as typeof QInput)?.resetValidation()"
|
@focus="($refs.emailInput as typeof QInput)?.resetValidation()"
|
||||||
ref="emailInput"
|
ref="emailInput"
|
||||||
|
:disable="type === 'changePwd'"
|
||||||
/>
|
/>
|
||||||
<q-stepper-navigation>
|
<q-stepper-navigation>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('continue')"
|
:label="$t('continue')"
|
||||||
|
:disable="code.length === 0"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
@@ -73,6 +75,7 @@
|
|||||||
v-model="password"
|
v-model="password"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
|
autofocus
|
||||||
:label = "$t('account_helper__password')"
|
:label = "$t('account_helper__password')"
|
||||||
:type="isPwd ? 'password' : 'text'"
|
:type="isPwd ? 'password' : 'text'"
|
||||||
hide-hint
|
hide-hint
|
||||||
@@ -114,15 +117,14 @@
|
|||||||
<pn-magic-overlay
|
<pn-magic-overlay
|
||||||
v-if="showSuccessOverlay"
|
v-if="showSuccessOverlay"
|
||||||
icon="mdi-check-circle-outline"
|
icon="mdi-check-circle-outline"
|
||||||
message1="account_helper__ok_message1"
|
:message1="getHelperMessage1()"
|
||||||
message2="account_helper__ok_message2"
|
:message2="getHelperMessage2()"
|
||||||
route-name="projects"
|
route-name="projects"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { AxiosError } from 'axios'
|
import type { AxiosError } from 'axios'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
@@ -134,7 +136,9 @@
|
|||||||
? 'register'
|
? 'register'
|
||||||
: props.type === 'forgotPwd'
|
: props.type === 'forgotPwd'
|
||||||
? 'forgot'
|
? 'forgot'
|
||||||
: 'change'
|
: props.type === 'changePwd'
|
||||||
|
? 'changePwd'
|
||||||
|
: 'changeMethod'
|
||||||
})
|
})
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
@@ -142,7 +146,7 @@
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: 'register' | 'forgotPwd' | 'changePwd'
|
type: 'register' | 'forgotPwd' | 'changePwd' | 'changeMethod'
|
||||||
email?: string
|
email?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -182,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
3: async () => {
|
3: async () => {
|
||||||
await authStore.setPassword(flowType.value, login.value, code.value, password.value)
|
await authStore.setPassword(flowType.value, login.value, code.value, password.value)
|
||||||
if (flowType.value === 'register') {
|
if (flowType.value === 'register' || flowType.value === 'changeMethod') {
|
||||||
await authStore.loginWithCredentials(login.value, password.value)
|
await authStore.loginWithCredentials(login.value, password.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +221,28 @@
|
|||||||
handleError(error as AxiosError)
|
handleError(error as AxiosError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getHelperMessage1 = () => {
|
||||||
|
switch (flowType.value) {
|
||||||
|
case 'register': return 'account_helper__register_message1'
|
||||||
|
case 'forgot': return 'account_helper__forgot_password_message1'
|
||||||
|
case 'changePwd': return 'account_helper__change_password_message1'
|
||||||
|
case 'changeMethod': return 'account_helper__change_method_message1'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHelperMessage2 = () => {
|
||||||
|
switch (flowType.value) {
|
||||||
|
case 'register': return 'slogan'
|
||||||
|
case 'forgot':
|
||||||
|
case 'changePwd':
|
||||||
|
case 'changeMethod':
|
||||||
|
return 'account_helper__go_projects'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
128
src/components/companyBlock.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded color="primary"
|
||||||
|
class="w100 q-mt-md q-mb-xs"
|
||||||
|
:disable="!(isFormValid && (isDirty(initialCompany, modelValue)))"
|
||||||
|
@click = "emit('update')"
|
||||||
|
>
|
||||||
|
{{ $t(btnText) }}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||||
|
<slot name="myCompany"/>
|
||||||
|
<pn-image-selector
|
||||||
|
v-model="modelValue.logo"
|
||||||
|
:size="100"
|
||||||
|
:iconsize="80"
|
||||||
|
class="q-pb-lg"
|
||||||
|
/>
|
||||||
|
<div class="q-gutter-y-lg w100">
|
||||||
|
<q-input
|
||||||
|
v-for="input in textInputs"
|
||||||
|
:key="input.id"
|
||||||
|
v-model.trim="modelValue[input.val]"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100"
|
||||||
|
:autogrow="input.val === 'description' || input.val === 'address'"
|
||||||
|
:class="input.val === 'name'
|
||||||
|
? 'fix-bottom-padding'
|
||||||
|
: input.val === 'address'
|
||||||
|
? 'input-fix q-pt-sm'
|
||||||
|
: 'q-pt-sm'"
|
||||||
|
:label="input.label ? $t(input.label) : void 0"
|
||||||
|
:rules="input.val === 'name' ? [rules[input.val]] : []"
|
||||||
|
no-error-icon
|
||||||
|
:label-slot="Boolean(input.label)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||||
|
</template>
|
||||||
|
<template #label v-if="input.label">
|
||||||
|
{{$t(input.label) }}
|
||||||
|
<span v-if="input.val === 'name'" class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onMounted, computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { isDirty } from 'helpers/helpers'
|
||||||
|
import type { CompanyParams } from 'types/Company'
|
||||||
|
|
||||||
|
const { t }= useI18n()
|
||||||
|
|
||||||
|
const modelValue = defineModel<CompanyParams>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string,
|
||||||
|
btnText: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
interface TextInput {
|
||||||
|
id: number
|
||||||
|
label?: string
|
||||||
|
icon?: string
|
||||||
|
val: keyof CompanyParams
|
||||||
|
rules: ((value: string) => boolean | string)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const textInputs: TextInput[] = [
|
||||||
|
{ id: 1, val: 'name', label: 'company_block__name', rules: [] },
|
||||||
|
{ id: 2, val: 'description', label: 'company_block__description', rules: [] },
|
||||||
|
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] },
|
||||||
|
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
|
||||||
|
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
|
||||||
|
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] }
|
||||||
|
]
|
||||||
|
|
||||||
|
const rulesErrorMessage = {
|
||||||
|
name: t('company_block__error_name')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: (val: CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
const validations = {
|
||||||
|
name: rules.name(modelValue.value.name) === true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(validations).every(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialCompany = ref({} as CompanyParams)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(111, modelValue.value)
|
||||||
|
initialCompany.value = { ...modelValue.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.q-field--with-bottom.fix-bottom-padding {
|
||||||
|
padding-bottom: 0 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-fix :deep(.q-field__prepend.q-field__marginal) {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex column items-center col-grow q-px-lg q-pt-sm">
|
|
||||||
<pn-image-selector :size="100" :iconsize="80" class="q-pb-xs" v-model="modelValue.logo"/>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-for="input in textInputs"
|
|
||||||
:key="input.id"
|
|
||||||
v-model.trim="modelValue[input.val]"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class = "q-mt-md w100"
|
|
||||||
:label = "input.label ? $t(input.label) : void 0"
|
|
||||||
:rules="input.val === 'name' ? [rules[input.val]] : []"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { CompanyParams } from 'src/types'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
const { t }= useI18n()
|
|
||||||
|
|
||||||
const modelValue = defineModel<CompanyParams>({
|
|
||||||
required: false,
|
|
||||||
default: () => ({
|
|
||||||
name: '',
|
|
||||||
logo: '',
|
|
||||||
description: '',
|
|
||||||
site: '',
|
|
||||||
address: '',
|
|
||||||
phone: '',
|
|
||||||
email: ''
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['valid'])
|
|
||||||
const rulesErrorMessage = {
|
|
||||||
name: t('company_card__error_name')
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: (val :CompanyParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = computed(() => {
|
|
||||||
const checkName = rules.name(modelValue.value.name)
|
|
||||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isValid, (newVal) => {
|
|
||||||
const allValid = Object.values(newVal).every(v => v)
|
|
||||||
emit('valid', allValid)
|
|
||||||
}, { immediate: true})
|
|
||||||
|
|
||||||
interface TextInput {
|
|
||||||
id: number
|
|
||||||
label?: string
|
|
||||||
icon?: string
|
|
||||||
val: keyof CompanyParams
|
|
||||||
rules: ((value: string) => boolean | string)[]
|
|
||||||
}
|
|
||||||
const textInputs: TextInput[] = [
|
|
||||||
{ id: 1, val: 'name', label: 'company_info__name', rules: [] },
|
|
||||||
{ id: 2, val: 'description', label: 'company_info__description', rules: [] },
|
|
||||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [] },
|
|
||||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [] },
|
|
||||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [] },
|
|
||||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [] },
|
|
||||||
]
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pt-md">
|
<div class="q-pt-md" v-if="mapUsers.length !==0 ">
|
||||||
<span class="q-pl-md text-h6">
|
<span class="q-pl-md text-h6">
|
||||||
{{ $t('company_info__persons') }}
|
{{ $t('company_info__users') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in persons"
|
v-for="item in mapUsers"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
v-ripple
|
v-ripple
|
||||||
clickable
|
clickable
|
||||||
@click="goPersonInfo()"
|
@click="goPersonInfo(item.id)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar>
|
<pn-auto-avatar
|
||||||
<img v-if="item.logo" :src="item.logo"/>
|
:img="item.photo"
|
||||||
<pn-auto-avatar v-else :name="item.name"/>
|
:name="item.section1"
|
||||||
</q-avatar>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label lines="1" class="text-bold">
|
<q-item-label lines="1" class="text-bold" v-if="item.section1">
|
||||||
{{item.name}}
|
{{item.section1}}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption lines="2">
|
<q-item-label caption lines="2">
|
||||||
<span>{{item.tname}}</span>
|
<span v-if="item.section2_1" class="q-mr-sm">{{item.section2_1}}</span>
|
||||||
<span class="text-blue q-ml-sm">{{item.tusername}}</span>
|
<span class="text-blue" v-if="item.section2_2">{{'@' + item.section2_2}}</span>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="1">
|
<q-item-label lines="1" v-if="item.section3">
|
||||||
{{item.role}}
|
{{item.section3}}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -36,20 +36,59 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import type { User } from 'types/Users'
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const router = useRouter()
|
const currentCompanyId = Number(route.params.companyId)
|
||||||
|
|
||||||
|
const users = usersStore.users
|
||||||
|
|
||||||
const persons = [
|
const mapUsers = users
|
||||||
{id: "p1", name: 'Кирюшкин Андрей', logo: 'https://cdn.quasar.dev/img/avatar4.jpg', tname: 'Kir_AA', tusername: '@kiruha90', role: 'DevOps' },
|
.filter(el => el.company_id === currentCompanyId)
|
||||||
{id: "p2", name: 'Пупкин Василий Александрович', logo: '', tname: 'Pupkin', tusername: '@super_pupkin', role: 'Руководитель проекта' },
|
.map(el => ({...el, ...userSection(el)}))
|
||||||
{id: "p3", name: 'Макарова Полина', logo: 'https://cdn.quasar.dev/img/avatar6.jpg', tname: 'Unikorn', tusername: '@unicorn_stars', role: 'Администратор' },
|
|
||||||
{id: "p4", name: 'Жабов Максим', logo: '', tname: 'Zhaba', tusername: '@Zhabchenko', role: 'Аналитик' },
|
|
||||||
]
|
|
||||||
|
|
||||||
async function goPersonInfo () {
|
async function goPersonInfo (userId: number) {
|
||||||
console.log('update')
|
await router.push({ name: 'user_info', params: { id: route.params.id, userId }})
|
||||||
await router.push({ name: 'person_info' })
|
}
|
||||||
|
|
||||||
|
// copy from 'pages/project-page/ProjectPageUsers.vue' кроме company.name
|
||||||
|
function userSection (user: User) {
|
||||||
|
const tname = () => {
|
||||||
|
return user.firstname
|
||||||
|
? user.lastname
|
||||||
|
? user.firstname + ' ' + user.lastname
|
||||||
|
: user.firstname
|
||||||
|
: user.lastname ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const section1 = user.name
|
||||||
|
? user.name
|
||||||
|
: tname()
|
||||||
|
|
||||||
|
const section2_1 = user.name
|
||||||
|
? tname()
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const section2_2 = user.username ?? ''
|
||||||
|
|
||||||
|
const section3 = (
|
||||||
|
user.department
|
||||||
|
? user.department + ' '
|
||||||
|
: ''
|
||||||
|
) + (
|
||||||
|
user.role ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
section1,
|
||||||
|
section2_1, section2_2,
|
||||||
|
section3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
75
src/components/docBlock.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{ $t(type + '__title') }}
|
||||||
|
</template>
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="q-px-md">
|
||||||
|
<div
|
||||||
|
v-if="fileText"
|
||||||
|
style="white-space: pre-wrap;"
|
||||||
|
>
|
||||||
|
{{ fileText }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
align="center"
|
||||||
|
class="text-negative"
|
||||||
|
>
|
||||||
|
{{ $t(type + '__not_ready') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
<div
|
||||||
|
class="flex column justify-center items-center w100"
|
||||||
|
style="position: absolute; bottom: 0;"
|
||||||
|
v-if="isLoading"
|
||||||
|
>
|
||||||
|
<q-linear-progress indeterminate />
|
||||||
|
</div>
|
||||||
|
</pn-page-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useSettingsStore } from 'stores/settings'
|
||||||
|
|
||||||
|
const fileText = ref<string>('')
|
||||||
|
const isLoading = ref<boolean>(true)
|
||||||
|
const error = ref<boolean>(false)
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const lang = ref<string>('EN')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: 'terms_of_use' | 'privacy'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function parseLocale(locale: string): string {
|
||||||
|
return locale.split(/[-_]/)[0] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDocName =
|
||||||
|
props.type ==='terms_of_use' ? 'Terms_of_use' : 'Privacy'
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const locale = settingsStore.settings.locale
|
||||||
|
lang.value = parseLocale(locale)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/doc/' + baseDocName + '_' + lang.value +'.txt')
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileText.value = await response.text()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('File load error:', err)
|
||||||
|
error.value = true
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center no-wrap overflow-hidden w100">
|
<div class="flex items-center no-wrap overflow-hidden w100">
|
||||||
<q-icon v-if="user?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
<q-icon v-if="customer?.email" size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
||||||
<q-avatar v-else size="32px" class="q-mr-sm">
|
<q-avatar v-else size="32px" class="q-mr-sm">
|
||||||
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
||||||
<q-icon v-else size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
<q-icon v-else size="md" class="q-mr-sm" name="mdi-account-circle-outline"/>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
<span v-if="user?.email" class="ellipsis">
|
<span v-if="customer?.email" class="ellipsis">
|
||||||
{{ user.email }}
|
{{ customer.email }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="ellipsis">
|
<span v-else class="ellipsis">
|
||||||
{{
|
{{
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const user = authStore.user
|
const customer = authStore.customer
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
const tgUser = tg.initDataUnsafe.user
|
const tgUser = tg.initDataUnsafe.user
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<q-avatar
|
||||||
:style="{ backgroundColor: stringToColour(props.name) } "
|
:square="type==='square'"
|
||||||
class="fit flex items-center justify-center text-white"
|
:rounded="type==='rounded'"
|
||||||
>
|
:size="size"
|
||||||
{{ props.name.substring(0, 1) }}
|
>
|
||||||
</div>
|
<img
|
||||||
|
v-if="img"
|
||||||
|
:src="img"
|
||||||
|
style=" object-fit: cover;"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:style="{ backgroundColor: stringToColour(name) } "
|
||||||
|
class="fit flex items-center justify-center text-white"
|
||||||
|
>
|
||||||
|
{{ name ? name.substring(0, 1) : '-' }}
|
||||||
|
</div>
|
||||||
|
</q-avatar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
const props = defineProps<{
|
interface Props {
|
||||||
name: string
|
img?: string | null
|
||||||
}>()
|
name: string,
|
||||||
|
size?: string,
|
||||||
|
type?: 'rounded' | 'square' | ''
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
img: null,
|
||||||
|
name: '-',
|
||||||
|
size: 'md',
|
||||||
|
type: ''
|
||||||
|
})
|
||||||
|
|
||||||
const stringToColour = (str: string) => {
|
const stringToColour = (str: string) => {
|
||||||
|
if (!str) return '#eee'
|
||||||
let hash = 0
|
let hash = 0
|
||||||
str.split('').forEach(char => {
|
str.split('').forEach(char => {
|
||||||
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||||
@@ -30,3 +53,40 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <template>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: stringToColour(props.name) } "
|
||||||
|
class="fit flex items-center justify-center text-white"
|
||||||
|
>
|
||||||
|
{{ props.name ? props.name.substring(0, 1) : '' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const stringToColour = (str: string) => {
|
||||||
|
if (!str) return '#eee'
|
||||||
|
let hash = 0
|
||||||
|
str.split('').forEach(char => {
|
||||||
|
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
||||||
|
})
|
||||||
|
let colour = '#'
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff
|
||||||
|
colour += value.toString(16).padStart(2, '0')
|
||||||
|
}
|
||||||
|
return colour
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style> -->
|
||||||
|
|||||||
@@ -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'}"
|
||||||
|
|||||||
@@ -4,18 +4,36 @@
|
|||||||
maximized
|
maximized
|
||||||
persistent
|
persistent
|
||||||
transition-show="slide-up"
|
transition-show="slide-up"
|
||||||
transition-hide="slide-down">
|
transition-hide="slide-down"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center fullscrean-card column"
|
class="flex items-center justify-center fullscrean-card column"
|
||||||
>
|
>
|
||||||
<q-icon :name = "icon" color="brand" size="160px"/>
|
<div
|
||||||
<div class="text-h5 q-mb-lg">
|
id="icon-wrapper"
|
||||||
|
:style="{ position: 'relative', height: size + 'px', width: size + 'px'}"
|
||||||
|
>
|
||||||
|
<div class="animation icon-position"></div>
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated zoomIn slow"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="showIcon"
|
||||||
|
:name = "icon"
|
||||||
|
color="brand"
|
||||||
|
size="160px"
|
||||||
|
class="icon-position"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<div class="text-h5 q-mb-lg q-mx-md" align="center">
|
||||||
{{ $t(message1) }}
|
{{ $t(message1) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="message2"
|
v-if="message2"
|
||||||
class="absolute-bottom q-py-lg flex justify-center row"
|
class="absolute-bottom q-py-lg q-mx-md" align="center"
|
||||||
>
|
>
|
||||||
{{ $t(message2) }}
|
{{ $t(message2) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -35,30 +53,35 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
const showIcon = ref(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const timers = ref<number[]>([])
|
const timers = ref<number[]>([])
|
||||||
|
const size = 160
|
||||||
|
|
||||||
const setupTimers = () => {
|
const setupTimers = () => {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
|
|
||||||
const timer1 = window.setTimeout(() => {
|
const timer1 = window.setTimeout(() => {
|
||||||
visible.value = false
|
showIcon.value = true
|
||||||
|
}, 300)
|
||||||
const timer2 = window.setTimeout(() => {
|
|
||||||
router.push({ name: props.routeName })
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
timers.value.push(timer2)
|
|
||||||
}, 2000)
|
|
||||||
|
|
||||||
timers.value.push(timer1)
|
timers.value.push(timer1)
|
||||||
};
|
const timer2 = window.setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
showIcon.value = false
|
||||||
|
}, 2000)
|
||||||
|
timers.value.push(timer2)
|
||||||
|
}
|
||||||
|
|
||||||
const clearTimers = () => {
|
const clearTimers = () => {
|
||||||
timers.value.forEach(timer => clearTimeout(timer))
|
timers.value.forEach(timer => clearTimeout(timer))
|
||||||
timers.value = []
|
timers.value = []
|
||||||
|
visible.value = false
|
||||||
|
showIcon.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(visible, async (newVal) => {
|
||||||
|
if (newVal === false) await router.push({ name: props.routeName })
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(setupTimers)
|
onMounted(setupTimers)
|
||||||
onUnmounted(clearTimers)
|
onUnmounted(clearTimers)
|
||||||
</script>
|
</script>
|
||||||
@@ -68,6 +91,32 @@ onUnmounted(clearTimers)
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property --percentage {
|
||||||
|
initial-value: 0%;
|
||||||
|
inherits: false;
|
||||||
|
syntax: "<percentage>";
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation {
|
||||||
|
background: conic-gradient(transparent var(--percentage), white 0);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: timer 1s linear;
|
||||||
|
animation-fill-mode:forwards;
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timer {
|
||||||
|
to {
|
||||||
|
--percentage: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-position {
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
49
src/components/pnOnboardBtn.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex w100 column q-pt-xl q-pa-md">
|
||||||
|
<div class="flex column justify-center col-grow items-center text-grey">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
@click="handleClick"
|
||||||
|
v-if="!noBtn"
|
||||||
|
>
|
||||||
|
<div class="flex column justify-center col-grow items-center">
|
||||||
|
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||||
|
<div class="text-h6 text-brand">
|
||||||
|
{{message1}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
<div v-else>
|
||||||
|
<div class="flex column justify-center col-grow items-center">
|
||||||
|
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||||
|
<div class="text-h6 text-brand">
|
||||||
|
{{message1}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message2" class="text-caption" align="center">
|
||||||
|
{{message2}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
icon: string
|
||||||
|
message1: string
|
||||||
|
message2?: string
|
||||||
|
noBtn?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['btn-click'])
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
emit('btn-click')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page class="column items-center no-scroll">
|
<div
|
||||||
|
class="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100"
|
||||||
<div
|
style="min-height: 48px"
|
||||||
class="text-white flex items-center w100 q-pl-md q-pr-sm q-ma-none text-h6 no-scroll"
|
>
|
||||||
style="min-height: 48px"
|
<slot name="title"/>
|
||||||
>
|
</div>
|
||||||
<slot name="title"/>
|
<slot/>
|
||||||
</div>
|
<div class="bg-white w100 q-ma-none q-px-md">
|
||||||
<slot/>
|
|
||||||
<slot name="footer"/>
|
<slot name="footer"/>
|
||||||
</q-page>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.glass-card {
|
|
||||||
opacity: 1 !important;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,132 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="card-body" class="w100 col-grow flex column" style="position: relative">
|
<div
|
||||||
<div
|
id="page-card"
|
||||||
class="glass-card fit top-rounded-card flex column"
|
class="w100 flex column glass-card top-rounded-card no-scroll no-wrap"
|
||||||
style="position: absolute; top: 0; left: 0"
|
>
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
id="card-body-header"
|
id="card-body-header"
|
||||||
style="min-height: var(--top-raduis);"
|
style="flex-shrink: 0; min-height: var(--top-raduis);"
|
||||||
>
|
>
|
||||||
|
<q-resize-observer @resize="onHeaderResize"/>
|
||||||
<slot name="card-body-header"/>
|
<slot name="card-body-header"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="fit flex column col-grow">
|
<div id="card-body" >
|
||||||
<q-resize-observer @resize="onResize" />
|
<q-resize-observer @resize="onBodyResize"/>
|
||||||
<div id="card-scroll-area" class="noscroll">
|
<pn-shadow-scroll :hideShadows="isResizing" :height="scrollAreaHeight">
|
||||||
|
<slot/>
|
||||||
<q-scroll-area
|
</pn-shadow-scroll>
|
||||||
ref="scrollArea"
|
|
||||||
:style="{height: heightCard+'px'}"
|
|
||||||
class="w100 q-pa-none q-ma-none"
|
|
||||||
id="scroll-area"
|
|
||||||
@scroll="onScroll"
|
|
||||||
:class="{
|
|
||||||
'shadow-top': hasScrolled,
|
|
||||||
'shadow-bottom': hasScrolledBottom
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot/>
|
|
||||||
<div class="q-pa-sm"/>
|
|
||||||
</q-scroll-area>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import type { QScrollArea } from 'quasar'
|
|
||||||
const heightCard = ref(100)
|
|
||||||
const hasScrolled = ref(false)
|
|
||||||
const hasScrolledBottom = ref(false)
|
|
||||||
|
|
||||||
interface sizeParams {
|
const heightCard = ref(100)
|
||||||
height: number,
|
const scrollAreaHeight = ref(100)
|
||||||
width: number
|
const headerHeight = ref(0)
|
||||||
}
|
|
||||||
|
interface sizeParams {
|
||||||
interface ScrollInfo {
|
height: number,
|
||||||
verticalPosition: number;
|
width: number
|
||||||
verticalPercentage: number;
|
}
|
||||||
verticalSize: number;
|
|
||||||
verticalContainerSize: number;
|
async function onHeaderResize(size: sizeParams) {
|
||||||
horizontalPosition: number;
|
headerHeight.value = size.height
|
||||||
horizontalPercentage: number;
|
await updateScrollAreaHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollArea = ref<InstanceType<typeof QScrollArea> | null>(null)
|
async function onBodyResize(size: sizeParams) {
|
||||||
|
heightCard.value = size.height
|
||||||
function onResize (size :sizeParams) {
|
await updateScrollAreaHeight()
|
||||||
heightCard.value = size.height
|
}
|
||||||
}
|
|
||||||
|
async function updateScrollAreaHeight() {
|
||||||
function onScroll (info: ScrollInfo) {
|
await nextTick(() => {
|
||||||
hasScrolled.value = info.verticalPosition > 0
|
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
||||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
})
|
||||||
hasScrolledBottom.value = !scrollEnd
|
}
|
||||||
|
|
||||||
|
watch(heightCard, updateScrollAreaHeight)
|
||||||
|
watch(headerHeight, updateScrollAreaHeight)
|
||||||
|
|
||||||
|
const isResizing = ref(false)
|
||||||
|
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
watch(heightCard, () => {
|
||||||
|
isResizing.value = true
|
||||||
|
|
||||||
|
if (resizeTimer) {
|
||||||
|
clearTimeout(resizeTimer)
|
||||||
|
resizeTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
isResizing.value = false
|
||||||
|
resizeTimer = null
|
||||||
|
}, 150)
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
#scroll-area div > .q-scrollarea__content {
|
.glass-card {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-card {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#card-body {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#card-body :deep(.q-scrollarea__content) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.q-scrollarea {
|
|
||||||
position: relative;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
background: linear-gradient(to bottom,
|
|
||||||
rgba(0,0,0,0.12) 0%,
|
|
||||||
rgba(0,0,0,0.08) 50%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 8px;
|
|
||||||
background: linear-gradient(to top,
|
|
||||||
rgba(0,0,0,0.12) 0%,
|
|
||||||
rgba(0,0,0,0.08) 50%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea.shadow-top::before {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea.shadow-bottom::after {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
107
src/components/pnShadowScroll.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'fix-scroll-area-content': hideShadows }">
|
||||||
|
<q-scroll-area
|
||||||
|
:style="{ height: height + 'px' }"
|
||||||
|
class="w100 q-pa-none q-ma-none"
|
||||||
|
@scroll="onScroll"
|
||||||
|
:class=" {
|
||||||
|
'shadow-top': hasScrolled,
|
||||||
|
'shadow-bottom': hasScrolledBottom
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
<div class="q-pa-sm"/>
|
||||||
|
</q-scroll-area>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
height: number
|
||||||
|
hideShadows: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hasScrolled = ref(false)
|
||||||
|
const hasScrolledBottom = ref(false)
|
||||||
|
|
||||||
|
interface ScrollInfo {
|
||||||
|
verticalPosition: number;
|
||||||
|
verticalPercentage: number;
|
||||||
|
verticalSize: number;
|
||||||
|
verticalContainerSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll(info: ScrollInfo) {
|
||||||
|
hasScrolled.value = info.verticalPosition > 0
|
||||||
|
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||||
|
hasScrolledBottom.value = !scrollEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.q-scrollarea {
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
rgba(0,0,0,0.12) 0%,
|
||||||
|
rgba(0,0,0,0.08) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(to top,
|
||||||
|
rgba(0,0,0,0.12) 0%,
|
||||||
|
rgba(0,0,0,0.08) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-scroll-area-content:deep(.q-scrollarea::after) {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea.shadow-top::before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-scrollarea.shadow-bottom::after {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
src/components/pnSmallDialog.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="modelValue"
|
||||||
|
>
|
||||||
|
<q-card class="q-pa-none q-ma-none w100 no-scroll" align="center">
|
||||||
|
<q-card-section>
|
||||||
|
<q-avatar :color :icon size="60px" font-size="45px" text-color="white"/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
class="wrap no-scroll q-gutter-y-lg q-pt-none "
|
||||||
|
style="overflow-wrap: break-word"
|
||||||
|
>
|
||||||
|
<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-actions align="center" vertical>
|
||||||
|
<div class="flex q-mt-lg no-wrap w100 justify-center q-gutter-x-md">
|
||||||
|
<q-btn
|
||||||
|
v-if="auxBtnLabel"
|
||||||
|
:label="$t(auxBtnLabel)"
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
v-close-popup
|
||||||
|
rounded
|
||||||
|
class="w50"
|
||||||
|
@click="emit('clickAuxBtn')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
:label="$t(mainBtnLabel)"
|
||||||
|
:color="color"
|
||||||
|
v-close-popup
|
||||||
|
rounded
|
||||||
|
:class="auxBtnLabel ? 'w50' : 'w80'"
|
||||||
|
@click="emit('clickMainBtn')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
class="w80 q-mt-md q-mb-sm" flat
|
||||||
|
v-close-popup rounded
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<q-icon name="close"/>
|
||||||
|
{{$t('cancel')}}
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
icon?: string
|
||||||
|
color?: string
|
||||||
|
title: string
|
||||||
|
message1?: string
|
||||||
|
message2?: string
|
||||||
|
mainBtnLabel: string
|
||||||
|
auxBtnLabel?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = defineModel<boolean>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'clickMainBtn',
|
||||||
|
'clickAuxBtn',
|
||||||
|
'close'
|
||||||
|
])
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
110
src/components/projectBlock.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded color="primary"
|
||||||
|
class="w100 q-mt-md q-mb-xs"
|
||||||
|
:disable="!(isFormValid && (isDirty(initialProject, modelValue)))"
|
||||||
|
@click = "emit('update')"
|
||||||
|
>
|
||||||
|
{{ $t(btnText) }}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||||
|
<pn-image-selector
|
||||||
|
v-model="modelValue.logo"
|
||||||
|
:size="100"
|
||||||
|
:iconsize="80"
|
||||||
|
class="q-pb-lg"
|
||||||
|
/>
|
||||||
|
<div class="q-gutter-y-lg w100">
|
||||||
|
<q-input
|
||||||
|
v-model="modelValue.name"
|
||||||
|
no-error-icon
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
label-slot
|
||||||
|
class = "w100 fix-bottom-padding"
|
||||||
|
:rules="[rules.name]"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
{{ $t('project_block__project_name') }} <span class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="modelValue.description"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
class="w100 q-pt-sm"
|
||||||
|
:label="$t('project_block__project_description')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- <q-checkbox
|
||||||
|
v-if="modelValue.logo"
|
||||||
|
v-model="modelValue.is_logo_bg"
|
||||||
|
class="w100"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
{{ $t('project_block__image_use_as_background_chats') }}
|
||||||
|
</q-checkbox> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { isDirty } from 'helpers/helpers'
|
||||||
|
import type { ProjectParams } from 'types/Project'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const modelValue = defineModel<ProjectParams>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string,
|
||||||
|
btnText: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const rulesErrorMessage = {
|
||||||
|
name: t('project_block__error_name')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: (val: ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
const validations = {
|
||||||
|
name: rules.name(modelValue.value.name) === true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(validations).every(Boolean)
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialProject = ref({} as ProjectParams)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initialProject.value = { ...modelValue.value }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
|
padding-bottom: 0 !important
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex column items-center q-pa-lg">
|
|
||||||
<pn-image-selector
|
|
||||||
v-model="modelValue.logo"
|
|
||||||
:size="100"
|
|
||||||
:iconsize="80"
|
|
||||||
class="q-pb-lg"
|
|
||||||
/>
|
|
||||||
<div class="q-gutter-y-lg w100">
|
|
||||||
<q-input
|
|
||||||
v-model="modelValue.name"
|
|
||||||
no-error-icon
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
label-slot
|
|
||||||
class = "w100 fix-bottom-padding"
|
|
||||||
:rules="[rules.name]"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
{{ $t('project_card__project_name') }} <span class="text-red">*</span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="modelValue.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
autogrow
|
|
||||||
class="w100 q-pt-sm"
|
|
||||||
:label="$t('project_card__project_description')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-checkbox
|
|
||||||
v-if="modelValue.logo"
|
|
||||||
v-model="modelValue.is_logo_bg"
|
|
||||||
class="w100"
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
{{ $t('project_card__image_use_as_background_chats') }}
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { ProjectParams } from 'types/Project'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
const { t }= useI18n()
|
|
||||||
|
|
||||||
const modelValue = defineModel<ProjectParams>({ required: true })
|
|
||||||
const emit = defineEmits(['valid'])
|
|
||||||
const rulesErrorMessage = {
|
|
||||||
name: t('project_card__error_name')
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
name: (val :ProjectParams['name']) => !!val?.trim() || rulesErrorMessage['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = computed(() => {
|
|
||||||
const checkName = rules.name(modelValue.value.name)
|
|
||||||
return { name: checkName && (checkName !== rulesErrorMessage['name']) }
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isValid, (newVal) => {
|
|
||||||
const allValid = Object.values(newVal).every(v => v)
|
|
||||||
emit('valid', allValid)
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.q-field--with-bottom.fix-bottom-padding {
|
|
||||||
padding-bottom: 0 !important
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
180
src/components/userBlock.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded color="primary"
|
||||||
|
class="w100 q-mt-md q-mb-xs"
|
||||||
|
:disable="!(isDirty(initialUser, modelValue))"
|
||||||
|
@click = "emit('update')"
|
||||||
|
>
|
||||||
|
{{ $t(btnText) }}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<pn-scroll-list>
|
||||||
|
<div class="flex column items-center q-pa-md q-pb-sm">
|
||||||
|
<div class="relative-position">
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="modelValue.photo"
|
||||||
|
:name="tname"
|
||||||
|
size="100px"
|
||||||
|
class="q-pb-lg"
|
||||||
|
:style="!userStatus ? {} : { filter: 'grayscale(100%)'}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="userStatus"
|
||||||
|
class="absolute-center text-h4 text-bold q-pa-sm"
|
||||||
|
:class ="'status-' + userStatus.status"
|
||||||
|
>
|
||||||
|
{{ $t(userStatus.text) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex row items-start justify-center no-wrap q-pb-lg">
|
||||||
|
<div class="flex column justify-center">
|
||||||
|
<div class="text-bold q-pr-xs text-center" align="center">{{ tname }}</div>
|
||||||
|
<div caption class="text-blue text-caption" align="center" v-if="modelValue.username">{{ modelValue.username }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-gutter-y-lg w100">
|
||||||
|
<q-input
|
||||||
|
v-model.trim="modelValue.fullname"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class = "w100"
|
||||||
|
:label = "$t('user_block__name')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="modelValue.company_id"
|
||||||
|
:options="displayCompanies"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 q-pt-sm"
|
||||||
|
:label = "$t('user_block__company')"
|
||||||
|
option-value="id"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<pn-auto-avatar
|
||||||
|
v-if="scope.opt.id"
|
||||||
|
:img="scope.opt.logo"
|
||||||
|
:name="scope.opt.label"
|
||||||
|
size="md"
|
||||||
|
type="rounded"
|
||||||
|
/>
|
||||||
|
<q-avatar
|
||||||
|
v-else
|
||||||
|
rounded
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<q-icon size="32px" color="grey" name="mdi-cancel"/>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model.trim="modelValue.department"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class = "w100 q-pt-sm"
|
||||||
|
:label = "$t('user_block__department')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model.trim="modelValue.role"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class = "w100 q-pt-sm"
|
||||||
|
:label = "$t('user_block__role')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, computed, ref } from 'vue'
|
||||||
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { isDirty } from 'helpers/helpers'
|
||||||
|
import type { User } from 'types/Users'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const modelValue = defineModel<User>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string,
|
||||||
|
btnText: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const initialUser = ref({} as User)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initialUser.value = { ...modelValue.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
const companiesStore = useCompaniesStore()
|
||||||
|
const companies = computed(() => companiesStore.companies)
|
||||||
|
|
||||||
|
const displayCompanies = computed(() => [
|
||||||
|
...companies.value.map(el => ({
|
||||||
|
id: el.id,
|
||||||
|
label: el.name,
|
||||||
|
logo: el.logo
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
id: null,
|
||||||
|
label: t('user_block__no_company'),
|
||||||
|
logo: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const tname = computed(() =>
|
||||||
|
[modelValue.value?.firstname, modelValue.value?.lastname]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
)
|
||||||
|
|
||||||
|
const userStatus = computed(() => {
|
||||||
|
if (modelValue.value.is_blocked) return { status: 'blocked', text: 'user_block__user_blocked'}
|
||||||
|
if (modelValue.value.is_leave) return { status: 'leave', text: 'user_block__user_leave'}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-bottom-padding.q-field--with-bottom {
|
||||||
|
padding-bottom: 0 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-blocked {
|
||||||
|
border: 2px solid red;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-leave {
|
||||||
|
border: 2px solid var(--q-primary);
|
||||||
|
color: var(--q-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,5 +18,5 @@ export function useNotify() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { notifyError}
|
return { notifyError }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ $base-height: 100;
|
|||||||
body {
|
body {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-fab-offset {
|
||||||
|
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.projects-header {
|
.projects-header {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
@@ -43,4 +52,16 @@ body {
|
|||||||
border-top-left-radius: var(--top-raduis);
|
border-top-left-radius: var(--top-raduis);
|
||||||
border-top-right-radius: var(--top-raduis);
|
border-top-right-radius: var(--top-raduis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.orline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orline:before,
|
||||||
|
.orline:after {
|
||||||
|
content: "";
|
||||||
|
flex: 1 1;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|||||||
98
src/helpers/helpers.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
function isDirty(
|
||||||
|
obj1: Record<string, unknown> | null | undefined,
|
||||||
|
obj2: Record<string, unknown> | null | undefined
|
||||||
|
): boolean {
|
||||||
|
const actualObj1 = obj1 ?? {}
|
||||||
|
const actualObj2 = obj2 ?? {}
|
||||||
|
|
||||||
|
const filteredObj1 = filterIgnored(actualObj1)
|
||||||
|
const filteredObj2 = filterIgnored(actualObj2)
|
||||||
|
|
||||||
|
const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)])
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
const hasKey1 = Object.hasOwn(filteredObj1, key)
|
||||||
|
const hasKey2 = Object.hasOwn(filteredObj2, key)
|
||||||
|
|
||||||
|
// Различие в наличии ключа
|
||||||
|
if (hasKey1 !== hasKey2) return true
|
||||||
|
|
||||||
|
if (hasKey1 && hasKey2) {
|
||||||
|
const val1 = filteredObj1[key]
|
||||||
|
const val2 = filteredObj2[key]
|
||||||
|
|
||||||
|
// Сравнение массивов
|
||||||
|
if (Array.isArray(val1) && Array.isArray(val2)) {
|
||||||
|
if (val1.length !== val2.length) return true
|
||||||
|
const set2 = new Set(val2)
|
||||||
|
if (!val1.every(item => set2.has(item))) return true
|
||||||
|
}
|
||||||
|
// Один массив, другой - нет
|
||||||
|
else if (Array.isArray(val1) || Array.isArray(val2)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Сравнение строк
|
||||||
|
else if (typeof val1 === 'string' && typeof val2 === 'string') {
|
||||||
|
if (val1.trim() !== val2.trim()) return true
|
||||||
|
}
|
||||||
|
// Сравнение примитивов
|
||||||
|
else if (val1 !== val2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterIgnored(obj: Record<string, unknown>): Record<string, string | number | boolean | (string | number)[]> {
|
||||||
|
const filtered: Record<string, string | number | boolean | (string | number)[]> = {}
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key]
|
||||||
|
|
||||||
|
// Обработка массивов
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// Отбрасываем пустые массивы
|
||||||
|
if (value.length === 0) continue
|
||||||
|
|
||||||
|
// Фильтруем массивы с некорректными элементами
|
||||||
|
if (value.every(item =>
|
||||||
|
typeof item === 'string' ||
|
||||||
|
typeof item === 'number'
|
||||||
|
)) {
|
||||||
|
filtered[key] = value
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка примитивов
|
||||||
|
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка строк
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (trimmed === '') continue
|
||||||
|
filtered[key] = trimmed
|
||||||
|
}
|
||||||
|
// Обработка чисел и boolean
|
||||||
|
else if (value !== 0 && value !== false) {
|
||||||
|
filtered[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIntString (s: string | string[] | undefined) :number | null {
|
||||||
|
if (typeof s !== 'string') return null
|
||||||
|
const regex = /^[+-]?\d+$/
|
||||||
|
return regex.test(s) ? Number(s) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
isDirty,
|
||||||
|
parseIntString
|
||||||
|
}
|
||||||
@@ -1,60 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-layout
|
|
||||||
view="lHr lpR lFr"
|
|
||||||
fit
|
|
||||||
class="fit no-scroll bg-transparent"
|
|
||||||
>
|
|
||||||
<q-drawer
|
|
||||||
v-if="existDrawer"
|
|
||||||
show-if-above
|
|
||||||
side="left"
|
|
||||||
class="drawer no-scroll"
|
|
||||||
:width="drawerWidth"
|
|
||||||
:breakpoint="bodyWidth"
|
|
||||||
/>
|
|
||||||
<q-drawer
|
|
||||||
v-if="existDrawer"
|
|
||||||
show-if-above
|
|
||||||
side="right"
|
|
||||||
class="drawer no-scroll"
|
|
||||||
:width="drawerWidth"
|
|
||||||
:breakpoint="bodyWidth"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<q-layout
|
||||||
|
view="lHr lpr lFr"
|
||||||
|
class="no-scroll bg-transparent"
|
||||||
|
>
|
||||||
<q-page-container
|
<q-page-container
|
||||||
class="q-pa-none q-ma-none no-scroll bg-transparent page-width"
|
class="main-content q-pa-none q-ma-none no-scroll bg-transparent"
|
||||||
>
|
>
|
||||||
<router-view />
|
<q-page class="no-scroll column">
|
||||||
|
<router-view />
|
||||||
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
<meshBackground/>
|
|
||||||
<q-resize-observer @resize="onResize"></q-resize-observer>
|
|
||||||
</q-layout>
|
</q-layout>
|
||||||
|
<meshBackground/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import meshBackground from 'components/meshBackground.vue'
|
import meshBackground from 'components/meshBackground.vue'
|
||||||
|
|
||||||
const existDrawer = ref<boolean>(true)
|
|
||||||
function getCSSVar (varName: string) {
|
|
||||||
const root = document.documentElement
|
|
||||||
return getComputedStyle(root).getPropertyValue(varName).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyWidth = parseInt(getCSSVar('--body-width'))
|
|
||||||
const drawerWidth = ref<number>(300)
|
|
||||||
function onResize () {
|
|
||||||
const clientWidth = document.documentElement.clientWidth;
|
|
||||||
drawerWidth.value = (clientWidth - bodyWidth)/2
|
|
||||||
existDrawer.value = clientWidth > bodyWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
aside {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
20
src/pages/AccountChangeAuthMethodPage.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-card>
|
||||||
|
<template #title>
|
||||||
|
{{$t('account__change_auth_method')}}
|
||||||
|
</template>
|
||||||
|
<pn-scroll-list>
|
||||||
|
<account-helper :type/>
|
||||||
|
</pn-scroll-list>
|
||||||
|
</pn-page-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import accountHelper from 'components/accountHelper.vue'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
|
||||||
|
const authStore= useAuthStore()
|
||||||
|
|
||||||
|
const type = 'changeMethod'
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,17 +1,283 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="col-grow">
|
{{$t('account_change_email__title')}}
|
||||||
{{$t('account__change_password')}}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<account-helper :type />
|
<q-stepper
|
||||||
|
v-model="step"
|
||||||
|
vertical
|
||||||
|
color="primary"
|
||||||
|
animated
|
||||||
|
flat
|
||||||
|
class="bg-transparent"
|
||||||
|
>
|
||||||
|
<q-step
|
||||||
|
:name="1"
|
||||||
|
:title="$t('account_change_email__current_email')"
|
||||||
|
:done="step > 1"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
v-model="login"
|
||||||
|
autofocus
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
:label = "$t('account_change_email__current_email')"
|
||||||
|
disable
|
||||||
|
/>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<q-btn
|
||||||
|
@click="handleSubmit"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('continue')"
|
||||||
|
/>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step
|
||||||
|
:name="2"
|
||||||
|
:title="$t('account_change_email__confirm_current_email')"
|
||||||
|
:done="step > 2"
|
||||||
|
>
|
||||||
|
<div class="q-pb-md">{{$t('account_change_email__confirm_email_message')}}</div>
|
||||||
|
<q-input
|
||||||
|
v-model="code"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autofocus
|
||||||
|
hide-bottom-space
|
||||||
|
:label = "$t('account_change_email__code')"
|
||||||
|
num="30"
|
||||||
|
/>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<q-btn
|
||||||
|
@click="handleSubmit"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('continue')"
|
||||||
|
:disable="code.length === 0"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
@click="step = 1"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('back')"
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step
|
||||||
|
:name="3"
|
||||||
|
:title="$t('account_change_email__new_email')"
|
||||||
|
:done="step > 2"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
v-model="newLogin"
|
||||||
|
autofocus
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
:label = "$t('account_change_email__new_email')"
|
||||||
|
:rules="validationRules.email"
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
@focus="($refs.emailInput as typeof QInput)?.resetValidation()"
|
||||||
|
ref="emailInput"
|
||||||
|
/>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<q-btn
|
||||||
|
@click="handleSubmit"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('continue')"
|
||||||
|
:disabled="!isEmailValid"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
@click="step = 2"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('back')"
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step
|
||||||
|
:name="4"
|
||||||
|
:title="$t('account_change_email__confirm_new_email')"
|
||||||
|
:done="step > 3"
|
||||||
|
>
|
||||||
|
<div class="q-pb-md">{{$t('account_change_email__confirm_email_message')}}</div>
|
||||||
|
<q-input
|
||||||
|
v-model="newCode"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autofocus
|
||||||
|
hide-bottom-space
|
||||||
|
:label = "$t('account_change_email__code')"
|
||||||
|
num="30"
|
||||||
|
/>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<q-btn
|
||||||
|
@click="handleSubmit"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('continue')"
|
||||||
|
:disable="newCode.length === 0"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
@click="step = 3"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('back')"
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step
|
||||||
|
:name="5"
|
||||||
|
:title="$t('account_change_email__set_password')"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
v-model="password"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
:label = "$t('account_change_email__password')"
|
||||||
|
:type="isPwd ? 'password' : 'text'"
|
||||||
|
hide-hint
|
||||||
|
:hint="passwordHint"
|
||||||
|
:rules="validationRules.password"
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
@focus="($refs.passwordInput as typeof QInput)?.resetValidation()"
|
||||||
|
ref="passwordInput"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon
|
||||||
|
color="grey-5"
|
||||||
|
:name="isPwd ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="isPwd = !isPwd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<q-btn
|
||||||
|
@click="handleSubmit"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('account_change_email__finish')"
|
||||||
|
:disabled = "!isPasswordValid"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
@click="step = 4"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('back')"
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</q-step>
|
||||||
|
</q-stepper>
|
||||||
|
|
||||||
|
<pn-magic-overlay
|
||||||
|
v-if="showSuccessOverlay"
|
||||||
|
icon="mdi-check-circle-outline"
|
||||||
|
message1="account_change_email__ok_message1"
|
||||||
|
message2="account_change_email__ok_message2"
|
||||||
|
route-name="account"
|
||||||
|
/>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import accountHelper from 'src/components/accountHelper.vue'
|
import { ref, computed } from 'vue'
|
||||||
const type = 'forgotPwd'
|
import type { AxiosError } from 'axios'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { useI18n } from "vue-i18n"
|
||||||
|
import { QInput } from 'quasar'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
type ValidationRule = (val: string) => boolean | string
|
||||||
|
type Step = 1 | 2 | 3 | 4 | 5
|
||||||
|
|
||||||
|
const step = ref<Step>(1)
|
||||||
|
const login = authStore.customer?.email
|
||||||
|
const code = ref<string>('')
|
||||||
|
const newLogin = ref<string>('')
|
||||||
|
const newCode = ref<string>('')
|
||||||
|
const password = ref<string>('')
|
||||||
|
const showSuccessOverlay = ref(false)
|
||||||
|
const isPwd = ref<boolean>(true)
|
||||||
|
const validationRules = {
|
||||||
|
email: [(val: string) => /.+@.+\..+/.test(val) || t('login__incorrect_email')] as [ValidationRule],
|
||||||
|
password: [(val: string) => val.length >= 8 || t('login__password_require')] as [ValidationRule]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmailValid = computed(() =>
|
||||||
|
validationRules.email.every(f => f(newLogin.value) === true)
|
||||||
|
)
|
||||||
|
|
||||||
|
const isPasswordValid = computed(() =>
|
||||||
|
validationRules.password.every(f => f(password.value) === true)
|
||||||
|
)
|
||||||
|
|
||||||
|
const passwordHint = computed(() => {
|
||||||
|
const result = validationRules.password[0](password.value)
|
||||||
|
return typeof result === 'string' ? result : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const stepActions: Record<Step, () => Promise<void>> = {
|
||||||
|
1: async () => {
|
||||||
|
await authStore.getCodeCurrentEmail()
|
||||||
|
},
|
||||||
|
2: async () => {
|
||||||
|
await authStore.confirmCurrentEmailCode(code.value)
|
||||||
|
console.log(code.value)
|
||||||
|
},
|
||||||
|
3: async () => {
|
||||||
|
await authStore.getCodeNewEmail(code.value, newLogin.value)
|
||||||
|
},
|
||||||
|
4: async () => {
|
||||||
|
await authStore.confirmNewEmailCode(code.value, newCode.value, newLogin.value,)
|
||||||
|
},
|
||||||
|
5: async () => {
|
||||||
|
await authStore.setNewEmailPassword(code.value, newCode.value, newLogin.value, password.value)
|
||||||
|
await authStore.loginWithCredentials(newLogin.value, password.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (err: AxiosError) => {
|
||||||
|
const error = err as AxiosError<{ error?: { message?: string } }>
|
||||||
|
const message = error.response?.data?.error?.message || t('unknown_error')
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
message: `${t('error')}: ${message}`,
|
||||||
|
type: 'negative',
|
||||||
|
position: 'bottom',
|
||||||
|
timeout: 2500
|
||||||
|
})
|
||||||
|
|
||||||
|
if (step.value > 1) {
|
||||||
|
code.value = ''
|
||||||
|
password.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await stepActions[step.value]()
|
||||||
|
if (step.value < 5) {
|
||||||
|
step.value++
|
||||||
|
} else {
|
||||||
|
showSuccessOverlay.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error as AxiosError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="col-grow">
|
{{$t('account__change_password')}}
|
||||||
{{$t('account__change_password')}}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<account-helper :type />
|
<account-helper :type :email/>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import accountHelper from 'src/components/accountHelper.vue'
|
import accountHelper from 'components/accountHelper.vue'
|
||||||
const type = 'change'
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
|
||||||
|
const authStore= useAuthStore()
|
||||||
|
|
||||||
|
const type = 'changePwd'
|
||||||
|
const email = authStore.customer?.email ? authStore.customer?.email : '???'
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-card>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="col-grow">
|
{{$t('login__register')}}
|
||||||
{{$t('login__register')}}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<account-helper :type :email/>
|
<account-helper :type :email/>
|
||||||
@@ -13,7 +11,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import accountHelper from 'src/components/accountHelper.vue'
|
import accountHelper from 'components/accountHelper.vue'
|
||||||
|
|
||||||
const type = 'register'
|
const type = 'register'
|
||||||
const email = ref(sessionStorage.getItem('pendingLogin') || '')
|
const email = ref(sessionStorage.getItem('pendingLogin') || '')
|
||||||
|
|||||||