feat(app): add MFA and tsdocker release

This commit is contained in:
2026-05-19 06:20:38 +00:00
parent ddf4861e95
commit 1e563115d0
23 changed files with 1939 additions and 211 deletions
+131 -1
View File
@@ -77,6 +77,18 @@ export class IdpAccountContent extends DeesElement {
@state()
private accessor passportEnrollment: plugins.idpCatalog.IIdpAdminPassportEnrollment | null = null;
@state()
private accessor totpEnabled = false;
@state()
private accessor backupCodesRemaining = 0;
@state()
private accessor totpEnrollment: any = null;
@state()
private accessor passkeys: any[] = [];
@state()
private accessor credentialMessage = '';
@@ -124,6 +136,10 @@ export class IdpAccountContent extends DeesElement {
.adminApps=${this.adminApps}
.passportDevices=${this.passportDevices}
.passportEnrollment=${this.passportEnrollment}
.totpEnabled=${this.totpEnabled}
.backupCodesRemaining=${this.backupCodesRemaining}
.totpEnrollment=${this.totpEnrollment}
.passkeys=${this.passkeys}
.credentialMessage=${this.credentialMessage}
@idp-admin-navigate=${this.handleAdminNavigate}
@idp-admin-org-select=${this.handleOrgSelect}
@@ -136,6 +152,12 @@ export class IdpAccountContent extends DeesElement {
@idp-admin-password-change=${this.handlePasswordChange}
@idp-admin-passport-enroll=${this.handlePassportEnroll}
@idp-admin-passport-revoke=${this.handlePassportRevoke}
@idp-admin-totp-start=${this.handleTotpStart}
@idp-admin-totp-verify=${this.handleTotpVerify}
@idp-admin-totp-disable=${this.handleTotpDisable}
@idp-admin-backup-codes-regenerate=${this.handleBackupCodesRegenerate}
@idp-admin-passkey-register=${this.handlePasskeyRegister}
@idp-admin-passkey-revoke=${this.handlePasskeyRevoke}
@idp-admin-member-invite=${this.handleMemberInvite}
@idp-admin-member-remove=${this.handleMemberRemove}
@idp-admin-member-roles-update=${this.handleMemberRolesUpdate}
@@ -495,6 +517,26 @@ export class IdpAccountContent extends DeesElement {
}));
}
private async loadMfaStatus(idpStateArg: IdpState, jwtArg: string) {
const request = idpStateArg.idpClient.typedsocket.createTypedRequest<any>('getMfaStatus');
const response = await request.fire({ jwt: jwtArg });
return {
totpEnabled: Boolean(response.totpEnabled),
backupCodesRemaining: Number(response.backupCodesRemaining || 0),
passkeys: (response.passkeys || []).map((passkeyArg: any) => ({
id: passkeyArg.id,
label: passkeyArg.data.label,
credentialId: passkeyArg.data.credentialId,
status: passkeyArg.data.status,
backedUp: passkeyArg.data.backedUp,
deviceType: passkeyArg.data.deviceType,
transports: passkeyArg.data.transports || [],
createdAt: passkeyArg.data.createdAt,
lastUsedAt: passkeyArg.data.lastUsedAt,
})),
};
}
private async loadAdminShellData() {
const currentRun = ++this.dataLoadRun;
this.dataLoading = true;
@@ -506,7 +548,7 @@ export class IdpAccountContent extends DeesElement {
const selectedOrg = this.getSelectedOrganization();
const orgId = selectedOrg?.id || '';
const [sessions, activities, members, invitations, orgApps, adminApps, passportDevices] = await Promise.all([
const [sessions, activities, members, invitations, orgApps, adminApps, passportDevices, mfaStatus] = await Promise.all([
this.loadSessions(idpState, jwt).catch((error) => {
console.error('Error loading sessions:', error);
return this.sessions;
@@ -535,6 +577,14 @@ export class IdpAccountContent extends DeesElement {
console.error('Error loading passport devices:', error);
return this.passportDevices;
}),
this.loadMfaStatus(idpState, jwt).catch((error) => {
console.error('Error loading MFA status:', error);
return {
totpEnabled: this.totpEnabled,
backupCodesRemaining: this.backupCodesRemaining,
passkeys: this.passkeys,
};
}),
]);
if (currentRun !== this.dataLoadRun) {
@@ -548,6 +598,9 @@ export class IdpAccountContent extends DeesElement {
this.orgApps = orgApps;
this.adminApps = adminApps;
this.passportDevices = passportDevices;
this.totpEnabled = mfaStatus.totpEnabled;
this.backupCodesRemaining = mfaStatus.backupCodesRemaining;
this.passkeys = mfaStatus.passkeys;
} catch (error) {
console.error('Error loading admin shell data:', error);
if (currentRun === this.dataLoadRun) {
@@ -657,6 +710,83 @@ export class IdpAccountContent extends DeesElement {
});
}
private async handleTotpStart() {
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const request = idpState.idpClient.typedsocket.createTypedRequest<any>('startTotpEnrollment');
this.totpEnrollment = await request.fire({ jwt: await idpState.idpClient.getJwt() });
this.credentialMessage = 'Authenticator app enrollment started.';
});
}
private async handleTotpVerify(eventArg: CustomEvent<any>) {
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const request = idpState.idpClient.typedsocket.createTypedRequest<any>('finishTotpEnrollment');
const response = await request.fire({
jwt: await idpState.idpClient.getJwt(),
credentialId: eventArg.detail.credentialId,
code: eventArg.detail.code,
});
this.totpEnrollment = null;
this.credentialMessage = `Authenticator app enabled. Save these backup codes now: ${(response.backupCodes || []).join(', ')}`;
});
}
private async handleTotpDisable(eventArg: CustomEvent<any>) {
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const request = idpState.idpClient.typedsocket.createTypedRequest<any>('disableTotp');
await request.fire({ jwt: await idpState.idpClient.getJwt(), code: eventArg.detail.code });
this.totpEnrollment = null;
this.credentialMessage = 'Authenticator app disabled.';
});
}
private async handleBackupCodesRegenerate(eventArg: CustomEvent<any>) {
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const request = idpState.idpClient.typedsocket.createTypedRequest<any>('regenerateBackupCodes');
const response = await request.fire({ jwt: await idpState.idpClient.getJwt(), code: eventArg.detail.code });
this.credentialMessage = `New backup codes generated. Save them now: ${(response.backupCodes || []).join(', ')}`;
});
}
private async handlePasskeyRegister(eventArg: CustomEvent<any>) {
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const startRequest = idpState.idpClient.typedsocket.createTypedRequest<any>('startPasskeyRegistration');
const finishRequest = idpState.idpClient.typedsocket.createTypedRequest<any>('finishPasskeyRegistration');
const startResponse = await startRequest.fire({
jwt: await idpState.idpClient.getJwt(),
label: eventArg.detail.label,
});
const registrationResponse = await plugins.simpleWebAuthnBrowser.startRegistration({
optionsJSON: startResponse.options,
});
await finishRequest.fire({
jwt: await idpState.idpClient.getJwt(),
challengeId: startResponse.challengeId,
label: eventArg.detail.label,
response: registrationResponse,
});
this.credentialMessage = 'Passkey registered.';
});
}
private async handlePasskeyRevoke(eventArg: CustomEvent<any>) {
const passkey = this.passkeys.find((passkeyArg) => passkeyArg.id === eventArg.detail.passkeyId);
if (!passkey || !confirm(`Revoke passkey ${passkey.label}?`)) {
return;
}
await this.runAdminAction(async () => {
const idpState = await IdpState.getSingletonInstance();
const request = idpState.idpClient.typedsocket.createTypedRequest<any>('revokePasskey');
await request.fire({ jwt: await idpState.idpClient.getJwt(), passkeyId: eventArg.detail.passkeyId });
this.credentialMessage = 'Passkey revoked.';
});
}
private async handleMemberInvite() {
const selectedOrg = this.getSelectedOrganization();
if (!selectedOrg) {