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