diff --git a/backend/db/user.go b/backend/db/user.go index 6e288c7..dddffa8 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -403,3 +403,16 @@ func (db *DB) UndoDeleteUser(ctx context.Context, id xid.ID) error { } return nil } + +func (db *DB) ForceDeleteUser(ctx context.Context, id xid.ID) error { + sql, args, err := sq.Delete("users").Where("id = ?", id).ToSql() + if err != nil { + return errors.Wrap(err, "building sql") + } + + _, err = db.Exec(ctx, sql, args...) + if err != nil { + return errors.Wrap(err, "executing query") + } + return nil +} diff --git a/backend/routes/auth/routes.go b/backend/routes/auth/routes.go index 79366b9..fd833a3 100644 --- a/backend/routes/auth/routes.go +++ b/backend/routes/auth/routes.go @@ -103,6 +103,9 @@ func Mount(srv *server.Server, r chi.Router) { // cancel user delete // uses a special token, so handled in the function itself r.Get("/cancel-delete", server.WrapHandler(s.cancelDelete)) + // force user delete + // uses a special token (same as above) + r.Get("/force-delete", server.WrapHandler(s.forceDelete)) }) } diff --git a/backend/routes/auth/undelete.go b/backend/routes/auth/undelete.go index c9db8f1..8768b2f 100644 --- a/backend/routes/auth/undelete.go +++ b/backend/routes/auth/undelete.go @@ -6,9 +6,11 @@ import ( "encoding/base64" "net/http" + "codeberg.org/u1f320/pronouns.cc/backend/db" "codeberg.org/u1f320/pronouns.cc/backend/log" "codeberg.org/u1f320/pronouns.cc/backend/server" "emperror.dev/errors" + "github.com/georgysavva/scany/pgxscan" "github.com/go-chi/render" "github.com/mediocregopher/radix/v4" "github.com/rs/xid" @@ -57,7 +59,7 @@ func (s *Server) saveUndeleteToken(ctx context.Context, userID xid.ID, token str func (s *Server) getUndeleteToken(ctx context.Context, token string) (userID xid.ID, err error) { var idString string - err = s.DB.Redis.Do(ctx, radix.Cmd(&idString, "GET", "undelete:"+token)) + err = s.DB.Redis.Do(ctx, radix.Cmd(&idString, "GETDEL", "undelete:"+token)) if err != nil { return userID, errors.Wrap(err, "getting undelete key") } @@ -68,3 +70,79 @@ func (s *Server) getUndeleteToken(ctx context.Context, token string) (userID xid } return userID, nil } + +func (s *Server) forceDelete(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + token := r.Header.Get("X-Delete-Token") + if token == "" { + return server.APIError{Code: server.ErrForbidden} + } + + id, err := s.getUndeleteToken(ctx, token) + if err != nil { + log.Errorf("getting delete token: %v", err) + return server.APIError{Code: server.ErrNotFound} // assume invalid token + } + + u, err := s.DB.User(ctx, id) + if err != nil { + log.Errorf("getting user: %v", err) + return errors.Wrap(err, "getting user") + } + + if u.Avatar != nil { + err = s.DB.DeleteUserAvatar(ctx, u.ID, *u.Avatar) + if err != nil { + log.Errorf("deleting avatars for user %v: %v", u.ID, err) + return errors.Wrap(err, "deleting user avatar") + } + } + + var exports []db.DataExport + err = pgxscan.Select(ctx, s.DB, &exports, "SELECT * FROM data_exports WHERE user_id = $1", u.ID) + if err != nil { + log.Errorf("getting to-be-deleted export files: %v", err) + return errors.Wrap(err, "getting export iles") + } + + for _, de := range exports { + err = s.DB.DeleteExport(ctx, de) + if err != nil { + log.Errorf("deleting export %v: %v", de.ID, err) + continue + } + + log.Debugf("deleted export %v", de.ID) + } + + members, err := s.DB.UserMembers(ctx, u.ID) + if err != nil { + log.Errorf("getting members for user %v: %v", u.ID, err) + return errors.Wrap(err, "getting members") + } + + for _, m := range members { + if m.Avatar == nil { + continue + } + + log.Debugf("deleting avatars for member %v", m.ID) + + err = s.DB.DeleteMemberAvatar(ctx, m.ID, *m.Avatar) + if err != nil { + log.Errorf("deleting avatars for member %v: %v", m.ID, err) + continue + } + + log.Debugf("deleted avatars for member %v", m.ID) + } + + err = s.DB.ForceDeleteUser(ctx, u.ID) + if err != nil { + log.Errorf("force deleting user: %v", err) + return errors.Wrap(err, "deleting user") + } + + render.JSON(w, r, map[string]any{"success": true}) + return nil +}