fix(console): correct panel node loading

This commit is contained in:
Haitao Pan 2026-04-08 18:56:18 +08:00
parent 2eb72e4aea
commit 6dfc9454fa
3 changed files with 25 additions and 199 deletions

View File

@ -2,17 +2,9 @@ import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { SESSION_COOKIE_NAME, clearSessionCookie } from "@lib/authGateway";
import {
getAccountServiceApiBaseUrl,
getAccountServiceBaseUrl,
} from "@server/serviceConfig";
import {
buildInternalServiceHeaders,
isServiceTokenConfigured,
} from "@server/internalServiceAuth";
import { getAccountServiceApiBaseUrl } from "@server/serviceConfig";
const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl();
const ACCOUNT_BASE = getAccountServiceBaseUrl();
type AccountUser = {
id?: string;
@ -48,13 +40,6 @@ type SessionResponse = {
error?: string;
};
type SandboxGuestResponse = {
email?: string;
proxyUuid?: string;
proxyUuidExpiresAt?: string;
error?: string;
};
function normalizeRole(role: unknown): string {
if (typeof role !== "string") {
return "user";
@ -94,69 +79,10 @@ async function fetchSession(token: string, requestHost?: string | null) {
}
}
async function fetchSandboxGuest(): Promise<AccountUser | null> {
if (!isServiceTokenConfigured()) {
return null;
}
try {
const response = await fetch(`${ACCOUNT_BASE}/api/internal/sandbox/guest`, {
method: "GET",
headers: buildInternalServiceHeaders({
Accept: "application/json",
}),
cache: "no-store",
});
if (!response.ok) {
return null;
}
const payload = (await response
.json()
.catch(() => null)) as SandboxGuestResponse | null;
const proxyUuid =
typeof payload?.proxyUuid === "string" ? payload.proxyUuid.trim() : "";
if (!proxyUuid) {
return null;
}
const proxyUuidExpiresAt =
typeof payload?.proxyUuidExpiresAt === "string" &&
payload.proxyUuidExpiresAt.trim().length > 0
? payload.proxyUuidExpiresAt.trim()
: undefined;
// Shape this as a pseudo-session user for the Guest/Demo experience.
return {
id: proxyUuid,
uuid: proxyUuid,
proxyUuid,
proxyUuidExpiresAt,
name: "Guest user",
username: "guest",
email: "sandbox@svc.plus",
role: "guest",
groups: ["guest", "sandbox"],
permissions: ["read"],
readOnly: true,
tenantId: "guest-sandbox",
tenants: [{ id: "guest-sandbox", name: "Guest Sandbox", role: "guest" }],
mfaEnabled: false,
mfaPending: false,
};
} catch (error) {
console.error("Sandbox guest session proxy failed", error);
return null;
}
}
export async function GET(request: NextRequest) {
void request;
const token = (await cookies()).get(SESSION_COOKIE_NAME)?.value;
if (!token) {
const sandboxGuest = await fetchSandboxGuest();
return NextResponse.json({ user: sandboxGuest });
return NextResponse.json({ user: null });
}
const requestHost = request.headers.get("host");

View File

@ -65,16 +65,6 @@ const KNOWN_ROLE_MAP: Record<string, UserRole> = {
member: 'user',
}
const GUEST_SESSION_STORAGE_KEY = 'xcontrol.guest.session'
const GUEST_SESSION_TTL_MS = 60 * 60 * 1000
const GUEST_SANDBOX_TENANT_ID = 'guest-sandbox'
const GUEST_SANDBOX_TENANT_NAME = 'Guest Sandbox'
type GuestSession = {
uuid: string
issuedAt: number
}
function normalizeRole(input?: string | null): UserRole {
if (!input || typeof input !== 'string') {
return 'guest'
@ -88,92 +78,6 @@ function normalizeRole(input?: string | null): UserRole {
return KNOWN_ROLE_MAP[normalized] ?? 'guest'
}
function createUUID(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
}
function readGuestSession(): GuestSession | null {
if (typeof window === 'undefined') {
return null
}
const raw = window.sessionStorage.getItem(GUEST_SESSION_STORAGE_KEY)
if (!raw) {
return null
}
try {
const parsed = JSON.parse(raw) as GuestSession
if (
typeof parsed?.uuid === 'string' &&
parsed.uuid.trim().length > 0 &&
typeof parsed?.issuedAt === 'number' &&
Number.isFinite(parsed.issuedAt)
) {
return {
uuid: parsed.uuid.trim(),
issuedAt: parsed.issuedAt,
}
}
} catch (error) {
console.warn('Failed to parse guest session payload', error)
}
return null
}
function writeGuestSession(session: GuestSession) {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.setItem(GUEST_SESSION_STORAGE_KEY, JSON.stringify(session))
}
function resolveGuestUUID(now = Date.now()): string {
const existing = readGuestSession()
if (!existing || now - existing.issuedAt >= GUEST_SESSION_TTL_MS) {
const next: GuestSession = { uuid: createUUID(), issuedAt: now }
writeGuestSession(next)
return next.uuid
}
return existing.uuid
}
function buildGuestUser(): User {
const identifier = resolveGuestUUID()
return {
id: identifier,
uuid: identifier,
email: 'sandbox@svc.plus',
name: 'Guest user',
username: 'guest',
mfaEnabled: false,
mfaPending: false,
mfa: {
totpEnabled: false,
totpPending: false,
},
role: 'guest',
groups: ['guest', 'sandbox'],
permissions: ['read'],
isGuest: true,
isUser: false,
isOperator: false,
isAdmin: false,
isReadOnly: true,
tenantId: GUEST_SANDBOX_TENANT_ID,
tenants: [
{
id: GUEST_SANDBOX_TENANT_ID,
name: GUEST_SANDBOX_TENANT_NAME,
role: 'guest',
},
],
}
}
async function fetchSessionUser(): Promise<User | null> {
try {
const response = await fetch('/api/auth/session', {
@ -185,7 +89,7 @@ async function fetchSessionUser(): Promise<User | null> {
})
if (!response.ok) {
return buildGuestUser()
return null
}
const payload = (await response.json()) as {
@ -217,7 +121,7 @@ async function fetchSessionUser(): Promise<User | null> {
const sessionUser = payload?.user
if (!sessionUser) {
return buildGuestUser()
return null
}
const { id, uuid, email, name, username, mfaEnabled, mfa, mfaPending, role, groups, permissions } = sessionUser
@ -229,7 +133,7 @@ async function fetchSessionUser(): Promise<User | null> {
: ''
if (!identifier) {
return buildGuestUser()
return null
}
const normalizedName = typeof name === 'string' && name.trim().length > 0 ? name.trim() : undefined
const normalizedUsername =
@ -309,18 +213,6 @@ async function fetchSessionUser(): Promise<User | null> {
.filter((tenant): tenant is TenantMembership => Boolean(tenant))
: undefined
const effectiveTenantId = normalizedRole === 'guest' ? GUEST_SANDBOX_TENANT_ID : normalizedTenantId
const effectiveTenants =
normalizedRole === 'guest'
? [
{
id: GUEST_SANDBOX_TENANT_ID,
name: GUEST_SANDBOX_TENANT_NAME,
role: 'guest' as UserRole,
},
]
: normalizedTenants
return {
id: identifier,
uuid: identifier,
@ -339,13 +231,13 @@ async function fetchSessionUser(): Promise<User | null> {
isUser: normalizedRole === 'user',
isOperator: normalizedRole === 'operator',
isAdmin: normalizedRole === 'admin',
isReadOnly: normalizedRole === 'guest' ? true : normalizedReadOnly,
tenantId: effectiveTenantId,
tenants: effectiveTenants,
isReadOnly: normalizedReadOnly,
tenantId: normalizedTenantId,
tenants: normalizedTenants,
}
} catch (error) {
console.warn('Failed to resolve user session', error)
return buildGuestUser()
return null
}
}

View File

@ -2,14 +2,17 @@
import type { VlessNode } from './vless'
const PRIMARY_ENDPOINT = '/api/agent-server/v1/nodes'
const PRIMARY_ENDPOINT = '/api/auth/sync/config?since_version=0'
const FALLBACK_ENDPOINT = '/api/agent/nodes'
type AgentNodePayload = {
nodes?: unknown
message?: unknown
error?: unknown
}
type AgentNodePayload =
| {
nodes?: unknown
profiles?: unknown
message?: unknown
error?: unknown
}
| VlessNode[]
type AgentNodesError = Error & {
status?: number
@ -34,7 +37,7 @@ async function requestAgentNodes(url: string): Promise<VlessNode[]> {
},
})
const payload = (await response.json().catch(() => null)) as AgentNodePayload | VlessNode[] | null
const payload = (await response.json().catch(() => null)) as AgentNodePayload | null
if (!response.ok) {
const error = new Error(extractMessage(Array.isArray(payload) ? null : payload, response.status)) as AgentNodesError
@ -45,10 +48,15 @@ async function requestAgentNodes(url: string): Promise<VlessNode[]> {
if (Array.isArray(payload)) {
return payload as VlessNode[]
}
if (payload && Array.isArray((payload as AgentNodePayload).nodes)) {
if (payload && Array.isArray((payload as { nodes?: unknown }).nodes)) {
return (payload as { nodes: VlessNode[] }).nodes
}
if (payload && Array.isArray((payload as { profiles?: unknown }).profiles)) {
return (payload as { profiles: VlessNode[] }).profiles
}
throw new Error('unexpected_agent_nodes_payload')
}