diff --git a/admin.go b/admin.go index 0a73a11..83dcc80 100644 --- a/admin.go +++ b/admin.go @@ -187,7 +187,11 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque var err error p.User, err = app.db.GetUserForAuth(username) if err != nil { - return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)} + if err == ErrUserNotFound { + return err + } + log.Error("Could not get user: %v", err) + return impart.HTTPError{http.StatusInternalServerError, err.Error()} } flashes, _ := getSessionFlashes(app, w, r, nil) diff --git a/app.go b/app.go index d465a3e..170c321 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/manifoldco/promptui" - "github.com/writeas/go-strip-markdown" + stripmd "github.com/writeas/go-strip-markdown" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/converter" @@ -689,6 +689,52 @@ func ResetPassword(apper Apper, username string) error { return nil } +// DoDeleteAccount runs the confirmation and account delete process. +func DoDeleteAccount(apper Apper, username string) error { + // Connect to the database + apper.LoadConfig() + connectToDatabase(apper.App()) + defer shutdown(apper.App()) + + // check user exists + u, err := apper.App().db.GetUserForAuth(username) + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + userID := u.ID + + // do not delete the admin account + // TODO: check for other admins and skip? + if u.IsAdmin() { + log.Error("Can not delete admin account") + os.Exit(1) + } + + // confirm deletion, w/ w/out posts + prompt := promptui.Prompt{ + Templates: &promptui.PromptTemplates{ + Success: "{{ . | bold | faint }}: ", + }, + Label: fmt.Sprintf("Really delete user : %s", username), + IsConfirm: true, + } + _, err = prompt.Run() + if err != nil { + log.Info("Aborted...") + os.Exit(0) + } + + log.Info("Deleting...") + err = apper.App().db.DeleteAccount(userID) + if err != nil { + log.Error("%s", err) + os.Exit(1) + } + log.Info("Success.") + return nil +} + func connectToDatabase(app *App) { log.Info("Connecting to %s database...", app.cfg.Database.Type) diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index 48993c7..7fc2342 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -13,11 +13,12 @@ package main import ( "flag" "fmt" + "os" + "strings" + "github.com/gorilla/mux" "github.com/writeas/web-core/log" "github.com/writeas/writefreely" - "os" - "strings" ) func main() { @@ -38,6 +39,7 @@ func main() { // Admin actions 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") + deleteUsername := flag.String("delete-user", "", "Delete a user with the given username") resetPassUser := flag.String("reset-pass", "", "Reset the given user's password") outputVersion := flag.Bool("v", false, "Output the current version") flag.Parse() @@ -102,6 +104,13 @@ func main() { os.Exit(1) } os.Exit(0) + } else if *deleteUsername != "" { + err := writefreely.DoDeleteAccount(app, *deleteUsername) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + os.Exit(0) } else if *migrate { err := writefreely.Migrate(app) if err != nil { diff --git a/database.go b/database.go index 2d233d4..2c44492 100644 --- a/database.go +++ b/database.go @@ -64,7 +64,7 @@ type writestore interface { GetAccessToken(userID int64) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) - DeleteAccount(userID int64) (l *string, err error) + DeleteAccount(userID int64) error ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error @@ -2114,22 +2114,13 @@ func (db *datastore) CollectionHasAttribute(id int64, attr string) bool { return true } -func (db *datastore) DeleteAccount(userID int64) (l *string, err error) { - debug := "" - l = &debug - - t, err := db.Begin() - if err != nil { - stringLogln(l, "Unable to begin: %v", err) - return - } - +// DeleteAccount will delete the entire account for userID +func (db *datastore) DeleteAccount(userID int64) error { // Get all collections rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) if err != nil { - t.Rollback() - stringLogln(l, "Unable to get collections: %v", err) - return + log.Error("Unable to get collections: %v", err) + return err } defer rows.Close() colls := []Collection{} @@ -2137,103 +2128,158 @@ func (db *datastore) DeleteAccount(userID int64) (l *string, err error) { for rows.Next() { err = rows.Scan(&c.ID, &c.Alias) if err != nil { - t.Rollback() - stringLogln(l, "Unable to scan collection cols: %v", err) - return + log.Error("Unable to scan collection cols: %v", err) + return err } 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 for _, c := range colls { - // TODO: user deleteCollection() func // Delete tokens res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete attributes on %s: %v", c.Alias, err) - return + log.Error("Unable to delete attributes on %s: %v", c.Alias, err) + return err } 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 res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete passwords on %s: %v", c.Alias, err) - return + log.Error("Unable to delete passwords on %s: %v", c.Alias, err) + return err } 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 res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete redirects on %s: %v", c.Alias, err) - return + log.Error("Unable to delete redirects on %s: %v", c.Alias, err) + return err } 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) + + // 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 res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete collections: %v", err) - return + log.Error("Unable to delete collections: %v", err) + return err } rs, _ := res.RowsAffected() - stringLogln(l, "Deleted %d from collections", rs) + log.Info("Deleted %d from collections", rs) // Delete tokens res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete access tokens: %v", err) - return + log.Error("Unable to delete access tokens: %v", err) + return err } rs, _ = res.RowsAffected() - stringLogln(l, "Deleted %d from accesstokens", rs) + log.Info("Deleted %d from accesstokens", rs) + + // Delete user attributes + res, err = t.Exec("DELETE FROM oauth_users WHERE user_id = ?", userID) + if err != nil { + t.Rollback() + log.Error("Unable to delete oauth_users: %v", err) + return err + } + rs, _ = res.RowsAffected() + log.Info("Deleted %d from oauth_users", rs) // Delete posts + // TODO: should maybe get each row so we can federate a delete + // if so needs to be outside of transaction like collections res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete posts: %v", err) - return + log.Error("Unable to delete posts: %v", err) + return err } rs, _ = res.RowsAffected() - stringLogln(l, "Deleted %d from posts", rs) + log.Info("Deleted %d from posts", rs) + // Delete user attributes res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete attributes: %v", err) - return + log.Error("Unable to delete attributes: %v", err) + return err } 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) if err != nil { t.Rollback() - stringLogln(l, "Unable to delete user: %v", err) - return + log.Error("Unable to delete user: %v", err) + return err } 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() if err != nil { t.Rollback() - stringLogln(l, "Unable to commit: %v", err) - return + log.Error("Unable to commit: %v", err) + return err } - return + // TODO: federate delete actor + + return nil } func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) {