mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-11-04 10:40:20 +00:00
Add runtime configuration loader and styles for web application
This commit is contained in:
1 parent
b8c4bcef0c
commit
363aec1652
8 files changed
+49
-58
No files matched your search
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NodeWarden Web</title>
|
||||
<link rel="stylesheet" href="/web/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/web/runtime-config.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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
|
||||
? ('<span class="vault-item-icon-wrap"><img class="vault-item-icon" src="/icons/'+esc(host)+'/icon.png" alt="" onerror="this.style.display=\\'none\\';this.nextElementSibling.style.display=\\'inline-flex\\';"><span class="vault-item-icon vault-item-icon-fallback" style="display:none;">🌐</span></span>')
|
||||
? ('<span class="vault-item-icon-wrap"><img class="vault-item-icon" src="/icons/'+esc(host)+'/icon.png" alt="" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'inline-flex\';"><span class="vault-item-icon vault-item-icon-fallback" style="display:none;">🌐</span></span>')
|
||||
: '<span class="vault-item-icon vault-item-icon-fallback">🌐</span>';
|
||||
rows += ''
|
||||
+ '<div class="vault-item '+(c.id===state.selectedCipherId?'active':'')+'" data-action="pick-cipher" data-id="'+esc(c.id)+'">'
|
||||
@@ -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;i<ids.length;i++) await authFetch('/api/ciphers/'+encodeURIComponent(ids[i]),{method:'DELETE'}); state.selectedMap={}; await loadVault(); render(); setMsg('Deleted 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;i<state.folders.length;i++){ var f=state.folders[i]; var label=(f.decName||f.name||f.id); opts.push(String(i+1)+') '+String(label)); } var pick=window.prompt('Move selected items to:\\n'+opts.join('\\n')+'\\n\\nInput number (empty to cancel):','0'); if(pick===null) return; pick=String(pick).trim(); if(!pick) return; var idx=Number(pick); if(!Number.isInteger(idx)||idx<0||idx>state.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;i<state.folders.length;i++){ var f=state.folders[i]; var label=(f.decName||f.name||f.id); opts.push(String(i+1)+') '+String(label)); } var pick=window.prompt('Move selected items to:\n'+opts.join('\n')+'\n\nInput number (empty to cancel):','0'); if(pick===null) return; pick=String(pick).trim(); if(!pick) return; var idx=Number(pick); if(!Number.isInteger(idx)||idx<0||idx>state.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<list.length;i++) state.selectedMap[list[i].id]=true; render(); return; }
|
||||
if(a==='select-none'){ state.selectedMap={}; render(); return; }
|
||||
if(a==='toggle-password'){ state.showSelectedPassword=!state.showSelectedPassword; render(); return; }
|
||||
if(a==='copy-totp-current'){ var tv=document.getElementById('totp-live-value'); var txt=tv?String(tv.textContent||'').replace(/\\s+/g,''):''; if(!txt) return setMsg('No current TOTP code.', 'err'); navigator.clipboard.writeText(txt).then(function(){ setMsg('Copied to clipboard.', 'ok'); }).catch(function(){ setMsg('Copy failed.', 'err'); }); return; }
|
||||
if(a==='copy-totp-current'){ var tv=document.getElementById('totp-live-value'); var txt=tv?String(tv.textContent||'').replace(/\s+/g,''):''; if(!txt) return setMsg('No current TOTP code.', 'err'); navigator.clipboard.writeText(txt).then(function(){ setMsg('Copied to clipboard.', 'ok'); }).catch(function(){ setMsg('Copy failed.', 'err'); }); return; }
|
||||
if(a==='copy-field'){ var val=n.getAttribute('data-value')||''; navigator.clipboard.writeText(val).then(function(){ setMsg('Copied to clipboard.', 'ok'); }).catch(function(){ setMsg('Copy failed.', 'err'); }); return; }
|
||||
if(a==='open-uri'){ var v=n.getAttribute('data-value')||''; if(!v) return; if(!/^https?:\\/\\//i.test(v)) v='https://'+v; window.open(v, '_blank', 'noopener'); return; }
|
||||
if(a==='open-uri'){ var v=n.getAttribute('data-value')||''; if(!v) return; if(!/^https?:\/\//i.test(v)) v='https://'+v; window.open(v, '_blank', 'noopener'); return; }
|
||||
if(a==='bulk-delete') return void onBulkDelete();
|
||||
if(a==='bulk-move') return void onBulkMove();
|
||||
if(a==='vault-refresh'){ loadVault().then(function(){ render(); setMsg('Vault refreshed.', 'ok'); }).catch(function(e){ setMsg('Refresh failed: '+(e&&e.message?e.message:String(e)), 'err'); }); return; }
|
||||
@@ -1516,8 +1515,4 @@
|
||||
});
|
||||
|
||||
init();
|
||||
})();
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { startNodewardenApp } from './app.js';
|
||||
|
||||
async function loadRuntimeConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/web/config', { method: 'GET' });
|
||||
if (!resp.ok) throw new Error('runtime config request failed');
|
||||
return await resp.json();
|
||||
} catch {
|
||||
return { defaultKdfIterations: 600000 };
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = await loadRuntimeConfig();
|
||||
startNodewardenApp(cfg || { defaultKdfIterations: 600000 });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const WEB_CLIENT_STYLES = `
|
||||
|
||||
:root {
|
||||
--bg: #F3F5F8;
|
||||
--panel: #FFFFFF;
|
||||
@@ -561,5 +561,4 @@
|
||||
.vault-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Env } from '../types';
|
||||
import { htmlResponse } from '../utils/response';
|
||||
import { renderWebClientHTML } from '../webclient/page';
|
||||
|
||||
export async function handleWebClientPage(request: Request, env: Env): Promise<Response> {
|
||||
void request;
|
||||
void env;
|
||||
return htmlResponse(renderWebClientHTML());
|
||||
}
|
||||
+7
-6
@@ -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<Respons
|
||||
// Route matching
|
||||
try {
|
||||
|
||||
// Web client entry (single-path app)
|
||||
if ((path === '/' || path === '/register' || path === '/login' || path === '/setup' || path === '/setup/legacy') && method === 'GET') {
|
||||
return handleWebClientPage(request, env);
|
||||
}
|
||||
|
||||
// Setup status
|
||||
if (path === '/setup/status' && method === 'GET') {
|
||||
return handleSetupStatus(request, env);
|
||||
}
|
||||
|
||||
// Web runtime config for static client bootstrap
|
||||
if (path === '/api/web/config' && method === 'GET') {
|
||||
return jsonResponse({
|
||||
defaultKdfIterations: LIMITS.auth.defaultKdfIterations,
|
||||
});
|
||||
}
|
||||
|
||||
// Browser/devtools probe endpoint
|
||||
if (path === '/.well-known/appspecific/com.chrome.devtools.json' && method === 'GET') {
|
||||
return new Response('{}', {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { LIMITS } from '../config/limits';
|
||||
import { renderWebClientScript } from './script';
|
||||
import { WEB_CLIENT_STYLES } from './styles';
|
||||
|
||||
export function renderWebClientHTML(): string {
|
||||
const defaultKdfIterations = LIMITS.auth.defaultKdfIterations;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NodeWarden Web</title>
|
||||
<style>
|
||||
${WEB_CLIENT_STYLES}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
${renderWebClientScript(defaultKdfIterations)}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user