feat(opsserver): add admin user create/delete management and default hosted idp.global auth support

This commit is contained in:
2026-05-19 17:06:50 +00:00
parent 0b01a4c26b
commit 7986d01245
14 changed files with 436 additions and 27 deletions
+139 -1
View File
@@ -116,12 +116,31 @@ export class OpsViewUsers extends DeesElement {
.showColumnFilters=${true}
.displayFunction=${(user: appstate.IUser) => ({
ID: html`<span class="userIdCell">${user.id}</span>`,
Username: user.username,
Email: user.email || user.username,
Name: user.name || '',
Role: this.renderRoleBadge(user.role),
Status: user.status || 'active',
Auth: (user.authSources || []).join(', ') || 'bootstrap',
Session: user.id === currentUserId
? html`<span class="sessionBadge">current</span>`
: '',
})}
.dataActions=${[
{
name: 'Create User',
iconName: 'lucide:userPlus',
type: ['header'],
actionFunc: async () => await this.showCreateUserDialog(),
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
await this.showDeleteUserDialog(actionData.item as appstate.IUser);
},
},
]}
></dees-table>
</div>
`;
@@ -132,6 +151,125 @@ export class OpsViewUsers extends DeesElement {
return html`<span class="roleBadge ${cls}">${role}</span>`;
}
private async showCreateUserDialog(): Promise<void> {
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: 'Create User',
content: html`
<dees-form>
<dees-input-text .key=${'email'} .label=${'Email'} .required=${true}></dees-input-text>
<dees-input-text .key=${'name'} .label=${'Display name'}></dees-input-text>
<dees-input-dropdown
.key=${'role'}
.label=${'Role'}
.options=${[
{ option: 'User', key: 'user' },
{ option: 'Admin', key: 'admin' },
]}
.selectedOption=${{ option: 'User', key: 'user' }}
.required=${true}
></dees-input-dropdown>
<dees-input-text .key=${'password'} .label=${'Password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
<dees-input-text .key=${'passwordConfirm'} .label=${'Confirm password'} .required=${true} .isPasswordBool=${true}></dees-input-text>
<dees-input-checkbox
.key=${'enableIdpGlobalAuth'}
.label=${'Allow idp.global login for this email'}
.description=${'Uses https://idp.global by default; the local dcrouter account and role remain authoritative.'}
></dees-input-checkbox>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Create',
iconName: 'lucide:userPlus',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
if (!form) return;
const data = await form.collectFormData();
const email = String(data.email || '').trim();
const name = String(data.name || '').trim();
const password = String(data.password || '');
const passwordConfirm = String(data.passwordConfirm || '');
const roleValue = String(data.role?.key ?? data.role ?? 'user');
if (!email || !password) {
form.setStatus?.('error', 'Email and password are required.');
return;
}
if (password !== passwordConfirm) {
form.setStatus?.('error', 'Passwords do not match.');
return;
}
form.setStatus?.('pending', 'Creating user...');
await appstate.usersStatePart.dispatchAction(appstate.createUserAction, {
email,
name,
role: roleValue === 'admin' ? 'admin' : 'user',
password,
enableIdpGlobalAuth: Boolean(data.enableIdpGlobalAuth),
});
const state = appstate.usersStatePart.getState();
if (state?.error) {
form.setStatus?.('error', state.error);
return;
}
DeesToast.show({ message: `User created for ${email}`, type: 'success', duration: 3000 });
await modalArg.destroy();
},
},
],
});
}
private async showDeleteUserDialog(userArg: appstate.IUser): Promise<void> {
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
const currentUserId = this.loginState.identity?.userId;
if (userArg.id === currentUserId) {
DeesToast.show({ message: 'You cannot delete the current user.', type: 'error', duration: 4000 });
return;
}
await DeesModal.createAndShow({
heading: 'Delete User',
content: html`
<div style="padding: 8px 0; font-size: 14px; line-height: 1.5;">
<p>Delete <strong>${userArg.email || userArg.username}</strong>?</p>
<p style="color: #f59e0b; margin-top: 12px;">This removes the local dcrouter account and cannot be undone.</p>
</div>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Delete',
iconName: 'lucide:trash2',
action: async (modalArg: any) => {
await appstate.usersStatePart.dispatchAction(appstate.deleteUserAction, userArg.id);
const state = appstate.usersStatePart.getState();
if (state?.error) {
DeesToast.show({ message: state.error, type: 'error', duration: 4000 });
return;
}
DeesToast.show({ message: 'User deleted.', type: 'success', duration: 3000 });
await modalArg.destroy();
},
},
],
});
}
async firstUpdated() {
if (this.loginState.isLoggedIn) {
await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);