Implement delete user button on admin user profile page.

This commit is contained in:
Jennifer Taylor
2026-08-30 20:24:03 +00:00
parent 947252ccc0
commit 40a4935ae0
8 changed files with 148 additions and 0 deletions

View File

@@ -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})

View File

@@ -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})

View File

@@ -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.

View File

@@ -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})

View File

@@ -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

View File

@@ -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})

View File

@@ -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/<int:userid>/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

View File

@@ -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()}
</div>
<div className="section">
<Delete title="delete user" onClick={this.deleteUser.bind(this)} />
</div>
<div className="section">
<h3>Cards</h3>
{this.state.cards.map(function(card) {