@serve.zone/dcrouter 13.4.2 → 13.7.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.
Files changed (148) hide show
  1. package/dist_serve/bundle.js +1779 -1375
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +2 -5
  4. package/dist_ts/classes.dcrouter.js +41 -10
  5. package/dist_ts/db/documents/classes.dns-provider.doc.d.ts +22 -0
  6. package/dist_ts/db/documents/classes.dns-provider.doc.js +134 -0
  7. package/dist_ts/db/documents/classes.dns-record.doc.d.ts +21 -0
  8. package/dist_ts/db/documents/classes.dns-record.doc.js +143 -0
  9. package/dist_ts/db/documents/classes.domain.doc.d.ts +22 -0
  10. package/dist_ts/db/documents/classes.domain.doc.js +146 -0
  11. package/dist_ts/db/documents/index.d.ts +3 -0
  12. package/dist_ts/db/documents/index.js +5 -1
  13. package/dist_ts/dns/index.d.ts +2 -0
  14. package/dist_ts/dns/index.js +3 -0
  15. package/dist_ts/dns/manager.dns.d.ts +227 -0
  16. package/dist_ts/dns/manager.dns.js +747 -0
  17. package/dist_ts/dns/providers/cloudflare.provider.d.ts +21 -0
  18. package/dist_ts/dns/providers/cloudflare.provider.js +106 -0
  19. package/dist_ts/dns/providers/factory.d.ts +23 -0
  20. package/dist_ts/dns/providers/factory.js +38 -0
  21. package/dist_ts/dns/providers/index.d.ts +3 -0
  22. package/dist_ts/dns/providers/index.js +4 -0
  23. package/dist_ts/dns/providers/interfaces.d.ts +54 -0
  24. package/dist_ts/dns/providers/interfaces.js +2 -0
  25. package/dist_ts/opsserver/classes.opsserver.d.ts +4 -0
  26. package/dist_ts/opsserver/classes.opsserver.js +9 -1
  27. package/dist_ts/opsserver/handlers/admin.handler.d.ts +9 -0
  28. package/dist_ts/opsserver/handlers/admin.handler.js +12 -1
  29. package/dist_ts/opsserver/handlers/config.handler.js +11 -2
  30. package/dist_ts/opsserver/handlers/dns-provider.handler.d.ts +16 -0
  31. package/dist_ts/opsserver/handlers/dns-provider.handler.js +119 -0
  32. package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +13 -0
  33. package/dist_ts/opsserver/handlers/dns-record.handler.js +98 -0
  34. package/dist_ts/opsserver/handlers/domain.handler.d.ts +13 -0
  35. package/dist_ts/opsserver/handlers/domain.handler.js +124 -0
  36. package/dist_ts/opsserver/handlers/index.d.ts +4 -0
  37. package/dist_ts/opsserver/handlers/index.js +5 -1
  38. package/dist_ts/opsserver/handlers/users.handler.d.ts +12 -0
  39. package/dist_ts/opsserver/handlers/users.handler.js +24 -0
  40. package/dist_ts_interfaces/data/dns-provider.d.ts +112 -0
  41. package/dist_ts_interfaces/data/dns-provider.js +27 -0
  42. package/dist_ts_interfaces/data/dns-record.d.ts +40 -0
  43. package/dist_ts_interfaces/data/dns-record.js +2 -0
  44. package/dist_ts_interfaces/data/domain.d.ts +34 -0
  45. package/dist_ts_interfaces/data/domain.js +2 -0
  46. package/dist_ts_interfaces/data/index.d.ts +3 -0
  47. package/dist_ts_interfaces/data/index.js +4 -1
  48. package/dist_ts_interfaces/data/route-management.d.ts +1 -1
  49. package/dist_ts_interfaces/requests/dns-providers.d.ts +117 -0
  50. package/dist_ts_interfaces/requests/dns-providers.js +2 -0
  51. package/dist_ts_interfaces/requests/dns-records.d.ts +89 -0
  52. package/dist_ts_interfaces/requests/dns-records.js +2 -0
  53. package/dist_ts_interfaces/requests/domains.d.ts +118 -0
  54. package/dist_ts_interfaces/requests/domains.js +2 -0
  55. package/dist_ts_interfaces/requests/index.d.ts +4 -0
  56. package/dist_ts_interfaces/requests/index.js +5 -1
  57. package/dist_ts_interfaces/requests/users.d.ts +19 -0
  58. package/dist_ts_interfaces/requests/users.js +3 -0
  59. package/dist_ts_web/00_commitinfo_data.js +1 -1
  60. package/dist_ts_web/appstate.d.ts +85 -0
  61. package/dist_ts_web/appstate.js +339 -6
  62. package/dist_ts_web/elements/access/index.d.ts +1 -0
  63. package/dist_ts_web/elements/access/index.js +2 -1
  64. package/dist_ts_web/elements/access/ops-view-apitokens.js +1 -1
  65. package/dist_ts_web/elements/access/ops-view-users.d.ts +11 -0
  66. package/dist_ts_web/elements/access/ops-view-users.js +190 -0
  67. package/dist_ts_web/elements/domains/dns-provider-form.d.ts +58 -0
  68. package/dist_ts_web/elements/domains/dns-provider-form.js +268 -0
  69. package/dist_ts_web/elements/domains/index.d.ts +5 -0
  70. package/dist_ts_web/elements/domains/index.js +6 -0
  71. package/dist_ts_web/elements/{ops-view-certificates.d.ts → domains/ops-view-certificates.d.ts} +1 -1
  72. package/dist_ts_web/elements/{ops-view-certificates.js → domains/ops-view-certificates.js} +5 -5
  73. package/dist_ts_web/elements/domains/ops-view-dns.d.ts +17 -0
  74. package/dist_ts_web/elements/domains/ops-view-dns.js +304 -0
  75. package/dist_ts_web/elements/domains/ops-view-domains.d.ts +18 -0
  76. package/dist_ts_web/elements/domains/ops-view-domains.js +361 -0
  77. package/dist_ts_web/elements/domains/ops-view-providers.d.ts +21 -0
  78. package/dist_ts_web/elements/domains/ops-view-providers.js +316 -0
  79. package/dist_ts_web/elements/email/ops-view-email-security.js +1 -1
  80. package/dist_ts_web/elements/email/ops-view-emails.js +1 -1
  81. package/dist_ts_web/elements/index.d.ts +1 -1
  82. package/dist_ts_web/elements/index.js +2 -2
  83. package/dist_ts_web/elements/network/ops-view-network-activity.js +6 -2
  84. package/dist_ts_web/elements/network/ops-view-networktargets.js +1 -1
  85. package/dist_ts_web/elements/network/ops-view-remoteingress.js +1 -1
  86. package/dist_ts_web/elements/network/ops-view-routes.js +1 -1
  87. package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +1 -1
  88. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +1 -1
  89. package/dist_ts_web/elements/network/ops-view-vpn.js +1 -1
  90. package/dist_ts_web/elements/ops-dashboard.js +16 -5
  91. package/dist_ts_web/elements/ops-view-logs.js +1 -1
  92. package/dist_ts_web/elements/overview/ops-view-config.js +3 -3
  93. package/dist_ts_web/elements/overview/ops-view-overview.js +1 -1
  94. package/dist_ts_web/elements/security/ops-view-security-authentication.js +1 -1
  95. package/dist_ts_web/elements/security/ops-view-security-blocked.js +1 -1
  96. package/dist_ts_web/elements/security/ops-view-security-overview.js +1 -1
  97. package/dist_ts_web/router.d.ts +1 -1
  98. package/dist_ts_web/router.js +5 -3
  99. package/package.json +2 -2
  100. package/ts/00_commitinfo_data.ts +1 -1
  101. package/ts/classes.dcrouter.ts +46 -17
  102. package/ts/db/documents/classes.dns-provider.doc.ts +63 -0
  103. package/ts/db/documents/classes.dns-record.doc.ts +62 -0
  104. package/ts/db/documents/classes.domain.doc.ts +66 -0
  105. package/ts/db/documents/index.ts +5 -0
  106. package/ts/dns/index.ts +2 -0
  107. package/ts/dns/manager.dns.ts +869 -0
  108. package/ts/dns/providers/cloudflare.provider.ts +131 -0
  109. package/ts/dns/providers/factory.ts +48 -0
  110. package/ts/dns/providers/index.ts +3 -0
  111. package/ts/dns/providers/interfaces.ts +67 -0
  112. package/ts/opsserver/classes.opsserver.ts +8 -0
  113. package/ts/opsserver/handlers/admin.handler.ts +12 -0
  114. package/ts/opsserver/handlers/config.handler.ts +10 -1
  115. package/ts/opsserver/handlers/dns-provider.handler.ts +159 -0
  116. package/ts/opsserver/handlers/dns-record.handler.ts +127 -0
  117. package/ts/opsserver/handlers/domain.handler.ts +161 -0
  118. package/ts/opsserver/handlers/index.ts +5 -1
  119. package/ts/opsserver/handlers/users.handler.ts +30 -0
  120. package/ts_web/00_commitinfo_data.ts +1 -1
  121. package/ts_web/appstate.ts +460 -5
  122. package/ts_web/elements/access/index.ts +1 -0
  123. package/ts_web/elements/access/ops-view-apitokens.ts +1 -1
  124. package/ts_web/elements/access/ops-view-users.ts +140 -0
  125. package/ts_web/elements/domains/dns-provider-form.ts +216 -0
  126. package/ts_web/elements/domains/index.ts +5 -0
  127. package/ts_web/elements/{ops-view-certificates.ts → domains/ops-view-certificates.ts} +4 -4
  128. package/ts_web/elements/domains/ops-view-dns.ts +273 -0
  129. package/ts_web/elements/domains/ops-view-domains.ts +335 -0
  130. package/ts_web/elements/domains/ops-view-providers.ts +284 -0
  131. package/ts_web/elements/email/ops-view-email-security.ts +1 -1
  132. package/ts_web/elements/email/ops-view-emails.ts +1 -1
  133. package/ts_web/elements/index.ts +1 -1
  134. package/ts_web/elements/network/ops-view-network-activity.ts +5 -1
  135. package/ts_web/elements/network/ops-view-networktargets.ts +1 -1
  136. package/ts_web/elements/network/ops-view-remoteingress.ts +1 -1
  137. package/ts_web/elements/network/ops-view-routes.ts +1 -1
  138. package/ts_web/elements/network/ops-view-sourceprofiles.ts +1 -1
  139. package/ts_web/elements/network/ops-view-targetprofiles.ts +1 -1
  140. package/ts_web/elements/network/ops-view-vpn.ts +1 -1
  141. package/ts_web/elements/ops-dashboard.ts +16 -4
  142. package/ts_web/elements/ops-view-logs.ts +1 -1
  143. package/ts_web/elements/overview/ops-view-config.ts +2 -2
  144. package/ts_web/elements/overview/ops-view-overview.ts +1 -1
  145. package/ts_web/elements/security/ops-view-security-authentication.ts +1 -1
  146. package/ts_web/elements/security/ops-view-security-blocked.ts +1 -1
  147. package/ts_web/elements/security/ops-view-security-overview.ts +1 -1
  148. package/ts_web/router.ts +4 -2
@@ -117,7 +117,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
117
117
  // Determine initial view from URL path
118
118
  const getInitialView = (): string => {
119
119
  const path = typeof window !== 'undefined' ? window.location.pathname : '/';
120
- const validViews = ['overview', 'network', 'email', 'logs', 'access', 'security', 'certificates'];
120
+ const validViews = ['overview', 'network', 'email', 'logs', 'access', 'security', 'domains'];
121
121
  const segments = path.split('/').filter(Boolean);
122
122
  const view = segments[0];
123
123
  return validViews.includes(view) ? view : 'overview';
@@ -251,6 +251,34 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
251
251
  'soft'
252
252
  );
253
253
 
254
+ // ============================================================================
255
+ // Users State (read-only list of OpsServer user accounts)
256
+ // ============================================================================
257
+
258
+ export interface IUser {
259
+ id: string;
260
+ username: string;
261
+ role: string;
262
+ }
263
+
264
+ export interface IUsersState {
265
+ users: IUser[];
266
+ isLoading: boolean;
267
+ error: string | null;
268
+ lastUpdated: number;
269
+ }
270
+
271
+ export const usersStatePart = await appState.getStatePart<IUsersState>(
272
+ 'users',
273
+ {
274
+ users: [],
275
+ isLoading: false,
276
+ error: null,
277
+ lastUpdated: 0,
278
+ },
279
+ 'soft',
280
+ );
281
+
254
282
  // Actions for state management
255
283
  interface IActionContext {
256
284
  identity: interfaces.data.IIdentity | null;
@@ -437,8 +465,9 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
437
465
  }, 100);
438
466
  }
439
467
 
440
- // If switching to certificates view, ensure we fetch certificate data
441
- if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
468
+ // If switching to the Domains group, ensure we fetch certificate data
469
+ // (Certificates is a subview of Domains).
470
+ if (viewName === 'domains' && currentState.activeView !== 'domains') {
442
471
  setTimeout(() => {
443
472
  certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
444
473
  }, 100);
@@ -1527,6 +1556,403 @@ export const deleteTargetAction = profilesTargetsStatePart.createAction<{
1527
1556
  }
1528
1557
  });
1529
1558
 
1559
+ // ============================================================================
1560
+ // Domains State (DNS providers + domains + records)
1561
+ // ============================================================================
1562
+
1563
+ export interface IDomainsState {
1564
+ providers: interfaces.data.IDnsProviderPublic[];
1565
+ domains: interfaces.data.IDomain[];
1566
+ records: interfaces.data.IDnsRecord[];
1567
+ /** id of the currently-selected domain in the DNS records subview. */
1568
+ selectedDomainId: string | null;
1569
+ isLoading: boolean;
1570
+ error: string | null;
1571
+ lastUpdated: number;
1572
+ }
1573
+
1574
+ export const domainsStatePart = await appState.getStatePart<IDomainsState>(
1575
+ 'domains',
1576
+ {
1577
+ providers: [],
1578
+ domains: [],
1579
+ records: [],
1580
+ selectedDomainId: null,
1581
+ isLoading: false,
1582
+ error: null,
1583
+ lastUpdated: 0,
1584
+ },
1585
+ 'soft',
1586
+ );
1587
+
1588
+ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
1589
+ async (statePartArg): Promise<IDomainsState> => {
1590
+ const context = getActionContext();
1591
+ const currentState = statePartArg.getState()!;
1592
+ if (!context.identity) return currentState;
1593
+
1594
+ try {
1595
+ const providersRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
1596
+ interfaces.requests.IReq_GetDnsProviders
1597
+ >('/typedrequest', 'getDnsProviders');
1598
+ const domainsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
1599
+ interfaces.requests.IReq_GetDomains
1600
+ >('/typedrequest', 'getDomains');
1601
+
1602
+ const [providersResponse, domainsResponse] = await Promise.all([
1603
+ providersRequest.fire({ identity: context.identity }),
1604
+ domainsRequest.fire({ identity: context.identity }),
1605
+ ]);
1606
+
1607
+ return {
1608
+ ...currentState,
1609
+ providers: providersResponse.providers,
1610
+ domains: domainsResponse.domains,
1611
+ isLoading: false,
1612
+ error: null,
1613
+ lastUpdated: Date.now(),
1614
+ };
1615
+ } catch (error: unknown) {
1616
+ return {
1617
+ ...currentState,
1618
+ isLoading: false,
1619
+ error: error instanceof Error ? error.message : 'Failed to fetch domains/providers',
1620
+ };
1621
+ }
1622
+ },
1623
+ );
1624
+
1625
+ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ domainId: string }>(
1626
+ async (statePartArg, dataArg): Promise<IDomainsState> => {
1627
+ const context = getActionContext();
1628
+ const currentState = statePartArg.getState()!;
1629
+ if (!context.identity) return currentState;
1630
+
1631
+ try {
1632
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1633
+ interfaces.requests.IReq_GetDnsRecords
1634
+ >('/typedrequest', 'getDnsRecords');
1635
+ const response = await request.fire({
1636
+ identity: context.identity,
1637
+ domainId: dataArg.domainId,
1638
+ });
1639
+ return {
1640
+ ...currentState,
1641
+ records: response.records,
1642
+ selectedDomainId: dataArg.domainId,
1643
+ error: null,
1644
+ };
1645
+ } catch (error: unknown) {
1646
+ return {
1647
+ ...currentState,
1648
+ error: error instanceof Error ? error.message : 'Failed to fetch DNS records',
1649
+ };
1650
+ }
1651
+ },
1652
+ );
1653
+
1654
+ export const createDnsProviderAction = domainsStatePart.createAction<{
1655
+ name: string;
1656
+ type: interfaces.data.TDnsProviderType;
1657
+ credentials: interfaces.data.TDnsProviderCredentials;
1658
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1659
+ const context = getActionContext();
1660
+ try {
1661
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1662
+ interfaces.requests.IReq_CreateDnsProvider
1663
+ >('/typedrequest', 'createDnsProvider');
1664
+ const response = await request.fire({
1665
+ identity: context.identity!,
1666
+ name: dataArg.name,
1667
+ type: dataArg.type,
1668
+ credentials: dataArg.credentials,
1669
+ });
1670
+ if (!response.success) {
1671
+ return {
1672
+ ...statePartArg.getState()!,
1673
+ error: response.message || 'Failed to create provider',
1674
+ };
1675
+ }
1676
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1677
+ } catch (error: unknown) {
1678
+ return {
1679
+ ...statePartArg.getState()!,
1680
+ error: error instanceof Error ? error.message : 'Failed to create provider',
1681
+ };
1682
+ }
1683
+ });
1684
+
1685
+ export const updateDnsProviderAction = domainsStatePart.createAction<{
1686
+ id: string;
1687
+ name?: string;
1688
+ credentials?: interfaces.data.TDnsProviderCredentials;
1689
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1690
+ const context = getActionContext();
1691
+ try {
1692
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1693
+ interfaces.requests.IReq_UpdateDnsProvider
1694
+ >('/typedrequest', 'updateDnsProvider');
1695
+ const response = await request.fire({
1696
+ identity: context.identity!,
1697
+ id: dataArg.id,
1698
+ name: dataArg.name,
1699
+ credentials: dataArg.credentials,
1700
+ });
1701
+ if (!response.success) {
1702
+ return {
1703
+ ...statePartArg.getState()!,
1704
+ error: response.message || 'Failed to update provider',
1705
+ };
1706
+ }
1707
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1708
+ } catch (error: unknown) {
1709
+ return {
1710
+ ...statePartArg.getState()!,
1711
+ error: error instanceof Error ? error.message : 'Failed to update provider',
1712
+ };
1713
+ }
1714
+ });
1715
+
1716
+ export const deleteDnsProviderAction = domainsStatePart.createAction<{ id: string; force?: boolean }>(
1717
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1718
+ const context = getActionContext();
1719
+ try {
1720
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1721
+ interfaces.requests.IReq_DeleteDnsProvider
1722
+ >('/typedrequest', 'deleteDnsProvider');
1723
+ const response = await request.fire({
1724
+ identity: context.identity!,
1725
+ id: dataArg.id,
1726
+ force: dataArg.force,
1727
+ });
1728
+ if (!response.success) {
1729
+ return {
1730
+ ...statePartArg.getState()!,
1731
+ error: response.message || 'Failed to delete provider',
1732
+ };
1733
+ }
1734
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1735
+ } catch (error: unknown) {
1736
+ return {
1737
+ ...statePartArg.getState()!,
1738
+ error: error instanceof Error ? error.message : 'Failed to delete provider',
1739
+ };
1740
+ }
1741
+ },
1742
+ );
1743
+
1744
+ export const testDnsProviderAction = domainsStatePart.createAction<{ id: string }>(
1745
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1746
+ const context = getActionContext();
1747
+ try {
1748
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1749
+ interfaces.requests.IReq_TestDnsProvider
1750
+ >('/typedrequest', 'testDnsProvider');
1751
+ await request.fire({ identity: context.identity!, id: dataArg.id });
1752
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1753
+ } catch (error: unknown) {
1754
+ return {
1755
+ ...statePartArg.getState()!,
1756
+ error: error instanceof Error ? error.message : 'Failed to test provider',
1757
+ };
1758
+ }
1759
+ },
1760
+ );
1761
+
1762
+ /** One-shot fetch for the import-domain modal. Does NOT modify state. */
1763
+ export async function fetchProviderDomains(
1764
+ providerId: string,
1765
+ ): Promise<{ success: boolean; domains?: interfaces.data.IProviderDomainListing[]; message?: string }> {
1766
+ const context = getActionContext();
1767
+ if (!context.identity) return { success: false, message: 'Not authenticated' };
1768
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1769
+ interfaces.requests.IReq_ListProviderDomains
1770
+ >('/typedrequest', 'listProviderDomains');
1771
+ return await request.fire({ identity: context.identity, providerId });
1772
+ }
1773
+
1774
+ export const createManualDomainAction = domainsStatePart.createAction<{
1775
+ name: string;
1776
+ description?: string;
1777
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1778
+ const context = getActionContext();
1779
+ try {
1780
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1781
+ interfaces.requests.IReq_CreateDomain
1782
+ >('/typedrequest', 'createDomain');
1783
+ const response = await request.fire({
1784
+ identity: context.identity!,
1785
+ name: dataArg.name,
1786
+ description: dataArg.description,
1787
+ });
1788
+ if (!response.success) {
1789
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to create domain' };
1790
+ }
1791
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1792
+ } catch (error: unknown) {
1793
+ return {
1794
+ ...statePartArg.getState()!,
1795
+ error: error instanceof Error ? error.message : 'Failed to create domain',
1796
+ };
1797
+ }
1798
+ });
1799
+
1800
+ export const importDomainsFromProviderAction = domainsStatePart.createAction<{
1801
+ providerId: string;
1802
+ domainNames: string[];
1803
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1804
+ const context = getActionContext();
1805
+ try {
1806
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1807
+ interfaces.requests.IReq_ImportDomain
1808
+ >('/typedrequest', 'importDomain');
1809
+ const response = await request.fire({
1810
+ identity: context.identity!,
1811
+ providerId: dataArg.providerId,
1812
+ domainNames: dataArg.domainNames,
1813
+ });
1814
+ if (!response.success) {
1815
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to import domains' };
1816
+ }
1817
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1818
+ } catch (error: unknown) {
1819
+ return {
1820
+ ...statePartArg.getState()!,
1821
+ error: error instanceof Error ? error.message : 'Failed to import domains',
1822
+ };
1823
+ }
1824
+ });
1825
+
1826
+ export const deleteDomainAction = domainsStatePart.createAction<{ id: string }>(
1827
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1828
+ const context = getActionContext();
1829
+ try {
1830
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1831
+ interfaces.requests.IReq_DeleteDomain
1832
+ >('/typedrequest', 'deleteDomain');
1833
+ const response = await request.fire({ identity: context.identity!, id: dataArg.id });
1834
+ if (!response.success) {
1835
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to delete domain' };
1836
+ }
1837
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1838
+ } catch (error: unknown) {
1839
+ return {
1840
+ ...statePartArg.getState()!,
1841
+ error: error instanceof Error ? error.message : 'Failed to delete domain',
1842
+ };
1843
+ }
1844
+ },
1845
+ );
1846
+
1847
+ export const syncDomainAction = domainsStatePart.createAction<{ id: string }>(
1848
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1849
+ const context = getActionContext();
1850
+ try {
1851
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1852
+ interfaces.requests.IReq_SyncDomain
1853
+ >('/typedrequest', 'syncDomain');
1854
+ const response = await request.fire({ identity: context.identity!, id: dataArg.id });
1855
+ if (!response.success) {
1856
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to sync domain' };
1857
+ }
1858
+ return await actionContext!.dispatch(fetchDomainsAndProvidersAction, null);
1859
+ } catch (error: unknown) {
1860
+ return {
1861
+ ...statePartArg.getState()!,
1862
+ error: error instanceof Error ? error.message : 'Failed to sync domain',
1863
+ };
1864
+ }
1865
+ },
1866
+ );
1867
+
1868
+ export const createDnsRecordAction = domainsStatePart.createAction<{
1869
+ domainId: string;
1870
+ name: string;
1871
+ type: interfaces.data.TDnsRecordType;
1872
+ value: string;
1873
+ ttl?: number;
1874
+ proxied?: boolean;
1875
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1876
+ const context = getActionContext();
1877
+ try {
1878
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1879
+ interfaces.requests.IReq_CreateDnsRecord
1880
+ >('/typedrequest', 'createDnsRecord');
1881
+ const response = await request.fire({
1882
+ identity: context.identity!,
1883
+ domainId: dataArg.domainId,
1884
+ name: dataArg.name,
1885
+ type: dataArg.type,
1886
+ value: dataArg.value,
1887
+ ttl: dataArg.ttl,
1888
+ proxied: dataArg.proxied,
1889
+ });
1890
+ if (!response.success) {
1891
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to create record' };
1892
+ }
1893
+ return await actionContext!.dispatch(fetchDnsRecordsForDomainAction, { domainId: dataArg.domainId });
1894
+ } catch (error: unknown) {
1895
+ return {
1896
+ ...statePartArg.getState()!,
1897
+ error: error instanceof Error ? error.message : 'Failed to create record',
1898
+ };
1899
+ }
1900
+ });
1901
+
1902
+ export const updateDnsRecordAction = domainsStatePart.createAction<{
1903
+ id: string;
1904
+ domainId: string;
1905
+ name?: string;
1906
+ value?: string;
1907
+ ttl?: number;
1908
+ proxied?: boolean;
1909
+ }>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1910
+ const context = getActionContext();
1911
+ try {
1912
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1913
+ interfaces.requests.IReq_UpdateDnsRecord
1914
+ >('/typedrequest', 'updateDnsRecord');
1915
+ const response = await request.fire({
1916
+ identity: context.identity!,
1917
+ id: dataArg.id,
1918
+ name: dataArg.name,
1919
+ value: dataArg.value,
1920
+ ttl: dataArg.ttl,
1921
+ proxied: dataArg.proxied,
1922
+ });
1923
+ if (!response.success) {
1924
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to update record' };
1925
+ }
1926
+ return await actionContext!.dispatch(fetchDnsRecordsForDomainAction, { domainId: dataArg.domainId });
1927
+ } catch (error: unknown) {
1928
+ return {
1929
+ ...statePartArg.getState()!,
1930
+ error: error instanceof Error ? error.message : 'Failed to update record',
1931
+ };
1932
+ }
1933
+ });
1934
+
1935
+ export const deleteDnsRecordAction = domainsStatePart.createAction<{ id: string; domainId: string }>(
1936
+ async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
1937
+ const context = getActionContext();
1938
+ try {
1939
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1940
+ interfaces.requests.IReq_DeleteDnsRecord
1941
+ >('/typedrequest', 'deleteDnsRecord');
1942
+ const response = await request.fire({ identity: context.identity!, id: dataArg.id });
1943
+ if (!response.success) {
1944
+ return { ...statePartArg.getState()!, error: response.message || 'Failed to delete record' };
1945
+ }
1946
+ return await actionContext!.dispatch(fetchDnsRecordsForDomainAction, { domainId: dataArg.domainId });
1947
+ } catch (error: unknown) {
1948
+ return {
1949
+ ...statePartArg.getState()!,
1950
+ error: error instanceof Error ? error.message : 'Failed to delete record',
1951
+ };
1952
+ }
1953
+ },
1954
+ );
1955
+
1530
1956
  // ============================================================================
1531
1957
  // Route Management Actions
1532
1958
  // ============================================================================
@@ -1756,6 +2182,35 @@ export const fetchApiTokensAction = routeManagementStatePart.createAction(async
1756
2182
  }
1757
2183
  });
1758
2184
 
2185
+ // Users (read-only list)
2186
+ export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
2187
+ const context = getActionContext();
2188
+ const currentState = statePartArg.getState()!;
2189
+ if (!context.identity) return currentState;
2190
+
2191
+ try {
2192
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2193
+ interfaces.requests.IReq_ListUsers
2194
+ >('/typedrequest', 'listUsers');
2195
+
2196
+ const response = await request.fire({
2197
+ identity: context.identity,
2198
+ });
2199
+
2200
+ return {
2201
+ ...currentState,
2202
+ users: response.users,
2203
+ error: null,
2204
+ lastUpdated: Date.now(),
2205
+ };
2206
+ } catch (error) {
2207
+ return {
2208
+ ...currentState,
2209
+ error: error instanceof Error ? error.message : 'Failed to fetch users',
2210
+ };
2211
+ }
2212
+ });
2213
+
1759
2214
  export async function createApiToken(name: string, scopes: interfaces.data.TApiTokenScope[], expiresInDays?: number | null) {
1760
2215
  const context = getActionContext();
1761
2216
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -2019,8 +2474,8 @@ async function dispatchCombinedRefreshActionInner() {
2019
2474
  }
2020
2475
  }
2021
2476
 
2022
- // Refresh certificate data if on certificates view
2023
- if (currentView === 'certificates') {
2477
+ // Refresh certificate data if on Domains > Certificates subview
2478
+ if (currentView === 'domains' && currentSubview === 'certificates') {
2024
2479
  try {
2025
2480
  await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
2026
2481
  } catch (error) {
@@ -1 +1,2 @@
1
1
  export * from './ops-view-apitokens.js';
2
+ export * from './ops-view-users.js';
@@ -100,7 +100,7 @@ export class OpsViewApiTokens extends DeesElement {
100
100
  const { apiTokens } = this.routeState;
101
101
 
102
102
  return html`
103
- <dees-heading level="2">API Tokens</dees-heading>
103
+ <dees-heading level="3">API Tokens</dees-heading>
104
104
 
105
105
  <div class="apiTokensContainer">
106
106
  <dees-table
@@ -0,0 +1,140 @@
1
+ import * as appstate from '../../appstate.js';
2
+ import { viewHostCss } from '../shared/css.js';
3
+
4
+ import {
5
+ DeesElement,
6
+ css,
7
+ cssManager,
8
+ customElement,
9
+ html,
10
+ state,
11
+ type TemplateResult,
12
+ } from '@design.estate/dees-element';
13
+
14
+ @customElement('ops-view-users')
15
+ export class OpsViewUsers extends DeesElement {
16
+ @state() accessor usersState: appstate.IUsersState = {
17
+ users: [],
18
+ isLoading: false,
19
+ error: null,
20
+ lastUpdated: 0,
21
+ };
22
+
23
+ @state() accessor loginState: appstate.ILoginState = {
24
+ identity: null,
25
+ isLoggedIn: false,
26
+ };
27
+
28
+ constructor() {
29
+ super();
30
+ const usersSub = appstate.usersStatePart
31
+ .select((s) => s)
32
+ .subscribe((usersState) => {
33
+ this.usersState = usersState;
34
+ });
35
+ this.rxSubscriptions.push(usersSub);
36
+
37
+ const loginSub = appstate.loginStatePart
38
+ .select((s) => s)
39
+ .subscribe((loginState) => {
40
+ this.loginState = loginState;
41
+ // Re-fetch users when user logs in (fixes race condition where
42
+ // the view is created before authentication completes)
43
+ if (loginState.isLoggedIn) {
44
+ appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
45
+ }
46
+ });
47
+ this.rxSubscriptions.push(loginSub);
48
+ }
49
+
50
+ public static styles = [
51
+ cssManager.defaultStyles,
52
+ viewHostCss,
53
+ css`
54
+ .usersContainer {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 24px;
58
+ }
59
+
60
+ .roleBadge {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ padding: 3px 10px;
64
+ border-radius: 12px;
65
+ font-size: 12px;
66
+ font-weight: 600;
67
+ letter-spacing: 0.02em;
68
+ text-transform: uppercase;
69
+ }
70
+
71
+ .roleBadge.admin {
72
+ background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
73
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
74
+ }
75
+
76
+ .roleBadge.user {
77
+ background: ${cssManager.bdTheme('#e0f2fe', '#0c4a6e')};
78
+ color: ${cssManager.bdTheme('#075985', '#7dd3fc')};
79
+ }
80
+
81
+ .sessionBadge {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ padding: 3px 10px;
85
+ border-radius: 12px;
86
+ font-size: 12px;
87
+ font-weight: 600;
88
+ letter-spacing: 0.02em;
89
+ text-transform: uppercase;
90
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
91
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
92
+ }
93
+
94
+ .userIdCell {
95
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
96
+ font-size: 11px;
97
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
98
+ }
99
+ `,
100
+ ];
101
+
102
+ public render(): TemplateResult {
103
+ const { users } = this.usersState;
104
+ const currentUserId = this.loginState.identity?.userId;
105
+
106
+ return html`
107
+ <dees-heading level="3">Users</dees-heading>
108
+
109
+ <div class="usersContainer">
110
+ <dees-table
111
+ .heading1=${'Users'}
112
+ .heading2=${'OpsServer user accounts'}
113
+ .data=${users}
114
+ .dataName=${'user'}
115
+ .searchable=${true}
116
+ .showColumnFilters=${true}
117
+ .displayFunction=${(user: appstate.IUser) => ({
118
+ ID: html`<span class="userIdCell">${user.id}</span>`,
119
+ Username: user.username,
120
+ Role: this.renderRoleBadge(user.role),
121
+ Session: user.id === currentUserId
122
+ ? html`<span class="sessionBadge">current</span>`
123
+ : '',
124
+ })}
125
+ ></dees-table>
126
+ </div>
127
+ `;
128
+ }
129
+
130
+ private renderRoleBadge(role: string): TemplateResult {
131
+ const cls = role === 'admin' ? 'admin' : 'user';
132
+ return html`<span class="roleBadge ${cls}">${role}</span>`;
133
+ }
134
+
135
+ async firstUpdated() {
136
+ if (this.loginState.isLoggedIn) {
137
+ await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
138
+ }
139
+ }
140
+ }