From 7e798a7a8333786de04f1253d9343db05bc5330c Mon Sep 17 00:00:00 2001 From: CCTVcalc Date: Mon, 14 Apr 2025 10:27:58 +0300 Subject: [PATCH] v2 --- backend/app.js | 96 + backend/apps/admin.js | 496 ++++ backend/apps/bot.js | 654 ++++++ backend/apps/miniapp.js | 643 ++++++ backend/data/db.sqlite | Bin 0 -> 94208 bytes backend/data/db.sqlite-shm | Bin 0 -> 32768 bytes backend/data/db.sqlite-wal | Bin 0 -> 28872 bytes backend/data/init.sql | 166 ++ backend/include/db.js | 57 + backend/package-lock.json | 1990 +++++++++++++++++ backend/package.json | 25 + backend/public/index.html | 337 +++ backend/public/miniapp.html | 65 + i18n-2.xlsm | Bin 45094 -> 44883 bytes index.html | 4 +- package-lock.json | 16 + package.json | 2 + quasar.config.ts | 22 +- src/App.vue | 21 +- src/boot/auth-init.ts | 6 + src/boot/axios.ts | 27 +- src/boot/helpers.ts | 36 +- src/boot/telegram-boot.ts | 18 + src/components/admin/accountHelper.vue | 11 +- src/components/admin/companyInfoBlock.vue | 23 + src/components/admin/login-page/loginLogo.vue | 4 +- .../admin/project-page/ProjectPageChats.vue | 80 +- .../project-page/ProjectPageCompanies.vue | 73 +- .../admin/project-page/ProjectPageHeader.vue | 53 +- src/components/admin/projectInfoBlock.vue | 51 +- src/css/app.scss | 7 + src/css/quasar.variables.scss | 1 + src/i18n/en-US/index.ts | 2 +- src/i18n/ru-RU/index.ts | 2 +- src/index copy.ts | 32 - src/layouts/MainLayout.vue | 21 +- src/pages/AccountPage.vue | 132 +- src/pages/CompanyInfoPage.vue | 52 +- src/pages/CreateAccountPage.vue | 6 +- src/pages/CreateProjectPage.vue | 26 +- src/pages/ForgotPasswordPage.vue | 9 +- src/pages/LoginPage.vue | 66 +- src/pages/PersonInfoPage.vue | 107 +- src/pages/ProjectInfoPage.vue | 33 +- src/pages/ProjectPage.vue | 5 +- src/pages/ProjectsPage.vue | 47 +- src/pages/SettingsPage.vue | 93 + src/pages/TermsPage.vue | 22 + src/router/index.ts | 57 +- src/router/routes.ts | 54 +- src/stores/auth.ts | 65 + src/stores/textSize.ts | 121 + src/types.ts | 10 + todo.txt | 27 +- tsconfig.json | 5 +- 55 files changed, 5625 insertions(+), 353 deletions(-) create mode 100644 backend/app.js create mode 100644 backend/apps/admin.js create mode 100644 backend/apps/bot.js create mode 100644 backend/apps/miniapp.js create mode 100644 backend/data/db.sqlite create mode 100644 backend/data/db.sqlite-shm create mode 100644 backend/data/db.sqlite-wal create mode 100644 backend/data/init.sql create mode 100644 backend/include/db.js create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/public/index.html create mode 100644 backend/public/miniapp.html create mode 100644 src/boot/auth-init.ts create mode 100644 src/boot/telegram-boot.ts delete mode 100644 src/index copy.ts create mode 100644 src/pages/SettingsPage.vue create mode 100644 src/pages/TermsPage.vue create mode 100644 src/stores/auth.ts create mode 100644 src/stores/textSize.ts diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..8f5d343 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,96 @@ +const express = require('express') +const bodyParser = require('body-parser') +const cookieParser = require('cookie-parser') +const crypto = require('crypto') +const fs = require('fs') +const util = require('util') +const bot = require('./apps/bot') + +const app = express() + +app.use(bodyParser.json()) +app.use(cookieParser()) + +BigInt.prototype.toJSON = function () { + return Number(this) +} + +app.use((req, res, next) => { + if(!(req.body instanceof Object)) + return next() + + const escapeHtml = str => str.replace(/&/g, '&').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() +}) + +// cors +app.use((req, res, next) => { + res.set({ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE', + 'Access-Control-Allow-Headers': 'Accept,Accept-Language,Content-Language,Content-Type,Authorization,Cookie,X-Requested-With,Origin,Host', + 'Access-Control-Allow-Credentials': true + }) + + return req.method == 'OPTIONS' ? res.status(200).json({success: true}) : next() +}) + +app.post('(/api/admin/customer/login|/api/miniapp/user/login)', (req, res, next) => { + const data = Object.assign({}, req.query) + delete data.hash + const hash = req.query?.hash + + const BOT_TOKEN = '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k' + const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join("\n") + const secretKey = crypto.createHmac("sha256", "WebAppData").update(BOT_TOKEN).digest() + const hmac = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex") + + const timeDiff = Date.now() / 1000 - data.auth_date + + if (hmac !== req.query.hash) // || timeDiff > 10) + throw Error('ACCESS_DENIED::401') + + const user = JSON.parse(req.query.user) + res.locals.telegram_id = user.id + res.locals.start_param = req.query.start_param + + if (!res.locals.telegram_id) + throw Error('ACCESS_DENIED::500') + + next() +}) + + +app.use('/api/admin', require('./apps/admin')) +app.use('/api/miniapp', require('./apps/miniapp')) + +app.use((err, req, res, next) => { + console.error(`Error for ${req.path}: ${err}`) + + let message, code + //if (err.code == 'SQLITE_ERROR' || err.code == 'SQLITE_CONSTRAINT_CHECK') { + // message = 'DATABASE_ERROR' + //code = err.code == 'SQLITE_CONSTRAINT_CHECK' ? 400 : 500 + //} else { + [message, code = 500] = err.message.split('::') + //} + + res.status(res.statusCode == 200 ? 500 : res.statusCode).json({success: false, error: { message, code}}) +}) + +app.use(express.static('public')) + +const PORT = process.env.PORT || 3000 +app.listen(PORT, async () => { + console.log(`Listening at port ${PORT}`) + bot.start( + process.env.API_ID || 26746106, + process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5', + process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k' + ) +}) \ No newline at end of file diff --git a/backend/apps/admin.js b/backend/apps/admin.js new file mode 100644 index 0000000..854a091 --- /dev/null +++ b/backend/apps/admin.js @@ -0,0 +1,496 @@ +const crypto = require('crypto') +const express = require('express') +const multer = require('multer') +const db = require('../include/db') +const bot = require('./bot') +const fs = require('fs') +const cookieParser = require('cookie-parser') + +const app = express.Router() +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 1_000_000 // 1mb + } +}) + +const sessions = {} + +app.use((req, res, next) => { + if (req.path == '/customer/login' || + req.path == '/customer/register' || + req.path == '/customer/activate') + 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() +}) + +// CUSTOMER +app.post('/customer/login', (req, res, next) => { + res.locals.email = req.body?.email + res.locals.password = req.body?.password + + let customer_id = db + .prepare(` + select id + from customers + where is_active = 1 and ( + email is not null and email = :email and password is not null and password = :password or + email is null and password is null and telegram_user_id = :telegram_id + ) + `) + .pluck(true) + .get(res.locals) + + if (!customer_id && !res.locals.email && !res.locals.password) { + customer_id = db + .prepare(`insert into customers (telegram_user_id, is_active) values (:telegram_id, 1) returning id`) + .safeIntegers(true) + .pluck(true) + .get(res.locals) + } + + if (!customer_id) + throw Error('AUTH_ERROR::401') + + res.locals.customer_id = customer_id + db + .prepare(`update customers set telegram_user_id = :telegram_id where id = :customer_id and email is not null`) + .run(res.locals) + + const asid = crypto.randomBytes(64).toString('hex') + req.session = sessions[asid] = {asid, customer_id } + res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`]) + + res.status(200).json({success: true}) +}) + +app.get('/customer/logout', (req, res, next) => { + delete sessions[req.session.asid] + res.setHeader('Set-Cookie', [`asid=; expired; httpOnly`]) + res.status(200).json({success: true}) +}) + +app.post('/customer/register', (req, res, next) => { + const email = String(req.body.email).trim() + const password = String(req.body.password).trim() + + const validateEmail = email => String(email).toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) + if (!validateEmail(email)) + throw Error('INCORRECT_EMAIL::400') + + if (!password) + throw Error('EMPTY_PASSWORD::400') + + const row = db + .prepare('select id from customers where email = :email') + .run({email}) + if (row) + throw Error('DUPLICATE_EMAIL::400') + + const key = crypto.randomBytes(32).toString('hex') + const info = db + .prepare('insert into customers (email, password, activation_key) values (:email, :password, :key)') + .run({email, password, key}) + + // To-Do: SEND MAIL + console.log(`http://127.0.0.1:3000/api/customer/activate?key=${key}`) + res.status(200).json({success: true, data: key}) +}) + +app.get('/customer/activate', (req, res, next) => { + const row = db + .prepare('update customers set is_active = 1 where activation_key = :key returning id') + .get({key: req.query.key}) + + if (!row || !row.id) + throw Error('BAD_ACTIVATION_KEY::400') + + res.status(200).json({success: true}) +}) + +app.get('/customer/profile', (req, res, next) => { + const row = db + .prepare(` + select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_group_id + from customers + where id = :customer_id and is_active = 1 + `) + .get(res.locals) + + if (row?.upload_group_id) { + row.upload_group = db + .prepare(`select id, name, telegram_id from groups where id = :group_id and project_id is null`) + .safeIntegers(true) + .get({ group_id: row.upload_group_id}) + delete row.upload_group_id + } + + for (const key in row) { + if (key.startsWith('json_')) { + row[key.substr(5)] = JSON.parse(row[key]) + delete row[key] + } + } + + res.status(200).json({success: true, data: row}) +}) + +app.put('/customer/profile', (req, res, next) => { + if (req.body.company instanceof Object) + req.body.json_company = JSON.stringify(req.body.company) + + const info = db + .prepareUpdate( + 'customers', + ['name', 'password', 'json_company'], + req.body, + ['id']) + .run(Object.assign({}, req.body, {id: req.session.customer_id})) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +// PROJECT +app.get('/project', (req, res, next) => { + const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, description, logo + from projects + where customer_id = :customer_id ${where} and is_deleted <> 1 + order by name + `) + .all(res.locals) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project?id=${req.params.pid}`) +}) + +app.post('/project', (req, res, next) => { + res.locals.name = req.body?.name + res.locals.description = req.body?.description + res.locals.logo = req.body?.logo + + const id = db + .prepare(` + insert into projects (customer_id, name, description, logo) + values (:customer_id, :name, :description, :logo) + returning id + `) + .pluck(true) + .get(res.locals) + + res.status(200).json({success: true, data: 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 + + const info = db + .prepareUpdate( + 'projects', + ['name', 'description', 'logo'], + res.locals, + ['id', 'customer_id']) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.delete('/project/:pid(\\d+)', async (req, res, next) => { + res.locals.id = req.params.pid + + const info = db + .prepare('update projects set id_deleted = 1 where id = :id and customer_id = :customer_id') + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + const groupIds = db + .prepare(`select id from groups where project_id = :id`) + .pluck(true) + .all(res.locals) + + for (const groupId of groupIds) { + await bot.sendMessage(groupId, 'Проект удален') + await bot.leaveGroup(groupId) + } + + db.prepare(`updates groups set project_id = null where id in (${ groupIds.join(', ')})`).run() + + res.status(200).json({success: true}) +}) + +app.use ('/project/:pid(\\d+)/*', (req, res, next) => { + res.locals.project_id = parseInt(req.params.pid) + + const row = db + .prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_deleted <> 1') + .get(res.locals) + + if (!row) + throw Error('ACCESS_DENIED::401') + + next() +}) + +// USER +app.get('/project/:pid(\\d+)/user', (req, res, next) => { + const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo, + ud.fullname, ud.role, ud.department, ud.is_blocked + from users u + left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id + where id in ( + select user_id + from group_users + where group_id in (select id from groups where project_id = :project_id) + ) ${where} + `) + .safeIntegers(true) + .all(res.locals) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`) +}) + +app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => { + res.locals.user_id = parseInt(req.params.uid) + + res.locals.fullname = req.body?.fullname + res.locals.role = req.body?.role + res.locals.department = req.body?.department + res.locals.is_blocked = req.body?.is_blocked + + const info = db + .prepareUpdate('user_details', + ['fullname', 'role', 'department', 'is_blocked'], + res.locals, + ['user_id', 'project_id'] + ) + .all(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.get('/project/:pid(\\d+)/token', (req, res, next) => { + res.locals.time = Math.floor(Date.now() / 1000) + + const key = db + .prepare('select generate_key(id, :time) from projects where id = :project_id and customer_id = :customer_id') + .pluck(true) + .get(res.locals) + + if (!key) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: key}) +}) + +// COMPANY +app.get('/project/:pid(\\d+)/company', (req, res, next) => { + const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, email, phone, description, logo, + (select json_group_array(user_id) from company_users where company_id = c.id) users + from companies c + where project_id = :project_id ${where} + order by name + `) + .all(res.locals) + + rows.forEach(row => row.users = JSON.parse(row.users || '[]')) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`) +}) + +app.post('/project/:pid(\\d+)/company', (req, res, next) => { + res.locals.name = req.body?.name + res.locals.email = req.body?.email + res.locals.phone = req.body?.phone + res.locals.site = req.body?.site + res.locals.description = req.body?.description + res.locals.logo = req.body?.logo + + const id = db + .prepare(` + insert into companies (project_id, name, email, phone, site, description, logo) + values (:project_id, :name, :email, :phone, :site, :description, :logo) + returning id + `) + .pluck(res.locals) + .get(res.locals) + + res.status(200).json({success: true, data: id}) +}) + +app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { + res.locals.id = parseInt(req.params.cid) + res.locals.name = req.body?.name + res.locals.email = req.body?.email + res.locals.phone = req.body?.phone + res.locals.site = req.body?.site + res.locals.description = req.body?.description + + const info = db + .prepareUpdate( + 'companies', + ['name', 'email', 'phone', 'site', 'description'], + res.locals, + ['id', 'project_id']) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { + res.locals.company_id = parseInt(req.params.cid) + + const info = db + .prepare(`delete from companies where id = :company_id and project_id = :project_id`) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.get('/project/:pid(\\d+)/group', (req, res, next) => { + const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, telegram_id, is_channel, user_count, bot_can_ban + from groups + 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+)/group/:gid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.uid}`) +}) + +app.delete('/project/:pid(\\d+)/group/:gid(\\d+)', async (req, res, next) => { + res.locals.group_id = parseInt(req.params.gid) + const info = db + .prepare(`update groups set project_id = null where id = :group_id and project_id = :project_id`) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + await bot.sendMessage(res.locals.group_id, 'Группа удалена из проекта') + + res.status(200).json({success: true}) +}) + +app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { + res.locals.company_id = parseInt(req.params.cid) + + // Проверка, что есть доступ к компании + const row = db + .prepare('select 1 from companies where id = :company_id and project_id = :project_id') + .get(res.locals) + + if (!row) + throw Error('NOT_FOUND::404') + + const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : [] + + // Проверка, что пользователи имеют доступ к проекту + let rows = db + .prepare(` + select user_id + from group_users + where group_id in (select id from groups where project_id = :project_id) + `) + .pluck(true) // .raw? + .get(res.locals) + + if (user_ids.some(user_id => !rows.contains(user_id))) + throw Error('INACCESSABLE_MEMBER::400') + + // Проверка, что пользователи не участвуют в других компаниях на проекте + rows = db + .prepare(` + select user_id + from company_users + where company_id in (select id from companies where id <> :company_id and project_id = :project_id) + `) + .pluck(true) // .raw? + .get(res.locals) + + if (user_ids.some(user_id => !rows.contains(user_id))) + throw Error('USED_MEMBER::400') + + + db + .prepare(`delete from company_users where company_id = :company_id`) + .run(res.locals) + + db + .prepare(` + insert into company_users (company_id, user_id) + select :company_id, value from json_each(:json_ids) + `) + .run(res.locals, {json_ids: JSON.stringify(user_ids)}) + + res.status(200).json({success: true}) +}) + +module.exports = app \ No newline at end of file diff --git a/backend/apps/bot.js b/backend/apps/bot.js new file mode 100644 index 0000000..963680c --- /dev/null +++ b/backend/apps/bot.js @@ -0,0 +1,654 @@ +const util = require('util') +const crypto = require('crypto') +const EventEmitter = require('events') +const fs = require('fs') +const db = require('../include/db') + +const { Api, TelegramClient } = require('telegram') +const { StringSession } = require('telegram/sessions') +const { NewMessage } = require('telegram/events') +const { Button } = require('telegram/tl/custom/button') +const { CustomFile } = require('telegram/client/uploads') + +// const session = new StringSession('1AgAOMTQ5LjE1NC4xNjcuNTABuxdIxmjimA0hmWpdrlZ4Fo7uoIGU4Bu9+G5QprS6zdtyeMfcssWEZp0doLRX/20MomQyF4Opsos0El0Ifj5aiNgg01z8khMLMeT98jS+1U/sh32p3GxZfxyXSxX1bD0NLRaXnqVyNNswYqRZPhboT28NMjDqwlz0nrW9rge+QMJDL7jIkXgSs+cmJBINiqsEI8jWjXmc8TU/17gngtjUHRf5kRM4y5gsNC4O8cF5lcHRx0G/U5ZVihTID8ItQ6EdEHjz6e4XErbVOJ81PfYkqEoPXVvkEmRM0/VbvCzFfixfas4Vzczfn98OHLd8P2MXcgokZ2rppvIV3fQXOHxJbA0=') +const session = new StringSession('') + +let client + +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: file.toString('base64') }) +} + +async function registerGroup (telegramId, isChannel) { + db + .prepare(`insert or ignore into groups (telegram_id, is_channel) values (:telegram_id, :is_channel)`) + .safeIntegers(true) + .run({ telegram_id: telegramId, is_channel: +isChannel }) + + const row = db + .prepare(`select id, name from groups where telegram_id = :telegram_id`) + .safeIntegers(true) + .get({telegram_id: telegramId}) + + if (!row?.name) { + const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId } + const group = await client.getEntity(isChannel ? new Api.PeerChannel(entity) : new Api.PeerChat(entity)) + + db + .prepare(`update groups set name = :name where id = :group_id`) + .run({ group_id: row.id, name: group.title }) + } + + return row.id +} + +async function attachGroup(groupId, isChannel, projectId) { + const info = db + .prepare(`update groups set project_id = :project_id where id = :group_id and coalesce(project_id, 1) = 1`) + .run({ group_id: groupId, project_id: projectId }) + + if (info.changes == 1) { + const inputPeer = isChannel ? + new Api.InputPeerChannel({ channelId: tgGroupId }) : + new Api.InputPeerChat({ chatlId: tgGroupId }) + + const query = `select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id` + const message = db + .prepare(query) + .pluck(true) + .get({project_id: projectId}) + if (message) + await client.sendMessage(inputPeer, {message}) + } + + return info.changes == 1 +} + +async function onGroupAttach (tgGroupId, isChannel) { + const projectId = db + .prepare(`select project_id from groups where telegram_id = :telegram_id`) + .safeIntegers(true) + .pluck(true) + .get({ telegram_id: tgGroupId }) + + const entity = isChannel ? { channelId: tgGroupId } : { chatId: tgGroupId } + const inputPeer = await client.getEntity( isChannel ? + new Api.InputPeerChannel(entity) : + new Api.InputPeerChat(entity) + ) + + const resultBtn = await client.sendMessage(inputPeer, { + message: 'ReadyOrNot', + buttons: client.buildReplyMarkup([[Button.url('Открыть проект', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user_' + projectId)]]) + }) + + await client.invoke(new Api.messages.UpdatePinnedMessage({ + peer: inputPeer, + id: resultBtn.id, + unpin: false + })) + +//fs.appendFileSync('./1.log', '\n>' + tgGroupId + ':' + isChannel + '<\n') +} + +async function reloadGroupUsers(groupId, onlyReset) { + db + .prepare(`delete from group_users where group_id = :group_id`) + .run({ group_id: groupId }) + + if (onlyReset) + return + + const group = db + .prepare(`select telegram_id, is_channel, access_hash from groups where id = :group_id`) + .get({ group_id: groupId}) + + console.log (123, group) + + if (!group) + return + + const tgGroupId = group.telegram_id + const isChannel = group.is_channel + let accessHash = group.access_hash + + console.log ('HERE') + + db + .prepare(`update groups set access_hash = :access_hash where id = :group_id`) + .safeIntegers(true) + .run({ + group_id: groupId, + access_hash: accessHash, + }) + + const result = isChannel ? + await client.invoke(new Api.channels.GetParticipants({ + channel: new Api.PeerChannel({ channelId: tgGroupId }), + filter: new Api.ChannelParticipantsRecent(), + limit: 999999, + offset: 0 + })) : await client.invoke(new Api.messages.GetFullChat({ + chatId: tgGroupId, + })) + + const users = result.users.filter(user => !user.bot) + for (const user of users) { + const userId = registerUser(user.id.value, user) + + if (updateUser(userId, user)) { + await updateUserPhoto (userId, user) + + const query = `insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)` + db.prepare(query).run({ group_id: groupId, user_id: userId }) + } + } +} + +async function registerUpload(data) { + if (!data.projectId || !data.media) + return false + + const uploadGroup = db + .prepare(` + select id, telegram_id, project_id, is_channel, access_hash + from groups + where id = (select upload_group_id + from customers + where id = (select customer_id from projects where id = :project_id limit 1) + limit 1) + limit 1 + `) + .safeIntegers(true) + .get({project_id: data.projectId}) + + if (!uploadGroup || !uploadGroup.telegram_id || uploadGroup.id == data.originGroupId) + return false + + const tgUploadGroupId = uploadGroup.telegram_id + + const peer = uploadGroup.is_channel ? + new Api.PeerChannel({ channelId: tgUploadGroupId }) : + new Api.PeerChat({ chatlId: tgUploadGroupId }) + + let resultId = 0 + + try { + const result = await client.invoke(new Api.messages.SendMedia({ + peer, + media: data.media, + message: data.caption || '', + background: true, + silent: true + })) + + const update = result.updates.find(u => + (u.className == 'UpdateNewMessage' || u.className == 'UpdateNewChannelMessage') && + u.message.className == 'Message' && + (u.message.peerId.channelId?.value == tgUploadGroupId || u.message.peerId.chatId?.value == tgUploadGroupId) && + u.message.media) + + const udoc = update?.message?.media?.document + if (udoc) { + resultId = db + .prepare(` + insert into documents (project_id, origin_group_id, origin_message_id, group_id, message_id, + file_id, access_hash, filename, mime, caption, size, published_by, parent_type, parent_id) + values (:project_id, :origin_group_id, :origin_message_id, :group_id, :message_id, + :file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :parent_type, :parent_id) + returning id + `) + .safeIntegers(true) + .pluck(true) + .get({ + project_id: data.projectId, + origin_group_id: data.originGroupId, + origin_message_id: data.originMessageId, + group_id: uploadGroup.id, + message_id: update.message.id, + file_id: udoc.id.value, + filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName, + access_hash: udoc.accessHash.value, + mime: udoc.mimeType, + caption: data.caption, + size: udoc.size.value, + published_by: data.publishedBy, + parent_type: data.parentType, + parent_id: data.parentId + }) + } + + } catch (err) { + fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n') + console.error('Message.registerUpload: ' + err.message) + } + + return resultId +} + +async function onNewServiceMessage (msg, isChannel) { + const action = msg.action || {} + const tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value + const groupId = await registerGroup(tgGroupId, isChannel) + + // Group/Channel rename + if (action.className == 'MessageActionChatEditTitle') { + const info = db + .prepare(` + update groups + set name = :name, is_channel = :is_channel, last_update_time = :last_update_time + where telegram_id = :telegram_id + `) + .safeIntegers(true) + .run({ + name: action.title, + is_channel: +isChannel, + last_update_time: Math.floor (Date.now() / 1000), + telegram_id: tgGroupId + }) + } + + // Chat to Channel + if (action.className == 'MessageActionChatMigrateTo') { + const info = db + .prepare(` + update groups + 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: tgGroupId, + new_telegram_id: action.channelId.value + }) + } + + // User/s un/register + if (action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChatDeleteUser' || + action.className == 'MessageActionChannelAddUser' || action.className == 'MessageActionChannelDeleteUser' + ) { + + const tgUserIds = [action.user, action.users, action.userId].flat().filter(Boolean).map(e => BigInt(e.value)) + const isAdd = action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChannelAddUser' + + if (tgUserIds.indexOf(bot.id) == -1) { + // Add/remove non-bot users + for (const tgUserId of tgUserIds) { + const userId = registerUser(tgUserId) + + if (isAdd) { + try { + const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId })) + updateUser(userId, user) + await updateUserPhoto (userId, user) + } catch (err) { + console.error(msg.className + ', ' + userId + ': ' + err.message) + } + } + + const query = isAdd ? + `insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)` : + `delete from group_users where group_id = :group_id and user_id = :user_id` + db.prepare(query).run({ group_id: groupId, user_id: userId }) + } + } + } +} + +async function onNewMessage (msg, isChannel) { + const tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value + const groupId = await registerGroup(tgGroupId, isChannel) + + // Document is detected + if (msg.media?.document) { + const doc = msg.media.document + + const projectId = db + .prepare(`select project_id from groups where telegram_id = :telegram_id`) + .safeIntegers(true) + .pluck(true) + .get({telegram_id: tgGroupId}) + + const media = new Api.InputMediaDocument({ + id: new Api.InputDocument({ + id: doc.id.value, + accessHash: doc.accessHash.value, + fileReference: doc.fileReference + }) + }) + + await registerUpload({ + projectId, + media, + caption: msg.message, + originGroupId: groupId, + originMessageId: msg.id, + parentType: 0, + publishedBy: registerUser (msg.fromId?.userId?.value) + }) + } + + if (msg.message?.startsWith('KEY-')) { + let projectName = db + .prepare(` + select name + from projects + where id in ( + select project_id from groups where id = :group_id + union + select id from projects where upload_group_id = :group_id) + `) + .pluck(true) + .get({ group_id: groupId }) + + if (projectName) + return await bot.sendMessage(groupId, 'Группа уже используется на проекте ' + projectName) + + const [_, time64, key] = msg.message.substr(3).split('-') + const now = Math.floor(Date.now() / 1000) + const time = Buffer.from(time64, 'base64') + + if (now - 3600 >= time && time >= now) + return await bot.sendMessage(groupId, 'Время действия ключа для привязки истекло') + + const projectId = db + .prepare(`select id from projects where generate_key(id, :time) = :key`) + .pluck(true) + .get({ key: msg.message.trim(), time }) + + if (projectId) { + await attachGroup(groupId, isChannel, projectId) + await onGroupAttach(tgGroupId, isChannel) + } + } + + if (msg.message?.startsWith('/start')) { + // Called by https://t.me/ready_or_not_2025_bot?startgroup= + if (/start@ready_or_not_2025_bot (-|)([\d]+)$/g.test(msg.message)) { + const tgUserId = msg.fromId?.userId?.value + const param = +msg.message.split(' ')[1] + + // Set upload group for customer + if (param < 0) { + const customerId = -param + + db + .prepare(` + update customers + set upload_group_id = :group_id + where id = :customer_id and telegram_user_id = :telegram_user_id + `) + .safeIntegers(true) + .run({ + group_id: groupId, + customer_id: customerId, + telegram_user_id: tgUserId + }) + } + + // Add group to project + if (param > 0) { + const projectId = param + + const customerId = db + .prepare(`select customer_id from projects where id = :project_id`) + .pluck(true) + .get({project_id: projectId}) + + db + .prepare(` + update groups + set project_id = :project_id + where id = :group_id and exists( + select 1 + from customers + where id = :customer_id and telegram_user_id = :telegram_user_id) + `) + .safeIntegers(true) + .run({ + project_id: projectId, + group_id: groupId, + customer_id: customerId, + telegram_user_id: tgUserId + }) + + await reloadGroupUsers(groupId, false) + await onGroupAttach(tgGroupId, isChannel) + } + } + } +} + +async function onNewUserMessage (msg) { + if (msg.message == '/start' && msg.peerId?.className == 'PeerUser') { + const tgUserId = msg.peerId?.userId?.value + const userId = registerUser(tgUserId) + + try { + const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId })) + updateUser(userId, user) + await updateUserPhoto (userId, user) + + const inputPeer = new Api.InputPeerUser({userId: tgUserId, accessHash: user.accessHash.value}) + const resultBtn = await client.sendMessage(inputPeer, { + message: 'Сообщение от бота', + buttons: client.buildReplyMarkup([ + [Button.url('Админка', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=admin')], + [Button.url('Пользователь', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user')] + ]) + }) + } catch (err) { + console.error(msg.className + ', ' + userId + ': ' + err.message) + } + } +} + +async function onUpdatePaticipant (update, isChannel) { + const tgGroupId = isChannel ? update.channelId?.value : update.chatlId?.value + if (!tgGroupId || update.userId?.value != bot.id) + return + + const groupId = await registerGroup (tgGroupId, isChannel) + + const isBan = update.prevParticipant && !update.newParticipant + const isAdd = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant + + if (isBan || isAdd) + await reloadGroupUsers(groupId, isBan) + + if (isBan) { + db + .prepare(`update groups set project_id = null where id = :group_id`) + .run({group_id: groupId}) + } + + const botCanBan = update.newParticipant?.adminRights?.banUsers || 0 + db + .prepare(`update groups set bot_can_ban = :bot_can_ban where id = :group_id`) + .run({group_id: groupId, bot_can_ban: +botCanBan}) +} + +class Bot extends EventEmitter { + + async start (apiId, apiHash, botAuthToken) { + this.id = 7236504417n + + client = new TelegramClient(session, apiId, apiHash, {}) + + client.addEventHandler(async (update) => { + if (update.className == 'UpdateConnectionState') + return + + if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') { + const msg = update?.message + const isChannel = update.className == 'UpdateNewChannelMessage' + + if (!msg) + return + + const result = msg.peerId?.className == 'PeerUser' ? await onNewUserMessage(msg) : + msg.className == 'MessageService' ? await onNewServiceMessage(msg, isChannel) : + await onNewMessage(msg, isChannel) + } + + if (update.className == 'UpdateChatParticipant' || update.className == 'UpdateChannelParticipant') + await onUpdatePaticipant(update, update.className == 'UpdateChannelParticipant') + }) + + await client.start({botAuthToken}) + } + + async uploadDocument(projectId, fileName, mime, data, parentType, parentId, publishedBy) { + const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 }) + + const media = new Api.InputMediaUploadedDocument({ + file, + mimeType: mime, + attributes: [new Api.DocumentAttributeFilename({ fileName })] + }) + + return await registerUpload({ + projectId, + media, + parentType, + parentId, + publishedBy + }) + } + + async downloadDocument(projectId, documentId) { + const document = db + .prepare(` + select file_id, access_hash, '' thumbSize, filename, mime + from documents where id = :document_id and project_id = :project_id + `) + .safeIntegers(true) + .get({project_id: projectId, document_id: documentId}) + + if (!document) + return false + + const result = await client.downloadFile(new Api.InputDocumentFileLocation({ + id: document.file_id, + accessHash: document.access_hash, + fileReference: Buffer.from(document.filename), + thumbSize: '' + }, {})) + + return { + filename: document.filename, + mime: document.mime, + size: result.length, + data: result + } + } + + async reloadGroupUsers(groupId, onlyReset) { + return reloadGroupUsers(groupId, onlyReset) + } + + async sendMessage (groupId, message) { + const group = db + .prepare(`select telegram_id, is_channel from groups where id = :group_id`) + .get({ group_id: groupId}) + + if (!group) + return + + const entity = group.is_channel ? { channelId: group.telegram_id } : { chatId: group.telegram_id } + const inputPeer = await client.getEntity( group.is_channel ? + new Api.InputPeerChannel(entity) : + new Api.InputPeerChat(entity) + ) + + await client.sendMessage(inputPeer, {message}) + + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) + await delay(1000) + } + + async leaveGroup (groupId) { + const group = db + .prepare(`select telegram_id, is_channel from groups where id = :group_id`) + .get({ group_id: groupId}) + + if (!group) + return + + if (group.is_channel) { + const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: group.telegram_id })) + await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer })) + } else { + await client.invoke(new Api.messages.DeleteChatUser({ chatId: group.telegram_id, userId: this.id })) + } + } +} + +const bot = new Bot() + +module.exports = bot + diff --git a/backend/apps/miniapp.js b/backend/apps/miniapp.js new file mode 100644 index 0000000..7a72498 --- /dev/null +++ b/backend/apps/miniapp.js @@ -0,0 +1,643 @@ +const express = require('express') +const multer = require('multer') +const crypto = require('crypto') +const fs = require('fs') +const contentDisposition = require('content-disposition') + +const bot = require('./bot') +const db = require('../include/db') + +const app = express.Router() +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 10_000_000 // 10mb + } +}) + +function hasAccess(project_id, user_id) { + return !!db + .prepare(` + select 1 + from group_users + where user_id = :user_id and + group_id in (select id from groups where project_id = :project_id) and + not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1) and + not exists(select 1 from projects where id = :project_id and is_deleted = 1) + `) + .get({project_id, user_id}) +} + +const sessions = {} + +app.use((req, res, next) => { + if (req.path == '/user/login') + return next() + + const sid = req.query.sid || req.cookies.sid + req.session = sessions[sid] + if (!req.session) + throw Error('ACCESS_DENIED::401') + + res.locals.user_id = req.session.user_id + next() +}) + + +app.post('/user/login', (req, res, next) => { + db + .prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`) + .safeIntegers(true) + .run(res.locals) + + const user_id = db + .prepare(`select id from users where telegram_id = :telegram_id`) + .safeIntegers(true) + .pluck(true) + .get(res.locals) + + const sid = crypto.randomBytes(64).toString('hex') + req.session = sessions[sid] = {sid, user_id} + res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/`]) + res.locals.user_id = user_id + + res.status(200).json({success: true}) +}) + +app.get('/project', (req, res, next) => { + const where = req.query.id ? ' and p.id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select p.id, p.name, p.description, p.logo, + c.name customer_name, c.upload_group_id <> 0 has_upload + from projects p + inner join customers c on p.customer_id = c.id + where p.id in ( + select project_id + from groups + where id in (select group_id from group_users where user_id = :user_id) + ) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1) + ${where} and is_deleted <> 1 + `) + .all(res.locals) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project?id=${req.params.pid}`) +}) + +app.use('/project/:pid(\\d+)/*', (req, res, next) => { + res.locals.project_id = parseInt(req.params.pid) + + if (!hasAccess(res.locals.project_id, res.locals.user_id)) + throw Error('ACCESS_DENIED::401') + + const row = db + .prepare('select customer_id from projects where id = :project_id') + .get(res.locals) + + res.locals.customer_id = row.customer_id + + next() +}) + +app.get('/project/:pid(\\d+)/user', (req, res, next) => { + const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : '' + + const users = db + .prepare(` + with actuals (user_id) as ( + select distinct user_id + from group_users + where group_id in (select id from groups where project_id = :project_id) + and group_id in (select group_id from group_users where user_id = :user_id) + ), + contributors (user_id) as ( + select created_by from tasks where project_id = :project_id + union + select assigned_to from tasks where project_id = :project_id + union + select created_by from meetings where project_id = :project_id + union + select published_by from documents where project_id = :project_id + ), + members (user_id, is_leave) as ( + select user_id, 0 is_leave from actuals + union all + select user_id, 1 is_leave from contributors where user_id not in (select user_id from actuals) + ) + select u.id, + u.telegram_id, + u.username, + u.firstname, + u.lastname, + u.photo, + u.json_phone_projects, + ud.fullname, + ud.role, + ud.department, + ud.is_blocked, + (select company_id + from company_users + where user_id = u.id and + company_id in (select id from companies where project_id = :project_id)) company_id, + m.is_leave + from users u + inner join members m on u.id = m.user_id + left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id + where 1 = 1 ${where} + `) + .all(res.locals) + + const companies = db + .prepare('select id, name, email, phone, site, description from companies where project_id = :project_id') + .all(res.locals) + .reduce((companies, row) => { + companies[row.id] = row + return companies + }, {}) + + const mappings = {} + const company_id = users.find(m => m.id == res.locals.user_id).company_id + if (company_id) { + res.locals.company_id = company_id + + db + .prepare('select show_as_id, show_to_id from company_mappings where project_id = :project_id and company_id = :company_id') + .all(res.locals) + .forEach(row => mappings[row.show_to_id] = row.show_to_id) + } + + users.forEach(m => { + m.company = companies[mappings[m.company_id] || m.company_id] + delete m.company_id + }) + + users.forEach(m => { + const isHide = JSON.parse(m.json_phone_projects || []).indexOf(res.locals.project_id) == -1 + if (isHide) + delete m.phone + delete m.json_phone_projects + }) + + if (where && users.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? users[0] : users}) +}) + +app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => { + const groupIds = db + .prepare(`select id from groups where project_id = :project_id`) + .all(res.locals) + .map(e => e.id) + + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + + for (const groupId of groupIds) { + await bot.reloadGroupUsers(groupId) + await sleep(1000) + } + + res.status(200).json({success: true}) +}) + +app.get('/project/:pid(\\d+)/group', (req, res, next) => { + const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, telegram_id + from groups + where project_id = :project_id and id in (select group_id from group_users where user_id = :user_id) + ${where} + `) + .all(res.locals) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)/group/:gid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.gid}`) +}) + +// TASK +app.get('/project/:pid(\\d+)/task', (req, res, next) => { + const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date, + (select json_group_array(user_id) from task_users where task_id = t.id) observers, + (select json_group_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments + from tasks t + where project_id = :project_id and + (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id)) + ${where} + `) + .all(res.locals) + + rows.forEach(row => { + row.observers = JSON.parse(row.observers) + row.attachments = JSON.parse(row.attachments) + }) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/task?id=${req.params.tid}`) +}) + +app.post('/project/:pid(\\d+)/task', (req, res, next) => { + res.locals.name = req.body?.name + res.locals.status = parseInt(req.body?.status) + res.locals.priority = parseInt(req.body?.priority) + res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined + res.locals.create_date = Math.floor(Date.now() / 1000) + res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined + + if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to)) + throw Error('INCORRECT_ASSIGNED_TO::400') + + const id = db + .prepare(` + insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date) + values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date) + returning id + `) + .pluck(true) + .get(res.locals) + + res.status(200).json({success: true, data: id}) +}) + +app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => { + res.locals.task_id = req.params.tid + + const task = db + .prepare(` + select created_by, assigned_to + from tasks + where id = :task_id and project_id = :project_id + and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = :task_id and user_id = :user_id)) + `) + .get(res.locals) + + if (!task) + throw Error('NOT_FOUND::404') + + res.locals.is_author = task.created_by == res.locals.user_id + res.locals.is_assigned = task.assigned_to == res.locals.user_id + + next() +}) + +app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { + if (!res.locals.is_author && !res.locals.is_assigned) + throw Error('ACCESS_DENIED::401') + + res.locals.id = res.locals.task_id + res.locals.name = req.body?.name + res.locals.status = parseInt(req.body?.status) + res.locals.priority = parseInt(req.body?.priority) + res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined + res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined + + const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent'] : ['status', 'time_spent'] + const info = db + .prepareUpdate('tasks', columns, res.locals, ['id', 'project_id']) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => { + if (!res.locals.is_author) + throw Error('ACCESS_DENIED::401') + + const info = db + .prepare(`delete from tasks where id = :task_id and project_id = :project_id and created_by = :user_id`) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => { + if (!res.locals.is_author && !res.locals.is_assigned) + throw Error('ACCESS_DENIED::401') + + const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : [] + + // Проверка, что выбранные пользователи имеют доступ к проекту + let rows = db + .prepare(` + select user_id + from group_users + where group_id in (select id from groups where project_id = :project_id) + `) + .pluck(true) + .all(res.locals) + + if (user_ids.some(user_id => !rows.contains(user_id))) + throw Error('INACCESSABLE_USER::400') + + res.locals.json_ids = JSON.stringify(user_ids) + + db + .prepare(` + delete from task_users where task_id = :task_id; + insert into task_users (task_id, user_id) select :task_id, value from json_each(:json_ids) + `) + .run(res.locals) + + res.status(200).json({success: true}) +}) + +// MEETINGS +app.get('/project/:pid(\\d+)/meeting', (req, res, next) => { + const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : '' + + const rows = db + .prepare(` + select id, name, description, created_by, meet_date, + (select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants, + (select json_group_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments + from meetings m + where project_id = :project_id and + (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id)) + ${where} + `) + .all(res.locals) + + rows.forEach(row => { + row.participants = JSON.parse(row.participants) + row.attachments = JSON.parse(row.attachments) + }) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: where ? rows[0] : rows}) +}) + +app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { + res.redirect(req.baseUrl + `/project/${req.params.pid}/meeting?id=${req.params.mid}`) +}) + +app.post('/project/:pid(\\d+)/meeting', (req, res, next) => { + res.locals.name = req.body?.name + res.locals.description = req.body?.description + res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined + + const id = db + .prepare(` + insert into meetings (project_id, name, description, created_by, meet_date) + values (:project_id, :name, :description, :user_id, :meet_date) + returning id + `) + .pluck(true) + .get(res.locals) + + res.status(200).json({success: true, data: id}) +}) + +app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => { + res.locals.meeting_id = req.params.mid + + const meeting = db + .prepare(` + select created_by + from meetings + where id = :meeting_id and project_id = :project_id + and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = :meeting_id and user_id = :user_id)) + `) + .get(res.locals) + + if (!meeting) + throw Error('NOT_FOUND::404') + + res.locals.is_author = meeting.created_by == res.locals.user_id + + next() +}) + +app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { + if (!res.locals.is_author) + throw Error('ACCESS_DENIED::401') + + res.locals.id = res.locals.meeting_id + res.locals.name = req.body?.name + res.locals.description = req.body?.description + res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined + + const info = db + .prepareUpdate('meetings', ['name', 'description', 'meet_date'], res.locals, ['id', 'project_id']) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => { + if (!res.locals.is_author) + throw Error('ACCESS_DENIED::401') + + const info = db + .prepare(`delete from meetings where id = :meeting_id and project_id = :project_id and created_by = :user_id`) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true}) +}) + +app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next) => { + if (!res.locals.is_author) + throw Error('ACCESS_DENIED::401') + + const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : [] + + // Проверка, что выбранные пользователи имеют доступ к проекту + let rows = db + .prepare(` + select user_id + from group_users + where group_id in (select id from groups where project_id = :project_id) + `) + .pluck(true) // .raw? + .all(res.locals) + + if (user_ids.some(user_id => rows.indexOf(user_id)) == -1) + throw Error('INACCESSABLE_USER::400') + + db + .prepare(`delete from meeting_users where meeting_id = :meeting_id`) + .run(res.locals) + + res.locals.json_ids = JSON.stringify(user_ids) + db + .prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`) + .run(res.locals) + + res.status(200).json({success: true}) +}) + +// DOCUMENTS +app.get('/project/:pid(\\d+)/document', (req, res, next) => { + const ids = String(req.query.id).split(',').map(e => parseInt(e)).filter(e => e > 0) + const where = ids.length > 0 ? ' and id in (' + ids.join(', ') + ')' : '' + + // Документы + // 1. Из групп, которые есть в проекте и в которых участвует пользователь + // 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель + // 3. Из встреч на проекте, где пользователь создатель или участник + // To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId + const rows = db + .prepare(` + select id, origin_group_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type + from documents d + where project_id = :project_id ${where} and ( + origin_group_id in (select group_id from group_users where user_id = :user_id) + or + parent_type = 1 and parent_id in ( + select id + from tasks t + where project_id = :project_id and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id)) + ) + or + parent_type = 2 and parent_id in ( + select id + from meetings m + where project_id = :project_id and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id)) + ) + ) + `) + .all(res.locals) + + if (where && rows.length == 0) + throw Error('NOT_FOUND::404') + + res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows}) +}) + +app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => { + const parentType = req.params.type == 'task' ? 1 : 2 + const parentId = req.params.id + + const ids = [] + for (const file of req.files) { + const id = await bot.uploadDocument(res.locals.project_id, file.originalname, file.mimetype, file.buffer, parentType, parentId, res.locals.user_id) + ids.push(id) + } + + res.redirect(req.baseUrl + `/project/${req.params.pid}/document?id=` + ids.join(',')) +}) + +app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => { + res.locals.document_id = req.params.did + + const doc = db + .prepare(`select * from documents where id = :document_id and project_id = :project_id`) + .get(res.locals) + + if (!doc) + throw Error('NOT_FOUND::404') + + if (doc.parent_type == 0) { + res.locals.group_id = doc.group_id + const row = db + .prepare(`select 1 from group_users where group_id = :group_id and user_id = :user_id`) + .get(res.locals) + + if (row) { + res.locals.can_download = true + } + } else { + res.locals.parent_id = doc.parent_id + const parent = doc.parent_type == 1 ? 'task' : 'meeting' + + const row = db + .prepare(` + select 1 + from ${parent}s + where id = :parent_id and project_id = :project_id or + exists(select 1 from ${parent}_users where ${parent}_id = :parent_id and user_id = :user_id) + `) + .get(res.locals) + + if (row) { + res.locals.can_download = true + res.locals.can_delete = doc.published_by == res.locals.user_id + } + } + + next() +}) + +app.get('/project/:pid(\\d+)/document/:did(\\d+)', async (req, res, next) => { + if (!res.locals.can_download) + throw Error('NOT_FOUND::404') + + const file = await bot.downloadDocument(res.locals.project_id, res.locals.document_id) + res.writeHead(200, { + 'Content-Length': file.size, + 'Content-Type': file.mime, + 'Content-Disposition': contentDisposition(file.filename) + }) + + res.end(file.data) +}) + +app.delete('/project/:pid(\\d+)/document/:id(\\d+)', (req, res, next) => { + if (!res.locals.can_delete) + throw Error('NOT_FOUND::404') + + const doc = db + .prepare(`delete from documents where id = :id and project_id = :project_id`) + .run(res.locals) + + res.status(200).json({success: true}) +}) + +app.get('/settings', (req, res, next) => { + const row = db + .prepare(`select coalesce(json_settings, '{}') from users where id = :user_id`) + .pluck(true) + .get(res.locals) + + res.status(200).json({success: true, data: JSON.parse(row)}) +}) + +app.put('/settings', (req, res, next) => { + res.locals.json_settings = JSON.stringify(req.body || {}) + + const row = db + .prepare(`update users set json_settings = :json_settings where id = :user_id`) + .run(res.locals) + + res.status(200).json({success: true}) +}) + +module.exports = app \ No newline at end of file diff --git a/backend/data/db.sqlite b/backend/data/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..438da8f3ada2c774a2fa3df0cc38a9223d1b6020 GIT binary patch literal 94208 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 literal 0 HcmV?d00001 diff --git a/backend/data/db.sqlite-shm b/backend/data/db.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..3d2446dc3793ec894aaf968434e185afa8691acb GIT binary patch literal 32768 zcmeI*Jq`gu6bJBGpYc)XIDw9Xa1f=|5%v}oYNd0C6NpNpR4RBQB3kYG{gXFucQe`K z{SGiQI!{tgGpa~w(2hQgDi7<;@jl$%7OT^)Q_lAN%Y3}NPDelPk6*pgiuLi8_vcTm z$J2LpGtT}e+~mLa zo_liccaurx&*Av4V-_RN8+Hg;BC6a=%e(#=r*!eb#0+L@ci3{giNuTT_;&jke^tYw z`@KBA0kQcTNq#SXnBT?U!T+4!!QaE*NVNe41V8`;KmY_l00ck)1V8`;KmY_lU{VEm zY|@!X$&%+N6)K(9!i-MZ(sHy4jTQ}>vq(omsZi+BRcTt1$G{ckSxU7`m9Ee-I84%( z(8;tJTACS2P9xGZSvhKL2#=s@TEPy{nMxv`nh~XYv9T z!o?!inZ#De3ryc`o5HjZTIGC4ThtqITPh8=FiQP1lj-Vu@6O2!cSN z_=Ze3XfvciL5UGjZ%L$K!FUHkG(4{pzQfUkBXDW;w@CHiH-`wb!u@u zBkid;`Kfr7POZarPEz&dY7G@%!SMoRW)AgSRH1rou297+#gB1#aX?xWg+kCm98YSQ z@XXqSklzUY*1BzCTXGdbt?Lo8>zml-UW*XF1fiC0wM-*>+!^YnK}$_V=x!rIJUr)? zRS*8p+xYR0P8>UNEuM3(F8CM+e4bhOmWDEs0=Xb5QXx4K;m8|#Au;kXT}DBOMiqj( zBk>=}q?5R?7z{d-&El}xEH;~C#^Z9#%+1(rE}v`8v#{V>aJZHNOA7&xEvQBad?0lc zozAl0usL|^e_2dlBLN%LqFRy=BAS381%&B1a>UnWjbQMrGB*GQOpP!%!NLTt4u~lHCaG|LWnUlBy0VzQK z=)s;#6?Qdd*soC^KdHaEZikR}k+39IX zbfso(!o%{Azlyt^MRix0eUcP~w{@%`nQ+Q=b1K>6_4=ucoBN|nma{qnhl?6?p&!hB zu;l3VE7c}+IPceMZl3$E&Ahlf;6{XgLrIf^k!vaaoGes!JDJeTvYX~5D}HO9Q&?D# zG}Xgx$xy;T@YOf%FP|g_UMt|rl8vDkH;_#})2*6vk9Pj6HP>hfs1t<>r@}p^SodxfqJ*rBvPIm7#o7L++Wi|E>#84Y z2*{}S$nud#yT_PNi(hly+WN}!auMHc`reB2)gP%ZTnhcC;}1788~W$?C-ytNUAyVv z+xhmlMmoEzMiuTKghvL?Xsp;ZByj&GL)#&~f7$x?MDk9!cl$b?b+#8>zo6@A?_Hla z4jq_&Y#`{j?b8lO)3@~)Bb4<_-@9+LEsOk4a((w2l~LV#gT5IuTl1Dl56T$)d-~y%sI`rZNJ&;-v9Cbp^}^1 zMOEAW+$?pAu<><{AAWCTyQ3ufcAULu=kM90;T{*ZmX;kXka7F=bV^^zBnHLz_2<>M zQXNAARc-Ns4I;Z+a@FoU=|J+nq&UI%8WZvxJ?t44>NAl4e#sywN&5Fzui!^J6BsLt zIThz4+Z?{FJoC`O>o2P|zt;5O&*ta4_R6;gaP1-j?<5u)hl}pSo*c1~EGbfmLz`DG zvo~&Dwn-^@^LRyvr%|@DZAYow zD=n)J6)BfW&N_BJ^e%23JyzhASu@+P-MGnikUx8^w7t);K1W}6t-rS7VE4X#r7g>& zsxIL7lZ^*~KMI-CsJ9mNfj6&ZjiH(!oFpDP=NhO=(J1mYIn+lZ-p$3!lX(XT*O5iH z4_KsA$pRA3J;*oUc?Tch6(}G80w4eaAOHd&00JNY0w4eaAOHd{rvQhf)2JLd8e)@l zf=YUmERxQlas~K=s1(4Hyg+SDPiejFuiuT07toK57tp_)rwzRZ0T2KI5C8!X_&*Zx zcsxFUX=R1anfg(L`tCC '' 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_user_id integer, + plan integer, + json_balance text default '{}', + activation_key text, + is_active integer default 0, + json_company text default '{}', + upload_group_id integer, + json_backup_server text default '{}', + json_backup_params 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_deleted integer default 0 +) strict; + +create table if not exists groups ( + id integer primary key autoincrement, + project_id integer references projects(id) on delete cascade, + name text, + telegram_id integer, + access_hash integer, + is_channel integer check(is_channel in (0, 1)) default 0, + bot_can_ban integer default 0, + user_count integer, + last_update_time integer +); +create unique index if not exists idx_groups_telegram_id on groups (telegram_id); + +create table if not exists users ( + id integer primary key autoincrement, + telegram_id integer, + access_hash integer, + firstname text, + lastname text, + username text, + photo_id integer, + photo text, + language_code text, + phone text, + json_phone_projects text default '[]', + 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, + role text, + department text, + is_blocked integer check(is_blocked in (0, 1)) default 0, + primary key (user_id, project_id) +) strict; + +create table if not exists tasks ( + id integer primary key autoincrement, + project_id integer references projects(id) on delete cascade, + name text not null check(trim(name) <> '' and length(name) < 4096), + created_by integer references users(id) on delete set null, + assigned_to integer references users(id) on delete set null, + closed_by integer references users(id) on delete set null, + priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0, + status integer check(status >= 1 and status <= 10) default 1, + time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 44640), -- one month + create_date integer, + plan_date integer, + close_date integer +) strict; + +create table if not exists meetings ( + id integer primary key autoincrement, + project_id integer references projects(id) on delete cascade, + name text not null check(trim(name) <> '' and length(name) < 4096), + description text check(description is null or length(description) < 4096), + created_by integer references users(id) on delete set null, + meet_date integer +) strict; + +create table if not exists documents ( + id integer primary key autoincrement, + project_id integer references projects(id) on delete cascade, + origin_group_id integer references groups(id) on delete set null, + origin_message_id integer, + group_id integer references groups(id) on delete set null, + message_id integer, + file_id integer, + access_hash integer, + filename text not null check(length(filename) < 256), + mime text check(mime is null or length(mime) < 128), + caption text check(caption is null or length(caption) < 4096), + size integer, + published_by integer references users(id) on delete set null, + parent_type integer check(parent_type in (0, 1, 2)) default 0, + parent_id integer, + backup_state integer default 0 +) strict; + +create table if not exists companies ( + id integer primary key autoincrement, + project_id integer references projects(id) on delete cascade, + name text not null check(length(name) < 4096), + email text check(email is null or length(email) < 128), + phone text check(phone is null or length(phone) < 128), + site text check(site is null or length(site) < 128), + description text check(description is null or length(description) < 4096), + logo text +) strict; + +create table if not exists company_mappings ( + project_id integer references projects(id) on delete cascade, + company_id integer references companies(id) on delete cascade, + show_as_id integer references companies(id) on delete cascade, + show_to_id integer references companies(id) on delete cascade +) strict; + +create table if not exists task_users ( + task_id integer references tasks(id) on delete cascade, + user_id integer references users(id) on delete cascade, + primary key (task_id, user_id) +) without rowid; + +create table if not exists meeting_users ( + meeting_id integer references meetings(id) on delete cascade, + user_id integer references users(id) on delete cascade, + primary key (meeting_id, user_id) +) without rowid; + +create table if not exists company_users ( + company_id integer references companies(id) on delete cascade, + user_id integer references users(id) on delete cascade, + primary key (company_id, user_id) +) without rowid; + +create table if not exists group_users ( + group_id integer references groups(id) on delete cascade, + user_id integer references users(id) on delete cascade, + primary key (group_id, user_id) +) without rowid; + +pragma foreign_keys = on; + + +create trigger if not exists trg_groups_update after update +on groups +when NEW.project_id is null +begin + delete from group_users where group_id = NEW.id; +end; + + + + + + diff --git a/backend/include/db.js b/backend/include/db.js new file mode 100644 index 0000000..7d65f17 --- /dev/null +++ b/backend/include/db.js @@ -0,0 +1,57 @@ +const fs = require('fs') +const crypto = require('crypto') +const sqlite3 = require('better-sqlite3') + +const db = sqlite3(`./data/db.sqlite`) +db.pragma('journal_mode = WAL') + +db.exec(fs.readFileSync('./data/init.sql', 'utf8')) + +/* +db.exec(`attach database './data/backup.sqlite' as backup`) + +db.function('backup', (tblname, ...values) => { + db + .prepare(`insert into backup.${tblname} select ` + values.map(e => '?').join(', ')) + .run(values) +}) + +const backupQuery = db + .prepare(` + select group_concat(tbl || char(13) || trg, char(13) || char(13)) from ( + select 'create table if not exists backup.' || t.name || ' (' || group_concat(c.name || ' ' || c.type, ', ') || ', time integer);' tbl, + 'create trigger if not exists trg_' || t.name || '_delete after delete on ' || t.name || ' for each row begin ' || + ' select backup (' || t.name || ',' || group_concat('OLD.' || c.name, ', ') || ', strftime(''%s'', ''now'')); end;' trg + from sqlite_master t left join pragma_table_xinfo c on t.tbl_name = c.arg and c.schema = 'main' + where t.sql is not null and t.type = 'table' and t.name <> 'sqlite_sequence' + group by t.type, t.name order by t.type, t.name) + `) + .pluck(true) + .get() +db.exec(backupQuery) +*/ + +db.function('generate_key', (id, time) => { + return [ + 'KEY', + Buffer.from(time + '').toString('base64'), + crypto.createHash('md5').update(`sa${time}-${id}lt`).digest('hex') + ].join('-') +}) + +process.on('exit', () => db.close()) +process.on('SIGHUP', () => process.exit(128 + 1)) +process.on('SIGINT', () => process.exit(128 + 2)) +process.on('SIGTERM', () => process.exit(128 + 15)) + +db.prepareUpdate = function (table, columns, data, where) { + const dataColumns = columns.filter(col => col in data) + if (dataColumns.length == 0) + throw Error('SQLite Error: No data to update') + + return db.prepare(`update "${table}" ` + + `set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') + + ` where ` + where.map(col => `"${col}" = :${col}`).join(' and ')) +} + +module.exports = db \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..d1bef8c --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1990 @@ +{ + "name": "telegram-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "telegram-bot", + "version": "1.0.0", + "license": "ISC", + "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" + } + }, + "node_modules/@cryptography/aes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz", + "integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==", + "license": "GPL-3.0-or-later" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", + "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", + "license": "MIT", + "dependencies": { + "write-file-atomic": "^1.1.4" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/real-cancellable-promise": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.1.tgz", + "integrity": "sha512-JwhiWJTMMyzFYfpKsiSb8CyQktCi1MZ8ZBn3wXvq28qXDh8Y5dM7RYzgW3r6SV22JTEcof8pRsvDp4GxLmGIxg==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/store2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", + "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/telegram": { + "version": "2.26.22", + "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz", + "integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==", + "license": "MIT", + "dependencies": { + "@cryptography/aes": "^0.1.1", + "async-mutex": "^0.3.0", + "big-integer": "^1.6.48", + "buffer": "^6.0.3", + "htmlparser2": "^6.1.0", + "mime": "^3.0.0", + "node-localstorage": "^2.2.1", + "pako": "^2.0.3", + "path-browserify": "^1.0.1", + "real-cancellable-promise": "^1.1.1", + "socks": "^2.6.2", + "store2": "^2.13.0", + "ts-custom-error": "^3.2.0", + "websocket": "^1.0.34" + }, + "optionalDependencies": { + "bufferutil": "^4.0.3", + "utf-8-validate": "^5.0.5" + } + }, + "node_modules/telegram/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/telegram/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=0.10.32" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..8c38a14 --- /dev/null +++ b/backend/package.json @@ -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" + } +} diff --git a/backend/public/index.html b/backend/public/index.html new file mode 100644 index 0000000..254ff81 --- /dev/null +++ b/backend/public/index.html @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/public/miniapp.html b/backend/public/miniapp.html new file mode 100644 index 0000000..7c786fb --- /dev/null +++ b/backend/public/miniapp.html @@ -0,0 +1,65 @@ + + + + +
+ + +
+ +
+
ПРОЕКТЫ
+
EMPTY
+
+ +
+
УЧАСТНИКИ
+
+
+ + + + + + \ No newline at end of file diff --git a/i18n-2.xlsm b/i18n-2.xlsm index 5bb50e0ae824262f58cdb35906cb498a5e2988c5..8e5cbbb4dae7efb63923bd9623a5dbf89fad168d 100644 GIT binary patch delta 34422 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 diff --git a/index.html b/index.html index faed010..e7797b8 100644 --- a/index.html +++ b/index.html @@ -12,9 +12,8 @@ - - + @@ -23,7 +22,6 @@ - diff --git a/package-lock.json b/package-lock.json index 1fb1b62..cbf24a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,9 @@ "@eslint/js": "^9.14.0", "@intlify/unplugin-vue-i18n": "^2.0.0", "@quasar/app-vite": "^2.0.0", + "@twa-dev/types": "^8.0.2", "@types/node": "^20.17.30", + "@types/telegram-web-app": "^7.10.1", "@vue/devtools": "^7.7.2", "@vue/eslint-config-typescript": "^14.1.3", "autoprefixer": "^10.4.2", @@ -2069,6 +2071,13 @@ "node": ">=14.16" } }, + "node_modules/@twa-dev/types": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@twa-dev/types/-/types-8.0.2.tgz", + "integrity": "sha512-ICQ6n4NaUPPzV3/GzflVQS6Nnu5QX2vr9OlOG8ZkFf3rSJXzRKazrLAbZlVhCPPWkIW3MMuELPsE6tByrA49qA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2316,6 +2325,13 @@ "@types/send": "*" } }, + "node_modules/@types/telegram-web-app": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@types/telegram-web-app/-/telegram-web-app-7.10.1.tgz", + "integrity": "sha512-GKL659G6lnHRAt3Dt7L1Fa0dwvc+ZZQtJcYrJVJGwjvsbi/Rcp1qKs9pFwb34/KC04+J+TlQj4D7yKo10sBSEw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", diff --git a/package.json b/package.json index 9066f85..c3c4c86 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "@eslint/js": "^9.14.0", "@intlify/unplugin-vue-i18n": "^2.0.0", "@quasar/app-vite": "^2.0.0", + "@twa-dev/types": "^8.0.2", "@types/node": "^20.17.30", + "@types/telegram-web-app": "^7.10.1", "@vue/devtools": "^7.7.2", "@vue/eslint-config-typescript": "^14.1.3", "autoprefixer": "^10.4.2", diff --git a/quasar.config.ts b/quasar.config.ts index cb37859..cfae256 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from '#q-app/wrappers' import { fileURLToPath } from 'node:url' -import path from 'node:path' export default defineConfig((ctx) => { return { @@ -16,7 +15,9 @@ export default defineConfig((ctx) => { boot: [ 'i18n', 'axios', - 'global-components' + 'auth-init', + 'global-components', + 'telegram-boot' ], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css @@ -52,6 +53,8 @@ export default defineConfig((ctx) => { }, vueRouterMode: 'history', // available values: 'hash', 'history' + vueDevtools: true, // Должно быть true + devtool: 'source-map', // Для лучшей отладки // vueRouterBase, // vueDevtools, // vueOptionsAPI: false, @@ -101,13 +104,22 @@ export default defineConfig((ctx) => { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver devServer: { vueDevtools: true, + port: 9000, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + } + } // https: true, - open: true // opens browser window automatically + // open: true // opens browser window automatically }, // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework framework: { - config: {}, + config: { + }, + // iconSet: 'material-icons', // Quasar icon set // lang: 'en-US', // Quasar language pack @@ -120,7 +132,7 @@ export default defineConfig((ctx) => { // directives: [], // Quasar plugins - plugins: [] + plugins: [ 'Notify' ] }, // animations: 'all', // --- includes all animations diff --git a/src/App.vue b/src/App.vue index 0711768..2df1ee8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,5 +3,24 @@ diff --git a/src/boot/auth-init.ts b/src/boot/auth-init.ts new file mode 100644 index 0000000..8ec1dd9 --- /dev/null +++ b/src/boot/auth-init.ts @@ -0,0 +1,6 @@ +import { useAuthStore } from 'stores/auth' + +export default async () => { + const authStore = useAuthStore() + await authStore.initialize() +} \ No newline at end of file diff --git a/src/boot/axios.ts b/src/boot/axios.ts index bf9b3ed..2272de7 100644 --- a/src/boot/axios.ts +++ b/src/boot/axios.ts @@ -1,5 +1,6 @@ -import { defineBoot } from '#q-app/wrappers'; -import axios, { type AxiosInstance } from 'axios'; +import { defineBoot } from '#q-app/wrappers' +import axios, { type AxiosInstance } from 'axios' +import { useAuthStore } from 'src/stores/auth' declare module 'vue' { interface ComponentCustomProperties { @@ -14,16 +15,32 @@ declare module 'vue' { // good idea to move this instance creation inside of the // "export default () => {}" function below (which runs individually // for each client) -const api = axios.create({ baseURL: 'https://api.example.com' }); +const api = axios.create({ + baseURL: '/', + withCredentials: true // Важно для работы с cookies +}) + +api.interceptors.response.use( + response => response, + async error => { + if (error.response?.status === 401) { + const authStore = useAuthStore() + await authStore.logout() + } + console.error(error) + return Promise.reject(new Error()) + } + +) export default defineBoot(({ app }) => { // for use inside Vue files (Options API) through this.$axios and this.$api - app.config.globalProperties.$axios = axios; + app.config.globalProperties.$axios = axios // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) // so you won't necessarily have to import axios in each vue file - app.config.globalProperties.$api = api; + app.config.globalProperties.$api = api // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) // so you can easily perform requests against your app's API }); diff --git a/src/boot/helpers.ts b/src/boot/helpers.ts index ef16bae..286e65a 100644 --- a/src/boot/helpers.ts +++ b/src/boot/helpers.ts @@ -1,8 +1,34 @@ -export function isObjEqual(obj1: Type, obj2: Type): boolean { - return obj1 && obj2 && Object.keys(obj1).length === Object.keys(obj2).length && - (Object.keys(obj1) as (keyof typeof obj1)[]).every(key => { - return Object.prototype.hasOwnProperty.call(obj2, key) && obj1[key] === obj2[key] - }) +export function isObjEqual(a: object, b: object): boolean { + // Сравнение примитивов и null/undefined + if (a === b) return true + if (!a || !b) return false + if (Object.keys(a).length !== Object.keys(b).length) return false + + // Получаем все уникальные ключи из обоих объектов + const allKeys = new Set([ + ...Object.keys(a), + ...Object.keys(b) + ]) + + // Проверяем каждое свойство + for (const key of allKeys) { + const valA = a[key as keyof typeof a] + const valB = b[key as keyof typeof b] + + // Если одно из значений undefined - объекты разные + if (valA === undefined || valB === undefined) return false + + // Рекурсивное сравнение для вложенных объектов + if (typeof valA === 'object' && typeof valB === 'object') { + if (!isObjEqual(valA, valB)) return false + } + // Сравнение примитивов + else if (!Object.is(valA, valB)) { + return false + } + } + + return true } export function parseIntString (s: string | string[] | undefined) :number | null { diff --git a/src/boot/telegram-boot.ts b/src/boot/telegram-boot.ts new file mode 100644 index 0000000..960a4ea --- /dev/null +++ b/src/boot/telegram-boot.ts @@ -0,0 +1,18 @@ +import type { BootParams } from '@quasar/app' + +export default ({ app }: BootParams) => { + + // Инициализация Telegram WebApp + if (window.Telegram?.WebApp) { + const webApp = window.Telegram.WebApp + // Помечаем приложение как готовое + webApp.ready() + // window.Telegram.WebApp.requestFullscreen() + // Опционально: сохраняем объект в Vue-приложение для глобального доступа + webApp.SettingsButton.isVisible = true + // webApp.BackButton.isVisible = true + app.config.globalProperties.$tg = webApp + // Для TypeScript: объявляем тип для инжекции + app.provide('tg', webApp) + } +} \ No newline at end of file diff --git a/src/components/admin/accountHelper.vue b/src/components/admin/accountHelper.vue index 5856783..36e63da 100644 --- a/src/components/admin/accountHelper.vue +++ b/src/components/admin/accountHelper.vue @@ -15,9 +15,10 @@ - +
{{$t('account_helper__code_error')}}
@@ -28,10 +29,11 @@ :title="$t('account_helper__confirm_email')" :done="step > 2" > - {{$t('account_helper__confirm_email_messege')}} +
{{$t('account_helper__confirm_email_message')}}
@@ -47,7 +49,8 @@ @@ -73,7 +76,7 @@ }>() const step = ref(1) - const login = ref(props.email ? props.email : '') + const login = ref(props.email || '') const code = ref('') const password = ref('') diff --git a/src/components/admin/companyInfoBlock.vue b/src/components/admin/companyInfoBlock.vue index 3118c66..ebb4812 100644 --- a/src/components/admin/companyInfoBlock.vue +++ b/src/components/admin/companyInfoBlock.vue @@ -10,6 +10,7 @@ filled class = "q-mt-md w100" :label = "input.label ? $t(input.label) : void 0" + :rules="input.val === 'name' ? [rules[input.val]] : []" > diff --git a/src/css/app.scss b/src/css/app.scss index 0617b56..77bcac9 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -22,6 +22,13 @@ $base-height: 100; :root { --body-width: 600px; --top-raduis: 12px; + --logo-color-bg-white: grey; + --dynamic-font-size: 16px; +} + +#q-app { + padding-bottom: env(safe-area-inset-bottom, 0); + padding-bottom: constant(safe-area-inset-bottom, 0); // Для старых iOS } body { diff --git a/src/css/quasar.variables.scss b/src/css/quasar.variables.scss index a157345..7e49afe 100644 --- a/src/css/quasar.variables.scss +++ b/src/css/quasar.variables.scss @@ -26,3 +26,4 @@ $warning : #F2C037; $lightgrey : #DCDCDC; +$body-font-size: var(--dynamic-font-size) \ No newline at end of file diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts index fd61eac..fda8ff3 100644 --- a/src/i18n/en-US/index.ts +++ b/src/i18n/en-US/index.ts @@ -1 +1 @@ -export default { EN: 'EN', RU: 'RU', continue: 'Continue', back: 'Back', month: 'month', months: 'months', login__email: 'E-mail', login__password: 'Password', login__forgot_password: 'Forgot Password?', login__sign_in: 'Log in', login__incorrect_login_data: 'User data not found. Edit your auth details before continuing', login__or_continue_as: 'or continue as', login__terms_of_use: 'Terms of use', login__accept_terms_of_use: 'I accept the', login__register: 'Create account', login__registration_message_ok: 'We sent message with instructions to your email', login__registration_message_error: 'Error', login__licensing_agreement: 'Licensing agreement', login__have_account: 'Already have an accont?', login__forgot_password_message: 'Enter your e-mail to recover your password. We will send instructions to the specified email address. If you havent received the email, check the Spam folder.', login__forgot_password_message_ok: 'We sent message with instructions to your email', login__forgot_password_message_error: 'Error', user__logout: 'Logout', projects__projects: 'Projects', projects__show_archive: 'Show archive', projects__hide_archive: 'Hide archive', projects__restore_archive_warning: 'Attention!', projects__restore_archive_warning_message: 'To restore a project from an archive, you must manually attach chats to it.', project__chats: 'Chats', project__persons: 'Persons', project__companies: 'Companies', project__edit: 'Edit', project__backup: 'Backup', project__archive: 'Archive', project__delete: 'Delete', project_chats__search: 'Search', project_chats__send_chat: 'Request for attach chat', project_chats__send_chat_description: 'Provide instructions to the chat admin', project_chats__attach_chat: 'Attach chat', project_chats__attach_chat_description: 'Requires chat administrator privileges', project_chat__delete_warning: 'Warning!', project_chat__delete_warning_message: 'Chat tracking will be discontinued. If necessary, the cat can be attached again.', project_card__add_project: 'Add project', project_card__project_name: 'Name', project_card__project_description: 'Desription', project_card__btn_accept: 'Accept', project_card__btn_back: 'Back', forgot_password__password_recovery: 'Password recovery', forgot_password__enter_email: 'Enter account e-mail', forgot_password__email: 'E-mail', forgot_password__confirm_email: 'Confirm e-mail', forgot_password__confirm_email_messege: 'Enter the Code from e-mail to continue recover your password. If you haven\'t received an e-mail with the Code, check the Spam folder.', forgot_password__code: 'Code', forgot_password__create_new_password: 'Set new password', forgot_password__password: 'Password', forgot_password__finish: 'Create', account__user_settings: 'User settings', account__your_company: 'Your company', account__change_auth: 'Change authorization method', account__change_auth_message_1: 'In case of corporate use, it is recommended to log in with a username and password.', account__change_auth_message_2: 'After creating a user, all data from the Telegram account will be transferred to the new account.', account__change_auth_btn: 'Create system account', account__change_auth_warning: 'WARNING!', account__change_auth_warning_message: 'Reverse data transfer is not possible.', account__chats: 'Chats', account__chats_active: 'Active', account__chats_archive: 'Archive', account__chats_free: 'Free', account__chats_total: 'Total', account__subscribe: 'Subscribe', account__subscribe_info: 'With a subscription, you can attach more chats. Archived chats are not counted.', account__subscribe_current_balance: 'Current balance', account__subscribe_about: 'about', account__subscribe_select_payment_1: 'You can pay for your subscription using ', account__subscribe_select_payment_2: 'Telegram stars', company__mask: 'Company cloacking', mask__title_table: 'Ignore cloaking', mask__title_table2: '(exclusion list)', mask__help_title: 'Cloacking', mask__help_message: 'It is possible to cloacking a company by representing its personnel as your own to companies other than those on the exclusion list.', company_info__title_card: 'Company card', company_info__name: 'Name', company_info__description: 'Description', company_info__persons: 'Persons', company_create__title_card: 'Add company', project_persons__search: 'Search', person_card__title: 'Person card', person_card__name: 'Name', person_card__company: 'Company name', person_card__department: 'Department', person_card__role: 'Role' } \ No newline at end of file +export default { EN: 'EN', RU: 'RU', continue: 'Continue', back: 'Back', month: 'month', months: 'months', slogan: 'Work together - it\'s magic!', login__email: 'E-mail', login__password: 'Password', login__forgot_password: 'Forgot Password?', login__sign_in: 'Log in', login__incorrect_login_data: 'User data not found. Edit your auth details before continuing', login__or_continue_as: 'or continue as', login__terms_of_use: 'Terms of use', login__accept_terms_of_use: 'I accept the', login__register: 'Create account', login__registration_message_error: 'Error', login__licensing_agreement: 'Licensing agreement', login__have_account: 'Already have an accont?', user__logout: 'Logout', projects__projects: 'Projects', projects__show_archive: 'Show archive', projects__hide_archive: 'Hide archive', projects__restore_archive_warning: 'Attention!', projects__restore_archive_warning_message: 'To restore a project from an archive, you must manually attach chats to it.', project__chats: 'Chats', project__persons: 'Persons', project__companies: 'Companies', project__edit: 'Edit', project__backup: 'Backup', project__archive: 'Archive', project__archive_warning: 'Are you sure?', project__archive_warning_message: 'Chat tracking in the project will be disabled after moving to the archive.', project__delete: 'Delete', project__delete_warning: 'Warning!', project__delete_warning_message: 'All project data will be removed. This action cannot be undone.', project_chats__search: 'Search', project_chats__send_chat: 'Request for attach chat', project_chats__send_chat_description: 'Provide instructions to the chat admin', project_chats__attach_chat: 'Attach chat', project_chats__attach_chat_description: 'Requires chat administrator privileges', project_chat__delete_warning: 'Warning!', project_chat__delete_warning_message: 'Chat tracking will be discontinued. If necessary, the cat can be attached again.', project_card__project_card: 'Project card', project_card__add_project: 'Add project', project_card__project_name: 'Name', project_card__project_description: 'Description', project_card__btn_accept: 'Accept', project_card__btn_back: 'Back', project_card__image_use_as_background_chats: 'logo as background for chats', project_card__error_name: 'Field is required', forgot_password__password_recovery: 'Password recovery', account_helper__enter_email: 'Enter account e-mail', account_helper__email: 'E-mail', account_helper__confirm_email: 'Confirm e-mail', account_helper__confirm_email_message: 'Enter the Code from e-mail to continue recover your password. If you haven\'t received an e-mail with the Code, check the Spam folder.', account_helper__code: 'Code', account_helper__code_error: 'Incorrect code. Ensure your e-mail is correct and try again.', account_helper__set_password: 'Set password', account_helper__password: 'Password', account_helper__finish: 'Finish', account_helper__finish_after_message: 'Done!', account__user_settings: 'User settings', account__your_company: 'Your company', account__change_auth: 'Change authorization method', account__change_auth_message_1: 'In case of corporate use, it is recommended to log in with a username and password.', account__change_auth_message_2: 'After creating a user, all data from the Telegram account will be transferred to the new account.', account__change_auth_btn: 'Create system account', account__change_auth_warning: 'WARNING!', account__change_auth_warning_message: 'Reverse data transfer is not possible.', account__chats: 'Chats', account__chats_active: 'Active', account__chats_archive: 'Archive', account__chats_free: 'Free', account__chats_total: 'Total', account__subscribe: 'Subscribe', account__subscribe_info: 'With a subscription, you can attach more chats. Archived chats are not counted.', account__subscribe_current_balance: 'Current balance', account__subscribe_about: 'about', account__subscribe_select_payment_1: 'You can pay for your subscription using ', account__subscribe_select_payment_2: 'Telegram stars', company__mask: 'Company cloacking', mask__title_table: 'Ignore cloaking', mask__title_table2: '(exclusion list)', mask__help_title: 'Cloacking', mask__help_message: 'It is possible to cloacking a company by representing its personnel as your own to companies other than those on the exclusion list.', company_info__title_card: 'Company card', company_info__name: 'Name', company_info__description: 'Description', company_info__persons: 'Persons', company_create__title_card: 'Add company', project_persons__search: 'Search', person_card__title: 'Person card', person_card__name: 'Name', person_card__company: 'Company name', person_card__department: 'Department', person_card__role: 'Role', settings__title: 'Settings', settings__language: 'Language', settings__font_size: 'Font size', terms__title: 'Terms of use' } \ No newline at end of file diff --git a/src/i18n/ru-RU/index.ts b/src/i18n/ru-RU/index.ts index 98aaeaa..2cdfa48 100644 --- a/src/i18n/ru-RU/index.ts +++ b/src/i18n/ru-RU/index.ts @@ -1 +1 @@ -export default { EN: 'EN', RU: 'RU', continue: 'Продолжить', back: 'Назад', month: 'мес.', months: 'мес.', login__email: 'Электронная почта', login__password: 'Пароль', login__forgot_password: 'Забыли пароль?', login__sign_in: 'Войти', login__incorrect_login_data: 'Пользователь с такими данными не найден. Отредактируйте введенные данные', login__or_continue_as: 'или продолжить', login__terms_of_use: 'Условия использования', login__accept_terms_of_use: 'Я принимаю', login__register: 'Зарегестрироваться', login__registration_message_ok: 'Мы отправили сообщение с инструкциями на указанную электронную почту', login__registration_message_error: 'Ошибка', login__licensing_agreement: 'Договор о лицензировании', login__have_account: 'Есть учетная запись', login__forgot_password_message: 'Введите e-mail, чтобы восстановить пароль. Мы пришлем инструкцию на указанную почту. Если вы не получили письмо, проверьте папку «Спам».', login__forgot_password_message_ok: 'Мы отправили сообщение с инструкциями на указанную электронную почту', login__forgot_password_message_error: 'Ошибка', user__logout: 'Выход', projects__projects: 'Проекты', projects__show_archive: 'Показать архив', projects__hide_archive: 'Скрыть архив', projects__restore_archive_warning: 'Внимание!', projects__restore_archive_warning_message: 'При восстановлении проекта из архива - присоединение чатов к проекту требуется осуществлять вручную.', project__chats: 'Чаты', project__persons: 'Люди', project__companies: 'Компании', project__edit: 'Редактировать', project__backup: 'Резервная копия', project__archive: 'В архив', project__delete: 'Удалить', project_chats__search: 'Поиск', project_chats__send_chat: 'Запрос на добавление чата', project_chats__send_chat_description: 'Отправить инструкцию администратору чата', project_chats__attach_chat: 'Добавить чат', project_chats__attach_chat_description: 'Необходимы права администратора чата', project_chat__delete_warning: 'Внимание!', project_chat__delete_warning_message: 'Отслеживание чата будет прекращено. При необходимости чат можно будет подключить снова.', project_card__add_project: 'Новый проект', project_card__project_name: 'Название', project_card__project_description: 'Описание', project_card__btn_accept: 'Подтвердить', project_card__btn_back: 'Назад', forgot_password__password_recovery: 'Восстановление пароля', forgot_password__enter_email: 'Введите электронную почту', forgot_password__email: 'Электронная почта', forgot_password__confirm_email: 'Подтверждение электронной почты', forgot_password__confirm_email_messege: 'Введите код из письма для продолжения восстановления пароля. Если не получили письмо с кодом - проверьте папку Спам', forgot_password__code: 'Код', forgot_password__create_new_password: 'Установка нового пароля', forgot_password__password: 'Пароль', forgot_password__finish: 'Создать', account__user_settings: 'Пользовательские настройки', account__your_company: 'Ваша компания', account__change_auth: 'Сменить способ авторизации', account__change_auth_message_1: 'В случае корпоративного использования рекомендуется входить в систему, указав логин и пароль.', account__change_auth_message_2: 'После создания пользователя все данные с учетной записи Telegram будут перенесены на новую учетную запись.', account__change_auth_btn: 'Создать пользователя', account__change_auth_warning: 'ВНИМАНИЕ!', account__change_auth_warning_message: 'Обратный перенос данных не возможен.', account__chats: 'Чаты', account__chats_active: 'Активные', account__chats_archive: 'Архивные', account__chats_free: 'Бесплатные', account__chats_total: 'Всего', account__subscribe: 'Подписка', account__subscribe_info: 'С помощью подписки можно подключить к бесплатным групповым чатам дополнительные. Архивные чаты не учитываются. ', account__subscribe_current_balance: 'Текущий баланс', account__subscribe_about: 'около', account__subscribe_select_payment_1: 'Вы можете оплатить подписку с помощью', account__subscribe_select_payment_2: 'Telegram stars', company__mask: 'Маскировка компаний', mask__title_table: 'Игнорирование маскировки', mask__title_table2: '(перечень исключений)', mask__help_title: 'Маскировка', mask__help_message: 'Возможно замаскировать компанию, представляя ее персонал как собственный для других компаний, кроме тех что есть в перечне исключений. ', company_info__title_card: 'Карточка компании', company_info__name: 'Название', company_info__description: 'Описание', company_info__persons: 'Сотрудники', company_create__title_card: 'Добавление компании', project_persons__search: 'Поиск', person_card__title: 'Карточка сотрудника', person_card__name: 'ФИО', person_card__company: 'Название компании', person_card__department: 'Подразделение', person_card__role: 'Функционал (должность)' } \ No newline at end of file +export default { EN: 'EN', RU: 'RU', continue: 'Продолжить', back: 'Назад', month: 'мес.', months: 'мес.', slogan: 'Работайте вместе - это волшебство!', login__email: 'Электронная почта', login__password: 'Пароль', login__forgot_password: 'Забыли пароль?', login__sign_in: 'Войти', login__incorrect_login_data: 'Пользователь с такими данными не найден. Отредактируйте введенные данные', login__or_continue_as: 'или продолжить', login__terms_of_use: 'Пользовательское соглашение', login__accept_terms_of_use: 'Я принимаю', login__register: 'Зарегестрироваться', login__registration_message_error: 'Ошибка', login__licensing_agreement: 'Договор о лицензировании', login__have_account: 'Есть учетная запись', user__logout: 'Выход', projects__projects: 'Проекты', projects__show_archive: 'Показать архив', projects__hide_archive: 'Скрыть архив', projects__restore_archive_warning: 'Внимание!', projects__restore_archive_warning_message: 'При восстановлении проекта из архива - присоединение чатов к проекту требуется осуществлять вручную.', project__chats: 'Чаты', project__persons: 'Люди', project__companies: 'Компании', project__edit: 'Редактировать', project__backup: 'Резервная копия', project__archive: 'В архив', project__archive_warning: 'Вы уверены?', project__archive_warning_message: 'После перемещения проекта в архив отслеживание чатов будет отключено.', project__delete: 'Удалить', project__delete_warning: 'Внимание!', project__delete_warning_message: 'Все данные проекта будут безвозвратно удалены.', project_chats__search: 'Поиск', project_chats__send_chat: 'Запрос на добавление чата', project_chats__send_chat_description: 'Отправить инструкцию администратору чата', project_chats__attach_chat: 'Добавить чат', project_chats__attach_chat_description: 'Необходимы права администратора чата', project_chat__delete_warning: 'Внимание!', project_chat__delete_warning_message: 'Отслеживание чата будет прекращено. При необходимости чат можно будет подключить снова.', project_card__project_card: 'Карточка компании', project_card__add_project: 'Новый проект', project_card__project_name: 'Название', project_card__project_description: 'Описание', project_card__btn_accept: 'Подтвердить', project_card__btn_back: 'Назад', project_card__image_use_as_background_chats: 'логотип в качестве фона для чатов', project_card__error_name: 'Поле обязательно к заполнению', forgot_password__password_recovery: 'Восстановление пароля', account_helper__enter_email: 'Введите электронную почту', account_helper__email: 'Электронная почта', account_helper__confirm_email: 'Подтверждение электронной почты', account_helper__confirm_email_message: 'Введите код из письма для продолжения восстановления пароля. Если не получили письмо с кодом - проверьте папку Спам', account_helper__code: 'Код', account_helper__code_error: 'Был введен неверный код. Проверьте адрес электронной почты и повторите попытку.', account_helper__set_password: 'Установка пароля', account_helper__password: 'Пароль', account_helper__finish: 'Отправить', account_helper__finish_after_message: 'Готово!', account__user_settings: 'Пользовательские настройки', account__your_company: 'Ваша компания', account__change_auth: 'Сменить способ авторизации', account__change_auth_message_1: 'В случае корпоративного использования рекомендуется входить в систему, указав логин и пароль.', account__change_auth_message_2: 'После создания пользователя все данные с учетной записи Telegram будут перенесены на новую учетную запись.', account__change_auth_btn: 'Создать пользователя', account__change_auth_warning: 'ВНИМАНИЕ!', account__change_auth_warning_message: 'Обратный перенос данных не возможен.', account__chats: 'Чаты', account__chats_active: 'Активные', account__chats_archive: 'Архивные', account__chats_free: 'Бесплатные', account__chats_total: 'Всего', account__subscribe: 'Подписка', account__subscribe_info: 'С помощью подписки можно подключить к бесплатным групповым чатам дополнительные. Архивные чаты не учитываются. ', account__subscribe_current_balance: 'Текущий баланс', account__subscribe_about: 'около', account__subscribe_select_payment_1: 'Вы можете оплатить подписку с помощью', account__subscribe_select_payment_2: 'Telegram stars', company__mask: 'Маскировка компаний', mask__title_table: 'Игнорирование маскировки', mask__title_table2: '(перечень исключений)', mask__help_title: 'Маскировка', mask__help_message: 'Возможно замаскировать компанию, представляя ее персонал как собственный для других компаний, кроме тех что есть в перечне исключений. ', company_info__title_card: 'Карточка компании', company_info__name: 'Название', company_info__description: 'Описание', company_info__persons: 'Сотрудники', company_create__title_card: 'Добавление компании', project_persons__search: 'Поиск', person_card__title: 'Карточка сотрудника', person_card__name: 'ФИО', person_card__company: 'Название компании', person_card__department: 'Подразделение', person_card__role: 'Функционал (должность)', settings__title: 'Настройки', settings__language: 'Язык', settings__font_size: 'Размер шрифта', terms__title: 'Пользовательское соглашение' } \ No newline at end of file diff --git a/src/index copy.ts b/src/index copy.ts deleted file mode 100644 index 066de2a..0000000 --- a/src/index copy.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { defineStore } from '#q-app/wrappers' -import { createPinia } from 'pinia' - -/* - * When adding new properties to stores, you should also - * extend the `PiniaCustomProperties` interface. - * @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties - */ -declare module 'pinia' { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - export interface PiniaCustomProperties { - // add your custom properties here, if any - } -} - -/* - * If not building with SSR mode, you can - * directly export the Store instantiation; - * - * The function below can be async too; either use - * async/await or return a Promise which resolves - * with the Store instance. - */ - -export default defineStore((/* { ssrContext } */) => { - const pinia = createPinia() - - // You can add Pinia plugins here - // pinia.use(SomePiniaPlugin) - - return pinia -}) diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 1a5af2a..40a112d 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -4,8 +4,22 @@ fit class="fit no-scroll bg-transparent" > - - + + import { ref } from 'vue' import meshBackground from '../components/admin/meshBackground.vue' + + const existDrawer = ref(true) function getCSSVar (varName: string) { const root = document.documentElement return getComputedStyle(root).getPropertyValue(varName).trim() @@ -30,6 +46,7 @@ function onResize () { const clientWidth = document.documentElement.clientWidth; drawerWidth.value = (clientWidth - bodyWidth)/2 + existDrawer.value = clientWidth > bodyWidth } diff --git a/src/pages/AccountPage.vue b/src/pages/AccountPage.vue index 5a388bf..1edc152 100644 --- a/src/pages/AccountPage.vue +++ b/src/pages/AccountPage.vue @@ -3,43 +3,63 @@
-
- {{ $t('account__user_settings') }} -
-
- - - +
+
+ {{ $t('account__user_settings') }}
+
+ + + + +
{{ $t('account__chats') }} -
{{ $t('account__subscribe') }}
-
- + + - + {{ $t('account__subscribe_info') }} + {{ $t('account__subscribe_select_payment_1') }} + + {{ $t('account__subscribe_select_payment_2') }} -
-
{{ $t('account__subscribe_current_balance') }}
-
- {{ $t('account__subscribe_about') }} 3 {{ $t('months') }} -
-
+ +
50
- - - - - {{ $t('month') }} - - +
- {{ $t('account__subscribe_select_payment_1') }} - - {{ $t('account__subscribe_select_payment_2') }} +
+ + \ No newline at end of file diff --git a/src/pages/CompanyInfoPage.vue b/src/pages/CompanyInfoPage.vue index 43b0962..8d1d56f 100644 --- a/src/pages/CompanyInfoPage.vue +++ b/src/pages/CompanyInfoPage.vue @@ -6,35 +6,69 @@ {{$t('company_info__title_card')}}
- + diff --git a/src/pages/CreateAccountPage.vue b/src/pages/CreateAccountPage.vue index c6559af..38ee95c 100644 --- a/src/pages/CreateAccountPage.vue +++ b/src/pages/CreateAccountPage.vue @@ -2,10 +2,12 @@ - + + + diff --git a/src/pages/CreateProjectPage.vue b/src/pages/CreateProjectPage.vue index 0d2b69c..3540940 100644 --- a/src/pages/CreateProjectPage.vue +++ b/src/pages/CreateProjectPage.vue @@ -6,7 +6,7 @@ {{$t('project_card__add_project')}}
- + diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index b01b5ff..ca3cd5d 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -1,6 +1,5 @@ diff --git a/src/pages/PersonInfoPage.vue b/src/pages/PersonInfoPage.vue index ff97185..75e5cda 100644 --- a/src/pages/PersonInfoPage.vue +++ b/src/pages/PersonInfoPage.vue @@ -14,70 +14,71 @@ -
-
+
+ -
- + +
{{ person.tname }}
{{ person.tusername }}
- +
+ - - - - + + + + - + - -
+ +
@@ -108,5 +109,5 @@ } - diff --git a/src/pages/ProjectInfoPage.vue b/src/pages/ProjectInfoPage.vue index ff17328..0481631 100644 --- a/src/pages/ProjectInfoPage.vue +++ b/src/pages/ProjectInfoPage.vue @@ -1,5 +1,4 @@ @@ -78,7 +77,7 @@ } const tabs = [ - {name: 'chats', label: 'project__chats', icon: 'mdi-message-outline', component: tabComponents.projectPageChats, to: { name: 'chats'} }, + {name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline', component: tabComponents.projectPageChats, to: { name: 'chats'} }, {name: 'persons', label: 'project__persons', icon: 'mdi-account-outline', component: tabComponents.projectPagePersons, to: { name: 'persons'} }, {name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline', component: tabComponents.projectPageCompanies, to: { name: 'companies'} }, ] diff --git a/src/pages/ProjectsPage.vue b/src/pages/ProjectsPage.vue index 7e2264c..028a88c 100644 --- a/src/pages/ProjectsPage.vue +++ b/src/pages/ProjectsPage.vue @@ -15,11 +15,15 @@ dense >
- - + +
- Alex mart + {{ + tgUser?.first_name + + (tgUser?.first_name && tgUser?.last_name ? ' ' : '') + + tgUser?.last_name + }}
@@ -62,28 +66,25 @@ {{ item.name }} {{item.description}} - -
-
- - {{ item.chats }} -
-
- - {{ item.persons }} -
-
- - {{ item.companies }} -
-
-
+ +
+ +
+
+ + {{ item.chats }} +
+
+ + {{ item.persons }} +
+
- + + + diff --git a/src/pages/TermsPage.vue b/src/pages/TermsPage.vue new file mode 100644 index 0000000..edd5408 --- /dev/null +++ b/src/pages/TermsPage.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index 1d43c18..124bd68 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,7 +6,8 @@ import { createWebHistory, } from 'vue-router' import routes from './routes' -import { useProjectsStore } from '../stores/projects' +import { useAuthStore } from 'stores/auth' +import { useProjectsStore } from 'stores/projects' /* * If not building with SSR mode, you can @@ -31,8 +32,62 @@ export default defineRouter(function (/* { store, ssrContext } */) { // quasar.conf.js -> build -> publicPath history: createHistory(process.env.VUE_ROUTER_BASE), }) + + const publicPaths = ['/login', '/terms-of-use', '/create-account', '/recovery-password'] + + Router.beforeEach(async (to) => { + const authStore = useAuthStore() + + // Инициализация хранилища перед проверкой + if (!authStore.isInitialized) { + await authStore.initialize() + } + + // Проверка авторизации для непубличных маршрутов + if (!publicPaths.includes(to.path)) { + if (!authStore.isAuthenticated) { + return { + path: '/login', + query: { redirect: to.fullPath } + } + } + } + + // Редирект авторизованных пользователей с публичных маршрутов + if (publicPaths.includes(to.path) && authStore.isAuthenticated) { + return { path: '/' } + } + }) + + const handleBackButton = async () => { + const currentRoute = Router.currentRoute.value + if (currentRoute.meta.backRoute) { + await Router.push(currentRoute.meta.backRoute); + } else { + if (window.history.length > 1) { + Router.go(-1) + } else { + await Router.push('/projects') + } + } + } + Router.afterEach((to) => { + const BackButton = window.Telegram?.WebApp?.BackButton; + if (BackButton) { + // Управление видимостью + if (to.meta.hideBackButton) { + BackButton.hide() + } else { + BackButton.show() + } + + // Обновляем обработчик клика + BackButton.offClick(handleBackButton as () => void) + BackButton.onClick(handleBackButton as () => void) + } + if (!to.params.id) { const projectsStore = useProjectsStore() projectsStore.setCurrentProjectId(null) diff --git a/src/router/routes.ts b/src/router/routes.ts index 047c6d9..acb2807 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -21,7 +21,8 @@ const routes: RouteRecordRaw[] = [ { name: 'projects', path: '/projects', - component: () => import('pages/ProjectsPage.vue') + component: () => import('pages/ProjectsPage.vue'), + meta: { hideBackButton: true } }, { name: 'project_add', @@ -55,48 +56,81 @@ const routes: RouteRecordRaw[] = [ { name: 'chats', path: 'chats', - component: () => import('../components/admin/project-page/ProjectPageChats.vue') + component: () => import('components/admin/project-page/ProjectPageChats.vue'), + meta: { backRoute: '/projects' } }, { name: 'persons', path: 'persons', - component: () => import('../components/admin/project-page/ProjectPagePersons.vue') + component: () => import('components/admin/project-page/ProjectPagePersons.vue'), + meta: { backRoute: '/projects' } }, { name: 'companies', path: 'companies', - component: () => import('../components/admin/project-page/ProjectPageCompanies.vue') + component: () => import('components/admin/project-page/ProjectPageCompanies.vue'), + meta: { backRoute: '/projects' } } ] }, { - path: '/account', + name: 'company_info', + path: '/project/:id(\\d+)/company/:companyId', + component: () => import('pages/CompanyInfoPage.vue'), + beforeEnter: setProjectBeforeEnter + }, + { + name: 'person_info', + path: '/project/:id(\\d+)/person/:personId', + component: () => import('pages/PersonInfoPage.vue'), + beforeEnter: setProjectBeforeEnter + }, + + { name: 'account', + path: '/account', component: () => import('pages/AccountPage.vue') }, + { + name: 'create_account', + path: '/create-account', + component: () => import('pages/CreateAccountPage.vue') + }, { - path: '/login', name: 'login', + path: '/login', component: () => import('pages/LoginPage.vue') }, { - path: '/recovery-password', name: 'recovery_password', + path: '/recovery-password', component: () => import('pages/ForgotPasswordPage.vue') }, { - path: '/create-company', - name: 'create_company', + name: 'add_company', + path: '/add-company', component: () => import('pages/CreateCompanyPage.vue') }, { - path: '/person-info', name: 'person_info', + path: '/person-info', component: () => import('pages/PersonInfoPage.vue') + }, + + { + name: 'settings', + path: '/settings', + component: () => import('pages/SettingsPage.vue') + }, + + { + name: 'terms', + path: '/terms-of-use', + component: () => import('pages/TermsPage.vue') } ] }, diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..a048f00 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,65 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { api } from 'boot/axios' + +interface User { + id: string + email?: string + username: string + first_name?: string + last_name?: string + avatar?: string +} + +export const useAuthStore = defineStore('auth', () => { + // State + const user = ref(null) + const isInitialized = ref(false) + + // Getters + const isAuthenticated = computed(() => !!user.value) + + // Actions + const initialize = async () => { + try { + const { data } = await api.get('/customer/profile') + user.value = data + } catch (error) { + console.error(error) + user.value = null + } finally { + isInitialized.value = true + } + } + + const loginWithCredentials = async (email: string, password: string) => { + // будет переделано на беке - нужно сменить урл + await api.post('/api/admin/customer/login', { email, password }, { withCredentials: true }) + await initialize() + } + + const loginWithTelegram = async (initData: string) => { + await api.post('/api/admin/customer/login', { initData }, { withCredentials: true }) + await initialize() + } + + const logout = async () => { + try { + await api.get('/customer/logout', {}) + } finally { + user.value = null + // @ts-expect-ignore + // window.Telegram?.WebApp.close() + } + } + + return { + user, + isAuthenticated, + isInitialized, + initialize, + loginWithCredentials, + loginWithTelegram, + logout + } +}) \ No newline at end of file diff --git a/src/stores/textSize.ts b/src/stores/textSize.ts new file mode 100644 index 0000000..9a4ed0a --- /dev/null +++ b/src/stores/textSize.ts @@ -0,0 +1,121 @@ +import { api } from 'boot/axios' +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +interface FontSizeResponse { + fontSize: number +} + +interface FontSizeError { + message: string + code: number +} + +export const useTextSizeStore = defineStore('textSize', () => { + // State + const baseSize = ref(16) // Значение по умолчанию + const isLoading = ref(false) + const error = ref(null) + const isInitialized = ref(false) + + // Константы + const minFontSize = 12 + const maxFontSize = 20 + const fontSizeStep = 2 + + // Getters + const currentFontSize = computed(() => baseSize.value) + const canIncrease = computed(() => baseSize.value < maxFontSize) + const canDecrease = computed(() => baseSize.value > minFontSize) + + // Actions + const fetchFontSize = async () => { + try { + isLoading.value = true + const response = await api.get('customer/settings') + baseSize.value = clampFontSize(response.data.fontSize) + updateCssVariable() + } catch (err) { + handleError(err, 'Failed to fetch font size') + baseSize.value = 16 // Fallback к значению по умолчанию + throw err + } finally { + isLoading.value = false + } + } + + const updateFontSize = async (newSize: number) => { + try { + const validatedSize = clampFontSize(newSize) + + await api.put('customer/settings', { fontSize: validatedSize }) + + baseSize.value = validatedSize + updateCssVariable() + error.value = null + } catch (err) { + handleError(err, 'Failed to update font size') + throw err + } + } + + const increaseFontSize = async () => { + if (!canIncrease.value) return + await updateFontSize(baseSize.value + fontSizeStep) + } + + const decreaseFontSize = async () => { + if (!canDecrease.value) return + await updateFontSize(baseSize.value - fontSizeStep) + } + + // Helpers + const clampFontSize = (size: number): number => { + return Math.max(minFontSize, Math.min(size, maxFontSize)) + } + + const updateCssVariable = () => { + document.documentElement.style.setProperty( + '--dynamic-font-size', + `${baseSize.value}px` + ) + } + + const handleError = (err: unknown, defaultMessage: string) => { + const apiError = err as { response?: { data: { message: string; code: number } } } + error.value = { + message: apiError?.response?.data?.message || defaultMessage, + code: apiError?.response?.data?.code || 500 + } + console.error('FontSize Error:', error.value) + } + + // Инициализация при первом использовании + const initialize = async () => { + if (isInitialized.value) return + + try { + await fetchFontSize() + } catch { + // Оставляем значение по умолчанию + } finally { + isInitialized.value = true + } + } + + return { + baseSize, + currentFontSize, + minFontSize, + maxFontSize, + isLoading, + error, + canIncrease, + canDecrease, + fetchFontSize, + increaseFontSize, + decreaseFontSize, + updateFontSize, + initialize + } +}) \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 04a2f59..d9a16e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,13 @@ +import type { WebApp } from "@twa-dev/types" + +declare global { + interface Window { + Telegram: { + WebApp: WebApp + } + } +} + interface ProjectParams { name: string description?: string diff --git a/todo.txt b/todo.txt index b231fb7..e405bf0 100644 --- a/todo.txt +++ b/todo.txt @@ -2,10 +2,10 @@ 1. Login: + Окно "Забыли пароль?" -- Надпись "Неправильный логин или пароль" -- Окно "Регистрация нового пользователя" ++ Надпись "Неправильный логин или пароль" ++ Окно "Регистрация нового пользователя" - Переводы -- Верификация e-mail ++ Верификация e-mail (не делать - плохо выглядит) 2. Account: + Работа с изображением логотипа компании @@ -17,14 +17,14 @@ + (баг) Промотка шапки в конце прокрутки списка проектов + Добавить тень при прокрутке списка на заголовке "Проекты" + Окно добавить проект -- При добавлении проекта проверять валидность, если не валидно то скрывать галку "Применить" ++ При добавлении проекта проверять валидность, если не валидно то скрывать галку "Применить" 4.1 ProjectPage - Заголовок: -- Анимация расширенной версии (плавное увеличение блока div) ++ Анимация расширенной версии (плавное увеличение блока div) + Окно редактирования проекта -- При изменении свойств проекта проверять валидность, если не валидно то скрывать галку "Применить" -- Продумать backup -- Окно отправки проекта в архив ++ При изменении свойств проекта проверять валидность, если не валидно то скрывать галку "Применить" ++ Продумать backup (потом) ++ Окно отправки проекта в архив + Окно удаления проекта 4.2 ProjectPage - Чаты: @@ -48,9 +48,18 @@ - При изменении компании проверять валидность, если не валидно то скрывать галку "Применить" - Окно настройки видимости компаний -4.5 ProjectPage - МаскировкаЖ +4.5 ProjectPage - Маскировка: - Сделать стор и настроить компоненты +5. Settings: +- Роутинг +- Переключатель языков +- Встроить в Телеграмм + +6. Лицензионное соглашение: +- Роутинг и заготовка +- Текст соглашения +- Встроить в Телеграмм BUGS: - 1. Прыгает кнопка fab при перещелкивании табов diff --git a/tsconfig.json b/tsconfig.json index 104aa55..96fc57b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "boot/*": ["./src/boot/*"], "stores/*": ["./src/stores/*"] }, - "types": ["node"] - } + "types": ["@twa-dev/types", "node"] + }, + "include": ["src/**/*", "types/**/*"] } \ No newline at end of file