From aa48c90e45f61725d3ca344cbb3349a80e0f87ba Mon Sep 17 00:00:00 2001
From: James Mortemore
Date: Tue, 16 Dec 2025 16:10:22 +0000
Subject: [PATCH 1/2] feat: make it easier to run locally
---
.env.example | 36 +++
.naverc | 1 +
README.md | 112 ++++++++-
docker-compose.yml | 25 +++
package-lock.json | 172 +++++++++++++-
package.json | 6 +
scripts/seed.js | 299 +++++++++++++++++++++++++
server/test/fixtures/appeal-comment.js | 2 +-
8 files changed, 639 insertions(+), 14 deletions(-)
create mode 100644 .env.example
create mode 100644 .naverc
create mode 100644 docker-compose.yml
create mode 100644 scripts/seed.js
diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..771099d21
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,36 @@
+# BanManager WebUI - Environment Configuration
+# Copy this file to .env and update values as needed
+
+# Server display name (shown in footer)
+SERVER_FOOTER_NAME=BanManagement
+
+# Contact email for push notification registration
+CONTACT_EMAIL=youremail@example.com
+
+# Database connection (defaults match docker-compose.yml)
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=password
+DB_NAME=bm_local_dev
+DB_CONNECTION_LIMIT=5
+
+# Security keys (generate unique values for production)
+# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
+ENCRYPTION_KEY=b097b390a68441cc3bb151dd0171f25c3aabc688c50eeb26dc5e13254b333911
+SESSION_KEY=a73545a5f08d2906e39a4438014200303f9269f3ade9227525ffb141294f1b62
+
+# Push notification VAPID keys (generate with: npx web-push generate-vapid-keys)
+NOTIFICATION_VAPID_PUBLIC_KEY=
+NOTIFICATION_VAPID_PRIVATE_KEY=
+
+# Admin user credentials (used by seed script and Cypress tests)
+ADMIN_USERNAME=admin@banmanagement.com
+ADMIN_PASSWORD=testing
+
+# Server configuration
+PORT=3000
+LOG_LEVEL=info
+
+# Set to 'production' for production builds
+NODE_ENV=development
diff --git a/.naverc b/.naverc
new file mode 100644
index 000000000..2bd5a0a98
--- /dev/null
+++ b/.naverc
@@ -0,0 +1 @@
+22
diff --git a/README.md b/README.md
index 1dc652816..6b2e005f1 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@
## Overview
+
- **Always connected.** Manage punishments from anywhere with seamless logins
- **Cross platform.** It doesn't matter what OS you use, it just works wherever Node.js runs
- **Responsive interface.** Manage your community from any device at any time
@@ -44,53 +45,152 @@
To learn more about configuration, usage and features of BanManager, take a look at [the website](https://banmanagement.com/) or view [the demo](https://demo.banmanagement.com).
## Features
+
- Appeal punishments
- Ban, unban, mute, and warn players
- Review and manage reports on the go
- Custom roles and flexible permissions
- A single interface for multiple Minecraft servers
-## Requirements
-- The latest [Node.js](https://nodejs.org/) LTS version (even numbered)
+## Installation (Production)
+
+For deploying BanManager WebUI on your own server, see the **[full installation guide](https://banmanagement.com/docs/webui/install)**.
+
+### Requirements
+
+- [Node.js](https://nodejs.org/) LTS (v20 or v22)
- MySQL v5+ or MariaDB v10+
- Minecraft server with [BanManager](https://github.com/BanManagement/BanManager) & [BanManager-WebEnhancer](https://ci.frostcast.net/job/BanManager-WebEnhancer/) plugins configured to [use MySQL or MariaDB](https://banmanagement.com/docs/banmanager/install#setup-shared-database-optional)
-## Installation
-See [setup instructions](https://banmanagement.com/docs/webui/install)
+### Quick Install
-## Development
+```bash
+git clone https://github.com/BanManagement/BanManager-WebUI.git
+cd BanManager-WebUI
+npm ci --production
+npm run setup
```
+
+The setup wizard will guide you through configuring your database connection and creating an admin account.
+
+---
+
+## Development
+
+Want to contribute or run a local development environment? This section is for you.
+
+### Prerequisites
+
+- [Node.js](https://nodejs.org/) LTS (v20 or v22)
+- [Docker](https://www.docker.com/) (for local MySQL database)
+
+### Quick Start
+
+```bash
+# Clone the repository
git clone git@github.com:BanManagement/BanManager-WebUI.git
+cd BanManager-WebUI
+
+# Install dependencies
npm install
-npm run setup
+
+# Copy environment configuration
+cp .env.example .env
+
+# Start MySQL and seed the database (first time setup)
+npm run dev:setup
+
+# Start the development server
npm run dev
```
+The application will be available at http://localhost:3000
+
+### Test Accounts
+
+After seeding, the following accounts are available:
+
+| Role | Email | Password |
+| ----- | ----------------------- | -------- |
+| Guest | guest@banmanagement.com | testing |
+| User | user@banmanagement.com | testing |
+| Admin | admin@banmanagement.com | testing |
+
+### Available Scripts
+
+| Script | Description |
+| -------------------- | ------------------------------------------------- |
+| `npm run dev:setup` | Start MySQL container and seed the database |
+| `npm run dev` | Start development server with hot reloading |
+| `npm run db:start` | Start the MySQL Docker container |
+| `npm run db:stop` | Stop the MySQL Docker container |
+| `npm run seed` | Run migrations and seed data (fails if DB exists) |
+| `npm run seed:reset` | Drop existing database and re-seed |
+| `npm run build` | Build for production |
+| `npm run test` | Run linting and tests |
+| `npm run lint` | Run linting only |
+| `npm run cypress` | Open Cypress for E2E tests |
+
+### Environment Configuration
+
+Copy `.env.example` to `.env` and adjust as needed. Key variables:
+
+- `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` - Database connection
+- `ADMIN_USERNAME`, `ADMIN_PASSWORD` - Admin account credentials (also used by Cypress)
+- `ENCRYPTION_KEY`, `SESSION_KEY` - Security keys (generate unique values for production)
+
+### Resetting the Database
+
+To reset the database with fresh seed data:
+
+```bash
+npm run seed:reset
+```
+
+### Running Tests
+
+```bash
+# Run all tests
+npm run test
+
+# Run Cypress E2E tests
+npm run cypress
+```
+
## Contributing
+
If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcome.
## Help / Bug / Feature Request
+
If you have found a bug please [open an issue](https://github.com/BanManagement/BanManager-WebUI/issues/new) with as much detail as possible, including relevant logs and screenshots where applicable
Have an idea for a new feature? Feel free to [open an issue](https://github.com/BanManagement/BanManager-WebUI/issues/new) or [join us on Discord](https://discord.gg/59bsgZB) to chat
## License
+
Free to use under the [MIT](LICENSE)
## Screenshots
+
Click to view
### Home
+
[](welcome.png)
### Player
+
[](player.png)
### Dashboard
+
[](dashboard.png)
### Appeal
+
[](appeal.png)
### Report
+
[](report.png)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..1fad94776
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,25 @@
+services:
+ mysql:
+ image: mysql:8.0
+ container_name: banmanager-mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: password
+ MYSQL_DATABASE: bm_local_dev
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-ppassword"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ start_period: 30s
+ command:
+ - --character-set-server=utf8mb4
+ - --collation-server=utf8mb4_unicode_ci
+ - --default-authentication-plugin=mysql_native_password
+
+volumes:
+ mysql_data:
diff --git a/package-lock.json b/package-lock.json
index 4613c4d6b..4aafd48e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -109,6 +109,7 @@
"mockdate": "3.0.5",
"nixt": "0.5.1",
"nock": "^14.0.0-beta.6",
+ "nodemon": "3.1.11",
"standard": "16.0.4",
"standardx": "7.0.0",
"supertest": "7.1.0",
@@ -5024,9 +5025,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001704",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz",
- "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==",
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"funding": [
{
"type": "opencollective",
@@ -5040,7 +5041,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/caseless": {
"version": "0.12.0",
@@ -8888,6 +8890,12 @@
"node": ">= 4"
}
},
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
"node_modules/image-q": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
@@ -11609,6 +11617,55 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
+ "node_modules/nodemon": {
+ "version": "3.1.11",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
+ "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -12788,6 +12845,12 @@
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"dev": true
},
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -13868,6 +13931,18 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -15041,6 +15116,15 @@
"node": ">=6"
}
},
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@@ -15296,6 +15380,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
"node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
@@ -19686,9 +19776,9 @@
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
},
"caniuse-lite": {
- "version": "1.0.30001704",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz",
- "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew=="
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="
},
"caseless": {
"version": "0.12.0",
@@ -22535,6 +22625,12 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ=="
},
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
"image-q": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
@@ -24520,6 +24616,41 @@
}
}
},
+ "nodemon": {
+ "version": "3.1.11",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
+ "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -25373,6 +25504,12 @@
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
"dev": true
},
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -26135,6 +26272,15 @@
}
}
},
+ "simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "requires": {
+ "semver": "^7.5.3"
+ }
+ },
"sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -26976,6 +27122,12 @@
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
},
+ "touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true
+ },
"tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@@ -27173,6 +27325,12 @@
"which-boxed-primitive": "^1.1.1"
}
},
+ "undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
"undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
diff --git a/package.json b/package.json
index 50d27f937..80f375a4d 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"mockdate": "3.0.5",
"nixt": "0.5.1",
"nock": "^14.0.0-beta.6",
+ "nodemon": "3.1.11",
"standard": "16.0.4",
"standardx": "7.0.0",
"supertest": "7.1.0",
@@ -120,9 +121,14 @@
"scripts": {
"build": "next build",
"coveralls": "cat ./coverage/lcov.info | coveralls",
+ "db:start": "docker compose up -d",
+ "db:stop": "docker compose down",
"dev": "npx nodemon server.js | npx pino-pretty",
+ "dev:setup": "npm run db:start && node scripts/seed.js",
"heroku-postbuild": "npm run build && node bin/run.js update",
"lint": "standardx",
+ "seed": "node scripts/seed.js",
+ "seed:reset": "node scripts/seed.js --force",
"start": "node server.js",
"test": "npm run lint && jest --coverage -w 1 --no-cache",
"setup": "node bin/run.js setup --writeFile .env",
diff --git a/scripts/seed.js b/scripts/seed.js
new file mode 100644
index 000000000..93f90275e
--- /dev/null
+++ b/scripts/seed.js
@@ -0,0 +1,299 @@
+#!/usr/bin/env node
+
+require('dotenv').config()
+
+const path = require('path')
+const DBMigrate = require('db-migrate')
+const { parse } = require('uuid-parse')
+const { setupPool } = require('../server/connections')
+const {
+ createServer,
+ createPlayer,
+ createBan,
+ createBanRecord,
+ createMute,
+ createMuteRecord,
+ createKick,
+ createNote,
+ createReport,
+ createReportComment,
+ createWarning,
+ createAppeal
+} = require('../server/test/fixtures')
+const createAppealComment = require('../server/test/fixtures/appeal-comment')
+const { hash } = require('../server/data/hash')
+
+const DB_NAME = process.env.DB_NAME || 'bm_local_dev'
+const FORCE = process.argv.includes('--force')
+
+async function waitForMySQL (config, maxRetries = 30) {
+ let retries = 0
+ while (retries < maxRetries) {
+ try {
+ const pool = await setupPool(config)
+ await pool.raw('SELECT 1')
+ await pool.destroy()
+ return true
+ } catch (error) {
+ retries++
+ if (retries === maxRetries) {
+ throw new Error(`Could not connect to MySQL after ${maxRetries} attempts. Is the database running?`)
+ }
+ console.log(`Waiting for MySQL... (attempt ${retries}/${maxRetries})`)
+ await new Promise(resolve => setTimeout(resolve, 2000))
+ }
+ }
+}
+
+async function seed () {
+ const dbConfig = {
+ host: process.env.DB_HOST || '127.0.0.1',
+ port: process.env.DB_PORT || 3306,
+ user: process.env.DB_USER || 'root',
+ password: process.env.DB_PASSWORD || 'password',
+ multipleStatements: true
+ }
+
+ console.log('Connecting to MySQL...')
+ await waitForMySQL(dbConfig)
+
+ let dbPool = await setupPool(dbConfig)
+
+ // Check if database exists
+ const [databases] = await dbPool.raw(`SHOW DATABASES LIKE '${DB_NAME}'`)
+
+ if (databases.length > 0) {
+ if (!FORCE) {
+ await dbPool.destroy()
+ console.error(`\nError: Database '${DB_NAME}' already exists.`)
+ console.error('Use --force flag to drop and recreate the database.')
+ console.error(' npm run seed:reset')
+ process.exit(1)
+ }
+
+ console.log(`Dropping existing database '${DB_NAME}'...`)
+ await dbPool.raw(`DROP DATABASE ${DB_NAME}`)
+ }
+
+ console.log(`Creating database '${DB_NAME}'...`)
+ await dbPool.raw(`CREATE DATABASE ${DB_NAME}`)
+ await dbPool.destroy()
+
+ dbConfig.database = DB_NAME
+ dbPool = await setupPool(dbConfig)
+
+ // Run WebUI migrations
+ console.log('Running WebUI migrations...')
+ const dbMigrateConfig = {
+ connectionLimit: 1,
+ host: dbConfig.host,
+ port: dbConfig.port,
+ user: dbConfig.user,
+ password: dbConfig.password,
+ database: DB_NAME,
+ multipleStatements: true,
+ driver: { require: '@confuser/db-migrate-mysql' }
+ }
+ let dbmOpts = {
+ throwUncatched: true,
+ config: { dev: dbMigrateConfig },
+ cmdOptions: { 'migrations-dir': path.join(__dirname, '..', 'server', 'data', 'migrations') }
+ }
+ let dbm = DBMigrate.getInstance(true, dbmOpts)
+ dbm.silence(true)
+ await dbm.up()
+
+ // Run BanManager plugin migrations (test migrations)
+ console.log('Running BanManager plugin migrations...')
+ dbmOpts = {
+ throwUncatched: true,
+ config: { dev: dbMigrateConfig },
+ cmdOptions: { 'migrations-dir': path.join(__dirname, '..', 'server', 'test', 'migrations') }
+ }
+ dbm = DBMigrate.getInstance(true, dbmOpts)
+ dbm.silence(true)
+ await dbm.up()
+
+ console.log('Seeding data...')
+
+ // Create console player (system account)
+ const playerConsole = createPlayer({ name: 'Console' })
+
+ // Create user accounts
+ const guestUser = createPlayer({ name: 'GuestPlayer' })
+ const loggedInUser = createPlayer({ name: 'RegularUser' })
+ const adminUser = createPlayer({
+ id: parse('ae51c849-3f2a-4a37-986d-55ed5b02307f', Buffer.alloc(16)),
+ name: 'AdminUser'
+ })
+
+ // Create additional players for realistic data
+ const players = [
+ createPlayer({ name: 'Griefer123' }),
+ createPlayer({ name: 'HackerNoob' }),
+ createPlayer({ name: 'ToxicPlayer' }),
+ createPlayer({ name: 'SpamBot' }),
+ createPlayer({ name: 'CheatEngine' }),
+ createPlayer({ name: 'RuleBreaker' }),
+ createPlayer({ name: 'GoodPlayer' }),
+ createPlayer({ name: 'NewPlayer' }),
+ createPlayer({ name: 'VeteranUser' }),
+ createPlayer({ name: 'ModHelper' })
+ ]
+
+ await dbPool('bm_players').insert([playerConsole, guestUser, loggedInUser, adminUser, ...players])
+ console.log(' - Created 14 players')
+
+ // Assign roles
+ await dbPool('bm_web_player_roles').insert([
+ { player_id: guestUser.id, role_id: 1 },
+ { player_id: loggedInUser.id, role_id: 2 },
+ { player_id: adminUser.id, role_id: 3 }
+ ])
+ console.log(' - Assigned roles')
+
+ // Create user accounts
+ const adminEmail = process.env.ADMIN_USERNAME || 'admin@banmanagement.com'
+ const adminPassword = process.env.ADMIN_PASSWORD || 'testing'
+ const updated = Math.floor(Date.now() / 1000)
+
+ await dbPool('bm_web_users').insert([
+ { player_id: guestUser.id, email: 'guest@banmanagement.com', password: await hash('testing'), updated },
+ { player_id: loggedInUser.id, email: 'user@banmanagement.com', password: await hash('testing'), updated },
+ { player_id: adminUser.id, email: adminEmail, password: await hash(adminPassword), updated }
+ ])
+ console.log(' - Created user accounts')
+
+ // Create server
+ const server = await createServer(playerConsole.id, DB_NAME)
+ await dbPool('bm_web_servers').insert(server)
+ console.log(' - Created server connection')
+
+ // Create active bans
+ const activeBans = [
+ createBan(players[0], adminUser),
+ createBan(players[1], adminUser),
+ createBan(players[4], playerConsole)
+ ]
+ const insertedBans = await dbPool('bm_player_bans').insert(activeBans)
+ activeBans.forEach((ban, i) => { ban.id = insertedBans[0] + i })
+ console.log(' - Created 3 active bans')
+
+ // Create ban records (historical bans)
+ const banRecords = [
+ createBanRecord(players[2], adminUser),
+ createBanRecord(players[3], adminUser),
+ createBanRecord(players[5], playerConsole),
+ createBanRecord(players[6], adminUser),
+ createBanRecord(players[7], adminUser)
+ ]
+ await dbPool('bm_player_ban_records').insert(banRecords)
+ console.log(' - Created 5 ban records')
+
+ // Create active mutes
+ const activeMutes = [
+ createMute(players[2], adminUser),
+ createMute(players[3], playerConsole)
+ ]
+ const insertedMutes = await dbPool('bm_player_mutes').insert(activeMutes)
+ activeMutes.forEach((mute, i) => { mute.id = insertedMutes[0] + i })
+ console.log(' - Created 2 active mutes')
+
+ // Create mute records
+ const muteRecords = [
+ createMuteRecord(players[0], adminUser),
+ createMuteRecord(players[1], adminUser),
+ createMuteRecord(players[5], playerConsole)
+ ]
+ await dbPool('bm_player_mute_records').insert(muteRecords)
+ console.log(' - Created 3 mute records')
+
+ // Create kicks
+ const kicks = [
+ createKick(players[0], adminUser),
+ createKick(players[1], adminUser),
+ createKick(players[2], playerConsole),
+ createKick(players[3], adminUser),
+ createKick(players[7], adminUser)
+ ]
+ await dbPool('bm_player_kicks').insert(kicks)
+ console.log(' - Created 5 kicks')
+
+ // Create warnings
+ const warnings = [
+ createWarning(players[0], adminUser),
+ createWarning(players[1], adminUser),
+ createWarning(players[2], adminUser),
+ createWarning(players[5], playerConsole),
+ createWarning(players[6], adminUser),
+ createWarning(players[7], adminUser),
+ createWarning(players[8], adminUser)
+ ]
+ await dbPool('bm_player_warnings').insert(warnings)
+ console.log(' - Created 7 warnings')
+
+ // Create notes
+ const notes = [
+ createNote(players[0], adminUser),
+ createNote(players[1], adminUser),
+ createNote(players[5], playerConsole)
+ ]
+ await dbPool('bm_player_notes').insert(notes)
+ console.log(' - Created 3 notes')
+
+ // Create reports with different states
+ const reports = [
+ createReport(players[0], players[6], null, 1), // Open
+ createReport(players[1], players[7], adminUser, 2), // Assigned
+ createReport(players[2], players[8], null, 3), // Resolved
+ createReport(players[3], players[9], null, 4) // Closed
+ ]
+ const insertedReports = await dbPool('bm_player_reports').insert(reports)
+ const firstReportId = insertedReports[0]
+ console.log(' - Created 4 reports')
+
+ // Create report comments
+ const reportComments = [
+ createReportComment(firstReportId, adminUser),
+ createReportComment(firstReportId, players[6]),
+ createReportComment(firstReportId + 1, adminUser)
+ ]
+ await dbPool('bm_player_report_comments').insert(reportComments)
+ console.log(' - Created 3 report comments')
+
+ // Create appeals with different states
+ const appeals = [
+ createAppeal(activeBans[0], 'ban', server, players[0], null, 1), // Open
+ createAppeal(activeBans[1], 'ban', server, players[1], adminUser, 2), // Assigned
+ createAppeal(activeMutes[0], 'mute', server, players[2], null, 3), // Resolved
+ createAppeal(activeMutes[1], 'mute', server, players[3], null, 4) // Rejected
+ ]
+ const insertedAppeals = await dbPool('bm_web_appeals').insert(appeals)
+ const firstAppealId = insertedAppeals[0]
+ console.log(' - Created 4 appeals')
+
+ // Create appeal comments
+ const appealComments = [
+ createAppealComment(firstAppealId, players[0]),
+ createAppealComment(firstAppealId, adminUser),
+ createAppealComment(firstAppealId + 1, players[1]),
+ createAppealComment(firstAppealId + 1, adminUser)
+ ]
+ await dbPool('bm_web_appeal_comments').insert(appealComments.map(c => ({ ...c, type: 0 })))
+ console.log(' - Created 4 appeal comments')
+
+ await dbPool.destroy()
+
+ console.log('\n✓ Database seeded successfully!')
+ console.log(`\nDatabase: ${DB_NAME}`)
+ console.log('\nTest accounts:')
+ console.log(' Guest: guest@banmanagement.com / testing')
+ console.log(' User: user@banmanagement.com / testing')
+ console.log(` Admin: ${adminEmail} / ${adminPassword}`)
+ console.log('\nStart the development server with: npm run dev')
+}
+
+seed().catch(error => {
+ console.error('Seed failed:', error)
+ process.exit(1)
+})
diff --git a/server/test/fixtures/appeal-comment.js b/server/test/fixtures/appeal-comment.js
index e6c44128b..7a23de096 100644
--- a/server/test/fixtures/appeal-comment.js
+++ b/server/test/fixtures/appeal-comment.js
@@ -6,7 +6,7 @@ module.exports = function (appealId, actor) {
return {
appeal_id: appealId,
actor_id: actor.id ? actor.id : actor,
- comment: lorem.sentence(),
+ content: lorem.sentence(),
created,
updated: created
}
From ad710a14181bba44ed9bf57171b341159b2927b7 Mon Sep 17 00:00:00 2001
From: James Mortemore
Date: Tue, 16 Dec 2025 16:22:22 +0000
Subject: [PATCH 2/2] fix: seed type
---
scripts/seed.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/scripts/seed.js b/scripts/seed.js
index 93f90275e..9d3fe2d07 100644
--- a/scripts/seed.js
+++ b/scripts/seed.js
@@ -263,10 +263,10 @@ async function seed () {
// Create appeals with different states
const appeals = [
- createAppeal(activeBans[0], 'ban', server, players[0], null, 1), // Open
- createAppeal(activeBans[1], 'ban', server, players[1], adminUser, 2), // Assigned
- createAppeal(activeMutes[0], 'mute', server, players[2], null, 3), // Resolved
- createAppeal(activeMutes[1], 'mute', server, players[3], null, 4) // Rejected
+ createAppeal(activeBans[0], 'PlayerBan', server, players[0], null, 1), // Open
+ createAppeal(activeBans[1], 'PlayerBan', server, players[1], adminUser, 2), // Assigned
+ createAppeal(activeMutes[0], 'PlayerMute', server, players[2], null, 3), // Resolved
+ createAppeal(activeMutes[1], 'PlayerMute', server, players[3], null, 4) // Rejected
]
const insertedAppeals = await dbPool('bm_web_appeals').insert(appeals)
const firstAppealId = insertedAppeals[0]