|
61 | 61 | publish: "community/publish.html", |
62 | 62 | support: "docs/support.html", |
63 | 63 | reference: "docs/reference.html", |
| 64 | + login: "login.html", |
64 | 65 | contact: "company/contact.html", |
65 | 66 | vision: "company/vision.html", |
66 | 67 | mission: "company/mission.html", |
|
108 | 109 | ]); |
109 | 110 | const mockDbStorageKey = "gamefoundry.mockDb.v1"; |
110 | 111 | const mockDbSessionStorageKey = "gamefoundry.mockDb.sessionUser.v1"; |
| 112 | + const mockDbSessionModeStorageKey = "gamefoundry.mockDb.sessionMode.v1"; |
111 | 113 |
|
112 | 114 | const currentScript = document.currentScript || document.querySelector("script[src*='gamefoundry-partials.js']"); |
113 | 115 | const assetRoot = currentScript ? new URL("../", currentScript.src) : null; |
|
193 | 195 | } |
194 | 196 | } |
195 | 197 |
|
| 198 | + function selectedSessionModeId() { |
| 199 | + try { |
| 200 | + return window.localStorage.getItem(mockDbSessionModeStorageKey) || "local"; |
| 201 | + } catch { |
| 202 | + return "local"; |
| 203 | + } |
| 204 | + } |
| 205 | + |
196 | 206 | function rolesForUser(state, userKey, fallbackRoleSlugs) { |
197 | 207 | const rows = state?.tables?.user_roles || []; |
198 | 208 | const roles = new Map((state?.tables?.roles || []).map(function (role) { |
|
210 | 220 | } |
211 | 221 |
|
212 | 222 | function localDevLoginState() { |
| 223 | + if (selectedSessionModeId() === "dev") { |
| 224 | + return { |
| 225 | + authenticated: false, |
| 226 | + displayName: "Login", |
| 227 | + mode: "dev", |
| 228 | + roleSlugs: [] |
| 229 | + }; |
| 230 | + } |
| 231 | + |
213 | 232 | const session = localSessionUsers[selectedLocalSessionId()]; |
214 | 233 | if (!session) { |
215 | 234 | return { |
216 | 235 | authenticated: false, |
217 | 236 | displayName: "Login", |
| 237 | + mode: "local", |
218 | 238 | roleSlugs: [] |
219 | 239 | }; |
220 | 240 | } |
221 | 241 |
|
222 | 242 | const state = readMockDbState(); |
| 243 | + if (!state) { |
| 244 | + return { |
| 245 | + authenticated: false, |
| 246 | + displayName: "Login", |
| 247 | + mode: "local", |
| 248 | + roleSlugs: [] |
| 249 | + }; |
| 250 | + } |
| 251 | + |
223 | 252 | const user = (state?.tables?.users || []).find(function (record) { |
224 | 253 | return record.key === session.userKey && record.isActive !== false; |
225 | 254 | }); |
226 | | - if (state && !user) { |
| 255 | + if (!user) { |
227 | 256 | return { |
228 | 257 | authenticated: false, |
229 | 258 | displayName: "Login", |
| 259 | + mode: "local", |
230 | 260 | roleSlugs: [] |
231 | 261 | }; |
232 | 262 | } |
233 | 263 |
|
234 | 264 | return { |
235 | 265 | authenticated: true, |
236 | 266 | displayName: user?.displayName || session.displayName, |
237 | | - roleSlugs: rolesForUser(state, session.userKey, session.roleSlugs) |
| 267 | + mode: "local", |
| 268 | + roleSlugs: rolesForUser(state, session.userKey, []) |
238 | 269 | }; |
239 | 270 | } |
240 | 271 |
|
|
272 | 303 | if (accountLink) { |
273 | 304 | accountLink.textContent = canUseAccount ? loginState.displayName + " \u25BE" : "Login"; |
274 | 305 | accountLink.setAttribute("aria-label", canUseAccount ? "Account menu for " + loginState.displayName : "Login"); |
| 306 | + accountLink.setAttribute("href", canUseAccount ? routeHref("account") : routeHref("login")); |
275 | 307 | } |
276 | 308 | if (accountMenu) { |
277 | 309 | accountMenu.hidden = !canUseAccount; |
|
283 | 315 | setMenuVisible(adminItem, canUseAdmin); |
284 | 316 | } |
285 | 317 |
|
| 318 | + function protectedPageRequirement(pagePath) { |
| 319 | + if (pagePath.indexOf("admin/") === 0) { |
| 320 | + return { |
| 321 | + role: "admin", |
| 322 | + title: "Admin role required", |
| 323 | + message: "Log in as Admin to open this Admin page." |
| 324 | + }; |
| 325 | + } |
| 326 | + if (pagePath.indexOf("account/") === 0) { |
| 327 | + return { |
| 328 | + role: "user", |
| 329 | + title: "Login required", |
| 330 | + message: "Log in as a local user to open Account pages." |
| 331 | + }; |
| 332 | + } |
| 333 | + return null; |
| 334 | + } |
| 335 | + |
| 336 | + function canUseProtectedPage(requirement, loginState) { |
| 337 | + if (!requirement) { |
| 338 | + return true; |
| 339 | + } |
| 340 | + if (requirement.role === "admin") { |
| 341 | + return loginState.authenticated && loginState.roleSlugs.includes("admin"); |
| 342 | + } |
| 343 | + if (requirement.role === "user") { |
| 344 | + return loginState.authenticated && loginState.roleSlugs.includes("user"); |
| 345 | + } |
| 346 | + return false; |
| 347 | + } |
| 348 | + |
| 349 | + function createAccessBlockedMain(requirement, pagePath, loginState) { |
| 350 | + const main = document.createElement("main"); |
| 351 | + main.dataset.sessionAccessBlocked = requirement.role; |
| 352 | + |
| 353 | + const titleSection = document.createElement("section"); |
| 354 | + titleSection.className = "page-title"; |
| 355 | + const titleContainer = document.createElement("div"); |
| 356 | + titleContainer.className = "container"; |
| 357 | + const kicker = document.createElement("div"); |
| 358 | + kicker.className = "kicker"; |
| 359 | + kicker.textContent = "Access"; |
| 360 | + const title = document.createElement("h1"); |
| 361 | + title.textContent = requirement.title; |
| 362 | + const lede = document.createElement("p"); |
| 363 | + lede.className = "lede"; |
| 364 | + lede.textContent = requirement.message; |
| 365 | + titleContainer.append(kicker, title, lede); |
| 366 | + titleSection.append(titleContainer); |
| 367 | + |
| 368 | + const bodySection = document.createElement("section"); |
| 369 | + bodySection.className = "section"; |
| 370 | + const bodyContainer = document.createElement("div"); |
| 371 | + bodyContainer.className = "container"; |
| 372 | + const card = document.createElement("div"); |
| 373 | + card.className = "card"; |
| 374 | + const body = document.createElement("div"); |
| 375 | + body.className = "card-body content-stack"; |
| 376 | + const status = document.createElement("p"); |
| 377 | + status.className = "status"; |
| 378 | + status.setAttribute("role", "status"); |
| 379 | + status.dataset.sessionAccessStatus = ""; |
| 380 | + status.textContent = `Blocked ${pagePath}. Current session: ${loginState.displayName}.`; |
| 381 | + const link = document.createElement("a"); |
| 382 | + link.className = "btn primary"; |
| 383 | + link.href = routeHref("login") + "?returnTo=" + encodeURIComponent(pagePath); |
| 384 | + link.textContent = "Open Login"; |
| 385 | + body.append(status, link); |
| 386 | + card.append(body); |
| 387 | + bodyContainer.append(card); |
| 388 | + bodySection.append(bodyContainer); |
| 389 | + |
| 390 | + main.append(titleSection, bodySection); |
| 391 | + return main; |
| 392 | + } |
| 393 | + |
| 394 | + function enforcePageProtection() { |
| 395 | + const pagePath = currentPagePath() || "index.html"; |
| 396 | + const requirement = protectedPageRequirement(pagePath); |
| 397 | + const loginState = localDevLoginState(); |
| 398 | + const allowed = canUseProtectedPage(requirement, loginState); |
| 399 | + window.GameFoundrySessionGuard = { |
| 400 | + blocked: !allowed, |
| 401 | + mode: loginState.mode, |
| 402 | + pagePath, |
| 403 | + requirement: requirement?.role || "" |
| 404 | + }; |
| 405 | + if (!requirement || allowed) { |
| 406 | + return false; |
| 407 | + } |
| 408 | + |
| 409 | + const existingMain = document.querySelector("main"); |
| 410 | + if (existingMain) { |
| 411 | + existingMain.replaceWith(createAccessBlockedMain(requirement, pagePath, loginState)); |
| 412 | + } |
| 413 | + document.title = `${requirement.title} - GameFoundryStudio`; |
| 414 | + return true; |
| 415 | + } |
| 416 | + |
286 | 417 | function markActiveNavigation(root) { |
287 | 418 | const pagePath = currentPagePath() || "index.html"; |
288 | 419 | root.querySelectorAll("[data-nav-link]").forEach(function (link) { |
|
351 | 482 | } |
352 | 483 |
|
353 | 484 | function refreshHeaderLoginState() { |
| 485 | + const header = document.querySelector("header.site-header"); |
| 486 | + if (header) { |
| 487 | + enforcePageProtection(); |
| 488 | + applyLocalDevLoginState(header); |
| 489 | + markActiveNavigation(header); |
| 490 | + } |
| 491 | + } |
| 492 | + |
| 493 | + function refreshHeaderOnly() { |
354 | 494 | const header = document.querySelector("header.site-header"); |
355 | 495 | if (header) { |
356 | 496 | applyLocalDevLoginState(header); |
357 | 497 | markActiveNavigation(header); |
358 | 498 | } |
359 | 499 | } |
360 | 500 |
|
| 501 | + enforcePageProtection(); |
361 | 502 | document.addEventListener("DOMContentLoaded", function () { |
| 503 | + enforcePageProtection(); |
362 | 504 | const slots = Array.from(document.querySelectorAll("[data-partial]")); |
363 | 505 | const tasks = slots.length ? slots.map(loadPartial) : [ |
364 | 506 | replaceExisting("header-nav", "header.site-header"), |
|
369 | 511 | }); |
370 | 512 | }); |
371 | 513 | window.addEventListener("gamefoundry:mock-db-session-user-changed", refreshHeaderLoginState); |
372 | | - window.addEventListener("gamefoundry:mock-db-changed", refreshHeaderLoginState); |
| 514 | + window.addEventListener("gamefoundry:mock-db-session-mode-changed", refreshHeaderLoginState); |
| 515 | + window.addEventListener("gamefoundry:mock-db-changed", refreshHeaderOnly); |
373 | 516 | }()); |
0 commit comments