From 35b939bc6456a3ed26e0401b2a9049e6e600e1da Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sat, 18 Jul 2026 21:05:23 +0000 Subject: [PATCH] Implement admin tool to list profiles on admin user page, including ability to delete a profile. --- bemani/common/constants.py | 17 +++- bemani/data/mysql/user.py | 38 +++++++++ bemani/frontend/admin/admin.py | 67 ++++++++++++--- bemani/frontend/arcade/arcade.py | 2 +- bemani/frontend/iidx/endpoints.py | 2 +- .../static/controllers/admin/user.react.js | 85 +++++++++++++++++++ 6 files changed, 195 insertions(+), 16 deletions(-) diff --git a/bemani/common/constants.py b/bemani/common/constants.py index a60e6b4..025b190 100644 --- a/bemani/common/constants.py +++ b/bemani/common/constants.py @@ -21,6 +21,21 @@ class GameConstants(Enum): REFLEC_BEAT = "reflec" SDVX = "sdvx" + @classmethod + def LUT(cls) -> Dict[str, str]: + return { + cls.BISHI_BASHI.value: "BishiBashi", + cls.DANCE_EVOLUTION.value: "Dance Evolution", + cls.DDR.value: "DDR", + cls.IIDX.value: "IIDX", + cls.JUBEAT.value: "Jubeat", + cls.MGA.value: "Metal Gear Arcade", + cls.MUSECA.value: "MÚSECA", + cls.POPN_MUSIC.value: "Pop'n Music", + cls.REFLEC_BEAT.value: "Reflec Beat", + cls.SDVX.value: "SDVX", + } + class VersionConstants: """ @@ -476,7 +491,7 @@ class _RegionConstants: raise Exception(f"Unexpected value {region} for game region!") - @property + @classmethod def LUT(cls) -> Dict[int, str]: return { cls.HOKKAIDO: "北海道 (Hokkaido)", diff --git a/bemani/data/mysql/user.py b/bemani/data/mysql/user.py index 45322ea..1bb1186 100644 --- a/bemani/data/mysql/user.py +++ b/bemani/data/mysql/user.py @@ -529,6 +529,38 @@ class UserData(BaseData): self.deserialize(result["data"]), ) + def get_profiles(self, userid: UserID) -> List[Profile]: + """ + Given a user ID, look up all associated profiles across all games and versions. If the user + does not have any profiles then returns an empty list. + + Parameters: + userid - Integer user ID, as looked up by one of the above functions. + + Returns: + A list of valid profiles that were pulled from the DB. + """ + sql = """ + SELECT refid.refid AS refid, extid.extid AS extid, refid.game AS game, refid.version AS version, profile.data AS data + FROM refid, extid, profile + WHERE + refid.userid = :userid AND + extid.userid = refid.userid AND + extid.game = refid.game AND + profile.refid = refid.refid + """ + cursor = self.execute(sql, {"userid": userid}) + return [ + Profile( + GameConstants(result["game"]), + result["version"], + result["refid"], + result["extid"], + self.deserialize(result["data"]), + ) + for result in cursor.mappings() + ] + def get_any_profile(self, game: GameConstants, version: int, userid: UserID) -> Optional[Profile]: """ Given a game/version/userid, look up the associated profile. If the profile for that version @@ -780,6 +812,12 @@ class UserData(BaseData): sql = "DELETE FROM profile WHERE refid = :refid LIMIT 1" self.execute(sql, {"refid": refid}) + # Now delete any associated data items that went with that profile. + sql = "DELETE FROM achievement WHERE refid = :refid" + self.execute(sql, {"refid": refid}) + sql = "DELETE FROM time_based_achievement WHERE refid = :refid" + self.execute(sql, {"refid": refid}) + def get_achievement( self, game: GameConstants, diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index c3486e4..415faa5 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -9,6 +9,8 @@ from bemani.common import ( GameConstants, RegionConstants, ValidatedDict, + Profile, + ID, ) from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException @@ -98,6 +100,16 @@ def format_user(user: User) -> Dict[str, Any]: } +def format_profile(profile: Profile) -> Dict[str, object]: + return { + "game": profile.game.value, + "version": profile.version, + "extid": ID.format_extid(profile.extid), + "refid": profile.refid, + "name": profile.get_str("name", ""), + } + + def format_news(news: News) -> Dict[str, Any]: return { "id": news.id, @@ -145,7 +157,7 @@ def viewsettings() -> Response: **{ "title": "Network Settings", "config": g.config, - "region": RegionConstants.LUT, + "region": RegionConstants.LUT(), }, ) ) @@ -240,7 +252,7 @@ def viewarcades() -> Response: "admin/arcades.react.js", { "arcades": [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()], - "regions": RegionConstants.LUT, + "regions": RegionConstants.LUT(), "usernames": g.data.local.user.get_all_usernames(), "paseli_enabled": g.config.paseli.enabled, "paseli_infinite": g.config.paseli.infinite, @@ -271,17 +283,7 @@ def viewmachines() -> Response: { "machines": [format_machine(machine) for machine in g.data.local.machine.get_all_machines()], "arcades": {arcade.id: arcade.name for arcade in g.data.local.machine.get_all_arcades()}, - "series": { - GameConstants.BISHI_BASHI.value: "BishiBashi", - GameConstants.DDR.value: "DDR", - GameConstants.IIDX.value: "IIDX", - GameConstants.JUBEAT.value: "Jubeat", - GameConstants.MGA.value: "Metal Gear Arcade", - GameConstants.MUSECA.value: "MÚSECA", - GameConstants.POPN_MUSIC.value: "Pop'n Music", - GameConstants.REFLEC_BEAT.value: "Reflec Beat", - GameConstants.SDVX.value: "SDVX", - }, + "series": GameConstants.LUT(), "games": games, "enforcing": g.config.server.enforce_pcbid, }, @@ -374,7 +376,15 @@ def viewuser(userid: int) -> Response: except CardCipherException: return "????????????????" + # Get list of all game names and versions. + games: Dict[str, Dict[int, str]] = {} + for game, version, name in Base.all_games(): + if game.value not in games: + games[game.value] = {} + games[game.value][version] = name + cards = [__format_card(card) for card in g.data.local.user.get_cards(userid)] + profiles = [format_profile(profile) for profile in g.data.local.user.get_profiles(userid)] arcades = g.data.local.machine.get_all_arcades() return render_react( "User", @@ -384,7 +394,9 @@ def viewuser(userid: int) -> Response: "email": user.email, "username": user.username, }, + "games": games, "cards": cards, + "profiles": profiles, "arcades": {arcade.id: arcade.name for arcade in arcades}, "balances": {arcade.id: g.data.local.user.get_balance(userid, arcade.id) for arcade in arcades}, "events": [ @@ -395,6 +407,7 @@ def viewuser(userid: int) -> Response: { "refresh": url_for("admin_pages.listuser", userid=userid), "removeusercard": url_for("admin_pages.removeusercard", userid=userid), + "removeuserprofile": url_for("admin_pages.removeuserprofile", userid=userid), "addusercard": url_for("admin_pages.addusercard", userid=userid), "updatebalance": url_for("admin_pages.updatebalance", userid=userid), "updateusername": url_for("admin_pages.updateusername", userid=userid), @@ -1078,6 +1091,34 @@ def removeusercard(userid: int) -> Dict[str, Any]: } +@admin_pages.route("/users//profiles/remove", methods=["POST"]) +@jsonify +@adminrequired +def removeuserprofile(userid: int) -> Dict[str, Any]: + # Cast the userID. + userid = UserID(userid) + + # Grab refid, see if it exists. + refid = request.get_json()["refid"] + user = g.data.local.user.get_user(userid) + + # Make sure the user ID is valid + if user is None: + raise Exception("Cannot find user to update!") + + # Make sure the profile is valid. + profiles = [p for p in g.data.local.user.get_profiles(userid) if p.refid == refid] + if len(profiles) != 1: + raise Exception("Cannot find profile to delete!") + profile = profiles[0] + + # Remove it from the user's account + g.data.local.user.delete_profile(profile.game, profile.version, userid) + + # Return new profile list + return {"profiles": [format_profile(profile) for profile in g.data.local.user.get_profiles(userid)]} + + @admin_pages.route("/users//cards/add", methods=["POST"]) @jsonify @adminrequired diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index 9e1811d..a7249a2 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -105,7 +105,7 @@ def viewarcade(arcadeid: int) -> Response: "arcade/arcade.react.js", { "arcade": format_arcade(arcade), - "regions": RegionConstants.LUT, + "regions": RegionConstants.LUT(), "machines": machines, "game_settings": get_game_settings(g.data, arcadeid), "balances": {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)}, diff --git a/bemani/frontend/iidx/endpoints.py b/bemani/frontend/iidx/endpoints.py index 6fbe402..cc98b55 100644 --- a/bemani/frontend/iidx/endpoints.py +++ b/bemani/frontend/iidx/endpoints.py @@ -324,7 +324,7 @@ def viewsettings() -> Response: "iidx/settings.react.js", { "player": djinfo, - "regions": RegionConstants.LUT, + "regions": RegionConstants.LUT(), "versions": {version: name for (game, version, name) in frontend.all_games()}, "qpros": frontend.get_all_items(versions), }, diff --git a/bemani/frontend/static/controllers/admin/user.react.js b/bemani/frontend/static/controllers/admin/user.react.js index 504e299..296e9c4 100644 --- a/bemani/frontend/static/controllers/admin/user.react.js +++ b/bemani/frontend/static/controllers/admin/user.react.js @@ -19,6 +19,7 @@ var user_management = createReactClass({ new_password1: '', new_password2: '', cards: window.cards, + profiles: window.profiles, new_card: '', balances: window.balances, credits: credits, @@ -392,6 +393,49 @@ var user_management = createReactClass({ ); }, + deleteExistingProfile: function(event, refid) { + $.confirm({ + escapeKey: 'Cancel', + animation: 'none', + closeAnimation: 'none', + title: 'Delete Profile', + content: 'Are you sure you want to delete this profile?', + buttons: { + Delete: { + btnClass: 'delete', + action: function() { + AJAX.post( + Link.get('removeuserprofile'), + {refid: refid}, + function(response) { + // Kill the entry we no longer need + var profiles = this.state.profiles; + this.setState({ + profiles: profiles.filter((prof) => prof.refid != refid), + }); + }.bind(this) + ); + }.bind(this), + }, + Cancel: function() { + }, + } + }); + event.preventDefault(); + }, + + renderDeleteProfileButton: function(profile) { + return ( + <> + + + ); + }, + render: function() { return (
@@ -430,6 +474,47 @@ var user_management = createReactClass({
+
+

Profiles

+ { this.state.profiles.length == 0 ? +
+ No profiles present! +
: + + } +

PASELI Balance

{ Object.keys(this.state.arcades).length == 0 ?