wip save ok

This commit is contained in:
Thomas Ramé
2026-03-02 17:55:54 +01:00
parent 9c438eba06
commit 54f2762e79
5 changed files with 34 additions and 6 deletions
+13 -5
View File
@@ -330,7 +330,15 @@ class DocumentSerializer(ListDocumentSerializer):
"attachments" field for access control.
"""
content = self.validated_data.get("content", "")
extracted_attachments = set(utils.extract_attachments(content))
# Encrypted content cannot be parsed as a Yjs update
# TODO: for now skip attachment extraction for encrypted documents but we should have them
is_encrypted = self.validated_data.get(
"is_encrypted", self.instance and self.instance.is_encrypted
)
extracted_attachments = (
set() if is_encrypted else set(utils.extract_attachments(content))
)
existing_attachments = (
set(self.instance.attachments or []) if self.instance else set()
@@ -418,15 +426,15 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
def get_fields(self):
"""Dynamically control field availability and requirements based on document encryption status."""
fields = super().get_fields()
# Get the document from context (if available)
document = None
if "view" in self.context and hasattr(self.context["view"], "document"):
document = self.context["view"].document
# Get the encrypted_document_symmetric_key_for_user field
key_field = fields.get("encrypted_document_symmetric_key_for_user")
if key_field:
# If document is encrypted, make the field required
if document and getattr(document, "is_encrypted", False):
@@ -435,7 +443,7 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
# If document is not encrypted, remove the field entirely
elif document and not getattr(document, "is_encrypted", False):
fields.pop("encrypted_document_symmetric_key_for_user", None)
return fields
def get_abilities(self, instance) -> dict:
@@ -8,6 +8,7 @@ import {
export class EncryptedWebSocket extends WebSocket {
protected readonly encryptionKey!: CryptoKey;
protected readonly decryptionKey!: CryptoKey;
protected readonly onSystemMessage?: (message: string) => void;
constructor(address: string | URL, protocols?: string | string[]) {
super(address, protocols);
@@ -24,6 +25,15 @@ export class EncryptedWebSocket extends WebSocket {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messageEvent = event as any;
// some messages are here to help adjusting the interface or even reloading it
// in case it there is an ongoing decryption, or symmetric key rotation...
// those messages must be parsable so they are not encrypted
if (typeof messageEvent.data === 'string') {
this.onSystemMessage?.(messageEvent.data as string);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!(messageEvent.data instanceof ArrayBuffer)) {
throw new Error(
@@ -127,9 +137,11 @@ export class EncryptedWebSocket extends WebSocket {
export function createAdaptedEncryptedWebsocketClass(options: {
encryptionKey: CryptoKey;
decryptionKey: CryptoKey;
onSystemMessage?: (message: string) => void;
}) {
return class extends EncryptedWebSocket {
protected readonly encryptionKey = options.encryptionKey;
protected readonly decryptionKey = options.decryptionKey;
protected readonly onSystemMessage = options.onSystemMessage;
};
}
@@ -9,7 +9,7 @@ import { isFirefox } from '@/utils/userAgent';
import { toBase64 } from '../utils';
const SAVE_INTERVAL = 100 * 60000;
const SAVE_INTERVAL = 60000;
export const useSaveDoc = (
docId: string,
@@ -56,6 +56,11 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
const AdaptedEncryptedWebSocket = createAdaptedEncryptedWebsocketClass({
encryptionKey: encryptionSymmetricKey,
decryptionKey: encryptionSymmetricKey,
onSystemMessage: (message) => {
if (message === 'system:authenticated') {
set({ isReady: true, isConnected: true });
}
},
});
provider = new RelayProvider(wsUrl, storeId, doc, {
@@ -84,6 +84,9 @@ export const collaborationWSHandler = async (
// Since for "end-to-end encryption" the server cannot maintains its own state for the document
// we use a different strategy with a relay server
if (document.is_encrypted) {
// mimick the Hocuspocus protocol to properly hide the frontend loader
ws.send('system:authenticated');
await handleRelayServerConnection(ws, roomId);
} else {
hocuspocusServer.hocuspocus.handleConnection(ws, req, {