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


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
};