diff --git a/public/index.html b/public/index.html index 4b4f505..7b1ecd6 100644 --- a/public/index.html +++ b/public/index.html @@ -8,7 +8,7 @@
+ - diff --git a/public/web/main.js b/public/web/main.js index 8e96fc5..6d8d5b1 100644 --- a/public/web/main.js +++ b/public/web/main.js @@ -214,6 +214,25 @@ export function startNodewardenApp(runtimeConfig) { function randomBase32Secret(len){ var a='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; var b=crypto.getRandomValues(new Uint8Array(len)); var o=''; for(var i=0;i'; + }catch(_){ + box.innerHTML='
QR unavailable
Use secret key below
'; + } + } function buildSymmetricKeyBytes(){ if(!state.session||!state.session.symEncKey||!state.session.symMacKey) return null; try{ @@ -833,13 +852,12 @@ export function startNodewardenApp(runtimeConfig) { function renderSettingsTab(){ var p=state.profile||{}; var secret=currentTotpSecret(); - var qr='https://api.qrserver.com/v1/create-qr-code/?size=180x180&data='+encodeURIComponent(buildTotpUri(secret)); return '' + renderMsg() + '

'+t('settings')+'

' + '

'+t('profile')+'

' + '

'+t('changePwd')+'

After success, current sessions are revoked and you must log in again.
' - + '

'+t('totpSetup')+'

TOTP QR
Disable action prompts for master password.
'; + + '

'+t('totpSetup')+'

QR loading...
Use secret key below
Disable action prompts for master password.
'; } function renderTotpDisableModal(){ if(!state.totpDisableOpen) return ''; @@ -937,6 +955,7 @@ export function startNodewardenApp(runtimeConfig) { if(state.phase==='login'){ app.innerHTML=renderLoginScreen(); return; } app.innerHTML=renderApp(); updateLiveTotpDisplay(); + renderTotpSetupQr(); } async function init(){ diff --git a/public/web/runtime-config.js b/public/web/runtime-config.js index 58f541c..427a7f4 100644 --- a/public/web/runtime-config.js +++ b/public/web/runtime-config.js @@ -1,5 +1,17 @@ import { startNodewardenApp } from './app.js'; +async function ensureQrLibrary() { + if (typeof window.qrcode === 'function') return; + await new Promise((resolve) => { + const s = document.createElement('script'); + s.src = '/web/vendor/qrcode-generator.min.js'; + s.async = true; + s.onload = () => resolve(null); + s.onerror = () => resolve(null); + document.head.appendChild(s); + }); +} + async function loadRuntimeConfig() { try { const resp = await fetch('/api/web/config', { method: 'GET' }); @@ -10,6 +22,6 @@ async function loadRuntimeConfig() { } } +await ensureQrLibrary(); const cfg = await loadRuntimeConfig(); startNodewardenApp(cfg || { defaultKdfIterations: 600000 }); - diff --git a/public/web/styles.css b/public/web/styles.css index a33ae81..7f4845f 100644 --- a/public/web/styles.css +++ b/public/web/styles.css @@ -166,6 +166,30 @@ } .alert-success { background: var(--success-bg); color: var(--success); border-color: #BADBCC; } .alert-danger { background: var(--danger-bg); color: var(--danger); border-color: #F5C2C7; } + .totp-qr-card { + background:#fff; + padding:16px; + border:1px solid var(--border-color); + border-radius:8px; + width: 200px; + min-height: 200px; + display:flex; + align-items:center; + justify-content:center; + } + .totp-qr-fallback { + width:100%; + min-height:168px; + border:1px dashed var(--border-color); + border-radius:8px; + display:flex; + flex-direction:column; + align-items:center; + justify-content:center; + color:var(--text-secondary); + text-align:center; + padding:8px; + } /* App Layout */ .navbar { @@ -561,4 +585,3 @@ .vault-grid { grid-template-columns: 1fr; } } - diff --git a/public/web/vendor/qrcode-generator.min.js b/public/web/vendor/qrcode-generator.min.js new file mode 100644 index 0000000..1434c6f --- /dev/null +++ b/public/web/vendor/qrcode-generator.min.js @@ -0,0 +1 @@ +var qrcode=function(){function i(t,r){function a(t,r){g=function(t){for(var r=new Array(t),e=0;e>e&1);g[Math.floor(e/3)][e%3+l-8-3]=n}for(e=0;e<18;e+=1){n=!t&&1==(r>>e&1);g[e%3+l-8-3][Math.floor(e/3)]=n}},v=function(t,r){for(var e=f<<3|r,n=B.getBCHTypeInfo(e),o=0;o<15;o+=1){var i=!t&&1==(n>>o&1);o<6?g[o][8]=i:o<8?g[o+1][8]=i:g[l-15+o][8]=i}for(o=0;o<15;o+=1){i=!t&&1==(n>>o&1);o<8?g[8][l-o-1]=i:o<9?g[8][15-o-1+1]=i:g[8][15-o-1]=i}g[l-8][8]=!t},d=function(t,r){for(var e=-1,n=l-1,o=7,i=0,a=B.getMaskFunction(r),u=l-1;0>>o&1)),a(n,u-f)&&(c=!c),g[n][u-f]=c,-1==(o-=1)&&(i+=1,o=7)}if((n+=e)<0||l<=n){n-=e,e=-e;break}}},w=function(t,r,e){for(var n=b.getRSBlocks(t,r),o=M(),i=0;i8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u',e+="";for(var n=0;n";for(var o=0;o';e+=""}return e+="",e+=""},s.createSvgTag=function(t,r,e,n){var o={};"object"==typeof t&&(t=(o=t).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,f,c=s.getModuleCount()*t+2*r,g="";for(f="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",g+=''+p(n.text)+"":"",g+=e.text?''+p(e.text)+"":"",g+='',g+='":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return s.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*s.getModuleCount()+2*t,u=t,f=a-t,c={"██":"█","█ ":"▀"," █":"▄"," ":" "},g={"██":"▀","█ ":"▀"," █":" "," ":" "},l="";for(r=0;r>>8),r.push(255&o)):r.push(a)}}return r}};var r,t,a=1,u=2,o=4,f=8,y={L:1,M:0,Q:3,H:2},e=0,n=1,c=2,g=3,l=4,h=5,s=6,v=7,B=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],(t={}).getBCHTypeInfo=function(t){for(var r=t<<10;0<=d(r)-d(1335);)r^=1335<>>=1;return r}var w=function(){for(var r=new Array(256),e=new Array(256),t=0;t<8;t+=1)r[t]=1<>>8)},writeBytes:function(t,r,e){r=r||0,e=e||t.length;for(var n=0;n>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return n},putBit:function(t){var r=Math.floor(n/8);e.length<=r&&e.push(0),t&&(e[r]|=128>>>n%8),n+=1}};return o},x=function(t){var r=a,n=t,e={getMode:function(){return r},getLength:function(t){return n.length},write:function(t){for(var r=n,e=0;e+2>>8&255)+(255&n),t.put(n,13),e+=2}if(e=e.length){if(0==i)return-1;throw"unexpected end of file./"+i}var t=e.charAt(n);if(n+=1,"="==t)return i=0,-1;t.match(/^\s$/)||(o=o<<6|a(t.charCodeAt(0)),i+=6)}var r=o>>>i-8&255;return i-=8,r}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return r},I=function(t,r,e){for(var n=function(t,r){var n=t,o=r,l=new Array(t*r),e={setPixel:function(t,r,e){l[r*n+t]=e},write:function(t){t.writeString("GIF87a"),t.writeShort(n),t.writeShort(o),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(n),t.writeShort(o),t.writeByte(0);var r=i(2);t.writeByte(2);for(var e=0;255>>r!=0)throw"length over";for(;8<=n+r;)e.writeByte(255&(t<>>=8-n,n=o=0;o|=t<>>o-6),o-=6},t.flush=function(){if(0>6,128|63&n):n<55296||57344<=n?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}(function(){return qrcode}); \ No newline at end of file diff --git a/src/config/limits.ts b/src/config/limits.ts index ffcd2d4..341908d 100644 --- a/src/config/limits.ts +++ b/src/config/limits.ts @@ -35,6 +35,9 @@ // /api/sync read request budget per minute. // /api/sync 读请求每分钟配额。 syncReadRequestsPerMinute: 1000, + // /api/devices/knowndevice probe budget per IP per minute. + // /api/devices/knowndevice 每 IP 每分钟探测配额。 + knownDeviceRequestsPerMinute: 10, // Fixed window size for API rate limiting in seconds. // API 限流固定窗口大小(秒)。 apiWindowSeconds: 60, diff --git a/src/router.ts b/src/router.ts index b3139a7..ec7f498 100644 --- a/src/router.ts +++ b/src/router.ts @@ -235,6 +235,13 @@ export async function handleRequest(request: Request, env: Env): Promise { + return this.consumeFixedWindowBudget( + identifier, + CONFIG.KNOWN_DEVICE_REQUESTS_PER_MINUTE, + CONFIG.API_WINDOW_SECONDS + ); + } } export function getClientIdentifier(request: Request): string {