@serve.zone/dcrouter 8.0.0 → 9.1.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 (88) hide show
  1. package/dist_serve/bundle.js +2420 -1227
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -0
  4. package/dist_ts/classes.dcrouter.js +27 -1
  5. package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
  6. package/dist_ts/config/classes.api-token-manager.js +134 -0
  7. package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
  8. package/dist_ts/config/classes.route-config-manager.js +231 -0
  9. package/dist_ts/config/index.d.ts +2 -0
  10. package/dist_ts/config/index.js +3 -1
  11. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  12. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  13. package/dist_ts/opsserver/handlers/{config.handler.d.ts → api-token.handler.d.ts} +5 -2
  14. package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
  15. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  16. package/dist_ts/opsserver/handlers/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/route-management.handler.d.ts +13 -0
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/route-management.d.ts +68 -0
  22. package/dist_ts_interfaces/data/route-management.js +2 -0
  23. package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
  24. package/dist_ts_interfaces/requests/api-tokens.js +2 -0
  25. package/dist_ts_interfaces/requests/config.d.ts +77 -1
  26. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  27. package/dist_ts_interfaces/requests/index.js +3 -1
  28. package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
  29. package/dist_ts_interfaces/requests/route-management.js +2 -0
  30. package/dist_ts_web/00_commitinfo_data.js +1 -1
  31. package/dist_ts_web/appstate.d.ts +37 -1
  32. package/dist_ts_web/appstate.js +220 -2
  33. package/dist_ts_web/elements/index.d.ts +2 -0
  34. package/dist_ts_web/elements/index.js +3 -1
  35. package/dist_ts_web/elements/ops-dashboard.js +23 -3
  36. package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
  37. package/dist_ts_web/elements/ops-view-apitokens.js +310 -0
  38. package/dist_ts_web/elements/ops-view-config.d.ts +10 -8
  39. package/dist_ts_web/elements/ops-view-config.js +215 -297
  40. package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
  41. package/dist_ts_web/elements/ops-view-routes.js +404 -0
  42. package/dist_ts_web/router.d.ts +1 -1
  43. package/dist_ts_web/router.js +2 -2
  44. package/package.json +2 -2
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dcrouter.ts +37 -1
  47. package/ts/config/classes.api-token-manager.ts +155 -0
  48. package/ts/config/classes.route-config-manager.ts +271 -0
  49. package/ts/config/index.ts +3 -1
  50. package/ts/opsserver/classes.opsserver.ts +4 -0
  51. package/ts/opsserver/handlers/api-token.handler.ts +96 -0
  52. package/ts/opsserver/handlers/config.handler.ts +154 -72
  53. package/ts/opsserver/handlers/index.ts +3 -1
  54. package/ts/opsserver/handlers/route-management.handler.ts +163 -0
  55. package/ts_web/00_commitinfo_data.ts +1 -1
  56. package/ts_web/appstate.ts +309 -2
  57. package/ts_web/elements/index.ts +2 -0
  58. package/ts_web/elements/ops-dashboard.ts +22 -2
  59. package/ts_web/elements/ops-view-apitokens.ts +285 -0
  60. package/ts_web/elements/ops-view-config.ts +237 -299
  61. package/ts_web/elements/ops-view-routes.ts +389 -0
  62. package/ts_web/router.ts +1 -1
  63. package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
  64. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  65. package/dist_ts/cache/classes.cached.document.d.ts +0 -76
  66. package/dist_ts/cache/classes.cached.document.js +0 -100
  67. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  68. package/dist_ts/cache/classes.cachedb.js +0 -126
  69. package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
  70. package/dist_ts/cache/documents/classes.cached.email.js +0 -337
  71. package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
  72. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
  73. package/dist_ts/cache/documents/index.d.ts +0 -2
  74. package/dist_ts/cache/documents/index.js +0 -3
  75. package/dist_ts/cache/index.d.ts +0 -4
  76. package/dist_ts/cache/index.js +0 -7
  77. package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
  78. package/dist_ts/monitoring/classes.metricscache.js +0 -63
  79. package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
  80. package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
  81. package/dist_ts/opsserver/handlers/config.handler.js +0 -67
  82. package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
  83. package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
  84. package/dist_ts/security/classes.securitylogger.js +0 -235
  85. package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
  86. package/dist_ts/storage/classes.storagemanager.js +0 -344
  87. package/dist_ts/storage/index.d.ts +0 -1
  88. package/dist_ts/storage/index.js +0 -3
@@ -21,7 +21,7 @@ export interface IStatsState {
21
21
  }
22
22
 
23
23
  export interface IConfigState {
24
- config: any | null;
24
+ config: interfaces.requests.IConfigData | null;
25
25
  isLoading: boolean;
26
26
  error: string | null;
27
27
  }
@@ -109,7 +109,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
109
109
  // Determine initial view from URL path
110
110
  const getInitialView = (): string => {
111
111
  const path = typeof window !== 'undefined' ? window.location.pathname : '/';
112
- const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
112
+ const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'];
113
113
  const segments = path.split('/').filter(Boolean);
114
114
  const view = segments[0];
115
115
  return validViews.includes(view) ? view : 'overview';
@@ -206,6 +206,32 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
206
206
  'soft'
207
207
  );
208
208
 
209
+ // ============================================================================
210
+ // Route Management State
211
+ // ============================================================================
212
+
213
+ export interface IRouteManagementState {
214
+ mergedRoutes: interfaces.data.IMergedRoute[];
215
+ warnings: interfaces.data.IRouteWarning[];
216
+ apiTokens: interfaces.data.IApiTokenInfo[];
217
+ isLoading: boolean;
218
+ error: string | null;
219
+ lastUpdated: number;
220
+ }
221
+
222
+ export const routeManagementStatePart = await appState.getStatePart<IRouteManagementState>(
223
+ 'routeManagement',
224
+ {
225
+ mergedRoutes: [],
226
+ warnings: [],
227
+ apiTokens: [],
228
+ isLoading: false,
229
+ error: null,
230
+ lastUpdated: 0,
231
+ },
232
+ 'soft'
233
+ );
234
+
209
235
  // Actions for state management
210
236
  interface IActionContext {
211
237
  identity: interfaces.data.IIdentity | null;
@@ -392,6 +418,20 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
392
418
  }, 100);
393
419
  }
394
420
 
421
+ // If switching to routes view, ensure we fetch route data
422
+ if (viewName === 'routes' && currentState.activeView !== 'routes') {
423
+ setTimeout(() => {
424
+ routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
425
+ }, 100);
426
+ }
427
+
428
+ // If switching to apitokens view, ensure we fetch token data
429
+ if (viewName === 'apitokens' && currentState.activeView !== 'apitokens') {
430
+ setTimeout(() => {
431
+ routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
432
+ }, 100);
433
+ }
434
+
395
435
  // If switching to remoteingress view, ensure we fetch edge data
396
436
  if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
397
437
  setTimeout(() => {
@@ -862,6 +902,273 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
862
902
  }
863
903
  });
864
904
 
905
+ // ============================================================================
906
+ // Route Management Actions
907
+ // ============================================================================
908
+
909
+ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
910
+ const context = getActionContext();
911
+ const currentState = statePartArg.getState();
912
+
913
+ try {
914
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
915
+ interfaces.requests.IReq_GetMergedRoutes
916
+ >('/typedrequest', 'getMergedRoutes');
917
+
918
+ const response = await request.fire({
919
+ identity: context.identity,
920
+ });
921
+
922
+ return {
923
+ ...currentState,
924
+ mergedRoutes: response.routes,
925
+ warnings: response.warnings,
926
+ isLoading: false,
927
+ error: null,
928
+ lastUpdated: Date.now(),
929
+ };
930
+ } catch (error) {
931
+ return {
932
+ ...currentState,
933
+ isLoading: false,
934
+ error: error instanceof Error ? error.message : 'Failed to fetch routes',
935
+ };
936
+ }
937
+ });
938
+
939
+ export const createRouteAction = routeManagementStatePart.createAction<{
940
+ route: any;
941
+ enabled?: boolean;
942
+ }>(async (statePartArg, dataArg) => {
943
+ const context = getActionContext();
944
+ const currentState = statePartArg.getState();
945
+
946
+ try {
947
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
948
+ interfaces.requests.IReq_CreateRoute
949
+ >('/typedrequest', 'createRoute');
950
+
951
+ await request.fire({
952
+ identity: context.identity,
953
+ route: dataArg.route,
954
+ enabled: dataArg.enabled,
955
+ });
956
+
957
+ await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
958
+ return statePartArg.getState();
959
+ } catch (error) {
960
+ return {
961
+ ...currentState,
962
+ error: error instanceof Error ? error.message : 'Failed to create route',
963
+ };
964
+ }
965
+ });
966
+
967
+ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
968
+ async (statePartArg, routeId) => {
969
+ const context = getActionContext();
970
+ const currentState = statePartArg.getState();
971
+
972
+ try {
973
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
974
+ interfaces.requests.IReq_DeleteRoute
975
+ >('/typedrequest', 'deleteRoute');
976
+
977
+ await request.fire({
978
+ identity: context.identity,
979
+ id: routeId,
980
+ });
981
+
982
+ await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
983
+ return statePartArg.getState();
984
+ } catch (error) {
985
+ return {
986
+ ...currentState,
987
+ error: error instanceof Error ? error.message : 'Failed to delete route',
988
+ };
989
+ }
990
+ }
991
+ );
992
+
993
+ export const toggleRouteAction = routeManagementStatePart.createAction<{
994
+ id: string;
995
+ enabled: boolean;
996
+ }>(async (statePartArg, dataArg) => {
997
+ const context = getActionContext();
998
+ const currentState = statePartArg.getState();
999
+
1000
+ try {
1001
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1002
+ interfaces.requests.IReq_ToggleRoute
1003
+ >('/typedrequest', 'toggleRoute');
1004
+
1005
+ await request.fire({
1006
+ identity: context.identity,
1007
+ id: dataArg.id,
1008
+ enabled: dataArg.enabled,
1009
+ });
1010
+
1011
+ await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1012
+ return statePartArg.getState();
1013
+ } catch (error) {
1014
+ return {
1015
+ ...currentState,
1016
+ error: error instanceof Error ? error.message : 'Failed to toggle route',
1017
+ };
1018
+ }
1019
+ });
1020
+
1021
+ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
1022
+ routeName: string;
1023
+ enabled: boolean;
1024
+ }>(async (statePartArg, dataArg) => {
1025
+ const context = getActionContext();
1026
+ const currentState = statePartArg.getState();
1027
+
1028
+ try {
1029
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1030
+ interfaces.requests.IReq_SetRouteOverride
1031
+ >('/typedrequest', 'setRouteOverride');
1032
+
1033
+ await request.fire({
1034
+ identity: context.identity,
1035
+ routeName: dataArg.routeName,
1036
+ enabled: dataArg.enabled,
1037
+ });
1038
+
1039
+ await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1040
+ return statePartArg.getState();
1041
+ } catch (error) {
1042
+ return {
1043
+ ...currentState,
1044
+ error: error instanceof Error ? error.message : 'Failed to set override',
1045
+ };
1046
+ }
1047
+ });
1048
+
1049
+ export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
1050
+ async (statePartArg, routeName) => {
1051
+ const context = getActionContext();
1052
+ const currentState = statePartArg.getState();
1053
+
1054
+ try {
1055
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1056
+ interfaces.requests.IReq_RemoveRouteOverride
1057
+ >('/typedrequest', 'removeRouteOverride');
1058
+
1059
+ await request.fire({
1060
+ identity: context.identity,
1061
+ routeName,
1062
+ });
1063
+
1064
+ await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
1065
+ return statePartArg.getState();
1066
+ } catch (error) {
1067
+ return {
1068
+ ...currentState,
1069
+ error: error instanceof Error ? error.message : 'Failed to remove override',
1070
+ };
1071
+ }
1072
+ }
1073
+ );
1074
+
1075
+ // ============================================================================
1076
+ // API Token Actions
1077
+ // ============================================================================
1078
+
1079
+ export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
1080
+ const context = getActionContext();
1081
+ const currentState = statePartArg.getState();
1082
+
1083
+ try {
1084
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1085
+ interfaces.requests.IReq_ListApiTokens
1086
+ >('/typedrequest', 'listApiTokens');
1087
+
1088
+ const response = await request.fire({
1089
+ identity: context.identity,
1090
+ });
1091
+
1092
+ return {
1093
+ ...currentState,
1094
+ apiTokens: response.tokens,
1095
+ };
1096
+ } catch (error) {
1097
+ return {
1098
+ ...currentState,
1099
+ error: error instanceof Error ? error.message : 'Failed to fetch tokens',
1100
+ };
1101
+ }
1102
+ });
1103
+
1104
+ export async function createApiToken(name: string, scopes: interfaces.data.TApiTokenScope[], expiresInDays?: number | null) {
1105
+ const context = getActionContext();
1106
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1107
+ interfaces.requests.IReq_CreateApiToken
1108
+ >('/typedrequest', 'createApiToken');
1109
+
1110
+ return request.fire({
1111
+ identity: context.identity,
1112
+ name,
1113
+ scopes,
1114
+ expiresInDays,
1115
+ });
1116
+ }
1117
+
1118
+ export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
1119
+ async (statePartArg, tokenId) => {
1120
+ const context = getActionContext();
1121
+ const currentState = statePartArg.getState();
1122
+
1123
+ try {
1124
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1125
+ interfaces.requests.IReq_RevokeApiToken
1126
+ >('/typedrequest', 'revokeApiToken');
1127
+
1128
+ await request.fire({
1129
+ identity: context.identity,
1130
+ id: tokenId,
1131
+ });
1132
+
1133
+ await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
1134
+ return statePartArg.getState();
1135
+ } catch (error) {
1136
+ return {
1137
+ ...currentState,
1138
+ error: error instanceof Error ? error.message : 'Failed to revoke token',
1139
+ };
1140
+ }
1141
+ }
1142
+ );
1143
+
1144
+ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
1145
+ id: string;
1146
+ enabled: boolean;
1147
+ }>(async (statePartArg, dataArg) => {
1148
+ const context = getActionContext();
1149
+ const currentState = statePartArg.getState();
1150
+
1151
+ try {
1152
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1153
+ interfaces.requests.IReq_ToggleApiToken
1154
+ >('/typedrequest', 'toggleApiToken');
1155
+
1156
+ await request.fire({
1157
+ identity: context.identity,
1158
+ id: dataArg.id,
1159
+ enabled: dataArg.enabled,
1160
+ });
1161
+
1162
+ await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
1163
+ return statePartArg.getState();
1164
+ } catch (error) {
1165
+ return {
1166
+ ...currentState,
1167
+ error: error instanceof Error ? error.message : 'Failed to toggle token',
1168
+ };
1169
+ }
1170
+ });
1171
+
865
1172
  // ============================================================================
866
1173
  // TypedSocket Client for Real-time Log Streaming
867
1174
  // ============================================================================
@@ -4,6 +4,8 @@ export * from './ops-view-network.js';
4
4
  export * from './ops-view-emails.js';
5
5
  export * from './ops-view-logs.js';
6
6
  export * from './ops-view-config.js';
7
+ export * from './ops-view-routes.js';
8
+ export * from './ops-view-apitokens.js';
7
9
  export * from './ops-view-security.js';
8
10
  export * from './ops-view-certificates.js';
9
11
  export * from './ops-view-remoteingress.js';
@@ -18,6 +18,8 @@ import { OpsViewNetwork } from './ops-view-network.js';
18
18
  import { OpsViewEmails } from './ops-view-emails.js';
19
19
  import { OpsViewLogs } from './ops-view-logs.js';
20
20
  import { OpsViewConfig } from './ops-view-config.js';
21
+ import { OpsViewRoutes } from './ops-view-routes.js';
22
+ import { OpsViewApiTokens } from './ops-view-apitokens.js';
21
23
  import { OpsViewSecurity } from './ops-view-security.js';
22
24
  import { OpsViewCertificates } from './ops-view-certificates.js';
23
25
  import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
@@ -41,34 +43,52 @@ export class OpsDashboard extends DeesElement {
41
43
  private viewTabs = [
42
44
  {
43
45
  name: 'Overview',
46
+ iconName: 'lucide:layoutDashboard',
44
47
  element: OpsViewOverview,
45
48
  },
49
+ {
50
+ name: 'Configuration',
51
+ iconName: 'lucide:settings',
52
+ element: OpsViewConfig,
53
+ },
46
54
  {
47
55
  name: 'Network',
56
+ iconName: 'lucide:network',
48
57
  element: OpsViewNetwork,
49
58
  },
50
59
  {
51
60
  name: 'Emails',
61
+ iconName: 'lucide:mail',
52
62
  element: OpsViewEmails,
53
63
  },
54
64
  {
55
65
  name: 'Logs',
66
+ iconName: 'lucide:scrollText',
56
67
  element: OpsViewLogs,
57
68
  },
58
69
  {
59
- name: 'Configuration',
60
- element: OpsViewConfig,
70
+ name: 'Routes',
71
+ iconName: 'lucide:route',
72
+ element: OpsViewRoutes,
73
+ },
74
+ {
75
+ name: 'ApiTokens',
76
+ iconName: 'lucide:key',
77
+ element: OpsViewApiTokens,
61
78
  },
62
79
  {
63
80
  name: 'Security',
81
+ iconName: 'lucide:shield',
64
82
  element: OpsViewSecurity,
65
83
  },
66
84
  {
67
85
  name: 'Certificates',
86
+ iconName: 'lucide:badgeCheck',
68
87
  element: OpsViewCertificates,
69
88
  },
70
89
  {
71
90
  name: 'RemoteIngress',
91
+ iconName: 'lucide:globe',
72
92
  element: OpsViewRemoteIngress,
73
93
  },
74
94
  ];
@@ -0,0 +1,285 @@
1
+ import * as appstate from '../appstate.js';
2
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
3
+ import { viewHostCss } from './shared/css.js';
4
+
5
+ import {
6
+ DeesElement,
7
+ css,
8
+ cssManager,
9
+ customElement,
10
+ html,
11
+ state,
12
+ type TemplateResult,
13
+ } from '@design.estate/dees-element';
14
+
15
+ type TApiTokenScope = interfaces.data.TApiTokenScope;
16
+
17
+ @customElement('ops-view-apitokens')
18
+ export class OpsViewApiTokens extends DeesElement {
19
+ @state() accessor routeState: appstate.IRouteManagementState = {
20
+ mergedRoutes: [],
21
+ warnings: [],
22
+ apiTokens: [],
23
+ isLoading: false,
24
+ error: null,
25
+ lastUpdated: 0,
26
+ };
27
+
28
+ constructor() {
29
+ super();
30
+ const sub = appstate.routeManagementStatePart
31
+ .select((s) => s)
32
+ .subscribe((routeState) => {
33
+ this.routeState = routeState;
34
+ });
35
+ this.rxSubscriptions.push(sub);
36
+
37
+ // Re-fetch tokens when user logs in (fixes race condition where
38
+ // the view is created before authentication completes)
39
+ const loginSub = appstate.loginStatePart
40
+ .select((s) => s.isLoggedIn)
41
+ .subscribe((isLoggedIn) => {
42
+ if (isLoggedIn) {
43
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
44
+ }
45
+ });
46
+ this.rxSubscriptions.push(loginSub);
47
+ }
48
+
49
+ public static styles = [
50
+ cssManager.defaultStyles,
51
+ viewHostCss,
52
+ css`
53
+ .apiTokensContainer {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 24px;
57
+ }
58
+
59
+ .scopePill {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ padding: 2px 6px;
63
+ border-radius: 3px;
64
+ font-size: 11px;
65
+ background: ${cssManager.bdTheme('rgba(0, 130, 200, 0.1)', 'rgba(0, 170, 255, 0.1)')};
66
+ color: ${cssManager.bdTheme('#0369a1', '#0af')};
67
+ margin-right: 4px;
68
+ margin-bottom: 2px;
69
+ }
70
+
71
+ .statusBadge {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ padding: 3px 10px;
75
+ border-radius: 12px;
76
+ font-size: 12px;
77
+ font-weight: 600;
78
+ letter-spacing: 0.02em;
79
+ text-transform: uppercase;
80
+ }
81
+
82
+ .statusBadge.active {
83
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
84
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
85
+ }
86
+
87
+ .statusBadge.disabled {
88
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
89
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
90
+ }
91
+
92
+ .statusBadge.expired {
93
+ background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
94
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
95
+ }
96
+ `,
97
+ ];
98
+
99
+ public render(): TemplateResult {
100
+ const { apiTokens } = this.routeState;
101
+
102
+ return html`
103
+ <ops-sectionheading>API Tokens</ops-sectionheading>
104
+
105
+ <div class="apiTokensContainer">
106
+ <dees-table
107
+ .heading1=${'API Tokens'}
108
+ .heading2=${'Manage programmatic access tokens'}
109
+ .data=${apiTokens}
110
+ .dataName=${'token'}
111
+ .searchable=${true}
112
+ .displayFunction=${(token: interfaces.data.IApiTokenInfo) => ({
113
+ name: token.name,
114
+ scopes: this.renderScopePills(token.scopes),
115
+ status: this.renderStatusBadge(token),
116
+ created: new Date(token.createdAt).toLocaleDateString(),
117
+ expires: token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : 'Never',
118
+ lastUsed: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : 'Never',
119
+ })}
120
+ .dataActions=${[
121
+ {
122
+ name: 'Create Token',
123
+ iconName: 'lucide:plus',
124
+ type: ['header'],
125
+ actionFunc: async () => {
126
+ await this.showCreateTokenDialog();
127
+ },
128
+ },
129
+ {
130
+ name: 'Enable',
131
+ iconName: 'lucide:play',
132
+ type: ['inRow', 'contextmenu'] as any,
133
+ actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
134
+ actionFunc: async (actionData: any) => {
135
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
136
+ await appstate.routeManagementStatePart.dispatchAction(
137
+ appstate.toggleApiTokenAction,
138
+ { id: token.id, enabled: true },
139
+ );
140
+ },
141
+ },
142
+ {
143
+ name: 'Disable',
144
+ iconName: 'lucide:pause',
145
+ type: ['inRow', 'contextmenu'] as any,
146
+ actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
147
+ actionFunc: async (actionData: any) => {
148
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
149
+ await appstate.routeManagementStatePart.dispatchAction(
150
+ appstate.toggleApiTokenAction,
151
+ { id: token.id, enabled: false },
152
+ );
153
+ },
154
+ },
155
+ {
156
+ name: 'Revoke',
157
+ iconName: 'lucide:trash2',
158
+ type: ['inRow', 'contextmenu'] as any,
159
+ actionFunc: async (actionData: any) => {
160
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
161
+ await appstate.routeManagementStatePart.dispatchAction(
162
+ appstate.revokeApiTokenAction,
163
+ token.id,
164
+ );
165
+ },
166
+ },
167
+ ]}
168
+ ></dees-table>
169
+ </div>
170
+ `;
171
+ }
172
+
173
+ private renderScopePills(scopes: TApiTokenScope[]): TemplateResult {
174
+ return html`<div style="display: flex; flex-wrap: wrap; gap: 2px;">${scopes.map(
175
+ (s) => html`<span class="scopePill">${s}</span>`,
176
+ )}</div>`;
177
+ }
178
+
179
+ private renderStatusBadge(token: interfaces.data.IApiTokenInfo): TemplateResult {
180
+ if (!token.enabled) {
181
+ return html`<span class="statusBadge disabled">Disabled</span>`;
182
+ }
183
+ if (token.expiresAt && token.expiresAt < Date.now()) {
184
+ return html`<span class="statusBadge expired">Expired</span>`;
185
+ }
186
+ return html`<span class="statusBadge active">Active</span>`;
187
+ }
188
+
189
+ private async showCreateTokenDialog() {
190
+ const { DeesModal } = await import('@design.estate/dees-catalog');
191
+
192
+ const allScopes: TApiTokenScope[] = [
193
+ 'routes:read',
194
+ 'routes:write',
195
+ 'config:read',
196
+ 'tokens:read',
197
+ 'tokens:manage',
198
+ ];
199
+
200
+ await DeesModal.createAndShow({
201
+ heading: 'Create API Token',
202
+ content: html`
203
+ <div style="color: #888; margin-bottom: 12px; font-size: 13px;">
204
+ The token value will be shown once after creation. Copy it immediately.
205
+ </div>
206
+ <dees-form>
207
+ <dees-input-text .key=${'name'} .label=${'Token Name'} .required=${true}></dees-input-text>
208
+ <dees-input-tags
209
+ .key=${'scopes'}
210
+ .label=${'Token Scopes'}
211
+ .value=${['routes:read', 'routes:write']}
212
+ .suggestions=${allScopes}
213
+ .required=${true}
214
+ ></dees-input-tags>
215
+ <dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text>
216
+ </dees-form>
217
+ `,
218
+ menuOptions: [
219
+ {
220
+ name: 'Cancel',
221
+ iconName: 'lucide:x',
222
+ action: async (modalArg: any) => await modalArg.destroy(),
223
+ },
224
+ {
225
+ name: 'Create',
226
+ iconName: 'lucide:key',
227
+ action: async (modalArg: any) => {
228
+ const contentEl = modalArg.shadowRoot?.querySelector('.content');
229
+ const form = contentEl?.querySelector('dees-form');
230
+ if (!form) return;
231
+ const formData = await form.collectFormData();
232
+ if (!formData.name) return;
233
+
234
+ // dees-input-tags is not in dees-form's FORM_INPUT_TYPES, so collectFormData() won't
235
+ // include it. Query the tags input directly and call getValue().
236
+ const tagsInput = form.querySelector('dees-input-tags') as any;
237
+ const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
238
+ const scopes = rawScopes
239
+ .filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
240
+
241
+ const expiresInDays = formData.expiresInDays
242
+ ? parseInt(formData.expiresInDays, 10)
243
+ : null;
244
+
245
+ await modalArg.destroy();
246
+
247
+ try {
248
+ const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
249
+ if (response.success && response.tokenValue) {
250
+ // Refresh the list first so it's ready when user dismisses the modal
251
+ await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
252
+
253
+ // Show the token value in a new modal
254
+ await DeesModal.createAndShow({
255
+ heading: 'Token Created',
256
+ content: html`
257
+ <div style="color: #ccc; padding: 8px 0;">
258
+ <p>Copy this token now. It will not be shown again.</p>
259
+ <div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
260
+ <code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
261
+ </div>
262
+ </div>
263
+ `,
264
+ menuOptions: [
265
+ {
266
+ name: 'Done',
267
+ iconName: 'lucide:check',
268
+ action: async (m: any) => await m.destroy(),
269
+ },
270
+ ],
271
+ });
272
+ }
273
+ } catch (error) {
274
+ console.error('Failed to create token:', error);
275
+ }
276
+ },
277
+ },
278
+ ],
279
+ });
280
+ }
281
+
282
+ async firstUpdated() {
283
+ await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
284
+ }
285
+ }