Compare commits

...

5 Commits

Author SHA1 Message Date
6141d7bf9e feat: not-found url error 2025-06-09 12:52:49 +03:00
3d1898f849 feat: loader 2025-06-09 11:59:38 +03:00
7e94b5c44f feat: expiry time 2025-06-09 11:47:13 +03:00
2bbc34f1cb feat: qrcode with url 2025-06-09 11:21:33 +03:00
f919b6b59a feat: redis and valid url 2025-06-09 11:07:29 +03:00
10 changed files with 171 additions and 42 deletions

View File

@@ -9,3 +9,5 @@ DB_PORT=
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
BOT_TOKEN=
REDIS_URL=redis://localhost:6379

View File

@@ -8,6 +8,8 @@
"@telegram-apps/init-data-node": "^2.0.7",
"@telegram-apps/sdk-react": "^3.3.0",
"@telegram-apps/telegram-ui": "^2.1.8",
"ioredis": "^5.6.1",
"lucide-react": "^0.513.0",
"next": "15.3.3",
"qrcode": "^1.5.4",
"qrcode.react": "^4.2.0",
@@ -130,6 +132,8 @@
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="],
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
@@ -434,6 +438,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -470,6 +476,8 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
@@ -636,6 +644,8 @@
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
@@ -750,6 +760,10 @@
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="],
@@ -758,6 +772,8 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lucide-react": ["lucide-react@0.513.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -872,6 +888,10 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
@@ -970,6 +990,8 @@
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],

View File

@@ -1,4 +1,10 @@
services:
redis:
image: redis:8-alpine
restart: unless-stopped
volumes:
- redis-data:/data
command: ["redis-server", "--appendonly", "yes"]
db:
image: postgres:17-alpine
restart: unless-stopped
@@ -7,7 +13,7 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-postgres}
ports:
- "${DB_PORT:-5432}:5432"
- "127.0.0.1:${DB_PORT:-5432}:5432"
volumes:
- db_data:/var/lib/postgresql/data
app:
@@ -16,3 +22,4 @@ services:
- "3900:3000"
volumes:
db_data:
redis-data:

View File

@@ -14,6 +14,8 @@
"@telegram-apps/init-data-node": "^2.0.7",
"@telegram-apps/sdk-react": "^3.3.0",
"@telegram-apps/telegram-ui": "^2.1.8",
"ioredis": "^5.6.1",
"lucide-react": "^0.513.0",
"next": "15.3.3",
"qrcode": "^1.5.4",
"qrcode.react": "^4.2.0",

View File

@@ -1,27 +1,32 @@
"use server";
import { API_LINK, XUIApiLinks } from "@/lib/enum";
import { authFetch } from "@/lib/login";
import { getValidUrl } from "@/lib/url";
import { prisma } from "@/utils/prisma";
import { redis } from "@/utils/redis";
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) {
async function getInboundApi() {
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 }
);
return inbound;
}
async function getUrlApi(email: string) {
const cachedInbound = await redis.get("inbound");
const inbound = cachedInbound ? JSON.parse(cachedInbound) : await getInboundApi();
if (!inbound) {
throw new Error("Inbound not found");
}
await redis.set("inbound", JSON.stringify(inbound), "EX", 3600);
const users: ClientSettings = JSON.parse(inbound.settings);
const user = users.clients.find((user) => user.email === email);
return user;
if (!user) {
throw new Error("User not found");
}
return { url: getValidUrl({ email, id: user.id }), expiryTime: user.expiryTime };
}
export async function getUrl(initData: string = "") {
try {
@@ -30,13 +35,23 @@ export async function getUrl(initData: string = "") {
expiresIn: 3600,
});
const initDataParsed = parse(initData);
const user = await prisma.user.findFirst({
if (!initDataParsed.user) {
throw new Error("User not found");
}
const cachedUser = await redis.get(`user:${initDataParsed.user.id}`);
const user = cachedUser
? JSON.parse(cachedUser)
: await prisma.user.findFirst({
where: {
tgId: initDataParsed.user ? initDataParsed.user.id.toString() : "0",
tgId: initDataParsed.user.id.toString(),
},
});
return await getUrlApi(user?.email || "");
if (!user) {
throw new Error("User not found");
}
await redis.set(`user:${initDataParsed.user.id}`, JSON.stringify(user), "EX", 60);
return await getUrlApi(user.email || "");
} catch (e) {
console.log(e);
throw e;
}
}

View File

@@ -1,14 +1,23 @@
"use client";
import { Block } from "@/components/Block";
import { Loader } from "@/components/Loader";
import { Page } from "@/components/Page";
import { cn } from "@/utils/cn";
import { useRawInitData } from "@telegram-apps/sdk-react";
import { ClipboardList } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react";
import { getUrl } from "./actions";
export default function Home() {
const [url, setUrl] = useState("");
const [expiryTime, setExpiryTime] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
const initData = useRawInitData();
const onCopyClick = async () => {
await navigator.clipboard.writeText(window.location.href);
if (!url.length) return;
await navigator.clipboard.writeText(url);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
@@ -17,36 +26,77 @@ export default function Home() {
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getUrl(initData);
console.log(data);
setUrl(data.url);
setExpiryTime(data.expiryTime);
setIsLoading(false);
} catch (e) {
console.error(e);
setNotFound(true);
setIsLoading(false);
}
};
fetchData();
}, [initData]);
if (isLoading) return <Loader />;
if (notFound) {
return (
<Page back={false}>
<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="Ссылка">Ссылка не найдена</Block>
</main>
</Page>
);
}
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="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)]">
{url && (
<>
<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%]"
<div
className="relative flex size-52 flex-col items-center justify-center rounded-md bg-white"
onClick={onCopyClick}
>
{isCopied ? "Скопировано!" : "Копировать"}
</button>
<div
className={cn(
"absolute top-0 left-0 flex h-full w-full flex-col items-center justify-center rounded-md bg-[var(--tg-theme-button-color)] opacity-0 transition-opacity",
isCopied && "opacity-95"
)}
>
<span className="flex flex-row text-lg font-semibold text-[var(--tg-theme-button-text-color)]">
Скопировано
<ClipboardList className="text-[var(--tg-theme-button-text-color)]" />
</span>
</div>
<QRCodeSVG value={url} className="size-48" />
</div>
<span className="text-center text-sm font-semibold">Нажмите на QR, чтобы скопировать!</span>
</Block>
<Block name="Подписка">
<div className="flex flex-col items-center gap-0.5">
<span>Статус:</span>
<span>Активна</span>
<span>{expiryTime > Date.now() || expiryTime === 0 ? "Активна" : "Не Активна"}</span>
</div>
<div className="flex flex-col items-center gap-0.5">
<span>Активна до:</span>
<span>01.01.2023</span>
<span>
{expiryTime === 0
? "Всегда"
: expiryTime > Date.now()
? new Date(expiryTime).toLocaleString("ru-RU")
: "Не Активна"}
</span>
</div>
</Block>
</>
)}
</main>
</Page>
);

10
src/components/Loader.tsx Normal file
View File

@@ -0,0 +1,10 @@
const Loader = () => {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-[--tg-theme-bg-color]">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-[--tg-theme-button-bg-color] border-t-transparent" />
<span className="text-semibold">Loading...</span>
</div>
);
};
export { Loader };

13
src/lib/url.ts Normal file
View File

@@ -0,0 +1,13 @@
export function getValidUrl({ email, id }: { email: string; id: string }) {
const obj = {
fp: "chrome",
alpn: "h2,h3",
packetEncoding: "xudp",
security: "tls",
type: "ws",
path: "/myverysecretpath",
};
const params = new URLSearchParams(obj).toString();
const url = `vless://${id}@nwaifu.su:443?${params}#WS-${email}`;
return url;
}

5
src/utils/cn.ts Normal file
View File

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

3
src/utils/redis.ts Normal file
View File

@@ -0,0 +1,3 @@
import Redis from "ioredis";
export const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");