passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
748 lines (695 loc) • 25.2 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 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) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.13.0
*/
import React from "react";
import PropTypes from "prop-types";
import { withAppContext } from "../../../../shared/context/AppContext/AppContext";
import { withDialog } from "../../../contexts/DialogContext";
import ContextualMenuWrapper from "../../Common/ContextualMenu/ContextualMenuWrapper";
import EditResource from "../EditResource/EditResource";
import ShareDialog from "../../Share/ShareDialog";
import { withActionFeedback } from "../../../contexts/ActionFeedbackContext";
import DeleteResource from "../DeleteResource/DeleteResource";
import { resourceLinkAuthorizedProtocols, withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext";
import sanitizeUrl, { urlProtocols } from "../../../lib/Sanitize/sanitizeUrl";
import { Trans, withTranslation } from "react-i18next";
import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration";
import { withRbac } from "../../../../shared/context/Rbac/RbacContext";
import { withProgress } from "../../../contexts/ProgressContext";
import { TotpCodeGeneratorService } from "../../../../shared/services/otp/TotpCodeGeneratorService";
import { withPasswordExpiry } from "../../../contexts/PasswordExpirySettingsContext";
import { formatDateForApi } from "../../../../shared/utils/dateUtils";
import { DateTime } from "luxon";
import PasswordExpiryDialog from "../PasswordExpiryDialog/PasswordExpiryDialog";
import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext";
import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection";
import OwnedByMeIcon from "../../../../img/svg/owned_by_me.svg";
import KeyIcon from "../../../../img/svg/key.svg";
import GlobeIcon from "../../../../img/svg/globe.svg";
import LinkIcon from "../../../../img/svg/link.svg";
import ShareIcon from "../../../../img/svg/share.svg";
import EditIcon from "../../../../img/svg/edit.svg";
import DeleteIcon from "../../../../img/svg/delete.svg";
import ClockIcon from "../../../../img/svg/clock.svg";
import CalendarIcon from "../../../../img/svg/calendar.svg";
import TotpIcon from "../../../../img/svg/totp.svg";
import GoIcon from "../../../../img/svg/go.svg";
import HistoryIcon from "../../../../img/svg/history.svg";
import { withClipboard } from "../../../contexts/Clipboard/ManagedClipboardServiceProvider";
import ActionAbortedMissingMetadataKeys from "../../Metadata/ActionAbortedMissingMetadataKeys/ActionAbortedMissingMetadataKeys";
import { withMetadataKeysSettingsLocalStorage } from "../../../../shared/context/MetadataKeysSettingsLocalStorageContext/MetadataKeysSettingsLocalStorageContext";
import MetadataKeysSettingsEntity from "../../../../shared/models/entity/metadata/metadataKeysSettingsEntity";
import Logger from "../../../../shared/utils/logger";
import { withSecretRevisionsSettings } from "../../../../shared/context/SecretRevisionSettingsContext/SecretRevisionsSettingsContext";
import SecretRevisionsSettingsEntity from "../../../../shared/models/entity/secretRevision/secretRevisionsSettingsEntity";
import DisplayResourceSecretHistory from "../../SecretHistory/DisplayResourceSecretHistory";
class DisplayResourcesListContextualMenu extends React.Component {
/**
* Constructor
* Initialize state and bind methods
*/
constructor(props) {
super(props);
this.bindCallbacks();
}
/**
* Bind callbacks methods
*/
bindCallbacks() {
this.handleEditClickEvent = this.handleEditClickEvent.bind(this);
this.handleShareClickEvent = this.handleShareClickEvent.bind(this);
this.handleUsernameClickEvent = this.handleUsernameClickEvent.bind(this);
this.handleUriClickEvent = this.handleUriClickEvent.bind(this);
this.handlePermalinkClickEvent = this.handlePermalinkClickEvent.bind(this);
this.handlePasswordClickEvent = this.handlePasswordClickEvent.bind(this);
this.handleTotpClickEvent = this.handleTotpClickEvent.bind(this);
this.handleDeleteClickEvent = this.handleDeleteClickEvent.bind(this);
this.handleGoToResourceUriClick = this.handleGoToResourceUriClick.bind(this);
this.handleSetExpiryDateClick = this.handleSetExpiryDateClick.bind(this);
this.handleMarkAsExpiredClick = this.handleMarkAsExpiredClick.bind(this);
this.handleSecretHistoryClickEvent = this.handleSecretHistoryClickEvent.bind(this);
}
/**
* handle edit resource
*/
handleEditClickEvent() {
const canEditResource = this.canEditResource();
if (canEditResource) {
this.props.dialogContext.open(EditResource, { resource: this.resource });
} else {
this.displayActionAborted();
}
this.props.hide();
}
/**
* Can edit the resource
* @return {boolean}
*/
canEditResource() {
const resourceType = this.props.resourceTypes.getFirstById(this.resource.resource_type_id);
if (resourceType.isV5()) {
const isMetadataSharedKeyEnforced = !this.props.metadataKeysSettings?.allowUsageOfPersonalKeys;
const isPersonalResource = this.resource.personal;
const userHasMissingKeys = this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0;
if (isPersonalResource && isMetadataSharedKeyEnforced && userHasMissingKeys) {
return false;
} else if (!isPersonalResource && userHasMissingKeys) {
return false;
}
}
return true;
}
/**
* handle share resource
*/
handleShareClickEvent() {
const canShareResource = this.canShareResource();
if (canShareResource) {
const resourcesIds = [this.resource.id];
this.props.context.setContext({ shareDialogProps: { resourcesIds } });
this.props.dialogContext.open(ShareDialog);
} else {
this.displayActionAborted();
}
this.props.hide();
}
/**
* handle secret history
*/
handleSecretHistoryClickEvent() {
this.props.dialogContext.open(DisplayResourceSecretHistory, { resource: this.resource });
this.props.hide();
}
/**
* Can share the resource
* @return {boolean}
*/
canShareResource() {
const resourceType = this.props.resourceTypes.getFirstById(this.resource.resource_type_id);
if (resourceType.isV5()) {
const userHasMissingKeys = this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0;
return !userHasMissingKeys;
}
return true;
}
/**
* handle username resource
*/
async handleUsernameClickEvent() {
await this.props.clipboardContext.copy(
this.resource.metadata.username,
this.translate("The username has been copied to clipboard."),
);
this.props.hide();
}
/**
* handle uri resource
*/
async handleUriClickEvent() {
await this.props.clipboardContext.copy(
this.resource.metadata.uris[0],
this.translate("The uri has been copied to clipboard."),
);
this.props.hide();
}
/**
* handle permalink resource
*/
async handlePermalinkClickEvent() {
const baseUrl = this.props.context.userSettings.getTrustedDomain();
const permalink = `${baseUrl}/app/passwords/view/${this.resource.id}`;
await this.props.clipboardContext.copy(permalink, this.translate("The permalink has been copied to clipboard."));
this.props.hide();
}
/**
* Decrypt the resource secret
* @returns {Promise<object>} The secret in plaintext format
* @throw UserAbortsOperationError If the user cancel the operation
*/
decryptResourceSecret() {
return this.props.context.port.request("passbolt.secret.find-by-resource-id", this.resource.id);
}
/**
* Copy password from dto to clipboard
* Support original password (a simple string) and composed objects)
*
* @param {object} plaintextSecretDto The plain text secret DTO.
* @returns {Promise<void>}
*/
async copyPasswordToClipboard(plaintextSecretDto) {
const password = plaintextSecretDto.password;
if (!password) {
throw new TypeError(this.translate("The password is empty."));
}
await this.props.clipboardContext.copyTemporarily(
password,
this.translate("The secret has been copied to clipboard."),
);
}
/**
* handle password resource
*/
async handlePasswordClickEvent() {
let plaintextSecretDto;
this.props.hide();
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret();
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
if (!plaintextSecretDto?.password?.length) {
await this.props.actionFeedbackContext.displayWarning(
this.translate("The password is empty and cannot be copied to clipboard."),
);
return;
}
await this.copyPasswordToClipboard(plaintextSecretDto);
this.props.resourceWorkspaceContext.onResourceCopied();
}
/**
* handle copy to clipboard the totp of the selected resource
*/
async handleTotpClickEvent() {
let plaintextSecretDto, code;
this.props.hide();
this.props.progressContext.open(this.props.t("Decrypting secret"));
try {
plaintextSecretDto = await this.decryptResourceSecret();
} catch (error) {
if (error.name !== "UserAbortsOperationError") {
this.props.actionFeedbackContext.displayError(error.message);
}
}
this.props.progressContext.close();
if (!plaintextSecretDto) {
return;
}
if (!plaintextSecretDto.totp) {
await this.props.actionFeedbackContext.displayError(
this.translate("The TOTP is empty and cannot be copied to clipboard."),
);
return;
}
try {
code = TotpCodeGeneratorService.generate(plaintextSecretDto.totp);
} catch (error) {
Logger.error(error);
await this.props.actionFeedbackContext.displayError(this.translate("Unable to copy the TOTP"));
return;
}
await this.props.clipboardContext.copyTemporarily(code, this.translate("The TOTP has been copied to clipboard."));
await this.props.resourceWorkspaceContext.onResourceCopied();
}
/**
* handle delete resource
*/
handleDeleteClickEvent() {
const resources = [this.resource];
this.props.dialogContext.open(DeleteResource, { resources });
this.props.hide();
}
/**
* handle open the uri in a new tab
*/
handleGoToResourceUriClick() {
this.props.resourceWorkspaceContext.onGoToResourceUriRequested(this.resource.metadata.uris[0]);
}
/**
* Handle mark as expired
* @return {Promise<void>}
*/
async handleMarkAsExpiredClick() {
try {
await this.props.context.port.request("passbolt.resources.set-expiration-date", [
{ id: this.resource.id, expired: formatDateForApi(DateTime.utc()) },
]);
// a count: 1 is used to minimize the translation file as a singular/plural version already exist.
await this.props.actionFeedbackContext.displaySuccess(
this.translate("The resource has been marked as expired.", { count: 1 }),
);
} catch (error) {
Logger.error(error);
await this.props.actionFeedbackContext.displayError(
this.translate("Unable to mark the resource as expired.", { count: 1 }),
);
} finally {
this.props.hide();
}
}
/**
* Handle set expiry date click.
*/
handleSetExpiryDateClick() {
this.props.dialogContext.open(PasswordExpiryDialog, {
resources: [this.resource],
});
this.props.hide();
}
/**
* Display action aborted
*/
displayActionAborted() {
this.props.dialogContext.open(ActionAbortedMissingMetadataKeys);
}
/**
*
* the resource selected
* @returns {*}
*/
get resource() {
return this.props.resource;
}
/**
* the resource safe uri
* @return {string|bool} Return safe uri or false if not safe
*/
get safeUri() {
return sanitizeUrl(this.resource.metadata.uris?.[0], {
whiteListedProtocols: resourceLinkAuthorizedProtocols,
defaultProtocol: urlProtocols.HTTPS,
});
}
/**
* Can update the resource
*/
canUpdate() {
return this.resource.permission.type >= 7;
}
/**
* Can share the resource
*/
canShare() {
return this.resource.permission.type === 15;
}
/**
* Can copy username
* @returns {boolean}
*/
canCopyUsername() {
return this.resource.metadata?.username && this.resource.metadata.username !== "";
}
/**
* Can copy password
* @returns {boolean}
*/
canCopyPassword() {
return this.isPasswordResources;
}
/**
* Is password resource
* @return {boolean}
*/
get isPasswordResources() {
return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasPassword();
}
/**
* Can copy totp
* @returns {boolean}
*/
canCopyTotp() {
return this.isTotpResources;
}
/**
* Is TOTP resource
* @return {boolean}
*/
get isTotpResources() {
return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasTotp();
}
/**
* Is TOTP resource
* @return {boolean}
*/
get isStandaloneTotpResource() {
return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.isStandaloneTotp();
}
/**
* Can copy uri
* @returns {boolean}
*/
canCopyUri() {
return this.resource.metadata?.uris?.length > 0 && this.resource.metadata.uris?.[0] !== "";
}
/**
* Can use Totp
* @return {boolean}
*/
get canUseTotp() {
return this.props.context.siteSettings.canIUse("totpResourceTypes");
}
/**
* Can use password expiry
* @return {boolean}
*/
get canUsePasswordExpiry() {
const passwordExpirySettings = this.props.passwordExpiryContext.getSettings();
return this.props.passwordExpiryContext.isFeatureEnabled() && passwordExpirySettings?.policy_override;
}
/**
* Can use secret history
* @return {boolean}
*/
get canUseSecretHistory() {
const isFeatureEnabled = this.props.context.siteSettings.canIUse("secretRevisions");
return isFeatureEnabled && this.props.secretRevisionsSettings?.isFeatureEnabled;
}
/**
* Get the translate function
* @returns {function(...[*]=)}
*/
get translate() {
return this.props.t;
}
/**
* Render the component.
* @returns {JSX}
*/
render() {
const canCopySecret = this.props.rbacContext.canIUseAction(uiActions.SECRETS_COPY);
const canViewShare = this.props.rbacContext.canIUseAction(uiActions.SHARE_VIEW_LIST);
return (
<ContextualMenuWrapper hide={this.props.hide} left={this.props.left} top={this.props.top} className="floating">
{!this.isStandaloneTotpResource && (
<li key="option-username-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="username"
className="link no-border"
disabled={!this.canCopyUsername()}
onClick={this.handleUsernameClickEvent}
>
<OwnedByMeIcon />
<span>
<Trans>Copy username</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
{canCopySecret && this.canCopyPassword() && (
<li key="option-copy-password-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
className="link no-border"
id="password"
onClick={this.handlePasswordClickEvent}
>
<KeyIcon />
<span>
<Trans>Copy password</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
<li key="option-copy-uri-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="uri"
className="link no-border"
disabled={!this.canCopyUri()}
onClick={this.handleUriClickEvent}
>
<GlobeIcon />
<span>
<Trans>Copy URI</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
<li key="option-permalink-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
className="link no-border"
type="button"
id="permalink"
onClick={this.handlePermalinkClickEvent}
>
<LinkIcon />
<span>
<Trans>Copy permalink</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
{canCopySecret && this.canUseTotp && this.canCopyTotp() && (
<li key="option-copy-totp-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button type="button" className="link no-border" id="totp" onClick={this.handleTotpClickEvent}>
<TotpIcon />
<span>
<Trans>Copy TOTP</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
<li key="option-open-uri-resource" className="ready separator-after">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="open-uri"
className="link no-border"
disabled={!this.safeUri}
onClick={this.handleGoToResourceUriClick}
>
<GoIcon />
<span>
<Trans>Open URI in a new Tab</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
{this.canUsePasswordExpiry && this.canUpdate() && (
<>
<li key="option-set-expiry-date" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="set-expiry-date"
className="link no-border"
onClick={this.handleSetExpiryDateClick}
>
<CalendarIcon />
<span>
<Trans>Set expiry date</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
<li key="option-mark-as-expired-resource" className="ready separator-after">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="mark-as-expired"
className="link no-border"
onClick={this.handleMarkAsExpiredClick}
>
<ClockIcon />
<span>
<Trans>Mark as expired</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
</>
)}
{this.canUpdate() && (
<li key="option-edit-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button type="button" id="edit" className="link no-border" onClick={this.handleEditClickEvent}>
<EditIcon />
<span>
<Trans>Edit</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
{canViewShare && this.canShare() && (
<li key="option-share-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button type="button" id="share" className="link no-border" onClick={this.handleShareClickEvent}>
<ShareIcon />
<span>
<Trans>Share</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
{this.canUseSecretHistory && (
<li key="option-secret_history" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button
type="button"
id="secret-history"
className="link no-border"
onClick={this.handleSecretHistoryClickEvent}
>
<HistoryIcon />
<span>
<Trans>Secret history</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
{this.canShare() && (
<li key="option-delete-resource" className="ready">
<div className="row">
<div className="main-cell-wrapper">
<div className="main-cell">
<button type="button" id="delete" className="link no-border" onClick={this.handleDeleteClickEvent}>
<DeleteIcon />
<span>
<Trans>Delete</Trans>
</span>
</button>
</div>
</div>
</div>
</li>
)}
</ContextualMenuWrapper>
);
}
}
DisplayResourcesListContextualMenu.propTypes = {
context: PropTypes.any, // The application context
rbacContext: PropTypes.any, // The role based access control context
hide: PropTypes.func, // Hide the contextual menu
left: PropTypes.number, // left position in px of the page
top: PropTypes.number, // top position in px of the page
resourceWorkspaceContext: PropTypes.any, // Resource workspace context
resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection
dialogContext: PropTypes.any, // the dialog context
progressContext: PropTypes.any, // The progress context
resource: PropTypes.object, // resource selected
actionFeedbackContext: PropTypes.any, // The action feedback context
passwordExpiryContext: PropTypes.object, // The password expiry context
clipboardContext: PropTypes.object, // the clipboard service provider
metadataKeysSettings: PropTypes.instanceOf(MetadataKeysSettingsEntity), // The metadata key settings
secretRevisionsSettings: PropTypes.instanceOf(SecretRevisionsSettingsEntity), // The secret revision settings
t: PropTypes.func, // The translation function
};
export default withAppContext(
withMetadataKeysSettingsLocalStorage(
withClipboard(
withRbac(
withResourceWorkspace(
withResourceTypesLocalStorage(
withPasswordExpiry(
withSecretRevisionsSettings(
withDialog(
withProgress(withActionFeedback(withTranslation("common")(DisplayResourcesListContextualMenu))),
),
),
),
),
),
),
),
),
);