2022-08-11 03:59:59 +00:00
|
|
|
package account
|
|
|
|
|
|
|
|
import (
|
2022-08-14 21:40:57 +00:00
|
|
|
"fmt"
|
2022-08-11 03:59:59 +00:00
|
|
|
"net/http"
|
2022-08-14 21:40:57 +00:00
|
|
|
nm "net/mail"
|
2023-08-20 02:11:33 +00:00
|
|
|
"strconv"
|
2022-08-11 03:59:59 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2022-08-26 04:21:46 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
2023-08-20 02:11:33 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/geoip"
|
2022-08-26 04:21:46 +00:00
|
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/mail"
|
|
|
|
"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"
|
2022-08-14 21:40:57 +00:00
|
|
|
"github.com/google/uuid"
|
2022-08-11 03:59:59 +00:00
|
|
|
)
|
|
|
|
|
2022-08-14 21:40:57 +00:00
|
|
|
// ChangeEmailToken for Redis.
|
|
|
|
type ChangeEmailToken struct {
|
|
|
|
Token string
|
|
|
|
UserID uint64
|
|
|
|
NewEmail string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete the change email token.
|
|
|
|
func (t ChangeEmailToken) Delete() error {
|
|
|
|
return redis.Delete(fmt.Sprintf(config.ChangeEmailRedisKey, t.Token))
|
|
|
|
}
|
|
|
|
|
2022-08-11 03:59:59 +00:00
|
|
|
// User settings page. (/settings).
|
|
|
|
func Settings() http.HandlerFunc {
|
|
|
|
tmpl := templates.Must("account/settings.html")
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
vars := map[string]interface{}{
|
|
|
|
"Enum": config.ProfileEnums,
|
|
|
|
}
|
|
|
|
|
2022-08-13 22:39:31 +00:00
|
|
|
// Load the current user in case of updates.
|
|
|
|
user, err := session.CurrentUser(r)
|
|
|
|
if err != nil {
|
|
|
|
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-06-24 05:18:09 +00:00
|
|
|
// URL hashtag to redirect to
|
|
|
|
var hashtag string
|
|
|
|
|
2022-08-11 03:59:59 +00:00
|
|
|
// Are we POSTing?
|
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
intent := r.PostFormValue("intent")
|
|
|
|
switch intent {
|
|
|
|
case "profile":
|
|
|
|
// Setting profile values.
|
2023-06-24 05:18:09 +00:00
|
|
|
hashtag = "#profile"
|
2022-08-11 03:59:59 +00:00
|
|
|
var (
|
|
|
|
displayName = r.PostFormValue("display_name")
|
|
|
|
dob = r.PostFormValue("dob")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Set user attributes.
|
|
|
|
user.Name = &displayName
|
2023-06-16 05:12:01 +00:00
|
|
|
|
|
|
|
// Birthdate, now required.
|
|
|
|
if birthdate, err := time.Parse("2006-01-02", dob); err != nil {
|
|
|
|
session.FlashError(w, r, "Incorrect format for birthdate; should be in yyyy-mm-dd format but got: %s", dob)
|
|
|
|
} else {
|
|
|
|
// Validate birthdate is at least age 18.
|
|
|
|
if utility.Age(birthdate) < 18 {
|
|
|
|
session.FlashError(w, r, "Invalid birthdate: you must be at least 18 years old to use this site.")
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-12 02:24:09 +00:00
|
|
|
// If the user changes their birthdate, notify the admin.
|
2023-06-16 05:12:01 +00:00
|
|
|
if !user.Birthdate.IsZero() && user.Birthdate.Format("2006-01-02") != dob {
|
2023-09-12 02:24:09 +00:00
|
|
|
// Create an admin Feedback model.
|
|
|
|
fb := &models.Feedback{
|
|
|
|
Intent: "report",
|
|
|
|
Subject: "report.dob",
|
|
|
|
UserID: user.ID,
|
|
|
|
TableName: "users",
|
|
|
|
TableID: user.ID,
|
|
|
|
Message: fmt.Sprintf(
|
|
|
|
"A user has modified their birthdate on their profile page!\n\n"+
|
|
|
|
"* Original: %s (age %d)\n* Updated: %s (age %d)",
|
|
|
|
user.Birthdate, utility.Age(user.Birthdate),
|
|
|
|
birthdate, utility.Age(birthdate),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the feedback.
|
|
|
|
if err := models.CreateFeedback(fb); err != nil {
|
|
|
|
log.Error("Couldn't save feedback from user updating their DOB: %s", err)
|
|
|
|
}
|
2022-08-11 03:59:59 +00:00
|
|
|
}
|
2023-09-12 02:24:09 +00:00
|
|
|
|
|
|
|
// Work around DST issues: set the hour to noon.
|
|
|
|
user.Birthdate = birthdate.Add(12 * time.Hour)
|
2022-08-11 03:59:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set profile attributes.
|
|
|
|
for _, attr := range config.ProfileFields {
|
|
|
|
user.SetProfileField(attr, r.PostFormValue(attr))
|
|
|
|
}
|
|
|
|
|
|
|
|
// "Looking For" checkbox list.
|
|
|
|
if hereFor, ok := r.PostForm["here_for"]; ok {
|
|
|
|
user.SetProfileField("here_for", strings.Join(hereFor, ","))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
session.Flash(w, r, "Profile settings updated!")
|
2022-08-13 22:39:31 +00:00
|
|
|
case "preferences":
|
2023-06-24 05:18:09 +00:00
|
|
|
hashtag = "#prefs"
|
2022-08-13 22:39:31 +00:00
|
|
|
var (
|
2023-09-20 01:24:57 +00:00
|
|
|
explicit = r.PostFormValue("explicit") == "true"
|
|
|
|
blurExplicit = r.PostFormValue("blur_explicit")
|
2023-09-24 18:41:19 +00:00
|
|
|
autoplayGif = r.PostFormValue("autoplay_gif")
|
2022-08-13 22:39:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
user.Explicit = explicit
|
2023-09-19 00:22:50 +00:00
|
|
|
|
2023-09-20 01:24:57 +00:00
|
|
|
// Set profile field prefs.
|
|
|
|
user.SetProfileField("blur_explicit", blurExplicit)
|
2023-09-24 18:41:19 +00:00
|
|
|
if autoplayGif != "true" {
|
|
|
|
autoplayGif = "false"
|
|
|
|
}
|
|
|
|
user.SetProfileField("autoplay_gif", autoplayGif)
|
2023-09-20 01:24:57 +00:00
|
|
|
|
2023-09-19 00:22:50 +00:00
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
session.Flash(w, r, "Website preferences updated!")
|
|
|
|
case "privacy":
|
|
|
|
hashtag = "#privacy"
|
|
|
|
var (
|
|
|
|
visibility = models.UserVisibility(r.PostFormValue("visibility"))
|
|
|
|
dmPrivacy = r.PostFormValue("dm_privacy")
|
|
|
|
)
|
|
|
|
|
2022-08-30 03:00:15 +00:00
|
|
|
user.Visibility = models.UserVisibilityPublic
|
|
|
|
|
|
|
|
for _, cmp := range models.UserVisibilityOptions {
|
|
|
|
if visibility == cmp {
|
|
|
|
user.Visibility = visibility
|
|
|
|
}
|
2022-08-22 00:29:39 +00:00
|
|
|
}
|
2022-08-13 22:39:31 +00:00
|
|
|
|
2023-06-24 05:18:09 +00:00
|
|
|
// Set profile field prefs.
|
2023-09-19 00:22:50 +00:00
|
|
|
user.SetProfileField("dm_privacy", dmPrivacy)
|
2023-06-24 05:18:09 +00:00
|
|
|
|
2022-08-13 22:39:31 +00:00
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
|
|
|
}
|
|
|
|
|
2023-09-19 00:22:50 +00:00
|
|
|
session.Flash(w, r, "Privacy settings updated!")
|
2023-10-28 21:34:35 +00:00
|
|
|
case "notifications":
|
|
|
|
hashtag = "#notifications"
|
|
|
|
|
|
|
|
// Store their notification opt-outs.
|
|
|
|
for _, key := range config.NotificationOptOutFields {
|
|
|
|
var value = r.PostFormValue(key)
|
|
|
|
|
|
|
|
// Boolean flip for DB storage:
|
|
|
|
// - Pre-existing users before these options are added have no pref stored in the DB
|
|
|
|
// - The default pref is opt-IN (receive all notifications)
|
|
|
|
// - The checkboxes on front-end are on by default, uncheck them to opt-out, checkbox value="true"
|
|
|
|
// - So when they post as "true" (default), we keep the notifications sending
|
|
|
|
// - If they uncheck the box, no value is sent and that's an opt-out.
|
|
|
|
if value == "" {
|
|
|
|
value = "true" // opt-out, store opt-out=true in the DB
|
|
|
|
} else if value == "true" {
|
|
|
|
value = "false" // the box remained checked, they don't opt-out, store opt-out=false in the DB
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save it. TODO: fires off inserts/updates for each one,
|
|
|
|
// probably not performant to do.
|
|
|
|
user.SetProfileField(key, value)
|
|
|
|
}
|
|
|
|
session.Flash(w, r, "Notification preferences updated!")
|
|
|
|
|
|
|
|
// Save the user for new fields to be committed to DB.
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are they unsubscribing from all threads?
|
|
|
|
if r.PostFormValue("unsubscribe_all_threads") == "true" {
|
|
|
|
if err := models.UnsubscribeAllThreads(user); err != nil {
|
|
|
|
session.FlashError(w, r, "Couldn't unsubscribe from threads: %s", err)
|
|
|
|
} else {
|
|
|
|
session.Flash(w, r, "Unsubscribed from all comment threads!")
|
|
|
|
}
|
|
|
|
}
|
2023-08-20 02:11:33 +00:00
|
|
|
case "location":
|
|
|
|
hashtag = "#location"
|
|
|
|
var (
|
|
|
|
source = r.PostFormValue("source")
|
|
|
|
latStr = r.PostFormValue("latitude")
|
|
|
|
lonStr = r.PostFormValue("longitude")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Get and update the user's location.
|
|
|
|
location := models.GetUserLocation(user.ID)
|
|
|
|
location.Source = source
|
|
|
|
|
|
|
|
if lat, err := strconv.ParseFloat(latStr, 64); err == nil {
|
|
|
|
location.Latitude = lat
|
|
|
|
} else {
|
|
|
|
location.Latitude = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
if lon, err := strconv.ParseFloat(lonStr, 64); err == nil {
|
|
|
|
location.Longitude = lon
|
|
|
|
} else {
|
|
|
|
location.Longitude = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save it.
|
|
|
|
if err := location.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Couldn't save your location preference: %s", err)
|
|
|
|
} else {
|
|
|
|
session.Flash(w, r, "Location settings updated!")
|
|
|
|
}
|
2022-08-11 03:59:59 +00:00
|
|
|
case "settings":
|
2023-06-24 05:18:09 +00:00
|
|
|
hashtag = "#account"
|
2022-08-14 21:40:57 +00:00
|
|
|
var (
|
|
|
|
oldPassword = r.PostFormValue("old_password")
|
|
|
|
changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email")))
|
2022-08-22 01:19:30 +00:00
|
|
|
password1 = strings.TrimSpace(r.PostFormValue("new_password"))
|
|
|
|
password2 = strings.TrimSpace(r.PostFormValue("new_password2"))
|
2022-08-14 21:40:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Their old password is needed to make any changes to their account.
|
|
|
|
if err := user.CheckPassword(oldPassword); err != nil {
|
|
|
|
session.FlashError(w, r, "Could not make changes to your account settings as the 'current password' you entered was incorrect.")
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Changing their email?
|
|
|
|
if changeEmail != user.Email {
|
|
|
|
// Validate the email.
|
|
|
|
if _, err := nm.ParseAddress(changeEmail); err != nil {
|
|
|
|
session.FlashError(w, r, "The email address you entered is not valid: %s", err)
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Email must not already exist.
|
|
|
|
if _, err := models.FindUser(changeEmail); err == nil {
|
|
|
|
session.FlashError(w, r, "That email address is already in use.")
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a tokenized link.
|
|
|
|
token := ChangeEmailToken{
|
|
|
|
Token: uuid.New().String(),
|
|
|
|
UserID: user.ID,
|
|
|
|
NewEmail: changeEmail,
|
|
|
|
}
|
|
|
|
if err := redis.Set(fmt.Sprintf(config.ChangeEmailRedisKey, token.Token), token, config.SignupTokenExpires); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to create change email token: %s", err)
|
|
|
|
templates.Redirect(w, r.URL.Path)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := mail.Send(mail.Message{
|
|
|
|
To: changeEmail,
|
|
|
|
Subject: "Verify your e-mail address",
|
|
|
|
Template: "email/verify_email.html",
|
|
|
|
Data: map[string]interface{}{
|
|
|
|
"Title": config.Title,
|
|
|
|
"URL": config.Current.BaseURL + "/settings/confirm-email?token=" + token.Token,
|
|
|
|
"ChangeEmail": true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
session.FlashError(w, r, "Error sending a confirmation email to %s: %s", changeEmail, err)
|
|
|
|
} else {
|
|
|
|
session.Flash(w, r, "Please verify your new email address. A link has been sent to %s to confirm.", changeEmail)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Changing their password?
|
|
|
|
if password1 != "" {
|
|
|
|
if password2 != password1 {
|
2022-08-22 01:19:30 +00:00
|
|
|
log.Error("pw1=%s pw2=%s", password1, password2)
|
2022-08-14 21:40:57 +00:00
|
|
|
session.FlashError(w, r, "Couldn't change your password: your new passwords do not match.")
|
|
|
|
} else {
|
|
|
|
// Hash the new password.
|
|
|
|
if err := user.HashPassword(password1); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to hash your new password: %s", err)
|
|
|
|
} else {
|
|
|
|
// Save the user row.
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Failed to update your password in the database: %s", err)
|
|
|
|
} else {
|
|
|
|
session.Flash(w, r, "Your password has been updated.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-11 03:59:59 +00:00
|
|
|
default:
|
|
|
|
session.FlashError(w, r, "Unknown POST intent value. Please try again.")
|
|
|
|
}
|
|
|
|
|
2023-06-24 05:18:09 +00:00
|
|
|
templates.Redirect(w, r.URL.Path+hashtag+".")
|
2022-08-11 03:59:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-20 02:11:33 +00:00
|
|
|
// For the Location tab: get GeoIP insights.
|
|
|
|
insights, err := geoip.GetRequestInsights(r)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("GetRequestInsights: %s", err)
|
|
|
|
}
|
|
|
|
vars["GeoIPInsights"] = insights
|
|
|
|
vars["UserLocation"] = models.GetUserLocation(user.ID)
|
|
|
|
|
2023-09-19 00:22:50 +00:00
|
|
|
// Show enabled status for 2FA.
|
|
|
|
vars["TwoFactorEnabled"] = models.Get2FA(user.ID).Enabled
|
|
|
|
|
2023-10-28 21:34:35 +00:00
|
|
|
// Count of subscribed comment threads.
|
|
|
|
vars["SubscriptionCount"] = models.CountSubscriptions(user)
|
|
|
|
|
2022-08-11 03:59:59 +00:00
|
|
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2022-08-14 21:40:57 +00:00
|
|
|
|
|
|
|
// ConfirmEmailChange after a user tries to change their email.
|
|
|
|
func ConfirmEmailChange() http.HandlerFunc {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var tokenStr = r.FormValue("token")
|
|
|
|
|
|
|
|
if tokenStr != "" {
|
|
|
|
var token ChangeEmailToken
|
|
|
|
if err := redis.Get(fmt.Sprintf(config.ChangeEmailRedisKey, tokenStr), &token); err != nil {
|
|
|
|
session.FlashError(w, r, "Invalid token. Please try again to change your email address.")
|
|
|
|
templates.Redirect(w, "/")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify new email still doesn't already exist.
|
|
|
|
if _, err := models.FindUser(token.NewEmail); err == nil {
|
|
|
|
session.FlashError(w, r, "Couldn't update your email address: it is already in use by another member.")
|
|
|
|
templates.Redirect(w, "/")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look up the user.
|
|
|
|
user, err := models.GetUser(token.UserID)
|
|
|
|
if err != nil {
|
|
|
|
session.FlashError(w, r, "Didn't find the user that this email change was for. Please try again.")
|
|
|
|
templates.Redirect(w, "/")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Burn the token.
|
|
|
|
if err := token.Delete(); err != nil {
|
|
|
|
log.Error("ChangeEmail: couldn't delete Redis token: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the change.
|
|
|
|
user.Email = token.NewEmail
|
|
|
|
if err := user.Save(); err != nil {
|
|
|
|
session.FlashError(w, r, "Couldn't save the change to your user: %s", err)
|
|
|
|
} else {
|
|
|
|
session.Flash(w, r, "Your email address has been confirmed and updated.")
|
|
|
|
templates.Redirect(w, "/")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
session.FlashError(w, r, "Invalid change email token. Please try again.")
|
|
|
|
}
|
|
|
|
|
|
|
|
templates.Redirect(w, "/")
|
|
|
|
})
|
|
|
|
}
|