update MFA, add parameters to reset admin pw,mfa if locked out

This commit is contained in:
ghostersk
2026-03-07 20:36:53 +00:00
parent 12b1a44b96
commit b1fe22863a
4 changed files with 203 additions and 6 deletions
+57
View File
@@ -308,6 +308,63 @@ func (d *DB) UpdateUserPassword(userID int64, newPassword string) error {
return err
}
// AdminListAdmins returns (username, email, mfa_enabled) for all admin-role users.
func (d *DB) AdminListAdmins() ([]struct {
Username string
Email string
MFAEnabled bool
}, error) {
rows, err := d.sql.Query(`SELECT username, email, mfa_enabled FROM users WHERE role='admin' ORDER BY username`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []struct {
Username string
Email string
MFAEnabled bool
}
for rows.Next() {
var r struct {
Username string
Email string
MFAEnabled bool
}
rows.Scan(&r.Username, &r.Email, &r.MFAEnabled)
out = append(out, r)
}
return out, rows.Err()
}
// AdminResetPassword sets a new password for an admin user by username (admin-only check).
func (d *DB) AdminResetPassword(username, newPassword string) error {
// Verify user exists and is admin
var id int64
var role string
err := d.sql.QueryRow(`SELECT id, role FROM users WHERE username=?`, username).Scan(&id, &role)
if err != nil || id == 0 {
return fmt.Errorf("user %q not found", username)
}
if role != "admin" {
return fmt.Errorf("user %q is not an admin (use the web UI for regular users)", username)
}
return d.UpdateUserPassword(id, newPassword)
}
// AdminDisableMFA disables MFA for an admin user by username (admin-only check).
func (d *DB) AdminDisableMFA(username string) error {
var id int64
var role string
err := d.sql.QueryRow(`SELECT id, role FROM users WHERE username=?`, username).Scan(&id, &role)
if err != nil || id == 0 {
return fmt.Errorf("user %q not found", username)
}
if role != "admin" {
return fmt.Errorf("user %q is not an admin (use the web UI for regular users)", username)
}
return d.DisableMFA(id)
}
func (d *DB) SetUserActive(userID int64, active bool) error {
v := 0
if active {
+11 -6
View File
@@ -10,6 +10,7 @@ import (
"encoding/base32"
"encoding/binary"
"fmt"
"log"
"math"
"net/url"
"strings"
@@ -17,9 +18,9 @@ import (
)
const (
totpDigits = 6
totpPeriod = 30 // seconds
totpWindow = 1 // accept ±1 period to allow for clock skew
totpDigits = 6
totpPeriod = 30 // seconds
totpWindow = 2 // accept ±2 periods (±60s) to handle clock skew and slow input
)
// GenerateSecret creates a new random 20-byte (160-bit) TOTP secret,
@@ -58,15 +59,19 @@ func QRCodeURL(issuer, accountName, secret string) string {
// Validate checks whether code is a valid TOTP code for secret at the current time.
// It accepts codes from [now-window*period, now+window*period] to handle clock skew.
// Handles both padded and unpadded base32 secrets.
func Validate(secret, code string) bool {
code = strings.TrimSpace(code)
if len(code) != totpDigits {
return false
}
keyBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(
strings.ToUpper(secret),
)
// Normalise: uppercase, strip spaces and padding, then re-decode.
// Accept both padded (JBSWY3DP====) and unpadded (JBSWY3DP) base32.
cleaned := strings.ToUpper(strings.ReplaceAll(secret, " ", ""))
cleaned = strings.TrimRight(cleaned, "=")
keyBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(cleaned)
if err != nil {
log.Printf("mfa: base32 decode error (secret len=%d): %v", len(secret), err)
return false
}
now := time.Now().Unix()