|
127 | 127 | const currentScript = document.currentScript || document.querySelector("script[src*='gamefoundry-partials.js']"); |
128 | 128 | const assetRoot = currentScript ? new URL("../", currentScript.src) : null; |
129 | 129 | let navigationAdminMenuCache = null; |
| 130 | + let publicConfigCache = null; |
| 131 | + let publicConfigDataCache = null; |
| 132 | + let publicConfigLoaded = false; |
| 133 | + let publicConfigSource = ""; |
| 134 | + const publicConfigDiagnostics = []; |
| 135 | + const publicConfigRoute = "/api/public/config"; |
| 136 | + |
| 137 | + function publishPublicConfigDiagnostics() { |
| 138 | + window.GameFoundryPublicConfigDiagnostics = { |
| 139 | + apiUrlConfigured: Boolean(publicConfigCache?.apiUrl), |
| 140 | + diagnostics: publicConfigDiagnostics.slice(), |
| 141 | + source: publicConfigSource |
| 142 | + }; |
| 143 | + } |
| 144 | + |
| 145 | + function recordPublicConfigDiagnostic(message) { |
| 146 | + const normalizedMessage = String(message || "").trim(); |
| 147 | + if (normalizedMessage && !publicConfigDiagnostics.includes(normalizedMessage)) { |
| 148 | + publicConfigDiagnostics.push(normalizedMessage); |
| 149 | + } |
| 150 | + publishPublicConfigDiagnostics(); |
| 151 | + return normalizedMessage; |
| 152 | + } |
| 153 | + |
| 154 | + function normalizePublicConfig(value) { |
| 155 | + const config = value && typeof value === "object" ? value : {}; |
| 156 | + return { |
| 157 | + apiUrl: typeof config.apiUrl === "string" ? config.apiUrl.trim() : "", |
| 158 | + environmentLabel: typeof config.environmentLabel === "string" ? config.environmentLabel.trim() : "", |
| 159 | + siteUrl: typeof config.siteUrl === "string" ? config.siteUrl.trim() : "" |
| 160 | + }; |
| 161 | + } |
| 162 | + |
| 163 | + function publicConfigFromPayload(payload) { |
| 164 | + if (payload?.ok === false) { |
| 165 | + return null; |
| 166 | + } |
| 167 | + return normalizePublicConfig(payload?.data?.publicConfig || payload?.publicConfig || {}); |
| 168 | + } |
| 169 | + |
| 170 | + function sameOriginConfigUrl() { |
| 171 | + return new URL(publicConfigRoute, window.location.origin).href; |
| 172 | + } |
| 173 | + |
| 174 | + function isLocalHostname(hostname) { |
| 175 | + return ["localhost", "127.0.0.1", "::1"].includes(String(hostname || "").toLowerCase()); |
| 176 | + } |
| 177 | + |
| 178 | + function companionLocalConfigUrl() { |
| 179 | + if (!isLocalHostname(window.location.hostname)) { |
| 180 | + return ""; |
| 181 | + } |
| 182 | + const port = Number(window.location.port || 0); |
| 183 | + if (!Number.isInteger(port) || port <= 0) { |
| 184 | + return ""; |
| 185 | + } |
| 186 | + const url = new URL(publicConfigRoute, window.location.origin); |
| 187 | + url.port = String(port + 1); |
| 188 | + return url.href; |
| 189 | + } |
| 190 | + |
| 191 | + function publicConfigCandidateUrls() { |
| 192 | + const sameOriginUrl = sameOriginConfigUrl(); |
| 193 | + const companionUrl = companionLocalConfigUrl(); |
| 194 | + const urls = window.location.port === "5500" |
| 195 | + ? [companionUrl, sameOriginUrl] |
| 196 | + : [sameOriginUrl, companionUrl]; |
| 197 | + return Array.from(new Set(urls.filter(Boolean))); |
| 198 | + } |
| 199 | + |
| 200 | + function missingApiUrlDiagnostic() { |
| 201 | + return "GAMEFOUNDRY_API_URL is missing from the server public config. Falling back to same-origin /api; static-only Live Server origins cannot serve API routes. Set GAMEFOUNDRY_API_URL in .env and restart the site API."; |
| 202 | + } |
| 203 | + |
| 204 | + function setLoadedPublicConfig(config, source, data) { |
| 205 | + publicConfigLoaded = true; |
| 206 | + publicConfigCache = normalizePublicConfig(config); |
| 207 | + publicConfigDataCache = data && typeof data === "object" |
| 208 | + ? data |
| 209 | + : { publicConfig: publicConfigCache }; |
| 210 | + publicConfigSource = source; |
| 211 | + publishPublicConfigDiagnostics(); |
| 212 | + return publicConfigCache; |
| 213 | + } |
| 214 | + |
| 215 | + function requestPublicConfigSync() { |
| 216 | + if (publicConfigLoaded) { |
| 217 | + return publicConfigCache; |
| 218 | + } |
| 219 | + if (window.GameFoundryPublicConfig && typeof window.GameFoundryPublicConfig === "object") { |
| 220 | + return setLoadedPublicConfig(window.GameFoundryPublicConfig, "browser-global"); |
| 221 | + } |
| 222 | + for (const url of publicConfigCandidateUrls()) { |
| 223 | + try { |
| 224 | + const request = new XMLHttpRequest(); |
| 225 | + request.open("GET", url, false); |
| 226 | + request.setRequestHeader("Accept", "application/json"); |
| 227 | + request.send(null); |
| 228 | + if (request.status < 200 || request.status >= 300) { |
| 229 | + continue; |
| 230 | + } |
| 231 | + const payload = request.responseText ? JSON.parse(request.responseText) : null; |
| 232 | + const config = publicConfigFromPayload(payload); |
| 233 | + if (config) { |
| 234 | + return setLoadedPublicConfig(config, url, payload?.data || {}); |
| 235 | + } |
| 236 | + } catch { |
| 237 | + // Try the next public config discovery URL. |
| 238 | + } |
| 239 | + } |
| 240 | + recordPublicConfigDiagnostic(missingApiUrlDiagnostic()); |
| 241 | + return setLoadedPublicConfig({}, "same-origin-fallback"); |
| 242 | + } |
| 243 | + |
| 244 | + async function requestPublicConfigAsync() { |
| 245 | + if (publicConfigLoaded) { |
| 246 | + return publicConfigCache; |
| 247 | + } |
| 248 | + if (window.GameFoundryPublicConfig && typeof window.GameFoundryPublicConfig === "object") { |
| 249 | + return setLoadedPublicConfig(window.GameFoundryPublicConfig, "browser-global"); |
| 250 | + } |
| 251 | + for (const url of publicConfigCandidateUrls()) { |
| 252 | + const response = await fetch(url, { |
| 253 | + headers: { "Accept": "application/json" }, |
| 254 | + method: "GET" |
| 255 | + }).catch(function () { |
| 256 | + return null; |
| 257 | + }); |
| 258 | + if (!response?.ok) { |
| 259 | + continue; |
| 260 | + } |
| 261 | + const payload = await response.json().catch(function () { |
| 262 | + return null; |
| 263 | + }); |
| 264 | + const config = publicConfigFromPayload(payload); |
| 265 | + if (config) { |
| 266 | + return setLoadedPublicConfig(config, url, payload?.data || {}); |
| 267 | + } |
| 268 | + } |
| 269 | + recordPublicConfigDiagnostic(missingApiUrlDiagnostic()); |
| 270 | + return setLoadedPublicConfig({}, "same-origin-fallback"); |
| 271 | + } |
| 272 | + |
| 273 | + async function requestPublicConfigDataAsync() { |
| 274 | + await requestPublicConfigAsync(); |
| 275 | + return publicConfigDataCache || { publicConfig: publicConfigCache || {} }; |
| 276 | + } |
| 277 | + |
| 278 | + function normalizeApiPath(path) { |
| 279 | + const value = String(path || "/").trim() || "/"; |
| 280 | + if (/^https?:\/\//i.test(value)) { |
| 281 | + return value; |
| 282 | + } |
| 283 | + const rootedPath = value.startsWith("/") ? value : "/" + value; |
| 284 | + return rootedPath.indexOf("/api/") === 0 || rootedPath === "/api" |
| 285 | + ? rootedPath.slice(4) || "/" |
| 286 | + : rootedPath; |
| 287 | + } |
| 288 | + |
| 289 | + function sameOriginApiUrl(path) { |
| 290 | + const normalizedPath = normalizeApiPath(path); |
| 291 | + if (/^https?:\/\//i.test(normalizedPath)) { |
| 292 | + return normalizedPath; |
| 293 | + } |
| 294 | + return "/api" + normalizedPath; |
| 295 | + } |
| 296 | + |
| 297 | + function configuredApiUrl(path, config) { |
| 298 | + const normalizedPath = normalizeApiPath(path); |
| 299 | + if (/^https?:\/\//i.test(normalizedPath)) { |
| 300 | + return normalizedPath; |
| 301 | + } |
| 302 | + const apiUrl = String(config?.apiUrl || "").trim().replace(/\/+$/, ""); |
| 303 | + if (!apiUrl) { |
| 304 | + recordPublicConfigDiagnostic(missingApiUrlDiagnostic()); |
| 305 | + return sameOriginApiUrl(normalizedPath); |
| 306 | + } |
| 307 | + return apiUrl + normalizedPath; |
| 308 | + } |
| 309 | + |
| 310 | + function resolveApiUrl(path) { |
| 311 | + return configuredApiUrl(path, requestPublicConfigSync()); |
| 312 | + } |
| 313 | + |
| 314 | + async function resolveApiFetchUrl(path) { |
| 315 | + return configuredApiUrl(path, await requestPublicConfigAsync()); |
| 316 | + } |
| 317 | + |
| 318 | + async function fetchApi(path, options) { |
| 319 | + return fetch(await resolveApiFetchUrl(path), options); |
| 320 | + } |
130 | 321 |
|
131 | 322 | function assetUrl(path) { |
132 | 323 | if (!assetRoot) return rootPrefix() + path; |
|
193 | 384 | } |
194 | 385 | try { |
195 | 386 | const request = new XMLHttpRequest(); |
196 | | - request.open("GET", "/api/navigation/admin-menu", false); |
| 387 | + const url = resolveApiUrl("/navigation/admin-menu"); |
| 388 | + request.open("GET", url, false); |
197 | 389 | request.setRequestHeader("Accept", "application/json"); |
198 | 390 | request.send(null); |
199 | 391 | const payload = request.responseText ? JSON.parse(request.responseText) : null; |
200 | 392 | if (request.status < 200 || request.status >= 300 || payload?.ok === false) { |
201 | 393 | if (request.status === 404 || request.status === 405) { |
202 | | - throw new Error(serverRouteUnavailableDiagnostic("GET", "/api/navigation/admin-menu", request.status)); |
| 394 | + throw new Error(serverRouteUnavailableDiagnostic("GET", url, request.status)); |
203 | 395 | } |
204 | 396 | throw new Error(payload?.error || "Navigation API did not return Admin menu data."); |
205 | 397 | } |
|
353 | 545 |
|
354 | 546 | function requestSessionApi(method, url, body) { |
355 | 547 | const request = new XMLHttpRequest(); |
356 | | - request.open(method, url, false); |
| 548 | + const apiUrl = resolveApiUrl(url); |
| 549 | + request.open(method, apiUrl, false); |
357 | 550 | request.setRequestHeader("Accept", "application/json"); |
358 | 551 | if (body !== undefined) { |
359 | 552 | request.setRequestHeader("Content-Type", "application/json"); |
|
362 | 555 | const payload = request.responseText ? JSON.parse(request.responseText) : null; |
363 | 556 | if (request.status < 200 || request.status >= 300 || payload?.ok === false) { |
364 | 557 | if (request.status === 404 || request.status === 405) { |
365 | | - throw new Error(serverRouteUnavailableDiagnostic(method, url, request.status)); |
| 558 | + throw new Error(serverRouteUnavailableDiagnostic(method, apiUrl, request.status)); |
366 | 559 | } |
367 | 560 | throw new Error(payload?.error || "Session API did not return a valid auth response."); |
368 | 561 | } |
|
395 | 588 |
|
396 | 589 | function currentLoginState() { |
397 | 590 | try { |
398 | | - return authSessionFromApiData(requestSessionApi("GET", "/api/session/current")); |
| 591 | + return authSessionFromApiData(requestSessionApi("GET", "/session/current")); |
399 | 592 | } catch (error) { |
400 | 593 | const diagnostic = error instanceof Error ? error.message : ""; |
401 | 594 | if (diagnostic) { |
|
419 | 612 | } |
420 | 613 |
|
421 | 614 | async function requestPlatformBanner() { |
422 | | - const response = await fetch("/api/platform-settings/banner", { |
| 615 | + const response = await fetchApi("/platform-settings/banner", { |
423 | 616 | headers: { "Accept": "application/json" }, |
424 | 617 | method: "GET" |
425 | 618 | }); |
|
440 | 633 | } |
441 | 634 |
|
442 | 635 | async function requestEnvironmentBanner() { |
443 | | - const response = await fetch("/api/public/config", { |
444 | | - headers: { "Accept": "application/json" }, |
445 | | - method: "GET" |
446 | | - }); |
447 | | - const payload = await response.json().catch(function () { |
448 | | - return null; |
449 | | - }); |
450 | | - if (!response.ok || payload?.ok === false) { |
451 | | - throw new Error(payload?.error || "Public configuration is unavailable."); |
452 | | - } |
453 | | - const data = payload?.data || {}; |
| 636 | + const data = await requestPublicConfigDataAsync(); |
454 | 637 | return { |
455 | 638 | banner: normalizedPlatformBanner(data.environmentBanner || {}, "environment-config"), |
456 | 639 | diagnostics: data.diagnostics || {} |
|
784 | 967 | event.preventDefault(); |
785 | 968 | const session = (() => { |
786 | 969 | try { |
787 | | - return authSessionFromApiData(requestSessionApi("POST", "/api/session/logout")); |
| 970 | + return authSessionFromApiData(requestSessionApi("POST", "/session/logout")); |
788 | 971 | } catch (error) { |
789 | 972 | return missingSessionApiLoginState(error instanceof Error ? error.message : ""); |
790 | 973 | } |
|
0 commit comments