Auth + Landing + Categories
- Local auth: registration/login with email+password (bcrypt, MariaDB) - Landing page: marketing page at / with hero, categories, advantages, CTA - Tool categories: Images (4), Code (3), Web (3), Utilities (2) - Sidebar: category dividers, user avatar with logout - Protected routes: all tools require auth, landing/login/register public - New files: lib/db.js, routes/auth.js, login.html, register.html, landing.html - Dependencies: bcrypt, mysql2
This commit is contained in:
parent
e3afa5b95a
commit
f8616e32b4
45
lib/auth.js
45
lib/auth.js
@ -1,41 +1,30 @@
|
||||
/**
|
||||
* Optional password auth middleware.
|
||||
* If AUTH_PASSWORD is set in .env, requires ?key=<password> 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(`<!DOCTYPE html>
|
||||
<html><head><meta charset="UTF-8"><title>Login</title>
|
||||
<style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#060c18;color:#c0c0c0}
|
||||
form{background:#091020;padding:32px;border-radius:12px;border:1px solid #162040}
|
||||
input{display:block;margin:8px 0;padding:10px;background:#0a1628;border:1px solid #162040;color:#c0c0c0;border-radius:6px;width:200px}
|
||||
button{padding:10px 24px;background:#0054e6;color:#fff;border:none;border-radius:6px;cursor:pointer;width:100%}
|
||||
</style></head><body>
|
||||
<form method="GET"><label>Пароль</label><input type="password" name="key" autofocus><button>Войти</button></form>
|
||||
</body></html>`);
|
||||
// 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' });
|
||||
|
||||
35
lib/db.js
Normal file
35
lib/db.js
Normal file
@ -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 };
|
||||
156
package-lock.json
generated
156
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
896
public/landing.html
Normal file
896
public/landing.html
Normal file
@ -0,0 +1,896 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WA Dev Tools — 13 бесплатных инструментов для веб-разработчика</title>
|
||||
<meta name="description" content="Сжатие картинок, форматирование кода, HTTP-клиент и ещё 10 инструментов для веб-разработчиков — всё в браузере, бесплатно." />
|
||||
<script src="/vendor/tailwind.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: { extend: {
|
||||
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
|
||||
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
|
||||
}}
|
||||
};
|
||||
</script>
|
||||
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||
<link href="/shared.css" rel="stylesheet">
|
||||
<style>
|
||||
/* ── Resets & base ── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(28px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% { opacity: 0.45; }
|
||||
50% { opacity: 0.70; }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.animate-fade-in { animation: fadeIn 0.7s ease both; }
|
||||
.animate-slide-up { animation: slideUp 0.7s ease both; }
|
||||
.delay-100 { animation-delay: 0.10s; }
|
||||
.delay-200 { animation-delay: 0.20s; }
|
||||
.delay-300 { animation-delay: 0.30s; }
|
||||
.delay-400 { animation-delay: 0.40s; }
|
||||
.delay-500 { animation-delay: 0.50s; }
|
||||
.delay-600 { animation-delay: 0.60s; }
|
||||
|
||||
/* Intersection-observer driven reveal */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||
}
|
||||
.reveal.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.reveal-delay-1 { transition-delay: 0.10s; }
|
||||
.reveal-delay-2 { transition-delay: 0.20s; }
|
||||
.reveal-delay-3 { transition-delay: 0.30s; }
|
||||
.reveal-delay-4 { transition-delay: 0.40s; }
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 80px 20px 60px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Glow blobs */
|
||||
.hero-glow-1 {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(700px, 120vw);
|
||||
height: min(700px, 120vw);
|
||||
background: radial-gradient(ellipse at center, rgba(0, 84, 230, 0.18) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
animation: glow-pulse 5s ease-in-out infinite;
|
||||
}
|
||||
.hero-glow-2 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -15%;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: radial-gradient(ellipse at center, rgba(107, 159, 255, 0.08) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
animation: glow-pulse 7s ease-in-out infinite reverse;
|
||||
}
|
||||
.hero-glow-3 {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
left: -10%;
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 67, 184, 0.10) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
animation: glow-pulse 6s ease-in-out infinite 1s;
|
||||
}
|
||||
|
||||
/* Grid lines overlay */
|
||||
.hero-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 84, 230, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 84, 230, 0.04) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
mask-image: radial-gradient(ellipse at center, black 20%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: clamp(11px, 1.5vw, 13px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.hero-logo span {
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.hero-headline {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(2rem, 5.5vw, 3.75rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--text-primary);
|
||||
max-width: 820px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
.hero-headline .accent { color: var(--accent); }
|
||||
|
||||
.hero-sub {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(1rem, 2vw, 1.2rem);
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto 44px;
|
||||
}
|
||||
|
||||
.hero-cta-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
.hero-cta-group { flex-direction: row; justify-content: center; }
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 15px 32px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 0 0 rgba(0, 84, 230, 0);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-dim);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(0, 84, 230, 0.35);
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.btn-secondary:hover { color: var(--text-primary); }
|
||||
.btn-secondary .link-text {
|
||||
border-bottom: 1px solid var(--surface-600);
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
/* Count badge in hero */
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--surface-700);
|
||||
border: 1px solid var(--surface-600);
|
||||
border-radius: 999px;
|
||||
padding: 6px 16px 6px 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.hero-badge .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-bright);
|
||||
box-shadow: 0 0 6px var(--accent-bright);
|
||||
animation: glow-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ── Scroll hint ── */
|
||||
.scroll-hint {
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
animation: float 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ── Section common ── */
|
||||
.section {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
.section-inner {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-bright);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.section-title {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(1.6rem, 4vw, 2.4rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 48px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* ── Category cards ── */
|
||||
.cat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.cat-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.cat-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
.cat-card {
|
||||
background: var(--surface-700);
|
||||
border: 1px solid var(--surface-600);
|
||||
border-radius: 16px;
|
||||
padding: 28px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
transition: border-color 0.25s, transform 0.25s, box-shadow 0.25s;
|
||||
}
|
||||
.cat-card:hover {
|
||||
border-color: rgba(0, 84, 230, 0.5);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 84, 230, 0.15);
|
||||
}
|
||||
|
||||
.cat-icon-wrap {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent-bright);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cat-title {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.cat-badge {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-bright);
|
||||
background: rgba(107, 159, 255, 0.12);
|
||||
border: 1px solid rgba(107, 159, 255, 0.2);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cat-desc {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cat-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--surface-600);
|
||||
}
|
||||
|
||||
.cat-tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.cat-tool-item::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-bright);
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Advantages ── */
|
||||
.adv-section {
|
||||
background: var(--surface-700);
|
||||
border-top: 1px solid var(--surface-600);
|
||||
border-bottom: 1px solid var(--surface-600);
|
||||
}
|
||||
|
||||
.adv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.adv-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.adv-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
.adv-card {
|
||||
padding: 36px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
border-right: 1px solid var(--surface-600);
|
||||
border-bottom: 1px solid var(--surface-600);
|
||||
}
|
||||
.adv-card:last-child { border-right: none; }
|
||||
@media (min-width: 640px) {
|
||||
.adv-card:nth-child(2) { border-right: none; }
|
||||
.adv-card:nth-child(3) { border-bottom: none; }
|
||||
.adv-card:nth-child(4) { border-right: none; border-bottom: none; }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.adv-card:nth-child(2) { border-right: 1px solid var(--surface-600); }
|
||||
.adv-card:nth-child(3) { border-bottom: 1px solid var(--surface-600); }
|
||||
.adv-card:last-child { border-right: none; border-bottom: none; }
|
||||
/* last row has no bottom border */
|
||||
.adv-card { border-bottom: none; }
|
||||
}
|
||||
|
||||
.adv-icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent-bright);
|
||||
}
|
||||
|
||||
.adv-title {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.adv-desc {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Final CTA ── */
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
padding: 100px 20px;
|
||||
}
|
||||
|
||||
.cta-glow {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.cta-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -40px -80px;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 84, 230, 0.12) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cta-headline {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.cta-sub {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(0.95rem, 1.8vw, 1.1rem);
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 auto 40px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* ── Nav ── */
|
||||
.landing-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px;
|
||||
background: rgba(6, 12, 24, 0);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: background 0.3s, border-color 0.3s, backdrop-filter 0.3s;
|
||||
}
|
||||
.landing-nav.scrolled {
|
||||
background: rgba(6, 12, 24, 0.85);
|
||||
border-color: var(--surface-600);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.nav-logo {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.nav-logo span { color: var(--accent); }
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.nav-link {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
.nav-link:hover { color: var(--text-primary); }
|
||||
.nav-btn {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 9px 20px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.nav-btn:hover { background: var(--accent-dim); }
|
||||
|
||||
/* ── Footer ── */
|
||||
.landing-footer {
|
||||
padding: 28px 32px;
|
||||
border-top: 1px solid var(--surface-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.footer-copy {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.footer-link {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.footer-link:hover { color: var(--text-secondary); }
|
||||
|
||||
/* ── Divider line with accent ── */
|
||||
.section-divider {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
NAV
|
||||
════════════════════════════════════════════ -->
|
||||
<nav class="landing-nav" id="mainNav">
|
||||
<a href="/" class="nav-logo">WA Dev <span>Tools</span></a>
|
||||
<div class="nav-actions">
|
||||
<a href="/auth/login" class="nav-link">Войти</a>
|
||||
<a href="/auth/register" class="nav-btn">Регистрация</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
HERO
|
||||
════════════════════════════════════════════ -->
|
||||
<section class="hero">
|
||||
<div class="hero-glow-1"></div>
|
||||
<div class="hero-glow-2"></div>
|
||||
<div class="hero-glow-3"></div>
|
||||
<div class="hero-grid"></div>
|
||||
|
||||
<div style="position:relative;z-index:1;width:100%;">
|
||||
<div class="hero-badge animate-fade-in">
|
||||
<span class="dot"></span>
|
||||
<span>13 инструментов — бесплатно навсегда</span>
|
||||
</div>
|
||||
|
||||
<div class="hero-logo animate-slide-up delay-100">
|
||||
WA Dev <span>Tools</span>
|
||||
</div>
|
||||
|
||||
<h1 class="hero-headline animate-slide-up delay-200">
|
||||
Инструменты для<br>
|
||||
<span class="accent">веб-разработчика</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-sub animate-slide-up delay-300">
|
||||
Сжатие картинок, форматирование кода, HTTP-клиент и ещё 10 инструментов —
|
||||
всё в браузере, бесплатно и без регистрации карты.
|
||||
</p>
|
||||
|
||||
<div class="hero-cta-group animate-slide-up delay-400">
|
||||
<a href="/auth/register" class="btn-primary">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 5H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6"/>
|
||||
<path d="M17 3h4v4"/>
|
||||
<path d="M10 14 21 3"/>
|
||||
</svg>
|
||||
Начать бесплатно
|
||||
</a>
|
||||
<a href="/auth/login" class="btn-secondary">
|
||||
<span class="link-text">Уже есть аккаунт? Войти</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#tools" class="scroll-hint" aria-label="Прокрутить вниз">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
<span>Подробнее</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
CATEGORIES
|
||||
════════════════════════════════════════════ -->
|
||||
<section class="section" id="tools">
|
||||
<div class="section-inner">
|
||||
<div class="reveal">
|
||||
<p class="section-label">Что внутри</p>
|
||||
<div class="section-divider"></div>
|
||||
<h2 class="section-title">Инструменты</h2>
|
||||
</div>
|
||||
|
||||
<div class="cat-grid">
|
||||
|
||||
<!-- Изображения -->
|
||||
<div class="cat-card reveal reveal-delay-1">
|
||||
<div class="cat-icon-wrap">
|
||||
<!-- Heroicons: photo -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="3"/>
|
||||
<path d="M3 9h18"/>
|
||||
<circle cx="9" cy="6" r="1" fill="currentColor" stroke="none"/>
|
||||
<path d="M3 15l4-4 4 4 3-3 5 5"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="cat-header">
|
||||
<span class="cat-title">Изображения</span>
|
||||
<span class="cat-badge">4 инструмента</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cat-desc">Сжатие, редактирование, SVG и placeholder-генератор</p>
|
||||
|
||||
<ul class="cat-tools">
|
||||
<li class="cat-tool-item">Сжатие картинок</li>
|
||||
<li class="cat-tool-item">Placeholder-генератор</li>
|
||||
<li class="cat-tool-item">SVG-редактор</li>
|
||||
<li class="cat-tool-item">Фоторедактор</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Код -->
|
||||
<div class="cat-card reveal reveal-delay-2">
|
||||
<div class="cat-icon-wrap">
|
||||
<!-- Heroicons: code-bracket -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m8 6-4 6 4 6"/>
|
||||
<path d="m16 6 4 6-4 6"/>
|
||||
<path d="m14 4-4 16"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="cat-header">
|
||||
<span class="cat-title">Код</span>
|
||||
<span class="cat-badge">3 инструмента</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cat-desc">Форматирование, очистка и конвертация кода между форматами</p>
|
||||
|
||||
<ul class="cat-tools">
|
||||
<li class="cat-tool-item">Форматирование кода</li>
|
||||
<li class="cat-tool-item">HTML Sanitizer</li>
|
||||
<li class="cat-tool-item">Конвертер форматов</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Веб -->
|
||||
<div class="cat-card reveal reveal-delay-3">
|
||||
<div class="cat-icon-wrap">
|
||||
<!-- Heroicons: globe-alt -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
<path d="M3.6 9h16.8M3.6 15h16.8"/>
|
||||
<path d="M12 3a14.4 14.4 0 0 1 0 18M12 3a14.4 14.4 0 0 0 0 18"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="cat-header">
|
||||
<span class="cat-title">Веб</span>
|
||||
<span class="cat-badge">3 инструмента</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cat-desc">HTTP-клиент, парсер страниц и анализ редиректов</p>
|
||||
|
||||
<ul class="cat-tools">
|
||||
<li class="cat-tool-item">HTTP-клиент</li>
|
||||
<li class="cat-tool-item">Парсер статей</li>
|
||||
<li class="cat-tool-item">Redirect-анализатор</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Утилиты -->
|
||||
<div class="cat-card reveal reveal-delay-4">
|
||||
<div class="cat-icon-wrap">
|
||||
<!-- Heroicons: wrench-screwdriver -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877"/>
|
||||
<path d="M11.42 15.17l-2.496-2.496a3.5 3.5 0 0 1-4.95-4.95l2.496 2.496"/>
|
||||
<path d="m6.496 10.22 4.258-4.257a3.5 3.5 0 0 1 4.95 4.95l-4.258 4.257"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="cat-header">
|
||||
<span class="cat-title">Утилиты</span>
|
||||
<span class="cat-badge">2 инструмента</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cat-desc">Генератор паролей и Markdown-просмотрщик с превью</p>
|
||||
|
||||
<ul class="cat-tools">
|
||||
<li class="cat-tool-item">Генератор паролей</li>
|
||||
<li class="cat-tool-item">Markdown Viewer</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
ADVANTAGES
|
||||
════════════════════════════════════════════ -->
|
||||
<section class="adv-section">
|
||||
<div style="max-width:1120px;margin:0 auto;">
|
||||
<div class="adv-grid">
|
||||
|
||||
<div class="adv-card reveal">
|
||||
<div class="adv-icon-wrap">
|
||||
<!-- Heroicons: gift -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 12v10H4V12"/>
|
||||
<path d="M22 7H2v5h20V7z"/>
|
||||
<path d="M12 22V7"/>
|
||||
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/>
|
||||
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adv-title">Бесплатно навсегда</div>
|
||||
<p class="adv-desc">Никаких подписок и скрытых платежей. Все 13 инструментов доступны сразу после регистрации.</p>
|
||||
</div>
|
||||
|
||||
<div class="adv-card reveal reveal-delay-1">
|
||||
<div class="adv-icon-wrap">
|
||||
<!-- Heroicons: computer-desktop -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||||
<path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adv-title">В браузере</div>
|
||||
<p class="adv-desc">Ничего устанавливать не нужно. Работает на любом устройстве с современным браузером.</p>
|
||||
</div>
|
||||
|
||||
<div class="adv-card reveal reveal-delay-2">
|
||||
<div class="adv-icon-wrap">
|
||||
<!-- Heroicons: shield-check -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
<path d="m9 12 2 2 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adv-title">Безопасно</div>
|
||||
<p class="adv-desc">Ваши файлы обрабатываются локально или транзитно и не сохраняются на нашем сервере.</p>
|
||||
</div>
|
||||
|
||||
<div class="adv-card reveal reveal-delay-3">
|
||||
<div class="adv-icon-wrap">
|
||||
<!-- Heroicons: bolt -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="adv-title">Быстро</div>
|
||||
<p class="adv-desc">Лёгкий интерфейс, мгновенная обработка без очередей и ожидания серверного ответа.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
FINAL CTA
|
||||
════════════════════════════════════════════ -->
|
||||
<section class="cta-section">
|
||||
<div class="cta-glow reveal">
|
||||
<p class="section-label" style="margin-bottom:16px;">Начните прямо сейчас</p>
|
||||
<h2 class="cta-headline">Готовы начать?</h2>
|
||||
<p class="cta-sub">
|
||||
Зарегистрируйтесь бесплатно и получите доступ ко всем инструментам — без кредитной карты.
|
||||
</p>
|
||||
<a href="/auth/register" class="btn-primary" style="font-size:17px;padding:17px 40px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<line x1="19" y1="8" x2="19" y2="14"/>
|
||||
<line x1="22" y1="11" x2="16" y2="11"/>
|
||||
</svg>
|
||||
Создать аккаунт
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════
|
||||
FOOTER
|
||||
════════════════════════════════════════════ -->
|
||||
<footer class="landing-footer">
|
||||
<span class="footer-copy">© 2024–2026 WA Dev Tools</span>
|
||||
<nav class="footer-links">
|
||||
<a href="/auth/login" class="footer-link">Войти</a>
|
||||
<a href="/auth/register" class="footer-link">Регистрация</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
/* ── Sticky nav ── */
|
||||
const nav = document.getElementById('mainNav');
|
||||
const onScroll = () => {
|
||||
nav.classList.toggle('scrolled', window.scrollY > 40);
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
/* ── Intersection observer for reveal animations ── */
|
||||
const revealEls = document.querySelectorAll('.reveal');
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
||||
|
||||
revealEls.forEach((el) => observer.observe(el));
|
||||
} else {
|
||||
// Fallback for old browsers
|
||||
revealEls.forEach((el) => el.classList.add('visible'));
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
95
public/login.html
Normal file
95
public/login.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Вход — WA Dev Tools</title>
|
||||
<script src="/vendor/tailwind.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: { extend: {
|
||||
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
|
||||
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
|
||||
}}
|
||||
};
|
||||
</script>
|
||||
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||
<link href="/shared.css" rel="stylesheet">
|
||||
<style>
|
||||
.auth-card { max-width: 400px; width: 100%; }
|
||||
.auth-input { width: 100%; background: var(--select-bg); border: 1px solid var(--surface-600); color: var(--select-color); padding: 12px 16px; border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 15px; outline: none; transition: border-color .2s; }
|
||||
.auth-input:focus { border-color: var(--accent); }
|
||||
.auth-btn { width: 100%; padding: 14px; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-family: 'Manrope', sans-serif; font-weight: 700; font-size: 16px; cursor: pointer; transition: background .2s; }
|
||||
.auth-btn:hover { background: var(--accent-dim); }
|
||||
.auth-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.auth-error { background: rgba(255,82,82,.1); border: 1px solid rgba(255,82,82,.3); color: #ff5252; padding: 10px 14px; border-radius: 8px; font-size: 13px; display: none; }
|
||||
.auth-error.visible { display: block; }
|
||||
.auth-link { color: var(--accent); text-decoration: none; }
|
||||
.auth-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;">
|
||||
<div class="auth-card">
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<a href="/" style="text-decoration:none;">
|
||||
<h1 style="font-size:28px;font-weight:800;color:var(--text-primary);font-family:'Manrope',sans-serif;">WA Dev <span style="color:var(--accent);">Tools</span></h1>
|
||||
</a>
|
||||
<p style="color:var(--text-muted);font-size:14px;margin-top:8px;">Войдите в аккаунт</p>
|
||||
</div>
|
||||
|
||||
<div id="error" class="auth-error"></div>
|
||||
|
||||
<form id="loginForm" style="display:flex;flex-direction:column;gap:16px;margin-top:16px;" onsubmit="return handleLogin(event)">
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Email</label>
|
||||
<input class="auth-input" type="email" name="email" required autofocus autocomplete="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Пароль</label>
|
||||
<input class="auth-input" type="password" name="password" required autocomplete="current-password" />
|
||||
</div>
|
||||
<button class="auth-btn" type="submit" id="submitBtn">Войти</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:24px;font-size:14px;color:var(--text-muted);">
|
||||
Нет аккаунта? <a href="/auth/register" class="auth-link">Зарегистрируйтесь</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const err = document.getElementById('error');
|
||||
err.classList.remove('visible');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Вход...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: form.email.value,
|
||||
password: form.password.value,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
window.location.href = data.redirect || '/dashboard';
|
||||
} else {
|
||||
err.textContent = data.error;
|
||||
err.classList.add('visible');
|
||||
}
|
||||
} catch {
|
||||
err.textContent = 'Ошибка сети';
|
||||
err.classList.add('visible');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Войти';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
112
public/register.html
Normal file
112
public/register.html
Normal file
@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Регистрация — WA Dev Tools</title>
|
||||
<script src="/vendor/tailwind.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: { extend: {
|
||||
fontFamily: { mono: ['JetBrains Mono', 'monospace'], sans: ['Manrope', 'sans-serif'] },
|
||||
colors: { surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' }, accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' } }
|
||||
}}
|
||||
};
|
||||
</script>
|
||||
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||
<link href="/shared.css" rel="stylesheet">
|
||||
<style>
|
||||
.auth-card { max-width: 400px; width: 100%; }
|
||||
.auth-input { width: 100%; background: var(--select-bg); border: 1px solid var(--surface-600); color: var(--select-color); padding: 12px 16px; border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 15px; outline: none; transition: border-color .2s; }
|
||||
.auth-input:focus { border-color: var(--accent); }
|
||||
.auth-btn { width: 100%; padding: 14px; background: var(--accent); color: #fff; border: none; border-radius: 10px; font-family: 'Manrope', sans-serif; font-weight: 700; font-size: 16px; cursor: pointer; transition: background .2s; }
|
||||
.auth-btn:hover { background: var(--accent-dim); }
|
||||
.auth-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.auth-error { background: rgba(255,82,82,.1); border: 1px solid rgba(255,82,82,.3); color: #ff5252; padding: 10px 14px; border-radius: 8px; font-size: 13px; display: none; }
|
||||
.auth-error.visible { display: block; }
|
||||
.auth-link { color: var(--accent); text-decoration: none; }
|
||||
.auth-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;">
|
||||
<div class="auth-card">
|
||||
<div style="text-align:center;margin-bottom:32px;">
|
||||
<a href="/" style="text-decoration:none;">
|
||||
<h1 style="font-size:28px;font-weight:800;color:var(--text-primary);font-family:'Manrope',sans-serif;">WA Dev <span style="color:var(--accent);">Tools</span></h1>
|
||||
</a>
|
||||
<p style="color:var(--text-muted);font-size:14px;margin-top:8px;">Создайте бесплатный аккаунт</p>
|
||||
</div>
|
||||
|
||||
<div id="error" class="auth-error"></div>
|
||||
|
||||
<form id="regForm" style="display:flex;flex-direction:column;gap:16px;margin-top:16px;" onsubmit="return handleRegister(event)">
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Имя</label>
|
||||
<input class="auth-input" type="text" name="name" placeholder="Как вас зовут" autocomplete="name" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Email</label>
|
||||
<input class="auth-input" type="email" name="email" required autofocus autocomplete="email" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Пароль</label>
|
||||
<input class="auth-input" type="password" name="password" required minlength="6" autocomplete="new-password" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:13px;color:var(--text-muted);margin-bottom:6px;font-family:'JetBrains Mono',monospace;">Подтвердите пароль</label>
|
||||
<input class="auth-input" type="password" name="password2" required minlength="6" autocomplete="new-password" />
|
||||
</div>
|
||||
<button class="auth-btn" type="submit" id="submitBtn">Зарегистрироваться</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:24px;font-size:14px;color:var(--text-muted);">
|
||||
Уже есть аккаунт? <a href="/auth/login" class="auth-link">Войти</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function handleRegister(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const err = document.getElementById('error');
|
||||
err.classList.remove('visible');
|
||||
|
||||
if (form.password.value !== form.password2.value) {
|
||||
err.textContent = 'Пароли не совпадают';
|
||||
err.classList.add('visible');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Регистрация...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: form.name.value,
|
||||
email: form.email.value,
|
||||
password: form.password.value,
|
||||
password2: form.password2.value,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
window.location.href = data.redirect || '/dashboard';
|
||||
} else {
|
||||
err.textContent = data.error;
|
||||
err.classList.add('visible');
|
||||
}
|
||||
} catch {
|
||||
err.textContent = 'Ошибка сети';
|
||||
err.classList.add('visible');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Зарегистрироваться';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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) }
|
||||
|
||||
@ -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: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>', tools: ['compress', 'placeholder', 'svgeditor', 'editor'] },
|
||||
{ id: 'code', title: 'Код', description: 'Форматирование, очистка и конвертация', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>', tools: ['formatter', 'sanitizer', 'converter'] },
|
||||
{ id: 'web', title: 'Веб', description: 'HTTP-клиент, парсер и анализ', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"/>', tools: ['parser', 'httpclient', 'redirects'] },
|
||||
{ id: 'utils', title: 'Утилиты', description: 'Пароли, Markdown и другие', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17l-5.645 3.232a.75.75 0 01-1.09-.791l1.078-6.285L1.347 7.06a.75.75 0 01.416-1.28l6.31-.917L10.89.598a.75.75 0 011.341 0l2.816 5.266 6.31.917a.75.75 0 01.416 1.28l-4.416 4.266 1.078 6.285a.75.75 0 01-1.09.791L12 15.17z"/>', tools: ['password', 'md'] },
|
||||
];
|
||||
|
||||
/* Sidebar tools definition */
|
||||
const WA_TOOLS = [
|
||||
{ id: 'home', path: '/', title: 'Главная', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>' },
|
||||
{ id: 'compress', path: '/compress', title: 'Картинки', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>' },
|
||||
{ id: 'md', path: '/md', title: 'Markdown', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/>' },
|
||||
{ id: 'placeholder', path: '/placeholder', title: 'Placeholder', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/>' },
|
||||
{ id: 'parser', path: '/parser', title: 'Parser', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/>' },
|
||||
{ id: 'password', path: '/password', title: 'Password', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/>' },
|
||||
{ id: 'sanitizer', path: '/sanitizer', title: 'Sanitizer', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/>' },
|
||||
{ id: 'converter', path: '/converter', title: 'Converter', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/>' },
|
||||
{ id: 'formatter', path: '/formatter', title: 'Formatter', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>' },
|
||||
{ id: 'editor', path: '/editor', title: 'Editor', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>' },
|
||||
{ id: 'httpclient', path: '/httpclient', title: 'HTTP Client', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/>' },
|
||||
{ id: 'redirects', path: '/redirects', title: 'Redirects', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>' },
|
||||
{ id: 'svgeditor', path: '/svgeditor', title: 'SVG Editor', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/>' },
|
||||
{ id: 'logs', path: '/logs', title: 'Logs', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z"/>' },
|
||||
{ id: 'home', path: '/dashboard', title: 'Главная', category: null, icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>' },
|
||||
// Изображения
|
||||
{ id: 'compress', path: '/compress', title: 'Сжатие', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/>' },
|
||||
{ id: 'placeholder', path: '/placeholder', title: 'Placeholder', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/>' },
|
||||
{ id: 'svgeditor', path: '/svgeditor', title: 'SVG', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/>' },
|
||||
{ id: 'editor', path: '/editor', title: 'Фото', category: 'images', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>' },
|
||||
// Код
|
||||
{ id: 'formatter', path: '/formatter', title: 'Formatter', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>' },
|
||||
{ id: 'sanitizer', path: '/sanitizer', title: 'Sanitizer', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/>' },
|
||||
{ id: 'converter', path: '/converter', title: 'Converter', category: 'code', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/>' },
|
||||
// Веб
|
||||
{ id: 'parser', path: '/parser', title: 'Parser', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/>' },
|
||||
{ id: 'httpclient', path: '/httpclient', title: 'HTTP', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/>' },
|
||||
{ id: 'redirects', path: '/redirects', title: 'Redirects', category: 'web', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>' },
|
||||
// Утилиты
|
||||
{ id: 'password', path: '/password', title: 'Пароли', category: 'utils', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/>' },
|
||||
{ id: 'md', path: '/md', title: 'Markdown', category: 'utils', icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/>' },
|
||||
// Admin
|
||||
{ id: 'logs', path: '/logs', title: 'Логи', category: null, icon: '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z"/>' },
|
||||
];
|
||||
|
||||
/* 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 =>
|
||||
`<a href="${t.path}" class="sidebar-link${t.id === activeId ? ' active' : ''}" title="${t.title}">${_svgIcon(t.icon)}</a>`
|
||||
).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 += '<div class="sidebar-divider"></div>';
|
||||
}
|
||||
lastCategory = t.category;
|
||||
|
||||
const isActive = t.id === activeId ? ' active' : '';
|
||||
linksHtml += `<a href="${t.path}" class="sidebar-link${isActive}" title="${t.title}">${_svgIcon(t.icon)}</a>\n`;
|
||||
}
|
||||
|
||||
const nav = document.createElement('nav');
|
||||
nav.className = 'sidebar';
|
||||
nav.innerHTML = `
|
||||
<div class="sidebar-logo">WA</div>
|
||||
<div class="sidebar-nav">
|
||||
${links}
|
||||
${linksHtml}
|
||||
</div>
|
||||
<div class="sidebar-bottom">
|
||||
<div class="sidebar-user" id="sidebarUser" style="display:none;">
|
||||
<div class="sidebar-avatar" id="sidebarAvatar" title="Профиль"></div>
|
||||
</div>
|
||||
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||
<div class="theme-toggle-knob">
|
||||
<svg class="wa-sun-icon" width="14" height="14" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||
@ -75,6 +101,30 @@ function initSidebar(activeId) {
|
||||
|
||||
document.body.prepend(nav);
|
||||
_updateThemeIcons();
|
||||
_loadUser();
|
||||
}
|
||||
|
||||
/* Load user info for sidebar avatar */
|
||||
async function _loadUser() {
|
||||
try {
|
||||
const res = await fetch('/auth/me');
|
||||
if (!res.ok) return;
|
||||
const user = await res.json();
|
||||
const avatar = document.getElementById('sidebarAvatar');
|
||||
const container = document.getElementById('sidebarUser');
|
||||
if (!avatar || !container) return;
|
||||
|
||||
const initials = (user.name || user.email || '?').charAt(0).toUpperCase();
|
||||
avatar.textContent = initials;
|
||||
avatar.title = user.name || user.email;
|
||||
container.style.display = 'block';
|
||||
|
||||
avatar.addEventListener('click', () => {
|
||||
if (confirm('Выйти из аккаунта?')) {
|
||||
window.location.href = '/auth/logout';
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* Theme management */
|
||||
|
||||
111
routes/auth.js
Normal file
111
routes/auth.js
Normal file
@ -0,0 +1,111 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const path = require('path');
|
||||
const db = require('../lib/db');
|
||||
const log = require('../lib/logger');
|
||||
|
||||
const router = express.Router();
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// Pages
|
||||
router.get('/login', (req, res) => {
|
||||
if (req.session.user) return res.redirect('/dashboard');
|
||||
res.sendFile(path.join(__dirname, '..', 'public', 'login.html'));
|
||||
});
|
||||
|
||||
router.get('/register', (req, res) => {
|
||||
if (req.session.user) return res.redirect('/dashboard');
|
||||
res.sendFile(path.join(__dirname, '..', 'public', 'register.html'));
|
||||
});
|
||||
|
||||
// Login API
|
||||
router.post('/login', express.json(), express.urlencoded({ extended: true }), async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email и пароль обязательны' });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await db.findUserByEmail(email.toLowerCase().trim());
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Неверный email или пароль' });
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ error: 'Неверный email или пароль' });
|
||||
}
|
||||
|
||||
await db.updateLastLogin(user.id);
|
||||
|
||||
req.session.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.display_name || user.email.split('@')[0],
|
||||
};
|
||||
|
||||
log.info(`Login: ${user.email}`);
|
||||
res.json({ ok: true, redirect: req.session.returnTo || '/dashboard' });
|
||||
delete req.session.returnTo;
|
||||
} catch (err) {
|
||||
log.error('Login error', { error: err.message });
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Register API
|
||||
router.post('/register', express.json(), express.urlencoded({ extended: true }), async (req, res) => {
|
||||
const { email, password, password2, name } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email и пароль обязательны' });
|
||||
}
|
||||
|
||||
const emailClean = email.toLowerCase().trim();
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailClean)) {
|
||||
return res.status(400).json({ error: 'Некорректный email' });
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({ error: 'Пароль минимум 6 символов' });
|
||||
}
|
||||
|
||||
if (password2 && password !== password2) {
|
||||
return res.status(400).json({ error: 'Пароли не совпадают' });
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await db.findUserByEmail(emailClean);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Этот email уже зарегистрирован' });
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
const displayName = (name || '').trim() || emailClean.split('@')[0];
|
||||
const userId = await db.createUser(emailClean, hash, displayName);
|
||||
|
||||
req.session.user = { id: userId, email: emailClean, name: displayName };
|
||||
|
||||
log.info(`Register: ${emailClean}`);
|
||||
res.json({ ok: true, redirect: '/dashboard' });
|
||||
} catch (err) {
|
||||
log.error('Register error', { error: err.message });
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
// Current user info
|
||||
router.get('/me', (req, res) => {
|
||||
if (!req.session.user) return res.status(401).json({ error: 'Not authenticated' });
|
||||
res.json(req.session.user);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -4,8 +4,14 @@ const path = require('path');
|
||||
const router = express.Router();
|
||||
const pub = (...p) => path.join(__dirname, '..', 'public', ...p);
|
||||
|
||||
router.get('/', (req, res) => res.sendFile(pub('index.html')));
|
||||
router.get('/home', (req, res) => res.sendFile(pub('index.html')));
|
||||
// Landing (public — served before auth middleware via express.static, but also as fallback route)
|
||||
router.get('/', (req, res) => res.sendFile(pub('landing.html')));
|
||||
|
||||
// Dashboard (protected — after auth)
|
||||
router.get('/dashboard', (req, res) => res.sendFile(pub('index.html')));
|
||||
router.get('/home', (req, res) => res.redirect('/dashboard'));
|
||||
|
||||
// Tools
|
||||
router.get('/md', (req, res) => res.sendFile(pub('md.html')));
|
||||
router.get('/editor', (req, res) => res.sendFile(pub('editor.html')));
|
||||
router.get('/sanitizer', (req, res) => res.sendFile(pub('sanitizer.html')));
|
||||
|
||||
@ -12,14 +12,17 @@ app.set('trust proxy', 1);
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'change-me-in-env',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 },
|
||||
}));
|
||||
|
||||
// Static assets (before auth — always public)
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Optional auth
|
||||
// Auth routes (before auth middleware — public)
|
||||
app.use('/auth', require('./routes/auth'));
|
||||
|
||||
// Auth middleware (protects everything below)
|
||||
app.use(authMiddleware);
|
||||
|
||||
// Health check (extended)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user