@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 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
- return () => page("Access", { isLoading, error }, () => {
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
- if (!r) return null;
430
- return [
431
- (0, import_vue8.h)("h2", { style: styles.subtitle }, "Admin token"),
432
- (0, import_vue8.h)("p", [
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.legacy.map(
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: { client: { type: Object, required: true } },
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: { client: { type: Object, required: true } },
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"),