@dudousxd/nestjs-agent-dashboard 0.10.1 → 0.11.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.
package/README.md CHANGED
@@ -112,6 +112,30 @@ AgentDashboardModule.forRoot({
112
112
 
113
113
  Have your host frontend call `POST <basePath>/auth/session` once it knows the visitor is signed in — no body this library reads. Success is `204` with the `Set-Cookie`; a denial is `401` and no cookie is set. An unauthenticated PAGE navigation under Mode-A-only has nowhere to redirect to (there's no login form) — it's served a small, static instruction page at `<basePath>/auth/session-required` instead, telling the visitor to sign in through the host app and reload.
114
114
 
115
+ ##### `unauthenticatedPage` — render that instruction page yourself
116
+
117
+ The built-in page cannot know who hosts the console, so it can only say "open this console from your application" in the abstract — it can't name your launcher, link to it, or look like the rest of your product. Pass `unauthenticatedPage` and the whole response is yours:
118
+
119
+ ```ts
120
+ AgentDashboardModule.forRoot({
121
+ dashboardAuth: {
122
+ secret: process.env.AGENT_DASHBOARD_SECRET,
123
+ session: (request) => resolveAdmin(request),
124
+ unauthenticatedPage: ({ request, response, basePath }) => {
125
+ // `request`/`response` are your platform's own objects (Express here). Render however you
126
+ // already render — a template engine, @dudousxd/nestjs-inertia, a plain string.
127
+ (response as Response).status(401).render('console-locked', { returnTo: basePath });
128
+ },
129
+ },
130
+ });
131
+ ```
132
+
133
+ The hook **owns** the response: it must write AND end it. It replaces what `<basePath>/auth/session-required` *contains*, not the flow that reaches it — a denied navigation is still redirected there.
134
+
135
+ It cannot open the console: it only ever runs on a request that has already been denied. If it throws, or returns without writing, the library logs one warning and falls back to the built-in page, so a broken page can't hang the request or turn a denial into a `500`. It is also not a mode — `unauthenticatedPage` alone still fails boot on the "at least one of `session` or `login`" check.
136
+
137
+ Under Mode B this route stays `404`: there is no session-required page to customise, because a denied navigation goes to the login form instead. To ship your own **login** UI, combine Mode A with this hook and `POST <basePath>/auth/session` from your page — that mint endpoint is the supported primitive for it.
138
+
115
139
  #### Mode B — `login` (built-in login screen)
116
140
 
117
141
  No host frontend/IdP to lean on — the console serves its own small, dependency-free, server-rendered login page. An unauthenticated page visit is redirected (302) to a built-in login form at `<basePath>/auth/login`.
@@ -189,6 +189,20 @@ function resolveRawResponse(response) {
189
189
  return null;
190
190
  }
191
191
  __name(resolveRawResponse, "resolveRawResponse");
192
+ function responseAlreadyWritten(response) {
193
+ const raw = resolveRawResponse(response);
194
+ return raw?.headersSent === true;
195
+ }
196
+ __name(responseAlreadyWritten, "responseAlreadyWritten");
197
+ function sendHtml(response, status, html) {
198
+ const raw = resolveRawResponse(response);
199
+ if (!raw) return;
200
+ raw.statusCode = status;
201
+ raw.setHeader("content-type", "text/html; charset=utf-8");
202
+ raw.setHeader("cache-control", "no-store, must-revalidate");
203
+ raw.end(html);
204
+ }
205
+ __name(sendHtml, "sendHtml");
192
206
  function sendRedirect(response, status, location) {
193
207
  const raw = resolveRawResponse(response);
194
208
  if (!raw) return;
@@ -1426,10 +1440,31 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1426
1440
  // Mode-A-only landing: the host mints the session, so there is nothing to submit here. Serves
1427
1441
  // as the redirect target `DashboardAuthPageGuard` bounces to instead of `/auth/login`, which
1428
1442
  // 404s under Mode A (see `loginPage` above).
1429
- sessionRequiredPage() {
1443
+ //
1444
+ // Non-passthrough `@Res()` (unlike its sibling routes) because `dashboardAuth.unauthenticatedPage`
1445
+ // hands the response to the HOST, which writes and ends it itself — there is no return value for
1446
+ // Nest to send, and letting Nest also send one would double-write. The `Content-Type` /
1447
+ // `Cache-Control` this route used to carry as decorators move into `sendHtml` for the built-in
1448
+ // path; the host owns its own headers on the hook path.
1449
+ async sessionRequiredPage(req, res) {
1430
1450
  const auth = this.requireAuth();
1431
1451
  if (auth.modes.includes("login")) throw new import_common7.NotFoundException();
1432
- return renderSessionRequiredPage();
1452
+ const hook = auth.unauthenticatedPage;
1453
+ if (hook) {
1454
+ try {
1455
+ await hook({
1456
+ request: req,
1457
+ response: res,
1458
+ basePath: this.basePath
1459
+ });
1460
+ if (responseAlreadyWritten(res)) return;
1461
+ this.warnHookOnce("unauthenticatedPage", "dashboardAuth.unauthenticatedPage returned without writing a response; falling back to the built-in page. The hook must write AND end the response it is given.");
1462
+ } catch (error) {
1463
+ this.warnHookOnce("unauthenticatedPage", `dashboardAuth.unauthenticatedPage threw; falling back to the built-in page. ${String(error)}`);
1464
+ if (responseAlreadyWritten(res)) return;
1465
+ }
1466
+ }
1467
+ sendHtml(res, 200, renderSessionRequiredPage());
1433
1468
  }
1434
1469
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
1435
1470
  redirectToLoginError(res, returnTo) {
@@ -1454,13 +1489,19 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1454
1489
  try {
1455
1490
  return await run() ?? null;
1456
1491
  } catch (error) {
1457
- if (!this.warnedHooks.has(kind)) {
1458
- this.warnedHooks.add(kind);
1459
- this.logger.warn(`AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1460
- }
1492
+ this.warnHookOnce(kind, `AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1461
1493
  return null;
1462
1494
  }
1463
1495
  }
1496
+ /**
1497
+ * Warn once per hook kind for the life of the process, not once per request: a broken hook fires
1498
+ * on every denied navigation and would otherwise flood the logs.
1499
+ */
1500
+ warnHookOnce(kind, message) {
1501
+ if (this.warnedHooks.has(kind)) return;
1502
+ this.warnedHooks.add(kind);
1503
+ this.logger.warn(message);
1504
+ }
1464
1505
  requireAuth() {
1465
1506
  if (!this.auth) throw new import_common7.NotFoundException();
1466
1507
  return this.auth;
@@ -1529,11 +1570,14 @@ _ts_decorate4([
1529
1570
  ], AgentDashboardAuthController.prototype, "session", null);
1530
1571
  _ts_decorate4([
1531
1572
  (0, import_common7.Get)("session-required"),
1532
- (0, import_common7.Header)("Content-Type", "text/html; charset=utf-8"),
1533
- (0, import_common7.Header)("Cache-Control", "no-store, must-revalidate"),
1573
+ _ts_param4(0, (0, import_common7.Req)()),
1574
+ _ts_param4(1, (0, import_common7.Res)()),
1534
1575
  _ts_metadata4("design:type", Function),
1535
- _ts_metadata4("design:paramtypes", []),
1536
- _ts_metadata4("design:returntype", String)
1576
+ _ts_metadata4("design:paramtypes", [
1577
+ typeof AuthPageRequest === "undefined" ? Object : AuthPageRequest,
1578
+ Object
1579
+ ]),
1580
+ _ts_metadata4("design:returntype", Promise)
1537
1581
  ], AgentDashboardAuthController.prototype, "sessionRequiredPage", null);
1538
1582
  AgentDashboardAuthController = _ts_decorate4([
1539
1583
  (0, import_common7.Controller)("auth"),
@@ -1863,6 +1907,9 @@ function resolveDashboardAuth(options) {
1863
1907
  if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
1864
1908
  throw new Error("AgentDashboardModule dashboardAuth: `revalidate` must be a function.");
1865
1909
  }
1910
+ if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
1911
+ throw new Error("AgentDashboardModule dashboardAuth: `unauthenticatedPage` must be a function.");
1912
+ }
1866
1913
  if (modes.length === 0) {
1867
1914
  throw new Error("AgentDashboardModule dashboardAuth: at least one of `session` (the host mints the session from its own auth) or `login` (the built-in login screen) is required \u2014 otherwise the cookie can never be minted. Failing closed.");
1868
1915
  }
@@ -1878,6 +1925,9 @@ function resolveDashboardAuth(options) {
1878
1925
  } : {},
1879
1926
  ...options.revalidate !== void 0 ? {
1880
1927
  revalidate: options.revalidate
1928
+ } : {},
1929
+ ...options.unauthenticatedPage !== void 0 ? {
1930
+ unauthenticatedPage: options.unauthenticatedPage
1881
1931
  } : {}
1882
1932
  };
1883
1933
  }