Compare commits
No commits in common. "7d115e18455615663ed56eb08f13331999da430f" and "12c0870c8fafcdd8983ac9c97ad0abc08d5a253b" have entirely different histories.
7d115e1845
...
12c0870c8f
@ -8,7 +8,6 @@ const session = require("express-session");
|
|||||||
const SQLiteStore = require("connect-sqlite3")(session);
|
const SQLiteStore = require("connect-sqlite3")(session);
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const cors = require("cors");
|
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
const bodyParser = require("body-parser");
|
const bodyParser = require("body-parser");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
@ -26,38 +25,6 @@ const PORT = process.env.PORT || 3001;
|
|||||||
// Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For)
|
// Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For)
|
||||||
app.set("trust proxy", true);
|
app.set("trust proxy", true);
|
||||||
|
|
||||||
// Конфигурация CORS (защита от CSRF)
|
|
||||||
const corsOptions = {
|
|
||||||
origin: function (origin, callback) {
|
|
||||||
// Список разрешённых доменов
|
|
||||||
const allowedOrigins = [
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost:5173", // Vite dev server
|
|
||||||
"http://localhost:5174",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
"http://127.0.0.1:5173",
|
|
||||||
];
|
|
||||||
|
|
||||||
// В продакшене добавить домены вашего сайта
|
|
||||||
if (process.env.ALLOWED_ORIGINS) {
|
|
||||||
allowedOrigins.push(...process.env.ALLOWED_ORIGINS.split(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если origin не указан (например, для мобильного приложения), разрешаем
|
|
||||||
if (!origin || allowedOrigins.includes(origin)) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
callback(new Error("CORS не разрешен для этого домена"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
credentials: true, // разрешить куки
|
|
||||||
optionsSuccessStatus: 200,
|
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
|
||||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
|
||||||
|
|
||||||
// Создаем директорию для аватарок, если её нет
|
// Создаем директорию для аватарок, если её нет
|
||||||
const uploadsDir = path.join(__dirname, "public", "uploads");
|
const uploadsDir = path.join(__dirname, "public", "uploads");
|
||||||
if (!fs.existsSync(uploadsDir)) {
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
@ -218,42 +185,12 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ограничение запросов (включено для защиты от DDoS и брутфорса)
|
// Ограничение запросов (отключено для разработки)
|
||||||
const limiter = rateLimit({
|
// const limiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 минут
|
// windowMs: 15 * 60 * 1000, // 15 минут
|
||||||
max: 1000, // максимум 1000 запросов с одного IP
|
// max: 100, // максимум 100 запросов с одного IP
|
||||||
standardHeaders: true, // вернёт информацию о rate limit в `RateLimit-*` заголовки
|
// });
|
||||||
legacyHeaders: false, // отключить `X-RateLimit-*` заголовки
|
// app.use(limiter);
|
||||||
message: "Слишком много запросов с этого IP адреса, пожалуйста попробуйте позже.",
|
|
||||||
skip: (req) => {
|
|
||||||
// Пропускаем health check запросы
|
|
||||||
return req.path === "/health" || req.path === "/manifest.json";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
app.use(limiter);
|
|
||||||
|
|
||||||
// Специальный rate limiter для защиты от брутфорса (для логина и регистрации)
|
|
||||||
const authLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000, // 15 минут
|
|
||||||
max: 5, // максимум 5 попыток с одного IP
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
message: "Слишком много попыток входа. Попробуйте позже.",
|
|
||||||
skipSuccessfulRequests: true, // не считаем успешные попытки
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rate limiter для API запросов (более мягкий для обычных операций)
|
|
||||||
const apiLimiter = rateLimit({
|
|
||||||
windowMs: 5 * 60 * 1000, // 5 минут
|
|
||||||
max: 2000, // максимум 2000 запросов за 5 минут (400 в минуту)
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
message: "Слишком много запросов к API. Пожалуйста, попробуйте позже.",
|
|
||||||
skip: (req) => {
|
|
||||||
// Пропускаем GET запросы (безопасные, не требуют защиты)
|
|
||||||
return req.method === "GET";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Статические файлы
|
// Статические файлы
|
||||||
app.use(express.static(path.join(__dirname, "public")));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
@ -281,9 +218,6 @@ app.get("/browserconfig.xml", (req, res) => {
|
|||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
// Rate limiter для API (применяется ко всем API запросам)
|
|
||||||
app.use("/api/", apiLimiter);
|
|
||||||
|
|
||||||
// Настройка сессий с хранением в SQLite
|
// Настройка сессий с хранением в SQLite
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
@ -296,9 +230,7 @@ app.use(
|
|||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: process.env.NODE_ENV === "production", // куки только через HTTPS в продакшене
|
secure: false, // в продакшене установить true с HTTPS
|
||||||
httpOnly: true, // куки недоступны для JavaScript (защита от XSS)
|
|
||||||
sameSite: "strict", // защита от CSRF атак
|
|
||||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -853,7 +785,7 @@ app.get("/register", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API регистрации
|
// API регистрации
|
||||||
app.post("/api/register", authLimiter, async (req, res) => {
|
app.post("/api/register", async (req, res) => {
|
||||||
const { username, password, confirmPassword } = req.body;
|
const { username, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
@ -909,7 +841,7 @@ app.post("/api/register", authLimiter, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API входа
|
// API входа
|
||||||
app.post("/api/login", authLimiter, async (req, res) => {
|
app.post("/api/login", async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
@ -967,7 +899,7 @@ app.post("/api/login", authLimiter, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Обработка входа (старый маршрут для совместимости)
|
// Обработка входа (старый маршрут для совместимости)
|
||||||
app.post("/login", authLimiter, async (req, res) => {
|
app.post("/login", async (req, res) => {
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
const correctPassword = process.env.APP_PASSWORD;
|
const correctPassword = process.env.APP_PASSWORD;
|
||||||
|
|
||||||
@ -3029,7 +2961,7 @@ app.post("/api/user/2fa/disable", requireApiAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API для проверки 2FA кода при входе
|
// API для проверки 2FA кода при входе
|
||||||
app.post("/api/user/2fa/verify", authLimiter, async (req, res) => {
|
app.post("/api/user/2fa/verify", async (req, res) => {
|
||||||
const { username, token } = req.body;
|
const { username, token } = req.body;
|
||||||
|
|
||||||
if (!username || !token) {
|
if (!username || !token) {
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export const InstallPrompt: React.FC = () => {
|
|||||||
Установите приложение для быстрого доступа
|
Установите приложение для быстрого доступа
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
<button
|
<button
|
||||||
onClick={handleInstallClick}
|
onClick={handleInstallClick}
|
||||||
style={{
|
style={{
|
||||||
@ -137,8 +137,6 @@ export const InstallPrompt: React.FC = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "6px",
|
gap: "6px",
|
||||||
flex: "1 1 auto",
|
|
||||||
minWidth: "100px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:download" width="18" height="18" />
|
<Icon icon="mdi:download" width="18" height="18" />
|
||||||
@ -156,7 +154,6 @@ export const InstallPrompt: React.FC = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
flex: "0 0 auto",
|
|
||||||
}}
|
}}
|
||||||
aria-label="Закрыть"
|
aria-label="Закрыть"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -139,18 +139,18 @@ export const GenerateTagsModal: React.FC<GenerateTagsModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h4 style={{ margin: 0, fontSize: "16px" }}>Предлагаемые теги:</h4>
|
<h4 style={{ margin: 0, fontSize: "16px" }}>Предлагаемые теги:</h4>
|
||||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<button
|
<button
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onClick={handleSelectAll}
|
onClick={handleSelectAll}
|
||||||
style={{ fontSize: "12px", padding: "5px 10px", flex: "1 1 auto", minWidth: "100px" }}
|
style={{ fontSize: "12px", padding: "5px 10px" }}
|
||||||
>
|
>
|
||||||
Выбрать все
|
Выбрать все
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onClick={handleDeselectAll}
|
onClick={handleDeselectAll}
|
||||||
style={{ fontSize: "12px", padding: "5px 10px", flex: "1 1 auto", minWidth: "100px" }}
|
style={{ fontSize: "12px", padding: "5px 10px" }}
|
||||||
>
|
>
|
||||||
Снять все
|
Снять все
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1081,8 +1081,8 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
<FileUpload files={files} onChange={setFiles} />
|
<FileUpload files={files} onChange={setFiles} />
|
||||||
|
|
||||||
{user?.is_public_profile === 1 && (
|
{user?.is_public_profile === 1 && (
|
||||||
<div className="privacy-toggle-container" style={{ marginBottom: "10px", display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" }}>
|
<div className="privacy-toggle-container" style={{ marginBottom: "10px", display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer", flex: "1 1 auto" }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isPrivate}
|
checked={isPrivate}
|
||||||
|
|||||||
@ -255,12 +255,11 @@ export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<button
|
<button
|
||||||
className="btnSave"
|
className="btnSave"
|
||||||
onClick={handleEnable}
|
onClick={handleEnable}
|
||||||
disabled={isLoading || verificationCode.length !== 6}
|
disabled={isLoading || verificationCode.length !== 6}
|
||||||
style={{ flex: "1 1 auto", minWidth: "120px" }}
|
|
||||||
>
|
>
|
||||||
{isLoading ? "Включение..." : "Включить 2FA"}
|
{isLoading ? "Включение..." : "Включить 2FA"}
|
||||||
</button>
|
</button>
|
||||||
@ -271,7 +270,6 @@ export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
|||||||
setVerificationCode("");
|
setVerificationCode("");
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
style={{ flex: "1 1 auto", minWidth: "100px" }}
|
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
@ -307,12 +305,11 @@ export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
|||||||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
||||||
Двухфакторная аутентификация активна для вашего аккаунта.
|
Двухфакторная аутентификация активна для вашего аккаунта.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<button
|
<button
|
||||||
className="btnSave"
|
className="btnSave"
|
||||||
onClick={handleGenerateBackupCodes}
|
onClick={handleGenerateBackupCodes}
|
||||||
disabled={isGeneratingBackupCodes}
|
disabled={isGeneratingBackupCodes}
|
||||||
style={{ flex: "1 1 auto", minWidth: "200px" }}
|
|
||||||
>
|
>
|
||||||
{isGeneratingBackupCodes ? (
|
{isGeneratingBackupCodes ? (
|
||||||
<>
|
<>
|
||||||
@ -327,7 +324,6 @@ export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
|||||||
<button
|
<button
|
||||||
className="btn-danger"
|
className="btn-danger"
|
||||||
onClick={() => setShowDisableModal(true)}
|
onClick={() => setShowDisableModal(true)}
|
||||||
style={{ flex: "1 1 auto", minWidth: "140px" }}
|
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:shield-off" /> Отключить 2FA
|
<Icon icon="mdi:shield-off" /> Отключить 2FA
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -272,12 +272,11 @@ const LoginPage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "10px" }}>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btnSave"
|
className="btnSave"
|
||||||
disabled={isLoading || !twoFactorCode.trim()}
|
disabled={isLoading || !twoFactorCode.trim()}
|
||||||
style={{ flex: "1 1 auto", minWidth: "120px" }}
|
|
||||||
>
|
>
|
||||||
{isLoading ? "Проверка..." : "Продолжить"}
|
{isLoading ? "Проверка..." : "Продолжить"}
|
||||||
</button>
|
</button>
|
||||||
@ -286,7 +285,6 @@ const LoginPage: React.FC = () => {
|
|||||||
className="btn-danger"
|
className="btn-danger"
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
style={{ flex: "1 1 auto", minWidth: "100px" }}
|
|
||||||
>
|
>
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -127,7 +127,7 @@ const PublicProfilePage: React.FC = () => {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<header className="notes-header">
|
<header className="notes-header">
|
||||||
<div className="notes-header-left">
|
<div className="notes-header-left">
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "15px", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||||
{profile.avatar ? (
|
{profile.avatar ? (
|
||||||
<img
|
<img
|
||||||
src={profile.avatar}
|
src={profile.avatar}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user