From fedfbed4ebeb71686ee155224bbcdebbc2cf570b Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 27 Jan 2024 13:57:24 -0800 Subject: [PATCH] Ability to change username --- pkg/controller/account/settings.go | 68 +++++++++++++++++++++++++---- pkg/controller/account/signup.go | 20 +++------ pkg/controller/chat/chat.go | 4 ++ pkg/models/user.go | 22 ++++++++++ pkg/worker/barertc.go | 30 +++++++------ web/templates/account/settings.html | 31 ++++++++++++- 6 files changed, 140 insertions(+), 35 deletions(-) diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go index 575d9e2..b454f49 100644 --- a/pkg/controller/account/settings.go +++ b/pkg/controller/account/settings.go @@ -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 } diff --git a/pkg/controller/account/signup.go b/pkg/controller/account/signup.go index 2cc4e18..3d0df19 100644 --- a/pkg/controller/account/signup.go +++ b/pkg/controller/account/signup.go @@ -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 } diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go index 9a3a178..ca5f5c5 100644 --- a/pkg/controller/chat/chat.go +++ b/pkg/controller/chat/chat.go @@ -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 diff --git a/pkg/models/user.go b/pkg/models/user.go index 4e04af0..561030d 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -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. diff --git a/pkg/worker/barertc.go b/pkg/worker/barertc.go index b165502..e3b97a1 100644 --- a/pkg/worker/barertc.go +++ b/pkg/worker/barertc.go @@ -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) diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index 2b177db..96987af 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -1120,7 +1120,7 @@ @@ -1129,6 +1129,13 @@ {{InputCSRF}} +
+ 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. +
+
+
+ + +

+ {{if .OnChat}} + + + 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. + + {{else}} + Usernames are 3 to 32 characters a-z 0-9 . - + {{end}} +

+
+