Загрузка данных


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("");
});