diff --git a/pkg/controller/account/signup.go b/pkg/controller/account/signup.go index f34790a..7d58806 100644 --- a/pkg/controller/account/signup.go +++ b/pkg/controller/account/signup.go @@ -273,6 +273,11 @@ func Signup() http.HandlerFunc { user.Birthdate = birthdate user.Save() + // Restore their block lists if this user has deleted their account before. + if err := models.RestoreDeletedUserMemory(user); err != nil { + log.Error("RestoreDeletedUserMemory(%s): %s", user.Username, err) + } + // Log in the user and send them to their dashboard. session.LoginUser(w, r, user) templates.Redirect(w, "/me") diff --git a/pkg/models/blocklist.go b/pkg/models/blocklist.go index 53f2d3f..0aca9ab 100644 --- a/pkg/models/blocklist.go +++ b/pkg/models/blocklist.go @@ -166,6 +166,54 @@ func BlockedUserIDsByUser(userId uint64) []uint64 { return userIDs } +// GetAllBlockedUserIDs returns the forward and reverse lists of blocked user IDs for the user. +func GetAllBlockedUserIDs(user *User) (forward, reverse []uint64) { + var ( + bs = []*Block{} + ) + DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs) + for _, row := range bs { + if row.SourceUserID == user.ID { + forward = append(forward, row.TargetUserID) + } else if row.TargetUserID == user.ID { + reverse = append(reverse, row.SourceUserID) + } + } + return forward, reverse +} + +// BulkRestoreBlockedUserIDs inserts many blocked user IDs in one query. +// +// Returns the count of blocks added. +func BulkRestoreBlockedUserIDs(user *User, forward, reverse []uint64) (int, error) { + var bs = []*Block{} + + // Forward list. + for _, uid := range forward { + bs = append(bs, &Block{ + SourceUserID: user.ID, + TargetUserID: uid, + }) + } + + // Reverse list. + for _, uid := range reverse { + bs = append(bs, &Block{ + SourceUserID: uid, + TargetUserID: user.ID, + }) + } + + // Anything to do? + if len(bs) == 0 { + return 0, nil + } + + // Batch create. + res := DB.Create(bs) + return len(bs), res.Error +} + // BlockedUsernames returns all usernames blocked by (or blocking) the user. func BlockedUsernames(user *User) []string { var ( diff --git a/pkg/models/deleted_user_memory.go b/pkg/models/deleted_user_memory.go new file mode 100644 index 0000000..bf9774a --- /dev/null +++ b/pkg/models/deleted_user_memory.go @@ -0,0 +1,130 @@ +package models + +import ( + "encoding/json" + "fmt" + "time" + + "code.nonshy.com/nonshy/website/pkg/encryption" + "code.nonshy.com/nonshy/website/pkg/log" + "gorm.io/gorm/clause" +) + +// DeletedUserMemory table stores security-related information, such as block lists, when a user +// deletes their account - so that when they sign up again later under the same username or email +// address, this information can be restored and other users on the site can have their block lists +// respected. +type DeletedUserMemory struct { + Username string `gorm:"uniqueIndex:idx_deleted_user_memory"` + HashedEmail string `gorm:"uniqueIndex:idx_deleted_user_memory"` + Data string + CreatedAt time.Time + UpdatedAt time.Time +} + +// DeletedUserMemoryData is a JSON serializable struct for data stored in the DeletedUserMemory table. +type DeletedUserMemoryData struct { + PreviousUsername string + BlockingUserIDs []uint64 + BlockedByUserIDs []uint64 +} + +// CreateDeletedUserMemory creates the row in the database just before a user account is deleted. +func CreateDeletedUserMemory(user *User) error { + // Get the user's blocked lists. + forward, reverse := GetAllBlockedUserIDs(user) + + // Store the memory. + data := DeletedUserMemoryData{ + PreviousUsername: user.Username, + BlockingUserIDs: forward, + BlockedByUserIDs: reverse, + } + bin, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("JSON marshal: %s", err) + } + + // Upsert the mute. + m := &DeletedUserMemory{ + Username: user.Username, + HashedEmail: string(encryption.Hash([]byte(user.Email))), + Data: string(bin), + } + res := DB.Model(&DeletedUserMemory{}).Clauses( + clause.OnConflict{ + Columns: []clause.Column{ + {Name: "username"}, + {Name: "hashed_email"}, + }, + UpdateAll: true, + }, + ).Create(m) + return res.Error +} + +// RestoreDeletedUserMemory checks for remembered data and will restore and clear the memory if found. +func RestoreDeletedUserMemory(user *User) error { + // Anything stored? + var ( + m *DeletedUserMemory + hashedEmail = string(encryption.Hash([]byte(user.Email))) + err = DB.Model(&DeletedUserMemory{}).Where( + "username = ? OR hashed_email = ?", + user.Username, + hashedEmail, + ).First(&m).Error + ) + + if err != nil { + return nil + } + + // Parse the remembered payload. + var data DeletedUserMemoryData + err = json.Unmarshal([]byte(m.Data), &data) + if err != nil { + return fmt.Errorf("RestoreDeletedUserMemory: JSON unmarshal: %s", err) + } + + // Bulk restore the user's block list. + blocks, err := BulkRestoreBlockedUserIDs(user, data.BlockingUserIDs, data.BlockedByUserIDs) + if err != nil { + log.Error("BulkRestoreBlockedUserIDs(%s): %s", user.Username, err) + } + + // If any blocks were added, notify the admin that the user has returned - for visibility + // and to detect any mistakes. + if blocks > 0 { + fb := &Feedback{ + Intent: "report", + Subject: "A deleted user has returned", + UserID: user.ID, + TableName: "users", + TableID: user.ID, + Message: fmt.Sprintf( + "The username **@%s**, who was previously deleted, has signed up a new account "+ + "with the same username or e-mail address.\n\n"+ + "Their previous username was **@%s** when they last deleted their account.\n\n"+ + "Their block lists from when they deleted their old account have been restored:\n\n"+ + "* Forward list: %d\n* Reverse list: %d", + user.Username, + data.PreviousUsername, + len(data.BlockingUserIDs), + len(data.BlockedByUserIDs), + ), + } + + // Save the feedback. + if err := CreateFeedback(fb); err != nil { + log.Error("Couldn't save feedback from user recovering their deleted account: %s", err) + } + } + + // Delete the stored user memory. + return DB.Where( + "username = ? OR hashed_email = ?", + user.Username, + hashedEmail, + ).Delete(&DeletedUserMemory{}).Error +} diff --git a/pkg/models/deletion/delete_user.go b/pkg/models/deletion/delete_user.go index 822e47c..966e8f9 100644 --- a/pkg/models/deletion/delete_user.go +++ b/pkg/models/deletion/delete_user.go @@ -13,6 +13,12 @@ import ( func DeleteUser(user *models.User) error { log.Error("BEGIN DeleteUser(%d, %s)", user.ID, user.Username) + // Store the user's block lists in case they come back soon under the same email address + // or username. + if err := models.CreateDeletedUserMemory(user); err != nil { + log.Error("DeleteUser(%s): CreateDeletedUserMemory: %s", user.Username, err) + } + // Clear their history on the chat room. go func() { i, err := chat.EraseChatHistory(user.Username) diff --git a/pkg/models/models.go b/pkg/models/models.go index 3b97f07..cca44ac 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -42,6 +42,7 @@ func AutoMigrate() { // Non-user or persistent data. &AdminScope{}, + &DeletedUserMemory{}, &Forum{}, // Vendor/misc data. diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index 71d3390..62ec1b7 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -207,7 +207,7 @@ {{end}}