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 }