usagi-server / server.js
eienmojiki's picture
Initial deploy
9ecc700
/**
* server.js
* Express API server (replaces api.php + proxy_check.php)
* All Telegram features have been removed.
* Token-based ext login has been removed — extension works without login.
*/
const express = require("express");
const cors = require("cors");
const crypto = require("crypto");
const path = require("path");
const https = require("https");
const http = require("http");
const fs = require("fs");
const Database = require("./database");
const gitSync = require("./git-sync");
// ─── Git sync: pull data from GitHub before starting ────────────
gitSync.initSync();
const app = express();
const PORT = process.env.PORT || 7860; // 7860 = HF Spaces default
const db = new Database(path.join(__dirname, "data"));
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Auto-migrate / sync hit counters on startup
db.migrateOldHitCounters();
db.autoSyncHitCounters();
// ─── Helpers ────────────────────────────────────────────────────
function apiResponse(res, success, data = {}, error = null, status = 200) {
const response = { success, ...data };
if (error) response.error = error;
return res.status(status).json(response);
}
function getInput(req, key, defaultVal = null) {
return req.query[key] || req.body?.[key] || defaultVal;
}
function sanitize(input, maxLen = 500) {
if (!input) return "";
input = String(input)
.trim()
.replace(/<[^>]*>/g, "");
return input.length > maxLen ? input.substring(0, maxLen) : input;
}
function generateToken(length = 15) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let token = "";
for (let i = 0; i < length; i++) {
token += chars[crypto.randomInt(0, chars.length)];
}
return token;
}
function generateLicenseKey(version) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let random = "";
for (let i = 0; i < 20; i++) {
random += chars[crypto.randomInt(0, chars.length)];
}
return `USAGI-${version.replace(/\./g, "")}-${random}`;
}
function requireToken(req, res) {
const token = getInput(req, "token");
if (!token) return apiResponse(res, false, {}, "Token required", 400);
if (token.length !== 15)
return apiResponse(res, false, {}, "Invalid token format (must be 15 chars)", 400);
const user = db.getUserByToken(token);
if (!user) return apiResponse(res, false, {}, "Invalid token", 401);
if (user.banned) return apiResponse(res, false, {}, "Account suspended", 403);
return user;
}
// ─── Feedback helpers (concurrent-safe) ──────────────────────────
const feedbackFile = path.join(__dirname, "feedback.json");
function readFeedback() {
try {
if (!fs.existsSync(feedbackFile)) return {};
return JSON.parse(fs.readFileSync(feedbackFile, "utf-8")) || {};
} catch (e) {
return {};
}
}
function writeFeedback(data) {
const tmp = feedbackFile + ".tmp." + process.pid;
try {
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
fs.renameSync(tmp, feedbackFile);
return true;
} catch (e) {
try {
fs.unlinkSync(tmp);
} catch (_) {}
return false;
}
}
// ─── Routes ─────────────────────────────────────────────────────
// GET/POST actions all route through ?action=...
app.all("/api", (req, res) => {
const action = getInput(req, "action", "");
switch (action) {
case "":
case "index":
return apiResponse(res, true, {
service: "UsagiAutoX API",
version: "2.5",
status: "online",
});
case "health":
return apiResponse(res, true, {
status: "healthy",
timestamp: new Date().toISOString(),
data_dir_writable: true,
});
// ── License key check ──
case "check-key": {
const key = getInput(req, "key");
if (!key) return apiResponse(res, false, {}, "License key required", 400);
const result = db.validateLicenseKey(key);
const latestVersion = db.getSetting("latest_version") || "1.0.7";
if (result.valid) {
return apiResponse(res, true, {
valid: true,
version: result.version || "",
latest_version: latestVersion,
message: "License key is valid",
});
} else {
return apiResponse(
res,
false,
{
valid: false,
latest_version: latestVersion,
message: "Invalid or expired license key.",
},
null,
403,
);
}
}
// ── Token validation ──
case "validate": {
const token = getInput(req, "token");
if (!token) return apiResponse(res, false, {}, "Token required", 400);
if (token.length !== 15)
return apiResponse(res, false, {}, "Token must be 15 characters", 400);
const user = db.getUserByToken(token);
if (!user) return apiResponse(res, false, {}, "Invalid token", 401);
if (user.banned) return apiResponse(res, false, {}, "Account suspended", 403);
db.updateUser(user.user_id, { last_active: new Date().toISOString() });
const counts = db.getHitCounts(user.user_id);
return apiResponse(res, true, {
user_id: String(user.user_id),
username: user.username || "",
first_name: user.first_name || "",
pfp_url: user.pfp_url || "",
hits: counts.user_hits,
attempts: user.attempts || 0,
global_hits: counts.global_hits,
user_hits: counts.user_hits,
});
}
// ── Attempt ──
case "attempt": {
const user = requireToken(req, res);
if (!user.user_id) return; // response already sent
const newAttempts = db.incrementStat(user.user_id, "attempts");
return apiResponse(res, true, {
user_id: String(user.user_id),
attempts: newAttempts,
message: "Attempt recorded",
});
}
// ── Hit ──
case "hit": {
const user = requireToken(req, res);
if (!user.user_id) return;
const fullCard = getInput(req, "full_card", "");
let currency = getInput(req, "currency", "usd");
if (!currency || currency === "null") currency = "usd";
if (!fullCard || fullCard === "null" || fullCard.length < 13 || !fullCard.includes("|")) {
return apiResponse(res, false, {}, `Invalid card data: ${fullCard || "empty"}`, 400);
}
const rawAmount = getInput(req, "amount", "0") || "0";
const details = {
full_card: sanitize(fullCard, 100),
amount: sanitize(rawAmount, 20),
currency: sanitize(currency, 10),
merchant: sanitize(getInput(req, "merchant", "") || "", 100),
};
db.incrementStat(user.user_id, "hits");
db.addHitRecord(user.user_id, details);
const counts = db.getHitCounts(user.user_id);
return apiResponse(res, true, {
user_id: String(user.user_id),
hits: counts.user_hits,
global_hits: counts.global_hits,
user_hits: counts.user_hits,
message: "Hit recorded successfully",
});
}
// ── User info ──
case "user": {
const userId = getInput(req, "user_id");
if (!userId) return apiResponse(res, false, {}, "User ID required", 400);
const user = db.getUser(userId);
if (!user) return apiResponse(res, false, {}, "User not found", 404);
return apiResponse(res, true, {
user_id: String(user.user_id),
username: user.username || "",
pfp_url: user.pfp_url || "",
hits: user.hits || 0,
attempts: user.attempts || 0,
});
}
// ── Stats ──
case "stats": {
const stats = db.getGlobalStats();
return apiResponse(res, true, {
total_users: stats.total_users,
total_attempts: stats.total_attempts,
total_hits: stats.total_hits,
});
}
// ── Leaderboard ──
case "leaderboard": {
const limit = Math.max(1, Math.min(50, parseInt(getInput(req, "limit", "10")) || 10));
const topUsers = db.getTopUsers(limit);
const leaderboard = topUsers.map((u, i) => ({
rank: i + 1,
user_id: String(u.user_id),
username: u.username || "",
first_name: u.first_name || "",
pfp_url: u.pfp_url || "",
hits: u.hits || 0,
}));
const reqUserId = getInput(req, "user_id");
const reqToken = getInput(req, "token");
let resolvedUserId = reqUserId;
if (reqToken && reqToken.length === 15) {
const reqUser = db.getUserByToken(reqToken);
if (reqUser) resolvedUserId = String(reqUser.user_id);
}
const response = { leaderboard };
if (resolvedUserId) {
const myRank = db.getUserRank(resolvedUserId);
if (myRank) response.my_rank = myRank;
}
return apiResponse(res, true, response);
}
// ── Popup dashboard stats ──
case "popup-stats": {
const users = db.getAllUsers();
const hits = db.getAllHits();
const today = new Date().toISOString().slice(0, 10);
const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
let hitsToday = 0,
hitsWeek = 0;
for (const h of hits) {
const d = (h.timestamp || "").slice(0, 10);
if (d === today) hitsToday++;
if (d >= weekAgo) hitsWeek++;
}
return apiResponse(res, true, {
total_users: users.length,
total_hits: hits.length,
hits_today: hitsToday,
hits_week: hitsWeek,
});
}
// ── Hit counts ──
case "hit-counts": {
const token = getInput(req, "token");
let userId = getInput(req, "user_id");
if (token && token.length === 15) {
const user = db.getUserByToken(token);
if (user && !user.banned) userId = String(user.user_id);
}
const counts = db.getHitCounts(userId || null);
return apiResponse(res, true, {
global_hits: counts.global_hits,
user_hits: counts.user_hits,
});
}
// ── User hits history ──
case "user-hits": {
const user = requireToken(req, res);
if (!user.user_id) return;
const limit = Math.max(1, Math.min(500, parseInt(getInput(req, "limit", "50")) || 50));
const allUserHits = db.getUserHits(user.user_id, limit);
const totalCount = db.getUserHitsTotal(user.user_id);
return apiResponse(res, true, {
user_id: String(user.user_id),
hits: allUserHits,
total_count: totalCount,
});
}
// ── Create user ──
case "create-user": {
const userId = getInput(req, "user_id");
const username = sanitize(getInput(req, "username", "") || "", 100);
const firstName = sanitize(getInput(req, "first_name", "") || "", 100);
const pfpUrl = sanitize(getInput(req, "pfp_url", "") || "", 500);
if (!userId) return apiResponse(res, false, {}, "User ID required", 400);
const existing = db.getUser(userId);
if (existing) {
const updates = {};
if (pfpUrl) updates.pfp_url = pfpUrl;
if (username) updates.username = username;
if (firstName) updates.first_name = firstName;
if (Object.keys(updates).length) db.updateUser(userId, updates);
return apiResponse(res, true, {
user_id: String(existing.user_id),
message: "User already exists",
token: existing.token || null,
});
}
const user = db.createUser({
user_id: userId,
username,
first_name: firstName,
pfp_url: pfpUrl,
});
return apiResponse(res, true, {
user_id: String(user.user_id),
message: "User created",
});
}
// ── Generate token ──
case "generate-token": {
const userId = getInput(req, "user_id");
const forceNew = getInput(req, "force_new", "false") === "true";
if (!userId) return apiResponse(res, false, {}, "User ID required", 400);
const user = db.getUser(userId);
if (!user) return apiResponse(res, false, {}, "User not found", 404);
if (forceNew || !user.token) {
let token = generateToken();
let retries = 10;
while (db.isTokenExists(token) && retries-- > 0) {
token = generateToken();
}
db.updateUser(userId, { token });
return apiResponse(res, true, {
user_id: String(userId),
token,
message: forceNew ? "Token regenerated" : "New token generated",
});
}
return apiResponse(res, true, {
user_id: String(userId),
token: user.token,
message: "Existing token returned",
});
}
// ── BIN library ──
case "bin-library": {
const bins = db.getBinLibrary();
const userId = getInput(req, "user_id");
const allFeedback = readFeedback();
let needsUpdate = false;
for (const bin of bins) {
if (!bin.id) {
bin.id = "bin_" + Date.now() + "_" + Math.random().toString(36).substr(2, 5);
needsUpdate = true;
}
const fb = allFeedback[bin.id];
bin.likes = fb ? fb.likes || 0 : 0;
bin.dislikes = fb ? fb.dislikes || 0 : 0;
bin.user_vote = (userId && fb?.voters?.[userId]?.vote) || null;
}
if (needsUpdate) db.saveBinLibrary(bins);
return apiResponse(res, true, { bins });
}
// ── BIN feedback ──
case "bin-feedback": {
const binId = getInput(req, "id");
const vote = getInput(req, "vote");
const userId = getInput(req, "user_id");
const userName = sanitize(getInput(req, "user_name", "") || "", 100);
if (!binId || !vote || !userId) {
return apiResponse(res, false, {}, "ID, vote, and user_id required", 400);
}
if (!["like", "dislike"].includes(vote)) {
return apiResponse(res, false, {}, 'Vote must be "like" or "dislike"', 400);
}
const feedback = readFeedback();
if (!feedback[binId]) feedback[binId] = { likes: 0, dislikes: 0, voters: {} };
if (!feedback[binId].voters) feedback[binId].voters = {};
const previousVote = feedback[binId].voters[userId]?.vote || null;
if (previousVote === vote) {
return apiResponse(res, true, {
message: "Already voted",
likes: feedback[binId].likes,
dislikes: feedback[binId].dislikes,
user_vote: vote,
id: binId,
});
}
// Undo previous vote
if (previousVote === "like") feedback[binId].likes = Math.max(0, feedback[binId].likes - 1);
else if (previousVote === "dislike")
feedback[binId].dislikes = Math.max(0, feedback[binId].dislikes - 1);
// Apply new vote
if (vote === "like") feedback[binId].likes++;
else if (vote === "dislike") feedback[binId].dislikes++;
feedback[binId].voters[userId] = {
vote,
name: userName || "Unknown",
time: new Date().toISOString(),
};
writeFeedback(feedback);
return apiResponse(res, true, {
message: "Feedback recorded",
likes: feedback[binId].likes,
dislikes: feedback[binId].dislikes,
user_vote: vote,
id: binId,
changed_from: previousVote,
});
}
// ═══════════════════ Admin endpoints ═══════════════════
case "admin-genkey": {
const version = getInput(req, "version");
if (!version) return apiResponse(res, false, {}, "Version required", 400);
const key = generateLicenseKey(version);
db.createLicenseKey(key, version, true);
return apiResponse(res, true, { key, version, active: true });
}
case "admin-listkeys":
return apiResponse(res, true, { keys: db.getAllLicenseKeys() });
case "admin-revokekey": {
const key = getInput(req, "key");
if (!key) return apiResponse(res, false, {}, "Key required", 400);
return db.revokeLicenseKey(key)
? apiResponse(res, true, { message: "Key revoked", key })
: apiResponse(res, false, {}, "Key not found", 404);
}
case "admin-activatekey": {
const key = getInput(req, "key");
if (!key) return apiResponse(res, false, {}, "Key required", 400);
return db.activateLicenseKey(key)
? apiResponse(res, true, { message: "Key activated", key })
: apiResponse(res, false, {}, "Key not found", 404);
}
case "admin-deletekey": {
const key = getInput(req, "key");
if (!key) return apiResponse(res, false, {}, "Key required", 400);
return db.deleteLicenseKey(key)
? apiResponse(res, true, { message: "Key deleted", key })
: apiResponse(res, false, {}, "Key not found", 404);
}
case "admin-setsetting": {
const name = getInput(req, "name");
const value = getInput(req, "value");
if (!name) return apiResponse(res, false, {}, "Setting name required", 400);
db.setSetting(name, value);
return apiResponse(res, true, { message: "Setting saved", name, value });
}
case "admin-getsettings":
return apiResponse(res, true, { settings: db.getAllSettings() });
case "admin-cleartokens": {
const count = db.clearAllTokensAndStats();
return apiResponse(res, true, { message: "Tokens and stats cleared", users_reset: count });
}
case "admin-cleanfakehits": {
const deleted = db.cleanFakeHits();
return apiResponse(res, true, { message: "Fake hits cleaned", deleted });
}
case "admin-resetdb": {
if (getInput(req, "confirm") !== "yes") {
return apiResponse(res, false, {}, "Add confirm=yes to reset database", 400);
}
const result = db.resetDatabase();
return apiResponse(res, true, { message: "Database reset complete", deleted: result });
}
case "admin-banuser": {
const userId = getInput(req, "user_id");
if (!userId) return apiResponse(res, false, {}, "User ID required", 400);
return db.updateUser(userId, { banned: true })
? apiResponse(res, true, { message: "User banned", user_id: userId })
: apiResponse(res, false, {}, "User not found", 404);
}
case "admin-unbanuser": {
const userId = getInput(req, "user_id");
if (!userId) return apiResponse(res, false, {}, "User ID required", 400);
return db.updateUser(userId, { banned: false })
? apiResponse(res, true, { message: "User unbanned", user_id: userId })
: apiResponse(res, false, {}, "User not found", 404);
}
case "admin-allusers":
return apiResponse(res, true, { users: db.getAllUsers() });
case "admin-stats": {
const users = db.getAllUsers();
const keys = db.getAllLicenseKeys();
const hits = db.getAllHits();
const bins = db.getBinLibrary();
let activeKeys = 0,
revokedKeys = 0;
for (const k of keys) {
k.active !== false ? activeKeys++ : revokedKeys++;
}
const today = new Date().toISOString().slice(0, 10);
const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
let hitsToday = 0,
hitsWeek = 0;
const activeToday = new Set();
for (const h of hits) {
const d = (h.timestamp || "").slice(0, 10);
if (d === today) hitsToday++;
if (d >= weekAgo) hitsWeek++;
}
for (const u of users) {
if ((u.last_active || "").slice(0, 10) === today) {
activeToday.add(u.user_id);
}
}
return apiResponse(res, true, {
stats: {
total_users: users.length,
active_today: activeToday.size,
total_keys: keys.length,
active_keys: activeKeys,
revoked_keys: revokedKeys,
total_hits: hits.length,
hits_today: hitsToday,
hits_week: hitsWeek,
total_bins: bins.length,
},
});
}
case "admin-allhits":
return apiResponse(res, true, { hits: db.getAllHits() });
case "admin-addbin": {
const site = sanitize(getInput(req, "site", "Unknown") || "Unknown", 200);
const bin = getInput(req, "bin");
const credit = sanitize(getInput(req, "credit", "Unknown") || "Unknown", 200);
if (!bin) return apiResponse(res, false, {}, "BIN is required", 400);
const newBin = {
id: "bin_" + Date.now(),
site,
bin: sanitize(bin, 20),
credit,
likes: 0,
dislikes: 0,
added_at: new Date().toISOString(),
};
return db.addBinToLibrary(newBin)
? apiResponse(res, true, { message: "BIN added to library", bin: newBin })
: apiResponse(res, false, {}, "Failed to add BIN", 500);
}
case "admin-removebin": {
const binId = getInput(req, "bin_id");
const binNumber = getInput(req, "bin");
if (!binId && !binNumber)
return apiResponse(res, false, {}, "BIN ID or BIN number required", 400);
const removed = db.removeBinFromLibrary(binId, binNumber);
return removed
? apiResponse(res, true, {
message: "BIN removed from library",
bin: removed.bin || "N/A",
site: removed.site || "N/A",
id: removed.id || binId,
})
: apiResponse(res, false, {}, "BIN not found", 404);
}
case "admin-rebuild-hits": {
const result = db.rebuildHitCounters();
return apiResponse(res, true, {
message: "Hit counters rebuilt from users.json",
total_hits: result.total_hits,
users_with_hits: result.users_with_hits,
});
}
case "admin-clearbin":
return db.clearBinLibrary()
? apiResponse(res, true, { message: "BIN library cleared" })
: apiResponse(res, false, {}, "Failed to clear library", 500);
case "debug": {
const users = db.getAllUsers();
const hits = db.getAllHits();
return apiResponse(res, true, {
users_count: users.length,
hits_count: hits.length,
data_dir: path.join(__dirname, "data"),
node_version: process.version,
users,
recent_hits: hits.slice(-5),
});
}
default:
return apiResponse(res, false, {}, `Unknown action: ${sanitize(action, 50)}`, 400);
}
});
// ─── Proxy check endpoint (replaces proxy_check.php) ────────────
app.all("/proxy_check", async (req, res) => {
let rawProxy = "";
if (req.method === "POST") {
rawProxy = req.body?.proxy || "";
} else {
rawProxy = req.query.proxy || "";
}
if (!rawProxy) {
return res.json({ success: false, status: "fail", error: "No proxy provided" });
}
rawProxy = rawProxy.trim();
try {
const checkUrl = `https://proxyshare.com/api/check-proxy?proxy=${encodeURIComponent(rawProxy)}`;
const response = await fetch(checkUrl, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(50000),
});
if (!response.ok) {
return res.json({
success: false,
status: "fail",
error: `Upstream HTTP ${response.status}`,
});
}
const data = await response.json();
return res.json(data);
} catch (e) {
return res.json({ success: false, status: "fail", error: e.message || "Proxy check failed" });
}
});
// ─── Start server ───────────────────────────────────────────────
app.listen(PORT, "0.0.0.0", () => {
console.log(`✅ UsagiAutoX API running on http://0.0.0.0:${PORT}`);
console.log(` API endpoint: http://localhost:${PORT}/api?action=health`);
console.log(` Proxy check: http://localhost:${PORT}/proxy_check?proxy=...`);
console.log(
` Git sync: ${gitSync.isConfigured() ? "ENABLED" : "DISABLED (set GITHUB_TOKEN + GITHUB_REPO)"}`,
);
});
// ─── Graceful shutdown: push pending data before exit ───────────
async function gracefulShutdown(signal) {
console.log(`\n🛑 ${signal} received, syncing data...`);
try {
await gitSync.forceSync();
console.log("✅ Data synced, exiting.");
} catch (e) {
console.error("⚠️ Final sync failed:", e.message);
}
process.exit(0);
}
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));