14.04.2026 Update

This commit is contained in:
2026-04-15 11:38:26 +03:00
parent 6aa0349f5d
commit f50d79fab3
45 changed files with 5645 additions and 751 deletions

211
handlers/auth.go Normal file
View File

@@ -0,0 +1,211 @@
package handlers
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"nano-router/auth"
"nano-router/config"
)
func HandleAuthChallenge(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
nonce := auth.Global.CreateChallenge()
ok(w, map[string]string{"nonce": nonce})
}
func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
Nonce string `json:"nonce"`
Response string `json:"response"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "invalid request")
return
}
clientIP := auth.GetClientIP(r)
sessionID, loginOk := auth.Global.Login(req.Nonce, req.Response, clientIP)
if !loginOk {
fail(w, http.StatusUnauthorized, "invalid credentials or expired challenge")
return
}
auth.SetSessionCookie(w, sessionID)
username, _, isDefault := auth.Global.GetCredentials()
ok(w, map[string]interface{}{
"message": "authenticated",
"default_password": isDefault,
"username": username,
})
}
func HandleAuthLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
token := auth.GetSessionToken(r)
if token != "" {
auth.Global.DestroySession(token)
}
auth.ClearSessionCookie(w)
ok(w, map[string]string{"message": "logged out"})
}
func HandleAuthStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
username, apiKey, isDefault := auth.Global.GetCredentials()
ok(w, map[string]interface{}{
"authenticated": true,
"username": username,
"default_password": isDefault,
"has_api_key": apiKey != "",
})
}
func HandleAuthProfile(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
username, apiKey, isDefault := auth.Global.GetCredentials()
ok(w, map[string]interface{}{
"username": username,
"default_password": isDefault,
"has_api_key": apiKey != "",
"api_key_prefix": apiKeyPrefix(apiKey),
})
case http.MethodPost:
var req struct {
Username string `json:"username"`
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
NewPassword2 string `json:"new_password2"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "invalid request")
return
}
if req.Username == "" && req.NewPassword == "" {
fail(w, http.StatusBadRequest, "nothing to update")
return
}
usernameChanged := false
passwordChanged := false
if req.NewPassword != "" {
if req.OldPassword == "" {
fail(w, http.StatusBadRequest, "current password required")
return
}
oldHash := auth.HashPassword(req.OldPassword)
if !auth.Global.VerifyPassword(oldHash) {
fail(w, http.StatusUnauthorized, "current password incorrect")
return
}
if req.NewPassword != req.NewPassword2 {
fail(w, http.StatusBadRequest, "passwords do not match")
return
}
if len(req.NewPassword) < 4 {
fail(w, http.StatusBadRequest, "password too short (min 4 chars)")
return
}
newHash := auth.HashPassword(req.NewPassword)
clientIP := auth.GetClientIP(r)
newSessionID := auth.Global.UpdatePassword(newHash, clientIP)
auth.SetSessionCookie(w, newSessionID)
passwordChanged = true
cfg, _ := config.Load()
cfg.Auth.PasswordHash = newHash
if req.Username != "" {
cfg.Auth.Username = req.Username
auth.Global.UpdateUsername(req.Username)
usernameChanged = true
}
config.Save(cfg)
} else if req.Username != "" {
auth.Global.UpdateUsername(req.Username)
usernameChanged = true
cfg, _ := config.Load()
cfg.Auth.Username = req.Username
config.Save(cfg)
}
username, apiKey, isDefault := auth.Global.GetCredentials()
result := map[string]interface{}{
"username": username,
"default_password": isDefault,
"has_api_key": apiKey != "",
"api_key_prefix": apiKeyPrefix(apiKey),
}
if passwordChanged {
result["password_changed"] = true
}
if usernameChanged {
result["username_changed"] = true
}
ok(w, result)
default:
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func HandleAuthAPIKey(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
key := generateAPIKey()
auth.Global.SetAPIKey(key)
cfg, _ := config.Load()
cfg.Auth.APIKey = key
config.Save(cfg)
ok(w, map[string]interface{}{
"api_key": key,
})
case http.MethodDelete:
auth.Global.SetAPIKey("")
cfg, _ := config.Load()
cfg.Auth.APIKey = ""
config.Save(cfg)
ok(w, map[string]string{"message": "api key revoked"})
default:
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func generateAPIKey() string {
b := make([]byte, 32)
rand.Read(b)
return "ar_" + hex.EncodeToString(b)
}
func apiKeyPrefix(key string) string {
if len(key) > 8 {
return key[:8] + "..."
}
return ""
}