@serve.zone/dcrouter 13.30.0 → 13.31.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 +846 -818
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +1 -1
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +11 -0
- package/dist_ts/opsserver/handlers/admin.handler.js +72 -8
- package/dist_ts/opsserver/handlers/users.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/users.handler.js +14 -3
- package/dist_ts_interfaces/requests/users.d.ts +35 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +8 -0
- package/dist_ts_web/appstate.js +52 -2
- package/dist_ts_web/elements/access/ops-view-users.d.ts +2 -0
- package/dist_ts_web/elements/access/ops-view-users.js +133 -2
- package/dist_ts_web/elements/network/ops-view-routes.js +11 -1
- package/dist_ts_web/elements/ops-dashboard.js +2 -4
- package/package.json +2 -2
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +1 -1
- package/ts/opsserver/handlers/admin.handler.ts +91 -10
- package/ts/opsserver/handlers/users.handler.ts +25 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +69 -1
- package/ts_web/elements/access/ops-view-users.ts +139 -1
- package/ts_web/elements/network/ops-view-routes.ts +9 -0
- package/ts_web/elements/ops-dashboard.ts +1 -3
|
@@ -159,6 +159,93 @@ export class AdminHandler {
|
|
|
159
159
|
throw new plugins.typedrequest.TypedResponseError((error as Error).message || 'failed to create initial admin');
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
public async createUser(optionsArg: {
|
|
164
|
+
email: string;
|
|
165
|
+
name?: string;
|
|
166
|
+
role: interfaces.requests.TUserManagementRole;
|
|
167
|
+
password: string;
|
|
168
|
+
enableIdpGlobalAuth?: boolean;
|
|
169
|
+
}): Promise<interfaces.requests.IReq_CreateUser['response']> {
|
|
170
|
+
const store = this.getAccountStore();
|
|
171
|
+
if (!store) {
|
|
172
|
+
return { success: false, message: 'database is not ready' };
|
|
173
|
+
}
|
|
174
|
+
if (!(await store.hasActiveAdminAccount())) {
|
|
175
|
+
return { success: false, message: 'initial admin bootstrap is required before creating users' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const role = optionsArg.role;
|
|
179
|
+
if (role !== 'admin' && role !== 'user') {
|
|
180
|
+
return { success: false, message: 'role must be admin or user' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const password = String(optionsArg.password || '');
|
|
184
|
+
if (!password) {
|
|
185
|
+
return { success: false, message: 'password is required' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const authSources: Array<'local' | 'idp.global'> = ['local'];
|
|
189
|
+
if (optionsArg.enableIdpGlobalAuth) {
|
|
190
|
+
authSources.push('idp.global');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const email = String(optionsArg.email || '').trim();
|
|
195
|
+
const account = await store.createAccount({
|
|
196
|
+
email,
|
|
197
|
+
name: String(optionsArg.name || '').trim() || email,
|
|
198
|
+
role,
|
|
199
|
+
authSources,
|
|
200
|
+
password,
|
|
201
|
+
});
|
|
202
|
+
return { success: true, user: this.accountToUser(account) };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return { success: false, message: (error as Error).message || 'failed to create user' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public async deleteUser(optionsArg: {
|
|
209
|
+
id: string;
|
|
210
|
+
requestingUserId: string;
|
|
211
|
+
}): Promise<interfaces.requests.IReq_DeleteUser['response']> {
|
|
212
|
+
const store = this.getAccountStore();
|
|
213
|
+
if (!store) {
|
|
214
|
+
return { success: false, message: 'database is not ready' };
|
|
215
|
+
}
|
|
216
|
+
if (!(await store.hasActiveAdminAccount())) {
|
|
217
|
+
return { success: false, message: 'initial admin bootstrap is required before deleting users' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const id = String(optionsArg.id || '').trim();
|
|
221
|
+
if (!id) {
|
|
222
|
+
return { success: false, message: 'user id is required' };
|
|
223
|
+
}
|
|
224
|
+
if (id === optionsArg.requestingUserId) {
|
|
225
|
+
return { success: false, message: 'cannot delete the current user' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const account = await store.getAccountById(id);
|
|
229
|
+
if (!account) {
|
|
230
|
+
return { success: false, message: 'user not found' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (account.role === 'admin' && account.status === 'active') {
|
|
234
|
+
const activeAdmins = (await store.listAccounts()).filter(
|
|
235
|
+
(accountArg) => accountArg.role === 'admin' && accountArg.status === 'active',
|
|
236
|
+
);
|
|
237
|
+
if (activeAdmins.length <= 1) {
|
|
238
|
+
return { success: false, message: 'cannot delete the last active admin' };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const doc = await plugins.idpSdkServer.IdpSdkAccountDoc.findById(id);
|
|
243
|
+
if (!doc) {
|
|
244
|
+
return { success: false, message: 'user not found' };
|
|
245
|
+
}
|
|
246
|
+
await doc.delete();
|
|
247
|
+
return { success: true };
|
|
248
|
+
}
|
|
162
249
|
|
|
163
250
|
private registerHandlers(): void {
|
|
164
251
|
this.typedrouter.addTypedHandler(
|
|
@@ -420,23 +507,17 @@ export class AdminHandler {
|
|
|
420
507
|
}
|
|
421
508
|
|
|
422
509
|
const baseUrl = this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl || process.env.DCROUTER_IDP_GLOBAL_URL;
|
|
423
|
-
if (!baseUrl) {
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
510
|
if (!this.idpClient) {
|
|
428
|
-
this.idpClient =
|
|
511
|
+
this.idpClient = baseUrl
|
|
512
|
+
? new plugins.idpSdkServer.IdpGlobalServerClient({ baseUrl })
|
|
513
|
+
: new plugins.idpSdkServer.IdpGlobalServerClient({} as plugins.idpSdkServer.IIdpGlobalServerClientOptions);
|
|
429
514
|
this.ownsIdpClient = true;
|
|
430
515
|
}
|
|
431
516
|
return this.idpClient;
|
|
432
517
|
}
|
|
433
518
|
|
|
434
519
|
private isIdpGlobalConfigured(): boolean {
|
|
435
|
-
return
|
|
436
|
-
this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient ||
|
|
437
|
-
this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl ||
|
|
438
|
-
process.env.DCROUTER_IDP_GLOBAL_URL
|
|
439
|
-
);
|
|
520
|
+
return true;
|
|
440
521
|
}
|
|
441
522
|
|
|
442
523
|
private accountToUser(accountArg: plugins.idpSdkServer.IIdpSdkAccount): TAdminUser {
|
|
@@ -3,7 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Handler for OpsServer user accounts. Registers on adminRouter,
|
|
7
7
|
* so admin middleware enforces auth + role check before the handler runs.
|
|
8
8
|
* User data is owned by AdminHandler; this handler just exposes a safe
|
|
9
9
|
* projection of it via TypedRequest.
|
|
@@ -16,7 +16,7 @@ export class UsersHandler {
|
|
|
16
16
|
private registerHandlers(): void {
|
|
17
17
|
const router = this.opsServerRef.adminRouter;
|
|
18
18
|
|
|
19
|
-
// List users (admin-only
|
|
19
|
+
// List users (admin-only)
|
|
20
20
|
router.addTypedHandler(
|
|
21
21
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListUsers>(
|
|
22
22
|
'listUsers',
|
|
@@ -26,5 +26,28 @@ export class UsersHandler {
|
|
|
26
26
|
},
|
|
27
27
|
),
|
|
28
28
|
);
|
|
29
|
+
|
|
30
|
+
router.addTypedHandler(
|
|
31
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateUser>(
|
|
32
|
+
'createUser',
|
|
33
|
+
async (dataArg) => this.opsServerRef.adminHandler.createUser({
|
|
34
|
+
email: dataArg.email,
|
|
35
|
+
name: dataArg.name,
|
|
36
|
+
role: dataArg.role,
|
|
37
|
+
password: dataArg.password,
|
|
38
|
+
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
router.addTypedHandler(
|
|
44
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteUser>(
|
|
45
|
+
'deleteUser',
|
|
46
|
+
async (dataArg) => this.opsServerRef.adminHandler.deleteUser({
|
|
47
|
+
id: dataArg.id,
|
|
48
|
+
requestingUserId: dataArg.identity.userId,
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
29
52
|
}
|
|
30
53
|
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -2637,7 +2637,7 @@ export async function createGatewayClientToken(
|
|
|
2637
2637
|
});
|
|
2638
2638
|
}
|
|
2639
2639
|
|
|
2640
|
-
// Users
|
|
2640
|
+
// Users
|
|
2641
2641
|
export const fetchUsersAction = usersStatePart.createAction(async (statePartArg): Promise<IUsersState> => {
|
|
2642
2642
|
const context = getActionContext();
|
|
2643
2643
|
const currentState = statePartArg.getState()!;
|
|
@@ -2666,6 +2666,74 @@ export const fetchUsersAction = usersStatePart.createAction(async (statePartArg)
|
|
|
2666
2666
|
}
|
|
2667
2667
|
});
|
|
2668
2668
|
|
|
2669
|
+
export const createUserAction = usersStatePart.createAction<{
|
|
2670
|
+
email: string;
|
|
2671
|
+
name?: string;
|
|
2672
|
+
role: interfaces.requests.TUserManagementRole;
|
|
2673
|
+
password: string;
|
|
2674
|
+
enableIdpGlobalAuth?: boolean;
|
|
2675
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IUsersState> => {
|
|
2676
|
+
const context = getActionContext();
|
|
2677
|
+
const currentState = statePartArg.getState()!;
|
|
2678
|
+
if (!context.identity) return currentState;
|
|
2679
|
+
|
|
2680
|
+
try {
|
|
2681
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2682
|
+
interfaces.requests.IReq_CreateUser
|
|
2683
|
+
>('/typedrequest', 'createUser');
|
|
2684
|
+
|
|
2685
|
+
const response = await request.fire({
|
|
2686
|
+
identity: context.identity,
|
|
2687
|
+
email: dataArg.email,
|
|
2688
|
+
name: dataArg.name,
|
|
2689
|
+
role: dataArg.role,
|
|
2690
|
+
password: dataArg.password,
|
|
2691
|
+
enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth,
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
if (!response.success) {
|
|
2695
|
+
throw new Error(response.message || 'Failed to create user');
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
return await actionContext!.dispatch(fetchUsersAction, null);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
return {
|
|
2701
|
+
...currentState,
|
|
2702
|
+
error: error instanceof Error ? error.message : 'Failed to create user',
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
export const deleteUserAction = usersStatePart.createAction<string>(
|
|
2708
|
+
async (statePartArg, userIdArg, actionContext): Promise<IUsersState> => {
|
|
2709
|
+
const context = getActionContext();
|
|
2710
|
+
const currentState = statePartArg.getState()!;
|
|
2711
|
+
if (!context.identity) return currentState;
|
|
2712
|
+
|
|
2713
|
+
try {
|
|
2714
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2715
|
+
interfaces.requests.IReq_DeleteUser
|
|
2716
|
+
>('/typedrequest', 'deleteUser');
|
|
2717
|
+
|
|
2718
|
+
const response = await request.fire({
|
|
2719
|
+
identity: context.identity,
|
|
2720
|
+
id: userIdArg,
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
if (!response.success) {
|
|
2724
|
+
throw new Error(response.message || 'Failed to delete user');
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
return await actionContext!.dispatch(fetchUsersAction, null);
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
return {
|
|
2730
|
+
...currentState,
|
|
2731
|
+
error: error instanceof Error ? error.message : 'Failed to delete user',
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
},
|
|
2735
|
+
);
|
|
2736
|
+
|
|
2669
2737
|
export async function createApiToken(
|
|
2670
2738
|
name: string,
|
|
2671
2739
|
scopes: interfaces.data.TApiTokenScope[],
|
|
@@ -116,12 +116,31 @@ export class OpsViewUsers extends DeesElement {
|
|
|
116
116
|
.showColumnFilters=${true}
|
|
117
117
|
.displayFunction=${(user: appstate.IUser) => ({
|
|
118
118
|
ID: html`<span class="userIdCell">${user.id}</span>`,
|
|
119
|
-
|
|
119
|
+
Email: user.email || user.username,
|
|
120
|
+
Name: user.name || '',
|
|
120
121
|
Role: this.renderRoleBadge(user.role),
|
|
122
|
+
Status: user.status || 'active',
|
|
123
|
+
Auth: (user.authSources || []).join(', ') || 'bootstrap',
|
|
121
124
|
Session: user.id === currentUserId
|
|
122
125
|
? html`<span class="sessionBadge">current</span>`
|
|
123
126
|
: '',
|
|
124
127
|
})}
|
|
128
|
+
.dataActions=${[
|
|
129
|
+
{
|
|
130
|
+
name: 'Create User',
|
|
131
|
+
iconName: 'lucide:userPlus',
|
|
132
|
+
type: ['header'],
|
|
133
|
+
actionFunc: async () => await this.showCreateUserDialog(),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'Delete',
|
|
137
|
+
iconName: 'lucide:trash2',
|
|
138
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
139
|
+
actionFunc: async (actionData: any) => {
|
|
140
|
+
await this.showDeleteUserDialog(actionData.item as appstate.IUser);
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
125
144
|
></dees-table>
|
|
126
145
|
</div>
|
|
127
146
|
`;
|
|
@@ -132,6 +151,125 @@ export class OpsViewUsers extends DeesElement {
|
|
|
132
151
|
return html`<span class="roleBadge ${cls}">${role}</span>`;
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
private async showCreateUserDialog(): Promise<void> {
|
|
155
|
+
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
|
156
|
+
|
|
157
|
+
await DeesModal.createAndShow({
|
|
158
|
+
heading: 'Create User',
|
|
159
|
+
content: html`
|
|
160
|
+
<dees-form>
|
|
161
|
+
<dees-input-text .key=${'email'} .label=${'Email'} .required=${true}></dees-input-text>
|
|
162
|
+
<dees-input-text .key=${'name'} .label=${'Display name'}></dees-input-text>
|
|
163
|
+
<dees-input-dropdown
|
|
164
|
+
.key=${'role'}
|
|
165
|
+
.label=${'Role'}
|
|
166
|
+
.options=${[
|
|
167
|
+
{ option: 'User', key: 'user' },
|
|
168
|
+
{ option: 'Admin', key: 'admin' },
|
|
169
|
+
]}
|
|
170
|
+
.selectedOption=${{ option: 'User', key: 'user' }}
|
|
171
|
+
.required=${true}
|
|
172
|
+
></dees-input-dropdown>
|
|
173
|
+
<dees-input-text .key=${'password'} .label=${'Password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
|
|
174
|
+
<dees-input-text .key=${'passwordConfirm'} .label=${'Confirm password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
|
|
175
|
+
<dees-input-checkbox
|
|
176
|
+
.key=${'enableIdpGlobalAuth'}
|
|
177
|
+
.label=${'Allow idp.global login for this email'}
|
|
178
|
+
.description=${'Uses https://idp.global by default; the local dcrouter account and role remain authoritative.'}
|
|
179
|
+
></dees-input-checkbox>
|
|
180
|
+
</dees-form>
|
|
181
|
+
`,
|
|
182
|
+
menuOptions: [
|
|
183
|
+
{
|
|
184
|
+
name: 'Cancel',
|
|
185
|
+
iconName: 'lucide:x',
|
|
186
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'Create',
|
|
190
|
+
iconName: 'lucide:userPlus',
|
|
191
|
+
action: async (modalArg: any) => {
|
|
192
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
|
193
|
+
if (!form) return;
|
|
194
|
+
const data = await form.collectFormData();
|
|
195
|
+
const email = String(data.email || '').trim();
|
|
196
|
+
const name = String(data.name || '').trim();
|
|
197
|
+
const password = String(data.password || '');
|
|
198
|
+
const passwordConfirm = String(data.passwordConfirm || '');
|
|
199
|
+
const roleValue = String(data.role?.key ?? data.role ?? 'user');
|
|
200
|
+
|
|
201
|
+
if (!email || !password) {
|
|
202
|
+
form.setStatus?.('error', 'Email and password are required.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (password !== passwordConfirm) {
|
|
206
|
+
form.setStatus?.('error', 'Passwords do not match.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
form.setStatus?.('pending', 'Creating user...');
|
|
211
|
+
await appstate.usersStatePart.dispatchAction(appstate.createUserAction, {
|
|
212
|
+
email,
|
|
213
|
+
name,
|
|
214
|
+
role: roleValue === 'admin' ? 'admin' : 'user',
|
|
215
|
+
password,
|
|
216
|
+
enableIdpGlobalAuth: Boolean(data.enableIdpGlobalAuth),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const state = appstate.usersStatePart.getState();
|
|
220
|
+
if (state?.error) {
|
|
221
|
+
form.setStatus?.('error', state.error);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
DeesToast.show({ message: `User created for ${email}`, type: 'success', duration: 3000 });
|
|
226
|
+
await modalArg.destroy();
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async showDeleteUserDialog(userArg: appstate.IUser): Promise<void> {
|
|
234
|
+
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
|
235
|
+
const currentUserId = this.loginState.identity?.userId;
|
|
236
|
+
if (userArg.id === currentUserId) {
|
|
237
|
+
DeesToast.show({ message: 'You cannot delete the current user.', type: 'error', duration: 4000 });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await DeesModal.createAndShow({
|
|
242
|
+
heading: 'Delete User',
|
|
243
|
+
content: html`
|
|
244
|
+
<div style="padding: 8px 0; font-size: 14px; line-height: 1.5;">
|
|
245
|
+
<p>Delete <strong>${userArg.email || userArg.username}</strong>?</p>
|
|
246
|
+
<p style="color: #f59e0b; margin-top: 12px;">This removes the local dcrouter account and cannot be undone.</p>
|
|
247
|
+
</div>
|
|
248
|
+
`,
|
|
249
|
+
menuOptions: [
|
|
250
|
+
{
|
|
251
|
+
name: 'Cancel',
|
|
252
|
+
iconName: 'lucide:x',
|
|
253
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'Delete',
|
|
257
|
+
iconName: 'lucide:trash2',
|
|
258
|
+
action: async (modalArg: any) => {
|
|
259
|
+
await appstate.usersStatePart.dispatchAction(appstate.deleteUserAction, userArg.id);
|
|
260
|
+
const state = appstate.usersStatePart.getState();
|
|
261
|
+
if (state?.error) {
|
|
262
|
+
DeesToast.show({ message: state.error, type: 'error', duration: 4000 });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
DeesToast.show({ message: 'User deleted.', type: 'success', duration: 3000 });
|
|
266
|
+
await modalArg.destroy();
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
135
273
|
async firstUpdated() {
|
|
136
274
|
if (this.loginState.isLoggedIn) {
|
|
137
275
|
await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
|
|
@@ -271,6 +271,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
271
271
|
const tags = [...(mr.route.tags || [])];
|
|
272
272
|
tags.push(mr.origin);
|
|
273
273
|
if (!mr.enabled) tags.push('disabled');
|
|
274
|
+
if (mr.route.vpnOnly) tags.push('vpn-only');
|
|
274
275
|
|
|
275
276
|
return {
|
|
276
277
|
...mr.route,
|
|
@@ -360,6 +361,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
360
361
|
<div style="color: #ccc; padding: 8px 0;">
|
|
361
362
|
<p>Origin: <strong style="color: #0af;">${merged.origin}</strong></p>
|
|
362
363
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
|
364
|
+
${merged.route.vpnOnly ? html`<p>Access: <strong style="color: #22c55e;">VPN only</strong></p>` : ''}
|
|
363
365
|
<p>ID: <code style="color: #888;">${merged.id}</code></p>
|
|
364
366
|
${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
|
|
365
367
|
${meta?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
|
|
@@ -491,6 +493,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
491
493
|
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
|
|
492
494
|
: '';
|
|
493
495
|
const currentTargetPort = typeof firstTarget?.port === 'number' ? String(firstTarget.port) : '';
|
|
496
|
+
const currentVpnOnly = route.vpnOnly === true;
|
|
494
497
|
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
|
|
495
498
|
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
|
|
496
499
|
|
|
@@ -518,6 +521,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
518
521
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
|
|
519
522
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
|
|
520
523
|
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${currentPreserveMatchPort}></dees-input-checkbox>
|
|
524
|
+
<dees-input-checkbox .key=${'vpnOnly'} .label=${'VPN only'} .description=${'Only VPN clients with matching target profiles can access this route'} .value=${currentVpnOnly}></dees-input-checkbox>
|
|
521
525
|
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${currentRemoteIngressEnabled}></dees-input-checkbox>
|
|
522
526
|
<div class="remoteIngressGroup" style="display: ${currentRemoteIngressEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
523
527
|
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'} .value=${currentEdgeFilter}></dees-input-list>
|
|
@@ -570,6 +574,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
570
574
|
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
|
571
575
|
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
|
572
576
|
: [];
|
|
577
|
+
const vpnOnly = Boolean(formData.vpnOnly);
|
|
573
578
|
|
|
574
579
|
const updatedRoute: any = {
|
|
575
580
|
name: formData.name,
|
|
@@ -586,6 +591,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
586
591
|
},
|
|
587
592
|
],
|
|
588
593
|
},
|
|
594
|
+
vpnOnly: vpnOnly ? true : null,
|
|
589
595
|
remoteIngress: remoteIngressEnabled
|
|
590
596
|
? {
|
|
591
597
|
enabled: true,
|
|
@@ -684,6 +690,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
684
690
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
|
|
685
691
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
|
|
686
692
|
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${false}></dees-input-checkbox>
|
|
693
|
+
<dees-input-checkbox .key=${'vpnOnly'} .label=${'VPN only'} .description=${'Only VPN clients with matching target profiles can access this route'} .value=${false}></dees-input-checkbox>
|
|
687
694
|
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${false}></dees-input-checkbox>
|
|
688
695
|
<div class="remoteIngressGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
689
696
|
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'}></dees-input-list>
|
|
@@ -736,6 +743,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
736
743
|
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
|
737
744
|
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
|
738
745
|
: [];
|
|
746
|
+
const vpnOnly = Boolean(formData.vpnOnly);
|
|
739
747
|
|
|
740
748
|
const route: any = {
|
|
741
749
|
name: formData.name,
|
|
@@ -752,6 +760,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
752
760
|
},
|
|
753
761
|
],
|
|
754
762
|
},
|
|
763
|
+
...(vpnOnly ? { vpnOnly: true } : {}),
|
|
755
764
|
...(remoteIngressEnabled
|
|
756
765
|
? {
|
|
757
766
|
remoteIngress: {
|
|
@@ -426,9 +426,7 @@ export class OpsDashboard extends DeesElement {
|
|
|
426
426
|
<dees-input-checkbox
|
|
427
427
|
.key=${'enableIdpGlobalAuth'}
|
|
428
428
|
.label=${'Allow idp.global login for this email'}
|
|
429
|
-
.description=${
|
|
430
|
-
? 'The local account remains authoritative; idp.global only verifies identity.'
|
|
431
|
-
: 'Requires DCROUTER_IDP_GLOBAL_URL before idp.global logins can work.'}
|
|
429
|
+
.description=${'Uses https://idp.global by default; the local dcrouter account and role remain authoritative.'}
|
|
432
430
|
></dees-input-checkbox>
|
|
433
431
|
</dees-form>
|
|
434
432
|
</div>
|