website/pkg/models/deleted_user_memory.go

131 lines
3.7 KiB
Go

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
}