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 }