Загрузка данных
const axios = require('axios');
const crypto = require('crypto');
function getInboundList() {
const list = [];
for (const key in process.env) {
const match = key.match(/^INBOUND_(\d+)_LABEL$/);
if (match) {
const id = match[1];
list.push({
key: id,
label: process.env[`INBOUND_${id}_LABEL`],
panel1: process.env[`INBOUND_${id}_PANEL1`],
panel2: process.env[`INBOUND_${id}_PANEL2`],
show: String(process.env[`INBOUND_${id}_SHOW`] || 'true').toLowerCase() === 'true'
});
}
}
return list.sort((a, b) => Number(a.key) - Number(b.key));
}
function buildSubscriptionUrl(subId) {
const subBase =
process.env.SUBSCRIPTION_BASE_URL ||
process.env.PANEL1_SUB_BASE ||
process.env.PANEL1_URL ||
'';
if (!subBase) {
throw new Error('Не заполнен SUBSCRIPTION_BASE_URL');
}
return `${subBase.replace(/\/$/, '')}/${subId}`;
}
async function login(panelUrl, username, password) {
if (!panelUrl) {
throw new Error('Не заполнен URL панели');
}
const client = axios.create({
baseURL: panelUrl,
withCredentials: true,
timeout: 20000,
validateStatus: () => true
});
const res = await client.post('/login', {
username,
password
});
const cookies = res.headers['set-cookie'];
if (!cookies || !cookies.length) {
throw new Error(`Ошибка логина в панель ${panelUrl}`);
}
client.defaults.headers.Cookie = cookies.map(c => c.split(';')[0]).join('; ');
return client;
}
function generateSubId() {
return crypto.randomBytes(8).toString('hex');
}
function expiry(days) {
return Date.now() + Number(days) * 24 * 60 * 60 * 1000;
}
async function addClient(panel, inboundId, clientData) {
const response = await panel.post('/panel/api/inbounds/addClient', {
id: Number(inboundId),
settings: JSON.stringify({
clients: [clientData]
})
});
if (response.status >= 400) {
throw new Error(`addClient HTTP ${response.status}`);
}
if (response.data && typeof response.data === 'object' && response.data.success === false) {
throw new Error(response.data.msg || 'Ошибка addClient');
}
}
async function updateClient(panel, inboundId, clientId, clientData) {
const response = await panel.post(`/panel/api/inbounds/updateClient/${clientId}`, {
id: Number(inboundId),
settings: JSON.stringify({
clients: [clientData]
})
});
if (response.status >= 400) {
throw new Error(`updateClient HTTP ${response.status}`);
}
if (response.data && typeof response.data === 'object' && response.data.success === false) {
throw new Error(response.data.msg || 'Ошибка updateClient');
}
}
async function deleteClient(panel, inboundId, clientId) {
const response = await panel.post(`/panel/api/inbounds/${Number(inboundId)}/delClient/${clientId}`);
if (response.status >= 400) {
throw new Error(`deleteClient HTTP ${response.status}`);
}
if (response.data && typeof response.data === 'object' && response.data.success === false) {
throw new Error(response.data.msg || 'Ошибка deleteClient');
}
}
async function getInbounds(panel) {
const response = await panel.get('/panel/api/inbounds/list');
if (response.status >= 400) {
throw new Error(`list HTTP ${response.status}`);
}
const body = response.data;
if (body && typeof body === 'object' && body.success === false) {
throw new Error(body.msg || 'Ошибка получения inbounds');
}
if (body && Array.isArray(body.obj)) return body.obj;
if (Array.isArray(body)) return body;
return [];
}
async function getClientTrafficsByEmail(panel, email) {
const encodedEmail = encodeURIComponent(String(email || '').trim());
const response = await panel.get(`/panel/api/inbounds/getClientTraffics/${encodedEmail}`);
if (response.status >= 400) {
throw new Error(`getClientTraffics HTTP ${response.status}`);
}
const body = response.data;
if (body && typeof body === 'object' && body.success === false) {
throw new Error(body.msg || 'Ошибка получения трафика клиента');
}
if (body && body.obj) return body.obj;
return body || null;
}
function parseSettings(settings) {
if (!settings) return { clients: [] };
if (typeof settings === 'string') {
try {
return JSON.parse(settings);
} catch {
return { clients: [] };
}
}
return settings;
}
function bytesToGb(bytes) {
return Number(bytes || 0) / 1024 / 1024 / 1024;
}
function findClientInsideInbound(inbound, email) {
const settings = parseSettings(inbound.settings);
const clients = Array.isArray(settings.clients) ? settings.clients : [];
const found = clients.find(client =>
String(client.email || '').trim() === String(email || '').trim()
);
if (!found) return null;
const usedBytes = Number(found.up || 0) + Number(found.down || 0);
const totalBytes = Number(found.totalGB || 0);
const remainingBytes = totalBytes > 0 ? Math.max(totalBytes - usedBytes, 0) : 0;
return {
inboundId: inbound.id,
inboundRemark: inbound.remark || '',
client: found,
usedBytes,
totalBytes,
remainingBytes
};
}
async function createClient(data) {
const inboundList = getInboundList();
const inbound = inboundList.find(i => i.key === String(data.inboundKey));
if (!inbound) {
throw new Error('Inbound не найден');
}
const uuid = crypto.randomUUID();
const subId = generateSubId();
const exp = expiry(data.days);
const client = {
id: uuid,
email: data.email,
flow: data.flow,
totalGB: Number(data.totalGb) * 1024 * 1024 * 1024,
expiryTime: exp,
enable: true,
subId,
comment: data.comment || '',
limitIp: 0,
reset: 0
};
const p1 = await login(process.env.PANEL1_URL, process.env.PANEL1_USERNAME, process.env.PANEL1_PASSWORD);
const p2 = await login(process.env.PANEL2_URL, process.env.PANEL2_USERNAME, process.env.PANEL2_PASSWORD);
await addClient(p1, inbound.panel1, client);
await addClient(p2, inbound.panel2, client);
return {
uuid,
subId,
subscriptionUrl: buildSubscriptionUrl(subId)
};
}
async function searchClientByEmail(email) {
const panels = [
{
panelIndex: 1,
url: process.env.PANEL1_URL,
username: process.env.PANEL1_USERNAME,
password: process.env.PANEL1_PASSWORD
},
{
panelIndex: 2,
url: process.env.PANEL2_URL,
username: process.env.PANEL2_USERNAME,
password: process.env.PANEL2_PASSWORD
}
];
let firstFound = null;
const locations = [];
for (const panelInfo of panels) {
const panel = await login(panelInfo.url, panelInfo.username, panelInfo.password);
const inbounds = await getInbounds(panel);
for (const inbound of inbounds) {
const found = findClientInsideInbound(inbound, email);
if (found) {
let trafficInfo = null;
try {
trafficInfo = await getClientTrafficsByEmail(panel, found.client.email);
} catch (_) {
trafficInfo = null;
}
const up = Number(
trafficInfo?.up ??
trafficInfo?.Up ??
trafficInfo?.upload ??
found.client.up ??
0
);
const down = Number(
trafficInfo?.down ??
trafficInfo?.Down ??
trafficInfo?.download ??
found.client.down ??
0
);
const totalBytes = Number(
trafficInfo?.total ??
trafficInfo?.totalGB ??
found.client.totalGB ??
0
);
const usedBytes = up + down;
const remainingBytes = totalBytes > 0 ? Math.max(totalBytes - usedBytes, 0) : 0;
const item = {
panelIndex: panelInfo.panelIndex,
panel,
inboundId: found.inboundId,
inboundRemark: found.inboundRemark,
client: found.client,
usedBytes,
totalBytes,
remainingBytes
};
locations.push(item);
if (!firstFound) firstFound = item;
}
}
}
if (!firstFound) return null;
return {
email: firstFound.client.email || email,
uuid: firstFound.client.id || '',
subId: firstFound.client.subId || '',
comment: firstFound.client.comment || '',
flow: firstFound.client.flow || '',
expiryTime: Number(firstFound.client.expiryTime || 0),
totalBytes: firstFound.totalBytes,
usedBytes: firstFound.usedBytes,
remainingBytes: firstFound.remainingBytes,
locations
};
}
async function searchClientsByPartialEmail(query) {
const q = String(query || '').trim().toLowerCase();
if (!q) return [];
const panels = [
{
panelIndex: 1,
url: process.env.PANEL1_URL,
username: process.env.PANEL1_USERNAME,
password: process.env.PANEL1_PASSWORD
},
{
panelIndex: 2,
url: process.env.PANEL2_URL,
username: process.env.PANEL2_USERNAME,
password: process.env.PANEL2_PASSWORD
}
];
const unique = new Map();
for (const panelInfo of panels) {
const panel = await login(panelInfo.url, panelInfo.username, panelInfo.password);
const inbounds = await getInbounds(panel);
for (const inbound of inbounds) {
const settings = parseSettings(inbound.settings);
const clients = Array.isArray(settings.clients) ? settings.clients : [];
for (const client of clients) {
const email = String(client.email || '').trim();
if (!email) continue;
if (email.toLowerCase().includes(q)) {
const key = client.subId || client.id || email;
if (!unique.has(key)) {
unique.set(key, {
email,
subId: client.subId || '',
uuid: client.id || '',
inboundRemark: inbound.remark || '',
inboundId: inbound.id
});
}
}
}
}
}
return Array.from(unique.values()).sort((a, b) => a.email.localeCompare(b.email, 'ru'));
}
async function extendClientByEmail(email, addDays) {
const days = Number(addDays);
if (!Number.isInteger(days) || days <= 0) {
throw new Error('Количество дней должно быть положительным числом');
}
const found = await searchClientByEmail(email);
if (!found) {
throw new Error(`Клиент ${email} не найден`);
}
const alreadyUpdated = [];
try {
for (const location of found.locations) {
const currentExpiry = Number(location.client.expiryTime || 0);
const baseTime = currentExpiry > Date.now() ? currentExpiry : Date.now();
const nextExpiry = baseTime + days * 24 * 60 * 60 * 1000;
const nextClient = {
...location.client,
expiryTime: nextExpiry
};
await updateClient(location.panel, location.inboundId, location.client.id, nextClient);
alreadyUpdated.push({
panel: location.panel,
inboundId: location.inboundId,
clientId: location.client.id,
oldClient: location.client
});
}
} catch (error) {
for (const item of alreadyUpdated.reverse()) {
try {
await updateClient(item.panel, item.inboundId, item.clientId, item.oldClient);
} catch (_) {}
}
throw new Error(`Продление не завершено: ${error.message}`);
}
return await searchClientByEmail(email);
}
async function deleteClientByEmail(email) {
const found = await searchClientByEmail(email);
if (!found) {
throw new Error(`Клиент ${email} не найден`);
}
for (const location of found.locations) {
await deleteClient(location.panel, location.inboundId, location.client.id);
}
return {
email: found.email,
subId: found.subId,
uuid: found.uuid
};
}
async function getDashboardStats() {
const panel = await login(
process.env.PANEL1_URL,
process.env.PANEL1_USERNAME,
process.env.PANEL1_PASSWORD
);
const inbounds = await getInbounds(panel);
const inboundList = getInboundList().filter(item => item.show);
const stats = [];
let totalClients = 0;
for (const configInbound of inboundList) {
const panel1Id = Number(configInbound.panel1);
const realInbound = inbounds.find(i => Number(i.id) === panel1Id);
if (!realInbound) {
stats.push({
key: configInbound.key,
label: configInbound.label,
count: 0
});
continue;
}
const settings = parseSettings(realInbound.settings);
const clients = Array.isArray(settings.clients) ? settings.clients : [];
const count = clients.length;
totalClients += count;
stats.push({
key: configInbound.key,
label: configInbound.label,
count
});
}
return {
totalClients,
inbounds: stats
};
}
module.exports = {
createClient,
getInboundList,
searchClientByEmail,
searchClientsByPartialEmail,
extendClientByEmail,
deleteClientByEmail,
buildSubscriptionUrl,
bytesToGb,
getDashboardStats
};