@@ -790,7 +790,6 @@ <h2 id="pin-title">Введите PIN</h2>
790790 const $ = ( id ) => document . getElementById ( id ) ;
791791 const gear = $ ( "gear-btn" ) , modal = $ ( "settings" ) , pinBox = $ ( "m-pin" ) , mainBox = $ ( "m-main" ) ;
792792 const tabsEl = $ ( "m-tabs" ) , content = $ ( "m-content" ) , oskEl = $ ( "osk" ) ;
793- const PIN_KEY = "supervisor.pin" ;
794793 const IDLE_MS = 120000 ;
795794
796795 function h ( tag , props , ...children ) {
@@ -805,27 +804,20 @@ <h2 id="pin-title">Введите PIN</h2>
805804 return el ;
806805 }
807806
808- /* ---------------- PIN storage (browser-local, on purpose) ---------------- */
809- function storeGet ( ) { try { return JSON . parse ( localStorage . getItem ( PIN_KEY ) || "null" ) ; } catch ( e ) { return null ; } }
810- function storeSet ( v ) { try { localStorage . setItem ( PIN_KEY , JSON . stringify ( v ) ) ; return true ; } catch ( e ) { return false ; } }
811- async function hashPin ( pin , salt ) {
812- const data = new TextEncoder ( ) . encode ( salt + ":" + pin ) ;
813- if ( window . crypto && crypto . subtle ) {
814- const buf = await crypto . subtle . digest ( "SHA-256" , data ) ;
815- return Array . from ( new Uint8Array ( buf ) ) . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
816- }
817- let x = 5381 ;
818- for ( const b of data ) x = ( ( x * 33 ) ^ b ) >>> 0 ;
819- return "d" + x . toString ( 16 ) ;
820- }
821-
822807 /* ---------------- session / api ---------------- */
823- let token = null , tokenAt = 0 ;
808+ let token = null , tokenAt = 0 , sessionPin = null ;
809+ async function pinCall ( path , body ) {
810+ const r = await fetch ( path , { method : body === undefined ? "GET" : "POST" , cache : "no-store" ,
811+ headers : { "Content-Type" : "application/json" } , body : body === undefined ? undefined : JSON . stringify ( body ) } ) ;
812+ let data = { } ;
813+ try { data = await r . json ( ) ; } catch ( e ) { /* empty body */ }
814+ return { status : r . status , data } ;
815+ }
824816 async function ensureToken ( force ) {
825817 if ( ! force && token && Date . now ( ) - tokenAt < 8 * 60 * 1000 ) return token ;
826- const r = await fetch ( "/boot/api/session" , { method : "POST" } ) ;
827- if ( ! r . ok ) throw new Error ( "нет доступа к настройкам (" + r . status + ")" ) ;
828- token = ( await r . json ( ) ) . access_token ;
818+ const r = await pinCall ( "/boot/api/session" , { pin : sessionPin } ) ;
819+ if ( r . status !== 200 ) throw new Error ( "нет доступа к настройкам (" + r . status + ")" ) ;
820+ token = r . data . access_token ;
829821 tokenAt = Date . now ( ) ;
830822 return token ;
831823 }
@@ -872,13 +864,14 @@ <h2 id="pin-title">Введите PIN</h2>
872864 osk . hide ( ) ;
873865 content . innerHTML = "" ;
874866 token = null ;
867+ sessionPin = null ;
875868 }
876869 gear . addEventListener ( "click" , openModal ) ;
877870 $ ( "m-close" ) . addEventListener ( "click" , closeModal ) ;
878871 $ ( "m-lock" ) . addEventListener ( "click" , closeModal ) ;
879872
880873 /* ---------------- PIN pad ---------------- */
881- let pinMode = "enter" , pinBuf = "" , pinFirst = "" , fails = 0 , lockedUntil = 0 , lockTimer = null , changing = false ;
874+ let pinMode = "enter" , pinBuf = "" , pinFirst = "" , lockedUntil = 0 , lockTimer = null , changing = false ;
882875 const dots = $ ( "pin-dots" ) . children , hint = $ ( "pin-hint" ) , title = $ ( "pin-title" ) ;
883876
884877 function setHint ( text , err ) { hint . textContent = text || "" ; hint . classList . toggle ( "err" , ! ! err ) ; }
@@ -898,12 +891,18 @@ <h2 id="pin-title">Введите PIN</h2>
898891 pad . append ( key ( "⌫" , backspace , "fn" ) ) ;
899892 }
900893
901- function startPinEntry ( ) {
894+ async function startPinEntry ( ) {
902895 changing = false ;
903896 pinBox . style . display = "flex" ; mainBox . style . display = "none" ;
904897 $ ( "m-lock" ) . style . display = "none" ;
905898 pinBuf = "" ; pinFirst = "" ;
906- pinMode = storeGet ( ) ? "enter" : "setup1" ;
899+ pinMode = "enter" ;
900+ paintPinMode ( ) ;
901+ setHint ( "Проверяю…" ) ;
902+ const r = await pinCall ( "/boot/api/pin" ) ;
903+ if ( r . status !== 200 ) { setHint ( "Настройки недоступны (" + r . status + ")" , true ) ; return ; }
904+ pinMode = r . data . set ? "enter" : "setup1" ;
905+ if ( r . data . locked_for > 0 ) lockedUntil = Date . now ( ) + r . data . locked_for * 1000 ;
907906 paintPinMode ( ) ;
908907 }
909908
@@ -946,18 +945,21 @@ <h2 id="pin-title">Введите PIN</h2>
946945 const entered = pinBuf ;
947946 pinBuf = "" ; renderDots ( ) ;
948947 if ( pinMode === "enter" ) {
949- const saved = storeGet ( ) ;
950- if ( saved && ( await hashPin ( entered , saved . salt ) ) === saved . hash ) { fails = 0 ; unlocked = true ; showMain ( ) ; return ; }
951- fails += 1 ; shake ( ) ;
952- if ( fails >= 5 ) { fails = 0 ; lockedUntil = Date . now ( ) + 30000 ; refreshLockout ( ) ; }
953- else setHint ( "Неверный PIN (" + fails + "/5)" , true ) ;
948+ const r = await pinCall ( "/boot/api/session" , { pin : entered } ) ;
949+ if ( r . status === 200 ) { sessionPin = entered ; token = r . data . access_token ; tokenAt = Date . now ( ) ; unlocked = true ; showMain ( ) ; return ; }
950+ shake ( ) ;
951+ if ( r . status === 429 ) { lockedUntil = Date . now ( ) + ( ( r . data . detail && r . data . detail . retry_after ) || 30 ) * 1000 ; refreshLockout ( ) ; }
952+ else if ( r . status === 401 ) setHint ( "Неверный PIN. Осталось попыток: " + ( r . data . detail && r . data . detail . attempts_left ) , true ) ;
953+ else setHint ( "Ошибка " + r . status , true ) ;
954954 } else if ( pinMode === "setup1" ) {
955955 pinFirst = entered ; pinMode = "setup2" ; paintPinMode ( ) ;
956956 } else {
957957 if ( entered !== pinFirst ) { shake ( ) ; pinMode = "setup1" ; paintPinMode ( ) ; setHint ( "PIN не совпал — введите заново" , true ) ; return ; }
958- const salt = Math . random ( ) . toString ( 36 ) . slice ( 2 ) + Date . now ( ) . toString ( 36 ) ;
959- const ok = storeSet ( { salt, hash : await hashPin ( entered , salt ) } ) ;
960- if ( ! ok ) { setHint ( "Не удалось сохранить PIN в браузере" , true ) ; return ; }
958+ const r = changing
959+ ? await pinCall ( "/boot/api/pin/change" , { current : sessionPin , new : entered } )
960+ : await pinCall ( "/boot/api/pin/setup" , { pin : entered } ) ;
961+ if ( r . status !== 200 ) { setHint ( "Не удалось сохранить PIN (" + r . status + ")" , true ) ; return ; }
962+ sessionPin = entered ; token = r . data . access_token || null ; tokenAt = Date . now ( ) ;
961963 unlocked = true ; changing = false ; showMain ( ) ;
962964 }
963965 }
0 commit comments