Spaces:
Running
Running
| /** | |
| * 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")); | |