Skip to content

Commit c0b433f

Browse files
authored
feat: web installer, setup mode, doctor command and Docker-first deploy (#1763)
* feat: web installer, setup mode, doctor command and Docker-first deploy Adds a first-run web installer at /setup so new operators can complete configuration from the browser without hand-editing .env, plus a companion `bmwebui doctor` command and Docker compose stacks for production. The CLI setup wizard gains BanManager config auto-detection and transactional admin creation, and the seed/update commands share the same migration helper. Highlights - Setup mode: server boots without keys/DB and serves only /setup, /health and the installer assets. Refuses to expose the main UI until configuration is complete. - Web installer: vanilla JS wizard (HTML/CSS/JS shipped from server/setup/static) walks through env vars, DB connection, schema migration, BanManager server registration and admin user creation. Atomic finalize wraps server + admin creation in a single Knex tx. - bmwebui doctor: validates env, DB connection, migration status, admin presence, and pings each configured BanManager server (with encrypted password decryption). - bmwebui setup: optional auto-detect from BanManager plugin folder (config.yml + console.yml), reuses WebUI database when desired, shared createAdminUser helper, transactional inserts. - Apache + Caddy reverse-proxy generators (`bmwebui setup apache` / `bmwebui setup caddy`) with validated domain/subdirectory inputs. - Docker: multi-arch image build, docker-entrypoint.js auto-generates missing keys, docker-compose.prod.yml + docker-compose.prod-no-db.yml, /health endpoint reports setup_required vs ok for orchestration. - /health endpoint reuses the existing dbPool to avoid opening a new connection per request. Security hardening - Timing-safe comparison for SETUP_TOKEN. - Same-origin (Origin/Referer) check on /api/setup/* writes. - Strict regex validation for domain/subdirectory passed to shell. - parseBanManagerConfig restricted to config.yml/console.yml only. - BASE_PATH validated and HTML/JSON-escaped before injection. - MySQL healthcheck password moved out of CLI args via MYSQL_PWD. - docker-compose.prod.yml requires explicit DB credentials (no silent defaults). Tests - 8 new server-side suites covering setup state, finalize atomicity, env validation, key generation, parse-config, mode boot, basepath handling, and admin creation. - New test/lib/setup-fresh.js harness applies WebUI + BanManager migrations against an isolated DB so installer code can run in realistic conditions without mocks. - doctor positive/negative paths covered. * test(e2e): expand Cypress coverage with deep journeys, setup wizard run + CI (#1764) * chore(cypress): expand e2e seed and add login/gql helpers Expands cypress/setup.js with the data shape the new journey specs need without breaking the existing documents.spec.js fixture contract: - Extra seeded players including an unbanned target for moderation flows and a banned target with active ban/mute/warning for appeal flows. - A second BanManager server so AssignPlayersRoleForm's ServerSelector is meaningfully exercised by the role lifecycle journey. - Pre-seeded reports and appeals at every state (open / assigned / resolved / closed) plus comments, scoped to the non-admin user so the admin ban + parameterised punishments stay unappealed. - A bcrypt-hashed bm_player_pins row valid for 10 minutes plus a PIN-only player to cover both the registration journey and the forgotten-password / appeal-login PIN branches. - An APPEAL_CREATED notification rule targeting the super-admin role so the appeal lifecycle journey can assert end-to-end notification delivery. cypress/fixtures/e2e-data.json is extended with the new ids the specs consume (secondServerId, unbannedPlayerId, bannedPlayer*, *ReportId, *AppealId, notificationRuleId, pin* fields). Existing serverId / banId keys are preserved for documents.spec.js. cypress.config.js now ships explicit retries / video / screenshot defaults so CI failures are debuggable, plus user/PIN env defaults consumed by the new helpers. cypress/support/commands.js gains loginAsAdmin / loginAsUser / loginAsPin / logout / gql helpers so journey specs can short-circuit authentication and warm state via the API instead of the UI. * chore(ui): add data-cy hooks for e2e coverage Threads data-cy attributes through every component the new journey specs target so selectors stay stable across refactors and aren't coupled to translatable copy or react-select internals. - Punishment forms (Ban / Mute / Warn / Note) and PlayerActions get per-input + submit hooks plus a canCreateNote permission check used by the moderation journey. - Admin Server / Role / NotificationRule forms and list items get field, row, edit and delete hooks. - Webhook (Custom + Discord) forms, list items, WebhookTestForm and WebhookDeliveryItem get hooks for the create / test / deliveries / edit / delete loop, including the status badge for delivery assertions. - Admin DocumentsTable rows expose preview / delete hooks for the moderation journey's documents check. - Report and appeal sidebars expose state and assignee controls for the lifecycle journeys. - PunishmentPicker exposes per-row hooks (with type, id and server-id data attributes) plus filter and empty-state hooks for the appeal journey's picker filter coverage. - NotificationContainer / NotificationList expose row and link hooks, the global PlayerSelector in DefaultLayout becomes data-cy=player-search, and dashboard widgets get container hooks. - PlayerRegisterForm: the duplicate data-cy=password on the confirm field becomes confirm-password to match ResetPasswordForm. No behavioural changes other than the canCreateNote gate and the fixed register selector. * test(e2e): add admin server / role / webhook lifecycle journeys Three new journey specs covering deep admin flows that previously had zero coverage. Each spec creates with a Date.now()-suffixed name and deletes at the end so reruns work without a re-seed. - admin-server-lifecycle: create, list, edit and delete a server, exercising the new ServerForm / ServerItem hooks. - admin-role-lifecycle: create a custom role, assign it globally and per-server (using the second seeded server so AssignPlayersRoleForm's ServerSelector is meaningfully exercised), edit and delete. - admin-webhook-lifecycle: parameterised across Custom and Discord variants. Walks add -> save -> Test webhook modal (with the example APPEAL_CREATED payload against httpbin.org/post) -> deliveries page -> edit URL -> delete, asserting the WebhookResponse status code and the WebhookDeliveryItem status badge. All three specs use cy.loginAsAdmin() for setup and re-query elements between steps to avoid Cypress unsafe chaining. * test(e2e): add player moderation, appeal and report lifecycle journeys - player-moderation: parameterised across ban / mute / warn / note. An admin opens the unbanned seeded player's profile, creates each punishment type, edits ban + mute via /player/{ban|mute}/[id], and asserts each entry appears in the relevant profile list. Also walks the admin documents page (preview modal, modal-confirm delete cancel) to cover the new admin-doc-* hooks. - appeal-lifecycle: parameterised across ban / mute / warning. The banned user submits an appeal via /appeal -> punishment picker (uses the seeded picker filters and clear-filters control) -> the admin receives the pre-seeded APPEAL_CREATED notification, opens the appeal, comments, assigns, marches state to resolved, then asserts the notification flips to read. Notifications coverage is fully merged in here because APPEAL_CREATED is currently the only NotificationType. Includes a dedicated case that opens the AnimatedDisclosure on the picker row before clicking Appeal. - report-lifecycle: an admin walks an open report through the full state machine (Open -> Assigned -> Resolved -> Closed), comments and assigns along the way, and asserts that comments cannot be added once closed. Also smoke-checks the /reports list page. All three specs delete what they create and use re-queried selectors to avoid Cypress unsafe chaining. * test(e2e): add registration, PIN forgotten-password and error-pages journeys - registration: covers PIN-as-login on /appeal for the seeded PIN-only player, account registration including the password mismatch branch, the forgotten-password page render, login via PIN for the existing seeded user through the appeal flow, and a smoke check of the global player search in the nav (data-cy=player-search in DefaultLayout) routing through to /player/{id}. - error-pages: visits a deliberately bad route to assert the 404 page renders with the expected title and Homepage link, then visits /500 directly with failOnStatusCode:false to assert the 500 page renders with its Homepage link too. Specs are self-contained and use re-queried selectors throughout. * test(e2e-setup): add isolated setup wizard run with installer + import specs The normal Cypress run boots against an already-set-up DB and can never exercise /setup. This adds a dedicated setup-mode E2E target driven by its own Cypress config + supervisor process so the wizard can be walked end-to-end without touching any developer or CI .env file. Infrastructure: - cypress.setup.config.js: separate config rooted at cypress/e2e-setup with custom tasks (prepareSetupDb, dropSetupDbs, writeBmConfigFixture, readEnvFile, clearEnvFile, getSetupConsoleUuid, sleep) and a CYPRESS_ / SETUP_ env-var pipeline that flows DB credentials and the dotenv target path through to the installer specs. - cypress/scripts/setup-server.js: supervisor that spawns server.js from a temporary sandbox cwd (so dotenv.config() can't pick up the developer's .env), strips DB / encryption env vars to force setup-mode boot, and respawns the child after a clean exit so the spec can verify the post-finalize transition into normal-mode and the resulting /setup -> 404. Sets DISABLE_UI=true on the second boot so it doesn't try to load Next.js. - cypress/scripts/prepare-setup-db.js: drops + recreates throwaway WebUI and BM databases, runs BM-side migrations and seeds a Console player with a deterministic UUID using inetPton for the IP column. - npm run e2e:setup:server / e2e:setup:run / e2e:setup:open scripts. Specs: - cypress/e2e-setup/installer.spec.js covers progress rendering, bad DB credentials, password mismatch, invalid UUID validation, the "Create database if missing" flow, and the full happy-path finalize including a polling check on /api/setup/state for the supervisor restart and a post-completion 404 assertion on /setup. - cypress/e2e-setup/installer-config-import.spec.js covers paste-mode and path-mode ingestion of config.yml + console.yml (success + missing-section / missing-uuid / blank / non-existent-path errors). Hooks + supporting tests: - server/setup/static/installer.html / installer.js gain data-cy attributes on banners, the progress indicator, every step input, navigation buttons and the success / continue-to-login screen so the specs can target stable selectors. - server/test/setup-mode-boot.test.js gets a SETUP_TOKEN describe block that exercises the API token gate (preflight no-leak, mutation rejection, wrong / right token flows). Token coverage stays at the jest layer to keep the E2E supervisor simple. Helpers (setText, setTextArea, waitForState polling) are used in both specs to avoid Cypress unsafe chaining and arbitrary cy.wait() values. * ci: add setup-mode e2e job New setup_e2e job that runs in parallel with the existing test matrix. Boots MySQL on the runner, runs npm run e2e:setup:server (the supervisor that handles setup-mode -> normal-mode restarts), waits on http://127.0.0.1:3001/health and then runs the cypress/e2e-setup specs. CYPRESS_setup_* env vars are forwarded into the Cypress action so prepareSetupDb / dropSetupDbs / readEnvFile / writeBmConfigFixture tasks pick up the right host, port, user, password, dotenv path and DB names. * fix(e2e): stabilise lifecycle/journey specs after first CI run - opengraph/player: resolve fonts dir absolutely so server boots in sandboxed CWDs (setup_e2e supervisor) - cypress/setup: seed Console with deterministic UUID + expose via fixture - pages/admin/roles: data-cy wrappers for assign-global/server-role panels - admin-server spec: use fixture consoleUuid (was an invalid hex string) - admin-role spec: target new data-cy wrappers and move portal selectors outside .within() - admin-webhook spec: drop incorrect deliveries assertion, scope test modal to visible dialog only - appeal-lifecycle spec: filter punishment picker by server-id, .first() on dual desktop/mobile sidebars - report-lifecycle spec: .first() on dual sidebars, switch to existing /dashboard/reports listing page - registration spec: combine register + mismatch tests so the PIN is only consumed once, simplify PIN appeal flow typing to use 6 inputmode=numeric inputs directly - error-pages spec: failOnStatusCode:false on /500 visit * fix(e2e): unblock setup wizard rate-limit + tighten lifecycle specs - setup rate-limit: read from env so e2e can disable it. The default 10 req/60s easily exhausts during retries (~6 step submits per spec) which previously presented as the wizard "hanging" between steps. The setup-server supervisor now runs with a 10000/60 budget. - installer.spec: open the create-database <details> wrapper before checking the checkbox so the click isn't intercepted by the card body. - admin-server-lifecycle: import the canonical tables/{key} mapping instead of generating bm_{key}, otherwise createServer rejects with "missing tables" and the redirect assertion times out. - admin-webhook-lifecycle: drop :visible filtering on modal selectors - Headless UI's transition wraps these in animating containers that briefly fail Cypress's visibility heuristic, and the response payload can push action buttons out of the visible scroll viewport. Use click({ force: true }) for modal interactions and assert the form is removed from the DOM after closing. - appeal-lifecycle: intercept POST /graphql for createAppeal so a server-side rejection produces an actionable assertion instead of a silent "URL didn't change". Also use .first().find() for both the assignee and state widgets so we always interact with the visible desktop sidebar (the mobile copy is still mounted but hidden). - report-lifecycle: same .first().find() pattern for the report sidebar so .react_select__input.last() doesn't accidentally pick the hidden mobile input. * fix(e2e): more lifecycle/installer fixes after CI run on 12ec80b - installer.spec: use a valid v4 admin UUID. The committed value aaaa-bbbb-cccc-dddd-... was rejected by validator.isUUID() (v4 only by default), causing the admin step to fail and the wizard to never reach the Review step. - admin-webhook-lifecycle: cy.reload() after navigating back to the list page so SWR's cached listWebhooks response (no revalidateOnMount, 2s dedupe) doesn't race the assertion that the edited URL is rendered. - admin-server-lifecycle: intercept POST /graphql for createServer so a server-side rejection (missing tables, console UUID lookup, etc.) produces an actionable assertion instead of a silent "URL didn't change" timeout. - report-lifecycle: drop the brittle initial 'Open' state assertion and just confirm the sidebar widgets mounted, mirroring the appeal-lifecycle pattern. The state transitions later in the test (Open -> Assigned -> Resolved -> Closed) implicitly prove the starting value was correct. * fix(e2e): make sleep cypress task resolve to null Cypress requires task handlers to return a value, null, or a Promise that resolves to a value/null. The original `sleep` task returned `new Promise((resolve) => setTimeout(resolve, ms))`, which resolves to `undefined` and fails the installer happy-path waitForState polling loop with `cy.task('sleep') failed: returned undefined`. * fix(e2e): poll setup state via Node task to survive supervisor restart The setup-mode child exits after finalize and is respawned by the supervisor ~500ms later in normal mode. The waitForState polling loop was using cy.request(), which surfaces ECONNREFUSED as a hard test failure even though the gap is expected. Switched the poll to a new fetchSetupState task that issues a plain Node http.request and resolves to { ok: false } on connection errors so the loop can keep retrying while the new process boots. * fix(e2e): make lifecycle specs resilient to retries via per-attempt names Cypress retries reuse the spec context, so describe-level random names get reused across attempts. When the first attempt creates a row in the shared test database and then fails on a later step, attempt 2 collides with that row ("server with this name already exists" / stale webhook URL state). - admin-server-lifecycle: move the random server name into the test body so every attempt mints a fresh Date.now()-based name. - admin-webhook-lifecycle: derive the URLs from Date.now() per attempt and intercept create/update mutations so backend rejections produce actionable errors. Also assert the URL field hydrates the saved value before clearing so we never race react-hook-form's defaultValues sync into submitting the original URL. * fix(test): pin MockDate to session.updated in setPassword test The test's `MockDate.set(Date.now() - 5000)` was time-relative to real wall-clock time, but the session cookie issued by the original `getAuthPassword()` call carries `session.updated` derived from the seeded user.updated timestamp. When total test runtime crossed the 5-second window the mocked time landed at >= the seeded value, so the resolver's `session.updated = Math.floor(Date.now() / 1000)` produced no change, koa-session detected nothing to commit, and the response carried no `set-cookie` header. The follow-up `header['set-cookie'].join(';')` then crashed with TypeError on Node 22.x where the suite happens to run a few seconds slower. Decode the session cookie, pluck its `updated` field, and pin the mocked time precisely 5s before that so the new session always strictly post-dates the existing one. * fix: mount setup router before main router so /setup lockdown can fire The main router's catch-all `(.*)` route was matching /setup and /api/setup/* requests before the setup router had a chance to handle them. With disableUI=true the catch-all would set ctx.respond=false and never write a response (so the request hung); with disableUI=false it would defer to Next.js, which has no /setup page. Either way the post-setup lockdown middleware in server/routes/setup.js never ran, meaning /setup stayed reachable after the WebUI database had been provisioned. Re-ordering the middleware fixes both cases without touching the catch-all itself. Also covers the lockdown deterministically with a new jest test (server/test/setup-lockdown-after-complete.test.js) and trims the e2e installer happy-path to skip the post-finalize verification, which was fragile against the supervisor's setup-mode -> normal-mode child restart on Cypress retries. * test(cypress): surface updateServer errors and fix webhook list races Two related improvements after CI showed both lifecycle specs failing on all 3 retry attempts: - admin-server-lifecycle: alias the updateServer mutation the same way we already alias createServer, so a silent server-side rejection (e.g. the saved password not round-tripping through encrypt/decrypt) produces the actual GraphQL error instead of a "URL didn't change" timeout when the edit form fails to redirect. - admin-webhook-lifecycle: capture the new webhook id directly from the createWebhook response and use it to scope all subsequent assertions, instead of reaching for `[data-cy-template=...].last()`. listWebhooks has no ORDER BY clause, so on Cypress retries (which reuse the shared test database) the .last() row could be an older webhook left over from a previous attempt with a stale URL. Also force a cy.reload() after the redirect back to /admin/webhooks so the SWR cache (no revalidate-on-mount, 2s default dedupe) picks up the brand-new row. * test(cypress): trim admin-server-lifecycle names to fit the 20-char limit The intercept added in the previous commit surfaced the underlying failure: \`Cypress\${Date.now()}Renamed\` is 27 characters but CreateServerInput.name / UpdateServerInput.name both have \`@constraint(maxLength: 20)\`, so updateServer was being rejected with a BAD_USER_INPUT error (and the silent rejection is exactly what kept the form on /admin/servers/[id]/edit instead of redirecting back). Switch to a \`Cy\${Date.now()}\` / \`\${name}R\` pattern (15 / 16 chars) so both create and rename stay within the schema limit. * test(cypress): reload admin servers list after edit to dodge SWR cache After the rename redirects back to /admin/servers/[id], the test does a \`cy.visit('/admin/servers')\` to assert the new name shows up in the list. The list page reads servers via SWR with no revalidate-on-mount and a default 2s dedupe window, so the cached response from the very first \`cy.visit('/admin/servers')\` (with the pre-rename label) was being served and the lookup by \`data-cy-server="\${renamedServer}"\` timed out. Mirror the webhook spec's pattern: \`cy.reload()\` immediately after the visit to force SWR to refetch. Also bumps the lookup timeout to 10s for the same reason as the create case. * fix(servers): refresh in-memory serversPool after updateServer updateServer wrote to bm_web_servers but never touched state.serversPool, so every resolver that reads from the pool (the /admin/servers list, per-server scoped resolvers, etc.) kept serving the pre-rename name and stale connection details until the 3-second background sync in connections/servers-pool.js caught up. createServer/deleteServer already keep the cache in sync inline; this brings updateServer into the same shape: - Refresh the cached config with the new name/host/port/user/db/tables. - Rebuild the knex pool when any connection detail (or password) changed and destroy the previous pool, mirroring what the background sync does. Also adds a jest test covering the refresh and drops the cy.reload() workaround the cypress lifecycle test had been carrying to mask this exact race. * fix(admin): invalidate list SWR caches on add/edit + sort listWebhooks The admin add/edit pages were calling router.push() back to the list page without invalidating its SWR cache, so within SWR's 2s dedupe window the list briefly showed pre-mutation data. The webhook lifecycle e2e papered over this with cy.reload() — drop the workaround and fix it at the source across webhooks, servers, notification-rules, and roles. Also add an ORDER BY clause to listWebhooks: it was relying on MySQL's implementation-defined row order, which makes LIMIT/OFFSET pagination formally undefined and made the e2e test pick the wrong row with .last(). * chore: condense lingering verbose lifecycle/setup comments Follow-up cleanup pass on multi-line comments that just narrate what the code does. No behaviour change. * fix(boot): boot in setup-mode when DB reachable but install incomplete The Docker compose smoke job was failing with curl "Connection reset by peer" because the webui container was crash-looping. Reproduced locally with the production compose stack. Root cause: when DB credentials + keys are set in the environment but no admin user has been created yet (i.e. fresh `docker compose up` before the operator visits /setup), server.js was running the strict non-setup env validator before consulting getSetupState. The validator hard-fails on missing CONTACT_EMAIL and called process.exit(1), so the container restarted forever and /health was never reachable. Reorder: consult getSetupState first, and when setup is incomplete validate in setup mode (warnings, not errors) so the wizard can run. Also extract the boot decision into a pure `decideBoot` function on server.js so jest can cover the regression directly without spawning the full process. Workflow side-fix: the smoke job's "Wait for /health" step was missing the MYSQL_PASSWORD env, so when the wait timed out and tried to dump container logs via `docker compose logs`, compose re-parsed docker-compose.prod.yml and bailed on the required-var check — hiding the actual webui container logs. Switch to `docker logs <container>` and add the env block so future failures are debuggable.
1 parent 46fd192 commit c0b433f

112 files changed

Lines changed: 7938 additions & 361 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# BanManager WebUI - Environment Configuration
2-
# Copy this file to .env and update values as needed
2+
# Copy this file to .env and update values as needed.
3+
#
4+
# You usually don't need to fill this in by hand:
5+
# - Docker: the entrypoint generates missing keys automatically.
6+
# - CLI: run `npx bmwebui setup` for an interactive wizard.
7+
# - Web: start the server with no keys and finish setup at http://your-host:3000/setup
8+
#
9+
# To verify your configuration at any time:
10+
# npx bmwebui doctor
311

412
# Server display name (shown in footer)
513
SERVER_FOOTER_NAME=BanManagement
@@ -15,12 +23,15 @@ DB_PASSWORD=password
1523
DB_NAME=bm_local_dev
1624
DB_CONNECTION_LIMIT=5
1725

18-
# Security keys (generate unique values for production)
19-
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
20-
ENCRYPTION_KEY=b097b390a68441cc3bb151dd0171f25c3aabc688c50eeb26dc5e13254b333911
21-
SESSION_KEY=a73545a5f08d2906e39a4438014200303f9269f3ade9227525ffb141294f1b62
26+
# Security keys
27+
# Leave blank to have setup (CLI/Docker/web installer) generate fresh values for you.
28+
# To generate manually: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
29+
# These MUST be unique 64-character hex strings in production.
30+
ENCRYPTION_KEY=
31+
SESSION_KEY=
2232

23-
# Push notification VAPID keys (generate with: npx web-push generate-vapid-keys)
33+
# Push notification VAPID keys
34+
# Leave blank to have setup generate them, or run: npx web-push generate-vapid-keys
2435
NOTIFICATION_VAPID_PUBLIC_KEY=
2536
NOTIFICATION_VAPID_PRIVATE_KEY=
2637

.github/workflows/build.yaml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,136 @@ jobs:
9696
flag-name: run-${{ matrix.test_number }}
9797
parallel: true
9898

99+
setup_e2e:
100+
runs-on: ubuntu-latest
101+
name: Setup wizard E2E
102+
103+
env:
104+
DB_HOST: 127.0.0.1
105+
DB_PORT: 3306
106+
DB_USER: root
107+
DB_PASSWORD: root
108+
SETUP_PORT: 3001
109+
SETUP_DOTENV_PATH: /tmp/bm-setup-test.env
110+
SETUP_DB_NAME: bm_e2e_setup
111+
SETUP_BM_DB_NAME: bm_e2e_setup_bm
112+
113+
steps:
114+
- uses: actions/checkout@v4
115+
- name: Set up Node.js 22
116+
uses: actions/setup-node@v4
117+
with:
118+
node-version: 22.x
119+
- name: Cache Node.js modules
120+
uses: actions/cache@v4
121+
with:
122+
path: ~/.npm
123+
key: ${{ runner.os }}-setup-e2e-${{ hashFiles('**/package-lock.json') }}
124+
restore-keys: |
125+
${{ runner.os }}-setup-e2e-
126+
- run: npm ci
127+
- name: Set up MySQL
128+
run: |
129+
sudo systemctl start mysql.service
130+
sleep 5 && mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';" -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
131+
mysql -e "SET GLOBAL sql_require_primary_key = ON;" -uroot
132+
- name: Cypress run (setup wizard)
133+
uses: cypress-io/github-action@v6
134+
env:
135+
DB_HOST: 127.0.0.1
136+
DB_PORT: 3306
137+
DB_USER: root
138+
DB_PASSWORD:
139+
SETUP_PORT: 3001
140+
SETUP_DOTENV_PATH: /tmp/bm-setup-test.env
141+
SETUP_DB_NAME: bm_e2e_setup
142+
SETUP_BM_DB_NAME: bm_e2e_setup_bm
143+
LOG_LEVEL: warn
144+
CYPRESS_setup_db_password: ''
145+
CYPRESS_setup_db_host: 127.0.0.1
146+
CYPRESS_setup_db_port: '3306'
147+
CYPRESS_setup_db_user: root
148+
CYPRESS_setup_dotenv_path: /tmp/bm-setup-test.env
149+
CYPRESS_setup_db_name: bm_e2e_setup
150+
with:
151+
install: false
152+
start: npm run e2e:setup:server
153+
wait-on: "http://127.0.0.1:3001/health"
154+
wait-on-timeout: 60
155+
command: npm run e2e:setup:run
156+
99157
build_docker:
100158
runs-on: ubuntu-latest
101159
steps:
102160
- uses: actions/checkout@v4
103161
- run: docker build . # TODO cache
104162

163+
smoke_docker:
164+
name: Docker compose smoke test
165+
runs-on: ubuntu-latest
166+
needs: build_docker
167+
steps:
168+
- uses: actions/checkout@v4
169+
170+
- name: Set up Docker Buildx
171+
uses: docker/setup-buildx-action@v3
172+
173+
- name: Build webui image (load locally)
174+
uses: docker/build-push-action@v6
175+
with:
176+
context: .
177+
load: true
178+
tags: banmanagement/webui:smoke
179+
cache-from: type=gha
180+
cache-to: type=gha,mode=max
181+
182+
- name: Start docker compose stack
183+
env:
184+
MYSQL_ROOT_PASSWORD: rootpassword
185+
MYSQL_PASSWORD: webuipassword
186+
WEBUI_PORT: 3000
187+
run: |
188+
# Use the locally built image instead of pulling
189+
sed -i 's|image: banmanagement/webui:latest|image: banmanagement/webui:smoke|' docker-compose.prod.yml
190+
docker compose -f docker-compose.prod.yml up -d
191+
192+
- name: Wait for /health to report setup_required
193+
env:
194+
MYSQL_ROOT_PASSWORD: rootpassword
195+
MYSQL_PASSWORD: webuipassword
196+
WEBUI_PORT: 3000
197+
run: |
198+
set -e
199+
for attempt in $(seq 1 60); do
200+
response=$(curl -fsS http://127.0.0.1:3000/health || true)
201+
echo "attempt $attempt: $response"
202+
if echo "$response" | grep -q '"status":"setup_required"' || echo "$response" | grep -q '"status":"ok"'; then
203+
echo "Health endpoint is responding"
204+
exit 0
205+
fi
206+
sleep 2
207+
done
208+
echo "Health endpoint never responded"
209+
echo "===== docker ps ====="
210+
docker ps -a
211+
echo "===== webui logs ====="
212+
docker logs banmanager-webui 2>&1 | tail -200 || true
213+
echo "===== mysql logs ====="
214+
docker logs banmanager-webui-mysql 2>&1 | tail -80 || true
215+
exit 1
216+
217+
- name: Confirm setup mode signal
218+
run: |
219+
curl -fsS http://127.0.0.1:3000/health | tee /tmp/health.json
220+
grep -q 'setup_required\|"status":"ok"' /tmp/health.json
221+
222+
- name: Stop docker compose stack
223+
if: always()
224+
env:
225+
MYSQL_ROOT_PASSWORD: rootpassword
226+
MYSQL_PASSWORD: webuipassword
227+
run: docker compose -f docker-compose.prod.yml down -v
228+
105229
finish:
106230
needs: test
107231
runs-on: ubuntu-latest

.github/workflows/deploy_docker.yaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ jobs:
1313
- name: Check out the repo
1414
uses: actions/checkout@v4
1515

16+
- name: Set up QEMU
17+
uses: docker/setup-qemu-action@v3
18+
19+
- name: Set up Docker Buildx
20+
uses: docker/setup-buildx-action@v3
21+
1622
- name: Log in to Docker Hub
1723
uses: docker/login-action@v3
1824
with:
@@ -25,12 +31,15 @@ jobs:
2531
with:
2632
images: banmanagement/webui
2733

28-
- name: Build and push Docker image
34+
- name: Build and push multi-platform Docker image
2935
uses: docker/build-push-action@v6
3036
with:
3137
context: .
38+
platforms: linux/amd64,linux/arm64
3239
push: true
3340
tags: |
3441
banmanagement/webui:${{ github.sha }}
3542
banmanagement/webui:latest
36-
labels: ${{ steps.meta.outputs.labels }}
43+
labels: ${{ steps.meta.outputs.labels }}
44+
cache-from: type=gha
45+
cache-to: type=gha,mode=max

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ npm-debug.log
55
typings*
66
.sync
77
.env
8+
.env.*
9+
!.env.example
810
.nyc_output/
911
.next
1012
bundles

CHECKS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/ BanManager-WebUI
1+
/health ok

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ RUN adduser --system --uid 1001 nextjs
3737

3838
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
3939
RUN mkdir -p /app/uploads/documents && chown nextjs:nodejs /app/uploads/documents
40+
RUN mkdir -p /app/config && chown nextjs:nodejs /app/config
4041

4142
COPY --from=builder --chown=nextjs:nodejs /app ./
4243

4344
VOLUME /app/.next/cache/images
4445
VOLUME /app/public/images/opengraph/cache
4546
VOLUME /app/uploads/documents
47+
VOLUME /app/config
4648

4749
USER nextjs
4850

@@ -52,4 +54,4 @@ EXPOSE 3000
5254

5355
ENV PORT 3000
5456

55-
CMD ["node", "server.js"]
57+
CMD ["node", "docker-entrypoint.js"]

README.md

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,85 @@ To learn more about configuration, usage and features of BanManager, take a look
5454

5555
## Installation (Production)
5656

57-
For deploying BanManager WebUI on your own server, see the **[full installation guide](https://banmanagement.com/docs/webui/install)**.
57+
Pick the path that matches your environment. Each one ends with a working WebUI you can sign in to.
58+
59+
For more depth (BanManager plugin install, advanced topics), see the **[full installation guide](https://banmanagement.com/docs/webui/install)**.
5860

5961
### Requirements
6062

61-
- [Node.js](https://nodejs.org/) LTS (v20 or v22)
62-
- MySQL v5+ or MariaDB v10+
63-
- 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)
63+
- MySQL v5+ or MariaDB v10+ (shared with the BanManager plugin)
64+
- A Minecraft server with [BanManager](https://github.com/BanManagement/BanManager) & [BanManager-WebEnhancer](https://ci.frostcast.net/job/BanManager-WebEnhancer/) configured to [use MySQL or MariaDB](https://banmanagement.com/docs/banmanager/install#setup-shared-database-optional)
65+
- For non-Docker installs: [Node.js](https://nodejs.org/) LTS (v20 or v22)
66+
67+
### Path A — Docker Compose (recommended)
68+
69+
Includes the WebUI and a MySQL database in one command. Already have MySQL? Use [`docker-compose.prod-no-db.yml`](docker-compose.prod-no-db.yml) instead.
70+
71+
```bash
72+
curl -O https://raw.githubusercontent.com/BanManagement/BanManager-WebUI/master/docker-compose.prod.yml
73+
74+
cat > .env <<'EOF'
75+
MYSQL_ROOT_PASSWORD=$(openssl rand -hex 24)
76+
MYSQL_PASSWORD=$(openssl rand -hex 24)
77+
EOF
78+
79+
docker compose -f docker-compose.prod.yml up -d
80+
```
81+
82+
The Compose file refuses to start without `MYSQL_ROOT_PASSWORD` and `MYSQL_PASSWORD` set — generate long random values (the snippet above does this for you) and keep them in a `.env` file alongside the compose file. The container generates encryption/session/VAPID keys, runs migrations, and persists state to a `webui_config` volume on first boot. Open `http://your-host:3000/setup` to finish setup in your browser, or attach a shell and run `docker compose exec webui npx bmwebui setup` for the CLI wizard.
83+
84+
To check things look right at any time:
85+
86+
```bash
87+
docker compose exec webui npx bmwebui doctor
88+
```
6489

65-
### Quick Install
90+
### Path B — Web installer (any host)
91+
92+
Useful if you want a one-shot install with no terminal interaction after the server is up.
6693

6794
```bash
6895
git clone https://github.com/BanManagement/BanManager-WebUI.git
6996
cd BanManager-WebUI
70-
npm ci --production
71-
npm run setup
97+
npm ci --omit=dev
98+
npm run build
99+
node server.js # starts in setup mode if no .env exists yet
72100
```
73101

74-
The setup wizard will guide you through configuring your database connection and creating an admin account.
102+
Then visit `http://your-host:3000/setup` and follow the wizard. The web installer writes a `.env` file, runs migrations, and creates the first admin account. After it finishes, restart the server (`Ctrl+C`, `node server.js`).
103+
104+
> **⚠ Security model.** The setup endpoint is open by default — whoever loads `/setup` first becomes the admin (same model as WordPress/Ghost). If your install host is reachable from the internet, set `SETUP_TOKEN=$(openssl rand -hex 24)` before starting and share that token only with the person doing the install. The setup screen will require it as the first step. Once an admin user exists the setup routes return 404.
105+
>
106+
> **Behind a reverse proxy?** Set `TRUST_PROXY=true` so the WebUI uses `X-Forwarded-For` / `X-Forwarded-Proto` to detect the real client IP and HTTPS status. Without it, every request looks like it came from `127.0.0.1` and the "you're on a secure local connection" banner can be misleading.
107+
108+
### Path C — CLI wizard (terminal-only)
109+
110+
Best when you have shell access and want everything done before the server starts.
111+
112+
```bash
113+
git clone https://github.com/BanManagement/BanManager-WebUI.git
114+
cd BanManager-WebUI
115+
npm ci --omit=dev
116+
npm run setup # interactive wizard, writes .env
117+
npm run build
118+
npm start
119+
```
120+
121+
The wizard auto-detects database settings and the console UUID from your BanManager `plugins/BanManager` folder when it can.
122+
123+
### Verify and run as a service
124+
125+
After any path:
126+
127+
- `npx bmwebui doctor` — runs preflight checks (env, DB, migrations, admin user, plugin tables) and tells you exactly what's wrong if anything is.
128+
- `npx bmwebui setup systemd` — registers the WebUI as a `systemd` service.
129+
- `npx bmwebui setup nginx` / `setup caddy` / `setup apache` — drops in a reverse-proxy template for your web server of choice (existing nginx setups are unchanged).
130+
131+
Need to add another account later?
132+
133+
```bash
134+
npx bmwebui account create
135+
```
75136

76137
---
77138

@@ -137,7 +198,7 @@ Copy `.env.example` to `.env` and adjust as needed. Key variables:
137198

138199
- `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` - Database connection
139200
- `ADMIN_USERNAME`, `ADMIN_PASSWORD` - Admin account credentials (also used by Cypress)
140-
- `ENCRYPTION_KEY`, `SESSION_KEY` - Security keys (generate unique values for production)
201+
- `ENCRYPTION_KEY`, `SESSION_KEY` - Security keys (leave blank and `npm run dev:setup` will generate + persist them, or set your own)
141202

142203
### Resetting the Database
143204

0 commit comments

Comments
 (0)