@dudousxd/nestjs-media-dashboard 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,6 +66,9 @@ function resolveConsoleAuth(options) {
66
66
  if (modes.length === 0) {
67
67
  throw new Error("MediaDashboardModule: auth needs a `login` and/or `session` hook.");
68
68
  }
69
+ if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
70
+ throw new Error("MediaDashboardModule: auth.unauthenticatedPage must be a function.");
71
+ }
69
72
  return {
70
73
  secret: options.secret,
71
74
  ttlMs: durationToMs(options.ttl),
@@ -78,6 +81,9 @@ function resolveConsoleAuth(options) {
78
81
  } : {},
79
82
  ...options.revalidate ? {
80
83
  revalidate: options.revalidate
84
+ } : {},
85
+ ...options.unauthenticatedPage ? {
86
+ unauthenticatedPage: options.unauthenticatedPage
81
87
  } : {}
82
88
  };
83
89
  }
@@ -224,6 +230,26 @@ function appendSetCookie(response, cookie) {
224
230
  ]);
225
231
  }
226
232
  __name(appendSetCookie, "appendSetCookie");
233
+ function resolveEndableResponse(response) {
234
+ const isEndable = /* @__PURE__ */ __name((value) => isRawNodeResponse(value) && typeof value.end === "function", "isEndable");
235
+ if (isEndable(response)) return response;
236
+ if (isRecord2(response) && isEndable(response.raw)) return response.raw;
237
+ return null;
238
+ }
239
+ __name(resolveEndableResponse, "resolveEndableResponse");
240
+ function responseAlreadyWritten(response) {
241
+ return resolveEndableResponse(response)?.headersSent === true;
242
+ }
243
+ __name(responseAlreadyWritten, "responseAlreadyWritten");
244
+ function sendHtml(response, status, html) {
245
+ const raw = resolveEndableResponse(response);
246
+ if (!raw) return;
247
+ raw.statusCode = status;
248
+ raw.setHeader("content-type", "text/html; charset=utf-8");
249
+ raw.setHeader("cache-control", "no-store, must-revalidate");
250
+ raw.end(html);
251
+ }
252
+ __name(sendHtml, "sendHtml");
227
253
 
228
254
  // src/server/auth/session-cookie.ts
229
255
  var import_node_crypto = require("crypto");
@@ -1483,8 +1509,14 @@ var MediaConsoleApiModule = class _MediaConsoleApiModule {
1483
1509
  // none — it's used as-is.
1484
1510
  ...(options.guards ?? []).filter(isGuardClass)
1485
1511
  ],
1512
+ // MEDIA_CONSOLE_AUTH is exported (not just provided) because MediaDashboardUiController —
1513
+ // hosted in MediaDashboardModule, which imports THIS module — reads it to decide whether to
1514
+ // hand an unauthenticated navigation to the host's `unauthenticatedPage`. Exporting keeps the
1515
+ // single `authProvider` instance shared; re-providing the same factory in both modules would
1516
+ // run a `forRootAsync` host's `useAuth` twice.
1486
1517
  exports: [
1487
- MediaConsoleService
1518
+ MediaConsoleService,
1519
+ MEDIA_CONSOLE_AUTH
1488
1520
  ]
1489
1521
  };
1490
1522
  }
@@ -1529,27 +1561,77 @@ var CONTENT_TYPES = {
1529
1561
  ".woff2": "font/woff2",
1530
1562
  ".ico": "image/x-icon"
1531
1563
  };
1532
- var MediaDashboardUiController = class {
1564
+ var MediaDashboardUiController = class _MediaDashboardUiController {
1533
1565
  static {
1534
1566
  __name(this, "MediaDashboardUiController");
1535
1567
  }
1536
1568
  basePath;
1537
1569
  apiBasePath;
1570
+ auth;
1538
1571
  dir = spaDir();
1539
- constructor(basePath, apiBasePath) {
1572
+ logger = new import_common7.Logger(_MediaDashboardUiController.name);
1573
+ warned = false;
1574
+ constructor(basePath, apiBasePath, auth = null) {
1540
1575
  this.basePath = basePath;
1541
1576
  this.apiBasePath = apiBasePath;
1577
+ this.auth = auth;
1542
1578
  }
1543
1579
  // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = "stuck
1544
- // loading after a deploy"). The hashed assets below are immutable.
1545
- index() {
1580
+ // loading after a deploy"). The hashed assets below are immutable. `sendHtml` sets both headers;
1581
+ // they are no longer `@Header` decorators because this route is now non-passthrough (see below).
1582
+ //
1583
+ // Non-passthrough `@Res()` so a host's `auth.unauthenticatedPage` can OWN the response and serve
1584
+ // its own page in place of the SPA. Without a hook this behaves exactly as before: the shell is
1585
+ // served to everyone, and the SPA's own auth screen renders once its API calls come back 401.
1586
+ async index(req, res) {
1587
+ if (await this.serveUnauthenticatedPage(req, res)) return;
1546
1588
  const indexPath = (0, import_node_path.join)(this.dir, "index.html");
1547
1589
  if (!(0, import_node_fs.existsSync)(indexPath)) {
1548
1590
  throw new import_common7.NotFoundException("Console SPA is not built. Run the package build.");
1549
1591
  }
1550
1592
  const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
1551
1593
  const inject = `<script>window.__MEDIA_BASE__='${this.basePath}';window.__MEDIA_API__='${this.apiBasePath}';</script>`;
1552
- return html.includes("</head>") ? html.replace("</head>", `${inject}</head>`) : inject + html;
1594
+ sendHtml(res, 200, html.includes("</head>") ? html.replace("</head>", `${inject}</head>`) : inject + html);
1595
+ }
1596
+ /**
1597
+ * Hand the response to the host's `unauthenticatedPage` when this navigation has no valid session.
1598
+ * Returns `true` when the host wrote a page (the caller must not also serve the SPA).
1599
+ *
1600
+ * Mode-A-only, deliberately: with `login` configured the SPA's own login form is the way in, so
1601
+ * replacing the shell would lock a Mode B host out of its own console.
1602
+ */
1603
+ async serveUnauthenticatedPage(req, res) {
1604
+ const auth = this.auth;
1605
+ const hook = auth?.unauthenticatedPage;
1606
+ if (!auth || !hook) return false;
1607
+ if (auth.modes.includes("login")) return false;
1608
+ if (this.hasValidSession(auth, req)) return false;
1609
+ try {
1610
+ await hook({
1611
+ request: req,
1612
+ response: res,
1613
+ basePath: this.basePath
1614
+ });
1615
+ if (responseAlreadyWritten(res)) return true;
1616
+ this.warnOnce("auth.unauthenticatedPage returned without writing a response; serving the console SPA instead. The hook must write AND end the response it is given.");
1617
+ } catch (error) {
1618
+ this.warnOnce(`auth.unauthenticatedPage threw; serving the console SPA instead. ${String(error)}`);
1619
+ if (responseAlreadyWritten(res)) return true;
1620
+ }
1621
+ return false;
1622
+ }
1623
+ hasValidSession(auth, req) {
1624
+ const cookieValue = parseCookieHeader(readCookieHeader(req))[SESSION_COOKIE_NAME];
1625
+ if (cookieValue === void 0) return false;
1626
+ return verifySessionCookie(cookieValue, {
1627
+ secret: auth.secret
1628
+ }) !== null;
1629
+ }
1630
+ /** Once per process, not once per request: a broken hook fires on every unauthenticated visit. */
1631
+ warnOnce(message) {
1632
+ if (this.warned) return;
1633
+ this.warned = true;
1634
+ this.logger.warn(message);
1553
1635
  }
1554
1636
  asset(file) {
1555
1637
  const safe = (0, import_node_path.basename)(file);
@@ -1567,11 +1649,14 @@ var MediaDashboardUiController = class {
1567
1649
  };
1568
1650
  _ts_decorate7([
1569
1651
  (0, import_common7.Get)(),
1570
- (0, import_common7.Header)("Content-Type", "text/html; charset=utf-8"),
1571
- (0, import_common7.Header)("Cache-Control", "no-store, must-revalidate"),
1652
+ _ts_param6(0, (0, import_common7.Req)()),
1653
+ _ts_param6(1, (0, import_common7.Res)()),
1572
1654
  _ts_metadata6("design:type", Function),
1573
- _ts_metadata6("design:paramtypes", []),
1574
- _ts_metadata6("design:returntype", String)
1655
+ _ts_metadata6("design:paramtypes", [
1656
+ Object,
1657
+ Object
1658
+ ]),
1659
+ _ts_metadata6("design:returntype", Promise)
1575
1660
  ], MediaDashboardUiController.prototype, "index", null);
1576
1661
  _ts_decorate7([
1577
1662
  (0, import_common7.Get)("assets/:file"),
@@ -1587,10 +1672,13 @@ MediaDashboardUiController = _ts_decorate7([
1587
1672
  (0, import_common7.Controller)(),
1588
1673
  _ts_param6(0, (0, import_common7.Inject)(MEDIA_DASHBOARD_BASE_PATH)),
1589
1674
  _ts_param6(1, (0, import_common7.Inject)(MEDIA_DASHBOARD_API_PATH)),
1675
+ _ts_param6(2, (0, import_common7.Optional)()),
1676
+ _ts_param6(2, (0, import_common7.Inject)(MEDIA_CONSOLE_AUTH)),
1590
1677
  _ts_metadata6("design:type", Function),
1591
1678
  _ts_metadata6("design:paramtypes", [
1592
1679
  String,
1593
- String
1680
+ String,
1681
+ Object
1594
1682
  ])
1595
1683
  ], MediaDashboardUiController);
1596
1684