2899058b5f
https://github.com/user-attachments/assets/af3fb042-d066-4e0c-9348-f86ea92a6fcd Front component anchors render a real host `<a>`, so clicking a link to another domain performed an uncontrolled full-page navigation. This adds a phishing-resistant "you're leaving Twenty" confirmation modal before navigating to an external origin (Fixes [#23260](https://github.com/twentyhq/twenty/issues/23260)). The renderer intercepts external anchor clicks in `createHtmlHostWrapper` and hands the destination to a host callback via context; twenty-front owns the modal (reuses `ConfirmationModal`) and a per-application list of trusted origins persisted in localStorage. A "Don't ask again for this site" checkbox (checked by default) skips the modal next time for that app. Scope is external cross-origin http(s) links only; same-origin links keep native behavior. External links always open in a new tab, so a component can never navigate the Twenty tab away, even once its origin is trusted. The modal is rendered by the trusted host, so components cannot style or suppress it. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23270?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
67 lines
1.7 KiB
TypeScript
67 lines
1.7 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { useId } from 'react';
|
|
import { Checkbox } from 'twenty-ui/input';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
const StyledContent = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[4]};
|
|
overflow-wrap: anywhere;
|
|
`;
|
|
|
|
const StyledTrustRow = styled.div`
|
|
align-items: center;
|
|
align-self: stretch;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
text-align: left;
|
|
`;
|
|
|
|
const StyledTrustLabel = styled.span`
|
|
cursor: pointer;
|
|
`;
|
|
|
|
type FrontComponentExternalLinkModalSubtitleProps = {
|
|
url: string;
|
|
origin: string;
|
|
shouldTrustOrigin: boolean;
|
|
onShouldTrustOriginChange: (shouldTrustOrigin: boolean) => void;
|
|
};
|
|
|
|
export const FrontComponentExternalLinkModalSubtitle = ({
|
|
url,
|
|
origin,
|
|
shouldTrustOrigin,
|
|
onShouldTrustOriginChange,
|
|
}: FrontComponentExternalLinkModalSubtitleProps) => {
|
|
const trustOriginLabelId = useId();
|
|
|
|
return (
|
|
<StyledContent>
|
|
<span>
|
|
<Trans>
|
|
This link will take you to an external site: <strong>{url}</strong>
|
|
</Trans>
|
|
</span>
|
|
<StyledTrustRow>
|
|
<Checkbox
|
|
checked={shouldTrustOrigin}
|
|
onCheckedChange={onShouldTrustOriginChange}
|
|
aria-labelledby={trustOriginLabelId}
|
|
/>
|
|
<StyledTrustLabel
|
|
id={trustOriginLabelId}
|
|
onClick={() => onShouldTrustOriginChange(!shouldTrustOrigin)}
|
|
>
|
|
<Trans>
|
|
Don't ask again for <strong>{origin}</strong>
|
|
</Trans>
|
|
</StyledTrustLabel>
|
|
</StyledTrustRow>
|
|
</StyledContent>
|
|
);
|
|
};
|