package server import ( "encoding/json" "net/http" "regexp" "sync" ) // The PIN: four digits an admin can set, asked for by the page in every new // browser tab (a reload of the same tab does not ask again). The server // checks it; five wrong ones in a row end the session. var pinRe = regexp.MustCompile(`^[0-9]{4}$`) // pinMisses counts wrong PINs per session. type pinMisses struct { mu sync.Mutex m map[string]int } func (p *pinMisses) miss(sid string) int { p.mu.Lock() defer p.mu.Unlock() if p.m == nil { p.m = map[string]int{} } p.m[sid]++ return p.m[sid] } func (p *pinMisses) clear(sid string) { p.mu.Lock() delete(p.m, sid) p.mu.Unlock() } // GET /api/panel/pin - whether the admin has a PIN to ask for. func (s *Server) handlePinState(w http.ResponseWriter, r *http.Request) { a := s.currentAdmin(r) w.Header().Set("Cache-Control", "no-store") writeJSON(w, map[string]any{"pin": s.db.AdminPin(a.ID) != ""}) } // POST /api/panel/pin/check {"pin"} - the PIN typed in a new tab. func (s *Server) handlePinCheck(w http.ResponseWriter, r *http.Request) { a := s.currentAdmin(r) var in struct { Pin string `json:"pin"` } _ = json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<10)).Decode(&in) h := s.db.AdminPin(a.ID) c, _ := r.Cookie(sessionCookie) if h == "" || (pinRe.MatchString(in.Pin) && checkPassword(h, in.Pin)) { s.pinMisses.clear(c.Value) writeJSON(w, map[string]any{"ok": true}) return } if n := s.pinMisses.miss(c.Value); n >= 5 { s.pinMisses.clear(c.Value) s.clearSession(w, r) s.db.Audit(a.ID, "", "pin_lockout") writeJSON(w, map[string]any{"ok": false, "out": true, "error": "слишком много неверных попыток"}) return } else { writeJSON(w, map[string]any{"ok": false, "left": 5 - n, "error": "неверный PIN"}) } } // POST /api/panel/account/pin {"pin", "current"} - set or change the PIN; // with pin empty, remove it. A PIN in place is needed to change or drop it. func (s *Server) handlePinSet(w http.ResponseWriter, r *http.Request) { a := s.currentAdmin(r) var in struct { Pin string `json:"pin"` Current string `json:"current"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<10)).Decode(&in); err != nil { writeJSON(w, map[string]any{"ok": false, "error": "плохой запрос"}) return } if h := s.db.AdminPin(a.ID); h != "" && !checkPassword(h, in.Current) { writeJSON(w, map[string]any{"ok": false, "error": "текущий PIN не тот"}) return } hash := "" if in.Pin != "" { if !pinRe.MatchString(in.Pin) { writeJSON(w, map[string]any{"ok": false, "error": "PIN - ровно 4 цифры"}) return } var err error if hash, err = hashPassword(in.Pin); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } } if err := s.db.SetAdminPin(a.ID, hash); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } action := "pin_set" if hash == "" { action = "pin_remove" } s.db.Audit(a.ID, "", action) writeJSON(w, map[string]any{"ok": true}) }