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.Birthdate = birthdate
|
||||||
user.Save()
|
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.
|
// Log in the user and send them to their dashboard.
|
||||||
session.LoginUser(w, r, user)
|
session.LoginUser(w, r, user)
|
||||||
templates.Redirect(w, "/me")
|
templates.Redirect(w, "/me")
|
||||||
|
|
|
@ -166,6 +166,54 @@ func BlockedUserIDsByUser(userId uint64) []uint64 {
|
||||||
return userIDs
|
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.
|
// BlockedUsernames returns all usernames blocked by (or blocking) the user.
|
||||||
func BlockedUsernames(user *User) []string {
|
func BlockedUsernames(user *User) []string {
|
||||||
var (
|
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 {
|
func DeleteUser(user *models.User) error {
|
||||||
log.Error("BEGIN DeleteUser(%d, %s)", user.ID, user.Username)
|
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.
|
// Clear their history on the chat room.
|
||||||
go func() {
|
go func() {
|
||||||
i, err := chat.EraseChatHistory(user.Username)
|
i, err := chat.EraseChatHistory(user.Username)
|
||||||
|
|
|
@ -42,6 +42,7 @@ func AutoMigrate() {
|
||||||
|
|
||||||
// Non-user or persistent data.
|
// Non-user or persistent data.
|
||||||
&AdminScope{},
|
&AdminScope{},
|
||||||
|
&DeletedUserMemory{},
|
||||||
&Forum{},
|
&Forum{},
|
||||||
|
|
||||||
// Vendor/misc data.
|
// Vendor/misc data.
|
||||||
|
|
|
@ -207,7 +207,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="field">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user