From 40a4935ae0faee7cf144a8163b6e5d929f9a6d06 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sun, 30 Aug 2026 20:24:03 +0000 Subject: [PATCH] Implement delete user button on admin user profile page. --- bemani/data/mysql/game.py | 11 ++++++ bemani/data/mysql/lobby.py | 11 ++++++ bemani/data/mysql/machine.py | 8 +++++ bemani/data/mysql/music.py | 11 ++++++ bemani/data/mysql/network.py | 8 +++++ bemani/data/mysql/user.py | 36 +++++++++++++++++++ bemani/frontend/admin/admin.py | 27 ++++++++++++++ .../static/controllers/admin/user.react.js | 36 +++++++++++++++++++ 8 files changed, 148 insertions(+) diff --git a/bemani/data/mysql/game.py b/bemani/data/mysql/game.py index 69fb3d5..a3d2a6d 100644 --- a/bemani/data/mysql/game.py +++ b/bemani/data/mysql/game.py @@ -435,3 +435,14 @@ class GameData(BaseData): ) for result in cursor.mappings() ] + + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all game-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "DELETE FROM series_achievement WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + + sql = "DELETE FROM game_settings WHERE userid = :userid" + self.execute(sql, {"userid": userid}) diff --git a/bemani/data/mysql/lobby.py b/bemani/data/mysql/lobby.py index f2d4e66..28e8dc5 100644 --- a/bemani/data/mysql/lobby.py +++ b/bemani/data/mysql/lobby.py @@ -302,3 +302,14 @@ class LobbyData(BaseData): # Prune any orphaned lobbies too sql = "DELETE FROM lobby WHERE time <= :time" self.execute(sql, {"time": Time.now() - Time.SECONDS_IN_HOUR}) + + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all lobby-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "DELETE FROM playsession WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + + sql = "DELETE FROM lobby WHERE userid = :userid" + self.execute(sql, {"userid": userid}) diff --git a/bemani/data/mysql/machine.py b/bemani/data/mysql/machine.py index aafd589..0f36b9b 100644 --- a/bemani/data/mysql/machine.py +++ b/bemani/data/mysql/machine.py @@ -572,6 +572,14 @@ class MachineData(BaseData): for entry in cursor.mappings() ] + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all machine-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "DELETE FROM arcade_owner WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + def create_session(self, arcadeid: ArcadeID, expiration: int = (30 * 86400)) -> str: """ Given an arcade ID, create a session string. diff --git a/bemani/data/mysql/music.py b/bemani/data/mysql/music.py index 33c4e4d..cdc894f 100644 --- a/bemani/data/mysql/music.py +++ b/bemani/data/mysql/music.py @@ -1021,3 +1021,14 @@ class MusicData(BaseData): ) for result in cursor.mappings() ] + + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all music-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "DELETE FROM score_history WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + + sql = "DELETE FROM score WHERE userid = :userid" + self.execute(sql, {"userid": userid}) diff --git a/bemani/data/mysql/network.py b/bemani/data/mysql/network.py index 27a8f28..584ace9 100644 --- a/bemani/data/mysql/network.py +++ b/bemani/data/mysql/network.py @@ -320,6 +320,14 @@ class NetworkData(BaseData): for result in cursor.mappings() ] + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all network-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "UPDATE audit SET userid = NULL WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + def delete_events(self, oldest_event_ts: int) -> None: """ Given a timestamp of the oldset event we should keep around, delete diff --git a/bemani/data/mysql/user.py b/bemani/data/mysql/user.py index b4af3fc..26c7345 100644 --- a/bemani/data/mysql/user.py +++ b/bemani/data/mysql/user.py @@ -1503,3 +1503,39 @@ class UserData(BaseData): # Finally, return the user ID return UserID(userid) + + def delete_user(self, userid: UserID) -> None: + """ + Given a user ID, make sure all user-related things are unlinked from + the user or deleted from the DB, whichever relevant. + """ + sql = "SELECT COUNT(refid) AS refcount FROM refid WHERE userid = :userid" + cursor = self.execute(sql, {"userid": userid}) + + # First nuke all profiles, and things related to a player's refid (profile ID basically). + if cursor.rowcount == 1 and cursor.mappings().fetchone()["refcount"] > 0: + sql = "DELETE FROM time_based_achievement WHERE refid IN (SELECT refid FROM refid WHERE userid = :userid)" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM achievement WHERE refid IN (SELECT refid FROM refid WHERE userid = :userid)" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM profile WHERE refid IN (SELECT refid FROM refid WHERE userid = :userid)" + self.execute(sql, {"userid": userid}) + + # Now nuke any active sessions for the user. + self._destroy_sessions(userid, "userid") + + # Finally, nuke anything we own that points at the user. + sql = "DELETE FROM link WHERE userid = :userid OR other_userid = :userid" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM balance WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM extid WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM refid WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + sql = "DELETE FROM card WHERE userid = :userid" + self.execute(sql, {"userid": userid}) + + # And finally, nuke the user themselves. + sql = "DELETE FROM user WHERE id = :userid" + self.execute(sql, {"userid": userid}) diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index 6bd0255..d71d5f5 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -427,6 +427,8 @@ def viewuser(userid: int) -> Response: "updateemail": url_for("admin_pages.updateemail", userid=userid), "updatepin": url_for("admin_pages.updatepin", userid=userid), "updatepassword": url_for("admin_pages.updatepassword", userid=userid), + "removeuser": url_for("admin_pages.removeuser", userid=userid), + "viewusers": url_for("admin_pages.viewusers"), }, ) @@ -467,6 +469,31 @@ def listuser(userid: int) -> Dict[str, Any]: } +@admin_pages.route("/users//remove", methods=["POST"]) +@jsonify +@adminrequired +def removeuser(userid: int) -> Dict[str, Any]: + # Cast the userID. + userid = UserID(userid) + user = g.data.local.user.get_user(userid) + + # We only try to run through deleting if the user exists, to avoid + # somebody accidentally putting a bad user ID in here. + if not user: + return {"success": False} + + # Now, go through and delete them from everything that matters. + g.data.local.network.delete_user(userid) + g.data.local.machine.delete_user(userid) + g.data.local.lobby.delete_user(userid) + g.data.local.music.delete_user(userid) + g.data.local.game.delete_user(userid) + g.data.local.user.delete_user(userid) + + # And return nothing, since this can be called multiple places. + return {"success": True} + + @admin_pages.route("/arcades/list") @jsonify @adminrequired diff --git a/bemani/frontend/static/controllers/admin/user.react.js b/bemani/frontend/static/controllers/admin/user.react.js index 75e1c45..0143638 100644 --- a/bemani/frontend/static/controllers/admin/user.react.js +++ b/bemani/frontend/static/controllers/admin/user.react.js @@ -471,6 +471,39 @@ var user_management = createReactClass({ event.preventDefault(); }, + deleteUser: function(event) { + $.confirm({ + escapeKey: 'Cancel', + animation: 'none', + closeAnimation: 'none', + title: 'Delete User', + content: ( + 'Are you sure you want to delete this user? All of their game profiles, scores, ' + + 'PASELI balances and settings will be deleted along with the account itself.' + ), + buttons: { + Delete: { + btnClass: 'delete', + action: function() { + AJAX.post( + Link.get('removeuser'), + {}, + function(response) { + // If it succeeded, redirect back to users. + if (response.success) { + window.location = Link.get('viewusers'); + } + }.bind(this) + ); + }.bind(this), + }, + Cancel: function() { + }, + } + }); + event.preventDefault(); + }, + renderDeleteProfileButton: function(profile) { return ( <> @@ -494,6 +527,9 @@ var user_management = createReactClass({ {this.renderPIN()} {this.renderLastPlayed()} +
+ +

Cards

{this.state.cards.map(function(card) {