From 5948b9d739814f03a16c9c6cbeb3ca020ef625f3 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sat, 7 Jun 2025 22:42:48 +0300 Subject: [PATCH] feat: login in xui --- .env.example | 3 ++ .gitignore | 6 ++- src/app/api/_lib/index.ts | 2 + src/app/api/_lib/login.ts | 97 ++++++++++++++++++++++++++++++++++++ src/app/api/get_url/route.ts | 8 +++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 src/app/api/_lib/index.ts create mode 100644 src/app/api/_lib/login.ts create mode 100644 src/app/api/get_url/route.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3f17de8 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +XUI_HOST= +XUI_USER= +XUI_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ec5eb9..a1126a6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel @@ -40,4 +41,7 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -certificates \ No newline at end of file +certificates + +# my +repomix-output.* \ No newline at end of file diff --git a/src/app/api/_lib/index.ts b/src/app/api/_lib/index.ts new file mode 100644 index 0000000..aaa094c --- /dev/null +++ b/src/app/api/_lib/index.ts @@ -0,0 +1,2 @@ +import { authFetch } from "./login"; +export { authFetch }; diff --git a/src/app/api/_lib/login.ts b/src/app/api/_lib/login.ts new file mode 100644 index 0000000..a4d958f --- /dev/null +++ b/src/app/api/_lib/login.ts @@ -0,0 +1,97 @@ +const cookieJar: Map = new Map(); + +let loginInProgress: Promise | null = null; + +interface Cookie { + name: string; + value: string; +} + +const parseSetCookie = (header: string): Cookie | null => { + const parts = header.split(";"); + const nameValue = parts[0].trim(); + const equalIndex = nameValue.indexOf("="); + if (equalIndex === -1) return null; + const name = nameValue.substring(0, equalIndex).trim(); + const value = nameValue.substring(equalIndex + 1).trim(); + return { name, value }; +}; + +const updateCookieJar = (setCookieHeaders: string[]) => { + setCookieHeaders.forEach((header) => { + const cookie = parseSetCookie(header); + if (cookie) cookieJar.set(cookie.name, cookie.value); + }); +}; + +const login = async (): Promise => { + const username = process.env.XUI_USER || ""; + const password = process.env.XUI_PASSWORD || ""; + const details = { + username: username, + password: password, + }; + const encodedData = new URLSearchParams(details).toString(); + const response = await fetch(process.env.XUI_HOST + "/login", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + }, + body: encodedData, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const setCookieHeaders = response.headers.getSetCookie(); + updateCookieJar(setCookieHeaders); +}; + +export const authFetch = async (url: string, options: RequestInit = {}) => { + if (cookieJar.size === 0) { + if (!loginInProgress) { + loginInProgress = login().finally(() => { + loginInProgress = null; + }); + } + await loginInProgress; + } + + const cookieHeader = Array.from(cookieJar.entries()) + .map(([name, value]) => `${name}=${value}`) + .join("; "); + const headers = { + ...options.headers, + Cookie: cookieHeader, + }; + + const response = await fetch(url, { ...options, headers }); + + const setCookieHeaders = response.headers.getSetCookie(); + if (setCookieHeaders.length > 0) updateCookieJar(setCookieHeaders); + + if (response.headers.get("Content-Type")?.startsWith("text/html")) { + if (!loginInProgress) { + loginInProgress = login().finally(() => { + loginInProgress = null; + }); + } + await loginInProgress; + + const newCookieHeader = Array.from(cookieJar.entries()) + .map(([name, value]) => `${name}=${value}`) + .join("; "); + const newHeaders = { + ...options.headers, + Cookie: newCookieHeader, + }; + + const retryResponse = await fetch(url, { ...options, headers: newHeaders }); + const retrySetCookieHeaders = retryResponse.headers.getSetCookie(); + if (retrySetCookieHeaders.length > 0) updateCookieJar(retrySetCookieHeaders); + return retryResponse; + } + + return response; +}; diff --git a/src/app/api/get_url/route.ts b/src/app/api/get_url/route.ts new file mode 100644 index 0000000..579b7ab --- /dev/null +++ b/src/app/api/get_url/route.ts @@ -0,0 +1,8 @@ +import { authFetch } from "../_lib/login"; + +//TODO: its just for testing. Make it normal +export async function GET() { + const res = await authFetch(process.env.XUI_HOST + "/panel/api/inbounds/list"); + const data = await res.json(); + return Response.json(data); +}