feat(console): enable sandbox guest vless qr fallback and hourly guest uuid semantics
This commit is contained in:
parent
f695a0e937
commit
34622a4afa
@ -135,6 +135,7 @@ export async function GET(request: NextRequest) {
|
||||
rawRole === 'readonly' ||
|
||||
rawRole === 'read_only' ||
|
||||
String(rawUser.email ?? '').trim().toLowerCase() === 'demo@svc.plus' ||
|
||||
String(rawUser.email ?? '').trim().toLowerCase() === 'sandbox@svc.plus' ||
|
||||
isNamedDemo
|
||||
const normalizedProxyUuid =
|
||||
typeof rawUser.proxyUuid === 'string' && rawUser.proxyUuid.trim().length > 0
|
||||
|
||||
@ -146,7 +146,7 @@ function buildGuestUser(): User {
|
||||
return {
|
||||
id: identifier,
|
||||
uuid: identifier,
|
||||
email: 'guest@sandbox.local',
|
||||
email: 'Sandbox@svc.plus',
|
||||
name: 'Guest user',
|
||||
username: 'guest',
|
||||
mfaEnabled: false,
|
||||
@ -273,6 +273,7 @@ async function fetchSessionUser(): Promise<User | null> {
|
||||
rawRole === 'readonly' ||
|
||||
rawRole === 'read_only' ||
|
||||
normalizedEmail === 'demo@svc.plus' ||
|
||||
normalizedEmail === 'sandbox@svc.plus' ||
|
||||
isNamedDemo ||
|
||||
normalizedGroups.some((value) => value.toLowerCase() === 'readonly role')
|
||||
const normalizedReadOnly = Boolean(sessionUser.readOnly) || inferredReadOnly
|
||||
|
||||
@ -55,9 +55,12 @@ export default function UserOverview({ hideMfaMainPrompt = false }: UserOverview
|
||||
const username = user?.username ?? '—'
|
||||
const email = user?.email ?? '—'
|
||||
const docsUrl = mfaCopy.actions.docsUrl
|
||||
const isDemoReadOnly = Boolean(user?.isReadOnly && user?.email?.toLowerCase() === 'demo@svc.plus')
|
||||
const demoUuidExpiresAtText = useMemo(() => {
|
||||
if (!isDemoReadOnly || !user?.proxyUuidExpiresAt) {
|
||||
const normalizedEmail = user?.email?.toLowerCase() ?? ''
|
||||
const isGuestSandboxReadOnly = Boolean(
|
||||
user?.isReadOnly && (normalizedEmail === 'sandbox@svc.plus' || normalizedEmail === 'demo@svc.plus'),
|
||||
)
|
||||
const guestUuidExpiresAtText = useMemo(() => {
|
||||
if (!isGuestSandboxReadOnly || !user?.proxyUuidExpiresAt) {
|
||||
return null
|
||||
}
|
||||
const date = new Date(user.proxyUuidExpiresAt)
|
||||
@ -65,7 +68,7 @@ export default function UserOverview({ hideMfaMainPrompt = false }: UserOverview
|
||||
return null
|
||||
}
|
||||
return date.toLocaleString()
|
||||
}, [isDemoReadOnly, user?.proxyUuidExpiresAt])
|
||||
}, [isGuestSandboxReadOnly, user?.proxyUuidExpiresAt])
|
||||
|
||||
const mfaStatusLabel = useMemo(() => {
|
||||
if (user?.mfaEnabled) {
|
||||
@ -118,7 +121,7 @@ export default function UserOverview({ hideMfaMainPrompt = false }: UserOverview
|
||||
}, [logout, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDemoReadOnly || !user?.proxyUuidExpiresAt) {
|
||||
if (!isGuestSandboxReadOnly || !user?.proxyUuidExpiresAt) {
|
||||
return
|
||||
}
|
||||
const expiresAt = new Date(user.proxyUuidExpiresAt).getTime()
|
||||
@ -132,17 +135,17 @@ export default function UserOverview({ hideMfaMainPrompt = false }: UserOverview
|
||||
return () => {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [isDemoReadOnly, refresh, user?.proxyUuidExpiresAt])
|
||||
}, [isGuestSandboxReadOnly, refresh, user?.proxyUuidExpiresAt])
|
||||
|
||||
return (
|
||||
<div className="space-y-6 text-[var(--color-text)] transition-colors">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--color-text-subtle)] opacity-90">{copy.uuidNote}</p>
|
||||
{isDemoReadOnly ? (
|
||||
{isGuestSandboxReadOnly ? (
|
||||
<p className="mt-2 rounded-[var(--radius-md)] border border-[color:var(--color-warning-muted)] bg-[var(--color-warning-muted)] px-3 py-2 text-xs text-[var(--color-warning-foreground)]">
|
||||
{language === 'zh'
|
||||
? `Demo 体验账号为只读模式:可浏览控制台、可使用 VLESS 二维码,但不能修改任何配置。UUID 每 1 小时自动刷新${demoUuidExpiresAtText ? `(下次刷新约 ${demoUuidExpiresAtText})` : ''}。`
|
||||
: `Demo account runs in read-only mode: browse safely and use the VLESS QR code, but no configuration changes are allowed. UUID rotates every hour${demoUuidExpiresAtText ? ` (next refresh around ${demoUuidExpiresAtText})` : ''}.`}
|
||||
? `Guest user(演示模式)为只读模式:可浏览控制台、可使用 VLESS 二维码,但不能修改任何配置。UUID 每 1 小时自动刷新${guestUuidExpiresAtText ? `(下次刷新约 ${guestUuidExpiresAtText})` : ''}。`
|
||||
: `Guest user (demo mode) runs in read-only mode: browse safely and use the VLESS QR code, but no configuration changes are allowed. UUID rotates every hour${guestUuidExpiresAtText ? ` (next refresh around ${guestUuidExpiresAtText})` : ''}.`}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
@ -199,7 +202,7 @@ export default function UserOverview({ hideMfaMainPrompt = false }: UserOverview
|
||||
<p className="mt-3 text-xs text-[var(--color-text-subtle)]">{copy.cards.uuid.description}</p>
|
||||
</Card>
|
||||
|
||||
<VlessQrCard uuid={vlessUuid} copy={copy.cards.vless} />
|
||||
<VlessQrCard uuid={vlessUuid} copy={copy.cards.vless} allowSandboxFallbackNode={isGuestSandboxReadOnly} />
|
||||
|
||||
<Card>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-[var(--color-primary)]">{copy.cards.username.label}</p>
|
||||
|
||||
@ -54,10 +54,30 @@ export type VlessQrCopy = {
|
||||
interface VlessQrCardProps {
|
||||
uuid: string | null | undefined
|
||||
copy: VlessQrCopy
|
||||
allowSandboxFallbackNode?: boolean
|
||||
}
|
||||
|
||||
export default function VlessQrCard({ uuid, copy }: VlessQrCardProps) {
|
||||
const { data: nodes } = useSWR<VlessNode[]>('/api/agent-server/v1/nodes', fetcher)
|
||||
function buildSandboxFallbackNode(): VlessNode {
|
||||
return {
|
||||
name: 'Sandbox Node',
|
||||
address: 'ha-proxy-jp.svc.plus',
|
||||
port: 1443,
|
||||
server_name: 'ha-proxy-jp.svc.plus',
|
||||
transport: 'tcp',
|
||||
tcp_port: 1443,
|
||||
xhttp_port: 443,
|
||||
path: '/split',
|
||||
mode: 'auto',
|
||||
flow: 'xtls-rprx-vision',
|
||||
uri_scheme_tcp:
|
||||
'vless://${UUID}@${DOMAIN}:1443?encryption=none&flow=${FLOW}&security=tls&sni=${SNI}&fp=${FP}&type=tcp#${TAG}',
|
||||
uri_scheme_xhttp:
|
||||
'vless://${UUID}@${DOMAIN}:443?encryption=none&security=tls&sni=${SNI}&fp=${FP}&type=xhttp&path=${PATH}&mode=${MODE}#${TAG}',
|
||||
}
|
||||
}
|
||||
|
||||
export default function VlessQrCard({ uuid, copy, allowSandboxFallbackNode = false }: VlessQrCardProps) {
|
||||
const { data: nodes, error: nodesError } = useSWR<VlessNode[]>('/api/agent-server/v1/nodes', fetcher)
|
||||
const [selectedNode, setSelectedNode] = useState<VlessNode | null>(null)
|
||||
const [preferredTransport, setPreferredTransport] = useState<VlessTransport>('tcp')
|
||||
const [isSelectorOpen, setIsSelectorOpen] = useState(false)
|
||||
@ -67,7 +87,12 @@ export default function VlessQrCard({ uuid, copy }: VlessQrCardProps) {
|
||||
const [generationError, setGenerationError] = useState<string | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const rawNode = useMemo(() => selectedNode || (nodes && nodes[0]) || undefined, [selectedNode, nodes])
|
||||
const rawNode = useMemo(() => {
|
||||
if (selectedNode) return selectedNode
|
||||
if (nodes && nodes[0]) return nodes[0]
|
||||
if (allowSandboxFallbackNode && uuid) return buildSandboxFallbackNode()
|
||||
return undefined
|
||||
}, [allowSandboxFallbackNode, nodes, selectedNode, uuid])
|
||||
|
||||
const effectiveNode = useMemo((): VlessNode | undefined => {
|
||||
if (!rawNode) return undefined
|
||||
@ -257,10 +282,12 @@ export default function VlessQrCard({ uuid, copy }: VlessQrCardProps) {
|
||||
<p className="font-semibold">❌ UUID 缺失</p>
|
||||
<p className="mt-1">{copy.missingUuid}</p>
|
||||
</div>
|
||||
) : !nodes || nodes.length === 0 ? (
|
||||
) : (!nodes || nodes.length === 0) && !rawNode ? (
|
||||
<div className="rounded-md border border-[color:var(--color-warning-border)] bg-[var(--color-warning-muted)] p-3 text-xs text-[var(--color-warning-foreground)]">
|
||||
<p className="font-semibold">❌ 节点数据缺失</p>
|
||||
<p className="mt-1">无法从服务器获取代理节点列表。请检查 /api/agent-server/v1/nodes 接口是否正常。</p>
|
||||
<p className="mt-1">
|
||||
无法从服务器获取代理节点列表{nodesError ? `(${nodesError.message})` : ''}。请检查 /api/agent-server/v1/nodes 接口是否正常。
|
||||
</p>
|
||||
</div>
|
||||
) : !effectiveNode ? (
|
||||
<div className="rounded-md border border-[color:var(--color-warning-border)] bg-[var(--color-warning-muted)] p-3 text-xs text-[var(--color-warning-foreground)]">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user