Files
lpra/internal/server/pin.go
T

109 lines
3.0 KiB
Go

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})
}