feat(opsserver-admin): add persisted admin bootstrap flow with optional idp.global authentication
This commit is contained in:
@@ -66,6 +66,9 @@ export class OpsDashboard extends DeesElement {
|
||||
isLoggedIn: false,
|
||||
};
|
||||
|
||||
private bootstrapStepper?: any;
|
||||
private bootstrapCheckPromise?: Promise<void>;
|
||||
|
||||
@state() accessor uiState: appstate.IUiState = {
|
||||
activeView: 'overview',
|
||||
activeSubview: null,
|
||||
@@ -336,6 +339,7 @@ export class OpsDashboard extends DeesElement {
|
||||
await (simpleLogin as any).switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
await this.ensureAdminBootstrap();
|
||||
} else {
|
||||
// Server rejected the JWT — clear state, show login
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
@@ -370,10 +374,106 @@ export class OpsDashboard extends DeesElement {
|
||||
await simpleLogin!.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
await this.ensureAdminBootstrap();
|
||||
} else {
|
||||
form!.setStatus('error', 'Login failed!');
|
||||
await domtools.convenience.smartdelay.delayFor(2000);
|
||||
form!.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAdminBootstrap(): Promise<void> {
|
||||
if (!this.loginState.identity || this.bootstrapStepper?.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (this.bootstrapCheckPromise) {
|
||||
return this.bootstrapCheckPromise;
|
||||
}
|
||||
|
||||
this.bootstrapCheckPromise = (async () => {
|
||||
try {
|
||||
const status = await appstate.getAdminBootstrapStatus();
|
||||
if (status.needsBootstrap) {
|
||||
await this.showAdminBootstrapStepper(status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Admin bootstrap status check failed:', error);
|
||||
} finally {
|
||||
this.bootstrapCheckPromise = undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
return this.bootstrapCheckPromise;
|
||||
}
|
||||
|
||||
private async showAdminBootstrapStepper(statusArg: appstate.IAdminBootstrapStatus): Promise<void> {
|
||||
const { DeesStepper } = await import('@design.estate/dees-catalog');
|
||||
this.bootstrapStepper = await DeesStepper.createAndShow({
|
||||
cancelable: false,
|
||||
steps: [
|
||||
{
|
||||
title: 'Create Persisted Admin',
|
||||
content: html`
|
||||
<div style="display: grid; gap: 16px; color: var(--dees-color-text-secondary); font-size: 14px; line-height: 1.5;">
|
||||
<p style="margin: 0;">
|
||||
This router is currently using the temporary bootstrap admin. Create the first persisted admin account to continue.
|
||||
</p>
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'email'} .label=${'Admin email'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'name'} .label=${'Display name'}></dees-input-text>
|
||||
<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=${statusArg.idpGlobalConfigured
|
||||
? 'The local account remains authoritative; idp.global only verifies identity.'
|
||||
: 'Requires DCROUTER_IDP_GLOBAL_URL before idp.global logins can work.'}
|
||||
></dees-input-checkbox>
|
||||
</dees-form>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create admin',
|
||||
action: async (stepperArg: any) => {
|
||||
const form = stepperArg.shadowRoot?.querySelector('.selected dees-form') as any;
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
const email = String(formData.email || '').trim();
|
||||
const name = String(formData.name || '').trim();
|
||||
const password = String(formData.password || '');
|
||||
const passwordConfirm = String(formData.passwordConfirm || '');
|
||||
|
||||
if (!email || !password) {
|
||||
form.setStatus?.('error', 'Email and password are required.');
|
||||
return;
|
||||
}
|
||||
if (password !== passwordConfirm) {
|
||||
form.setStatus?.('error', 'Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
form.setStatus?.('pending', 'Creating persisted admin...');
|
||||
await appstate.createInitialAdminUser({
|
||||
email,
|
||||
name,
|
||||
password,
|
||||
enableIdpGlobalAuth: Boolean(formData.enableIdpGlobalAuth),
|
||||
});
|
||||
form.setStatus?.('success', 'Persisted admin created.');
|
||||
await stepperArg.destroy();
|
||||
this.bootstrapStepper = undefined;
|
||||
await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
|
||||
} catch (error) {
|
||||
form.setStatus?.('error', error instanceof Error ? error.message : 'Failed to create admin.');
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user