DeletedUserMemory to restore blocklists on returning accounts
This commit is contained in:
parent
6a2e5d0809
commit
332bcc9f66
|
@ -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")
|
||||
|
|
|
@ -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 (
|
||||
|
|
130
pkg/models/deleted_user_memory.go
Normal file
130
pkg/models/deleted_user_memory.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -42,6 +42,7 @@ func AutoMigrate() {
|
|||
|
||||
// Non-user or persistent data.
|
||||
&AdminScope{},
|
||||
&DeletedUserMemory{},
|
||||
&Forum{},
|
||||
|
||||
// Vendor/misc data.
|
||||
|
|
|
@ -207,7 +207,7 @@
|
|||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
<button type="submit" class="button is-primary">Continue and verify email</button>
|
||||
<button type="submit" class="button is-primary">Continue{{if not .SignupToken}} and verify email{{end}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user