1
0
mirror of synced 2024-11-24 06:20:12 +01:00

Add support for self generated PCBIDs on arcade management page as well as a setting to control it.

This commit is contained in:
Jennifer Taylor 2021-08-29 01:47:45 +00:00
parent 94fec0dec1
commit 6ebc8de311
11 changed files with 475 additions and 55 deletions

View File

@ -75,6 +75,10 @@ class Server:
def enforce_pcbid(self) -> bool:
return bool(self.__config.get('server', {}).get('enforce_pcbid', False))
@property
def pcbid_self_grant_limit(self) -> int:
return int(self.__config.get('server', {}).get('pcbid_self_grant_limit', 0))
class Client:
def __init__(self, parent_config: "Config") -> None:

View File

@ -226,7 +226,7 @@ def viewarcades() -> Response:
)
@admin_pages.route('/machines')
@admin_pages.route('/pcbids')
@adminrequired
def viewmachines() -> Response:
games: Dict[str, Dict[int, str]] = {}
@ -255,6 +255,7 @@ def viewmachines() -> Response:
'enforcing': g.config.server.enforce_pcbid,
},
{
'refresh': url_for('admin_pages.listmachines'),
'generatepcbid': url_for('admin_pages.generatepcbid'),
'addpcbid': url_for('admin_pages.addpcbid'),
'updatepcbid': url_for('admin_pages.updatepcbid'),
@ -378,6 +379,16 @@ def listuser(userid: int) -> Dict[str, Any]:
}
@admin_pages.route('/arcades/list')
@jsonify
@adminrequired
def listmachines() -> Dict[str, Any]:
return {
'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()},
}
@admin_pages.route('/arcades/update', methods=['POST'])
@jsonify
@adminrequired
@ -619,7 +630,7 @@ def removeserver() -> Dict[str, Any]:
}
@admin_pages.route('/machines/generate', methods=['POST'])
@admin_pages.route('/pcbids/generate', methods=['POST'])
@jsonify
@adminrequired
def generatepcbid() -> Dict[str, Any]:
@ -647,7 +658,7 @@ def generatepcbid() -> Dict[str, Any]:
}
@admin_pages.route('/machines/add', methods=['POST'])
@admin_pages.route('/pcbids/add', methods=['POST'])
@jsonify
@adminrequired
def addpcbid() -> Dict[str, Any]:
@ -678,11 +689,11 @@ def addpcbid() -> Dict[str, Any]:
}
@admin_pages.route('/machines/update', methods=['POST'])
@admin_pages.route('/pcbids/update', methods=['POST'])
@jsonify
@adminrequired
def updatepcbid() -> Dict[str, Any]:
# Attempt to look this arcade up
# Attempt to look this machine up
machine = request.get_json()['machine']
if machine['arcade'] is not None:
arcade = g.data.local.machine.get_arcade(machine['arcade'])
@ -692,7 +703,7 @@ def updatepcbid() -> Dict[str, Any]:
# Make sure we don't duplicate port assignments
other_pcbid = g.data.local.machine.from_port(machine['port'])
if other_pcbid is not None and other_pcbid != machine['pcbid']:
raise Exception(f'This port is already in use by \'{other_pcbid}\'!')
raise Exception(f'The specified port is already in use by \'{other_pcbid}\'!')
if machine['port'] < 1 or machine['port'] > 65535:
raise Exception('The specified port is out of range!')
@ -711,14 +722,14 @@ def updatepcbid() -> Dict[str, Any]:
}
@admin_pages.route('/machines/remove', methods=['POST'])
@admin_pages.route('/pcbids/remove', methods=['POST'])
@jsonify
@adminrequired
def removepcbid() -> Dict[str, Any]:
# Attempt to look this arcade up
# Attempt to look this machine up
pcbid = request.get_json()['pcbid']
if g.data.local.machine.get_machine(pcbid) is None:
raise Exception('Unable to find machine to delete!')
raise Exception('Unable to find PCBID to delete!')
g.data.local.machine.destroy_machine(pcbid)

View File

@ -670,7 +670,7 @@ def navigation() -> Dict[str, Any]:
'uri': url_for('admin_pages.viewarcades'),
},
{
'label': 'Machines',
'label': 'PCBIDs',
'uri': url_for('admin_pages.viewmachines'),
},
{

View File

@ -1,4 +1,5 @@
from typing import Any, Dict, List
import random
from typing import Any, Dict, List, Optional
from flask import Blueprint, request, Response, abort, url_for
from bemani.backend.base import Base
@ -19,6 +20,10 @@ arcade_pages = Blueprint(
)
def is_user_editable(machine: Machine) -> bool:
return machine.game is None
def format_machine(machine: Machine) -> Dict[str, Any]:
if machine.game is None:
game = 'any game'
@ -50,6 +55,7 @@ def format_machine(machine: Machine) -> Dict[str, Any]:
'description': machine.description,
'port': machine.port,
'game': game,
'editable': is_user_editable(machine),
}
@ -154,10 +160,10 @@ def viewarcade(arcadeid: int) -> Response:
'users': {user.id: user.username for user in g.data.local.user.get_all_users()},
'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')],
'enforcing': g.config.server.enforce_pcbid,
'max_pcbids': g.config.server.pcbid_self_grant_limit,
},
{
'refresh': url_for('arcade_pages.listarcade', arcadeid=arcadeid),
'viewuser': url_for('admin_pages.viewuser', userid=-1),
'paseli_enabled': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='paseli_enabled'),
'paseli_infinite': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='paseli_infinite'),
'mask_services_url': url_for('arcade_pages.updatearcade', arcadeid=arcadeid, attribute='mask_services_url'),
@ -165,6 +171,9 @@ def viewarcade(arcadeid: int) -> Response:
'add_balance': url_for('arcade_pages.addbalance', arcadeid=arcadeid),
'update_balance': url_for('arcade_pages.updatebalance', arcadeid=arcadeid),
'update_pin': url_for('arcade_pages.updatepin', arcadeid=arcadeid),
'generatepcbid': url_for('arcade_pages.generatepcbid', arcadeid=arcadeid),
'updatepcbid': url_for('arcade_pages.updatepcbid', arcadeid=arcadeid),
'removepcbid': url_for('arcade_pages.removepcbid', arcadeid=arcadeid),
},
)
@ -296,6 +305,137 @@ def updatepin(arcadeid: int) -> Dict[str, Any]:
return {'pin': pin}
@arcade_pages.route('/<int:arcadeid>/pcbids/generate', methods=['POST'])
@jsonify
@loginrequired
def generatepcbid(arcadeid: int) -> Dict[str, Any]:
# Cast the ID for type safety.
arcadeid = ArcadeID(arcadeid)
# Make sure that arcade owners are allowed to generate PCBIDs in the first place.
if g.config.server.pcbid_self_grant_limit <= 0:
raise Exception('You don\'t have permission to generate PCBIDs!')
# Make sure the arcade is valid and the current user has permissions to
# modify it.
arcade = g.data.local.machine.get_arcade(arcadeid)
if arcade is None or g.userID not in arcade.owners:
raise Exception('You don\'t own this arcade, refusing to update!')
# Make sure the user hasn't gone over their limit of PCBIDs.
existing_machine_count = len(
[machine for machine in g.data.local.machine.get_all_machines(arcade.id) if is_user_editable(machine)]
)
if existing_machine_count >= g.config.server.pcbid_self_grant_limit:
raise Exception('You have hit your limit of allowed PCBIDs!')
# Will be set by the game on boot.
name: str = 'なし'
pcbid: Optional[str] = None
new_machine = request.get_json()['machine']
while pcbid is None:
# Generate a new PCBID, check for uniqueness
potential_pcbid = "01201000000000" + "".join([random.choice("0123456789ABCDEF") for _ in range(6)])
if g.data.local.machine.get_machine(potential_pcbid) is None:
pcbid = potential_pcbid
# Finally, add the generated PCBID to the network.
g.data.local.machine.create_machine(pcbid, name, new_machine['description'], arcade.id)
# Just return all machines for ease of updating
return {
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
}
@arcade_pages.route('/<int:arcadeid>/pcbids/update', methods=['POST'])
@jsonify
@loginrequired
def updatepcbid(arcadeid: int) -> Dict[str, Any]:
# Cast the ID for type safety.
arcadeid = ArcadeID(arcadeid)
# Make sure that arcade owners are allowed to edit PCBIDs in the first place.
if g.config.server.pcbid_self_grant_limit <= 0:
raise Exception('You don\'t have permission to edit PCBIDs!')
# Make sure the arcade is valid and the current user has permissions to
# modify it.
arcade = g.data.local.machine.get_arcade(arcadeid)
if arcade is None or g.userID not in arcade.owners:
raise Exception('You don\'t own this arcade, refusing to update!')
# Grab the new updates as well as the old values to validate editing permissions.
updated_machine = request.get_json()['machine']
current_machine = g.data.local.machine.get_machine(updated_machine['pcbid'])
# Make sure the PCBID we are trying to modify is actually owned by this arcade.
# Also, make sure that the PCBID is actually user-editable.
if current_machine is None or current_machine.arcade != arcadeid or not is_user_editable(current_machine):
raise Exception('You don\'t own this PCBID, refusing to update!')
# Make sure the port is actually valid.
try:
port = int(updated_machine['port'])
except ValueError:
port = None
if port is None:
raise Exception('The specified port is invalid!')
if port < 1 or port > 65535:
raise Exception('The specified port is out of range!')
# Make sure we don't duplicate port assignments.
other_pcbid = g.data.local.machine.from_port(port)
if other_pcbid is not None and other_pcbid != updated_machine['pcbid']:
raise Exception('The specified port is already in use!')
# Update the allowed bits of data.
current_machine.description = updated_machine['description']
current_machine.port = port
g.data.local.machine.put_machine(current_machine)
# Just return all machines for ease of updating
return {
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
}
@arcade_pages.route('/<int:arcadeid>/pcbids/remove', methods=['POST'])
@jsonify
@loginrequired
def removepcbid(arcadeid: int) -> Dict[str, Any]:
# Cast the ID for type safety.
arcadeid = ArcadeID(arcadeid)
# Make sure that arcade owners are allowed to edit PCBIDs in the first place.
if g.config.server.pcbid_self_grant_limit <= 0:
raise Exception('You don\'t have permission to edit PCBIDs!')
# Make sure the arcade is valid and the current user has permissions to
# modify it.
arcade = g.data.local.machine.get_arcade(arcadeid)
if arcade is None or g.userID not in arcade.owners:
raise Exception('You don\'t own this arcade, refusing to update!')
# Attempt to look the PCBID we are deleting up to ensure it exists.
pcbid = request.get_json()['pcbid']
# Make sure the PCBID we are trying to delete is actually owned by this arcade.
# Also, make sure that the PCBID is actually user-editable.
machine = g.data.local.machine.get_machine(pcbid)
if machine is None or machine.arcade != arcadeid or not is_user_editable(machine):
raise Exception('You don\'t own this PCBID, refusing to update!')
# Actually delete it.
g.data.local.machine.destroy_machine(pcbid)
# Just return all machines for ease of updating
return {
'machines': [format_machine(machine) for machine in g.data.local.machine.get_all_machines(arcade.id)],
}
@arcade_pages.route('/<int:arcadeid>/update/<string:attribute>', methods=['POST'])
@jsonify
@loginrequired

View File

@ -510,7 +510,7 @@ var api_management = React.createClass({
<td>
<input
type="submit"
value="save"
value="add client"
/>
</td>
</tr>
@ -593,7 +593,7 @@ var api_management = React.createClass({
<td>
<input
type="submit"
value="save"
value="add server"
/>
</td>
</tr>

View File

@ -457,7 +457,7 @@ var card_management = React.createClass({
<td>
<input
type="submit"
value="save"
value="add arcade"
/>
</td>
</tr>

View File

@ -20,6 +20,10 @@ var machine_management = React.createClass({
};
},
componentDidMount: function() {
this.refreshMachines();
},
componentDidUpdate: function() {
if (this.focus_element && this.focus_element != this.already_focused) {
this.focus_element.focus();
@ -27,6 +31,20 @@ var machine_management = React.createClass({
}
},
refreshMachines: function() {
AJAX.get(
Link.get('refresh'),
function(response) {
this.setState({
machines: response.machines,
arcade: response.arcades,
});
// Refresh every 5 seconds
setTimeout(this.refreshMachines, 5000);
}.bind(this)
);
},
generateNewMachine: function(event) {
AJAX.post(
Link.get('generatepcbid'),
@ -65,6 +83,11 @@ var machine_management = React.createClass({
},
saveMachine: function(event) {
machine = this.state.editing_machine;
if (machine.port == '') {
machine.port = 0;
}
AJAX.post(
Link.get('updatepcbid'),
{machine: this.state.editing_machine},
@ -83,8 +106,8 @@ var machine_management = React.createClass({
escapeKey: 'Cancel',
animation: 'none',
closeAnimation: 'none',
title: 'Delete Arcade',
content: 'Are you sure you want to delete this arcade from the network?',
title: 'Delete PCBID',
content: 'Are you sure you want to delete this PCBID from the network?',
buttons: {
Delete: {
btnClass: 'delete',
@ -348,7 +371,11 @@ var machine_management = React.createClass({
var machine = this.state.editing_machine;
var intRegex = /^\d*$/;
if (intRegex.test(event.target.value)) {
machine.port = parseInt(event.target.value);
if (event.target.value.length > 0) {
machine.port = parseInt(event.target.value);
} else {
machine.port = '';
}
this.setState({
editing_machine: machine,
});
@ -457,6 +484,7 @@ var machine_management = React.createClass({
{
name: '',
render: this.renderEditButton,
action: true,
},
]}
rows={this.state.machines}
@ -519,7 +547,7 @@ var machine_management = React.createClass({
<td>
<input
type="submit"
value="save"
value="add PCBID"
/>
</td>
</tr>
@ -569,7 +597,7 @@ var machine_management = React.createClass({
<td>
<input
type="submit"
value="save"
value="generate PCBID"
/>
</td>
</tr>

View File

@ -280,7 +280,7 @@ var news_management = React.createClass({
/>
<input
type="submit"
value="save"
value="post"
/>
</LabelledSection>
</form>

View File

@ -8,6 +8,14 @@ var valid_settings = window.game_settings.map(function(setting) {
});
var pagenav = new History(valid_settings);
function count_pcbids(machines) {
var count = 0;
machines.map(function(machine) {
count += (machine.editable ? 1 : 0);
});
return count;
}
var arcade_management = React.createClass({
getInitialState: function(props) {
var credits = {};
@ -27,8 +35,13 @@ var arcade_management = React.createClass({
paseli_enabled_saving: false,
paseli_infinite_saving: false,
mask_services_url_saving: false,
editing_machine: null,
machines: window.machines,
settings: window.game_settings,
pcbcount: count_pcbids(window.machines),
random_pcbid: {
description: '',
},
current_setting: pagenav.getInitialState(makeSettingName(window.game_settings[0])),
settings_changed: {},
settings_saving: {},
@ -66,9 +79,10 @@ var arcade_management = React.createClass({
users: response.users,
balances: response.balances,
machines: response.machines,
pcbcount: count_pcbids(response.machines),
events: response.events,
});
// Refresh every 15 seconds
// Refresh every 5 seconds
setTimeout(this.refreshArcade, 5000);
}.bind(this)
);
@ -282,6 +296,177 @@ var arcade_management = React.createClass({
);
},
generateNewMachine: function(event) {
AJAX.post(
Link.get('generatepcbid'),
{machine: this.state.random_pcbid},
function(response) {
this.setState({
machines: response.machines,
pcbcount: count_pcbids(response.machines),
random_pcbid: {
description: '',
},
});
}.bind(this)
);
event.preventDefault();
},
deleteExistingMachine: function(event, pcbid) {
$.confirm({
escapeKey: 'Cancel',
animation: 'none',
closeAnimation: 'none',
title: 'Delete PCBID',
content: 'Are you sure you want to delete this PCBID from the network?',
buttons: {
Delete: {
btnClass: 'delete',
action: function() {
AJAX.post(
Link.get('removepcbid'),
{pcbid: pcbid},
function(response) {
this.setState({
machines: response.machines,
pcbcount: count_pcbids(response.machines),
});
}.bind(this)
);
}.bind(this),
},
Cancel: function() {
},
}
});
event.preventDefault();
},
saveMachine: function(event) {
machine = this.state.editing_machine;
if (machine.port == '') {
machine.port = 0;
}
AJAX.post(
Link.get('updatepcbid'),
{machine: this.state.editing_machine},
function(response) {
this.setState({
machines: response.machines,
pcbcount: count_pcbids(response.machines),
editing_machine: null,
});
}.bind(this)
);
event.preventDefault();
},
renderDescription: function(machine) {
if (this.state.editing_machine && machine.pcbid == this.state.editing_machine.pcbid) {
return <input
name="description"
type="text"
autofocus="true"
ref={c => (this.focus_element = c)}
value={ this.state.editing_machine.description }
onChange={function(event) {
var machine = this.state.editing_machine;
machine.description = event.target.value;
this.setState({
editing_machine: machine,
});
}.bind(this)}
/>;
} else {
return (
<span>{ machine.description }</span>
);
}
},
renderPort: function(machine) {
if (this.state.editing_machine && machine.pcbid == this.state.editing_machine.pcbid) {
return <input
name="port"
type="text"
value={ this.state.editing_machine.port }
onChange={function(event) {
var machine = this.state.editing_machine;
var intRegex = /^\d*$/;
if (intRegex.test(event.target.value)) {
if (event.target.value.length > 0) {
machine.port = parseInt(event.target.value);
} else {
machine.port = '';
}
this.setState({
editing_machine: machine,
});
}
}.bind(this)}
/>;
} else {
return (
<span>{ machine.port }</span>
);
}
},
renderEditButton: function(machine) {
if (this.state.editing_machine) {
if (this.state.editing_machine.pcbid == machine.pcbid) {
return (
<span>
<input
type="submit"
value="save"
/>
<input
type="button"
value="cancel"
onClick={function(event) {
this.setState({
editing_machine: null,
});
}.bind(this)}
/>
</span>
);
} else {
return <span></span>;
}
} else {
if (window.max_pcbids > 0 && machine.editable) {
return (
<span>
<Edit
onClick={function(event) {
var editing_machine = null;
this.state.machines.map(function(a) {
if (a.pcbid == machine.pcbid) {
editing_machine = jQuery.extend(true, {}, a);
}
});
this.setState({
editing_machine: editing_machine,
});
}.bind(this)}
/>
<Delete
onClick={function(event) {
this.deleteExistingMachine(event, machine.pcbid);
}.bind(this)}
/>
</span>
);
} else {
return <span></span>;
}
}
},
render: function() {
return (
<div>
@ -320,40 +505,86 @@ var arcade_management = React.createClass({
</div>
<div className="section">
<h3>PCBIDs Assigned to This Arcade</h3>
<Table
className="list machine"
columns={[
{
name: "PCBID",
render: function(machine) { return machine.pcbid; },
sort: function(a, b) { return a.pcbid.localeCompare(b.pcbid); },
},
{
name: "Name",
render: function(machine) { return machine.name; },
sort: function(a, b) { return a.name.localeCompare(b.name); },
},
{
name: "Description",
render: function(machine) { return machine.description; },
sort: function(a, b) { return a.description.localeCompare(b.description); },
},
{
name: "Applicable Game",
render: function(machine) { return machine.game; },
sort: function(a, b) { return a.game.localeCompare(b.game); },
hidden: !window.enforcing,
},
{
name: "Port",
render: function(machine) { return machine.port; },
sort: function(a, b) { return a.port - b.port; },
},
]}
rows={this.state.machines}
emptymessage="There are no PCBIDs assigned to this arcade."
/>
<form className="inline" onSubmit={this.saveMachine}>
<Table
className="list machine"
columns={[
{
name: "PCBID",
render: function(machine) { return machine.pcbid; },
sort: function(a, b) { return a.pcbid.localeCompare(b.pcbid); },
},
{
name: "Name",
render: function(machine) { return machine.name; },
sort: function(a, b) { return a.name.localeCompare(b.name); },
},
{
name: "Description",
render: this.renderDescription,
sort: function(a, b) { return a.description.localeCompare(b.description); },
},
{
name: "Applicable Game",
render: function(machine) { return machine.game; },
sort: function(a, b) { return a.game.localeCompare(b.game); },
hidden: !window.enforcing,
},
{
name: "Port",
render: this.renderPort,
sort: function(a, b) { return a.port - b.port; },
},
{
name: '',
render: this.renderEditButton,
hidden: !window.enforcing || window.max_pcbids < 1,
action: true,
},
]}
rows={this.state.machines}
emptymessage="There are no PCBIDs assigned to this arcade."
/>
</form>
</div>
{ window.enforcing && this.state.pcbcount < window.max_pcbids ?
<div className="section">
<h3>Generate PCBID</h3>
<form className="inline" onSubmit={this.generateNewMachine}>
<table className="add machine">
<thead>
<tr>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input
name="description"
type="text"
value={ this.state.random_pcbid.description }
onChange={function(event) {
var pcbid = this.state.random_pcbid;
pcbid.description = event.target.value;
this.setState({random_pcbid: pcbid});
}.bind(this)}
/>
</td>
<td>
<input
type="submit"
value="generate PCBID"
/>
</td>
</tr>
</tbody>
</table>
</form>
</div>
: null
}
<div className="section settings-nav">
<h3>Game Settings For This Arcade</h3>
{ this.state.settings.map(function(game_settings) {

View File

@ -59,6 +59,8 @@
<dl>
<dt>PCBID Enforcement</dt>
<dd>{{ 'active' if config.server.enforce_pcbid else 'inactive' }}</dd>
<dt>Self-Generated PCBID Limit</dt>
<dd>{{ 'disabled' if (config.server.pcbid_self_grant_limit <= 0 or not config.server.enforce_pcbid) else config.server.pcbid_self_grant_limit }}</dd>
<dt>PASELI Enabled</dt>
<dd>{{ 'yes' if config.paseli.enabled else 'no' }} (can be overridden by arcade settings)</dd>
<dt>Infinite PASELI Enabled</dt>

View File

@ -29,6 +29,10 @@ server:
redirect: "https://eagate.573.jp"
# Whether PCBIDs must be added to the network before games will work.
enforce_pcbid: False
# How many PCBIDs an arcade owner can grant to themselves on the arcade
# page. Note that this setting is irrelevant if PCBID enforcing is off.
# Set to 0 or delete this setting to disable self-granting PCBIDs.
pcbid_self_grant_limit: 0
# Webhook URLs. These allow for game scores from games with scorecard support to be broadcasted to outside services.
# Delete this to disable this support.