Prepare production deploy for belixa.ru
This commit is contained in:
@@ -1,35 +1,47 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const ADMIN_HASHED_PASSWORD = "$2b$10$JQm79m.C64TWTwc7qmbJLOW2UnDLCUsL5e34eFb1MhRPqGBBxw0QK";
|
||||
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { password } = await req.json();
|
||||
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, ADMIN_HASHED_PASSWORD);
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Неверный пароль' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET!, { expiresIn: "1h" });
|
||||
|
||||
const res = NextResponse.json({ success: true });
|
||||
|
||||
res.cookies.set('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: 3600
|
||||
});
|
||||
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении запроса:', error);
|
||||
return NextResponse.json({ error: 'Произошла ошибка при обработке запроса.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
import { NextResponse } from 'next/server';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
function getAuthConfig() {
|
||||
const adminHash = process.env.ADMIN_HASHED_PASSWORD;
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
|
||||
if (!adminHash || !jwtSecret || jwtSecret === 'replace-with-generated-secret') {
|
||||
throw new Error('Admin auth environment is not configured');
|
||||
}
|
||||
|
||||
return { adminHash, jwtSecret };
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { password } = await req.json();
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
return NextResponse.json({ error: 'Некорректный пароль' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { adminHash, jwtSecret } = getAuthConfig();
|
||||
const isValid = await bcrypt.compare(password, adminHash);
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Неверный пароль' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ role: 'admin' }, jwtSecret, { expiresIn: '1h' });
|
||||
const res = NextResponse.json({ success: true });
|
||||
|
||||
res.cookies.set('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 3600,
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Admin login failed:', error);
|
||||
return NextResponse.json({ error: 'Произошла ошибка при обработке запроса.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,103 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin';
|
||||
import { bd } from '@/lib/bd';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await req.formData();
|
||||
|
||||
const title = formData.get('title');
|
||||
const platform = formData.get('platform');
|
||||
const image = formData.get('image') as File | null;
|
||||
|
||||
if (
|
||||
typeof title !== 'string' ||
|
||||
typeof platform !== 'string' ||
|
||||
!image ||
|
||||
typeof image !== 'object'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Заполните все поля корректно' }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = await image.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
const fileName = `${Date.now()}-${image.name.replace(/\s+/g, '_')}`;
|
||||
const uploadDir = path.join(process.cwd(), 'uploads/games');
|
||||
const filePath = path.join(uploadDir, fileName);
|
||||
|
||||
try {
|
||||
// создаём директорию если её нет
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
|
||||
// сохраняем файл
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
const [result] = await bd.query(
|
||||
'INSERT INTO games (title, image_url, platform) VALUES (?, ?, ?)',
|
||||
[title, fileName, platform]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
id: result.insertId,
|
||||
title,
|
||||
platform,
|
||||
image_url: fileName,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Ошибка при сохранении файла или в базе данных:', err);
|
||||
return NextResponse.json({ error: 'Ошибка сервера' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin';
|
||||
import { bd } from '@/lib/bd';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const MAX_IMAGE_SIZE = 8 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_TYPES: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
};
|
||||
|
||||
function detectImageExtension(buffer: Buffer, mimeType: string): string | null {
|
||||
const ext = ALLOWED_IMAGE_TYPES[mimeType];
|
||||
if (!ext) return null;
|
||||
|
||||
const isJpeg = buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
|
||||
const isPng = buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
const isGif = buffer.length >= 6 && (buffer.subarray(0, 6).toString('ascii') === 'GIF87a' || buffer.subarray(0, 6).toString('ascii') === 'GIF89a');
|
||||
const isWebp = buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP';
|
||||
|
||||
if (ext === 'jpg' && isJpeg) return ext;
|
||||
if (ext === 'png' && isPng) return ext;
|
||||
if (ext === 'gif' && isGif) return ext;
|
||||
if (ext === 'webp' && isWebp) return ext;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSafeImageName(ext: string): string {
|
||||
return `${Date.now()}-${crypto.randomUUID()}.${ext}`;
|
||||
}
|
||||
|
||||
function normalizeText(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await req.formData();
|
||||
const title = formData.get('title');
|
||||
const platform = formData.get('platform');
|
||||
const image = formData.get('image') as File | null;
|
||||
|
||||
if (
|
||||
typeof title !== 'string' ||
|
||||
typeof platform !== 'string' ||
|
||||
!image ||
|
||||
typeof image !== 'object'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Заполните все поля корректно' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (image.size <= 0 || image.size > MAX_IMAGE_SIZE) {
|
||||
return NextResponse.json({ error: 'Загрузите изображение JPG, PNG, WEBP или GIF до 8 МБ' }, { status: 400 });
|
||||
}
|
||||
|
||||
const normalizedTitle = normalizeText(title);
|
||||
const normalizedPlatform = normalizeText(platform);
|
||||
if (!normalizedTitle || !normalizedPlatform) {
|
||||
return NextResponse.json({ error: 'Заполните все поля корректно' }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = await image.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
const ext = detectImageExtension(buffer, image.type);
|
||||
if (!ext) {
|
||||
return NextResponse.json({ error: 'Загрузите корректное изображение JPG, PNG, WEBP или GIF' }, { status: 400 });
|
||||
}
|
||||
|
||||
const fileName = getSafeImageName(ext);
|
||||
const uploadDir = path.join(process.cwd(), 'uploads/games');
|
||||
const filePath = path.join(uploadDir, fileName);
|
||||
|
||||
try {
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
const [result] = await bd.query(
|
||||
'INSERT INTO games (title, image_url, platform) VALUES (?, ?, ?)',
|
||||
[normalizedTitle, fileName, normalizedPlatform]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
id: result.insertId,
|
||||
title: normalizedTitle,
|
||||
platform: normalizedPlatform,
|
||||
image_url: fileName,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to save game image:', err);
|
||||
return NextResponse.json({ error: 'Ошибка сервера' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const pathname = req.nextUrl.pathname; // /api/uploads/games/image.png
|
||||
const relativePath = pathname.replace(/^\/api\/uploads\//, ''); // games/image.png
|
||||
|
||||
const filePath = path.join(process.cwd(), 'uploads', ...relativePath.split('/'));
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(filePath).slice(1).toLowerCase();
|
||||
const contentTypes: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
};
|
||||
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentTypes[ext] || 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Файл не найден:', err);
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
const UPLOADS_DIR = path.join(process.cwd(), 'uploads');
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
};
|
||||
|
||||
function resolveUploadPath(relativePath: string): string | null {
|
||||
const requestedPath = path.normalize(relativePath).replace(/^(\.\.[/\\])+/, '');
|
||||
const filePath = path.resolve(UPLOADS_DIR, requestedPath);
|
||||
const relativeToUploads = path.relative(UPLOADS_DIR, filePath);
|
||||
|
||||
if (relativeToUploads.startsWith('..') || path.isAbsolute(relativeToUploads)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const pathname = decodeURIComponent(req.nextUrl.pathname);
|
||||
const relativePath = pathname.replace(/^\/api\/uploads\//, '');
|
||||
const filePath = resolveUploadPath(relativePath);
|
||||
|
||||
if (!filePath) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile()) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).slice(1).toLowerCase();
|
||||
const contentType = CONTENT_TYPES[ext];
|
||||
if (!contentType) {
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
return new NextResponse(new Uint8Array(fileBuffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=2592000, immutable',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Upload file not found:', err);
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,20 +10,20 @@ export const viewport = {
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://strike-arena.kz"),
|
||||
metadataBase: new URL("https://belixa.ru"),
|
||||
title: "Strike Arena — Киберклуб нового уровня",
|
||||
description: "Киберклуб Strike Arena: современные ПК и PS5, VIP-кабины, быстрая бронь, турниры и вознаграждения. Бронируй онлайн и выигрывай чаще.",
|
||||
keywords: ["Strike Arena", "киберклуб", "компьютерный клуб", "игровые залы", "бронь ПК", "PlayStation", "турниры", "игры", "e-sports"],
|
||||
applicationName: "Strike Arena",
|
||||
generator: "Next.js",
|
||||
manifest: "/site.webmanifest",
|
||||
authors: [{ name: "Strike Arena Team", url: "https://strike-arena.kz" }],
|
||||
authors: [{ name: "Strike Arena Team", url: "https://belixa.ru" }],
|
||||
creator: "Strike Arena",
|
||||
publisher: "Strike Arena",
|
||||
openGraph: {
|
||||
title: "Strike Arena — Игровой клуб нового поколения",
|
||||
description: "Современный киберклуб с бронью мест, турнирами, призами и VIP-комфортом.",
|
||||
url: "https://strike-arena.kz",
|
||||
url: "https://belixa.ru",
|
||||
siteName: "Strike Arena",
|
||||
images: [
|
||||
{
|
||||
|
||||
+19
-16
@@ -1,16 +1,19 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
interface AdminPayload {
|
||||
role: string
|
||||
}
|
||||
|
||||
export function verifyAdminToken(token: string | undefined): boolean {
|
||||
if (!token) return false
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as AdminPayload
|
||||
return decoded.role === 'admin'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
interface AdminPayload {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export function verifyAdminToken(token: string | undefined): boolean {
|
||||
if (!token) return false;
|
||||
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (!jwtSecret || jwtSecret === 'replace-with-generated-secret') return false;
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, jwtSecret) as AdminPayload;
|
||||
return decoded.role === 'admin';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+29
-30
@@ -1,30 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { jwtVerify } from "jose";
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const token = req.cookies.get("token")?.value;
|
||||
const url = req.nextUrl.clone();
|
||||
|
||||
if (req.nextUrl.pathname.startsWith("/admin") && req.nextUrl.pathname !== "/admin/login") {
|
||||
if (!token) {
|
||||
url.pathname = "/admin/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
if (payload.role !== "admin") throw new Error();
|
||||
} catch {
|
||||
url.pathname = "/admin/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/admin", "/admin/:path*"],
|
||||
};
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { jwtVerify } from 'jose';
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
const url = req.nextUrl.clone();
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
|
||||
if (req.nextUrl.pathname.startsWith('/admin') && req.nextUrl.pathname !== '/admin/login') {
|
||||
if (!token || !jwtSecret || jwtSecret === 'replace-with-generated-secret') {
|
||||
url.pathname = '/admin/login';
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, new TextEncoder().encode(jwtSecret));
|
||||
if (payload.role !== 'admin') throw new Error('Invalid role');
|
||||
} catch {
|
||||
url.pathname = '/admin/login';
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/admin', '/admin/:path*'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user