@serve.zone/dcrouter 8.0.0 → 8.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 +1659 -891
- 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/{remoteingress.handler.d.ts → api-token.handler.d.ts} +5 -1
- 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/{radius.handler.d.ts → route-management.handler.d.ts} +6 -1
- 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/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 +36 -0
- 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 +11 -1
- package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
- package/dist_ts_web/elements/ops-view-apitokens.js +306 -0
- 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/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 +308 -1
- package/ts_web/elements/index.ts +2 -0
- package/ts_web/elements/ops-dashboard.ts +10 -0
- package/ts_web/elements/ops-view-apitokens.ts +281 -0
- 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/monitoring/classes.metricsmanager.d.ts +0 -169
- package/dist_ts/monitoring/classes.metricsmanager.js +0 -591
- package/dist_ts/monitoring/index.d.ts +0 -1
- package/dist_ts/monitoring/index.js +0 -2
- 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/certificate.handler.d.ts +0 -34
- package/dist_ts/opsserver/handlers/certificate.handler.js +0 -419
- package/dist_ts/opsserver/handlers/config.handler.d.ts +0 -9
- package/dist_ts/opsserver/handlers/config.handler.js +0 -67
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +0 -32
- package/dist_ts/opsserver/handlers/email-ops.handler.js +0 -226
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
- package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
- package/dist_ts/opsserver/handlers/radius.handler.js +0 -296
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +0 -154
- package/dist_ts/opsserver/handlers/security.handler.d.ts +0 -11
- package/dist_ts/opsserver/handlers/security.handler.js +0 -232
- package/dist_ts/opsserver/handlers/stats.handler.d.ts +0 -13
- package/dist_ts/opsserver/handlers/stats.handler.js +0 -400
- package/dist_ts/security/classes.securitylogger.d.ts +0 -140
- 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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import type { StorageManager } from '../storage/index.js';
|
|
4
|
+
import type {
|
|
5
|
+
IStoredApiToken,
|
|
6
|
+
IApiTokenInfo,
|
|
7
|
+
TApiTokenScope,
|
|
8
|
+
} from '../../ts_interfaces/data/route-management.js';
|
|
9
|
+
|
|
10
|
+
const TOKENS_PREFIX = '/config-api/tokens/';
|
|
11
|
+
const TOKEN_PREFIX_STR = 'dcr_';
|
|
12
|
+
|
|
13
|
+
export class ApiTokenManager {
|
|
14
|
+
private tokens = new Map<string, IStoredApiToken>();
|
|
15
|
+
|
|
16
|
+
constructor(private storageManager: StorageManager) {}
|
|
17
|
+
|
|
18
|
+
public async initialize(): Promise<void> {
|
|
19
|
+
await this.loadTokens();
|
|
20
|
+
if (this.tokens.size > 0) {
|
|
21
|
+
logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// =========================================================================
|
|
26
|
+
// Token lifecycle
|
|
27
|
+
// =========================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new API token. Returns the raw token value (shown once).
|
|
31
|
+
*/
|
|
32
|
+
public async createToken(
|
|
33
|
+
name: string,
|
|
34
|
+
scopes: TApiTokenScope[],
|
|
35
|
+
expiresInDays: number | null,
|
|
36
|
+
createdBy: string,
|
|
37
|
+
): Promise<{ id: string; rawToken: string }> {
|
|
38
|
+
const id = plugins.uuid.v4();
|
|
39
|
+
const randomBytes = plugins.crypto.randomBytes(32);
|
|
40
|
+
const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
|
|
41
|
+
const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;
|
|
42
|
+
|
|
43
|
+
const tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
|
44
|
+
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const stored: IStoredApiToken = {
|
|
47
|
+
id,
|
|
48
|
+
name,
|
|
49
|
+
tokenHash,
|
|
50
|
+
scopes,
|
|
51
|
+
createdAt: now,
|
|
52
|
+
expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
|
|
53
|
+
lastUsedAt: null,
|
|
54
|
+
createdBy,
|
|
55
|
+
enabled: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.tokens.set(id, stored);
|
|
59
|
+
await this.persistToken(stored);
|
|
60
|
+
logger.log('info', `API token '${name}' created (id: ${id})`);
|
|
61
|
+
return { id, rawToken };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate a raw token string. Returns the stored token if valid, null otherwise.
|
|
66
|
+
* Also updates lastUsedAt.
|
|
67
|
+
*/
|
|
68
|
+
public async validateToken(rawToken: string): Promise<IStoredApiToken | null> {
|
|
69
|
+
if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null;
|
|
70
|
+
|
|
71
|
+
const hash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
|
72
|
+
|
|
73
|
+
for (const stored of this.tokens.values()) {
|
|
74
|
+
if (stored.tokenHash === hash) {
|
|
75
|
+
if (!stored.enabled) return null;
|
|
76
|
+
if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;
|
|
77
|
+
|
|
78
|
+
// Update lastUsedAt (fire and forget)
|
|
79
|
+
stored.lastUsedAt = Date.now();
|
|
80
|
+
this.persistToken(stored).catch(() => {});
|
|
81
|
+
return stored;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a token has a specific scope.
|
|
89
|
+
*/
|
|
90
|
+
public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
|
|
91
|
+
return token.scopes.includes(scope);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List all tokens (safe info only, no hashes).
|
|
96
|
+
*/
|
|
97
|
+
public listTokens(): IApiTokenInfo[] {
|
|
98
|
+
const result: IApiTokenInfo[] = [];
|
|
99
|
+
for (const stored of this.tokens.values()) {
|
|
100
|
+
result.push({
|
|
101
|
+
id: stored.id,
|
|
102
|
+
name: stored.name,
|
|
103
|
+
scopes: stored.scopes,
|
|
104
|
+
createdAt: stored.createdAt,
|
|
105
|
+
expiresAt: stored.expiresAt,
|
|
106
|
+
lastUsedAt: stored.lastUsedAt,
|
|
107
|
+
enabled: stored.enabled,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Revoke (delete) a token.
|
|
115
|
+
*/
|
|
116
|
+
public async revokeToken(id: string): Promise<boolean> {
|
|
117
|
+
if (!this.tokens.has(id)) return false;
|
|
118
|
+
const token = this.tokens.get(id)!;
|
|
119
|
+
this.tokens.delete(id);
|
|
120
|
+
await this.storageManager.delete(`${TOKENS_PREFIX}${id}.json`);
|
|
121
|
+
logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Enable or disable a token.
|
|
127
|
+
*/
|
|
128
|
+
public async toggleToken(id: string, enabled: boolean): Promise<boolean> {
|
|
129
|
+
const stored = this.tokens.get(id);
|
|
130
|
+
if (!stored) return false;
|
|
131
|
+
stored.enabled = enabled;
|
|
132
|
+
await this.persistToken(stored);
|
|
133
|
+
logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =========================================================================
|
|
138
|
+
// Private
|
|
139
|
+
// =========================================================================
|
|
140
|
+
|
|
141
|
+
private async loadTokens(): Promise<void> {
|
|
142
|
+
const keys = await this.storageManager.list(TOKENS_PREFIX);
|
|
143
|
+
for (const key of keys) {
|
|
144
|
+
if (!key.endsWith('.json')) continue;
|
|
145
|
+
const stored = await this.storageManager.getJSON<IStoredApiToken>(key);
|
|
146
|
+
if (stored?.id) {
|
|
147
|
+
this.tokens.set(stored.id, stored);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async persistToken(stored: IStoredApiToken): Promise<void> {
|
|
153
|
+
await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import type { StorageManager } from '../storage/index.js';
|
|
4
|
+
import type {
|
|
5
|
+
IStoredRoute,
|
|
6
|
+
IRouteOverride,
|
|
7
|
+
IMergedRoute,
|
|
8
|
+
IRouteWarning,
|
|
9
|
+
} from '../../ts_interfaces/data/route-management.js';
|
|
10
|
+
|
|
11
|
+
const ROUTES_PREFIX = '/config-api/routes/';
|
|
12
|
+
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
|
13
|
+
|
|
14
|
+
export class RouteConfigManager {
|
|
15
|
+
private storedRoutes = new Map<string, IStoredRoute>();
|
|
16
|
+
private overrides = new Map<string, IRouteOverride>();
|
|
17
|
+
private warnings: IRouteWarning[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private storageManager: StorageManager,
|
|
21
|
+
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
|
22
|
+
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load persisted routes and overrides, compute warnings, apply to SmartProxy.
|
|
27
|
+
*/
|
|
28
|
+
public async initialize(): Promise<void> {
|
|
29
|
+
await this.loadStoredRoutes();
|
|
30
|
+
await this.loadOverrides();
|
|
31
|
+
this.computeWarnings();
|
|
32
|
+
this.logWarnings();
|
|
33
|
+
await this.applyRoutes();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =========================================================================
|
|
37
|
+
// Merged view
|
|
38
|
+
// =========================================================================
|
|
39
|
+
|
|
40
|
+
public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
|
|
41
|
+
const merged: IMergedRoute[] = [];
|
|
42
|
+
|
|
43
|
+
// Hardcoded routes
|
|
44
|
+
for (const route of this.getHardcodedRoutes()) {
|
|
45
|
+
const name = route.name || '';
|
|
46
|
+
const override = this.overrides.get(name);
|
|
47
|
+
merged.push({
|
|
48
|
+
route,
|
|
49
|
+
source: 'hardcoded',
|
|
50
|
+
enabled: override ? override.enabled : true,
|
|
51
|
+
overridden: !!override,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Programmatic routes
|
|
56
|
+
for (const stored of this.storedRoutes.values()) {
|
|
57
|
+
merged.push({
|
|
58
|
+
route: stored.route,
|
|
59
|
+
source: 'programmatic',
|
|
60
|
+
enabled: stored.enabled,
|
|
61
|
+
overridden: false,
|
|
62
|
+
storedRouteId: stored.id,
|
|
63
|
+
createdAt: stored.createdAt,
|
|
64
|
+
updatedAt: stored.updatedAt,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { routes: merged, warnings: [...this.warnings] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =========================================================================
|
|
72
|
+
// Programmatic route CRUD
|
|
73
|
+
// =========================================================================
|
|
74
|
+
|
|
75
|
+
public async createRoute(
|
|
76
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
77
|
+
createdBy: string,
|
|
78
|
+
enabled = true,
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
const id = plugins.uuid.v4();
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
|
|
83
|
+
// Ensure route has a name
|
|
84
|
+
if (!route.name) {
|
|
85
|
+
route.name = `programmatic-${id.slice(0, 8)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const stored: IStoredRoute = {
|
|
89
|
+
id,
|
|
90
|
+
route,
|
|
91
|
+
enabled,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
createdBy,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.storedRoutes.set(id, stored);
|
|
98
|
+
await this.persistRoute(stored);
|
|
99
|
+
await this.applyRoutes();
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async updateRoute(
|
|
104
|
+
id: string,
|
|
105
|
+
patch: { route?: Partial<plugins.smartproxy.IRouteConfig>; enabled?: boolean },
|
|
106
|
+
): Promise<boolean> {
|
|
107
|
+
const stored = this.storedRoutes.get(id);
|
|
108
|
+
if (!stored) return false;
|
|
109
|
+
|
|
110
|
+
if (patch.route) {
|
|
111
|
+
stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig;
|
|
112
|
+
}
|
|
113
|
+
if (patch.enabled !== undefined) {
|
|
114
|
+
stored.enabled = patch.enabled;
|
|
115
|
+
}
|
|
116
|
+
stored.updatedAt = Date.now();
|
|
117
|
+
|
|
118
|
+
await this.persistRoute(stored);
|
|
119
|
+
await this.applyRoutes();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async deleteRoute(id: string): Promise<boolean> {
|
|
124
|
+
if (!this.storedRoutes.has(id)) return false;
|
|
125
|
+
this.storedRoutes.delete(id);
|
|
126
|
+
await this.storageManager.delete(`${ROUTES_PREFIX}${id}.json`);
|
|
127
|
+
await this.applyRoutes();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
|
|
132
|
+
return this.updateRoute(id, { enabled });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// =========================================================================
|
|
136
|
+
// Hardcoded route overrides
|
|
137
|
+
// =========================================================================
|
|
138
|
+
|
|
139
|
+
public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise<void> {
|
|
140
|
+
const override: IRouteOverride = {
|
|
141
|
+
routeName,
|
|
142
|
+
enabled,
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
updatedBy,
|
|
145
|
+
};
|
|
146
|
+
this.overrides.set(routeName, override);
|
|
147
|
+
await this.storageManager.setJSON(`${OVERRIDES_PREFIX}${routeName}.json`, override);
|
|
148
|
+
this.computeWarnings();
|
|
149
|
+
await this.applyRoutes();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public async removeOverride(routeName: string): Promise<boolean> {
|
|
153
|
+
if (!this.overrides.has(routeName)) return false;
|
|
154
|
+
this.overrides.delete(routeName);
|
|
155
|
+
await this.storageManager.delete(`${OVERRIDES_PREFIX}${routeName}.json`);
|
|
156
|
+
this.computeWarnings();
|
|
157
|
+
await this.applyRoutes();
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =========================================================================
|
|
162
|
+
// Private: persistence
|
|
163
|
+
// =========================================================================
|
|
164
|
+
|
|
165
|
+
private async loadStoredRoutes(): Promise<void> {
|
|
166
|
+
const keys = await this.storageManager.list(ROUTES_PREFIX);
|
|
167
|
+
for (const key of keys) {
|
|
168
|
+
if (!key.endsWith('.json')) continue;
|
|
169
|
+
const stored = await this.storageManager.getJSON<IStoredRoute>(key);
|
|
170
|
+
if (stored?.id) {
|
|
171
|
+
this.storedRoutes.set(stored.id, stored);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (this.storedRoutes.size > 0) {
|
|
175
|
+
logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async loadOverrides(): Promise<void> {
|
|
180
|
+
const keys = await this.storageManager.list(OVERRIDES_PREFIX);
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
if (!key.endsWith('.json')) continue;
|
|
183
|
+
const override = await this.storageManager.getJSON<IRouteOverride>(key);
|
|
184
|
+
if (override?.routeName) {
|
|
185
|
+
this.overrides.set(override.routeName, override);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (this.overrides.size > 0) {
|
|
189
|
+
logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async persistRoute(stored: IStoredRoute): Promise<void> {
|
|
194
|
+
await this.storageManager.setJSON(`${ROUTES_PREFIX}${stored.id}.json`, stored);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// Private: warnings
|
|
199
|
+
// =========================================================================
|
|
200
|
+
|
|
201
|
+
private computeWarnings(): void {
|
|
202
|
+
this.warnings = [];
|
|
203
|
+
const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || ''));
|
|
204
|
+
|
|
205
|
+
// Check overrides
|
|
206
|
+
for (const [routeName, override] of this.overrides) {
|
|
207
|
+
if (!hardcodedNames.has(routeName)) {
|
|
208
|
+
this.warnings.push({
|
|
209
|
+
type: 'orphaned-override',
|
|
210
|
+
routeName,
|
|
211
|
+
message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`,
|
|
212
|
+
});
|
|
213
|
+
} else if (!override.enabled) {
|
|
214
|
+
this.warnings.push({
|
|
215
|
+
type: 'disabled-hardcoded',
|
|
216
|
+
routeName,
|
|
217
|
+
message: `Route '${routeName}' is disabled via API override`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check disabled programmatic routes
|
|
223
|
+
for (const stored of this.storedRoutes.values()) {
|
|
224
|
+
if (!stored.enabled) {
|
|
225
|
+
const name = stored.route.name || stored.id;
|
|
226
|
+
this.warnings.push({
|
|
227
|
+
type: 'disabled-programmatic',
|
|
228
|
+
routeName: name,
|
|
229
|
+
message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private logWarnings(): void {
|
|
236
|
+
for (const w of this.warnings) {
|
|
237
|
+
logger.log('warn', w.message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// =========================================================================
|
|
242
|
+
// Private: apply merged routes to SmartProxy
|
|
243
|
+
// =========================================================================
|
|
244
|
+
|
|
245
|
+
private async applyRoutes(): Promise<void> {
|
|
246
|
+
const smartProxy = this.getSmartProxy();
|
|
247
|
+
if (!smartProxy) return;
|
|
248
|
+
|
|
249
|
+
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
250
|
+
|
|
251
|
+
// Add enabled hardcoded routes (respecting overrides)
|
|
252
|
+
for (const route of this.getHardcodedRoutes()) {
|
|
253
|
+
const name = route.name || '';
|
|
254
|
+
const override = this.overrides.get(name);
|
|
255
|
+
if (override && !override.enabled) {
|
|
256
|
+
continue; // Skip disabled hardcoded route
|
|
257
|
+
}
|
|
258
|
+
enabledRoutes.push(route);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add enabled programmatic routes
|
|
262
|
+
for (const stored of this.storedRoutes.values()) {
|
|
263
|
+
if (stored.enabled) {
|
|
264
|
+
enabledRoutes.push(stored.route);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await smartProxy.updateRoutes(enabledRoutes);
|
|
269
|
+
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
|
|
270
|
+
}
|
|
271
|
+
}
|
package/ts/config/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ export class OpsServer {
|
|
|
20
20
|
private emailOpsHandler: handlers.EmailOpsHandler;
|
|
21
21
|
private certificateHandler: handlers.CertificateHandler;
|
|
22
22
|
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
|
23
|
+
private routeManagementHandler: handlers.RouteManagementHandler;
|
|
24
|
+
private apiTokenHandler: handlers.ApiTokenHandler;
|
|
23
25
|
|
|
24
26
|
constructor(dcRouterRefArg: DcRouter) {
|
|
25
27
|
this.dcRouterRef = dcRouterRefArg;
|
|
@@ -61,6 +63,8 @@ export class OpsServer {
|
|
|
61
63
|
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
|
62
64
|
this.certificateHandler = new handlers.CertificateHandler(this);
|
|
63
65
|
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
|
66
|
+
this.routeManagementHandler = new handlers.RouteManagementHandler(this);
|
|
67
|
+
this.apiTokenHandler = new handlers.ApiTokenHandler(this);
|
|
64
68
|
|
|
65
69
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
|
66
70
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export class ApiTokenHandler {
|
|
6
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
7
|
+
|
|
8
|
+
constructor(private opsServerRef: OpsServer) {
|
|
9
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
10
|
+
this.registerHandlers();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Token management requires admin JWT only (tokens cannot manage tokens).
|
|
15
|
+
*/
|
|
16
|
+
private async requireAdmin(identity?: interfaces.data.IIdentity): Promise<string> {
|
|
17
|
+
if (!identity?.jwt) {
|
|
18
|
+
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
19
|
+
}
|
|
20
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity });
|
|
21
|
+
if (!isAdmin) {
|
|
22
|
+
throw new plugins.typedrequest.TypedResponseError('admin access required');
|
|
23
|
+
}
|
|
24
|
+
return identity.userId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private registerHandlers(): void {
|
|
28
|
+
// Create API token
|
|
29
|
+
this.typedrouter.addTypedHandler(
|
|
30
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
|
31
|
+
'createApiToken',
|
|
32
|
+
async (dataArg) => {
|
|
33
|
+
const userId = await this.requireAdmin(dataArg.identity);
|
|
34
|
+
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
35
|
+
if (!manager) {
|
|
36
|
+
return { success: false, message: 'Token management not initialized' };
|
|
37
|
+
}
|
|
38
|
+
const result = await manager.createToken(
|
|
39
|
+
dataArg.name,
|
|
40
|
+
dataArg.scopes,
|
|
41
|
+
dataArg.expiresInDays ?? null,
|
|
42
|
+
userId,
|
|
43
|
+
);
|
|
44
|
+
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// List API tokens
|
|
50
|
+
this.typedrouter.addTypedHandler(
|
|
51
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
|
52
|
+
'listApiTokens',
|
|
53
|
+
async (dataArg) => {
|
|
54
|
+
await this.requireAdmin(dataArg.identity);
|
|
55
|
+
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
56
|
+
if (!manager) {
|
|
57
|
+
return { tokens: [] };
|
|
58
|
+
}
|
|
59
|
+
return { tokens: manager.listTokens() };
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Revoke API token
|
|
65
|
+
this.typedrouter.addTypedHandler(
|
|
66
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
|
67
|
+
'revokeApiToken',
|
|
68
|
+
async (dataArg) => {
|
|
69
|
+
await this.requireAdmin(dataArg.identity);
|
|
70
|
+
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
71
|
+
if (!manager) {
|
|
72
|
+
return { success: false, message: 'Token management not initialized' };
|
|
73
|
+
}
|
|
74
|
+
const ok = await manager.revokeToken(dataArg.id);
|
|
75
|
+
return { success: ok, message: ok ? undefined : 'Token not found' };
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Toggle API token
|
|
81
|
+
this.typedrouter.addTypedHandler(
|
|
82
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
|
83
|
+
'toggleApiToken',
|
|
84
|
+
async (dataArg) => {
|
|
85
|
+
await this.requireAdmin(dataArg.identity);
|
|
86
|
+
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
87
|
+
if (!manager) {
|
|
88
|
+
return { success: false, message: 'Token management not initialized' };
|
|
89
|
+
}
|
|
90
|
+
const ok = await manager.toggleToken(dataArg.id, dataArg.enabled);
|
|
91
|
+
return { success: ok, message: ok ? undefined : 'Token not found' };
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -6,4 +6,6 @@ export * from './stats.handler.js';
|
|
|
6
6
|
export * from './radius.handler.js';
|
|
7
7
|
export * from './email-ops.handler.js';
|
|
8
8
|
export * from './certificate.handler.js';
|
|
9
|
-
export * from './remoteingress.handler.js';
|
|
9
|
+
export * from './remoteingress.handler.js';
|
|
10
|
+
export * from './route-management.handler.js';
|
|
11
|
+
export * from './api-token.handler.js';
|