package web import ( "errors" "net/http" "strings" "time" "github.com/gramanas/blogspace/internal/auth" "github.com/gramanas/blogspace/internal/store" ) func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { if currentUser(r) != nil { http.Redirect(w, r, "/dashboard", http.StatusSeeOther) return } http.Redirect(w, r, "/login", http.StatusSeeOther) } func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) { if currentUser(r) != nil { http.Redirect(w, r, "/dashboard", http.StatusSeeOther) return } s.render(w, r, "auth/login.html", map[string]any{"next": safeNext(r.URL.Query().Get("next"))}) } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") next := safeNext(r.FormValue("next")) fail := func() { s.renderStatus(w, r, http.StatusUnauthorized, "auth/login.html", map[string]any{"error": "Wrong username or password.", "username": username, "next": next}) } u, err := s.st.UserByUsername(r.Context(), username) if err != nil { if !errors.Is(err, store.ErrNotFound) { s.serverError(w, err) return } auth.CheckPassword("$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinval", password) // constant-ish time fail() return } if u.Disabled || !auth.CheckPassword(u.PasswordHash, password) { fail() return } tok, err := auth.IssueToken(s.cfg.JWTSecret, u.ID, u.TokenVersion, time.Now()) if err != nil { s.serverError(w, err) return } auth.SetSessionCookie(w, tok) if next == "" { next = "/dashboard" } http.Redirect(w, r, next, http.StatusSeeOther) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { auth.ClearSessionCookie(w) http.Redirect(w, r, "/login", http.StatusSeeOther) } func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { u := currentUser(r) if u.IsSuperadmin() { http.Redirect(w, r, "/admin/", http.StatusSeeOther) return } blog, err := s.st.BlogByOwner(r.Context(), u.ID) if err != nil { if errors.Is(err, store.ErrNotFound) { s.plainError(w, http.StatusNotFound, "You have no blog yet. Ask the administrator to create one.") return } s.serverError(w, err) return } http.Redirect(w, r, "/b/"+blog.Subdomain+"/", http.StatusSeeOther) } func (s *Server) handlePasswordForm(w http.ResponseWriter, r *http.Request) { s.render(w, r, "dashboard/password.html", nil) } func (s *Server) handlePassword(w http.ResponseWriter, r *http.Request) { u := currentUser(r) cur, pw, pw2 := r.FormValue("current"), r.FormValue("password"), r.FormValue("password2") var msg string switch { case !auth.CheckPassword(u.PasswordHash, cur): msg = "Current password is wrong." case len(pw) < 8: msg = "New password must be at least 8 characters." case pw != pw2: msg = "New passwords do not match." } if msg != "" { s.renderStatus(w, r, http.StatusBadRequest, "dashboard/password.html", map[string]any{"error": msg}) return } hash, err := auth.HashPassword(pw) if err != nil { s.serverError(w, err) return } if err := s.st.SetPassword(r.Context(), u.ID, hash); err != nil { s.serverError(w, err) return } // token_version changed, so re-issue the session instead of logging the user out tok, err := auth.IssueToken(s.cfg.JWTSecret, u.ID, u.TokenVersion+1, time.Now()) if err != nil { s.serverError(w, err) return } auth.SetSessionCookie(w, tok) redirectOK(w, r, "/dashboard", "Password changed.") } // safeNext only allows local paths as post-login redirect targets. func safeNext(n string) string { if strings.HasPrefix(n, "/") && !strings.HasPrefix(n, "//") { return n } return "" }