diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..4b4f505 --- /dev/null +++ b/public/index.html @@ -0,0 +1,14 @@ + + + + + + NodeWarden Web + + + +
+ + + + diff --git a/src/webclient/script.ts b/public/web/app.js similarity index 98% rename from src/webclient/script.ts rename to public/web/app.js index 4eda343..d28ca4d 100644 --- a/src/webclient/script.ts +++ b/public/web/app.js @@ -1,8 +1,7 @@ -export function renderWebClientScript(defaultKdfIterations: number): string { - return ` -(function () { + +export function startNodewardenApp(runtimeConfig) { var app = document.getElementById('app'); - var defaultKdfIterations = ${defaultKdfIterations}; + var defaultKdfIterations = Number(runtimeConfig && runtimeConfig.defaultKdfIterations) || 600000; var state = { phase: 'loading', lang: (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en', @@ -429,7 +428,7 @@ function hostFromUri(uri){ try{ if(!uri) return ''; - var fixed=/^https?:\\/\\//i.test(uri)?uri:('https://'+uri); + var fixed=/^https?:\/\//i.test(uri)?uri:('https://'+uri); return new URL(fixed).hostname; }catch(e){ return ''; } } @@ -452,7 +451,7 @@ function extractTotpSecret(raw){ var s=String(raw||'').trim(); if(!s) return ''; - if(/^otpauth:\\/\\//i.test(s)){ + if(/^otpauth:\/\//i.test(s)){ try{ var u=new URL(s); var sec=u.searchParams.get('secret'); @@ -1097,7 +1096,7 @@ var uri=firstCipherUri(c); var host=hostFromUri(uri); var icon=host - ? ('') + ? ('') : '🌐'; rows += '' + '
' @@ -1376,7 +1375,7 @@ setMsg('Change master password failed: '+(e&&e.message?e.message:String(e)), 'err'); } } - async function onEnableTotp(form){ var fd=new FormData(form); state.totpSetupSecret=String(fd.get('secret')||'').toUpperCase().replace(/[\\s-]/g,'').replace(/=+$/g,''); state.totpSetupToken=String(fd.get('token')||'').trim(); if(!state.totpSetupSecret) return setMsg('TOTP secret is required.', 'err'); if(!state.totpSetupToken) return setMsg('TOTP token is required.', 'err'); var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:true,secret:state.totpSetupSecret,token:state.totpSetupToken})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Enable TOTP failed.', 'err'); state.totpSetupToken=''; render(); setMsg('TOTP enabled.', 'ok'); } + async function onEnableTotp(form){ var fd=new FormData(form); state.totpSetupSecret=String(fd.get('secret')||'').toUpperCase().replace(/[\s-]/g,'').replace(/=+$/g,''); state.totpSetupToken=String(fd.get('token')||'').trim(); if(!state.totpSetupSecret) return setMsg('TOTP secret is required.', 'err'); if(!state.totpSetupToken) return setMsg('TOTP token is required.', 'err'); var r=await authFetch('/api/accounts/totp',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:true,secret:state.totpSetupSecret,token:state.totpSetupToken})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Enable TOTP failed.', 'err'); state.totpSetupToken=''; render(); setMsg('TOTP enabled.', 'ok'); } function onDisableTotp(){ state.totpDisableOpen=true; state.totpDisablePassword=''; state.totpDisableError=''; render(); } async function onDisableTotpSubmit(form){ var fd=new FormData(form); state.totpDisablePassword=String(fd.get('masterPassword')||''); @@ -1395,7 +1394,7 @@ } async function onBulkDelete(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); if(!window.confirm('Delete selected '+ids.length+' items?')) return; for(var i=0;istate.folders.length) return setMsg('Invalid folder selection.', 'err'); var folderId=idx===0?null:state.folders[idx-1].id; var r=await authFetch('/api/ciphers/move',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,folderId:folderId})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Bulk move failed.', 'err'); await loadVault(); render(); setMsg('Moved selected items.', 'ok'); } + async function onBulkMove(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); var opts=['0) No folder']; for(var i=0;istate.folders.length) return setMsg('Invalid folder selection.', 'err'); var folderId=idx===0?null:state.folders[idx-1].id; var r=await authFetch('/api/ciphers/move',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,folderId:folderId})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Bulk move failed.', 'err'); await loadVault(); render(); setMsg('Moved selected items.', 'ok'); } async function onCreateInvite(form){ var fd=new FormData(form); var h=Number(fd.get('hours')||168); var r=await authFetch('/api/admin/invites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expiresInHours:h})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Create invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite created.', 'ok'); } async function onToggleUserStatus(id,status){ var n=status==='active'?'banned':'active'; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id)+'/status',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:n})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Update user status failed.', 'err'); await loadAdminData(); render(); setMsg('User status updated.', 'ok'); } @@ -1449,9 +1448,9 @@ if(a==='select-all'){ var list=filteredCiphers(); state.selectedMap={}; for(var i=0;i { - void request; - void env; - return htmlResponse(renderWebClientHTML()); -} diff --git a/src/router.ts b/src/router.ts index 6a2b472..b3139a7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -49,7 +49,6 @@ import { handleSync } from './handlers/sync'; // Setup handlers import { handleSetupStatus } from './handlers/setup'; -import { handleWebClientPage } from './handlers/web'; import { handleKnownDevice, handleGetDevices, handleUpdateDeviceToken } from './handlers/devices'; // Import handler @@ -186,16 +185,18 @@ export async function handleRequest(request: Request, env: Env): Promise - - - - - NodeWarden Web - - - -
- - -`; -} diff --git a/wrangler.toml b/wrangler.toml index 6d578b5..e6c8961 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,6 +1,7 @@ name = "nodewarden" main = "src/index.ts" compatibility_date = "2024-01-01" +assets = { directory = "./public", not_found_handling = "single-page-application", run_worker_first = ["/api/*", "/identity/*", "/icons/*", "/setup/*", "/config", "/notifications/*", "/.well-known/*", "/favicon.ico", "/favicon.svg"] } # D1 Database for storing vault data [[d1_databases]]