feat: login in xui

This commit is contained in:
2025-06-07 22:42:48 +03:00
parent b3bb15dff7
commit 5948b9d739
5 changed files with 115 additions and 1 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
XUI_HOST=
XUI_USER=
XUI_PASSWORD=

4
.gitignore vendored
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
@@ -41,3 +42,6 @@ yarn-error.log*
next-env.d.ts
certificates
# my
repomix-output.*

View File

@@ -0,0 +1,2 @@
import { authFetch } from "./login";
export { authFetch };

97
src/app/api/_lib/login.ts Normal file
View File

@@ -0,0 +1,97 @@
const cookieJar: Map<string, string> = new Map();
let loginInProgress: Promise<void> | 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<void> => {
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;
};

View File

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