From 20335aaebe26efc5b3a12f9bdce7ddfce5299269 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 12:47:10 -0400 Subject: [PATCH 01/15] add download report api --- core/allnet.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++-- index.py | 2 +- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 9ad5949..7946fdd 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -6,6 +6,8 @@ from datetime import datetime import pytz import base64 import zlib +import json +from enum import Enum from Crypto.PublicKey import RSA from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 @@ -18,6 +20,9 @@ from core.utils import Utils from core.data import Data from core.const import * +class DLIMG_TYPE(Enum): + app = 0 + opt = 1 class AllnetServlet: def __init__(self, core_cfg: CoreConfig, cfg_folder: str): @@ -241,6 +246,7 @@ class AllnetServlet: if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") + return open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" ).read() @@ -249,10 +255,31 @@ class AllnetServlet: return b"" def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: - self.logger.info( - f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}" - ) - return b"" + req_raw = request.content.getvalue() + try: + req_dict: Dict = json.loads(req_raw) + except Exception as e: + self.logger.warn(f"Failed to parse DL Report: {e}") + return "NG" + + dl_data_type = DLIMG_TYPE.app + dl_data = req_dict.get("appimage", {}) + + if dl_data is None or not dl_data: + dl_data_type = DLIMG_TYPE.opt + dl_data = req_dict.get("optimage", {}) + + if dl_data is None or not dl_data: + self.logger.warn(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage") + return "NG" + + dl_report_data = DLReport(dl_data, dl_data_type) + + if not dl_report_data.validate(): + self.logger.warn(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}") + return "NG" + + return "OK" def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: req_data = request.content.getvalue() @@ -529,3 +556,86 @@ class AllnetRequestException(Exception): def __init__(self, message="") -> None: self.message = message super().__init__(self.message) + +class DLReport: + def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None: + self.serial = data.get("serial") + self.dfl = data.get("dfl") + self.wfl = data.get("wfl") + self.tsc = data.get("tsc") + self.tdsc = data.get("tdsc") + self.at = data.get("at") + self.ot = data.get("ot") + self.rt = data.get("rt") + self.as_ = data.get("as") + self.rf_state = data.get("rf_state") + self.gd = data.get("gd") + self.dav = data.get("dav") + self.wdav = data.get("wdav") # app only + self.dov = data.get("dov") + self.wdov = data.get("wdov") # app only + self.__type = report_type + self.err = "" + + def validate(self) -> bool: + if self.serial is None: + self.err = "serial not provided" + return False + + if self.dfl is None: + self.err = "dfl not provided" + return False + + if self.wfl is None: + self.err = "wfl not provided" + return False + + if self.tsc is None: + self.err = "tsc not provided" + return False + + if self.tdsc is None: + self.err = "tdsc not provided" + return False + + if self.at is None: + self.err = "at not provided" + return False + + if self.ot is None: + self.err = "ot not provided" + return False + + if self.rt is None: + self.err = "rt not provided" + return False + + if self.as_ is None: + self.err = "as not provided" + return False + + if self.rf_state is None: + self.err = "rf_state not provided" + return False + + if self.gd is None: + self.err = "gd not provided" + return False + + if self.dav is None: + self.err = "dav not provided" + return False + + if self.dov is None: + self.err = "dov not provided" + return False + + if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app: + self.err = "wdav or wdov not provided in app image" + return False + + if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt: + self.err = "wdav or wdov provided in opt image" + return False + + return True diff --git a/index.py b/index.py index 2ff7c04..cb569a0 100644 --- a/index.py +++ b/index.py @@ -36,7 +36,7 @@ class HttpDispatcher(resource.Resource): self.map_post.connect( "allnet_downloadorder_report", - "/dl/report", + "/report-api/Report", controller="allnet", action="handle_dlorder_report", conditions=dict(method=["POST"]), From f417be671b9ead975148e6ca9f28305880f300cb Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 12:47:37 -0400 Subject: [PATCH 02/15] pokken: fix typo --- titles/pokken/const.py | 1 + titles/pokken/schema/item.py | 6 +++++- titles/pokken/schema/profile.py | 10 +++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/titles/pokken/const.py b/titles/pokken/const.py index e7ffdd8..9fa3b06 100644 --- a/titles/pokken/const.py +++ b/titles/pokken/const.py @@ -15,6 +15,7 @@ class PokkenConstants: AI = 2 LAN = 3 WAN = 4 + TUTORIAL_3 = 7 class BATTLE_RESULT(Enum): WIN = 1 diff --git a/titles/pokken/schema/item.py b/titles/pokken/schema/item.py index 32bff2a..6c13306 100644 --- a/titles/pokken/schema/item.py +++ b/titles/pokken/schema/item.py @@ -39,7 +39,11 @@ class PokkenItemData(BaseData): type=item_type, ) - result = self.execute(sql) + conflict = sql.on_duplicate_key_update( + content=content, + ) + + result = self.execute(conflict) if result is None: self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}") return None diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 812964d..1d745f6 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -259,7 +259,7 @@ class PokkenProfileData(BaseData): illustration_book_no=illust_no, bp_point_atk=atk, bp_point_res=res, - bp_point_defe=defe, + bp_point_def=defe, bp_point_sp=sp, ) @@ -267,7 +267,7 @@ class PokkenProfileData(BaseData): illustration_book_no=illust_no, bp_point_atk=atk, bp_point_res=res, - bp_point_defe=defe, + bp_point_def=defe, bp_point_sp=sp, ) @@ -347,7 +347,11 @@ class PokkenProfileData(BaseData): if result is None: self.logger.warn(f"Failed to update stats for user {user_id}") - def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None: + def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None: + if support1 == 4294967295: + support1 = None + if support2 == 4294967295: + support2 = None sql = update(profile).where(profile.c.user==user_id).values( support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1, support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2, From b94380790463055bdfe97f11e4681a6f493c1342 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 22:21:41 -0400 Subject: [PATCH 03/15] core: add columns to machine table, bump to v5 --- core/data/database.py | 2 +- core/data/schema/arcade.py | 44 ++++++++++++++++--- core/data/schema/user.py | 14 ++++++ core/data/schema/versions/CORE_4_rollback.sql | 3 ++ core/data/schema/versions/CORE_5_upgrade.sql | 3 ++ 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 core/data/schema/versions/CORE_4_rollback.sql create mode 100644 core/data/schema/versions/CORE_5_upgrade.sql diff --git a/core/data/database.py b/core/data/database.py index 9fb2606..51c170b 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -15,7 +15,7 @@ from core.utils import Utils class Data: - current_schema_version = 4 + current_schema_version = 5 engine = None session = None user = None diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index e1d9b1f..c45541b 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -1,9 +1,10 @@ -from typing import Optional, Dict -from sqlalchemy import Table, Column +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, and_, or_ from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint -from sqlalchemy.types import Integer, String, Boolean +from sqlalchemy.types import Integer, String, Boolean, JSON from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from sqlalchemy.engine import Row import re from core.data.schema.base import BaseData, metadata @@ -40,6 +41,9 @@ machine = Table( Column("timezone", String(255)), Column("ota_enable", Boolean), Column("is_cab", Boolean), + Column("memo", String(255)), + Column("is_cab", Boolean), + Column("data", JSON), mysql_charset="utf8mb4", ) @@ -65,7 +69,7 @@ arcade_owner = Table( class ArcadeData(BaseData): - def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: + def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]: if serial is not None: serial = serial.replace("-", "") if len(serial) == 11: @@ -130,12 +134,19 @@ class ArcadeData(BaseData): f"Failed to update board id for machine {machine_id} -> {boardid}" ) - def get_arcade(self, id: int) -> Optional[Dict]: + def get_arcade(self, id: int) -> Optional[Row]: sql = arcade.select(arcade.c.id == id) result = self.execute(sql) if result is None: return None return result.fetchone() + + def get_arcade_machines(self, id: int) -> Optional[List[Row]]: + sql = machine.select(machine.c.arcade == id) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def put_arcade( self, @@ -165,7 +176,21 @@ class ArcadeData(BaseData): return None return result.lastrowid - def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: + def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]: + sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() + + def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]: + sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id)) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def get_arcade_owners(self, arcade_id: int) -> Optional[Row]: sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) result = self.execute(sql) @@ -217,3 +242,10 @@ class ArcadeData(BaseData): return False return True + + def find_arcade_by_name(self, name: str) -> List[Row]: + sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 6a95005..221ba81 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -107,3 +107,17 @@ class UserData(BaseData): if result is None: return None return result.fetchall() + + def find_user_by_email(self, email: str) -> Row: + sql = select(aime_user).where(aime_user.c.email == email) + result = self.execute(sql) + if result is None: + return False + return result.fetchone() + + def find_user_by_username(self, username: str) -> List[Row]: + sql = aime_user.select(aime_user.c.username.like(f"%{username}%")) + result = self.execute(sql) + if result is None: + return False + return result.fetchall() diff --git a/core/data/schema/versions/CORE_4_rollback.sql b/core/data/schema/versions/CORE_4_rollback.sql new file mode 100644 index 0000000..4464915 --- /dev/null +++ b/core/data/schema/versions/CORE_4_rollback.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine DROP COLUMN memo; +ALTER TABLE machine DROP COLUMN is_blacklisted; +ALTER TABLE machine DROP COLUMN `data`; diff --git a/core/data/schema/versions/CORE_5_upgrade.sql b/core/data/schema/versions/CORE_5_upgrade.sql new file mode 100644 index 0000000..8e88b00 --- /dev/null +++ b/core/data/schema/versions/CORE_5_upgrade.sql @@ -0,0 +1,3 @@ +ALTER TABLE machine ADD memo varchar(255) NULL; +ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL; +ALTER TABLE machine ADD `data` longtext NULL; From 6c89a97fe34f28920996c3d13414d28db36dfcb5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 23 Jul 2023 22:21:49 -0400 Subject: [PATCH 04/15] frontend: add management pages --- core/frontend.py | 200 ++++++++++++++++++++++++- core/frontend/arcade/index.jinja | 4 + core/frontend/machine/index.jinja | 5 + core/frontend/sys/index.jinja | 98 ++++++++++++ core/frontend/user/index.jinja | 14 +- core/frontend/widgets/err_banner.jinja | 4 + core/frontend/widgets/topbar.jinja | 3 + 7 files changed, 321 insertions(+), 7 deletions(-) create mode 100644 core/frontend/arcade/index.jinja create mode 100644 core/frontend/machine/index.jinja create mode 100644 core/frontend/sys/index.jinja diff --git a/core/frontend.py b/core/frontend.py index f01be50..a79bd94 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -9,6 +9,9 @@ from zope.interface import Interface, Attribute, implementer from twisted.python.components import registerAdapter import jinja2 import bcrypt +import re +from enum import Enum +from urllib import parse from core import CoreConfig, Utils from core.data import Data @@ -19,6 +22,13 @@ class IUserSession(Interface): current_ip = Attribute("User's current ip address") permissions = Attribute("User's permission level") +class PermissionOffset(Enum): + USER = 0 # Regular user + USERMOD = 1 # Can moderate other users + ACMOD = 2 # Can add arcades and cabs + SYSADMIN = 3 # Can change settings + # 4 - 6 reserved for future use + OWNER = 7 # Can do anything @implementer(IUserSession) class UserSession(object): @@ -80,6 +90,9 @@ class FrontendServlet(resource.Resource): self.environment.globals["game_list"] = self.game_list self.putChild(b"gate", FE_Gate(cfg, self.environment)) self.putChild(b"user", FE_User(cfg, self.environment)) + self.putChild(b"sys", FE_System(cfg, self.environment)) + self.putChild(b"arcade", FE_Arcade(cfg, self.environment)) + self.putChild(b"cab", FE_Machine(cfg, self.environment)) self.putChild(b"game", fe_game) self.logger.info( @@ -154,6 +167,7 @@ class FE_Gate(FE_Base): passwd = None uid = self.data.card.get_user_id_from_card(access_code) + user = self.data.user.get_user(uid) if uid is None: return redirectTo(b"/gate?e=1", request) @@ -175,6 +189,7 @@ class FE_Gate(FE_Base): usr_sesh = IUserSession(sesh) usr_sesh.userId = uid usr_sesh.current_ip = ip + usr_sesh.permissions = user['permissions'] return redirectTo(b"/user", request) @@ -192,7 +207,7 @@ class FE_Gate(FE_Base): hashed = bcrypt.hashpw(passwd, salt) result = self.data.user.create_user( - uid, username, email, hashed.decode(), 1 + uid, username, email.lower(), hashed.decode(), 1 ) if result is None: return redirectTo(b"/gate?e=3", request) @@ -210,17 +225,29 @@ class FE_Gate(FE_Base): return redirectTo(b"/gate?e=2", request) ac = request.args[b"ac"][0].decode() + card = self.data.card.get_card_by_access_code(ac) + if card is None: + return redirectTo(b"/gate?e=1", request) + + user = self.data.user.get_user(card['user']) + if user is None: + self.logger.warn(f"Card {ac} exists with no/invalid associated user ID {card['user']}") + return redirectTo(b"/gate?e=0", request) + + if user['password'] is not None: + return redirectTo(b"/gate?e=1", request) template = self.environment.get_template("core/frontend/gate/create.jinja") return template.render( title=f"{self.core_config.server.name} | Create User", code=ac, - sesh={"userId": 0}, + sesh={"userId": 0, "permissions": 0}, ).encode("utf-16") class FE_User(FE_Base): def render_GET(self, request: Request): + uri = request.uri.decode() template = self.environment.get_template("core/frontend/user/index.jinja") sesh: Session = request.getSession() @@ -228,9 +255,26 @@ class FE_User(FE_Base): if usr_sesh.userId == 0: return redirectTo(b"/gate", request) - cards = self.data.card.get_user_cards(usr_sesh.userId) - user = self.data.user.get_user(usr_sesh.userId) + m = re.match("\/user\/(\d*)", uri) + + if m is not None: + usrid = m.group(1) + if usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value or not usrid == usr_sesh.userId: + return redirectTo(b"/user", request) + + else: + usrid = usr_sesh.userId + + user = self.data.user.get_user(usrid) + if user is None: + return redirectTo(b"/user", request) + + cards = self.data.card.get_user_cards(usrid) + arcades = self.data.arcade.get_arcades_managed_by_user(usrid) + card_data = [] + arcade_data = [] + for c in cards: if c['is_locked']: status = 'Locked' @@ -240,9 +284,104 @@ class FE_User(FE_Base): status = 'Active' card_data.append({'access_code': c['access_code'], 'status': status}) + + for a in arcades: + arcade_data.append({'id': a['id'], 'name': a['name']}) return template.render( - title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username'] + title=f"{self.core_config.server.name} | Account", + sesh=vars(usr_sesh), + cards=card_data, + username=user['username'], + arcades=arcade_data + ).encode("utf-16") + + def render_POST(self, request: Request): + pass + + +class FE_System(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/sys/index.jinja") + usrlist = [] + aclist = [] + cablist = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0 or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value: + return redirectTo(b"/gate", request) + + if uri.startswith("/sys/lookup.user?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.user?", "")) # lop off the first bit + uid_search = uri_parse.get("usrId") + email_search = uri_parse.get("usrEmail") + uname_search = uri_parse.get("usrName") + + if uid_search is not None: + u = self.data.user.get_user(uid_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif email_search is not None: + u = self.data.user.find_user_by_email(email_search[0]) + if u is not None: + usrlist.append(u._asdict()) + + elif uname_search is not None: + ul = self.data.user.find_user_by_username(uname_search[0]) + for u in ul: + usrlist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.arcade?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.arcade?", "")) # lop off the first bit + ac_id_search = uri_parse.get("arcadeId") + ac_name_search = uri_parse.get("arcadeName") + ac_user_search = uri_parse.get("arcadeUser") + + if ac_id_search is not None: + u = self.data.arcade.get_arcade(ac_id_search[0]) + if u is not None: + aclist.append(u._asdict()) + + elif ac_name_search is not None: + ul = self.data.arcade.find_arcade_by_name(ac_name_search[0]) + for u in ul: + aclist.append(u._asdict()) + + elif ac_user_search is not None: + ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0]) + for u in ul: + aclist.append(u._asdict()) + + elif uri.startswith("/sys/lookup.cab?"): + uri_parse = parse.parse_qs(uri.replace("/sys/lookup.cab?", "")) # lop off the first bit + cab_id_search = uri_parse.get("cabId") + cab_serial_search = uri_parse.get("cabSerial") + cab_acid_search = uri_parse.get("cabAcId") + + if cab_id_search is not None: + u = self.data.arcade.get_machine(id=cab_id_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_serial_search is not None: + u = self.data.arcade.get_machine(serial=cab_serial_search[0]) + if u is not None: + cablist.append(u._asdict()) + + elif cab_acid_search is not None: + ul = self.data.arcade.get_arcade_machines(cab_acid_search[0]) + for u in ul: + cablist.append(u._asdict()) + + return template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usrlist=usrlist, + aclist=aclist, + cablist=cablist, ).encode("utf-16") @@ -257,3 +396,54 @@ class FE_Game(FE_Base): def render_GET(self, request: Request) -> bytes: return redirectTo(b"/user", request) + + +class FE_Arcade(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/arcade/index.jinja") + managed = [] + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + m = re.match("\/arcade\/(\d*)", uri) + + if m is not None: + arcadeid = m.group(1) + perms = self.data.arcade.get_manager_permissions(usr_sesh.userId, arcadeid) + arcade = self.data.arcade.get_arcade(arcadeid) + + if perms is None: + perms = 0 + + else: + return redirectTo(b"/user", request) + + return template.render( + title=f"{self.core_config.server.name} | Arcade", + sesh=vars(usr_sesh), + error=0, + perms=perms, + arcade=arcade._asdict() + ).encode("utf-16") + + +class FE_Machine(FE_Base): + def render_GET(self, request: Request): + uri = request.uri.decode() + template = self.environment.get_template("core/frontend/machine/index.jinja") + + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + if usr_sesh.userId == 0: + return redirectTo(b"/gate", request) + + return template.render( + title=f"{self.core_config.server.name} | Machine", + sesh=vars(usr_sesh), + arcade={}, + error=0, + ).encode("utf-16") \ No newline at end of file diff --git a/core/frontend/arcade/index.jinja b/core/frontend/arcade/index.jinja new file mode 100644 index 0000000..20a1f46 --- /dev/null +++ b/core/frontend/arcade/index.jinja @@ -0,0 +1,4 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

{{ arcade.name }}

+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/machine/index.jinja b/core/frontend/machine/index.jinja new file mode 100644 index 0000000..01e90a0 --- /dev/null +++ b/core/frontend/machine/index.jinja @@ -0,0 +1,5 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +{% include "core/frontend/widgets/err_banner.jinja" %} +

Machine Management

+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/sys/index.jinja b/core/frontend/sys/index.jinja new file mode 100644 index 0000000..2da821e --- /dev/null +++ b/core/frontend/sys/index.jinja @@ -0,0 +1,98 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

System Management

+ +
+ {% if sesh.permissions >= 2 %} +
+
+

User Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} + {% if sesh.permissions >= 4 %} +
+
+

Arcade Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+
+
+

Machine Search

+
+ + +
+ OR +
+ + +
+ OR +
+ + +
+
+ +
+
+ {% endif %} +
+
+ {% if sesh.permissions >= 2 %} +
+ {% for usr in usrlist %} +
{{ usr.id }} | {{ usr.username }}
+ {% endfor %} +
+ {% endif %} + {% if sesh.permissions >= 4 %} +
+ {% for ac in aclist %} +
{{ ac.id }} | {{ ac.name }}
+ {% endfor %} +
+ {% endif %} +
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja index 2911e67..2f76b14 100644 --- a/core/frontend/user/index.jinja +++ b/core/frontend/user/index.jinja @@ -2,11 +2,21 @@ {% block content %}

Management for {{ username }}

Cards

-
    +
      {% for c in cards %} -
    • {{ c.access_code }}: {{ c.status }}
    • +
    • {{ c.access_code }}: {{ c.status }} {% if c.status == 'Active'%}{% elif c.status == 'Locked' %}{% endif %} 
    • {% endfor %}
    + +{% if arcades is defined %} +

    Arcades

    + +{% endif %} +