Commit Graph

24 Commits

Author SHA1 Message Date
Vantz Stockwell
759bd0be2e feat: Add loot builder backend + static data + DB migration
All checks were successful
Build Companion Agent / build (push) Successful in 26s
Test Asgard Runner / test (push) Successful in 3s
- Migration 013: loot_profiles table (JSONB loot_table + loot_groups, license-scoped)
- TypeORM entity matching migration schema exactly
- NestJS loot module: 10 endpoints (CRUD, duplicate, apply, import, export, containers)
- Multiplier logic recursively scales Min/Max/Scrap across loot tables and groups
- Apply-to-server writes BetterLoot JSON via NATS file manager + RCON reload
- Frontend static data: 191 Rust items, 51 container prefabs
- TypeScript types for BetterLoot data model (PrefabLoot, LootEntry, LootRNG, etc.)
- Fix vue-tsc errors: UngroupedItems uses LootRNG, null safety in store/view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:30:11 -05:00
Vantz Stockwell
fee0ae2420 feat: Add files module — VueFinder-compatible REST API over NATS
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Implements GET/POST /api/files proxy for all VueFinder operations
(list, search, preview, download, delete, rename, move, copy, mkdir,
new-file, save, upload). Routes via NATS request-reply to the companion
agent on corrosion.{license_id}.files.cmd with 30s timeout. Gated
behind files.view (GET) and files.manage (POST) permissions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:10:32 -05:00
Vantz Stockwell
2b45413c20 feat: Wire uMod browse proxy and custom plugin upload
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Backend:
- GET /plugins/browse proxies uMod search.json filtered to Rust category,
  with 5-minute in-memory Map cache to avoid hammering the upstream API
- POST /plugins/upload accepts .cs files up to 5 MB via multipart, persists
  to plugin_registry, and dispatches plugin_upload action over NATS so the
  companion agent can write the file to the game server
- Legacy GET /plugins/search stub preserved (now directs callers to /browse)
- FileInterceptor + @UploadedFile follow the existing maps upload pattern

Frontend:
- useApi composable gains upload() method for multipart/form-data requests
  (omits Content-Type so the browser sets the correct multipart boundary)
- plugins store adds browseUmod() calling GET /plugins/browse and
  uploadPlugin() calling POST /plugins/upload with FormData;
  UmodPlugin and UmodBrowseResult TypeScript interfaces exported
- PluginsView Browse tab now calls browseUmod() through the backend proxy
  (no cross-origin requests to uMod directly); results show title,
  downloads_shortened, and latest_release_version_formatted from the
  real uMod payload
- New Upload Custom tab: drag-and-drop or click file input for .cs files,
  client-side extension/size validation, spinner during upload, success
  toast + auto-switch to Installed tab on completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:09:19 -05:00
Vantz Stockwell
cbb3ba6586 feat: Wire execution engines for schedules, alerts, wipes, and module install
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- schedules/: Add schedule executor in SchedulesService — polls every 60s for
  tasks where next_run <= now, dispatches NATS commands per task_type
  (restart, announcement, command, plugin_reload). Calculates next_run from
  cron expression on create/update/toggle. Bootstraps missing next_run values
  on startup. Wire NatsService into SchedulesModule.

- alerts/: Add alert evaluator in AlertsService — polls every 90s, loads all
  alert_config rows, queries latest server_stats per license, evaluates FPS
  degradation and population drop thresholds. Fires alert_history records on
  breach. Enforces 10-minute in-memory cooldown per alert type per license to
  prevent flooding. Wire ServerStats repo into AlertsModule.

- wipes/: Replace hardcoded dry-run mock with profile-aware simulation.
  Resolves actual WipeProfile by ID (cross-tenant protected), builds
  would_delete/would_preserve lists from wipe_type, factors pre_wipe_config
  (backup, countdown warnings) and post_wipe_config (health checks, retry
  attempts) into estimated_duration_seconds. Returns profile_name and notes.

- store/: Fix installModule stub — creates a real module_installations record
  with status='installed' and installed_at timestamp. Idempotent on retry,
  resets failed installations. Wire ModuleInstallation repo into StoreModule.
  getMyModules now returns real installation data instead of filtered purchases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:02:49 -05:00
Vantz Stockwell
9240feedaf fix: Wire real data sources across players, analytics, status, and maps services
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- players: Primary data from player_sessions (online status, playtime aggregates);
  ban/unban status overlaid from player_actions latest action per steam_id.
  Register PlayerSession entity in PlayersModule. Extend NATS forwarding to
  include 'unban' alongside kick and ban.
- analytics: Fix retention period boundary bug — sessions were queried with only
  a lower-bound filter (MoreThan), causing all future cycles to bleed into earlier
  wipe windows. Replaced with Between(wipeDate, endDate) for correct isolation.
- status: Replace hardcoded player_count=0/max_players=0 with live data from
  most-recent server_stats row per license. Register ServerStats entity in
  StatusModule. Falls back to 0 gracefully when no stats exist yet.
- maps: File buffer was computed and discarded — never written to disk.
  Now writes to /app/map_data/{licenseId}/{timestamp}_{filename} (tenant-isolated,
  docker volume map_data). Creates directories with mkdirSync(recursive:true).
  Logs success/failure via NestJS Logger. Throws 500 on disk write failure
  instead of silently losing data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:01:45 -05:00
Vantz Stockwell
7bf3e5639e feat: Wire NATS command publishing for server commands and plugin installs
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- servers.service: sendCommand() now throws InternalServerErrorException on
  NATS failure instead of silently succeeding; returns { success, message }
  instead of the legacy { output } shape; adds NestJS Logger
- plugins.service: installPlugin() dispatches plugin_install to
  corrosion.{license_id}.cmd.server after DB save; NATS failure is logged
  but non-fatal so the DB record is preserved regardless

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:59:59 -05:00
Vantz Stockwell
fee16c3b2b fix: Add license_id to JWT payload — unblocks all tenant-scoped operations
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
The JWT was missing license_id, causing @CurrentTenant() to throw 401
on every protected route. Now generateTokens() accepts a licenseId
param, and all three callers (register, login, refresh) look up the
user's license and pass it through.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:52:22 -05:00
Vantz Stockwell
8253680fbd fix: License key format, login populates license, case-insensitive email
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- admin.service.ts: createLicense() now uses CORR-XXXX-XXXX-XXXX format
  instead of raw hex hash
- admin.service.ts: getLicenses() flattens owner_email in response to
  match frontend expected shape
- auth.service.ts: Login/register responses now include full license
  object so frontend can populate auth store
- auth.service.ts: Email lookups are case-insensitive (LOWER()) to
  prevent duplicate accounts from case variations
- LoginView/RegisterView: Call setLicense() after setAuth()
- AdminLicenses: Handle null expires_at (was showing Dec 31, 1969),
  fix nullable types, fix query param name (per_page → limit)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:32:35 -05:00
Vantz Stockwell
14b099b075 fix: Replace socket.io with native WS adapter — fixes WebSocket 1006
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Frontend uses native WebSocket API, backend was using socket.io which
speaks an incompatible protocol. Switched to @nestjs/platform-ws so
both sides speak native WebSocket. Also fixed JWT TTL override in
docker-compose.yml (was hardcoded to 900s, now 14400s/4h).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:21:36 -05:00
Vantz Stockwell
f39a418e9c fix: Refresh endpoint returns new refresh_token + bump access TTL to 4h
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
The refresh endpoint only returned access_token, causing the frontend to
set refreshToken=undefined after first refresh — breaking the entire
token chain. Now returns both tokens (rotating refresh). Access token
default bumped from 15min to 4h (14400s) for practical server setup
sessions. Also fixed empty license_key for super admin via DB update.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:05:19 -05:00
Vantz Stockwell
834e17e7cf feat: Add backend support for one-click Rust server deployment
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Add deploy endpoint, DTO, NATS command publisher, and WebSocket bridge
subscription to support the one-click server deployment feature via the
companion agent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:45:06 -05:00
Vantz Stockwell
8bb6cc0890 feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Frontend:
- Wire Dashboard quick actions (start/stop/trigger wipe) + next wipe schedule
- Wire Console WebSocket streaming for real-time output
- Implement TOTP 2FA challenge flow in LoginView
- Wire Plugin load/unload toggle + uninstall buttons with confirmations
- Wire WipesView profile selector, disable trigger when no profiles
- Build full WipeProfiles create/edit modal with all config fields
- Wire MapsView file upload with multipart FormData
- Fix SettingsView empty catch blocks → toast error messages
- Fix stale localStorage token reads in CSV exports → auth store
- Fix auth store hardcoded permissions → JWT-decoded role permissions
- Fix wipe store onMounted lifecycle bug → explicit subscribe action
- Update EarlyAccessView from countdown to "Now Live" state

Backend:
- Wire wipe trigger to publish NATS cmd (corrosion.{id}.cmd.wipe)
- Wire plugin reload/uninstall to publish NATS cmd
- Expand NatsBridgeService: add files, wipe status, server status subs
- Add PATCH schedules/:id/toggle endpoint for task toggling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:34:09 -05:00
Vantz Stockwell
a181ed7ded feat: Complete stub services with real implementations and graceful not-configured responses
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
- ChangelogService: inject PlatformChangelog repo, query with findAndCount(skip/take), return actual data
- ChangelogModule: add TypeOrmModule.forFeature([PlatformChangelog])
- MapsService: add uploadMap() — SHA-256 checksum, storage path, full entity save
- MapsController: add POST /maps/upload with FileInterceptor, map.manage permission, @UploadedFile
- AuthService: replace console.log stubs with Logger; forgotPassword returns 200 with clear message; resetPassword throws NotImplementedException
- PluginsService: searchUmod returns { results: [], message: 'not yet configured' } instead of bare []
- SteamService: add Logger.warn on every stub path (checkForceWipe, getPlayerSummary)
- SettingsService: add Logger; both Cloudflare DNS stubs emit Logger.warn before DB save
- MigrationService: add Logger; exportConfig logs warning + returns note field; importConfig throws NotImplementedException
- Install @types/multer dev dependency for Express.Multer.File type support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 13:33:08 -05:00
Vantz Stockwell
e1a3ea3b78 feat: Wave 2 — entities, security guards, API key encryption (15 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Entities:
- Create 5 new TypeORM entities: webstore_config, webstore_categories,
  webstore_items, webstore_transactions, module_store (all verified against live DB)
- Fix wipe-profile entity: remove incorrect default {} for pre/post wipe configs

Security:
- Add @RequirePermission guards to 7 controllers (36 endpoints total):
  team, webstore, notifications, alerts, analytics, settings, schedules
- Encrypt panel API key with AES-256-GCM in setup service (was plaintext)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:28:48 -05:00
Vantz Stockwell
208622000c fix: Wave 1 — critical bug fixes across 9 files
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- Fix double-prefix URL bugs in 4 analytics/revenue views (/api/api → /api)
- Fix AdminDashboard quick-links routing (/platform-admin/* → /admin/*)
- Fix MigrationView import missing Authorization header
- Remove dead ConsoleModule from app.module (conflicts with NatsBridgeGateway on /ws)
- Fix store.service.ts raw Error throws → NotFoundException/ForbiddenException
- Fix payment-order entity FK (webstore_subscription_id → WebstoreSubscription)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:24:24 -05:00
Vantz Stockwell
a5e9d02a9a fix: Wrap admin subscriptions response to match frontend contract
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Frontend expects { subscriptions: [{ owner_email, module_name, license_id }] }
but backend returned raw WebstoreSubscription entities. Shaped the response
with license.owner join for owner_email and plan_id mapped to module_name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:25:05 -05:00
Vantz Stockwell
9bca8bd2fc fix: Response wrapping, error logging, and controller hardening (COA 3)
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- HttpExceptionFilter: Log actual error details for non-HttpExceptions (was silently swallowing 500s)
- ServersService: Return null fields instead of 404 for new licenses without servers
- NotificationsController: Wrap config responses as { config } to match frontend expectations
- WebstoreController: Wrap config responses as { config } to match frontend expectations
- ChatController: Replace ParseIntPipe with manual parseInt (400 on missing optional param)
- WipesController: Same ParseIntPipe fix for history limit param

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:52:25 -05:00
Vantz Stockwell
78e97babf1 fix: Align NestJS entities with actual DB schema — 12 files, 5 entities
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Root cause of all remaining 500s: TypeORM entities were scaffolded with
"ideal" column names that don't match the Postgres columns created by
the Rust migrations. Every query generated SQL referencing non-existent
columns.

Entity fixes:
- notifications_config: email_enabled→email_alerts_enabled, removed
  6 phantom columns (email_address, notify_on_start, notify_on_stop,
  notify_on_player_threshold, player_threshold), renamed 4 notify
  columns to match DB (notify_server_crash, notify_wipe_start, etc),
  added 3 missing columns (notify_server_offline, notify_store_purchase,
  notify_player_report)
- team_members: joined_at→accepted_at (nullable, matches DB)
- roles: removed description column (doesn't exist in DB)
- scheduled_tasks: is_enabled→is_active, removed phantom last_run
- wipe_profiles: pre/post_wipe_config nullable→NOT NULL with default

Service/DTO fixes:
- Updated all property references across notifications, team, schedules
  services and DTOs to match corrected entity names
- Added is_active to UpdateTaskDto (frontend sends it, was being
  rejected by forbidNonWhitelisted validation)
- Removed description from CreateRoleDto

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:33:55 -05:00
Vantz Stockwell
3cb714a792 fix: Resolve 500/404 cascade — JWT tenant context, wipe routes, changelog stub
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Root cause: super_admin JWT returned early with no license_id, causing
@CurrentTenant() to pass undefined to every tenant-scoped service query.

- jwt.strategy: Move license lookup before super_admin early return so
  admins who own licenses get their license_id in the JWT payload
- CurrentTenant decorator: Throw 401 with clear message when license_id
  is undefined instead of letting undefined cascade into TypeORM queries
- Wipe store: Fix 6 wrong routes (/profiles → /wipes/profiles, etc.)
  and remove redundant manual license_id guards
- Changelog module: Add stub controller/service returning empty array
  to eliminate 404 on /api/changelog
- ChangelogView: Handle both array and {entries} response shapes
- AGENTS.md: Streamlined 3-tier roster (Opus/Sonnet/Haiku)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:11:41 -05:00
Vantz Stockwell
e849d7803c fix: JWT tokens expire instantly + double /api prefix in analytics
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- Auth service used flat env var names (JWT_SECRET, JWT_ACCESS_EXPIRY_SECONDS)
  but @nestjs/config nests them under jwt.* — configService.get() returned
  undefined, so expiresIn was 0 and tokens expired on issue (iat === exp)
- JWT strategy had same bug for secretOrKey
- AnalyticsView passed /api/analytics/... to useApi which already prepends /api,
  resulting in /api/api/analytics/... (404)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:40:32 -05:00
Vantz Stockwell
2ad6a658ca fix: Add missing maps module and scope gitignore rules
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
maps/ gitignore rule was catching backend-nest/src/modules/maps/.
Scoped to /maps/ (root only) so runtime data is still ignored
but source code isn't.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:35:47 -05:00
Vantz Stockwell
d20493d533 feat: Complete NestJS backend scaffold — 22 modules, 39 entities, WebSocket gateway
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Full backend rewrite from Rust/Axum to NestJS/TypeScript.
- 22 feature modules (auth, servers, wipes, maps, plugins, players, console,
  chat, team, notifications, settings, schedules, analytics, alerts, status,
  store, webstore, admin, setup, migration, users, licenses)
- 39 TypeORM entities matching PostgreSQL schema (12 migrations)
- Common infrastructure: JWT/RBAC guards, decorators, exception filter
- NATS service with pub/sub/request-reply
- Socket.IO WebSocket gateway with NATS bridge
- Docker: NestJS Dockerfile + updated docker-compose.yml
- Zero compile errors (npx tsc --noEmit clean)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:29:25 -05:00
Vantz Stockwell
0f8d0dd14f fix: Align NestJS entity columns with service expectations
Resolved 12 TypeScript compilation errors caused by mismatched column names between TypeORM entities and service layer.

Entity changes:
- NotificationsConfig: Renamed email_alerts_enabled → email_enabled, added email_address, standardized notification event column names (notify_on_start, notify_on_stop, notify_on_crash, etc.)
- ScheduledTask: Renamed is_active → is_enabled, added last_run timestamp
- TeamMember: Renamed accepted_at → joined_at to match service expectations
- Role: Added description column for custom role metadata

Service changes:
- JwtStrategy: Updated to reference joined_at instead of accepted_at

All services now compile cleanly against updated entity schemas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:28:20 -05:00
Vantz Stockwell
8cd792eb75 feat: Implement NestJS Auth, Users, and Licenses modules
Complete authentication system with JWT, refresh tokens, and TOTP 2FA.
Auto-generates license keys on registration (CORR-XXXX-XXXX-XXXX format).
JwtStrategy enriches payload with license_id and permissions from roles.
Multi-tenant isolation enforced at license access layer.

Auth Module:
- 9 REST endpoints (login, register, refresh, 2FA setup/verify, profile, password reset)
- Argon2 password hashing, TOTP with QR code generation
- Public endpoints: login, register, forgot-password, reset-password, validate-key
- Authenticated endpoints require JWT Bearer token

Users Module:
- Admin CRUD for user management (requires users.view permission)
- Password fields excluded from all responses

Licenses Module:
- License lookup with owner authorization
- Public key validation endpoint for plugin verification
- License key generation via random hex parts

All DTOs use class-validator, all controllers documented via Swagger.
Custom decorators: @Public(), @CurrentUser(), @RequirePermission().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:13:57 -05:00