Загрузка данных
const express = require("express");
const path = require("path");
const crypto = require("crypto");
const { DatabaseSync } = require("node:sqlite");
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
// ==========================================
// DATABASE
// ==========================================
const db = new DatabaseSync(
path.join(__dirname, "roxBargain.db")
);
// ==========================================
// USERS
// ==========================================
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'buyer',
created_at TEXT NOT NULL
)
`);
// Исправление старой базы.
// Если users была создана предыдущей версией
// без поля role — добавляем его.
const userColumns = db
.prepare(`PRAGMA table_info(users)`)
.all();
const hasRole = userColumns.some(
column => column.name === "role"
);
if (!hasRole) {
db.exec(`
ALTER TABLE users
ADD COLUMN role TEXT NOT NULL DEFAULT 'buyer'
`);
console.log("База обновлена: добавлено поле role.");
}
// ==========================================
// SELLERS
// ==========================================
db.exec(`
CREATE TABLE IF NOT EXISTS sellers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
telegram TEXT NOT NULL,
price INTEGER NOT NULL,
rating REAL NOT NULL DEFAULT 5.0,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
// ==========================================
// SESSIONS
// ==========================================
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
// ==========================================
// ORDERS
// ==========================================
db.exec(`
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_code TEXT NOT NULL UNIQUE,
buyer_id INTEGER NOT NULL,
seller_id INTEGER NOT NULL,
robux INTEGER NOT NULL,
price_per_1000 INTEGER NOT NULL,
total_price INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'waiting',
created_at TEXT NOT NULL,
FOREIGN KEY (buyer_id) REFERENCES users(id),
FOREIGN KEY (seller_id) REFERENCES sellers(id)
)
`);
// ==========================================
// PASSWORD
// ==========================================
function hashPassword(password) {
const salt =
crypto.randomBytes(16).toString("hex");
const hash =
crypto.scryptSync(
password,
salt,
64
).toString("hex");
return `${salt}:${hash}`;
}
function checkPassword(password, stored) {
const parts = stored.split(":");
if (parts.length !== 2) {
return false;
}
const salt = parts[0];
const originalHash = parts[1];
const hash =
crypto.scryptSync(
password,
salt,
64
).toString("hex");
const a = Buffer.from(hash, "hex");
const b = Buffer.from(originalHash, "hex");
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
// ==========================================
// COOKIE
// ==========================================
function getCookie(req, name) {
const cookies = req.headers.cookie;
if (!cookies) {
return null;
}
for (const cookie of cookies.split(";")) {
const [key, ...value] =
cookie.trim().split("=");
if (key === name) {
return decodeURIComponent(
value.join("=")
);
}
}
return null;
}
// ==========================================
// CURRENT USER
// ==========================================
function getCurrentUser(req) {
const token =
getCookie(req, "rox_session");
if (!token) {
return null;
}
return db.prepare(`
SELECT
users.id,
users.username,
users.role
FROM sessions
JOIN users
ON users.id = sessions.user_id
WHERE sessions.token = ?
`).get(token) || null;
}
// ==========================================
// REGISTER
// ==========================================
app.post("/api/register", (req, res) => {
try {
const username =
String(req.body.username || "").trim();
const password =
String(req.body.password || "");
if (!username || !password) {
return res.status(400).json({
success: false,
message: "Заполни все поля"
});
}
if (!/^[a-zA-Z0-9_а-яА-ЯёЁ-]+$/.test(username)) {
return res.status(400).json({
success: false,
message:
"В имени можно использовать только буквы, цифры, _ и -"
});
}
if (username.length < 3) {
return res.status(400).json({
success: false,
message:
"Имя должно содержать минимум 3 символа"
});
}
if (username.length > 30) {
return res.status(400).json({
success: false,
message:
"Имя слишком длинное"
});
}
if (password.length < 6) {
return res.status(400).json({
success: false,
message:
"Пароль должен содержать минимум 6 символов"
});
}
const existing =
db.prepare(`
SELECT id
FROM users
WHERE username = ?
`).get(username);
if (existing) {
return res.status(409).json({
success: false,
message:
"Такой пользователь уже существует"
});
}
const passwordHash =
hashPassword(password);
db.prepare(`
INSERT INTO users
(
username,
password_hash,
role,
created_at
)
VALUES (?, ?, 'buyer', ?)
`).run(
username,
passwordHash,
new Date().toISOString()
);
console.log(
`Зарегистрирован пользователь: ${username}`
);
res.json({
success: true,
message:
"Регистрация успешна! Теперь войди."
});
} catch (error) {
console.error(
"Ошибка регистрации:",
error
);
res.status(500).json({
success: false,
message:
"Ошибка сервера при регистрации"
});
}
});
// ==========================================
// LOGIN
// ==========================================
app.post("/api/login", (req, res) => {
try {
const username =
String(req.body.username || "").trim();
const password =
String(req.body.password || "");
const user =
db.prepare(`
SELECT *
FROM users
WHERE username = ?
`).get(username);
if (!user) {
return res.status(401).json({
success: false,
message:
"Неверный логин или пароль"
});
}
if (!checkPassword(
password,
user.password_hash
)) {
return res.status(401).json({
success: false,
message:
"Неверный логин или пароль"
});
}
const token =
crypto.randomBytes(32).toString("hex");
db.prepare(`
INSERT INTO sessions
(
token,
user_id,
created_at
)
VALUES (?, ?, ?)
`).run(
token,
user.id,
new Date().toISOString()
);
res.setHeader(
"Set-Cookie",
`rox_session=${encodeURIComponent(token)}; HttpOnly; Path=/; SameSite=Lax`
);
res.json({
success: true,
username: user.username,
role: user.role
});
} catch (error) {
console.error("Ошибка входа:", error);
res.status(500).json({
success: false,
message:
"Ошибка сервера"
});
}
});
// ==========================================
// CURRENT USER
// ==========================================
app.get("/api/me", (req, res) => {
const user =
getCurrentUser(req);
if (!user) {
return res.json({
loggedIn: false
});
}
const seller =
db.prepare(`
SELECT
telegram,
price,
rating
FROM sellers
WHERE user_id = ?
`).get(user.id);
res.json({
loggedIn: true,
username: user.username,
role: user.role,
seller: seller || null
});
});
// ==========================================
// LOGOUT
// ==========================================
app.post("/api/logout", (req, res) => {
const token =
getCookie(req, "rox_session");
if (token) {
db.prepare(`
DELETE FROM sessions
WHERE token = ?
`).run(token);
}
res.setHeader(
"Set-Cookie",
"rox_session=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax"
);
res.json({
success: true
});
});
// ==========================================
// CREATE / UPDATE SELLER
// ==========================================
app.post("/api/seller", (req, res) => {
const user =
getCurrentUser(req);
if (!user) {
return res.status(401).json({
success: false,
message:
"Сначала войди в аккаунт"
});
}
const telegram =
String(req.body.telegram || "").trim();
const price =
Number(req.body.price);
if (!telegram || !price) {
return res.status(400).json({
success: false,
message:
"Заполни все поля"
});
}
if (
!Number.isInteger(price) ||
price <= 0
) {
return res.status(400).json({
success: false,
message:
"Некорректная цена"
});
}
const existing =
db.prepare(`
SELECT id
FROM sellers
WHERE user_id = ?
`).get(user.id);
if (existing) {
db.prepare(`
UPDATE sellers
SET
telegram = ?,
price = ?
WHERE user_id = ?
`).run(
telegram,
price,
user.id
);
} else {
db.prepare(`
INSERT INTO sellers
(
user_id,
telegram,
price,
rating,
created_at
)
VALUES (?, ?, ?, 5.0, ?)
`).run(
user.id,
telegram,
price,
new Date().toISOString()
);
}
db.prepare(`
UPDATE users
SET role = 'seller'
WHERE id = ?
`).run(user.id);
res.json({
success: true,
message:
"Профиль продавца сохранён"
});
});
// ==========================================
// SELLERS
// ==========================================
app.get("/api/sellers", (req, res) => {
const sellers =
db.prepare(`
SELECT
sellers.id,
users.username,
sellers.telegram,
sellers.price,
sellers.rating
FROM sellers
JOIN users
ON users.id = sellers.user_id
ORDER BY sellers.rating DESC
`).all();
res.json(sellers);
});
// ==========================================
// CREATE ORDER
// ==========================================
app.post("/api/orders", (req, res) => {
const user =
getCurrentUser(req);
if (!user) {
return res.status(401).json({
success: false,
message:
"Для заказа нужно войти в аккаунт"
});
}
const sellerId =
Number(req.body.sellerId);
const robux =
Number(req.body.robux);
if (
!Number.isInteger(sellerId) ||
!Number.isInteger(robux)
) {
return res.status(400).json({
success: false,
message:
"Некорректные данные"
});
}
if (robux < 100) {
return res.status(400).json({
success: false,
message:
"Минимальный заказ — 100 Robux"
});
}
if (robux > 1000000) {
return res.status(400).json({
success: false,
message:
"Слишком большое количество Robux"
});
}
if (robux % 100 !== 0) {
return res.status(400).json({
success: false,
message:
"Количество Robux должно быть кратно 100"
});
}
const seller =
db.prepare(`
SELECT
id,
user_id,
price
FROM sellers
WHERE id = ?
`).get(sellerId);
if (!seller) {
return res.status(404).json({
success: false,
message:
"Продавец не найден"
});
}
if (seller.user_id === user.id) {
return res.status(400).json({
success: false,
message:
"Нельзя заказать у самого себя"
});
}
const totalPrice =
Math.round(
robux / 1000 * seller.price
);
const orderCode =
"R" +
Date.now().toString(36).toUpperCase() +
crypto.randomBytes(3)
.toString("hex")
.toUpperCase();
db.prepare(`
INSERT INTO orders
(
order_code,
buyer_id,
seller_id,
robux,
price_per_1000,
total_price,
status,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, 'waiting', ?)
`).run(
orderCode,
user.id,
seller.id,
robux,
seller.price,
totalPrice,
new Date().toISOString()
);
res.json({
success: true,
orderCode,
robux,
totalPrice
});
});
// ==========================================
// GET MY ORDERS
// ==========================================
app.get("/api/orders", (req, res) => {
const user =
getCurrentUser(req);
if (!user) {
return res.status(401).json({
success: false,
message:
"Необходима авторизация"
});
}
const orders =
db.prepare(`
SELECT
orders.order_code,
orders.robux,
orders.price_per_1000,
orders.total_price,
orders.status,
orders.created_at,
buyer.username AS buyer,
seller_user.username AS seller,
sellers.telegram
FROM orders
JOIN users AS buyer
ON buyer.id = orders.buyer_id
JOIN sellers
ON sellers.id = orders.seller_id
JOIN users AS seller_user
ON seller_user.id = sellers.user_id
WHERE
orders.buyer_id = ?
OR sellers.user_id = ?
ORDER BY orders.id DESC
`).all(
user.id,
user.id
);
res.json(orders);
});
// ==========================================
// CHANGE ORDER STATUS
// ==========================================
app.patch(
"/api/orders/:code/status",
(req, res) => {
const user =
getCurrentUser(req);
if (!user) {
return res.status(401).json({
success: false,
message:
"Необходима авторизация"
});
}
const code =
req.params.code;
const status =
req.body.status;
const allowed = [
"waiting",
"working",
"completed",
"cancelled"
];
if (!allowed.includes(status)) {
return res.status(400).json({
success: false,
message:
"Недопустимый статус"
});
}
const order =
db.prepare(`
SELECT
orders.id,
orders.buyer_id,
sellers.user_id AS seller_user_id
FROM orders
JOIN sellers
ON sellers.id = orders.seller_id
WHERE orders.order_code = ?
`).get(code);
if (!order) {
return res.status(404).json({
success: false,
message:
"Заказ не найден"
});
}
// Пока менять статус может продавец
// или покупатель, если отменяет заказ.
if (
user.id !== order.seller_user_id &&
!(
user.id === order.buyer_id &&
status === "cancelled"
)
) {
return res.status(403).json({
success: false,
message:
"Нет доступа к изменению этого заказа"
});
}
db.prepare(`
UPDATE orders
SET status = ?
WHERE id = ?
`).run(
status,
order.id
);
res.json({
success: true,
message:
"Статус заказа изменён"
});
}
);
// ==========================================
// STATUS
// ==========================================
app.get("/api/status", (req, res) => {
res.json({
shop: "RoxBargain",
status: "online",
rate: 100
});
});
// ==========================================
// START
// ==========================================
app.listen(PORT, () => {
console.log("");
console.log("==============================");
console.log(" RoxBargain ONLINE");
console.log("==============================");
console.log(
`Сайт: http://localhost:${PORT}`
);
console.log("");
});