mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 02:44:50 +01:00 
			
		
		
		
	 e617ccc252
			
		
	
	e617ccc252
	
	
	
		
			
			Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
		
			
				
	
	
		
			165 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { getImportedKey } from "../data";
 | |
| import { createIV } from "./index";
 | |
| import { ExcalidrawElement } from "../../element/types";
 | |
| import { getSceneVersion } from "../../element";
 | |
| import Portal from "../collab/Portal";
 | |
| import { restoreElements } from "../../data/restore";
 | |
| 
 | |
| let firebasePromise: Promise<
 | |
|   typeof import("firebase/app").default
 | |
| > | null = null;
 | |
| 
 | |
| const loadFirebase = async () => {
 | |
|   const firebase = (
 | |
|     await import(/* webpackChunkName: "firebase" */ "firebase/app")
 | |
|   ).default;
 | |
|   await import(/* webpackChunkName: "firestore" */ "firebase/firestore");
 | |
| 
 | |
|   const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
 | |
|   firebase.initializeApp(firebaseConfig);
 | |
| 
 | |
|   return firebase;
 | |
| };
 | |
| 
 | |
| const getFirebase = async (): Promise<
 | |
|   typeof import("firebase/app").default
 | |
| > => {
 | |
|   if (!firebasePromise) {
 | |
|     firebasePromise = loadFirebase();
 | |
|   }
 | |
|   return await firebasePromise!;
 | |
| };
 | |
| 
 | |
| interface FirebaseStoredScene {
 | |
|   sceneVersion: number;
 | |
|   iv: firebase.default.firestore.Blob;
 | |
|   ciphertext: firebase.default.firestore.Blob;
 | |
| }
 | |
| 
 | |
| const encryptElements = async (
 | |
|   key: string,
 | |
|   elements: readonly ExcalidrawElement[],
 | |
| ): Promise<{ ciphertext: ArrayBuffer; iv: Uint8Array }> => {
 | |
|   const importedKey = await getImportedKey(key, "encrypt");
 | |
|   const iv = createIV();
 | |
|   const json = JSON.stringify(elements);
 | |
|   const encoded = new TextEncoder().encode(json);
 | |
|   const ciphertext = await window.crypto.subtle.encrypt(
 | |
|     {
 | |
|       name: "AES-GCM",
 | |
|       iv,
 | |
|     },
 | |
|     importedKey,
 | |
|     encoded,
 | |
|   );
 | |
| 
 | |
|   return { ciphertext, iv };
 | |
| };
 | |
| 
 | |
| const decryptElements = async (
 | |
|   key: string,
 | |
|   iv: Uint8Array,
 | |
|   ciphertext: ArrayBuffer,
 | |
| ): Promise<readonly ExcalidrawElement[]> => {
 | |
|   const importedKey = await getImportedKey(key, "decrypt");
 | |
|   const decrypted = await window.crypto.subtle.decrypt(
 | |
|     {
 | |
|       name: "AES-GCM",
 | |
|       iv,
 | |
|     },
 | |
|     importedKey,
 | |
|     ciphertext,
 | |
|   );
 | |
| 
 | |
|   const decodedData = new TextDecoder("utf-8").decode(
 | |
|     new Uint8Array(decrypted) as any,
 | |
|   );
 | |
|   return JSON.parse(decodedData);
 | |
| };
 | |
| 
 | |
| const firebaseSceneVersionCache = new WeakMap<SocketIOClient.Socket, number>();
 | |
| 
 | |
| export const isSavedToFirebase = (
 | |
|   portal: Portal,
 | |
|   elements: readonly ExcalidrawElement[],
 | |
| ): boolean => {
 | |
|   if (portal.socket && portal.roomId && portal.roomKey) {
 | |
|     const sceneVersion = getSceneVersion(elements);
 | |
|     return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
 | |
|   }
 | |
|   // if no room exists, consider the room saved so that we don't unnecessarily
 | |
|   // prevent unload (there's nothing we could do at that point anyway)
 | |
|   return true;
 | |
| };
 | |
| 
 | |
| export const saveToFirebase = async (
 | |
|   portal: Portal,
 | |
|   elements: readonly ExcalidrawElement[],
 | |
| ) => {
 | |
|   const { roomId, roomKey, socket } = portal;
 | |
|   if (
 | |
|     // if no room exists, consider the room saved because there's nothing we can
 | |
|     // do at this point
 | |
|     !roomId ||
 | |
|     !roomKey ||
 | |
|     !socket ||
 | |
|     isSavedToFirebase(portal, elements)
 | |
|   ) {
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   const firebase = await getFirebase();
 | |
|   const sceneVersion = getSceneVersion(elements);
 | |
|   const { ciphertext, iv } = await encryptElements(roomKey, elements);
 | |
| 
 | |
|   const nextDocData = {
 | |
|     sceneVersion,
 | |
|     ciphertext: firebase.firestore.Blob.fromUint8Array(
 | |
|       new Uint8Array(ciphertext),
 | |
|     ),
 | |
|     iv: firebase.firestore.Blob.fromUint8Array(iv),
 | |
|   } as FirebaseStoredScene;
 | |
| 
 | |
|   const db = firebase.firestore();
 | |
|   const docRef = db.collection("scenes").doc(roomId);
 | |
|   const didUpdate = await db.runTransaction(async (transaction) => {
 | |
|     const doc = await transaction.get(docRef);
 | |
|     if (!doc.exists) {
 | |
|       transaction.set(docRef, nextDocData);
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     const prevDocData = doc.data() as FirebaseStoredScene;
 | |
|     if (prevDocData.sceneVersion >= nextDocData.sceneVersion) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     transaction.update(docRef, nextDocData);
 | |
|     return true;
 | |
|   });
 | |
| 
 | |
|   if (didUpdate) {
 | |
|     firebaseSceneVersionCache.set(socket, sceneVersion);
 | |
|   }
 | |
| 
 | |
|   return didUpdate;
 | |
| };
 | |
| 
 | |
| export const loadFromFirebase = async (
 | |
|   roomId: string,
 | |
|   roomKey: string,
 | |
| ): Promise<readonly ExcalidrawElement[] | null> => {
 | |
|   const firebase = await getFirebase();
 | |
|   const db = firebase.firestore();
 | |
| 
 | |
|   const docRef = db.collection("scenes").doc(roomId);
 | |
|   const doc = await docRef.get();
 | |
|   if (!doc.exists) {
 | |
|     return null;
 | |
|   }
 | |
|   const storedScene = doc.data() as FirebaseStoredScene;
 | |
|   const ciphertext = storedScene.ciphertext.toUint8Array();
 | |
|   const iv = storedScene.iv.toUint8Array();
 | |
|   return restoreElements(await decryptElements(roomKey, iv, ciphertext));
 | |
| };
 |