add account deletion

CLI only but backend supports calls from app.db.DeleteAccount already

takes --delete-account user_id_number with optional --posts to also
delete posts. if --posts is omitted all user posts will be updated to
anonymous posts
This commit is contained in:
Rob Loranger 2019-10-31 15:16:10 -07:00
parent 3759f16ed3
commit c87ca11a52
No known key found for this signature in database
GPG Key ID: D6F1633A4F0903B8
4 changed files with 161 additions and 51 deletions

View File

@ -1068,3 +1068,7 @@ func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) s
// Return value // Return value
return s return s
} }
func deleteAccount(app *App, userID int64, posts bool) error {
return app.db.DeleteAccount(userID, posts)
}

45
app.go
View File

@ -30,7 +30,7 @@ import (
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/writeas/go-strip-markdown" stripmd "github.com/writeas/go-strip-markdown"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
@ -681,6 +681,49 @@ func ResetPassword(apper Apper, username string) error {
return nil return nil
} }
// DoDeleteAccount runs the confirmation and account delete process.
func DoDeleteAccount(apper Apper, userID int64, posts bool) error {
// Connect to the database
apper.LoadConfig()
connectToDatabase(apper.App())
defer shutdown(apper.App())
// do not delete the root admin account
// TODO: check for other admins and skip?
if userID == 1 {
log.Error("Can not delete admin account")
os.Exit(1)
}
// check user exists
if _, err := apper.App().db.GetUserByID(userID); err != nil {
log.Error("%s", err)
os.Exit(1)
}
// confirm deletion, w/ w/out posts
prompt := promptui.Prompt{
Templates: &promptui.PromptTemplates{
Success: "{{ . | bold | faint }}: ",
},
Label: fmt.Sprintf("Delete user with ID: %d", userID),
IsConfirm: true,
}
_, err := prompt.Run()
if err != nil {
log.Info("Aborted...")
os.Exit(0)
}
log.Info("Deleting...")
err = deleteAccount(apper.App(), userID, posts)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
log.Info("Success.")
return nil
}
func connectToDatabase(app *App) { func connectToDatabase(app *App) {
log.Info("Connecting to %s database...", app.cfg.Database.Type) log.Info("Connecting to %s database...", app.cfg.Database.Type)

View File

@ -13,11 +13,12 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely" "github.com/writeas/writefreely"
"os"
"strings"
) )
func main() { func main() {
@ -38,6 +39,8 @@ func main() {
// Admin actions // Admin actions
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
createUser := flag.String("create-user", "", "Create a regular user with the given username:password") createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
deleteUserID := flag.Int64("delete-user", 0, "Delete a user with the given id, does not delete posts. Use `--delete-user id --posts`")
deletePosts := flag.Bool("posts", false, "Optionally delete the user's posts during account deletion")
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
outputVersion := flag.Bool("v", false, "Output the current version") outputVersion := flag.Bool("v", false, "Output the current version")
flag.Parse() flag.Parse()
@ -102,6 +105,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
os.Exit(0) os.Exit(0)
} else if *deleteUserID != 0 {
err := writefreely.DoDeleteAccount(app, *deleteUserID, *deletePosts)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *migrate { } else if *migrate {
err := writefreely.Migrate(app) err := writefreely.Migrate(app)
if err != nil { if err != nil {

View File

@ -61,7 +61,7 @@ type writestore interface {
GetAccessToken(userID int64) (string, error) GetAccessToken(userID int64) (string, error)
GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (string, error)
GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error)
DeleteAccount(userID int64) (l *string, err error) DeleteAccount(userID int64, posts bool) error
ChangeSettings(app *App, u *User, s *userSettings) error ChangeSettings(app *App, u *User, s *userSettings) error
ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error
@ -2079,22 +2079,14 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool {
return true return true
} }
func (db *datastore) DeleteAccount(userID int64) (l *string, err error) { // DeleteAccount will delete the entire account for userID, and if posts
debug := "" // is true, also all posts associated with the userID
l = &debug func (db *datastore) DeleteAccount(userID int64, posts bool) error {
t, err := db.Begin()
if err != nil {
stringLogln(l, "Unable to begin: %v", err)
return
}
// Get all collections // Get all collections
rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() log.Error("Unable to get collections: %v", err)
stringLogln(l, "Unable to get collections: %v", err) return err
return
} }
defer rows.Close() defer rows.Close()
colls := []Collection{} colls := []Collection{}
@ -2102,13 +2094,20 @@ func (db *datastore) DeleteAccount(userID int64) (l *string, err error) {
for rows.Next() { for rows.Next() {
err = rows.Scan(&c.ID, &c.Alias) err = rows.Scan(&c.ID, &c.Alias)
if err != nil { if err != nil {
t.Rollback() log.Error("Unable to scan collection cols: %v", err)
stringLogln(l, "Unable to scan collection cols: %v", err) return err
return
} }
colls = append(colls, c) colls = append(colls, c)
} }
// Start transaction
t, err := db.Begin()
if err != nil {
log.Error("Unable to begin: %v", err)
return err
}
// Clean up all collection related information
var res sql.Result var res sql.Result
for _, c := range colls { for _, c := range colls {
// TODO: user deleteCollection() func // TODO: user deleteCollection() func
@ -2116,89 +2115,143 @@ func (db *datastore) DeleteAccount(userID int64) (l *string, err error) {
res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete attributes on %s: %v", c.Alias, err) log.Error("Unable to delete attributes on %s: %v", c.Alias, err)
return return err
} }
rs, _ := res.RowsAffected() rs, _ := res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionattributes", rs, c.Alias) log.Info("Deleted %d for %s from collectionattributes", rs, c.Alias)
// Remove any optional collection password // Remove any optional collection password
res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete passwords on %s: %v", c.Alias, err) log.Error("Unable to delete passwords on %s: %v", c.Alias, err)
return return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionpasswords", rs, c.Alias) log.Info("Deleted %d for %s from collectionpasswords", rs, c.Alias)
// Remove redirects to this collection // Remove redirects to this collection
res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete redirects on %s: %v", c.Alias, err) log.Error("Unable to delete redirects on %s: %v", c.Alias, err)
return return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d for %s from collectionredirects", rs, c.Alias) log.Info("Deleted %d for %s from collectionredirects", rs, c.Alias)
// Remove any collection keys
res, err = t.Exec("DELETE FROM collectionkeys WHERE collection_id = ?", c.ID)
if err != nil {
t.Rollback()
log.Error("Unable to delete keys on %s: %v", c.Alias, err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d for %s from collectionkeys", rs, c.Alias)
// only remove collection in posts if not deleting the user posts
if !posts {
// Float all collection's posts
res, err = t.Exec("UPDATE posts SET collection_id = NULL WHERE collection_id = ? AND owner_id = ?", c.ID, userID)
if err != nil {
t.Rollback()
log.Error("Unable to update collection %s for posts: %v", err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Removed %d posts from collection %s", rs, c.Alias)
}
// TODO: federate delete collection
// Remove remote follows
res, err = t.Exec("DELETE FROM remotefollows WHERE collection_id = ?", c.ID)
if err != nil {
t.Rollback()
log.Error("Unable to delete remote follows on %s: %v", c.Alias, err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d for %s from remotefollows", rs, c.Alias)
} }
// Delete collections // Delete collections
res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete collections: %v", err) log.Error("Unable to delete collections: %v", err)
return return err
} }
rs, _ := res.RowsAffected() rs, _ := res.RowsAffected()
stringLogln(l, "Deleted %d from collections", rs) log.Info("Deleted %d from collections", rs)
// Delete tokens // Delete tokens
res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete access tokens: %v", err) log.Error("Unable to delete access tokens: %v", err)
return return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from accesstokens", rs) log.Info("Deleted %d from accesstokens", rs)
// Delete posts // Delete posts
res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID) if posts {
if err != nil { // TODO: should maybe get each row so we can federate a delete
t.Rollback() // if so needs to be outside of transaction like collections
stringLogln(l, "Unable to delete posts: %v", err) res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID)
return if err != nil {
t.Rollback()
log.Error("Unable to delete posts: %v", err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d from posts", rs)
} }
rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from posts", rs)
// Delete user attributes
res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID) res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete attributes: %v", err) log.Error("Unable to delete attributes: %v", err)
return return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from userattributes", rs) log.Info("Deleted %d from userattributes", rs)
// Delete user invites
res, err = t.Exec("DELETE FROM userinvites WHERE owner_id = ?", userID)
if err != nil {
t.Rollback()
log.Error("Unable to delete invites: %v", err)
return err
}
rs, _ = res.RowsAffected()
log.Info("Deleted %d from userinvites", rs)
// Delete the user
res, err = t.Exec("DELETE FROM users WHERE id = ?", userID) res, err = t.Exec("DELETE FROM users WHERE id = ?", userID)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to delete user: %v", err) log.Error("Unable to delete user: %v", err)
return return err
} }
rs, _ = res.RowsAffected() rs, _ = res.RowsAffected()
stringLogln(l, "Deleted %d from users", rs) log.Info("Deleted %d from users", rs)
// Commit all changes to the database
err = t.Commit() err = t.Commit()
if err != nil { if err != nil {
t.Rollback() t.Rollback()
stringLogln(l, "Unable to commit: %v", err) log.Error("Unable to commit: %v", err)
return return err
} }
return // TODO: federate delete actor
return nil
} }
func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) { func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) {