diff --git a/lib/auth.js b/lib/auth.js index a65058a..8345527 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,41 +1,30 @@ /** - * Optional password auth middleware. - * If AUTH_PASSWORD is set in .env, requires ?key= or session auth. - * Static assets and health check are always public. + * Auth middleware — requires user session for protected routes. + * Public: landing page, auth routes, static assets, health, placeholder API. */ -const PUBLIC_PATHS = ['/health', '/favicon.ico']; +const PUBLIC_PATHS = ['/', '/health', '/favicon.ico']; +const PUBLIC_PREFIXES = ['/auth/', '/vendor/', '/placeholder-img/']; +const PUBLIC_FILES = ['/shared.css', '/shared.js', '/landing.html']; function authMiddleware(req, res, next) { - const password = process.env.AUTH_PASSWORD; - - // No password set — everything is public - if (!password) return next(); - - // Always allow public paths and static assets + // Public paths if (PUBLIC_PATHS.includes(req.path)) return next(); - if (req.path.startsWith('/vendor/')) return next(); + if (PUBLIC_FILES.includes(req.path)) return next(); + if (PUBLIC_PREFIXES.some(p => req.path.startsWith(p))) return next(); - // Check session - if (req.session && req.session.authenticated) return next(); + // Authenticated user + if (req.session && req.session.user) return next(); - // Check query param - if (req.query.key === password) { - if (req.session) req.session.authenticated = true; - return next(); + // API requests — 401 JSON + if (req.path.startsWith('/api/') || req.path.startsWith('/download/')) { + return res.status(401).json({ error: 'Unauthorized' }); } - // Show login form for GET requests to pages - if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/download/')) { - return res.status(401).send(` -Login - -
-`); + // Page requests — save return URL and redirect to login + if (req.method === 'GET') { + req.session.returnTo = req.originalUrl; + return res.redirect('/auth/login'); } return res.status(401).json({ error: 'Unauthorized' }); diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..0686933 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,35 @@ +const mysql = require('mysql2/promise'); + +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'wa_tools', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'wa_tools', + waitForConnections: true, + connectionLimit: 5, + charset: 'utf8mb4', +}); + +async function findUserByEmail(email) { + const [rows] = await pool.execute('SELECT * FROM users WHERE email = ?', [email]); + return rows[0] || null; +} + +async function findUserById(id) { + const [rows] = await pool.execute('SELECT id, email, display_name, created_at FROM users WHERE id = ?', [id]); + return rows[0] || null; +} + +async function createUser(email, passwordHash, displayName) { + const [result] = await pool.execute( + 'INSERT INTO users (email, password_hash, display_name) VALUES (?, ?, ?)', + [email, passwordHash, displayName || null] + ); + return result.insertId; +} + +async function updateLastLogin(id) { + await pool.execute('UPDATE users SET last_login = NOW() WHERE id = ?', [id]); +} + +module.exports = { pool, findUserByEmail, findUserById, createUser, updateLastLogin }; diff --git a/package-lock.json b/package-lock.json index bbdd14e..a4deb6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@mozilla/readability": "^0.6.0", "archiver": "^7.0.1", + "bcrypt": "^6.0.0", "dotenv": "^17.3.1", "express": "^5.1.0", "express-rate-limit": "^8.3.0", @@ -19,6 +20,7 @@ "iconv-lite": "^0.7.2", "jsdom": "^28.1.0", "multer": "^1.4.5-lts.2", + "mysql2": "^3.20.0", "openid-client": "^6.4.2", "opentype.js": "^1.3.4", "passport": "^0.7.0", @@ -662,6 +664,16 @@ "node": ">=14" } }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -768,6 +780,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -807,6 +828,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -1241,6 +1276,15 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1568,6 +1612,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/geoip-lite": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.10.tgz", @@ -1881,6 +1934,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2032,12 +2091,33 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2207,6 +2287,40 @@ "node": ">= 0.6" } }, + "node_modules/mysql2": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", + "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2216,6 +2330,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "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/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2898,6 +3032,21 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "license": "BSD-3-Clause" }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -3182,6 +3331,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT", + "peer": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 49ce4fa..602aa7f 100755 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@mozilla/readability": "^0.6.0", "archiver": "^7.0.1", + "bcrypt": "^6.0.0", "dotenv": "^17.3.1", "express": "^5.1.0", "express-rate-limit": "^8.3.0", @@ -22,6 +23,7 @@ "iconv-lite": "^0.7.2", "jsdom": "^28.1.0", "multer": "^1.4.5-lts.2", + "mysql2": "^3.20.0", "openid-client": "^6.4.2", "opentype.js": "^1.3.4", "passport": "^0.7.0", diff --git a/public/landing.html b/public/landing.html new file mode 100644 index 0000000..529d3f4 --- /dev/null +++ b/public/landing.html @@ -0,0 +1,896 @@ + + + + + + WA Dev Tools — 13 бесплатных инструментов для веб-разработчика + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+ + 13 инструментов — бесплатно навсегда +
+ + + +

+ Инструменты для
+ веб-разработчика +

+ +

+ Сжатие картинок, форматирование кода, HTTP-клиент и ещё 10 инструментов — + всё в браузере, бесплатно и без регистрации карты. +

+ + +
+ + + + + + Подробнее + +
+ + +
+
+
+ +
+

Инструменты

+
+ +
+ + +
+
+ + + + + + + +
+ +
+
+ Изображения + 4 инструмента +
+
+ +

Сжатие, редактирование, SVG и placeholder-генератор

+ +
    +
  • Сжатие картинок
  • +
  • Placeholder-генератор
  • +
  • SVG-редактор
  • +
  • Фоторедактор
  • +
+
+ + +
+
+ + + + + + +
+ +
+
+ Код + 3 инструмента +
+
+ +

Форматирование, очистка и конвертация кода между форматами

+ +
    +
  • Форматирование кода
  • +
  • HTML Sanitizer
  • +
  • Конвертер форматов
  • +
+
+ + +
+
+ + + + + + +
+ +
+
+ Веб + 3 инструмента +
+
+ +

HTTP-клиент, парсер страниц и анализ редиректов

+ +
    +
  • HTTP-клиент
  • +
  • Парсер статей
  • +
  • Redirect-анализатор
  • +
+
+ + +
+
+ + + + + + +
+ +
+
+ Утилиты + 2 инструмента +
+
+ +

Генератор паролей и Markdown-просмотрщик с превью

+ +
    +
  • Генератор паролей
  • +
  • Markdown Viewer
  • +
+
+ +
+
+
+ + +
+
+
+ +
+
+ + + + + + + + +
+
Бесплатно навсегда
+

Никаких подписок и скрытых платежей. Все 13 инструментов доступны сразу после регистрации.

+
+ +
+
+ + + + + +
+
В браузере
+

Ничего устанавливать не нужно. Работает на любом устройстве с современным браузером.

+
+ +
+
+ + + + + +
+
Безопасно
+

Ваши файлы обрабатываются локально или транзитно и не сохраняются на нашем сервере.

+
+ +
+
+ + + + +
+
Быстро
+

Лёгкий интерфейс, мгновенная обработка без очередей и ожидания серверного ответа.

+
+ +
+
+
+ + +
+
+ +

Готовы начать?

+

+ Зарегистрируйтесь бесплатно и получите доступ ко всем инструментам — без кредитной карты. +

+ + + + + + + + Создать аккаунт + +
+
+ + + + + + + + diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..547d64c --- /dev/null +++ b/public/login.html @@ -0,0 +1,95 @@ + + + + + + Вход — WA Dev Tools + + + + + + + +
+
+ +

WA Dev Tools

+
+

Войдите в аккаунт

+
+ +
+ +
+
+ + +
+
+ + +
+ +
+ +

+ Нет аккаунта? Зарегистрируйтесь +

+
+ + + + diff --git a/public/register.html b/public/register.html new file mode 100644 index 0000000..105d85b --- /dev/null +++ b/public/register.html @@ -0,0 +1,112 @@ + + + + + + Регистрация — WA Dev Tools + + + + + + + +
+
+ +

WA Dev Tools

+
+

Создайте бесплатный аккаунт

+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +

+ Уже есть аккаунт? Войти +

+
+ + + + diff --git a/public/shared.css b/public/shared.css index b977906..7ea3201 100644 --- a/public/shared.css +++ b/public/shared.css @@ -56,6 +56,13 @@ body { .sidebar-bottom { margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px } .main-content { margin-left:56px } +/* Sidebar dividers */ +.sidebar-divider { width:24px;height:1px;background:var(--surface-600);margin:4px 0 } + +/* Sidebar user avatar */ +.sidebar-avatar { width:32px;height:32px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;font-family:'Manrope',sans-serif;cursor:pointer;transition:opacity .2s } +.sidebar-avatar:hover { opacity:.8 } + /* Theme toggle */ .theme-toggle { width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .2s;color:var(--text-muted) } .theme-toggle:hover { background:rgba(255,255,255,.06);color:var(--text-secondary) } diff --git a/public/shared.js b/public/shared.js index 335a771..9e4e3ec 100644 --- a/public/shared.js +++ b/public/shared.js @@ -1,4 +1,4 @@ -/* WA Dev Tools — Shared JS (sidebar, theme, tailwind config) */ +/* WA Dev Tools — Shared JS (sidebar, theme, tailwind config, categories) */ /* Tailwind config */ if (typeof tailwind !== 'undefined') { @@ -21,22 +21,35 @@ if (typeof tailwind !== 'undefined') { }; } +/* Categories */ +const WA_CATEGORIES = [ + { id: 'images', title: 'Изображения', description: 'Сжатие, редактирование и генерация', icon: '', tools: ['compress', 'placeholder', 'svgeditor', 'editor'] }, + { id: 'code', title: 'Код', description: 'Форматирование, очистка и конвертация', icon: '', tools: ['formatter', 'sanitizer', 'converter'] }, + { id: 'web', title: 'Веб', description: 'HTTP-клиент, парсер и анализ', icon: '', tools: ['parser', 'httpclient', 'redirects'] }, + { id: 'utils', title: 'Утилиты', description: 'Пароли, Markdown и другие', icon: '', tools: ['password', 'md'] }, +]; + /* Sidebar tools definition */ const WA_TOOLS = [ - { id: 'home', path: '/', title: 'Главная', icon: '' }, - { id: 'compress', path: '/compress', title: 'Картинки', icon: '' }, - { id: 'md', path: '/md', title: 'Markdown', icon: '' }, - { id: 'placeholder', path: '/placeholder', title: 'Placeholder', icon: '' }, - { id: 'parser', path: '/parser', title: 'Parser', icon: '' }, - { id: 'password', path: '/password', title: 'Password', icon: '' }, - { id: 'sanitizer', path: '/sanitizer', title: 'Sanitizer', icon: '' }, - { id: 'converter', path: '/converter', title: 'Converter', icon: '' }, - { id: 'formatter', path: '/formatter', title: 'Formatter', icon: '' }, - { id: 'editor', path: '/editor', title: 'Editor', icon: '' }, - { id: 'httpclient', path: '/httpclient', title: 'HTTP Client', icon: '' }, - { id: 'redirects', path: '/redirects', title: 'Redirects', icon: '' }, - { id: 'svgeditor', path: '/svgeditor', title: 'SVG Editor', icon: '' }, - { id: 'logs', path: '/logs', title: 'Logs', icon: '' }, + { id: 'home', path: '/dashboard', title: 'Главная', category: null, icon: '' }, + // Изображения + { id: 'compress', path: '/compress', title: 'Сжатие', category: 'images', icon: '' }, + { id: 'placeholder', path: '/placeholder', title: 'Placeholder', category: 'images', icon: '' }, + { id: 'svgeditor', path: '/svgeditor', title: 'SVG', category: 'images', icon: '' }, + { id: 'editor', path: '/editor', title: 'Фото', category: 'images', icon: '' }, + // Код + { id: 'formatter', path: '/formatter', title: 'Formatter', category: 'code', icon: '' }, + { id: 'sanitizer', path: '/sanitizer', title: 'Sanitizer', category: 'code', icon: '' }, + { id: 'converter', path: '/converter', title: 'Converter', category: 'code', icon: '' }, + // Веб + { id: 'parser', path: '/parser', title: 'Parser', category: 'web', icon: '' }, + { id: 'httpclient', path: '/httpclient', title: 'HTTP', category: 'web', icon: '' }, + { id: 'redirects', path: '/redirects', title: 'Redirects', category: 'web', icon: '' }, + // Утилиты + { id: 'password', path: '/password', title: 'Пароли', category: 'utils', icon: '' }, + { id: 'md', path: '/md', title: 'Markdown', category: 'utils', icon: '' }, + // Admin + { id: 'logs', path: '/logs', title: 'Логи', category: null, icon: '' }, ]; /* Build sidebar SVG icon */ @@ -46,25 +59,38 @@ function _svgIcon(innerPath) { /* Inject sidebar into the page */ function initSidebar(activeId) { - // Determine active tool from activeId or URL if (!activeId) { const path = window.location.pathname; - const match = WA_TOOLS.find(t => t.path !== '/' && path.startsWith(t.path)); + const match = WA_TOOLS.find(t => t.path !== '/dashboard' && path.startsWith(t.path)); activeId = match ? match.id : 'home'; } - const links = WA_TOOLS.map(t => - `${_svgIcon(t.icon)}` - ).join('\n '); + // Build links grouped by category + let linksHtml = ''; + let lastCategory = '__none__'; + + for (const t of WA_TOOLS) { + // Add category divider + if (t.category !== lastCategory && t.category !== null && lastCategory !== '__none__') { + linksHtml += ''; + } + lastCategory = t.category; + + const isActive = t.id === activeId ? ' active' : ''; + linksHtml += `${_svgIcon(t.icon)}\n`; + } const nav = document.createElement('nav'); nav.className = 'sidebar'; nav.innerHTML = `