passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
504 lines (459 loc) • 17.4 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 3.3.0
*/
import React from "react";
import PropTypes from "prop-types";
import { withTranslation } from "react-i18next";
import DisplayInFormMenuItem from "./DisplayInFormMenuItem";
import { withAppContext } from "../../../shared/context/AppContext/AppContext";
import { SecretGenerator } from "../../../shared/lib/SecretGenerator/SecretGenerator";
import { withPasswordPolicies } from "../../../shared/context/PasswordPoliciesContext/PasswordPoliciesContext";
import { withResourceTypesLocalStorage } from "../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext";
import { withMetadataTypesSettingsLocalStorage } from "../../../shared/context/MetadataTypesSettingsLocalStorageContext/MetadataTypesSettingsLocalStorageContext";
import MetadataTypesSettingsEntity from "../../../shared/models/entity/metadata/metadataTypesSettingsEntity";
import ResourceTypesCollection from "../../../shared/models/entity/resourceType/resourceTypesCollection";
import {
RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG,
RESOURCE_TYPE_V5_DEFAULT_SLUG,
} from "../../../shared/models/entity/resourceType/resourceTypeSchemasDefinition";
import DiceSVG from "../../../img/svg/dice.svg";
import AddSVG from "../../../img/svg/add.svg";
import SearchSVG from "../../../img/svg/search.svg";
import ResourceIcon from "../../../shared/components/Icons/ResourceIcon";
import { withMetadataKeysSettingsLocalStorage } from "../../../shared/context/MetadataKeysSettingsLocalStorageContext/MetadataKeysSettingsLocalStorageContext";
import MetadataKeysSettingsEntity from "../../../shared/models/entity/metadata/metadataKeysSettingsEntity";
import Logger from "../../../shared/utils/logger";
/** The maximum length of visibility of a generated password */
const TRUNCATED_GENERATED_PASSWORD_MAX_LENGTH = 15;
/**
* This component is a menu integrated into a target web page which includes
* an identified authentication form. After the call-to-action performed,
* the menu proposes different available actions given the situation
*/
class DisplayInFormMenu extends React.Component {
/**
* Default constructor
* @param props Component props
*/
constructor(props) {
super(props);
this.state = this.defaultState;
this.createRefs();
this.bindCallbacks();
}
/**
* Create DOM nodes or React elements references in order to be able to access them programmatically.
*/
createRefs() {
this.inFormMenuRef = React.createRef();
}
/**
* Whenever the component is mounted
*/
componentDidMount() {
this.handleDisplayConfigurationReceivedEvent();
document.addEventListener("click", this.handleInFormMenuClickEvent, { capture: true });
}
componentWillUnmount() {
document.removeEventListener("click", this.handleInFormMenuClickEvent, { capture: true });
}
/**
* Handle click events on in form menu. close the component if the click occurred outside of the component.
* @param {ReactEvent} event The event
*/
handleInFormMenuClickEvent(event) {
// Prevent close when the user click on an element of the in form menu
if (this.inFormMenuRef.current.contains(event.target)) {
return;
}
this.props.context.port.request("passbolt.in-form-menu.close");
}
/**
* Binds methods callbacks
*/
bindCallbacks() {
this.handleInFormMenuClickEvent = this.handleInFormMenuClickEvent.bind(this);
this.handleCreateNewCredentialsRequestedEvent = this.handleCreateNewCredentialsRequestedEvent.bind(this);
this.handleSaveCredentialsRequestedEvent = this.handleSaveCredentialsRequestedEvent.bind(this);
this.handleBrowseCredentialsRequestedEvent = this.handleBrowseCredentialsRequestedEvent.bind(this);
this.handleUseSuggestedResourceRequestedEvent = this.handleUseSuggestedResourceRequestedEvent.bind(this);
this.handleGeneratePasswordRequestedEvent = this.handleGeneratePasswordRequestedEvent.bind(this);
}
/**
* The component default state
*/
get defaultState() {
return {
configuration: {
inputType: null, // Input inputType attached to the menu
inputValue: null, // Input inputValue attached to the menu
suggestedResources: null, // Suggested resources to display
}, // The display configuration of the menu
generatedPassword: null, // Generated password
resourceIdProcessing: null, // The resource id processing
};
}
/**
* Returns true if the component has a display configuration
*/
get hasConfiguration() {
return Boolean(this.state.configuration) && Boolean(this.state.configuration.inputType);
}
/**
* Returns true if the component has a "username" display configuration
*/
get isUsernameConfiguration() {
return this.state.configuration.inputType === "username";
}
/**
* Returns true if the username field has been (
*/
get isUsernameFilled() {
return this.state.configuration.inputValue && this.state.configuration.inputValue !== "";
}
/**
* Returns true if the component has a "password" display configuration
*/
get isPasswordConfiguration() {
return this.state.configuration.inputType === "password";
}
/**
* Returns true if the password field has been (
*/
get isPasswordFilled() {
return this.state.configuration.inputValue && this.state.configuration.inputValue !== "";
}
/**
* Returns true if the component has a "otp" display configuration
*/
get isOTPConfiguration() {
return this.state.configuration.inputType === "otp";
}
/**
* Returns the list of the menu items to display
*/
get items() {
if (this.hasConfiguration) {
if (this.isUsernameConfiguration) {
return this.isUsernameFilled ? this.filledUsernameMenuItems : this.emptyUsernameMenuItems;
} else if (this.isPasswordConfiguration) {
return this.isPasswordFilled ? this.filledPasswordMenuItems : this.emptyPasswordMenuItems;
} else if (this.isOTPConfiguration) {
return this.OTPMenuItems;
}
}
return [];
}
/**
* Returns the truncated version of generated password
*/
get truncatedGeneratedPassword() {
if (this.state.generatedPassword) {
const uplimitIndex = Math.min(
TRUNCATED_GENERATED_PASSWORD_MAX_LENGTH,
Math.floor(this.state.generatedPassword.length / 2),
);
return this.state.generatedPassword.substring(0, uplimitIndex);
}
return this.state.generatedPassword;
}
/**
* Returns the list of menu items in case of filled username configuration
* @return {JSX.Element[]}
*/
get filledUsernameMenuItems() {
const usernameMenuItems = this.suggestedResourcesItems;
if (this.hasMetadataTypesSettings() && this.canCreatePassword()) {
usernameMenuItems.push(this.saveAsNewCredentialItem);
}
usernameMenuItems.push(this.browseCredentialsItem);
return usernameMenuItems;
}
/**
* Returns the list of menu items in case of empty username configuration
* @return {JSX.Element[]}
*/
get emptyUsernameMenuItems() {
const usernameMenuItems = this.suggestedResourcesItems;
if (this.hasMetadataTypesSettings() && this.canCreatePassword()) {
usernameMenuItems.push(this.createNewCredentialItem);
}
usernameMenuItems.push(this.browseCredentialsItem);
return usernameMenuItems;
}
/**
* Returns the list of menu items in case of filled password configuration
* @return {JSX.Element[]}
*/
get filledPasswordMenuItems() {
const passwordMenuItems = this.suggestedResourcesItems;
if (this.hasMetadataTypesSettings() && this.canCreatePassword()) {
passwordMenuItems.push(this.saveAsNewCredentialItem);
}
passwordMenuItems.push(this.browseCredentialsItem);
return passwordMenuItems;
}
/**
* Returns the list of menu items in case of empty password configuration
* @return {JSX.Element[]}
*/
get emptyPasswordMenuItems() {
const passwordMenuItems = this.suggestedResourcesItems;
if (this.hasMetadataTypesSettings() && this.canCreatePassword()) {
passwordMenuItems.push(this.generateNewPasswordItem);
passwordMenuItems.push(this.createNewCredentialItem);
}
passwordMenuItems.push(this.browseCredentialsItem);
return passwordMenuItems;
}
/**
* Returns the list of menu items in case of OTP configuration
* @return {JSX.Element[]}
*/
get OTPMenuItems() {
return [...this.suggestedResourcesItems, this.browseCredentialsItem];
}
/**
* Return the generate a new credential menu item.
* @returns {JSX.Element}
*/
get generateNewPasswordItem() {
const disabled = this.state.generatedPassword === null;
return (
<DisplayInFormMenuItem
key="generate-password"
onClick={this.handleGeneratePasswordRequestedEvent}
title={this.props.t("Generate a new password securely")}
subtitle={
<span className="in-form-menu-item-content-subheader-password">{this.truncatedGeneratedPassword}</span>
}
description={this.props.t("You will be able to save it after submitting")}
icon={<DiceSVG />}
disabled={disabled}
/>
);
}
/**
* Return the save as new credential menu item.
* @returns {JSX.Element}
*/
get saveAsNewCredentialItem() {
return (
<DisplayInFormMenuItem
key="save-credentials"
onClick={this.handleSaveCredentialsRequestedEvent}
title={this.props.t("Save as new credential")}
description={this.props.t("Save the data entered as a new credential")}
icon={<AddSVG />}
/>
);
}
/**
* Return the create a new credential menu item.
* @returns {JSX.Element}
*/
get createNewCredentialItem() {
return (
<DisplayInFormMenuItem
key="create-new-credentials"
onClick={this.handleCreateNewCredentialsRequestedEvent}
title={this.props.t("Create a new credential")}
description={this.props.t("Create and customize it yourself")}
icon={<AddSVG />}
/>
);
}
/**
* Return the browse credentials menu item.
* @returns {JSX.Element}
*/
get browseCredentialsItem() {
return (
<DisplayInFormMenuItem
key="browse-credentials"
onClick={this.handleBrowseCredentialsRequestedEvent}
title={this.props.t("Browse credentials")}
description={this.props.t("Search among available credentials")}
icon={<SearchSVG />}
/>
);
}
/**
* Returns the list of suggested resources menu items
*/
get suggestedResourcesItems() {
const suggestedResources = (this.state.configuration && this.state.configuration.suggestedResources) || [];
return suggestedResources.reduce(
(menuItems, resource) =>
menuItems.concat([
<DisplayInFormMenuItem
key={resource.id}
onClick={() => this.handleUseSuggestedResourceRequestedEvent(resource.id)}
processing={this.state.resourceIdProcessing === resource.id}
disabled={this.state.resourceIdProcessing === resource.id}
title={resource.metadata.name}
description={resource.metadata?.username}
icon={<ResourceIcon resource={resource} />}
showTimer={this.isOTPConfiguration}
/>,
]),
[],
);
}
/**
* Whenever the display configuration of the menu is received
*/
async handleDisplayConfigurationReceivedEvent() {
const configuration = await this.props.context.port.request("passbolt.in-form-menu.init");
this.setState({ configuration });
if (!this.isPasswordFilled) {
// Pre-generate the password
this.generateSecret();
}
}
/**
* Generates a new secret and store it in the component state.
* @returns {Promise<void>}
*/
async generateSecret() {
const passwordPolicies = await this.props.context.port.request("passbolt.password-policies.get", true);
const generatedPassword = SecretGenerator.generate(passwordPolicies);
this.setState({ generatedPassword });
}
/**
* Whenever the user requests to create a new credential
*/
async handleCreateNewCredentialsRequestedEvent() {
const isApplicationOverlaid = await this.handleApplicationOverlaidRequestedEvent();
if (isApplicationOverlaid) {
Logger.error("Overlap detected, action interrupted for safety reasons");
return;
}
this.props.context.port.request("passbolt.in-form-menu.create-new-credentials");
}
/**
* Whenever the user requests to browse credentials
*/
async handleBrowseCredentialsRequestedEvent() {
const isApplicationOverlaid = await this.handleApplicationOverlaidRequestedEvent();
if (isApplicationOverlaid) {
Logger.error("Overlap detected, action interrupted for safety reasons");
return;
}
this.props.context.port.request("passbolt.in-form-menu.browse-credentials");
}
/**
* Whenever the user requests to save the credentials
*/
async handleSaveCredentialsRequestedEvent() {
const isApplicationOverlaid = await this.handleApplicationOverlaidRequestedEvent();
if (isApplicationOverlaid) {
Logger.error("Overlap detected, action interrupted for safety reasons");
return;
}
this.props.context.port.request("passbolt.in-form-menu.save-credentials");
}
/**
* Whenever the user requests to use the suggested resource as credentials in the current page
* @param resourceId
*/
async handleUseSuggestedResourceRequestedEvent(resourceId) {
const isApplicationOverlaid = await this.handleApplicationOverlaidRequestedEvent();
if (isApplicationOverlaid) {
Logger.error("Overlap detected, action interrupted for safety reasons");
return;
}
this.setState({ resourceIdProcessing: resourceId });
try {
await this.props.context.port.request("passbolt.in-form-menu.use-suggested-resource", resourceId);
} catch (error) {
console.error(error);
}
this.setState({ resourceIdProcessing: null });
}
/**
* Whenever the user request to generate a password for the current page
*/
async handleGeneratePasswordRequestedEvent() {
const isApplicationOverlaid = await this.handleApplicationOverlaidRequestedEvent();
if (isApplicationOverlaid) {
Logger.error("Overlap detected, action interrupted for safety reasons");
return;
}
this.props.context.port.request("passbolt.in-form-menu.fill-password", this.state.generatedPassword);
}
/**
* Handle the check if application is overlaid
* @return {Promise<boolean>}
*/
handleApplicationOverlaidRequestedEvent() {
return this.props.context.port.request(
"passbolt.in-form-menu.is-application-overlaid",
this.props.context.applicationId,
);
}
/**
* Has metadata types settings
* @returns {boolean}
*/
hasMetadataTypesSettings() {
return Boolean(this.props.metadataTypeSettings);
}
/**
* Can create password
* @returns {boolean}
*/
canCreatePassword() {
if (this.props.metadataTypeSettings.isDefaultResourceTypeV5) {
const isMetadataSharedKeyEnforced = !this.props.metadataKeysSettings?.allowUsageOfPersonalKeys;
const userHasMissingKeys = this.props.context.loggedInUser?.missing_metadata_key_ids?.length > 0;
return (
!(isMetadataSharedKeyEnforced && userHasMissingKeys) &&
this.props.resourceTypes?.hasOneWithSlug(RESOURCE_TYPE_V5_DEFAULT_SLUG)
);
} else if (this.props.metadataTypeSettings.isDefaultResourceTypeV4) {
return this.props.resourceTypes?.hasOneWithSlug(RESOURCE_TYPE_PASSWORD_AND_DESCRIPTION_SLUG);
} else {
return false;
}
}
/**
* Render the component
*/
render() {
if (!this.hasConfiguration) {
return null;
}
const items = this.items;
return (
<div className={`in-form-menu ${items.length > 3 && "in-form-menu--scrollable"}`} ref={this.inFormMenuRef}>
{items}
</div>
);
}
}
DisplayInFormMenu.propTypes = {
context: PropTypes.any, // The application context
t: PropTypes.func, // The translation function
resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection
metadataTypeSettings: PropTypes.instanceOf(MetadataTypesSettingsEntity), // The metadata type settings
metadataKeysSettings: PropTypes.instanceOf(MetadataKeysSettingsEntity), // The metadata key settings
passwordPoliciesContext: PropTypes.object, // The password policy context
};
export default withAppContext(
withResourceTypesLocalStorage(
withMetadataTypesSettingsLocalStorage(
withMetadataKeysSettingsLocalStorage(withPasswordPolicies(withTranslation("common")(DisplayInFormMenu))),
),
),
);