E2E Test and setup
This commit is contained in:
parent
9645f411f3
commit
50d7f41248
6 changed files with 111 additions and 3058 deletions
BIN
backend/notes.db
BIN
backend/notes.db
Binary file not shown.
|
|
@ -15,13 +15,88 @@ export interface NoteCreate {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
folder_id: number | null;
|
folder_id: number | null;
|
||||||
|
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) => {
|
||||||
|
if (!note.encrypted) {
|
||||||
|
return axios.post(`${API_URL}/notes`, note);
|
||||||
|
} else {
|
||||||
|
var key = await deriveKey("Test");
|
||||||
|
var eNote = await encryptNote(note.content, key);
|
||||||
|
|
||||||
|
console.log(eNote);
|
||||||
|
|
||||||
|
var unENote = await decryptNote(eNote, key);
|
||||||
|
|
||||||
|
console.log(unENote);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const notesApi = {
|
export const notesApi = {
|
||||||
list: () => axios.get(`${API_URL}/notes`),
|
list: () => axios.get(`${API_URL}/notes`),
|
||||||
get: (id: number) => axios.get(`${API_URL}/notes/${id}`),
|
get: (id: number) => axios.get(`${API_URL}/notes/${id}`),
|
||||||
create: (note: NoteCreate) => axios.post(`${API_URL}/notes`, note),
|
create: (note: NoteCreate) => createNote(note),
|
||||||
update: (id: number, note: Partial) =>
|
update: (id: number, note: Partial<Note>) =>
|
||||||
axios.patch(`${API_URL}/notes/${id}`, note),
|
axios.patch(`${API_URL}/notes/${id}`, note),
|
||||||
delete: (id: number) => axios.delete(`${API_URL}/notes/${id}`),
|
delete: (id: number) => axios.delete(`${API_URL}/notes/${id}`),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
6
frontend/src/assets/fontawesome/js/fontawesome.min.js
vendored
Normal file
6
frontend/src/assets/fontawesome/js/fontawesome.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2,8 +2,8 @@ import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
import "./assets/fontawesome/js/fontawesome.min.js";
|
||||||
import "./assets/fontawesome/js/duotone-regular.js";
|
import "./assets/fontawesome/js/duotone-regular.js";
|
||||||
import "./assets/fontawesome/js/fontawesome.js ";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,13 @@ import {
|
||||||
} from "../api/folders";
|
} from "../api/folders";
|
||||||
import { NoteCreate, notesApi } from "../api/notes";
|
import { NoteCreate, notesApi } from "../api/notes";
|
||||||
import "../main.css";
|
import "../main.css";
|
||||||
import { DndContext, DragEndEvent } from "@dnd-kit/core";
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
|
||||||
import "@mdxeditor/editor/style.css";
|
import "@mdxeditor/editor/style.css";
|
||||||
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
|
import { DroppableFolder } from "../components/sidebar/DroppableFolder";
|
||||||
|
|
@ -50,7 +56,13 @@ function Home() {
|
||||||
const [newFolder, setNewFolder] = useState(false);
|
const [newFolder, setNewFolder] = useState(false);
|
||||||
const [newFolderText, setNewFolderText] = useState("");
|
const [newFolderText, setNewFolderText] = useState("");
|
||||||
const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
|
const [selectedFolder, setSelectedFolder] = useState<number | null>(null);
|
||||||
|
const [encrypted, setEncrypted] = useState(false);
|
||||||
|
const pointer = useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const sensors = useSensors(pointer);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFolderTree();
|
loadFolderTree();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -62,7 +74,12 @@ function Home() {
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
const newNote: NoteCreate = { title, content, folder_id: selectedFolder };
|
const newNote: NoteCreate = {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
folder_id: selectedFolder,
|
||||||
|
encrypted,
|
||||||
|
};
|
||||||
await notesApi.create(newNote);
|
await notesApi.create(newNote);
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setContent("#");
|
setContent("#");
|
||||||
|
|
@ -96,6 +113,7 @@ function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectNote = (note: NoteRead) => {
|
const selectNote = (note: NoteRead) => {
|
||||||
|
console.log("setting note.... " + note.id);
|
||||||
setSelectedNote(note);
|
setSelectedNote(note);
|
||||||
setTitle(note.title);
|
setTitle(note.title);
|
||||||
setContent(note.content);
|
setContent(note.content);
|
||||||
|
|
@ -155,7 +173,7 @@ function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext onDragEnd={handleDragEnd} autoScroll={false}>
|
<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"
|
||||||
|
|
@ -292,6 +310,11 @@ function Home() {
|
||||||
) : (
|
) : (
|
||||||
<button onClick={handleCreate}>Create Note</button>
|
<button onClick={handleCreate}>Create Note</button>
|
||||||
)}
|
)}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={encrypted}
|
||||||
|
onChange={() => setEncrypted(!encrypted)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue