mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 06:06:08 +00:00
9b91f0f42e
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { RefreshCw } from 'lucide-react';
|
|
import {
|
|
generateX25519KeyPair,
|
|
isX25519Available,
|
|
randomShortId,
|
|
randomUuid,
|
|
realityClientLink,
|
|
realityServerInbound,
|
|
type RealityConfig,
|
|
type X25519KeyPair,
|
|
} from '@/lib/xray/reality';
|
|
import { ToolFrame } from './tool-frame';
|
|
import { TextField, SelectField } from './shared/fields';
|
|
import { OutputBlock } from './shared/output-block';
|
|
import { CopyButton } from './shared/copy-button';
|
|
|
|
const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'] as const;
|
|
|
|
export function RealityConfigGenerator() {
|
|
const [address, setAddress] = useState('your-server.com');
|
|
const [port, setPort] = useState('443');
|
|
const [dest, setDest] = useState('www.microsoft.com:443');
|
|
const [sni, setSni] = useState('www.microsoft.com');
|
|
const [fingerprint, setFingerprint] = useState<string>('chrome');
|
|
const [uuid, setUuid] = useState('');
|
|
const [shortId, setShortId] = useState('');
|
|
const [keys, setKeys] = useState<X25519KeyPair | null>(null);
|
|
const [unavailable, setUnavailable] = useState(false);
|
|
|
|
const regenerate = useCallback(async () => {
|
|
setUuid(randomUuid());
|
|
setShortId(randomShortId(4));
|
|
if (!isX25519Available()) {
|
|
setUnavailable(true);
|
|
return;
|
|
}
|
|
try {
|
|
setKeys(await generateX25519KeyPair());
|
|
setUnavailable(false);
|
|
} catch {
|
|
setUnavailable(true);
|
|
}
|
|
}, []);
|
|
|
|
// Generate keys/identifiers on the client after hydration. This is a genuine
|
|
// client-only side effect (WebCrypto + randomness), not derived render state.
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
void regenerate();
|
|
}, [regenerate]);
|
|
|
|
const config: RealityConfig | null =
|
|
keys && uuid
|
|
? {
|
|
address,
|
|
port: Number(port) || 443,
|
|
uuid,
|
|
dest,
|
|
serverNames: [sni],
|
|
shortIds: [shortId],
|
|
privateKey: keys.privateKey,
|
|
publicKey: keys.publicKey,
|
|
fingerprint,
|
|
spiderX: '/',
|
|
flow: 'xtls-rprx-vision',
|
|
}
|
|
: null;
|
|
|
|
const serverJson = config ? JSON.stringify(realityServerInbound(config), null, 2) : '';
|
|
const clientLink = config ? realityClientLink(config) : '';
|
|
|
|
return (
|
|
<ToolFrame
|
|
title="REALITY config generator"
|
|
description="Generate a VLESS + REALITY inbound and client link. Keys are created in your browser — nothing is sent anywhere."
|
|
onReset={() => void regenerate()}
|
|
>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<TextField
|
|
label="Server address"
|
|
value={address}
|
|
onChange={setAddress}
|
|
hint="Your domain or IP"
|
|
/>
|
|
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
|
<TextField
|
|
label="Dest (camouflage target)"
|
|
value={dest}
|
|
onChange={setDest}
|
|
hint="A real TLS 1.3 site, e.g. www.microsoft.com:443"
|
|
/>
|
|
<TextField label="SNI / Server name" value={sni} onChange={setSni} />
|
|
<SelectField
|
|
label="Fingerprint"
|
|
value={fingerprint}
|
|
onChange={setFingerprint}
|
|
options={FINGERPRINTS}
|
|
/>
|
|
</div>
|
|
|
|
{unavailable ? (
|
|
<div className="mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
|
Your browser can't generate X25519 keys here. Generate them on the server instead:
|
|
<div className="mt-2">
|
|
<OutputBlock label="run on the server" value="xray x25519" />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-medium">Generated keys & identifiers</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => void regenerate()}
|
|
className="inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
|
>
|
|
<RefreshCw className="size-3.5" aria-hidden />
|
|
Regenerate
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
<KeyRow label="Public key" value={keys?.publicKey ?? ''} />
|
|
<KeyRow label="Private key" value={keys?.privateKey ?? ''} />
|
|
<KeyRow label="UUID" value={uuid} />
|
|
<KeyRow label="Short ID" value={shortId} />
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-1 gap-4">
|
|
<OutputBlock label="Server inbound (Xray JSON)" value={serverJson} />
|
|
<OutputBlock label="Client share link" value={clientLink} qr />
|
|
</div>
|
|
</>
|
|
)}
|
|
</ToolFrame>
|
|
);
|
|
}
|
|
|
|
function KeyRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex items-center gap-2 rounded-lg border bg-fd-background px-3 py-2">
|
|
<span className="shrink-0 text-xs font-medium text-fd-muted-foreground">{label}</span>
|
|
<code dir="ltr" className="flex-1 truncate text-start text-xs">
|
|
{value}
|
|
</code>
|
|
<CopyButton value={value} label="" className="px-1.5" />
|
|
</div>
|
|
);
|
|
}
|