@@ -13,11 +13,50 @@ import CopyableField from './CopyableField';
1313
1414const IntegrationsTab = ( { settings, onChange, commit, isDarkTheme } ) => {
1515 const foxitEnabled = settings . FOXIT_ENABLED === true || settings . FOXIT_ENABLED === 'true' ;
16+ const proxyEnabled = settings . PROXY_ENABLED === true || settings . PROXY_ENABLED === 'true' ;
1617 const { showToast } = useToast ( ) ;
1718
1819 const [ jinaLoading , setJinaLoading ] = useState ( false ) ;
1920 const [ tmdbTesting , setTmdbTesting ] = useState ( false ) ;
2021 const [ foxitTokenCopied , setFoxitTokenCopied ] = useState ( false ) ;
22+ const [ proxyTokenCopied , setProxyTokenCopied ] = useState ( false ) ;
23+ const [ proxyTesting , setProxyTesting ] = useState ( false ) ;
24+
25+ const serverOrigin = typeof window !== 'undefined' ? window . location . origin : '' ;
26+
27+ const generateProxyToken = useCallback ( ( ) => {
28+ const bytes = new Uint8Array ( 24 ) ;
29+ window . crypto . getRandomValues ( bytes ) ;
30+ const token = Array . from ( bytes , ( b ) => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
31+ commit ( { ...settings , PROXY_TOKEN : token } ) ;
32+ if ( navigator . clipboard ) {
33+ navigator . clipboard . writeText ( token ) . then (
34+ ( ) => { setProxyTokenCopied ( true ) ; setTimeout ( ( ) => setProxyTokenCopied ( false ) , 2500 ) ; } ,
35+ ( ) => { }
36+ ) ;
37+ }
38+ } , [ settings , commit ] ) ;
39+
40+ const clearProxyToken = useCallback ( ( ) => {
41+ setProxyTokenCopied ( false ) ;
42+ commit ( { ...settings , PROXY_TOKEN : '' } ) ;
43+ } , [ settings , commit ] ) ;
44+
45+ const testProxyConnection = useCallback ( async ( ) => {
46+ setProxyTesting ( true ) ;
47+ try {
48+ const data = await notesApi . testProxyConnection ( ) ;
49+ if ( data . connected ) {
50+ showToast ( 'Proxy agent is connected' , { variant : 'success' } ) ;
51+ } else {
52+ showToast ( 'No proxy agent connected' , { variant : 'error' } ) ;
53+ }
54+ } catch ( e ) {
55+ showToast ( 'Could not reach server' , { variant : 'error' } ) ;
56+ } finally {
57+ setProxyTesting ( false ) ;
58+ }
59+ } , [ showToast ] ) ;
2160
2261 // Fill the Foxit snooper token with a fresh random secret and copy it, since
2362 // the same value has to be pasted into the snooper on the other machine.
@@ -44,8 +83,8 @@ const IntegrationsTab = ({ settings, onChange, commit, isDarkTheme }) => {
4483 const [ extLoading , setExtLoading ] = useState ( false ) ;
4584 const [ extToken , setExtToken ] = useState ( null ) ;
4685 const [ extError , setExtError ] = useState ( null ) ;
47- // The address the extension should point at — this app's own origin.
48- const serverOrigin = typeof window !== 'undefined' ? window . location . origin : '' ;
86+ // The address the extension (and proxy agent) should point at — this app's own origin.
87+ // serverOrigin is already declared above.
4988
5089 const generateExtensionToken = useCallback ( async ( ) => {
5190 setExtLoading ( true ) ;
@@ -100,6 +139,99 @@ const IntegrationsTab = ({ settings, onChange, commit, isDarkTheme }) => {
100139
101140 return (
102141 < >
142+ < SectionContainer >
143+ < div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : '8px' } } >
144+ < SectionTitle style = { { margin : 0 } } > Proxy</ SectionTitle >
145+ < button
146+ onClick = { testProxyConnection }
147+ disabled = { proxyTesting }
148+ style = { {
149+ background : 'none' ,
150+ border : '1px solid var(--border-color)' ,
151+ borderRadius : '6px' ,
152+ padding : '4px 10px' ,
153+ fontSize : '12px' ,
154+ cursor : proxyTesting ? 'not-allowed' : 'pointer' ,
155+ color : 'var(--text-secondary-color)' ,
156+ opacity : proxyTesting ? 0.6 : 1 ,
157+ } }
158+ >
159+ { proxyTesting ? 'Testing…' : 'Test connection' }
160+ </ button >
161+ </ div >
162+ < p style = { { fontSize : '14px' , color : 'var(--text-secondary-color)' } } >
163+ Route URL fetches (link previews, article extraction) through a proxy instead of the server.
164+ Useful when the server's IP is blocked by certain sites — YouTube thumbnails, geo-restricted content, etc.
165+ The proxy connects outbound; no port forwarding needed.
166+ </ p >
167+ < FormGroup style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' } } >
168+ < Label style = { { marginBottom : 0 } } > Enable proxy</ Label >
169+ < Switch
170+ id = "proxy-enabled-toggle"
171+ checked = { proxyEnabled }
172+ onChange = { ( ) => commit ( { ...settings , PROXY_ENABLED : ! proxyEnabled } ) }
173+ />
174+ </ FormGroup >
175+ { proxyEnabled && (
176+ < >
177+ < FormGroup >
178+ < CopyableField
179+ label = "Server URL"
180+ value = { serverOrigin }
181+ isDark = { isDarkTheme }
182+ copyTitle = "Copy server URL"
183+ />
184+ </ FormGroup >
185+ < FormGroup >
186+ { settings . PROXY_TOKEN ? (
187+ < >
188+ < CopyableField
189+ label = "Proxy token"
190+ value = { settings . PROXY_TOKEN }
191+ isDark = { isDarkTheme }
192+ copyTitle = "Copy token"
193+ />
194+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '8px' , marginTop : '8px' } } >
195+ < button
196+ onClick = { generateProxyToken }
197+ style = { { background : 'none' , border : '1px solid var(--border-color)' , borderRadius : '6px' , padding : '4px 10px' , fontSize : '12px' , color : 'var(--text-secondary-color)' , cursor : 'pointer' } }
198+ >
199+ Regenerate
200+ </ button >
201+ < button
202+ onClick = { clearProxyToken }
203+ style = { { background : 'none' , border : '1px solid var(--border-color)' , borderRadius : '6px' , padding : '4px 10px' , fontSize : '12px' , color : 'var(--text-secondary-color)' , cursor : 'pointer' } }
204+ >
205+ Clear
206+ </ button >
207+ < span style = { { fontSize : '12px' , color : 'var(--text-secondary-color)' } } >
208+ { proxyTokenCopied ? 'Copied!' : 'Keep this secret.' }
209+ </ span >
210+ </ div >
211+ </ >
212+ ) : (
213+ < >
214+ < Label > Proxy token</ Label >
215+ < button
216+ onClick = { generateProxyToken }
217+ style = { {
218+ background : 'none' ,
219+ border : '1px solid var(--border-color)' ,
220+ borderRadius : '6px' ,
221+ padding : '8px 14px' ,
222+ fontSize : '13px' ,
223+ cursor : 'pointer' ,
224+ color : 'var(--text-secondary-color)' ,
225+ } }
226+ >
227+ Generate token
228+ </ button >
229+ </ >
230+ ) }
231+ </ FormGroup >
232+ </ >
233+ ) }
234+ </ SectionContainer >
103235 < SectionContainer >
104236 < SectionTitle > Browser Extension (itsnotes clipper)</ SectionTitle >
105237 < p style = { { fontSize : '14px' , color : 'var(--text-secondary-color)' } } >
0 commit comments