add rate limiting, CSRF, newsletter, auto-checker, /uses and /projects pages
This commit is contained in:
@@ -182,31 +182,57 @@ func (h *Handler) Resume(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "resume", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) Uses(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "uses", nil)
|
||||
}
|
||||
|
||||
func (h *Handler) Projects(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "projects", nil)
|
||||
}
|
||||
|
||||
// --- Hire / Contact ---
|
||||
|
||||
type hireData struct {
|
||||
Name string
|
||||
Email string
|
||||
Company string
|
||||
Message string
|
||||
Error string
|
||||
Success bool
|
||||
Name string
|
||||
Email string
|
||||
Company string
|
||||
Message string
|
||||
Error string
|
||||
Success bool
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
func (h *Handler) Hire(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "hire", hireData{})
|
||||
h.render(w, "hire", hireData{CSRFToken: csrfToken()})
|
||||
}
|
||||
|
||||
func (h *Handler) HirePost(w http.ResponseWriter, r *http.Request) {
|
||||
// Rate limit
|
||||
if !h.postLimit.Allow(r.RemoteAddr) {
|
||||
h.renderErr(w, http.StatusTooManyRequests, "Too many requests. Please try again later.")
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
h.renderErr(w, http.StatusBadRequest, "Bad form data.")
|
||||
return
|
||||
}
|
||||
// Honeypot: bots fill hidden fields, humans don't
|
||||
if r.FormValue("website") != "" {
|
||||
// Silently succeed to not reveal the check
|
||||
h.render(w, "hire", hireData{Success: true})
|
||||
return
|
||||
}
|
||||
// CSRF
|
||||
if !csrfValid(r.FormValue("csrf_token")) {
|
||||
h.renderErr(w, http.StatusForbidden, "Invalid or expired form token. Please reload the page and try again.")
|
||||
return
|
||||
}
|
||||
d := hireData{
|
||||
Name: strings.TrimSpace(r.FormValue("name")),
|
||||
Email: strings.TrimSpace(r.FormValue("email")),
|
||||
Company: strings.TrimSpace(r.FormValue("company")),
|
||||
Message: strings.TrimSpace(r.FormValue("message")),
|
||||
Name: strings.TrimSpace(r.FormValue("name")),
|
||||
Email: strings.TrimSpace(r.FormValue("email")),
|
||||
Company: strings.TrimSpace(r.FormValue("company")),
|
||||
Message: strings.TrimSpace(r.FormValue("message")),
|
||||
CSRFToken: csrfToken(),
|
||||
}
|
||||
if d.Name == "" || d.Email == "" || d.Message == "" {
|
||||
d.Error = "Name, email, and message are required."
|
||||
@@ -234,6 +260,44 @@ func (h *Handler) HirePost(w http.ResponseWriter, r *http.Request) {
|
||||
h.render(w, "hire", d)
|
||||
}
|
||||
|
||||
// --- Newsletter ---
|
||||
|
||||
type newsletterData struct {
|
||||
Error string
|
||||
Success bool
|
||||
}
|
||||
|
||||
func (h *Handler) NewsletterPost(w http.ResponseWriter, r *http.Request) {
|
||||
// Rate limit
|
||||
if !h.postLimit.Allow(r.RemoteAddr) {
|
||||
http.Error(w, "Too many requests", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Honeypot
|
||||
if r.FormValue("url") != "" {
|
||||
http.Redirect(w, r, r.Referer(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(r.FormValue("email")))
|
||||
if email == "" || !strings.Contains(email, "@") {
|
||||
http.Redirect(w, r, r.Referer()+"?subscribe=invalid", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if _, err := h.news.Add(email); err != nil {
|
||||
log.Printf("newsletter add %s: %v", email, err)
|
||||
}
|
||||
// Redirect back with success param regardless (don't confirm existence)
|
||||
ref := r.Referer()
|
||||
if ref == "" {
|
||||
ref = "/"
|
||||
}
|
||||
http.Redirect(w, r, ref+"?subscribe=ok", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// --- Sitemap ---
|
||||
|
||||
type urlset struct {
|
||||
@@ -256,7 +320,9 @@ func (h *Handler) Sitemap(w http.ResponseWriter, r *http.Request) {
|
||||
{Loc: h.siteURL + "/", Freq: "weekly", Prio: "1.0"},
|
||||
{Loc: h.siteURL + "/blog", Freq: "weekly", Prio: "0.9"},
|
||||
{Loc: h.siteURL + "/hire", Freq: "monthly", Prio: "0.9"},
|
||||
{Loc: h.siteURL + "/resume", Freq: "monthly", Prio: "0.7"},
|
||||
{Loc: h.siteURL + "/resume", Freq: "monthly", Prio: "0.8"},
|
||||
{Loc: h.siteURL + "/projects", Freq: "monthly", Prio: "0.7"},
|
||||
{Loc: h.siteURL + "/uses", Freq: "monthly", Prio: "0.6"},
|
||||
{Loc: h.siteURL + "/infrastructure", Freq: "monthly", Prio: "0.7"},
|
||||
{Loc: h.siteURL + "/status", Freq: "daily", Prio: "0.6"},
|
||||
{Loc: h.siteURL + "/about", Freq: "monthly", Prio: "0.5"},
|
||||
|
||||
Reference in New Issue
Block a user