from max
This commit is contained in:
parent
50d7f41248
commit
51709c9731
13 changed files with 163 additions and 124 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
|
|
@ -59,7 +59,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -636,7 +635,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
"@codemirror/view": "^6.23.0",
|
"@codemirror/view": "^6.23.0",
|
||||||
|
|
@ -726,7 +724,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -736,7 +733,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
|
||||||
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
|
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
"crelt": "^1.0.6",
|
"crelt": "^1.0.6",
|
||||||
|
|
@ -1273,7 +1269,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
||||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1638,7 +1633,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.3.0"
|
"@lezer/common": "^1.3.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3336,7 +3330,6 @@
|
||||||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -3347,7 +3340,6 @@
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3384,7 +3376,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -3492,7 +3483,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
|
|
@ -4351,6 +4341,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "GitHub Sponsors ❤",
|
"type": "GitHub Sponsors ❤",
|
||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
|
|
@ -4429,6 +4420,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
||||||
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic.js": "^0.2.4"
|
"isomorphic.js": "^0.2.4"
|
||||||
},
|
},
|
||||||
|
|
@ -5896,7 +5888,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -5918,7 +5909,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
|
|
@ -6217,8 +6207,7 @@
|
||||||
"version": "4.1.17",
|
"version": "4.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|
@ -6439,7 +6428,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|
|
||||||
95
frontend/src/api/encryption.tsx
Normal file
95
frontend/src/api/encryption.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { FolderTreeResponse, FolderTreeNode } from "./folders";
|
||||||
|
|
||||||
|
export async function deriveKey(password: string) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
enc.encode(password),
|
||||||
|
"PBKDF2",
|
||||||
|
false,
|
||||||
|
["deriveKey"],
|
||||||
|
);
|
||||||
|
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: "PBKDF2",
|
||||||
|
salt: enc.encode("your-app-salt"), // Store this somewhere consistent
|
||||||
|
iterations: 100000,
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: "AES-GCM", length: 256 },
|
||||||
|
false,
|
||||||
|
["encrypt", "decrypt"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encryptString(
|
||||||
|
text: string,
|
||||||
|
key: CryptoKey,
|
||||||
|
): Promise<string> {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{ name: "AES-GCM", iv },
|
||||||
|
key,
|
||||||
|
enc.encode(text),
|
||||||
|
);
|
||||||
|
|
||||||
|
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
||||||
|
combined.set(iv);
|
||||||
|
combined.set(new Uint8Array(encrypted), iv.length);
|
||||||
|
|
||||||
|
return btoa(String.fromCharCode(...combined));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptString(encrypted: string, key: CryptoKey) {
|
||||||
|
const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
|
||||||
|
const iv = combined.slice(0, 12);
|
||||||
|
const data = combined.slice(12);
|
||||||
|
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{ name: "AES-GCM", iv },
|
||||||
|
key,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptFolderTree(
|
||||||
|
tree: FolderTreeResponse,
|
||||||
|
encryptionKey: CryptoKey,
|
||||||
|
): Promise<FolderTreeResponse> {
|
||||||
|
const decryptFolder = async (
|
||||||
|
folder: FolderTreeNode,
|
||||||
|
): Promise<FolderTreeNode> => {
|
||||||
|
return {
|
||||||
|
...folder,
|
||||||
|
notes: await Promise.all(
|
||||||
|
folder.notes.map(async (note) => ({
|
||||||
|
...note,
|
||||||
|
title: await decryptString(note.title, encryptionKey),
|
||||||
|
content: await decryptString(note.content, encryptionKey),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
children: await Promise.all(
|
||||||
|
folder.children.map((child) => decryptFolder(child)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
folders: await Promise.all(
|
||||||
|
tree.folders.map((folder) => decryptFolder(folder)),
|
||||||
|
),
|
||||||
|
orphaned_notes: await Promise.all(
|
||||||
|
tree.orphaned_notes.map(async (note) => ({
|
||||||
|
...note,
|
||||||
|
title: await decryptString(note.title, encryptionKey),
|
||||||
|
content: await decryptString(note.content, encryptionKey),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { decryptFolderTree, deriveKey } from "./encryption";
|
||||||
|
|
||||||
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
|
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
|
||||||
|
|
||||||
|
|
@ -35,8 +36,18 @@ export interface FolderCreate {
|
||||||
parent_id: number | null;
|
parent_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFolderTree = async () => {
|
||||||
|
const { data } = await axios.get<FolderTreeResponse>(
|
||||||
|
`${API_URL}/folders/tree`,
|
||||||
|
);
|
||||||
|
var key = await deriveKey("Test");
|
||||||
|
const decryptedFolderTree = await decryptFolderTree(data, key);
|
||||||
|
|
||||||
|
return decryptedFolderTree;
|
||||||
|
};
|
||||||
|
|
||||||
export const folderApi = {
|
export const folderApi = {
|
||||||
tree: () => axios.get<FolderTreeResponse>(`${API_URL}/folders/tree`),
|
tree: () => getFolderTree(),
|
||||||
list: () => axios.get<Folder[]>(`${API_URL}/folders`),
|
list: () => axios.get<Folder[]>(`${API_URL}/folders`),
|
||||||
create: (folder: FolderCreate) =>
|
create: (folder: FolderCreate) =>
|
||||||
axios.post<Folder>(`${API_URL}/folders`, folder),
|
axios.post<Folder>(`${API_URL}/folders`, folder),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { NoteRead } from "./folders";
|
||||||
|
import { deriveKey, encryptString, decryptString } from "./encryption";
|
||||||
|
|
||||||
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
|
const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api";
|
||||||
|
|
||||||
|
|
@ -18,82 +20,39 @@ export interface NoteCreate {
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive key from password
|
|
||||||
async function deriveKey(password: string) {
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
|
||||||
"raw",
|
|
||||||
enc.encode(password),
|
|
||||||
"PBKDF2",
|
|
||||||
false,
|
|
||||||
["deriveKey"],
|
|
||||||
);
|
|
||||||
|
|
||||||
return crypto.subtle.deriveKey(
|
|
||||||
{
|
|
||||||
name: "PBKDF2",
|
|
||||||
salt: enc.encode("your-app-salt"), // Store this somewhere consistent
|
|
||||||
iterations: 100000,
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
keyMaterial,
|
|
||||||
{ name: "AES-GCM", length: 256 },
|
|
||||||
false,
|
|
||||||
["encrypt", "decrypt"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt content
|
|
||||||
async function encryptNote(content: string, key: CryptoKey) {
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
|
|
||||||
const encrypted = await crypto.subtle.encrypt(
|
|
||||||
{ name: "AES-GCM", iv },
|
|
||||||
key,
|
|
||||||
enc.encode(content),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return IV + encrypted data as base64
|
|
||||||
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
||||||
combined.set(iv);
|
|
||||||
combined.set(new Uint8Array(encrypted), iv.length);
|
|
||||||
|
|
||||||
return btoa(String.fromCharCode(...combined));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt content
|
|
||||||
async function decryptNote(encrypted: string, key: CryptoKey) {
|
|
||||||
const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
|
|
||||||
const iv = combined.slice(0, 12);
|
|
||||||
const data = combined.slice(12);
|
|
||||||
|
|
||||||
const decrypted = await crypto.subtle.decrypt(
|
|
||||||
{ name: "AES-GCM", iv },
|
|
||||||
key,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new TextDecoder().decode(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNote = async (note: NoteCreate) => {
|
const createNote = async (note: NoteCreate) => {
|
||||||
if (!note.encrypted) {
|
var key = await deriveKey("Test");
|
||||||
return axios.post(`${API_URL}/notes`, note);
|
var noteContent = await encryptString(note.content, key);
|
||||||
} else {
|
var noteTitle = await encryptString(note.title, key);
|
||||||
var key = await deriveKey("Test");
|
|
||||||
var eNote = await encryptNote(note.content, key);
|
|
||||||
|
|
||||||
console.log(eNote);
|
var encryptedNote = {
|
||||||
|
title: noteTitle,
|
||||||
|
content: noteContent,
|
||||||
|
folder_id: note.folder_id,
|
||||||
|
};
|
||||||
|
|
||||||
var unENote = await decryptNote(eNote, key);
|
console.log(encryptedNote);
|
||||||
|
return axios.post(`${API_URL}/notes`, encryptedNote);
|
||||||
|
};
|
||||||
|
|
||||||
console.log(unENote);
|
const fetchNotes = async () => {
|
||||||
}
|
const { data } = await axios.get(`${API_URL}/notes`);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
var key = await deriveKey("Test");
|
||||||
|
const decryptedNotes = await Promise.all(
|
||||||
|
data.map(async (note: Note) => ({
|
||||||
|
...note,
|
||||||
|
title: await decryptString(note.title, key),
|
||||||
|
content: await decryptString(note.content, key),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return decryptedNotes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notesApi = {
|
export const notesApi = {
|
||||||
list: () => axios.get(`${API_URL}/notes`),
|
list: () => fetchNotes(),
|
||||||
get: (id: number) => axios.get(`${API_URL}/notes/${id}`),
|
get: (id: number) => axios.get(`${API_URL}/notes/${id}`),
|
||||||
create: (note: NoteCreate) => createNote(note),
|
create: (note: NoteCreate) => createNote(note),
|
||||||
update: (id: number, note: Partial<Note>) =>
|
update: (id: number, note: Partial<Note>) =>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const DraggableNote = ({
|
||||||
<div
|
<div
|
||||||
key={note.id}
|
key={note.id}
|
||||||
onClick={() => selectNote(note)}
|
onClick={() => selectNote(note)}
|
||||||
className={`ml-5 rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
|
className={` rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
|
||||||
selectedNote?.id === note.id
|
selectedNote?.id === note.id
|
||||||
? "bg-ctp-mauve text-ctp-base"
|
? "bg-ctp-mauve text-ctp-base"
|
||||||
: "hover:bg-ctp-surface1"
|
: "hover:bg-ctp-surface1"
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ function Home() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadFolderTree = async () => {
|
const loadFolderTree = async () => {
|
||||||
const { data } = await folderApi.tree();
|
const data = await folderApi.tree();
|
||||||
setFolderTree(data);
|
setFolderTree(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectNote = (note: NoteRead) => {
|
const selectNote = (note: NoteRead) => {
|
||||||
console.log("setting note.... " + note.id);
|
console.log(note);
|
||||||
setSelectedNote(note);
|
setSelectedNote(note);
|
||||||
setTitle(note.title);
|
setTitle(note.title);
|
||||||
setContent(note.content);
|
setContent(note.content);
|
||||||
|
|
@ -146,14 +146,16 @@ function Home() {
|
||||||
selectedFolder={selectedFolder}
|
selectedFolder={selectedFolder}
|
||||||
selectedNote={selectedNote}
|
selectedNote={selectedNote}
|
||||||
/>
|
/>
|
||||||
{folder.notes.map((note) => (
|
<div className="flex flex-col ml-5">
|
||||||
<DraggableNote
|
{folder.notes.map((note) => (
|
||||||
key={note.id}
|
<DraggableNote
|
||||||
note={note}
|
key={note.id}
|
||||||
selectNote={selectNote}
|
note={note}
|
||||||
selectedNote={selectedNote}
|
selectNote={selectNote}
|
||||||
/>
|
selectedNote={selectedNote}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{folder.children.map((child) => renderFolder(child, depth + 1))}
|
{folder.children.map((child) => renderFolder(child, depth + 1))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -176,14 +178,9 @@ function Home() {
|
||||||
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
|
<DndContext onDragEnd={handleDragEnd} autoScroll={false} sensors={sensors}>
|
||||||
<div className="flex bg-ctp-base min-h-screen text-ctp-text">
|
<div className="flex bg-ctp-base min-h-screen text-ctp-text">
|
||||||
<div
|
<div
|
||||||
className="bg-ctp-mantle border-r-ctp-surface2 border-r overflow-hidden"
|
className="bg-ctp-mantle border-r-ctp-surface2 border-r overflow-hidden w-[300px] p-4 overflow-y-auto sm:block hidden"
|
||||||
style={{
|
onDragOver={(e) => e.preventDefault()}
|
||||||
width: "300px",
|
onTouchMove={(e) => e.preventDefault()}
|
||||||
padding: "1rem",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => e.preventDefault()} // Add this
|
|
||||||
onTouchMove={(e) => e.preventDefault()} // And this for touch devices
|
|
||||||
>
|
>
|
||||||
<h2>Notes</h2>
|
<h2>Notes</h2>
|
||||||
<button
|
<button
|
||||||
|
|
@ -237,33 +234,22 @@ function Home() {
|
||||||
{/* Render orphaned notes */}
|
{/* Render orphaned notes */}
|
||||||
{folderTree?.orphaned_notes &&
|
{folderTree?.orphaned_notes &&
|
||||||
folderTree.orphaned_notes.length > 0 && (
|
folderTree.orphaned_notes.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex flex-col">
|
||||||
<div className="text-ctp-subtext0 text-sm mb-1">Unsorted</div>
|
<div className="text-ctp-subtext0 text-sm mb-1">Unsorted</div>
|
||||||
{folderTree.orphaned_notes.map((note) => (
|
{folderTree.orphaned_notes.map((note) => (
|
||||||
<div
|
<DraggableNote
|
||||||
key={note.id}
|
key={note.id}
|
||||||
onClick={() => selectNote(note)}
|
note={note}
|
||||||
className={`rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${
|
selectNote={selectNote}
|
||||||
selectedNote?.id === note.id
|
selectedNote={selectedNote}
|
||||||
? "bg-ctp-mauve text-ctp-base"
|
/>
|
||||||
: "hover:bg-ctp-surface1"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<i className="fadr fa-file text-xs"></i>
|
|
||||||
<span>{note.title}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={{
|
<div className="flex flex-col w-full">
|
||||||
flex: 1,
|
<div className="w-full bg-ctp-crust h-4"></div>
|
||||||
padding: "1rem",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Note title..."
|
placeholder="Note title..."
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue