@fonderie/vue-admin-screens 0.4.0 → 0.6.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 +8 -1
- package/dist/index.cjs +255 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +279 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Status: **experimental** (0.x).
|
|
|
14
14
|
| Money | Catalog · Subscriber | what am I selling (configured vs stored) · what is this subscriber on, their wallet, ledger, a manual grant (`billingClient`) |
|
|
15
15
|
| Settings | Config & secrets | the `@fonderie/config` admin screens, as a sub-page |
|
|
16
16
|
| Messaging | Templates | the `@fonderie/courier` admin screens, as a sub-page |
|
|
17
|
-
| Activity | Audit · Admin log · Access | what happened, every workspace (`auditClient`) · who did what through here · who can be here |
|
|
17
|
+
| Activity | Audit · Admin log · Access | what happened, every workspace (`auditClient`) · who did what through here · who can be here, and issue or revoke scoped tokens |
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import { AdminClient, AuditAdminClient, AuthAdminClient, BillingAdminClient, ConfigAdminClient, CourierAdminClient } from '@fonderie/client';
|
|
@@ -38,6 +38,13 @@ Pass `page` and listen to `navigate` to own the URL (one route per page);
|
|
|
38
38
|
omit `page` and the shell keeps it itself. Settings and Messaging appear only
|
|
39
39
|
when their client is given.
|
|
40
40
|
|
|
41
|
+
**Give the shell a `read` token, not the root one.** With `@fonderie/admin`
|
|
42
|
+
≥ 0.6 the configured `adminToken` is the root — every scope, including the
|
|
43
|
+
plaintext secret reveal, and the only credential that can mint or revoke.
|
|
44
|
+
Issue a scoped one for the dashboard (`fonderie admin token issue dashboard
|
|
45
|
+
--scopes read`, or the Access page itself) and hand the shell that. The
|
|
46
|
+
Access page then shows issuing as unavailable, which is the point.
|
|
47
|
+
|
|
41
48
|
Nothing here stores the token: the app constructs the clients and decides
|
|
42
49
|
where the token lives. Every request through the shell lands in the admin
|
|
43
50
|
log, including the ones the token refuses.
|
package/dist/index.cjs
CHANGED
|
@@ -419,17 +419,28 @@ var RoutesScreen = (0, import_vue7.defineComponent)({
|
|
|
419
419
|
// src/screens/TokensScreen.ts
|
|
420
420
|
var import_vue_admin7 = require("@fonderie/vue-admin");
|
|
421
421
|
var import_vue8 = require("vue");
|
|
422
|
+
var ALL = ["read", "write", "secrets"];
|
|
422
423
|
var TokensScreen = (0, import_vue8.defineComponent)({
|
|
423
424
|
name: "FonderieTokensScreen",
|
|
424
425
|
props: { client: { type: Object, required: true } },
|
|
425
426
|
setup(props) {
|
|
426
|
-
const { report, isLoading, error } = (0, import_vue_admin7.useAdminTokens)(props.client);
|
|
427
|
-
|
|
427
|
+
const { report, isLoading, error, issue, revoke } = (0, import_vue_admin7.useAdminTokens)(props.client);
|
|
428
|
+
const name = (0, import_vue8.ref)("");
|
|
429
|
+
const scopes = (0, import_vue8.ref)(["read"]);
|
|
430
|
+
const days = (0, import_vue8.ref)("");
|
|
431
|
+
const minted = (0, import_vue8.ref)(null);
|
|
432
|
+
const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
|
|
433
|
+
return () => {
|
|
428
434
|
const r = report.value;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
(0, import_vue8.h)(
|
|
432
|
-
|
|
435
|
+
return (0, import_vue8.h)("div", { style: styles.container }, [
|
|
436
|
+
(0, import_vue8.h)("h1", { style: styles.title }, "Access"),
|
|
437
|
+
error.value ? (0, import_vue8.h)(
|
|
438
|
+
"p",
|
|
439
|
+
{ style: styles.error, role: "alert" },
|
|
440
|
+
error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
|
|
441
|
+
) : null,
|
|
442
|
+
(0, import_vue8.h)("h2", { style: styles.subtitle }, "Root token"),
|
|
443
|
+
r ? (0, import_vue8.h)("p", [
|
|
433
444
|
(0, import_vue8.h)(
|
|
434
445
|
"span",
|
|
435
446
|
{ style: r.admin.ok ? styles.ok : styles.bad },
|
|
@@ -438,16 +449,124 @@ var TokensScreen = (0, import_vue8.defineComponent)({
|
|
|
438
449
|
...r.admin.problems.map(
|
|
439
450
|
(p) => (0, import_vue8.h)("div", { key: p.message, style: styles.bad }, p.message)
|
|
440
451
|
)
|
|
441
|
-
]),
|
|
452
|
+
]) : null,
|
|
453
|
+
(0, import_vue8.h)("h2", { style: styles.subtitle }, "Issued tokens"),
|
|
454
|
+
r?.issued === null ? (0, import_vue8.h)(
|
|
455
|
+
"p",
|
|
456
|
+
{ style: styles.muted },
|
|
457
|
+
"Issuing is off \u2014 give AdminModule a store and run its migrations."
|
|
458
|
+
) : [
|
|
459
|
+
minted.value ? (0, import_vue8.h)("p", { style: styles.ok }, [
|
|
460
|
+
"Copy this now \u2014 it is never shown again. ",
|
|
461
|
+
(0, import_vue8.h)("span", { style: styles.mono }, minted.value.token),
|
|
462
|
+
` (${minted.value.name})`
|
|
463
|
+
]) : null,
|
|
464
|
+
(0, import_vue8.h)(
|
|
465
|
+
"form",
|
|
466
|
+
{
|
|
467
|
+
style: styles.toolbar,
|
|
468
|
+
onSubmit: (e) => {
|
|
469
|
+
e.preventDefault();
|
|
470
|
+
const d = Number(days.value);
|
|
471
|
+
void issue({
|
|
472
|
+
name: name.value.trim(),
|
|
473
|
+
scopes: scopes.value,
|
|
474
|
+
...days.value && Number.isInteger(d) && d > 0 ? { expiresInDays: d } : {}
|
|
475
|
+
}).then((t) => {
|
|
476
|
+
minted.value = { name: t.name, token: t.token };
|
|
477
|
+
name.value = "";
|
|
478
|
+
days.value = "";
|
|
479
|
+
}).catch(() => {
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
[
|
|
484
|
+
(0, import_vue8.h)("input", {
|
|
485
|
+
value: name.value,
|
|
486
|
+
onInput: (e) => name.value = e.target.value,
|
|
487
|
+
placeholder: "name",
|
|
488
|
+
style: { ...styles.button, cursor: "text" },
|
|
489
|
+
"aria-label": "Name"
|
|
490
|
+
}),
|
|
491
|
+
...ALL.map(
|
|
492
|
+
(s) => (0, import_vue8.h)("label", { key: s, style: { ...styles.badge, cursor: "pointer" } }, [
|
|
493
|
+
(0, import_vue8.h)("input", {
|
|
494
|
+
type: "checkbox",
|
|
495
|
+
checked: scopes.value.includes(s),
|
|
496
|
+
onChange: () => toggle(s)
|
|
497
|
+
}),
|
|
498
|
+
` ${s}`
|
|
499
|
+
])
|
|
500
|
+
),
|
|
501
|
+
(0, import_vue8.h)("input", {
|
|
502
|
+
value: days.value,
|
|
503
|
+
onInput: (e) => days.value = e.target.value,
|
|
504
|
+
placeholder: "days (optional)",
|
|
505
|
+
style: { ...styles.button, cursor: "text", width: "140px" },
|
|
506
|
+
"aria-label": "Days"
|
|
507
|
+
}),
|
|
508
|
+
(0, import_vue8.h)(
|
|
509
|
+
"button",
|
|
510
|
+
{
|
|
511
|
+
type: "submit",
|
|
512
|
+
style: styles.button,
|
|
513
|
+
disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
|
|
514
|
+
},
|
|
515
|
+
"Issue"
|
|
516
|
+
)
|
|
517
|
+
]
|
|
518
|
+
),
|
|
519
|
+
r?.issued && r.issued.length > 0 ? table(
|
|
520
|
+
["Name", "Scopes", "Created", "Expires", "Last used", ""],
|
|
521
|
+
r.issued.map(
|
|
522
|
+
(t) => (0, import_vue8.h)("tr", { key: t.id }, [
|
|
523
|
+
td([
|
|
524
|
+
t.name,
|
|
525
|
+
t.revokedAt ? (0, import_vue8.h)("span", { style: styles.badge }, " revoked") : ""
|
|
526
|
+
]),
|
|
527
|
+
td(t.scopes.join(", "), styles.mono),
|
|
528
|
+
td(
|
|
529
|
+
`${new Date(t.createdAt).toLocaleDateString()} \xB7 ${t.createdBy}`,
|
|
530
|
+
styles.muted
|
|
531
|
+
),
|
|
532
|
+
td(
|
|
533
|
+
t.expiresAt ? new Date(t.expiresAt).toLocaleDateString() : "never",
|
|
534
|
+
styles.muted
|
|
535
|
+
),
|
|
536
|
+
td(
|
|
537
|
+
t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleString() : "never",
|
|
538
|
+
styles.muted
|
|
539
|
+
),
|
|
540
|
+
td(
|
|
541
|
+
t.revokedAt ? "" : (0, import_vue8.h)(
|
|
542
|
+
"button",
|
|
543
|
+
{
|
|
544
|
+
type: "button",
|
|
545
|
+
style: styles.button,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
if (window.confirm(
|
|
548
|
+
`Revoke "${t.name}"? Anything using it stops working immediately.`
|
|
549
|
+
))
|
|
550
|
+
void revoke(t.id).catch(() => {
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
"Revoke"
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
])
|
|
558
|
+
)
|
|
559
|
+
) : (0, import_vue8.h)("p", { style: styles.muted }, "None issued.")
|
|
560
|
+
],
|
|
442
561
|
(0, import_vue8.h)("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
|
|
443
|
-
r.legacy.length === 0 ? (0, import_vue8.h)(
|
|
562
|
+
r && r.legacy.length === 0 ? (0, import_vue8.h)(
|
|
444
563
|
"p",
|
|
445
564
|
{ style: styles.muted },
|
|
446
565
|
"None \u2014 every brick's admin surface goes through this token."
|
|
447
566
|
) : (0, import_vue8.h)(
|
|
448
567
|
"ul",
|
|
449
568
|
{ style: styles.list },
|
|
450
|
-
r
|
|
569
|
+
(r?.legacy ?? []).map(
|
|
451
570
|
(l) => (0, import_vue8.h)("li", { key: l.module, style: styles.row }, [
|
|
452
571
|
(0, import_vue8.h)("span", { style: styles.mono }, l.module),
|
|
453
572
|
" ",
|
|
@@ -459,8 +578,8 @@ var TokensScreen = (0, import_vue8.defineComponent)({
|
|
|
459
578
|
])
|
|
460
579
|
)
|
|
461
580
|
)
|
|
462
|
-
];
|
|
463
|
-
}
|
|
581
|
+
]);
|
|
582
|
+
};
|
|
464
583
|
}
|
|
465
584
|
});
|
|
466
585
|
|
|
@@ -469,19 +588,30 @@ var import_vue_admin8 = require("@fonderie/vue-admin");
|
|
|
469
588
|
var import_vue9 = require("vue");
|
|
470
589
|
var UsersScreen = (0, import_vue9.defineComponent)({
|
|
471
590
|
name: "FonderieUsersScreen",
|
|
472
|
-
props: {
|
|
591
|
+
props: {
|
|
592
|
+
client: { type: Object, required: true },
|
|
593
|
+
pageSize: { type: Number, default: 50 }
|
|
594
|
+
},
|
|
473
595
|
setup(props) {
|
|
474
596
|
const input = (0, import_vue9.ref)("");
|
|
475
597
|
const email = (0, import_vue9.ref)("");
|
|
598
|
+
const selectedId = (0, import_vue9.ref)("");
|
|
599
|
+
const list = (0, import_vue_admin8.useAdminUsers)(props.client, { limit: props.pageSize });
|
|
476
600
|
const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin8.useAdminUser)(
|
|
477
601
|
props.client,
|
|
478
|
-
{ email }
|
|
602
|
+
{ email, id: selectedId }
|
|
479
603
|
);
|
|
480
604
|
const userId = (0, import_vue9.computed)(() => user.value?.id ?? null);
|
|
481
605
|
const sessions = (0, import_vue_admin8.useAdminUserSessions)(props.client, userId);
|
|
482
606
|
const history = (0, import_vue_admin8.useAdminLoginHistory)(props.client, userId, { limit: 20 });
|
|
607
|
+
const showList = (0, import_vue9.computed)(() => !email.value && !selectedId.value);
|
|
483
608
|
const yesNo = (v) => (0, import_vue9.h)("span", { style: v ? styles.ok : styles.muted }, v ? "yes" : "no");
|
|
484
609
|
const row = (k, v) => (0, import_vue9.h)("tr", [td(k), td(v)]);
|
|
610
|
+
const clear = () => {
|
|
611
|
+
email.value = "";
|
|
612
|
+
selectedId.value = "";
|
|
613
|
+
input.value = "";
|
|
614
|
+
};
|
|
485
615
|
return () => {
|
|
486
616
|
const u = user.value;
|
|
487
617
|
return (0, import_vue9.h)("div", { style: styles.container }, [
|
|
@@ -492,6 +622,7 @@ var UsersScreen = (0, import_vue9.defineComponent)({
|
|
|
492
622
|
style: styles.toolbar,
|
|
493
623
|
onSubmit: (e) => {
|
|
494
624
|
e.preventDefault();
|
|
625
|
+
selectedId.value = "";
|
|
495
626
|
email.value = input.value.trim();
|
|
496
627
|
}
|
|
497
628
|
},
|
|
@@ -508,9 +639,48 @@ var UsersScreen = (0, import_vue9.defineComponent)({
|
|
|
508
639
|
"button",
|
|
509
640
|
{ type: "submit", style: styles.button, disabled: isLoading.value },
|
|
510
641
|
"Look up"
|
|
511
|
-
)
|
|
642
|
+
),
|
|
643
|
+
showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : refreshButton("\u2190 All users", false, clear)
|
|
512
644
|
]
|
|
513
645
|
),
|
|
646
|
+
showList.value ? (0, import_vue9.h)("div", [
|
|
647
|
+
list.error.value ? (0, import_vue9.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
648
|
+
list.users.value.length === 0 && !list.isLoading.value ? (0, import_vue9.h)("p", { style: styles.muted }, "No users yet.") : table(
|
|
649
|
+
["Email", "Name", "Created", "Status"],
|
|
650
|
+
list.users.value.map(
|
|
651
|
+
(u2) => (0, import_vue9.h)("tr", { key: u2.id }, [
|
|
652
|
+
td(
|
|
653
|
+
(0, import_vue9.h)(
|
|
654
|
+
"button",
|
|
655
|
+
{
|
|
656
|
+
type: "button",
|
|
657
|
+
style: { ...styles.navItem, padding: 0 },
|
|
658
|
+
onClick: () => selectedId.value = u2.id
|
|
659
|
+
},
|
|
660
|
+
u2.email
|
|
661
|
+
)
|
|
662
|
+
),
|
|
663
|
+
td(
|
|
664
|
+
`${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue9.h)("span", { style: styles.muted }, "\u2014")
|
|
665
|
+
),
|
|
666
|
+
td(new Date(u2.createdAt).toLocaleDateString()),
|
|
667
|
+
td(
|
|
668
|
+
u2.suspended ? (0, import_vue9.h)("span", { style: styles.badge }, "suspended") : u2.deletedAt ? (0, import_vue9.h)("span", { style: styles.badge }, "deleted") : (0, import_vue9.h)("span", { style: styles.muted }, "active")
|
|
669
|
+
)
|
|
670
|
+
])
|
|
671
|
+
)
|
|
672
|
+
),
|
|
673
|
+
list.isLoading.value ? (0, import_vue9.h)("p", { style: styles.status }, "Loading\u2026") : null,
|
|
674
|
+
list.hasMore.value && !list.isLoading.value ? (0, import_vue9.h)(
|
|
675
|
+
"button",
|
|
676
|
+
{
|
|
677
|
+
type: "button",
|
|
678
|
+
style: { ...styles.button, marginTop: "8px" },
|
|
679
|
+
onClick: () => void list.loadMore()
|
|
680
|
+
},
|
|
681
|
+
"Load more"
|
|
682
|
+
) : null
|
|
683
|
+
]) : null,
|
|
514
684
|
error.value ? (0, import_vue9.h)(
|
|
515
685
|
"p",
|
|
516
686
|
{ style: styles.error, role: "alert" },
|
|
@@ -673,12 +843,22 @@ var import_vue_admin10 = require("@fonderie/vue-admin");
|
|
|
673
843
|
var import_vue11 = require("vue");
|
|
674
844
|
var SubscriberScreen = (0, import_vue11.defineComponent)({
|
|
675
845
|
name: "FonderieSubscriberScreen",
|
|
676
|
-
props: {
|
|
846
|
+
props: {
|
|
847
|
+
client: { type: Object, required: true },
|
|
848
|
+
pageSize: { type: Number, default: 50 }
|
|
849
|
+
},
|
|
677
850
|
setup(props) {
|
|
678
851
|
const type = (0, import_vue11.ref)("user");
|
|
679
852
|
const idInput = (0, import_vue11.ref)("");
|
|
680
853
|
const subscriber = (0, import_vue11.ref)(null);
|
|
854
|
+
const list = (0, import_vue_admin10.useAdminSubscribers)(props.client, { limit: props.pageSize });
|
|
681
855
|
const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin10.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
|
|
856
|
+
const open = (t, id) => {
|
|
857
|
+
granted.value = null;
|
|
858
|
+
type.value = t;
|
|
859
|
+
idInput.value = id;
|
|
860
|
+
subscriber.value = { type: t, id };
|
|
861
|
+
};
|
|
682
862
|
const amount = (0, import_vue11.ref)("");
|
|
683
863
|
const note = (0, import_vue11.ref)("");
|
|
684
864
|
const granted = (0, import_vue11.ref)(null);
|
|
@@ -723,9 +903,69 @@ var SubscriberScreen = (0, import_vue11.defineComponent)({
|
|
|
723
903
|
"button",
|
|
724
904
|
{ type: "submit", style: styles.button, disabled: isLoading.value },
|
|
725
905
|
"Look up"
|
|
906
|
+
),
|
|
907
|
+
subscriber.value ? (0, import_vue11.h)(
|
|
908
|
+
"button",
|
|
909
|
+
{
|
|
910
|
+
type: "button",
|
|
911
|
+
style: styles.button,
|
|
912
|
+
onClick: () => {
|
|
913
|
+
subscriber.value = null;
|
|
914
|
+
idInput.value = "";
|
|
915
|
+
granted.value = null;
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
"\u2190 All subscribers"
|
|
919
|
+
) : (0, import_vue11.h)(
|
|
920
|
+
"button",
|
|
921
|
+
{
|
|
922
|
+
type: "button",
|
|
923
|
+
style: styles.button,
|
|
924
|
+
disabled: list.isLoading.value,
|
|
925
|
+
onClick: () => void list.refresh()
|
|
926
|
+
},
|
|
927
|
+
"Refresh"
|
|
726
928
|
)
|
|
727
929
|
]
|
|
728
930
|
),
|
|
931
|
+
!subscriber.value ? (0, import_vue11.h)("div", [
|
|
932
|
+
list.error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
|
|
933
|
+
list.subscriptions.value.length === 0 && !list.isLoading.value ? (0, import_vue11.h)("p", { style: styles.muted }, "No subscribers yet.") : table(
|
|
934
|
+
["Subscriber", "Plan", "Status", "Renews"],
|
|
935
|
+
list.subscriptions.value.map(
|
|
936
|
+
(sub) => (0, import_vue11.h)("tr", { key: sub.id }, [
|
|
937
|
+
td(
|
|
938
|
+
(0, import_vue11.h)(
|
|
939
|
+
"button",
|
|
940
|
+
{
|
|
941
|
+
type: "button",
|
|
942
|
+
style: { ...styles.navItem, padding: 0, ...styles.mono },
|
|
943
|
+
onClick: () => open(sub.subscriberType, sub.subscriberId)
|
|
944
|
+
},
|
|
945
|
+
`${sub.subscriberType}/${sub.subscriberId}`
|
|
946
|
+
)
|
|
947
|
+
),
|
|
948
|
+
td(`${sub.plan} \xB7 ${sub.interval}`),
|
|
949
|
+
td(
|
|
950
|
+
sub.cancelAtPeriodEnd ? [sub.status, " ", (0, import_vue11.h)("span", { style: styles.badge }, "cancels")] : sub.status
|
|
951
|
+
),
|
|
952
|
+
td(
|
|
953
|
+
sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue11.h)("span", { style: styles.muted }, "\u2014")
|
|
954
|
+
)
|
|
955
|
+
])
|
|
956
|
+
)
|
|
957
|
+
),
|
|
958
|
+
list.isLoading.value ? (0, import_vue11.h)("p", { style: styles.status }, "Loading\u2026") : null,
|
|
959
|
+
list.hasMore.value && !list.isLoading.value ? (0, import_vue11.h)(
|
|
960
|
+
"button",
|
|
961
|
+
{
|
|
962
|
+
type: "button",
|
|
963
|
+
style: { ...styles.button, marginTop: "8px" },
|
|
964
|
+
onClick: () => void list.loadMore()
|
|
965
|
+
},
|
|
966
|
+
"Load more"
|
|
967
|
+
) : null
|
|
968
|
+
]) : null,
|
|
729
969
|
error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
|
|
730
970
|
subscriber.value && !isLoading.value ? [
|
|
731
971
|
(0, import_vue11.h)("h2", { style: styles.subtitle }, "Subscription"),
|