154 lines
4 KiB
Go
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
|
|
}
|