feat(opsserver-admin): add persisted admin bootstrap flow with optional idp.global authentication
This commit is contained in:
@@ -10,6 +10,8 @@ export interface ILoginState {
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export type IAdminBootstrapStatus = interfaces.requests.IReq_GetAdminBootstrapStatus['response'];
|
||||
|
||||
export interface IStatsState {
|
||||
serverStats: interfaces.data.IServerStats | null;
|
||||
emailStats: interfaces.data.IEmailStats | null;
|
||||
@@ -312,7 +314,11 @@ export const routeManagementStatePart = await appState.getStatePart<IRouteManage
|
||||
export interface IUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role: string;
|
||||
status?: 'active' | 'disabled';
|
||||
authSources?: Array<'local' | 'idp.global'>;
|
||||
}
|
||||
|
||||
export interface IUsersState {
|
||||
@@ -351,6 +357,7 @@ const getActionContext = (): IActionContext => {
|
||||
export const loginAction = loginStatePart.createAction<{
|
||||
username: string;
|
||||
password: string;
|
||||
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
||||
}>(async (statePartArg, dataArg): Promise<ILoginState> => {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
||||
@@ -360,6 +367,7 @@ export const loginAction = loginStatePart.createAction<{
|
||||
const response = await typedRequest.fire({
|
||||
username: dataArg.username,
|
||||
password: dataArg.password,
|
||||
authSource: dataArg.authSource,
|
||||
});
|
||||
|
||||
if (response.identity) {
|
||||
@@ -375,6 +383,47 @@ export const loginAction = loginStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
export async function getAdminBootstrapStatus(): Promise<IAdminBootstrapStatus> {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetAdminBootstrapStatus
|
||||
>('/typedrequest', 'getAdminBootstrapStatus');
|
||||
|
||||
return request.fire({});
|
||||
}
|
||||
|
||||
export async function createInitialAdminUser(optionsArg: {
|
||||
email: string;
|
||||
name?: string;
|
||||
password: string;
|
||||
enableIdpGlobalAuth?: boolean;
|
||||
}) {
|
||||
const context = getActionContext();
|
||||
if (!context.identity) {
|
||||
throw new Error('No identity available for admin bootstrap');
|
||||
}
|
||||
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateInitialAdminUser
|
||||
>('/typedrequest', 'createInitialAdminUser');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
email: optionsArg.email,
|
||||
name: optionsArg.name,
|
||||
password: optionsArg.password,
|
||||
enableIdpGlobalAuth: optionsArg.enableIdpGlobalAuth,
|
||||
});
|
||||
|
||||
if (response.identity) {
|
||||
loginStatePart.setState({
|
||||
identity: response.identity,
|
||||
isLoggedIn: true,
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Logout Action — always clears state, even if identity is expired/missing
|
||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -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