vault/ui/app/components/totp-edit.js
lane-wetmore 55674ddd1f
UI: Add TOTP secrets engine (#29751)
* TOTP secrets in the web UI
---------

Co-authored-by: Moritz Pflanzer <moritz@pflanzer.eu>
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Co-authored-by: Shannon Roberts (Beagin) <beagins@users.noreply.github.com>
2025-04-17 12:59:45 -05:00

113 lines
3.3 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
/**
* @module TotpEdit
* `TotpEdit` is a component that allows you to create, view or delete a TOTP key.
* When creating a key if `generate` and `exported` are true then after a successful save the UI renders a QR code for the generated key.
* @example
* <TotpEdit @model={{this.model}} @mode={{this.mode}} />
*
* @param {object} model - The totp key ember data model.
* @param {string} mode - The mode to render. Either 'create' or 'show'.
*/
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
export default class TotpEdit extends Component {
@service router;
@service flashMessages;
@tracked hasGenerated = false;
@tracked invalidFormAlert = '';
@tracked modelValidations;
successCallback;
get defaultKeyFormFields() {
const shared = ['name', 'generate', 'issuer', 'accountName'];
const generated = [...shared, 'exported'];
const nonGenerated = [...shared, 'url', 'key'];
return this.args.model.generate ? generated : nonGenerated;
}
get groups() {
const { generate } = this.args.model;
const groups = {
'TOTP Code Options': ['algorithm', 'digits', 'period'],
};
if (generate) {
groups['Provider Options'] = ['keySize', 'skew', 'qrSize'];
}
return groups;
}
transitionToRoute() {
this.router.transitionTo(...arguments);
}
@action
reset() {
const { name } = this.args.model;
this.args.model.unloadRecord();
this.transitionToRoute(SHOW_ROUTE, name);
}
@action
async deleteKey() {
try {
const { id } = this.args.model;
await this.args.model.destroyRecord();
this.transitionToRoute(LIST_ROOT_ROUTE);
this.flashMessages.success(`${id} was successfully deleted.`);
} catch (err) {
this.flashMessages.danger(errorMessage(err));
}
}
createKey = task(
waitFor(async (event) => {
event.preventDefault();
const { isValid, state, invalidFormMessage } = this.args.model.validate();
this.modelValidations = isValid ? null : state;
this.invalidFormAlert = invalidFormMessage;
if (!isValid) return;
try {
const allFields = [...this.defaultKeyFormFields, ...Object.values(this.groups).flat()];
await this.args.model.save({
adapterOptions: {
keyFormFields: allFields,
},
});
const { generate, exported } = this.args.model;
if (generate && exported) {
// stay in this template and show QR code returned from response
this.hasGenerated = true;
} else {
// nothing is returned from response, transition to key details route
this.transitionToRoute(SHOW_ROUTE, this.args.model.name);
}
} catch (err) {
// err will display via model state
return;
}
this.flashMessages.success('Successfully created key.');
})
);
}