Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for recaptcha for contact pages #88

Merged
merged 2 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func SetupRoutes(app_settings common.AppSettings, database database.Database) *g
addCachableHandler(r, "GET", "/page/:num", homeHandler, &cache, app_settings, database)

// DO not cache as it needs to handlenew form values
r.POST("/contact-send", makeContactFormHandler())
r.POST("/contact-send", makeContactFormHandler(app_settings))

// Where all the static files (css, js, etc) are served from
r.Static("/static", "./static")
Expand Down
94 changes: 86 additions & 8 deletions app/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package app

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/mail"
"net/url"

"github.com/gin-gonic/gin"
"github.com/matheusgomes28/urchin/common"
Expand All @@ -12,7 +16,70 @@ import (
"github.com/rs/zerolog/log"
)

func makeContactFormHandler() func(*gin.Context) {
// Change this in case Google decides to deprecate
// the reCAPTCHA validation endpoint
const RECAPTCHA_VERIFY_URL string = "https://www.google.com/recaptcha/api/siteverify"

type RecaptchaResponse struct {
Success bool `json:"success"`
Score float32 `json:"score"`
Timestamp string `json:"challenge_ts"`
Hostname string `json:"hostname"`
}

func verifyRecaptcha(recaptcha_secret string, recaptcha_response string) error {
// Validate that the recaptcha response was actually
// not a bot by checking the success rate
recaptcha_response_data, err := http.PostForm(RECAPTCHA_VERIFY_URL, url.Values{
"secret": {recaptcha_secret},
"response": {recaptcha_response},
})
if err != nil {
err_str := fmt.Sprintf("could not do recaptcha post request: %s", err)
return fmt.Errorf("%s: %s", err_str, err)
}
defer recaptcha_response_data.Body.Close()

if recaptcha_response_data.StatusCode != http.StatusOK {
return fmt.Errorf("invalid recaptcha response: %s", recaptcha_response_data.Status)
}
var recaptcha_answer RecaptchaResponse
recaptcha_response_data_buffer, _ := io.ReadAll(recaptcha_response_data.Body)
err = json.Unmarshal(recaptcha_response_data_buffer, &recaptcha_answer)
if err != nil {
return fmt.Errorf("could not parse recaptcha response: %s", err)
}

if !recaptcha_answer.Success || (recaptcha_answer.Score < 0.9) {
return fmt.Errorf("could not validate recaptcha")
}

return nil
}

func validateEmail(email string) error {
_, err := mail.ParseAddress(email)
if err != nil {
return fmt.Errorf("could not parse email: %s", email)
}

return nil
}

func renderErrorPage(c *gin.Context, email string, err error) error {
if err = render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())); err != nil {
log.Error().Msgf("could not render error page: %v", err)
}
return err
}

func logError(err error) {
if err != nil {
log.Error().Msgf("%v", err)
}
}

func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) {
return func(c *gin.Context) {
if err := c.Request.ParseForm(); err != nil {
log.Error().Msgf("could not parse form %v", err)
Expand All @@ -25,28 +92,39 @@ func makeContactFormHandler() func(*gin.Context) {
email := c.Request.FormValue("email")
name := c.Request.FormValue("name")
message := c.Request.FormValue("message")
recaptcha_response := c.Request.FormValue("g-recaptcha-response")

// Parse email
_, err := mail.ParseAddress(email)
if err != nil {
log.Error().Msgf("could not parse email: %v", err)
if err = render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())); err != nil {
log.Error().Msgf("could not render: %v", err)
// Make the request to Google's API only if user
// configured recatpcha settings
if (len(app_settings.RecaptchaSecret) > 0) && (len(app_settings.RecaptchaSiteKey) > 0) {
err := verifyRecaptcha(app_settings.RecaptchaSecret, recaptcha_response)
if err != nil {
log.Error().Msgf("%v", err)
defer logError(renderErrorPage(c, email, err))
return
}
}

err := validateEmail(email)
if err != nil {
log.Error().Msgf("%v", err)
defer logError(renderErrorPage(c, email, err))
return
}

// Make sure name and message is reasonable
if len(name) > 200 {
if err = render(c, http.StatusOK, views.MakeContactFailure(email, "name too long (200 chars max)")); err != nil {
log.Error().Msgf("could not render: %v", err)
logError(renderErrorPage(c, email, err))
}
return
}

if len(message) > 10000 {
if err = render(c, http.StatusOK, views.MakeContactFailure(email, "message too long (1000 chars max)")); err != nil {
log.Error().Msgf("could not render: %v", err)
logError(renderErrorPage(c, email, err))
}
return
}
Expand All @@ -59,7 +137,7 @@ func makeContactFormHandler() func(*gin.Context) {

// TODO : This is a duplicate of the index handler... abstract
func contactHandler(c *gin.Context, app_settings common.AppSettings, db database.Database) ([]byte, error) {
index_view := views.MakeContactPage(app_settings.AppNavbar.Links)
index_view := views.MakeContactPage(app_settings.AppNavbar.Links, app_settings.RecaptchaSiteKey)
html_buffer := bytes.NewBuffer(nil)
if err := index_view.Render(c, html_buffer); err != nil {
log.Error().Msgf("could not render: %v", err)
Expand Down
2 changes: 2 additions & 0 deletions common/app_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type AppSettings struct {
AdminPort int `toml:"admin_port"`
ImageDirectory string `toml:"image_dir"`
CacheEnabled bool `toml:"cache_enabled"`
RecaptchaSiteKey string `toml:"recaptcha_sitekey,omitempty"`
RecaptchaSecret string `toml:"recaptcha_secret,omitempty"`
AppNavbar Navbar `toml:"navbar"`
}

Expand Down
5 changes: 5 additions & 0 deletions urchin_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ image_dir = "./images"
# Enable/disable endpoint cache
cache_enabled = true

# Recaptcha settings
recaptcha_sitekey = "6LecCewpAAAAAK7QS2SwuyCIDzwlyXMs4J1Z5LBq"
recaptcha_secret = "6LecCewpAAAAAPP8Aaxh6jWaDE1xG_MBQa1OOs8f"


[navbar]
links = [
{ name = "Home", href = "/", title = "Homepage" },
Expand Down
61 changes: 48 additions & 13 deletions views/contact.templ
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,46 @@ package views

import "github.com/matheusgomes28/urchin/common"

templ MakeContactPage(links []common.Link) {
templ MakeContactFormWithRecaptcha(recaptcha_sitekey string) {
<form id="demo-form" method="post" hx-post="/contact-send" hx-target="#contact-form" hx-trigger="verified" >
<label for="name">Name:</label>
<input type="text" id="name" name="name" required /><br/><br/>

<label for="email">Email:</label>
<input type="email" id="email" name="email" required/><br/><br/>

<label for="message">Message:</label><br/>
<textarea id="message" name="message" rows="4" cols="50" required></textarea><br /><br />

<button class="g-recaptcha"
data-sitekey={recaptcha_sitekey}
data-callback='onSubmit'
data-action='submit'>Submit</button>
<script>
function onSubmit(){
const event = new Event('verified');
const elem = document.querySelector("#demo-form");
elem.dispatchEvent(event);
}
</script>
</form>
}

templ MakeContactForm() {
<form id="demo-form" method="post" hx-post="/contact-send" hx-target="#contact-form" >
<label for="name">Name:</label>
<input type="text" id="name" name="name" required /><br/><br/>

<label for="email">Email:</label>
<input type="email" id="email" name="email" required/><br/><br/>

<label for="message">Message:</label><br/>
<textarea id="message" name="message" rows="4" cols="50" required></textarea><br /><br />
<input type="submit" value="Submit" />
</form>
}

templ MakeContactPage(links []common.Link, recaptcha_sitekey string) {
<!DOCTYPE html>
<html lang="en">

Expand All @@ -13,25 +52,21 @@ templ MakeContactPage(links []common.Link) {
<link rel="stylesheet" href="/static/simple.min.css" />
<script src="/static/htmx.min.js"></script>
<script src="/static/client-side-templates.js"></script>
if len(recaptcha_sitekey) > 0 {
<script src="https://www.google.com/recaptcha/api.js"></script>
}
</head>

<body>
@MakeNavBar(links)
<main>
<div id="contact-form">
<h2>Contact Us</h2>
<form action="#" method="post" hx-post="/contact-send" hx-target="#contact-form">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required /><br/><br/>

<label for="email">Email:</label>
<input type="email" id="email" name="email" required/><br/><br/>

<label for="message">Message:</label><br/>
<textarea id="message" name="message" rows="4" cols="50" required></textarea><br /><br />

<input type="submit" value="Submit" />
</form>
if len(recaptcha_sitekey) > 0 {
@MakeContactFormWithRecaptcha(recaptcha_sitekey)
} else {
@MakeContactForm()
}
</div>
</main>
@MakeFooter()
Expand Down
Loading