Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
207ec7e56b | ||
|
|
d920f1c559 |
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.env*
|
||||||
|
.vercel
|
||||||
|
*.log
|
||||||
|
.next
|
||||||
|
npm-debug.log*
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
# Скопируйте этот файл в .env.local и заполните реальными значениями.
|
# Скопируйте этот файл в .env.local и заполните реальными значениями.
|
||||||
# .env.local никогда не должен попадать в git (уже добавлен в .gitignore).
|
# .env.local никогда не должен попадать в git (уже добавлен в .gitignore).
|
||||||
|
|
||||||
|
# ВАЖНО: предыдущий токен (your_new_bot_token_from_botfather)
|
||||||
|
# был закоммичен в git и считается скомпрометированным.
|
||||||
|
# Его нужно отозвать через @BotFather (/revoke) и вписать сюда новый.
|
||||||
|
# В репозиторий реальные секреты не класть!
|
||||||
|
|
||||||
# Токен вашего Telegram-бота, полученный у @BotFather
|
# Токен вашего Telegram-бота, полученный у @BotFather
|
||||||
TELEGRAM_BOT_TOKEN=your_new_bot_token_from_botfather
|
TELEGRAM_BOT_TOKEN=your_new_bot_token_from_botfather
|
||||||
|
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# ---- deps ----
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
# ---- build ----
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- runtime ----
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
|
PORT=3000 \
|
||||||
|
HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
|
||||||
|
|
||||||
|
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
COPY --from=build --chown=nextjs:nodejs /app/public ./public
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -8,6 +8,42 @@ const MAX_LENGTHS = {
|
|||||||
message: 2000,
|
message: 2000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Rate limiting (in-memory, per IP) ---
|
||||||
|
// Простой sliding-window лимитер без внешних зависимостей.
|
||||||
|
// Для продакшена с несколькими инстансами замените на Redis/внешний стор.
|
||||||
|
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000; // 10 минут
|
||||||
|
const RATE_LIMIT_MAX = 5; // максимум 5 запросов за окно
|
||||||
|
const rateStore = new Map<string, { count: number; windowStart: number }>();
|
||||||
|
|
||||||
|
function getClientIp(req: NextRequest): string {
|
||||||
|
const forwarded = req.headers.get("x-forwarded-for");
|
||||||
|
if (forwarded) {
|
||||||
|
return forwarded.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
return req.headers.get("x-real-ip") ?? "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRateLimited(ip: string): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = rateStore.get(ip);
|
||||||
|
if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
||||||
|
rateStore.set(ip, { count: 1, windowStart: now });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entry.count += 1;
|
||||||
|
return entry.count > RATE_LIMIT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Периодическая очистка устаревших записей, чтобы Map не рос бесконечно.
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [ip, entry] of rateStore) {
|
||||||
|
if (now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
||||||
|
rateStore.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, RATE_LIMIT_WINDOW_MS).unref?.();
|
||||||
|
|
||||||
// Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать
|
// Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать
|
||||||
// форматирование сообщения в Telegram или протащить лишний код.
|
// форматирование сообщения в Telegram или протащить лишний код.
|
||||||
function sanitize(value: unknown, maxLength: number): string {
|
function sanitize(value: unknown, maxLength: number): string {
|
||||||
@@ -21,6 +57,15 @@ function escapeMarkdown(text: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
const ip = getClientIp(req);
|
||||||
|
if (isRateLimited(ip)) {
|
||||||
|
console.warn(`Rate limit exceeded for ${ip}`);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: false, error: "Слишком много запросов. Попробуйте позже" },
|
||||||
|
{ status: 429 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Секреты — только из .env (см. .env.local.example)
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env.local
|
||||||
|
restart: unless-stopped
|
||||||
@@ -17,6 +17,7 @@ const securityHeaders = [
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
|
output: "standalone",
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+6344
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^14.2.5",
|
"next": "^14.2.35",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"lucide-react": "^0.427.0"
|
"lucide-react": "^0.427.0"
|
||||||
@@ -23,6 +23,6 @@
|
|||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.7",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "^14.2.5"
|
"eslint-config-next": "^14.2.35"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user