284 lines
9.2 KiB
Go
284 lines
9.2 KiB
Go
package account
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
nm "net/mail"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/mail"
|
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
|
"code.nonshy.com/nonshy/website/pkg/models"
|
|
"code.nonshy.com/nonshy/website/pkg/redis"
|
|
"code.nonshy.com/nonshy/website/pkg/session"
|
|
"code.nonshy.com/nonshy/website/pkg/spam"
|
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// SignupToken goes in Redis when the user first gives us their email address. They
|
|
// verify their email before signing up, so cache only in Redis until verified.
|
|
type SignupToken struct {
|
|
Email string
|
|
Token string
|
|
}
|
|
|
|
// Delete a SignupToken when it's been used up.
|
|
func (st SignupToken) Delete() error {
|
|
return redis.Delete(fmt.Sprintf(config.SignupTokenRedisKey, st.Token))
|
|
}
|
|
|
|
// Initial signup controller.
|
|
func Signup() http.HandlerFunc {
|
|
tmpl := templates.Must("account/signup.html")
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Maintenance mode?
|
|
if middleware.SignupMaintenance(w, r) {
|
|
return
|
|
}
|
|
|
|
// Template vars.
|
|
var vars = map[string]interface{}{
|
|
"SignupToken": "", // non-empty if user has clicked verification link
|
|
"SkipEmailVerification": false, // true if email verification is disabled
|
|
"Email": "", // pre-filled user email
|
|
}
|
|
|
|
// Is email verification disabled?
|
|
if config.SkipEmailVerification {
|
|
vars["SkipEmailVerification"] = true
|
|
}
|
|
|
|
// Are we called with an email verification token?
|
|
var tokenStr = r.URL.Query().Get("token")
|
|
if r.Method == http.MethodPost {
|
|
tokenStr = r.PostFormValue("token")
|
|
}
|
|
|
|
var token SignupToken
|
|
if tokenStr != "" {
|
|
// Validate it.
|
|
if err := redis.Get(fmt.Sprintf(config.SignupTokenRedisKey, tokenStr), &token); err != nil || token.Token != tokenStr {
|
|
session.FlashError(w, r, "Invalid email verification token. Please try signing up again.")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
vars["SignupToken"] = tokenStr
|
|
vars["Email"] = token.Email
|
|
}
|
|
|
|
// Posting?
|
|
if r.Method == http.MethodPost {
|
|
var (
|
|
// Collect form fields.
|
|
email = strings.TrimSpace(strings.ToLower(r.PostFormValue("email")))
|
|
confirm = r.PostFormValue("confirm") == "true"
|
|
|
|
// Only on full signup form
|
|
username = strings.TrimSpace(strings.ToLower(r.PostFormValue("username")))
|
|
password = strings.TrimSpace(r.PostFormValue("password"))
|
|
password2 = strings.TrimSpace(r.PostFormValue("password2"))
|
|
dob = r.PostFormValue("dob")
|
|
|
|
// CAPTCHA response.
|
|
turnstileCAPTCHA = r.PostFormValue("cf-turnstile-response")
|
|
|
|
// Honeytrap fields for lazy spam bots.
|
|
honeytrap1 = r.PostFormValue("phone") == ""
|
|
honeytrap2 = r.PostFormValue("referral") == "Word of mouth"
|
|
|
|
// Validation errors but still show the form again.
|
|
hasError bool
|
|
)
|
|
|
|
// Honeytrap fields check.
|
|
if !honeytrap1 || !honeytrap2 {
|
|
session.Flash(w, r, "We have sent an e-mail to %s with a link to continue signing up your account. Please go and check your e-mail.", email)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Validate the CAPTCHA token.
|
|
if config.Current.Turnstile.Enabled {
|
|
if err := spam.ValidateTurnstileCAPTCHA(turnstileCAPTCHA, "signup"); err != nil {
|
|
session.FlashError(w, r, "There was an error validating your CAPTCHA response.")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Don't let them sneakily change their verified email address on us.
|
|
if vars["SignupToken"] != "" && email != vars["Email"] {
|
|
session.FlashError(w, r, "This email address is not verified. Please start over from the beginning.")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Cache username in case of passwd validation errors.
|
|
vars["Email"] = email
|
|
vars["Username"] = username
|
|
|
|
// Validate the email.
|
|
if _, err := nm.ParseAddress(email); err != nil {
|
|
session.FlashError(w, r, "The email address you entered is not valid: %s", err)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Didn't confirm?
|
|
if !confirm {
|
|
session.FlashError(w, r, "Confirm that you have read the rules.")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Already an account?
|
|
if _, err := models.FindUser(email); err == nil {
|
|
// We don't want to admit that the email already is registered, so send an email to the
|
|
// address in case the user legitimately forgot, but flash the regular success message.
|
|
if err := mail.LockSending("signup", email, config.SignupTokenExpires); err == nil {
|
|
err := mail.Send(mail.Message{
|
|
To: email,
|
|
Subject: "You already have a nonshy account",
|
|
Template: "email/already_signed_up.html",
|
|
Data: map[string]interface{}{
|
|
"Title": config.Title,
|
|
"URL": config.Current.BaseURL + "/forgot-password",
|
|
},
|
|
})
|
|
if err != nil {
|
|
session.FlashError(w, r, "Error sending an email: %s", err)
|
|
}
|
|
} else {
|
|
log.Error("LockSending: signup e-mail is not sent to %s: one was sent recently", email)
|
|
}
|
|
|
|
session.Flash(w, r, "We have sent an e-mail to %s with a link to continue signing up your account. Please go and check your e-mail.", email)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// Email verification step!
|
|
if !config.SkipEmailVerification && vars["SignupToken"] == "" {
|
|
// Create a SignupToken verification link to send to their inbox.
|
|
token = SignupToken{
|
|
Email: email,
|
|
Token: uuid.New().String(),
|
|
}
|
|
if err := redis.Set(fmt.Sprintf(config.SignupTokenRedisKey, token.Token), token, config.SignupTokenExpires); err != nil {
|
|
session.FlashError(w, r, "Error creating a link to send you: %s", err)
|
|
}
|
|
|
|
// Is the app not configured to send email?
|
|
if !config.Current.Mail.Enabled && !config.SkipEmailVerification {
|
|
// Log the signup token for local dev.
|
|
log.Error("Signup: the app is not configured to send email. To continue, visit the URL: /signup?token=%s", token.Token)
|
|
session.FlashError(w, r, "This app is not configured to send email so you can not sign up at this time. "+
|
|
"Please contact the website administrator about this issue!")
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
if err := mail.LockSending("signup", email, config.SignupTokenExpires); err == nil {
|
|
err := mail.Send(mail.Message{
|
|
To: email,
|
|
Subject: "Verify your e-mail address",
|
|
Template: "email/verify_email.html",
|
|
Data: map[string]interface{}{
|
|
"Title": config.Title,
|
|
"URL": config.Current.BaseURL + "/signup?token=" + token.Token,
|
|
},
|
|
})
|
|
if err != nil {
|
|
session.FlashError(w, r, "Error sending an email: %s", err)
|
|
}
|
|
} else {
|
|
log.Error("LockSending: signup e-mail is not sent to %s: one was sent recently", email)
|
|
}
|
|
|
|
session.Flash(w, r, "We have sent an e-mail to %s with a link to continue signing up your account. Please go and check your e-mail.", email)
|
|
|
|
// Reminder to check their spam folder too (Gmail users)
|
|
session.Flash(w, r, "If you don't see the confirmation e-mail, check in case it went to your spam folder.")
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
}
|
|
|
|
// DOB check.
|
|
birthdate, err := time.Parse("2006-01-02", dob)
|
|
if err != nil {
|
|
session.FlashError(w, r, "Incorrect format for birthdate; should be in yyyy-mm-dd format but got: %s", dob)
|
|
templates.Redirect(w, r.URL.Path)
|
|
return
|
|
} else {
|
|
// Validate birthdate is at least age 18.
|
|
if utility.Age(birthdate) < 18 {
|
|
session.FlashError(w, r, "You must be at least 18 years old to use this site.")
|
|
templates.Redirect(w, "/")
|
|
|
|
// Burn the signup token.
|
|
if token.Token != "" {
|
|
if err := token.Delete(); err != nil {
|
|
log.Error("SignupToken.Delete(%s): %s", token.Token, err)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// Full sign-up step (w/ email verification token), validate more things.
|
|
if len(password) < 3 {
|
|
session.FlashError(w, r, "Please enter a password longer than 3 characters.")
|
|
hasError = true
|
|
} else if password != password2 {
|
|
session.FlashError(w, r, "Your passwords do not match.")
|
|
hasError = true
|
|
}
|
|
|
|
// Validate the username is OK: well formatted, not reserved, not existing.
|
|
if err := models.IsValidUsername(username); err != nil {
|
|
session.FlashError(w, r, err.Error())
|
|
hasError = true
|
|
}
|
|
|
|
// Looking good?
|
|
if !hasError {
|
|
user, err := models.CreateUser(username, email, password)
|
|
if err != nil {
|
|
session.FlashError(w, r, err.Error())
|
|
} else {
|
|
session.Flash(w, r, "User account created. Now logged in as %s.", user.Username)
|
|
|
|
// Burn the signup token.
|
|
if token.Token != "" {
|
|
if err := token.Delete(); err != nil {
|
|
log.Error("SignupToken.Delete(%s): %s", token.Token, err)
|
|
}
|
|
}
|
|
|
|
// Put their birthdate in.
|
|
user.Birthdate = birthdate
|
|
user.Save()
|
|
|
|
// Log in the user and send them to their dashboard.
|
|
session.LoginUser(w, r, user)
|
|
templates.Redirect(w, "/me")
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
})
|
|
}
|