211 lines
5.1 KiB
Go
211 lines
5.1 KiB
Go
|
|
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 ""
|
||
|
|
}
|