feat: started working with api

This commit is contained in:
2025-06-08 13:41:10 +03:00
parent 5948b9d739
commit 11c6d538b1
19 changed files with 299 additions and 31 deletions

View File

@@ -1,8 +1,13 @@
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap");
@import "tailwindcss";
html {
height: 100%;
}
body {
background: var(--tg-theme-secondary-bg-color, white);
font-family: "Montserrat", sans-serif;
height: 100%;
font-optical-sizing: auto;
}

56
src/app/_dto/inbounds.ts Normal file
View File

@@ -0,0 +1,56 @@
export interface InboundResponse {
success: boolean;
msg: string;
obj: Obj[];
}
export interface Obj {
id: number;
up: number;
down: number;
total: number;
remark: string;
enable: boolean;
expiryTime: number;
clientStats: ClientStat[];
listen: string;
port: number;
protocol: string;
settings: string;
streamSettings: string;
tag: string;
sniffing: string;
allocate: string;
}
export interface ClientStat {
id: number;
inboundId: number;
enable: boolean;
email: string;
up: number;
down: number;
expiryTime: number;
total: number;
reset: number;
}
export interface ClientSettings {
clients: Client[];
decryption: string;
fallbacks: object[];
}
export interface Client {
email: string;
enable: boolean;
expiryTime: number;
flow: string;
id: string;
limitIp: number;
reset: number;
subId: string;
tgId: string;
totalGB: number;
comment?: string;
}

42
src/app/actions.ts Normal file
View File

@@ -0,0 +1,42 @@
"use server";
import { API_LINK, XUIApiLinks } from "@/lib/enum";
import { authFetch } from "@/lib/login";
import { prisma } from "@/utils/prisma";
import { parse, validate } from "@telegram-apps/init-data-node";
import { ClientSettings, InboundResponse } from "./_dto/inbounds";
//TODO: Make it valid proxy url
async function getUrlApi(email: string) {
const res = await authFetch(API_LINK + XUIApiLinks.GET_INBOUNDS);
const data: InboundResponse = await res.json();
const inbound = data.obj.find((inbound) => inbound.remark === "WS");
if (!inbound) {
return Response.json(
{
success: false,
error: "Inbound not found",
},
{ status: 404 }
);
}
const users: ClientSettings = JSON.parse(inbound.settings);
const user = users.clients.find((user) => user.email === email);
return user;
}
export async function getUrl(initData: string = "") {
try {
if (process.env.NODE_ENV === "production")
validate(initData, process.env.BOT_TOKEN || "", {
expiresIn: 3600,
});
const initDataParsed = parse(initData);
const user = await prisma.user.findFirst({
where: {
tgId: initDataParsed.user ? initDataParsed.user.id.toString() : "0",
},
});
return await getUrlApi(user?.email || "");
} catch (e) {
console.log(e);
}
}

View File

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

View File

@@ -0,0 +1,21 @@
import { ClientSettings, InboundResponse } from "@/app/_dto/inbounds";
import { API_LINK, XUIApiLinks } from "@/lib/enum";
import { authFetch } from "@/lib/login";
//TODO: its just for testing. Make it normal
export async function GET() {
const res = await authFetch(API_LINK + XUIApiLinks.GET_INBOUNDS);
const data: InboundResponse = await res.json();
const inbound = data.obj.find((inbound) => inbound.remark === "WS");
if (!inbound) {
return Response.json(
{
success: false,
error: "Inbound not found",
},
{ status: 404 }
);
}
const users: ClientSettings = JSON.parse(inbound.settings);
return Response.json(users);
}

View File

@@ -1,8 +0,0 @@
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);
}

View File

@@ -1,9 +1,12 @@
"use client";
import { Block } from "@/components/Block";
import { Page } from "@/components/Page";
import { useState } from "react";
import { useRawInitData } from "@telegram-apps/sdk-react";
import { useEffect, useState } from "react";
import { getUrl } from "./actions";
export default function Home() {
const initData = useRawInitData();
const onCopyClick = async () => {
await navigator.clipboard.writeText(window.location.href);
setIsCopied(true);
@@ -12,12 +15,28 @@ export default function Home() {
}, 2000);
};
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
const fetchData = async () => {
const data = await getUrl(initData);
console.log(data);
};
fetchData();
}, [initData]);
return (
<Page back={false}>
<header className="flex h-12 w-full flex-col items-center justify-center bg-[var(--tg-theme-header-bg-color)]">
<span className="text-xl font-semibold text-[var(--tg-theme-text-color)]">Nwaifu Proxy</span>
</header>
<main className="flex h-full w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]">
<main className="absolute bottom-0 left-0 flex w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]">
<Block name="Ссылка">
<div className="size-44 rounded-md bg-white"></div>
<button
className="h-8 w-48 rounded-md bg-[var(--tg-theme-button-color)] text-[var(--tg-theme-button-text-color)] transition-[scale] hover:scale-[110%] active:scale-[115%]"
onClick={onCopyClick}
>
{isCopied ? "Скопировано!" : "Копировать"}
</button>
</Block>
<Block name="Подписка">
<div className="flex flex-col items-center gap-0.5">
<span>Статус:</span>
@@ -28,15 +47,6 @@ export default function Home() {
<span>01.01.2023</span>
</div>
</Block>
<Block name="Ссылка">
<div className="size-44 rounded-md bg-white"></div>
<button
className="mt-4 h-8 w-48 rounded-md bg-[var(--tg-theme-button-color)] text-[var(--tg-theme-button-text-color)] transition-[scale] hover:scale-[110%] active:scale-[115%]"
onClick={onCopyClick}
>
{isCopied ? "Скопировано!" : "Копировать"}
</button>
</Block>
</main>
</Page>
);

View File

@@ -13,6 +13,7 @@ const RootInner: React.FC<PropsWithChildren> = ({ children }) => {
return (
<AppRoot
appearance={isDark ? "dark" : "light"}
className="relative h-full"
platform={["macos", "ios"].includes(lp.tgWebAppPlatform) ? "ios" : "base"}
>
{children}

View File

@@ -1,5 +0,0 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export default function cn(...inputs: ClassValue[]) {
return twMerge(clsx(...inputs));
}

5
src/lib/enum.ts Normal file
View File

@@ -0,0 +1,5 @@
export const API_LINK = process.env.XUI_HOST || "";
export enum XUIApiLinks {
GET_INBOUNDS = "/panel/api/inbounds/list",
LOGIN = "/login",
}

View File

@@ -1,3 +1,5 @@
import { API_LINK, XUIApiLinks } from "./enum";
const cookieJar: Map<string, string> = new Map();
let loginInProgress: Promise<void> | null = null;
@@ -32,7 +34,7 @@ const login = async (): Promise<void> => {
password: password,
};
const encodedData = new URLSearchParams(details).toString();
const response = await fetch(process.env.XUI_HOST + "/login", {
const response = await fetch(API_LINK + XUIApiLinks.LOGIN, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",

11
src/utils/prisma.ts Normal file
View File

@@ -0,0 +1,11 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ["query", "info", "warn", "error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;