update_server/internal/http/auth_handlers.go
2026-06-10 20:51:17 +03:00

154 lines
4 KiB
Go

package httpserver
import (
"errors"
"net"
"net/http"
"net/url"
"strings"
authservice "update_server/internal/auth"
)
const defaultAdminRedirect = "/admin"
func (h *handler) adminLoginForm(w http.ResponseWriter, r *http.Request) {
if redirected, err := h.redirectAuthenticatedAdmin(w, r); err != nil {
http.Error(w, "session lookup failed", http.StatusInternalServerError)
return
} else if redirected {
return
}
h.renderLoginPage(w, r, http.StatusOK, "", safeNextPath(r.URL.Query().Get("next")), "")
}
func (h *handler) adminLogin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid login form", http.StatusBadRequest)
return
}
nextPath := safeNextPath(r.FormValue("next"))
email := strings.TrimSpace(r.FormValue("email"))
password := r.FormValue("password")
token, sessionState, err := h.auth.Authenticate(r.Context(), email, password, clientIP(r), r.UserAgent())
if err != nil {
if errors.Is(err, authservice.ErrInvalidCredentials) {
h.renderLoginPage(w, r, http.StatusUnauthorized, email, nextPath, "Invalid email or password.")
return
}
http.Error(w, "login failed", http.StatusInternalServerError)
return
}
http.SetCookie(w, h.auth.SessionCookie(token, sessionState.Session.ExpiresAt))
if _, err := h.issueCSRFCookie(w); err != nil {
http.Error(w, "login failed", http.StatusInternalServerError)
return
}
http.Redirect(w, r, nextPath, http.StatusSeeOther)
}
func (h *handler) adminLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(h.auth.SessionCookieName()); err == nil {
if err := h.auth.InvalidateSession(r.Context(), cookie.Value); err != nil {
http.Error(w, "logout failed", http.StatusInternalServerError)
return
}
}
http.SetCookie(w, h.auth.ClearSessionCookie())
http.SetCookie(w, h.clearCSRFCookie())
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
}
func (h *handler) renderLoginPage(w http.ResponseWriter, r *http.Request, status int, email, nextPath, errorMessage string) {
hasActiveAdmin, err := h.store.Users.HasActiveAdmin(r.Context())
if err != nil {
http.Error(w, "admin status lookup failed", http.StatusInternalServerError)
return
}
setupHint := ""
if !hasActiveAdmin {
setupHint = "No active admin user exists yet. Set ADMIN_EMAIL and ADMIN_PASSWORD, then restart the server once to bootstrap the first admin account."
}
loginData := LoginPageData{
PageData: h.basePageData(
r,
"Admin Login",
"Secure Sign-In",
"Admin Login",
"Use your admin credentials to open the protected server-rendered dashboard.",
),
Login: LoginFormData{
Action: "/admin/login",
Email: email,
Next: nextPath,
Error: errorMessage,
SetupHint: setupHint,
},
}
renderPageStatus(w, h.renderer, "login", status, loginData)
}
func (h *handler) redirectAuthenticatedAdmin(w http.ResponseWriter, r *http.Request) (bool, error) {
cookie, err := r.Cookie(h.auth.SessionCookieName())
if err != nil {
return false, nil
}
sessionState, err := h.auth.LoadSession(r.Context(), cookie.Value)
if err != nil {
if errors.Is(err, authservice.ErrUnauthenticated) {
http.SetCookie(w, h.auth.ClearSessionCookie())
return false, nil
}
return false, err
}
http.Redirect(w, r, safeNextPath(r.URL.Query().Get("next")), http.StatusSeeOther)
_ = sessionState
return true, nil
}
func safeNextPath(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return defaultAdminRedirect
}
if !strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "//") {
return defaultAdminRedirect
}
return raw
}
func loginRedirectPath(nextPath string) string {
values := url.Values{}
if nextPath = safeNextPath(nextPath); nextPath != defaultAdminRedirect {
values.Set("next", nextPath)
}
if encoded := values.Encode(); encoded != "" {
return "/admin/login?" + encoded
}
return "/admin/login"
}
func clientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}