@dudousxd/nestjs-media-dashboard 0.8.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.
- package/dist/server/index.cjs +133 -20
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +46 -1
- package/dist/server/index.d.ts +46 -1
- package/dist/server/index.js +134 -21
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/server/index.cjs
CHANGED
|
@@ -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),
|
|
@@ -75,6 +78,12 @@ function resolveConsoleAuth(options) {
|
|
|
75
78
|
} : {},
|
|
76
79
|
...options.login ? {
|
|
77
80
|
login: options.login
|
|
81
|
+
} : {},
|
|
82
|
+
...options.revalidate ? {
|
|
83
|
+
revalidate: options.revalidate
|
|
84
|
+
} : {},
|
|
85
|
+
...options.unauthenticatedPage ? {
|
|
86
|
+
unauthenticatedPage: options.unauthenticatedPage
|
|
78
87
|
} : {}
|
|
79
88
|
};
|
|
80
89
|
}
|
|
@@ -221,6 +230,26 @@ function appendSetCookie(response, cookie) {
|
|
|
221
230
|
]);
|
|
222
231
|
}
|
|
223
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");
|
|
224
253
|
|
|
225
254
|
// src/server/auth/session-cookie.ts
|
|
226
255
|
var import_node_crypto = require("crypto");
|
|
@@ -349,14 +378,16 @@ var MediaConsoleGuard = class {
|
|
|
349
378
|
this.auth = auth;
|
|
350
379
|
this.cookiePath = cookiePath;
|
|
351
380
|
}
|
|
352
|
-
canActivate(context) {
|
|
381
|
+
async canActivate(context) {
|
|
353
382
|
if (!this.auth) return true;
|
|
354
383
|
const http = context.switchToHttp();
|
|
355
384
|
const request = http.getRequest();
|
|
356
385
|
const session = this.verifyRequestSession(request);
|
|
357
386
|
if (!session) throw new import_common.UnauthorizedException();
|
|
358
387
|
attachSession(request, session);
|
|
359
|
-
this.maybeRenew(http.getResponse(), request, session)
|
|
388
|
+
if (!await this.maybeRenew(http.getResponse(), request, session)) {
|
|
389
|
+
throw new import_common.UnauthorizedException();
|
|
390
|
+
}
|
|
360
391
|
return true;
|
|
361
392
|
}
|
|
362
393
|
verifyRequestSession(request) {
|
|
@@ -368,26 +399,46 @@ var MediaConsoleGuard = class {
|
|
|
368
399
|
});
|
|
369
400
|
}
|
|
370
401
|
/**
|
|
371
|
-
* Sliding renewal: when a valid cookie is past half its TTL, re-issue a fresh one
|
|
372
|
-
* never get logged out mid-session
|
|
402
|
+
* Sliding renewal + revalidation: when a valid cookie is past half its TTL, re-issue a fresh one
|
|
403
|
+
* so active users never get logged out mid-session — but first let the host's `revalidate` hook
|
|
404
|
+
* re-check the user, so a deactivated or demoted operator loses access instead of riding a
|
|
405
|
+
* self-renewing cookie. Returns `false` when the session was revoked (cookie already cleared).
|
|
373
406
|
*/
|
|
374
|
-
maybeRenew(response, request, session) {
|
|
375
|
-
if (!this.auth) return;
|
|
407
|
+
async maybeRenew(response, request, session) {
|
|
408
|
+
if (!this.auth) return true;
|
|
376
409
|
const now = Date.now();
|
|
377
|
-
if (now - session.iat <= this.auth.ttlMs / 2) return;
|
|
378
|
-
|
|
410
|
+
if (now - session.iat <= this.auth.ttlMs / 2) return true;
|
|
411
|
+
const user = {
|
|
379
412
|
id: session.sub,
|
|
380
413
|
...session.name !== void 0 ? {
|
|
381
414
|
name: session.name
|
|
382
415
|
} : {},
|
|
383
416
|
roles: session.roles
|
|
384
|
-
}
|
|
417
|
+
};
|
|
418
|
+
if (this.auth.revalidate) {
|
|
419
|
+
let allowed;
|
|
420
|
+
try {
|
|
421
|
+
allowed = await this.auth.revalidate(user);
|
|
422
|
+
} catch {
|
|
423
|
+
allowed = false;
|
|
424
|
+
}
|
|
425
|
+
if (!allowed) {
|
|
426
|
+
clearSessionCookie({
|
|
427
|
+
cookiePath: this.cookiePath ?? "/",
|
|
428
|
+
request,
|
|
429
|
+
response
|
|
430
|
+
});
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
issueSessionCookie(user, {
|
|
385
435
|
auth: this.auth,
|
|
386
436
|
cookiePath: this.cookiePath ?? "/",
|
|
387
437
|
request,
|
|
388
438
|
response,
|
|
389
439
|
now
|
|
390
440
|
});
|
|
441
|
+
return true;
|
|
391
442
|
}
|
|
392
443
|
};
|
|
393
444
|
MediaConsoleGuard = _ts_decorate([
|
|
@@ -1458,8 +1509,14 @@ var MediaConsoleApiModule = class _MediaConsoleApiModule {
|
|
|
1458
1509
|
// none — it's used as-is.
|
|
1459
1510
|
...(options.guards ?? []).filter(isGuardClass)
|
|
1460
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.
|
|
1461
1517
|
exports: [
|
|
1462
|
-
MediaConsoleService
|
|
1518
|
+
MediaConsoleService,
|
|
1519
|
+
MEDIA_CONSOLE_AUTH
|
|
1463
1520
|
]
|
|
1464
1521
|
};
|
|
1465
1522
|
}
|
|
@@ -1504,27 +1561,77 @@ var CONTENT_TYPES = {
|
|
|
1504
1561
|
".woff2": "font/woff2",
|
|
1505
1562
|
".ico": "image/x-icon"
|
|
1506
1563
|
};
|
|
1507
|
-
var MediaDashboardUiController = class {
|
|
1564
|
+
var MediaDashboardUiController = class _MediaDashboardUiController {
|
|
1508
1565
|
static {
|
|
1509
1566
|
__name(this, "MediaDashboardUiController");
|
|
1510
1567
|
}
|
|
1511
1568
|
basePath;
|
|
1512
1569
|
apiBasePath;
|
|
1570
|
+
auth;
|
|
1513
1571
|
dir = spaDir();
|
|
1514
|
-
|
|
1572
|
+
logger = new import_common7.Logger(_MediaDashboardUiController.name);
|
|
1573
|
+
warned = false;
|
|
1574
|
+
constructor(basePath, apiBasePath, auth = null) {
|
|
1515
1575
|
this.basePath = basePath;
|
|
1516
1576
|
this.apiBasePath = apiBasePath;
|
|
1577
|
+
this.auth = auth;
|
|
1517
1578
|
}
|
|
1518
1579
|
// index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = "stuck
|
|
1519
|
-
// loading after a deploy"). The hashed assets below are immutable.
|
|
1520
|
-
|
|
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;
|
|
1521
1588
|
const indexPath = (0, import_node_path.join)(this.dir, "index.html");
|
|
1522
1589
|
if (!(0, import_node_fs.existsSync)(indexPath)) {
|
|
1523
1590
|
throw new import_common7.NotFoundException("Console SPA is not built. Run the package build.");
|
|
1524
1591
|
}
|
|
1525
1592
|
const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
|
|
1526
1593
|
const inject = `<script>window.__MEDIA_BASE__='${this.basePath}';window.__MEDIA_API__='${this.apiBasePath}';</script>`;
|
|
1527
|
-
|
|
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);
|
|
1528
1635
|
}
|
|
1529
1636
|
asset(file) {
|
|
1530
1637
|
const safe = (0, import_node_path.basename)(file);
|
|
@@ -1542,11 +1649,14 @@ var MediaDashboardUiController = class {
|
|
|
1542
1649
|
};
|
|
1543
1650
|
_ts_decorate7([
|
|
1544
1651
|
(0, import_common7.Get)(),
|
|
1545
|
-
(0, import_common7.
|
|
1546
|
-
(0, import_common7.
|
|
1652
|
+
_ts_param6(0, (0, import_common7.Req)()),
|
|
1653
|
+
_ts_param6(1, (0, import_common7.Res)()),
|
|
1547
1654
|
_ts_metadata6("design:type", Function),
|
|
1548
|
-
_ts_metadata6("design:paramtypes", [
|
|
1549
|
-
|
|
1655
|
+
_ts_metadata6("design:paramtypes", [
|
|
1656
|
+
Object,
|
|
1657
|
+
Object
|
|
1658
|
+
]),
|
|
1659
|
+
_ts_metadata6("design:returntype", Promise)
|
|
1550
1660
|
], MediaDashboardUiController.prototype, "index", null);
|
|
1551
1661
|
_ts_decorate7([
|
|
1552
1662
|
(0, import_common7.Get)("assets/:file"),
|
|
@@ -1562,10 +1672,13 @@ MediaDashboardUiController = _ts_decorate7([
|
|
|
1562
1672
|
(0, import_common7.Controller)(),
|
|
1563
1673
|
_ts_param6(0, (0, import_common7.Inject)(MEDIA_DASHBOARD_BASE_PATH)),
|
|
1564
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)),
|
|
1565
1677
|
_ts_metadata6("design:type", Function),
|
|
1566
1678
|
_ts_metadata6("design:paramtypes", [
|
|
1567
1679
|
String,
|
|
1568
|
-
String
|
|
1680
|
+
String,
|
|
1681
|
+
Object
|
|
1569
1682
|
])
|
|
1570
1683
|
], MediaDashboardUiController);
|
|
1571
1684
|
|