742a5fa1af
Users whose accounts are no longer eligible to be in the chat room will be disconnected immediately from chat when their account status changes. The places in nonshy where these disconnects may happen include: * When the user deactivates or deletes their account. * When they modify their settings to mark their profile as 'private,' making them become a Shy Account. * When they edit or delete their photos in case they have moved their final public photo to be private, making them become a Shy Account. * When the user deletes their certification photo, or uploads a new cert photo to be reviewed (in both cases, losing account certified status). * When an admin user rejects their certification photo, even retroactively. * On admin actions against a user, including: banning them, deleting their user account. Other changes made include: * When signing up an account and e-mail sending is not enabled (e.g. local dev environment), the SignupToken is still created and logged to the console so you can continue the signup manually. * On the new account DOB prompt, add a link to manually input their birthdate as text similar to on the Age Gate page.
254 lines
8.0 KiB
Go
254 lines
8.0 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/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
|
|
log.Info("SignupToken: %s", tokenStr)
|
|
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
|
|
}
|
|
log.Info("Vars: %+v", vars)
|
|
|
|
// 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")
|
|
|
|
// Validation errors but still show the form again.
|
|
hasError bool
|
|
)
|
|
|
|
// 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.
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|