@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.
- package/dist_serve/bundle.js +2420 -1227
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -0
- package/dist_ts/classes.dcrouter.js +27 -1
- package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
- package/dist_ts/config/classes.api-token-manager.js +134 -0
- package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
- package/dist_ts/config/classes.route-config-manager.js +231 -0
- package/dist_ts/config/index.d.ts +2 -0
- package/dist_ts/config/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/{config.handler.d.ts → api-token.handler.d.ts} +5 -2
- package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
- package/dist_ts/opsserver/handlers/index.d.ts +2 -0
- package/dist_ts/opsserver/handlers/index.js +3 -1
- package/dist_ts/opsserver/handlers/route-management.handler.d.ts +13 -0
- package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +68 -0
- package/dist_ts_interfaces/data/route-management.js +2 -0
- package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
- package/dist_ts_interfaces/requests/api-tokens.js +2 -0
- package/dist_ts_interfaces/requests/config.d.ts +77 -1
- package/dist_ts_interfaces/requests/index.d.ts +2 -0
- package/dist_ts_interfaces/requests/index.js +3 -1
- package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
- package/dist_ts_interfaces/requests/route-management.js +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +37 -1
- package/dist_ts_web/appstate.js +220 -2
- package/dist_ts_web/elements/index.d.ts +2 -0
- package/dist_ts_web/elements/index.js +3 -1
- package/dist_ts_web/elements/ops-dashboard.js +23 -3
- package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
- package/dist_ts_web/elements/ops-view-apitokens.js +310 -0
- package/dist_ts_web/elements/ops-view-config.d.ts +10 -8
- package/dist_ts_web/elements/ops-view-config.js +215 -297
- package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
- package/dist_ts_web/elements/ops-view-routes.js +404 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +37 -1
- package/ts/config/classes.api-token-manager.ts +155 -0
- package/ts/config/classes.route-config-manager.ts +271 -0
- package/ts/config/index.ts +3 -1
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/api-token.handler.ts +96 -0
- package/ts/opsserver/handlers/config.handler.ts +154 -72
- package/ts/opsserver/handlers/index.ts +3 -1
- package/ts/opsserver/handlers/route-management.handler.ts +163 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +309 -2
- package/ts_web/elements/index.ts +2 -0
- package/ts_web/elements/ops-dashboard.ts +22 -2
- package/ts_web/elements/ops-view-apitokens.ts +285 -0
- package/ts_web/elements/ops-view-config.ts +237 -299
- package/ts_web/elements/ops-view-routes.ts +389 -0
- package/ts_web/router.ts +1 -1
- package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
- package/dist_ts/cache/classes.cache.cleaner.js +0 -130
- package/dist_ts/cache/classes.cached.document.d.ts +0 -76
- package/dist_ts/cache/classes.cached.document.js +0 -100
- package/dist_ts/cache/classes.cachedb.d.ts +0 -60
- package/dist_ts/cache/classes.cachedb.js +0 -126
- package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
- package/dist_ts/cache/documents/classes.cached.email.js +0 -337
- package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
- package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
- package/dist_ts/cache/documents/index.d.ts +0 -2
- package/dist_ts/cache/documents/index.js +0 -3
- package/dist_ts/cache/index.d.ts +0 -4
- package/dist_ts/cache/index.js +0 -7
- package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
- package/dist_ts/monitoring/classes.metricscache.js +0 -63
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
- package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
- package/dist_ts/opsserver/handlers/config.handler.js +0 -67
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
- package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
- package/dist_ts/security/classes.securitylogger.js +0 -235
- package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
- package/dist_ts/storage/classes.storagemanager.js +0 -344
- package/dist_ts/storage/index.d.ts +0 -1
- package/dist_ts/storage/index.js +0 -3
package/ts_web/appstate.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface IStatsState {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface IConfigState {
|
|
24
|
-
config:
|
|
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
|
// ============================================================================
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -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: '
|
|
60
|
-
|
|
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
|
+
}
|