feat: the password is turned off by hand in the account, with a passkey in place; password change; tabs sized by their titles

This commit is contained in:
redtea committed 2026-10-14 20:46:35 +03:00
1 parent 67222e2e05
commit 6dd1120a41
9 files changed
+212 -25

No files matched your search

+69 -3
View File
@@ -26,8 +26,8 @@ import (
)
// Passkeys (WebAuthn): an admin signs in with a key held by the device - a
// fingerprint, the phone's lock, a hardware key - instead of a password. Once
// an admin has a key, the password no longer signs them in.
// fingerprint, the phone's lock, a hardware key - instead of a password. With
// a key in place, the admin can turn the password off in their account.
//
// Attestation is not checked ("none"): the panel trusts any authenticator,
// it only needs the public key and, at sign-in, a signature made with it.
@@ -353,7 +353,73 @@ func (s *Server) handleAccount(w http.ResponseWriter, r *http.Request) {
for _, k := range keys {
list = append(list, map[string]any{"id": k.ID, "name": k.Name, "created": k.CreatedAt, "last_used": k.LastUsed})
}
writeJSON(w, map[string]any{"username": a.Username, "keys": list})
writeJSON(w, map[string]any{"username": a.Username, "keys": list, "password_off": s.db.PasswordOff(a.ID)})
}
// POST /api/panel/account/password/change - a new password, given the
// current one.
func (s *Server) handlePasswordChange(w http.ResponseWriter, r *http.Request) {
a := s.currentAdmin(r)
var in struct {
Current string `json:"current"`
New string `json:"new"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&in); err != nil {
writeJSON(w, map[string]any{"ok": false, "error": "плохой запрос"})
return
}
ip := clientIP(r)
if s.limiter.blocked(ip) {
writeJSON(w, map[string]any{"ok": false, "error": "Слишком много попыток. Подождите немного."})
return
}
if !checkPassword(a.PassHash, in.Current) {
s.limiter.fin(ip, false)
writeJSON(w, map[string]any{"ok": false, "error": "текущий пароль не тот"})
return
}
if len(in.New) < 8 {
writeJSON(w, map[string]any{"ok": false, "error": "новый пароль - не короче 8 символов"})
return
}
hash, err := hashPassword(in.New)
if err == nil {
err = s.db.SetAdminPassword(a.ID, hash)
}
if err != nil {
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
return
}
s.limiter.fin(ip, true)
s.db.Audit(a.ID, "", "password_change")
writeJSON(w, map[string]any{"ok": true})
}
// POST /api/panel/account/password {"off": bool} - turn signing in with the
// password off or on again. Off only with a passkey to sign in with.
func (s *Server) handlePasswordToggle(w http.ResponseWriter, r *http.Request) {
a := s.currentAdmin(r)
var in struct {
Off bool `json:"off"`
}
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 in.Off && s.db.CountAdminKeys(a.ID) == 0 {
writeJSON(w, map[string]any{"ok": false, "error": "сначала добавьте ключ доступа"})
return
}
if err := s.db.SetPasswordOff(a.ID, in.Off); err != nil {
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
return
}
action := "password_on"
if in.Off {
action = "password_off"
}
s.db.Audit(a.ID, "", action)
writeJSON(w, map[string]any{"ok": true})
}
// POST /api/panel/account/keys/begin - options to make a new passkey with.