109 lines
3.0 KiB
Go
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})
|
|
}
|