@@ -60,6 +60,8 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
6060 noteEditorTags : '' ,
6161 noteEditorReminder : '' ,
6262 noteEditorBodyEncoded : true ,
63+ noteEditorIsEncrypted : false ,
64+ currentNotePassword : null ,
6365
6466 // --- Notification Data ---
6567 notificationsEnabled : false ,
@@ -384,7 +386,11 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
384386 minute : '2-digit' ,
385387 } )
386388 } ,
387- getNotePreview ( content ) {
389+ getNotePreview ( note ) {
390+ if ( note ?. isEncrypted ) {
391+ return '**********' ;
392+ }
393+ const content = note ?. content ;
388394 return content ? `${ content . trim ( ) . replace ( / ( \r ? \n ) + / g, '\n' ) . substring ( 0 , 300 ) } ...` : '<!-- EMPTY -->' ;
389395 } ,
390396
@@ -1024,7 +1030,7 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
10241030 }
10251031 } ,
10261032
1027- async addNote ( title , content , reminder , tags , bodyEncoded ) {
1033+ async addNote ( title , content , reminder , tags , bodyEncoded , isEncrypted = false ) {
10281034 try {
10291035 const now = new Date ( ) . toISOString ( ) ;
10301036 const newNote = {
@@ -1037,6 +1043,7 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
10371043 tags : tags || [ ] ,
10381044 priority : 0 ,
10391045 bodyEncoded : bodyEncoded !== false ,
1046+ isEncrypted : ! ! isEncrypted ,
10401047 } ;
10411048 await addNoteDB ( newNote ) ;
10421049 this . notes . unshift ( newNote ) ;
@@ -1104,11 +1111,20 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
11041111 const content = window . easyMDEInstance ?. value ( ) ;
11051112 if ( content === null || content === undefined ) return ;
11061113
1114+ let finalContent = content ;
1115+ if ( this . noteEditorIsEncrypted ) {
1116+ if ( ! this . currentNotePassword ) {
1117+ return ; // Cannot safely encrypt without password, skip autosave
1118+ }
1119+ finalContent = await _GLOBAL . encrypt ( finalContent , this . currentNotePassword ) ;
1120+ }
1121+
11071122 const noteData = {
11081123 title : this . noteEditorTitle . trim ( ) || ( 'Note at ' + new Date ( ) . toString ( ) . substr ( 0 , 21 ) ) ,
1109- content : content ,
1124+ content : finalContent ,
11101125 reminder : this . noteEditorReminder . trim ( ) !== '' ? this . noteEditorReminder : undefined ,
11111126 tags : this . noteEditorTags . split ( ',' ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ,
1127+ isEncrypted : this . noteEditorIsEncrypted ,
11121128 } ;
11131129
11141130 if ( noteInDb . title === noteData . title &&
@@ -1754,6 +1770,13 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
17541770 } ,
17551771 className : "fa fa-share-square-o" ,
17561772 title : "Share" ,
1773+ } , {
1774+ name : "Encrypt" ,
1775+ action : function ( editor ) {
1776+ Alpine . $data ( document . querySelector ( 'body' ) ) ?. toggleNoteEncryption ( ) ;
1777+ } ,
1778+ className : "fa fa-unlock-alt" ,
1779+ title : "Encrypt / Decrypt Note" ,
17571780 } ,
17581781 "|" ,
17591782 "bold" , "italic" , "heading" ,
@@ -2038,6 +2061,10 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
20382061 && window . easyMDEInstance ?. togglePreview ?. ( )
20392062 ) ;
20402063
2064+ this . $nextTick ( ( ) => {
2065+ this . updatePadlockIcon ( ) ;
2066+ } ) ;
2067+
20412068 this . noteEditorVisible = false ;
20422069 this . easyMDEIniting = false ;
20432070 } catch ( ex ) {
@@ -2224,9 +2251,31 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
22242251 const note = isId ? await this . getNote ( id ) : noteOrId ;
22252252
22262253 if ( note ) {
2254+ let finalContent = note . content ;
2255+ this . noteEditorIsEncrypted = ! ! note . isEncrypted ;
2256+ this . currentNotePassword = null ;
2257+
2258+ if ( note . isEncrypted ) {
2259+ const pwd = prompt ( "This note is encrypted. Please enter the password to view/edit:" ) ;
2260+ if ( ! pwd ) {
2261+ this . showToast ( { variant : 'error' , title : 'Cancelled' , description : 'Cannot view note without password.' } ) ;
2262+ this . cancelEdit ( ) ;
2263+ return ;
2264+ }
2265+ try {
2266+ finalContent = await _GLOBAL . decrypt ( note . content , pwd ) ;
2267+ this . currentNotePassword = pwd ;
2268+ } catch ( e ) {
2269+ console . error ( "Decryption failed:" , e ) ;
2270+ this . showToast ( { variant : 'error' , title : 'Decryption Failed' , description : 'Incorrect password or corrupted content.' } ) ;
2271+ this . cancelEdit ( ) ;
2272+ return ;
2273+ }
2274+ }
2275+
22272276 this . noteEditorTitle = note . title ;
2228- this . noteEditorContent = note . content ;
2229- this . noteEditorBaseContent = note . content ; // Capture base content for merge
2277+ this . noteEditorContent = finalContent ;
2278+ this . noteEditorBaseContent = finalContent ; // Capture base content for merge
22302279 this . noteEditorReminder = note . reminder || '' ;
22312280 this . noteEditorTags = note . tags ? note . tags . join ( ', ' ) : '' ;
22322281 this . noteEditorBodyEncoded = note . bodyEncoded !== false ;
@@ -2238,6 +2287,49 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
22382287 this . $nextTick ( ( ) => this . prepareEasyMDE ( id , options ) ) ;
22392288 } ,
22402289
2290+ toggleNoteEncryption ( ) {
2291+ const app = this ;
2292+ if ( app . noteEditorIsEncrypted ) {
2293+ // Ask if they want to decrypt the note (remove encryption)
2294+ const confirmDecrypt = confirm ( "Do you want to decrypt this note (remove password protection permanently)?" ) ;
2295+ if ( confirmDecrypt ) {
2296+ app . noteEditorIsEncrypted = false ;
2297+ app . currentNotePassword = null ;
2298+ app . showToast ( { title : 'Decryption Pending' , description : 'Save the note to remove password protection.' } ) ;
2299+ app . updatePadlockIcon ( ) ;
2300+ }
2301+ } else {
2302+ // Prompt for a password to encrypt
2303+ const pwd = prompt ( "Enter a password to encrypt this note (this password is NOT stored anywhere):" ) ;
2304+ if ( ! pwd ) {
2305+ app . showToast ( { variant : 'error' , title : 'Cancelled' , description : 'Encryption was cancelled.' } ) ;
2306+ return ;
2307+ }
2308+ const pwdConfirm = prompt ( "Please confirm your password:" ) ;
2309+ if ( pwd !== pwdConfirm ) {
2310+ app . showToast ( { variant : 'error' , title : 'Error' , description : 'Passwords do not match.' } ) ;
2311+ return ;
2312+ }
2313+ app . noteEditorIsEncrypted = true ;
2314+ app . currentNotePassword = pwd ;
2315+ app . showToast ( { title : 'Encryption Pending' , description : 'Save the note to finalize password protection.' } ) ;
2316+ app . updatePadlockIcon ( ) ;
2317+ }
2318+ } ,
2319+
2320+ updatePadlockIcon ( ) {
2321+ const lockBtn = document . querySelector ( '.EasyMDEContainer .editor-toolbar button.fa-lock, .EasyMDEContainer .editor-toolbar button.fa-unlock-alt' ) ;
2322+ if ( lockBtn ) {
2323+ if ( this . noteEditorIsEncrypted ) {
2324+ lockBtn . className = 'fa fa-lock active text-yellow-500' ;
2325+ lockBtn . title = 'Note is Encrypted (Click to Decrypt)' ;
2326+ } else {
2327+ lockBtn . className = 'fa fa-unlock-alt' ;
2328+ lockBtn . title = 'Note is Unencrypted (Click to Encrypt)' ;
2329+ }
2330+ }
2331+ } ,
2332+
22412333 async saveNote ( isAuto ) {
22422334 if ( this . isSaving ) return ;
22432335 this . isSaving = true ;
@@ -2250,18 +2342,34 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
22502342
22512343 const tags = this . noteEditorTags . split ( ',' ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ;
22522344
2345+ let rawContent = window . easyMDEInstance ?. value ( ) || this . noteEditorContent ;
2346+ let isEncrypted = this . noteEditorIsEncrypted ;
2347+
2348+ if ( isEncrypted ) {
2349+ if ( ! this . currentNotePassword ) {
2350+ const pwd = prompt ( "Enter a password to encrypt this note:" ) ;
2351+ if ( ! pwd ) {
2352+ this . showToast ( { variant : 'error' , title : 'Error' , description : 'A password is required to save an encrypted note.' } ) ;
2353+ return ;
2354+ }
2355+ this . currentNotePassword = pwd ;
2356+ }
2357+ rawContent = await _GLOBAL . encrypt ( rawContent , this . currentNotePassword ) ;
2358+ }
2359+
22532360 const noteData = {
22542361 title : finalTitle ,
2255- content : window . easyMDEInstance ?. value ( ) || this . noteEditorContent ,
2362+ content : rawContent ,
22562363 reminder : this . noteEditorReminder . trim ( ) !== '' ? this . noteEditorReminder : undefined ,
22572364 tags : tags ,
22582365 bodyEncoded : this . noteEditorBodyEncoded ,
2366+ isEncrypted : isEncrypted ,
22592367 } ;
22602368
22612369 if ( this . noteEditorNoteId && this . noteEditorNoteId !== 'new' ) {
22622370 await this . updateNote ( this . noteEditorNoteId , noteData ) ;
22632371 } else {
2264- const newNote = await this . addNote ( noteData . title , noteData . content , noteData . reminder , noteData . tags , noteData . bodyEncoded ) ;
2372+ const newNote = await this . addNote ( noteData . title , noteData . content , noteData . reminder , noteData . tags , noteData . bodyEncoded , isEncrypted ) ;
22652373 if ( newNote ) {
22662374 this . noteEditorNoteId = newNote . id ;
22672375 }
@@ -2332,13 +2440,37 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
23322440 const credentials = await decryptSettings ( encryptedSettings , this . userId ) ;
23332441 if ( credentials && credentials . s3Bucket ) {
23342442 try {
2335- this . showToast ( { title : 'Publishing to S3...' , description : 'Creating a shareable link via S3.' } ) ;
2443+ let shareHtml = marked . parse ( content ) ;
2444+ const encryptShare = confirm ( "Do you want to encrypt this shared S3 link with a password?" ) ;
2445+ if ( encryptShare ) {
2446+ const sharePassword = prompt ( "Enter a password for the viewer of this link (do not forget it!):" ) ;
2447+ if ( ! sharePassword ) {
2448+ this . showToast ( { variant : 'error' , title : 'Cancelled' , description : 'Sharing cancelled.' } ) ;
2449+ return ;
2450+ }
2451+
2452+ const wrappedHtml = `<h1 style="font-size: 2.25rem; font-weight: 800; margin-bottom: 1.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e5e7eb;">${ this . noteEditorTitle } </h1><div>${ shareHtml } </div>` ;
2453+ const encryptedPayload = await _GLOBAL . encrypt ( wrappedHtml , sharePassword ) ;
2454+
2455+ try {
2456+ const template = await fetch ( '/self-decrypting.html' ) . then ( r => r . text ( ) ) ;
2457+ shareHtml = template
2458+ . replace ( '{{TITLE}}' , this . noteEditorTitle )
2459+ . replace ( '{{ENCRYPTED_PAYLOAD}}' , encryptedPayload ) ;
2460+ } catch ( e ) {
2461+ console . error ( 'Failed to load self-decrypting template:' , e ) ;
2462+ this . showToast ( { variant : 'error' , title : 'Error' , description : 'Could not load the encryption template.' } ) ;
2463+ return ;
2464+ }
2465+ }
2466+
2467+ this . showToast ( { title : 'Publishing to S3...' , description : 'Creating a shareable link via S3.' } ) ;
23362468 const note = {
23372469 id : this . noteEditorNoteId || generateUniqueId ( this . noteEditorTitle ) ,
23382470 title : this . noteEditorTitle ,
23392471 content : content ,
23402472 updatedAt : new Date ( ) . toISOString ( ) ,
2341- html : marked . parse ( content ) ,
2473+ html : encryptShare ? shareHtml : marked . parse ( content ) ,
23422474 } ;
23432475 await uploadNoteToS3 ( note , credentials ) ;
23442476 const url = await getPresignedUrl ( note , credentials ) ;
@@ -2369,14 +2501,29 @@ document.addEventListener('alpine:init', () => { Alpine.data('mainApp', () => ({
23692501 // Fallback using server storage
23702502 try {
23712503 this . showToast ( { title : 'Publishing...' , description : 'Creating a shareable link via the server.' } ) ;
2504+
2505+ let finalContent = content ;
2506+ let isEncrypted = false ;
2507+ const encryptShare = confirm ( "Do you want to encrypt this shared server link with a password?" ) ;
2508+ if ( encryptShare ) {
2509+ const sharePassword = prompt ( "Enter a password for the viewer of this link (do not forget it!):" ) ;
2510+ if ( ! sharePassword ) {
2511+ this . showToast ( { variant : 'error' , title : 'Cancelled' , description : 'Sharing cancelled.' } ) ;
2512+ return ;
2513+ }
2514+ finalContent = await _GLOBAL . encrypt ( finalContent , sharePassword ) ;
2515+ isEncrypted = true ;
2516+ }
2517+
23722518 const response = await fetch ( '/api/publish' , {
23732519 method : 'POST' ,
23742520 headers : {
23752521 'Content-Type' : 'application/json' ,
23762522 } ,
23772523 body : JSON . stringify ( {
23782524 title : this . noteEditorTitle ,
2379- content : content
2525+ content : finalContent ,
2526+ isEncrypted : isEncrypted
23802527 } ) ,
23812528 } ) ;
23822529
0 commit comments