wip use a context instead for global usage

This commit is contained in:
Thomas Ramé
2026-03-05 10:26:34 +01:00
parent 4baef38cae
commit 7a55e31a73
15 changed files with 258 additions and 164 deletions
+8
View File
@@ -1385,6 +1385,14 @@ class DocumentViewSet(
# Check permissions first
document = self.get_object()
if document.is_encrypted:
raise drf.exceptions.ValidationError(
{
"detail": "Visibility cannot be changed for encrypted documents. "
"Encrypted documents must remain restricted.",
}
)
# Deserialize and validate the data
serializer = serializers.LinkDocumentSerializer(
document, data=request.data, partial=True
@@ -9,6 +9,7 @@ import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { UserEncryptionProvider } from '@/features/docs/doc-collaboration';
import { useResponsiveStore } from '@/stores/';
import { ConfigProvider } from './config/';
@@ -74,7 +75,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<ConfigProvider>
<Auth>{children}</Auth>
<Auth>
<UserEncryptionProvider>{children}</UserEncryptionProvider>
</Auth>
</ConfigProvider>
</CunninghamProvider>
</QueryClientProvider>
@@ -0,0 +1,39 @@
import { createContext, useContext } from 'react';
import { useAuth } from '@/features/auth';
import { EncryptionError, useEncryption } from './hook/useEncryption';
interface UserEncryptionContextValue {
encryptionLoading: boolean;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
encryptionError: EncryptionError;
}
const UserEncryptionContext = createContext<UserEncryptionContextValue>({
encryptionLoading: true,
encryptionSettings: null,
encryptionError: null,
});
export const UserEncryptionProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { user } = useAuth();
const value = useEncryption(user?.id);
return (
<UserEncryptionContext.Provider value={value}>
{children}
</UserEncryptionContext.Provider>
);
};
export const useUserEncryption = (): UserEncryptionContextValue =>
useContext(UserEncryptionContext);
@@ -2,18 +2,14 @@ import { useEffect, useState } from 'react';
import { decryptSymmetricKey } from '@/docs/doc-collaboration/encryption';
import { useUserEncryption } from '../UserEncryptionProvider';
export type DocumentEncryptionError =
| 'missing_symmetric_key'
| 'decryption_failed'
| null;
export function useDocumentEncryption(
encryptionLoading: boolean,
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null,
isDocumentEncrypted: boolean | undefined,
userEncryptedSymmetricKey: string | undefined,
): {
@@ -23,6 +19,7 @@ export function useDocumentEncryption(
} | null;
documentEncryptionError: DocumentEncryptionError;
} {
const { encryptionLoading, encryptionSettings } = useUserEncryption();
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<{
documentSymmetricKey: CryptoKey;
@@ -16,5 +16,9 @@ export {
useEncryption,
type EncryptionError,
} from './hook/useEncryption';
export {
UserEncryptionProvider,
useUserEncryption,
} from './UserEncryptionProvider';
export { useKeyFingerprint } from './hook/useKeyFingerprint';
export { usePublicKeyRegistry } from './hook/usePublicKeyRegistry';
@@ -78,7 +78,7 @@ describe('DocEditor', () => {
},
} as any;
const { rerender } = render(<DocEditor doc={doc} encryptionSettings={null} documentEncryptionSettings={null} />, {
const { rerender } = render(<DocEditor doc={doc} documentEncryptionSettings={null} />, {
wrapper: AppWrapper,
});
@@ -90,7 +90,7 @@ describe('DocEditor', () => {
// Rerender with same doc to check that event is not tracked again
rerender(
<DocEditor doc={{ ...doc, computed_link_reach: LinkReach.RESTRICTED }} encryptionSettings={null} documentEncryptionSettings={null} />,
<DocEditor doc={{ ...doc, computed_link_reach: LinkReach.RESTRICTED }} documentEncryptionSettings={null} />,
);
expect(TrackEventMock).toHaveBeenNthCalledWith(1, {
@@ -107,7 +107,6 @@ describe('DocEditor', () => {
id: 'test-doc-id-2',
computed_link_reach: LinkReach.RESTRICTED,
}}
encryptionSettings={null}
documentEncryptionSettings={null}
/>,
);
@@ -76,11 +76,6 @@ export const DocEditorContainer = ({
interface DocEditorProps {
doc: Doc;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
documentEncryptionSettings: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -88,7 +83,6 @@ interface DocEditorProps {
export const DocEditor = ({
doc,
encryptionSettings,
documentEncryptionSettings,
}: DocEditorProps) => {
const { isDesktop } = useResponsiveStore();
@@ -145,7 +139,6 @@ export const DocEditor = ({
docHeader={
<DocHeader
doc={doc}
encryptionSettings={encryptionSettings}
documentEncryptionSettings={documentEncryptionSettings}
/>
}
@@ -37,7 +37,7 @@ describe('DocToolBox - Licence', () => {
const { DocToolBox } = await import('../components/DocToolBox');
render(<DocToolBox doc={doc as any} encryptionSettings={null} />, {
render(<DocToolBox doc={doc as any} />, {
wrapper: AppWrapper,
});
const optionsButton = await screen.findByLabelText('Export the document');
@@ -55,7 +55,7 @@ describe('DocToolBox - Licence', () => {
const { DocToolBox } = await import('../components/DocToolBox');
render(<DocToolBox doc={doc as any} encryptionSettings={null} />, {
render(<DocToolBox doc={doc as any} />, {
wrapper: AppWrapper,
});
@@ -20,11 +20,6 @@ import { DocToolBox } from './DocToolBox';
interface DocHeaderProps {
doc: Doc;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
documentEncryptionSettings?: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -32,7 +27,6 @@ interface DocHeaderProps {
export const DocHeader = ({
doc,
encryptionSettings,
documentEncryptionSettings,
}: DocHeaderProps) => {
const { spacingsTokens } = useCunninghamTheme();
@@ -80,7 +74,6 @@ export const DocHeader = ({
{!isDeletedDoc && (
<DocToolBox
doc={doc}
encryptionSettings={encryptionSettings}
documentEncryptionSettings={documentEncryptionSettings}
/>
)}
@@ -31,7 +31,10 @@ import {
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
import {
usePublicKeyRegistry,
useUserEncryption,
} from '@/docs/doc-collaboration';
import { DocShareModal } from '@/docs/doc-share';
import {
KEY_LIST_DOC_VERSIONS,
@@ -47,11 +50,6 @@ const ModalExport = Export?.ModalExport;
interface DocToolBoxProps {
doc: Doc;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
documentEncryptionSettings?: {
documentSymmetricKey: CryptoKey;
} | null;
@@ -59,7 +57,6 @@ interface DocToolBoxProps {
export const DocToolBox = ({
doc,
encryptionSettings,
documentEncryptionSettings,
}: DocToolBoxProps) => {
const { t } = useTranslation();
@@ -69,6 +66,7 @@ export const DocToolBox = ({
const { isChild, isTopRoot } = useDocUtils(doc);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { encryptionSettings } = useUserEncryption();
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
@@ -336,7 +334,6 @@ export const DocToolBox = ({
{isModalEncryptOpen && (
<ModalEncryptDoc
doc={doc}
encryptionSettings={encryptionSettings}
onClose={() => setIsModalEncryptOpen(false)}
onSuccess={() => {
//
@@ -21,6 +21,7 @@ import {
generateUserKeyPair,
getEncryptionDB,
prepareEncryptedSymmetricKeysForUsers,
useUserEncryption,
} from '@/docs/doc-collaboration';
import { createDocAttachment } from '@/docs/doc-editor/api';
import { toBase64 } from '@/docs/doc-editor';
@@ -121,18 +122,12 @@ const encryptRemoteAttachments = async (
interface ModalEncryptDocProps {
doc: Doc;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
onClose: () => void;
onSuccess?: (doc: Doc) => void;
}
export const ModalEncryptDoc = ({
doc,
encryptionSettings,
onClose,
onSuccess,
}: ModalEncryptDocProps) => {
@@ -140,6 +135,7 @@ export const ModalEncryptDoc = ({
const { toast } = useToastProvider();
const { provider } = useProviderStore();
const { user } = useAuth();
const { encryptionSettings } = useUserEncryption();
const { mutateAsync: updateUser } = useUserUpdate();
const [isPending, setIsPending] = useState(false);
@@ -10,6 +10,7 @@ import {
ButtonCloseModal,
HorizontalSeparator,
Icon,
Loading,
Text,
} from '@/components';
import {
@@ -17,7 +18,11 @@ import {
QuickSearchData,
QuickSearchGroup,
} from '@/components/quick-search/';
import { usePublicKeyRegistry } from '@/docs/doc-collaboration';
import {
useDocumentEncryption,
usePublicKeyRegistry,
useUserEncryption,
} from '@/docs/doc-collaboration';
import type { PublicKeyMismatch } from '@/docs/doc-collaboration/hook/usePublicKeyRegistry';
import { Doc } from '@/docs/doc-management';
import { User, useAuth } from '@/features/auth';
@@ -77,6 +82,28 @@ export const DocShareModal = ({
const { isDesktop } = useResponsiveStore();
const { user } = useAuth();
// When document encryption settings exist they should be passed as prop, on it will use this fallback
// that's because in some cases we want them to only be computed at this step (to avoid computing just when listed in a list)
const needsDerivation = !documentEncryptionSettings;
const { encryptionLoading, encryptionError } = useUserEncryption();
const {
documentEncryptionLoading,
documentEncryptionSettings: derivedEncryptionSettings,
documentEncryptionError,
} = useDocumentEncryption(
needsDerivation ? doc.is_encrypted : undefined,
needsDerivation ? doc.encrypted_document_symmetric_key_for_user : undefined,
);
const effectiveEncryptionSettings =
documentEncryptionSettings ?? derivedEncryptionSettings ?? null;
const isEncryptionDeriving =
needsDerivation && (encryptionLoading || documentEncryptionLoading);
const derivedEncryptionError =
needsDerivation && doc.is_encrypted
? encryptionError || documentEncryptionError
: null;
const { mismatches: keyMismatches, acceptNewKey } = usePublicKeyRegistry(
doc.accesses_public_keys_per_user,
user?.id,
@@ -240,125 +267,168 @@ export const DocShareModal = ({
>
{liveAnnouncement}
</div>
<Box
$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
$overflow="hidden"
className="--docs--doc-share-modal noPadding "
$justify="space-between"
role="dialog"
aria-label={t('Share modal content')}
>
{isEncryptionDeriving && <Loading />}
{!isEncryptionDeriving && derivedEncryptionError && (
<Box $align="center" $gap="sm" $padding="lg">
<Icon iconName="lock" $size="2rem" $theme="warning" />
<Text as="h3" $textAlign="center" $margin="0">
{t('Encryption keys unavailable')}
</Text>
<Text $variation="secondary" $textAlign="center" $size="sm">
{t(
'This is an encrypted document, but your current device does not have the required encryption keys to decrypt it.',
)}
</Text>
{(encryptionError === 'missing_private_key' ||
encryptionError === 'missing_public_key') && (
<Text $variation="secondary" $textAlign="center" $size="sm">
{t(
'This usually happens when you switch to a new device or browser without restoring your encryption backup.',
)}
</Text>
)}
{documentEncryptionError === 'missing_symmetric_key' && (
<Text $variation="secondary" $textAlign="center" $size="sm">
{t(
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
)}
</Text>
)}
{documentEncryptionError === 'decryption_failed' && (
<Text $variation="secondary" $textAlign="center" $size="sm">
{t(
'Your encryption keys could not decrypt this document. This may happen if your keys were recreated. Ask the document owner to share it with you again.',
)}
</Text>
)}
</Box>
)}
{!isEncryptionDeriving && !derivedEncryptionError && (
<Box
$flex={1}
$css={css`
[cmdk-list] {
overflow-y: auto;
height: ${listHeight};
}
`}
$height="auto"
$maxHeight={canViewAccesses ? modalContentHeight : 'none'}
$overflow="hidden"
className="--docs--doc-share-modal noPadding "
$justify="space-between"
role="dialog"
aria-label={t('Share modal content')}
>
<Box ref={selectedUsersRef}>
{canShare && selectedUsers.length > 0 && (
<Box $padding={{ horizontal: 'base' }} $margin={{ top: '12x' }}>
<DocShareAddMemberList
doc={doc}
documentEncryptionSettings={
documentEncryptionSettings ?? null
}
selectedUsers={selectedUsers}
onRemoveUser={onRemoveUser}
afterInvite={() => {
setUserQuery('');
setInputValue('');
setSelectedUsers([]);
}}
/>
</Box>
)}
{!canViewAccesses && <HorizontalSeparator customPadding="12px" />}
</Box>
<Box data-testid="doc-share-quick-search">
{!canViewAccesses && (
<Box
$height={listHeight}
$align="center"
$justify="center"
$gap="1rem"
>
<Text
$maxWidth="320px"
$textAlign="center"
$variation="secondary"
$size="sm"
as="p"
<Box
$flex={1}
$css={css`
[cmdk-list] {
overflow-y: auto;
height: ${listHeight};
}
`}
>
<Box ref={selectedUsersRef}>
{canShare && selectedUsers.length > 0 && (
<Box
$padding={{ horizontal: 'base' }}
$margin={{ top: '12x' }}
>
{t(
'You can view this document but need additional access to see its members or modify settings.',
)}
</Text>
<ButtonAccessRequest
docId={doc.id}
variant="secondary"
size="small"
/>
</Box>
)}
{canViewAccesses && (
<QuickSearch
label={t('Search results')}
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{showInheritedShareContent && (
<DocInheritedShareContent
rawAccesses={
membersQuery?.filter(
(access) => access.document.id !== doc.id,
) ?? []
}
<DocShareAddMemberList
doc={doc}
documentEncryptionSettings={effectiveEncryptionSettings}
selectedUsers={selectedUsers}
onRemoveUser={onRemoveUser}
afterInvite={() => {
setUserQuery('');
setInputValue('');
setSelectedUsers([]);
}}
/>
)}
{showMemberSection && isRootDoc && (
<Box $padding={{ horizontal: 'base' }}>
<QuickSearchGroupAccessRequest doc={doc} />
<QuickSearchGroupInvitation doc={doc} />
<QuickSearchGroupMember
doc={doc}
</Box>
)}
{!canViewAccesses && (
<HorizontalSeparator customPadding="12px" />
)}
</Box>
<Box data-testid="doc-share-quick-search">
{!canViewAccesses && (
<Box
$height={listHeight}
$align="center"
$justify="center"
$gap="1rem"
>
<Text
$maxWidth="320px"
$textAlign="center"
$variation="secondary"
$size="sm"
as="p"
>
{t(
'You can view this document but need additional access to see its members or modify settings.',
)}
</Text>
<ButtonAccessRequest
docId={doc.id}
variant="secondary"
size="small"
/>
</Box>
)}
{canViewAccesses && (
<QuickSearch
label={t('Search results')}
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{showInheritedShareContent && (
<DocInheritedShareContent
rawAccesses={
membersQuery?.filter(
(access) => access.document.id !== doc.id,
) ?? []
}
/>
)}
{showMemberSection && isRootDoc && (
<Box $padding={{ horizontal: 'base' }}>
<QuickSearchGroupAccessRequest doc={doc} />
<QuickSearchGroupInvitation doc={doc} />
<QuickSearchGroupMember
doc={doc}
keyMismatchUserIds={keyMismatchUserIds}
keyMismatches={keyMismatches}
acceptNewKey={acceptNewKey}
/>
</Box>
)}
{!showMemberSection && canShare && (
<QuickSearchInviteInputSection
searchUsersRawData={searchUsersQuery.data}
onSelect={onSelect}
userQuery={userQuery}
isEncrypted={doc.is_encrypted}
keyMismatchUserIds={keyMismatchUserIds}
keyMismatches={keyMismatches}
acceptNewKey={acceptNewKey}
/>
</Box>
)}
)}
</QuickSearch>
)}
</Box>
</Box>
{!showMemberSection && canShare && (
<QuickSearchInviteInputSection
searchUsersRawData={searchUsersQuery.data}
onSelect={onSelect}
userQuery={userQuery}
isEncrypted={doc.is_encrypted}
keyMismatchUserIds={keyMismatchUserIds}
keyMismatches={keyMismatches}
acceptNewKey={acceptNewKey}
/>
)}
</QuickSearch>
<Box ref={handleRef}>
{showFooter && (
<DocShareModalFooter doc={doc} onClose={onClose} />
)}
</Box>
</Box>
<Box ref={handleRef}>
{showFooter && <DocShareModalFooter doc={doc} onClose={onClose} />}
</Box>
</Box>
)}
</Modal>
</>
);
@@ -27,8 +27,12 @@ export const DocShareModalFooter = ({
>
<HorizontalSeparator $withPadding={true} customPadding="12px" />
<DocVisibility doc={doc} />
<HorizontalSeparator customPadding="12px" />
{!doc.is_encrypted && (
<>
<DocVisibility doc={doc} />
<HorizontalSeparator customPadding="12px" />
</>
)}
<Box
$direction="row"
@@ -10,8 +10,7 @@ import styled, { css } from 'styled-components';
import AllDocs from '@/assets/icons/doc-all.svg';
import { Box, Card, Icon, Text } from '@/components';
import { DocDefaultFilter, useInfiniteDocs } from '@/docs/doc-management';
import { useAuth } from '@/features/auth';
import { useEncryption } from '@/features/docs/doc-collaboration';
import { useUserEncryption } from '@/features/docs/doc-collaboration';
import { useResponsiveStore } from '@/stores';
import { useInfiniteDocsTrashbin } from '../api';
@@ -81,11 +80,7 @@ export const DocsGrid = ({
});
}, [data?.pages]);
const { user } = useAuth();
const { encryptionLoading, encryptionSettings } = useEncryption(user?.id);
// TODO:
// TODO: from here `encryptionSettings` should be used in case of adjusting accesses on a document
// TODO:
const { encryptionLoading, encryptionSettings } = useUserEncryption();
const loading = isFetching || isLoading || encryptionLoading;
const hasDocs = data?.pages.some((page) => page.results.length > 0);
@@ -23,7 +23,7 @@ import {
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
import {
useDocumentEncryption,
useEncryption,
useUserEncryption,
} from '@/features/docs/doc-collaboration';
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
import { useSkeletonStore } from '@/features/skeletons';
@@ -93,17 +93,14 @@ const DocPage = ({ id }: DocProps) => {
},
);
const { authenticated, user } = useAuth();
const { authenticated } = useAuth();
const [doc, setDoc] = useState<Doc>();
const { encryptionLoading, encryptionSettings, encryptionError } =
useEncryption(user?.id);
const { encryptionLoading, encryptionError } = useUserEncryption();
const {
documentEncryptionLoading,
documentEncryptionSettings,
documentEncryptionError,
} = useDocumentEncryption(
encryptionLoading,
encryptionSettings,
doc?.is_encrypted,
doc?.encrypted_document_symmetric_key_for_user,
);
@@ -357,7 +354,6 @@ const DocPage = ({ id }: DocProps) => {
</Head>
<DocEditor
doc={doc}
encryptionSettings={encryptionSettings}
documentEncryptionSettings={documentEncryptionSettings}
/>
</>