Ability to change username

pull/38/head
Noah Petherbridge 2024-01-27 13:57:24 -08:00
parent ef8abec7bf
commit fedfbed4eb
6 changed files with 140 additions and 35 deletions

View File

@ -18,6 +18,7 @@ import (
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
"code.nonshy.com/nonshy/website/pkg/utility"
"code.nonshy.com/nonshy/website/pkg/worker"
"github.com/google/uuid"
)
@ -50,6 +51,10 @@ func Settings() http.HandlerFunc {
return
}
// Is the user currently in the chat room? Gate username changes when so.
var isOnChat = worker.GetChatStatistics().IsOnline(user.Username)
vars["OnChat"] = isOnChat
// URL hashtag to redirect to
var hashtag string
@ -293,32 +298,79 @@ func Settings() http.HandlerFunc {
case "settings":
hashtag = "#account"
var (
oldPassword = r.PostFormValue("old_password")
changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email")))
password1 = strings.TrimSpace(r.PostFormValue("new_password"))
password2 = strings.TrimSpace(r.PostFormValue("new_password2"))
oldPassword = r.PostFormValue("old_password")
changeEmail = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_email")))
changeUsername = strings.TrimSpace(strings.ToLower(r.PostFormValue("change_username")))
password1 = strings.TrimSpace(r.PostFormValue("new_password"))
password2 = strings.TrimSpace(r.PostFormValue("new_password2"))
)
// 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)
templates.Redirect(w, r.URL.Path+hashtag)
return
}
// Changing their username?
if changeUsername != user.Username {
// Not if they are in the chat room!
if isOnChat {
session.FlashError(w, r, "Your username could not be changed right now because you are logged into the chat room. Please exit the chat room, wait a minute, and try your request again.")
templates.Redirect(w, r.URL.Path+hashtag)
return
}
// Check if the new name is OK.
if err := models.IsValidUsername(changeUsername); err != nil {
session.FlashError(w, r, "Could not change your username: %s", err.Error())
templates.Redirect(w, r.URL.Path+hashtag)
return
}
// Set their name.
origUsername := user.Username
user.Username = changeUsername
if err := user.Save(); err != nil {
session.FlashError(w, r, "Error saving your new username: %s", err)
} else {
session.Flash(w, r, "Your username has been updated to: %s", user.Username)
// Notify the admin about this to keep tabs if someone is acting strangely
// with too-frequent username changes.
fb := &models.Feedback{
Intent: "report",
Subject: "Change of username",
UserID: user.ID,
TableName: "users",
TableID: user.ID,
Message: fmt.Sprintf(
"A user has modified their username on their profile page!\n\n"+
"* Original: %s\n* Updated: %s",
origUsername, changeUsername,
),
}
// Save the feedback.
if err := models.CreateFeedback(fb); err != nil {
log.Error("Couldn't save feedback from user updating their DOB: %s", err)
}
}
}
// 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)
templates.Redirect(w, r.URL.Path+hashtag)
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)
templates.Redirect(w, r.URL.Path+hashtag)
return
}
@ -330,7 +382,7 @@ func Settings() http.HandlerFunc {
}
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)
templates.Redirect(w, r.URL.Path+hashtag)
return
}

View File

@ -86,6 +86,9 @@ func Signup() http.HandlerFunc {
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.
@ -95,21 +98,12 @@ func Signup() http.HandlerFunc {
return
}
// Reserved username check.
for _, cmp := range config.ReservedUsernames {
if username == cmp {
session.FlashError(w, r, "That username is reserved, please choose a different username.")
templates.Redirect(w, r.URL.Path+"?token="+tokenStr)
return
}
}
// Cache username in case of passwd validation errors.
vars["Email"] = email
vars["Username"] = username
// Is the app not configured to send email?
if !config.Current.Mail.Enabled {
if !config.Current.Mail.Enabled && !config.SkipEmailVerification {
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)
@ -209,7 +203,6 @@ func Signup() http.HandlerFunc {
}
// Full sign-up step (w/ email verification token), validate more things.
var hasError bool
if len(password) < 3 {
session.FlashError(w, r, "Please enter a password longer than 3 characters.")
hasError = true
@ -218,8 +211,9 @@ func Signup() http.HandlerFunc {
hasError = true
}
if !config.UsernameRegexp.MatchString(username) {
session.FlashError(w, r, "Your username must consist of only numbers, letters, - . and be 3-32 characters.")
// 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
}

View File

@ -153,6 +153,10 @@ func Landing() http.HandlerFunc {
log.Error("SendBlocklist: %s", err)
}
// Mark them as online immediately: so e.g. on the Change Username screen we leave no window
// of time where they can exist in chat but change their name on the site.
worker.GetChatStatistics().SetOnlineNow(currentUser.Username)
// Redirect them to the chat room.
templates.Redirect(w, strings.TrimSuffix(chatURL, "/")+"/?jwt="+ss)
return

View File

@ -186,6 +186,28 @@ func FindUser(username string) (*User, error) {
return u, result.Error
}
// IsValidUsername checks if a username is available and not reserved.
func IsValidUsername(username string) error {
// Check the formatting of the name.
if !config.UsernameRegexp.MatchString(username) {
return errors.New("Your username must consist of only numbers, letters, - . and be 3-32 characters.")
}
// Reserved username check.
for _, cmp := range config.ReservedUsernames {
if username == cmp {
return errors.New("That username is reserved, please choose a different username.")
}
}
// Does the username already exist?
if _, err := FindUser(username); err == nil {
return errors.New("That username already exists. Please try a different username.")
}
return nil
}
// IsShyFrom tells whether the user is shy from the perspective of the other user.
//
// That is, depending on our profile visibility and friendship status.

View File

@ -2,7 +2,7 @@ package worker
import (
"encoding/json"
"io/ioutil"
"io"
"net/http"
"sync"
"time"
@ -22,16 +22,10 @@ type ChatStatistics struct {
}
// GetChatStatistics returns the latest (cached) chat statistics.
func GetChatStatistics() ChatStatistics {
func GetChatStatistics() *ChatStatistics {
chatStatisticsMu.RLock()
defer chatStatisticsMu.RUnlock()
if cachedChatStatistics != nil {
return *cachedChatStatistics
}
return ChatStatistics{
Usernames: []string{},
}
return cachedChatStatistics
}
// SetChatStatistics updates the cached chat statistics, holding a write lock briefly.
@ -42,7 +36,7 @@ func SetChatStatistics(stats *ChatStatistics) {
}
// IsOnline returns whether the username is currently logged-in to chat.
func (cs ChatStatistics) IsOnline(username string) bool {
func (cs *ChatStatistics) IsOnline(username string) bool {
for _, user := range cs.Usernames {
if user == username {
return true
@ -51,10 +45,20 @@ func (cs ChatStatistics) IsOnline(username string) bool {
return false
}
// SetOnlineNow patches the current ChatStatistics to mark a user as online immediately, e.g.
// because the main site has just sent them to the chat with a JWT token.
func (cs *ChatStatistics) SetOnlineNow(username string) {
if !cs.IsOnline(username) {
chatStatisticsMu.Lock()
defer chatStatisticsMu.Unlock()
cs.Usernames = append(cs.Usernames, username)
}
}
type UserOnChatMap map[string]bool
// MapUsersOnline returns a hashmap of usernames to online status.
func (cs ChatStatistics) MapUsersOnline(usernames []string) UserOnChatMap {
func (cs *ChatStatistics) MapUsersOnline(usernames []string) UserOnChatMap {
var result = UserOnChatMap{}
for _, user := range cs.Usernames {
result[user] = true
@ -68,7 +72,7 @@ func (m UserOnChatMap) Get(username string) bool {
}
var (
cachedChatStatistics *ChatStatistics
cachedChatStatistics = &ChatStatistics{}
chatStatisticsMu sync.RWMutex
)
@ -117,7 +121,7 @@ func DoCheckBareRTC() {
if res.StatusCode == http.StatusOK {
var cs ChatStatistics
body, _ := ioutil.ReadAll(res.Body)
body, _ := io.ReadAll(res.Body)
res.Body.Close()
if err = json.Unmarshal(body, &cs); err != nil {
log.Error("WatchBareRTC: json decode error: %s", err)

View File

@ -1120,7 +1120,7 @@
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">
<i class="fa fa-lock pr-2"></i>
Update E-mail or Password
Update E-mail, Username or Password
</p>
</header>
@ -1129,6 +1129,13 @@
<input type="hidden" name="intent" value="settings">
{{InputCSRF}}
<div class="block">
You may use the fields below to change the e-mail address you log in with,
change your username on the site, or set a new password. You will need to
confirm your current password first before making these changes to your
account.
</div>
<div class="field">
<label class="label" for="old_password">
Current Password
@ -1152,6 +1159,28 @@
value="{{.CurrentUser.Email}}">
</div>
<div class="field">
<label class="label" for="change_username">Change Username</label>
<input type="text" class="input"
id="change_username"
name="change_username"
placeholder="{{.CurrentUser.Username}}"
value="{{.CurrentUser.Username}}"
{{if .OnChat}}readonly{{end}}>
<p class="help">
{{if .OnChat}}
<span class="has-text-danger">
<i class="fa fa-exclamation-triangle mr-1"></i>
You are currently logged into the chat room, so your username may not be
updated at this time. To change your username, please log out of the chat
room and wait a minute before reloading this page.
</span>
{{else}}
Usernames are 3 to 32 characters a-z 0-9 . -
{{end}}
</p>
</div>
<div class="field">
<label class="label">Change Password</label>
<input type="password" class="input mb-2"