mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web,hub): in-place connection switching via setServerUrl + CORS allowlist for lanUrl
This commit is contained in:
@@ -21,6 +21,7 @@ export interface ServerSettings {
|
||||
listenPort: number
|
||||
publicUrl: string
|
||||
corsOrigins: string[]
|
||||
lanUrl: string | null
|
||||
}
|
||||
|
||||
export interface ServerSettingsResult {
|
||||
@@ -33,6 +34,7 @@ export interface ServerSettingsResult {
|
||||
listenHost: 'env' | 'file' | 'default'
|
||||
listenPort: 'env' | 'file' | 'default'
|
||||
publicUrl: 'env' | 'file' | 'default'
|
||||
lanUrl: 'file' | 'default'
|
||||
corsOrigins: 'env' | 'file' | 'default'
|
||||
}
|
||||
savedToFile: boolean
|
||||
@@ -110,6 +112,7 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
|
||||
listenHost: 'default',
|
||||
listenPort: 'default',
|
||||
publicUrl: 'default',
|
||||
lanUrl: 'default',
|
||||
corsOrigins: 'default',
|
||||
}
|
||||
// telegramBotToken: env > file > null
|
||||
@@ -214,6 +217,15 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
|
||||
sources.publicUrl = 'file'
|
||||
}
|
||||
|
||||
// lanUrl: file only (optional alternative access URL, e.g. LAN direct)
|
||||
let lanUrl: string | null = null
|
||||
if (typeof settings.lanUrl === 'string' && settings.lanUrl.trim()) {
|
||||
lanUrl = settings.lanUrl.trim()
|
||||
sources.lanUrl = 'file'
|
||||
} else {
|
||||
sources.lanUrl = 'default'
|
||||
}
|
||||
|
||||
// corsOrigins: env > file > derived from publicUrl
|
||||
let corsOrigins: string[]
|
||||
if (process.env.CORS_ORIGINS) {
|
||||
@@ -244,6 +256,7 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
|
||||
listenHost,
|
||||
listenPort,
|
||||
publicUrl,
|
||||
lanUrl,
|
||||
corsOrigins,
|
||||
},
|
||||
sources,
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface ConfigSources {
|
||||
listenHost: ConfigSource
|
||||
listenPort: ConfigSource
|
||||
publicUrl: ConfigSource
|
||||
lanUrl: ConfigSource
|
||||
corsOrigins: ConfigSource
|
||||
cliApiToken: 'env' | 'file' | 'generated'
|
||||
}
|
||||
@@ -87,6 +88,9 @@ class Configuration {
|
||||
/** Public URL for external access (e.g., Telegram Mini App) */
|
||||
public readonly publicUrl: string
|
||||
|
||||
/** Optional alternative access URL (e.g. LAN direct) */
|
||||
public readonly lanUrl: string | null
|
||||
|
||||
/** Allowed CORS origins for Mini App + Socket.IO (comma-separated env override) */
|
||||
public readonly corsOrigins: string[]
|
||||
|
||||
@@ -113,6 +117,7 @@ class Configuration {
|
||||
this.listenHost = serverSettings.listenHost
|
||||
this.listenPort = serverSettings.listenPort
|
||||
this.publicUrl = serverSettings.publicUrl
|
||||
this.lanUrl = serverSettings.lanUrl
|
||||
this.corsOrigins = serverSettings.corsOrigins
|
||||
|
||||
// CLI API token - will be set by _setCliApiToken() before create() returns
|
||||
|
||||
+10
-1
@@ -232,7 +232,16 @@ function createWebApp(options: {
|
||||
|
||||
const configuration = getConfiguration()
|
||||
const corsOrigins = options.corsOrigins ?? configuration.corsOrigins
|
||||
const corsOriginOption = corsOrigins.includes('*') ? '*' : corsOrigins
|
||||
const mergedCorsOrigins = configuration.lanUrl
|
||||
? (() => {
|
||||
try {
|
||||
return [...new Set([...corsOrigins, new URL(configuration.lanUrl).origin])]
|
||||
} catch {
|
||||
return corsOrigins
|
||||
}
|
||||
})()
|
||||
: corsOrigins
|
||||
const corsOriginOption = mergedCorsOrigins.includes('*') ? '*' : mergedCorsOrigins
|
||||
const corsMiddleware = cors({
|
||||
origin: corsOriginOption,
|
||||
allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
|
||||
+1
-1
@@ -449,7 +449,7 @@ function AppInner() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContextProvider value={{ api, token, baseUrl }}>
|
||||
<AppContextProvider value={{ api, token, baseUrl, setServerUrl }}>
|
||||
<VoiceProvider>
|
||||
<PwaUpdateBannerWithStatusOffset
|
||||
isSyncing={isSyncing}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
|
||||
import type { ServerUrlResult } from '@/hooks/useServerUrl'
|
||||
|
||||
type AppContextValue = {
|
||||
api: ApiClient
|
||||
token: string
|
||||
baseUrl: string
|
||||
setServerUrl: (input: string) => ServerUrlResult
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextValue | null>(null)
|
||||
|
||||
@@ -11,7 +11,7 @@ const locales: ReadonlyArray<{ value: Locale; label: string }> = [
|
||||
|
||||
function ConnectionSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const { api } = useAppContext()
|
||||
const { api, setServerUrl } = useAppContext()
|
||||
const [info, setInfo] = useState<{ publicUrl: string | null; lanUrl: string | null } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,7 +68,11 @@ function ConnectionSwitch() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.href = target
|
||||
// Switch the hub connection in place — the app
|
||||
// reconnects to the target origin without a full
|
||||
// page navigation (same hub, so the token stays
|
||||
// valid).
|
||||
setServerUrl(target)
|
||||
}}
|
||||
className="shrink-0 rounded-md border border-[var(--app-border)] bg-[var(--app-subtle-bg)] px-2 py-1.5 text-xs text-[var(--app-fg)] hover:bg-[var(--app-secondary-bg)]"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user