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>
This commit is contained in:
Vantz Stockwell
2026-02-15 21:29:25 -05:00
parent 0f8d0dd14f
commit d20493d533
141 changed files with 13552 additions and 4 deletions

View File

@@ -0,0 +1,43 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Index } from 'typeorm';
import { License } from './license.entity';
@Entity('alert_config')
@Index(['license_id'], { unique: true })
export class AlertConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'boolean', default: true })
population_drop_enabled: boolean;
@Column({ type: 'integer', default: 30 })
population_drop_threshold_percent: number;
@Column({ type: 'boolean', default: true })
fps_degradation_enabled: boolean;
@Column({ type: 'integer', default: 30 })
fps_threshold: number;
@Column({ type: 'boolean', default: true })
notify_discord: boolean;
@Column({ type: 'boolean', default: false })
notify_pushbullet: boolean;
@Column({ type: 'boolean', default: false })
notify_email: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,42 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
@Entity('alert_history')
export class AlertHistory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 50 })
alert_type: string;
@Column({ type: 'varchar', length: 20 })
severity: string;
@Column({ type: 'varchar', length: 255 })
title: string;
@Column({ type: 'text' })
message: string;
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;
@Column({ type: 'boolean', default: false })
notified_discord: boolean;
@Column({ type: 'boolean', default: false })
notified_pushbullet: boolean;
@Column({ type: 'boolean', default: false })
notified_email: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
triggered_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,45 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { User } from './user.entity';
@Entity('chat_logs')
@Check(`"channel" IN ('global', 'team', 'server')`)
export class ChatLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 20 })
steam_id: string;
@Column({ type: 'varchar', length: 100 })
player_name: string;
@Column({ type: 'varchar', length: 20, default: 'global' })
channel: string;
@Column({ type: 'text' })
message: string;
@Column({ type: 'boolean', default: false })
flagged: boolean;
@Column({ type: 'uuid', nullable: true })
flagged_by: string | null;
@Column({ type: 'text', nullable: true })
flag_reason: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'flagged_by' })
flagger: User | null;
}

View File

@@ -0,0 +1,16 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('early_access_signups')
export class EarlyAccessSignup {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255, unique: true })
email: string;
@Column({ type: 'varchar', length: 10 })
server_count: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
}

View File

@@ -0,0 +1,40 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique, Check } from 'typeorm';
import { License } from './license.entity';
import { User } from './user.entity';
@Entity('game_admins')
@Unique(['license_id', 'steam_id'])
@Check(`"admin_level" IN ('owner', 'admin', 'moderator')`)
export class GameAdmin {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 20 })
steam_id: string;
@Column({ type: 'varchar', length: 100, default: '' })
display_name: string;
@Column({ type: 'varchar', length: 20, default: 'admin' })
admin_level: string;
@Column({ type: 'jsonb', default: {} })
permissions: Record<string, any>;
@Column({ type: 'uuid' })
added_by: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => User)
@JoinColumn({ name: 'added_by' })
adder: User;
}

View File

@@ -0,0 +1,31 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { Host } from './host.entity';
@Entity('host_billing_records')
@Unique(['host_id', 'billing_month'])
export class HostBillingRecord {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
host_id: string;
@Column({ type: 'date' })
billing_month: Date;
@Column({ type: 'integer' })
active_license_count: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
wholesale_rate_usd: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
total_amount_usd: number;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
generated_at: Date;
@ManyToOne(() => Host, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'host_id' })
host: Host;
}

View File

@@ -0,0 +1,36 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { Host } from './host.entity';
import { License } from './license.entity';
@Entity('host_licenses')
@Unique(['host_id', 'license_id'])
export class HostLicense {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
host_id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 255, nullable: true })
server_identifier: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
customer_email: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
provisioned_at: Date;
@Column({ type: 'timestamptz', nullable: true })
last_seen_at: Date | null;
@ManyToOne(() => Host, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'host_id' })
host: Host;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('hosts')
export class Host {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 200 })
company_name: string;
@Column({ type: 'varchar', length: 255, unique: true })
contact_email: string;
@Column({ type: 'varchar', length: 64, unique: true })
api_key: string;
@Column({ type: 'decimal', precision: 10, scale: 2, default: 6.00 })
wholesale_rate_usd: number;
@Column({ type: 'boolean', default: true })
active: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
}

View File

@@ -0,0 +1,38 @@
export { User } from './user.entity';
export { License } from './license.entity';
export { Role } from './role.entity';
export { TeamMember } from './team-member.entity';
export { ServerConnection } from './server-connection.entity';
export { ServerConfig } from './server-config.entity';
export { GameAdmin } from './game-admin.entity';
export { WipeProfile } from './wipe-profile.entity';
export { WipeSchedule } from './wipe-schedule.entity';
export { WipeHistory } from './wipe-history.entity';
export { MapLibrary } from './map-library.entity';
export { MapRotation } from './map-rotation.entity';
export { PluginRegistry } from './plugin-registry.entity';
export { ScheduledTask } from './scheduled-task.entity';
export { NotificationsConfig } from './notifications-config.entity';
export { ChatLog } from './chat-log.entity';
export { PlayerAction } from './player-action.entity';
export { ServerStats } from './server-stats.entity';
export { ServerStatsHourly } from './server-stats-hourly.entity';
export { PublicSiteConfig } from './public-site-config.entity';
export { PlatformChangelog } from './platform-changelog.entity';
export { MigrationExport } from './migration-export.entity';
export { EarlyAccessSignup } from './early-access-signup.entity';
export { PlayerSession } from './player-session.entity';
export { AlertConfig } from './alert-config.entity';
export { AlertHistory } from './alert-history.entity';
export { Module } from './module.entity';
export { ModulePurchase } from './module-purchase.entity';
export { ModuleInstallation } from './module-installation.entity';
export { PaymentOrder } from './payment-order.entity';
export { WebstoreSubscription } from './webstore-subscription.entity';
export { StoreConfig } from './store-config.entity';
export { StoreCategory } from './store-category.entity';
export { StoreItem } from './store-item.entity';
export { StoreTransaction } from './store-transaction.entity';
export { Host } from './host.entity';
export { HostLicense } from './host-license.entity';
export { HostBillingRecord } from './host-billing-record.entity';

View File

@@ -0,0 +1,46 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { User } from './user.entity';
@Entity('licenses')
@Check(`"status" IN ('active', 'suspended', 'expired', 'revoked')`)
export class License {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 64, unique: true })
license_key: string;
@Column({ type: 'varchar', length: 20, default: 'active' })
status: string;
@Column({ type: 'uuid' })
owner_user_id: string;
@Column({ type: 'varchar', length: 100, nullable: true })
server_name: string | null;
@Column({ type: 'varchar', length: 63, unique: true, nullable: true })
subdomain: string | null;
@Column({ type: 'varchar', length: 255, unique: true, nullable: true })
custom_domain: string | null;
@Column('text', { array: true, default: '{}' })
modules_enabled: string[];
@Column({ type: 'boolean', default: false })
webstore_active: boolean;
@Column({ type: 'varchar', length: 100, nullable: true })
webstore_subscription_id: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', nullable: true })
expires_at: Date | null;
@ManyToOne(() => User, user => user.licenses, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'owner_user_id' })
owner: User;
}

View File

@@ -0,0 +1,46 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
@Entity('map_library')
@Check(`"map_type" IN ('custom', 'procedural')`)
export class MapLibrary {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 255 })
filename: string;
@Column({ type: 'varchar', length: 255 })
display_name: string;
@Column({ type: 'text' })
storage_path: string;
@Column({ type: 'bigint', default: 0 })
file_size_bytes: number;
@Column({ type: 'varchar', length: 20, default: 'custom' })
map_type: string;
@Column({ type: 'integer', nullable: true })
seed: number | null;
@Column({ type: 'integer', nullable: true })
world_size: number | null;
@Column({ type: 'text', nullable: true })
thumbnail_path: string | null;
@Column({ type: 'varchar', length: 64 })
checksum: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
uploaded_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,30 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { License } from './license.entity';
import { MapLibrary } from './map-library.entity';
@Entity('map_rotations')
@Unique(['license_id', 'rotation_order'])
export class MapRotation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid' })
map_id: string;
@Column({ type: 'integer', default: 0 })
rotation_order: number;
@Column({ type: 'boolean', default: true })
is_active: boolean;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => MapLibrary, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'map_id' })
map: MapLibrary;
}

View File

@@ -0,0 +1,39 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { User } from './user.entity';
@Entity('migration_exports')
@Check(`"export_type" IN ('full', 'config_only', 'store_only')`)
export class MigrationExport {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 20, default: 'full' })
export_type: string;
@Column({ type: 'text' })
storage_path: string;
@Column({ type: 'bigint', default: 0 })
file_size_bytes: number;
@Column({ type: 'uuid' })
created_by: string;
@Column({ type: 'timestamptz' })
expires_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator: User;
}

View File

@@ -0,0 +1,34 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique, Check } from 'typeorm';
import { License } from './license.entity';
import { Module } from './module.entity';
@Entity('module_installations')
@Unique(['license_id', 'module_id'])
@Check(`"status" IN ('pending', 'installing', 'installed', 'failed')`)
export class ModuleInstallation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid' })
module_id: string;
@Column({ type: 'varchar', length: 50, default: 'pending' })
status: string;
@Column({ type: 'timestamptz', nullable: true })
installed_at: Date | null;
@Column({ type: 'text', nullable: true })
error_message: string | null;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => Module, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'module_id' })
module: Module;
}

View File

@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { License } from './license.entity';
import { Module } from './module.entity';
@Entity('module_purchases')
@Unique(['license_id', 'module_id'])
export class ModulePurchase {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid' })
module_id: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
purchased_at: Date;
@Column({ type: 'varchar', length: 255, nullable: true })
transaction_id: string | null;
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
amount_paid: number | null;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => Module, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'module_id' })
module: Module;
}

View File

@@ -0,0 +1,40 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('modules')
export class Module {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 100, unique: true })
slug: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'varchar', length: 50, nullable: true })
category: string | null;
@Column({ type: 'decimal', precision: 10, scale: 2 })
price_usd: number;
@Column({ type: 'text', nullable: true })
preview_image_url: string | null;
@Column({ type: 'jsonb', nullable: true })
screenshots: any | null;
@Column({ type: 'jsonb', nullable: true })
features: any | null;
@Column({ type: 'varchar', length: 20 })
version: string;
@Column({ type: 'text', nullable: true })
plugin_file_url: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
}

View File

@@ -0,0 +1,58 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { Module } from './module.entity';
import { License } from './license.entity';
@Entity('payment_orders')
@Check(`"status" IN ('pending', 'completed', 'failed', 'refunded')`)
export class PaymentOrder {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255, unique: true })
order_id: string;
@Column({ type: 'uuid', nullable: true })
module_id: string | null;
@Column({ type: 'uuid', nullable: true })
webstore_subscription_id: string | null;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'decimal', precision: 10, scale: 2 })
amount: number;
@Column({ type: 'varchar', length: 3, default: 'USD' })
currency: string;
@Column({ type: 'varchar', length: 50 })
status: string;
@Column({ type: 'varchar', length: 255, nullable: true })
transaction_id: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
payer_email: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', nullable: true })
completed_at: Date | null;
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any> | null;
@ManyToOne(() => Module, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'module_id' })
module: Module | null;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => License, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'webstore_subscription_id' })
webstore_subscription: License | null;
}

View File

@@ -0,0 +1,23 @@
import { Entity, PrimaryGeneratedColumn, Column, Check } from 'typeorm';
@Entity('platform_changelog')
@Check(`"category" IN ('feature', 'bugfix', 'module', 'security')`)
export class PlatformChangelog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 20 })
version: string;
@Column({ type: 'varchar', length: 200 })
title: string;
@Column({ type: 'text' })
body: string;
@Column({ type: 'varchar', length: 20, default: 'feature' })
category: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
published_at: Date;
}

View File

@@ -0,0 +1,42 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { User } from './user.entity';
@Entity('player_actions')
@Check(`"action_type" IN ('kick', 'ban', 'unban', 'warn', 'note')`)
export class PlayerAction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 20 })
steam_id: string;
@Column({ type: 'varchar', length: 100 })
player_name: string;
@Column({ type: 'varchar', length: 20 })
action_type: string;
@Column({ type: 'text', nullable: true })
reason: string | null;
@Column({ type: 'integer', nullable: true })
duration_minutes: number | null;
@Column({ type: 'uuid' })
performed_by: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => User)
@JoinColumn({ name: 'performed_by' })
performer: User;
}

View File

@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
@Entity('player_sessions')
export class PlayerSession {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 20 })
steam_id: string;
@Column({ type: 'varchar', length: 100 })
player_name: string;
@Column({ type: 'timestamptz' })
session_start: Date;
@Column({ type: 'timestamptz', nullable: true })
session_end: Date | null;
@Column({ type: 'integer', nullable: true })
duration_seconds: number | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,59 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique, Check } from 'typeorm';
import { License } from './license.entity';
@Entity('plugin_registry')
@Unique(['license_id', 'plugin_name'])
@Check(`"source" IN ('umod', 'corrosion_module', 'manual')`)
export class PluginRegistry {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 255 })
plugin_name: string;
@Column({ type: 'varchar', length: 50, nullable: true })
plugin_version: string | null;
@Column({ type: 'varchar', length: 20, default: 'manual' })
source: string;
@Column({ type: 'varchar', length: 255, nullable: true })
umod_slug: string | null;
@Column({ type: 'boolean', default: false })
is_installed: boolean;
@Column({ type: 'boolean', default: false })
is_loaded: boolean;
@Column({ type: 'jsonb', nullable: true })
config_json: Record<string, any> | null;
@Column({ type: 'text', nullable: true })
data_path: string | null;
@Column({ type: 'boolean', default: false })
wipe_on_map: boolean;
@Column({ type: 'boolean', default: false })
wipe_on_bp: boolean;
@Column({ type: 'boolean', default: false })
wipe_on_full: boolean;
@Column({ type: 'boolean', default: false })
never_wipe: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
installed_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,60 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
@Entity('public_site_config')
export class PublicSiteConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', unique: true })
license_id: string;
@Column({ type: 'boolean', default: true })
site_enabled: boolean;
@Column({ type: 'boolean', default: false })
show_on_status_page: boolean;
@Column({ type: 'varchar', length: 255, nullable: true })
steam_connect_url: string | null;
@Column({ type: 'text', nullable: true })
motd: string | null;
@Column('text', { array: true, default: '{}' })
public_mods: string[];
@Column({ type: 'text', nullable: true })
header_image_url: string | null;
@Column({ type: 'varchar', length: 7, default: '#ef4444' })
theme_color: string;
@Column({ type: 'text', nullable: true })
custom_css: string | null;
@Column({ type: 'text', nullable: true })
discord_invite_url: string | null;
@Column({ type: 'boolean', default: true })
show_player_count: boolean;
@Column({ type: 'boolean', default: true })
show_wipe_schedule: boolean;
@Column({ type: 'boolean', default: true })
show_wipe_countdown: boolean;
@Column({ type: 'boolean', default: true })
show_mod_list: boolean;
@Column({ type: 'text', nullable: true })
status_page_description: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,80 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
import { MapLibrary } from './map-library.entity';
@Entity('server_config')
export class ServerConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', unique: true })
license_id: string;
@Column({ type: 'varchar', length: 255, default: '' })
server_name: string;
@Column({ type: 'integer', nullable: true })
max_players: number | null;
@Column({ type: 'integer', nullable: true })
world_size: number | null;
@Column({ type: 'integer', nullable: true })
current_seed: number | null;
@Column({ type: 'uuid', nullable: true })
current_map_id: string | null;
@Column({ type: 'text', nullable: true })
server_description: string | null;
@Column({ type: 'text', nullable: true })
server_url: string | null;
@Column({ type: 'text', nullable: true })
server_header_image: string | null;
@Column('text', { array: true, default: '{}' })
tags: string[];
@Column({ type: 'boolean', default: false })
auto_restart_enabled: boolean;
@Column({ type: 'varchar', length: 100, nullable: true })
auto_restart_cron: string | null;
@Column({ type: 'varchar', length: 50, nullable: true })
auto_restart_timezone: string | null;
@Column({ type: 'boolean', default: true })
crash_recovery_enabled: boolean;
@Column({ type: 'integer', default: 3 })
crash_recovery_max_attempts: number;
@Column({ type: 'integer', default: 10 })
crash_recovery_cooldown_minutes: number;
@Column({ type: 'boolean', default: true })
force_wipe_eligible: boolean;
@Column({ type: 'boolean', default: true })
auto_update_on_force_wipe: boolean;
@Column({ type: 'jsonb', default: {} })
config_overrides: Record<string, any>;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => MapLibrary, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'current_map_id' })
current_map: MapLibrary | null;
}

View File

@@ -0,0 +1,56 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
@Entity('server_connections')
@Check(`"connection_type" IN ('amp', 'pterodactyl', 'bare_metal')`)
@Check(`"connection_status" IN ('connected', 'degraded', 'offline')`)
export class ServerConnection {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', unique: true })
license_id: string;
@Column({ type: 'varchar', length: 20 })
connection_type: string;
@Column({ type: 'text', nullable: true })
panel_api_endpoint: string | null;
@Column({ type: 'text', nullable: true })
panel_api_key_encrypted: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
panel_server_identifier: string | null;
@Column({ type: 'varchar', length: 128, nullable: true })
companion_agent_token: string | null;
@Column({ type: 'timestamptz', nullable: true })
companion_last_seen: Date | null;
@Column({ type: 'timestamptz', nullable: true })
plugin_last_seen: Date | null;
@Column({ type: 'varchar', length: 45, nullable: true })
server_ip: string | null;
@Column({ type: 'integer', nullable: true })
server_port: number | null;
@Column({ type: 'integer', nullable: true })
game_port: number | null;
@Column({ type: 'varchar', length: 20, default: 'offline' })
connection_status: string;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,37 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { License } from './license.entity';
@Entity('server_stats_hourly')
@Unique(['license_id', 'hour'])
export class ServerStatsHourly {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'timestamptz' })
hour: Date;
@Column({ type: 'double precision', default: 0 })
avg_players: number;
@Column({ type: 'integer', default: 0 })
max_players: number;
@Column({ type: 'double precision', default: 0 })
avg_fps: number;
@Column({ type: 'double precision', default: 0 })
min_fps: number;
@Column({ type: 'integer', default: 0 })
avg_entities: number;
@Column({ type: 'double precision', default: 0 })
uptime_percentage: number;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,44 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
import { MapLibrary } from './map-library.entity';
@Entity('server_stats')
export class ServerStats {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'integer', default: 0 })
player_count: number;
@Column({ type: 'integer', default: 0 })
max_players: number;
@Column({ type: 'double precision', default: 0 })
fps: number;
@Column({ type: 'integer', default: 0 })
entity_count: number;
@Column({ type: 'integer', default: 0 })
uptime_seconds: number;
@Column({ type: 'integer', default: 0 })
memory_usage_mb: number;
@Column({ type: 'uuid', nullable: true })
map_id: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
recorded_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => MapLibrary, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'map_id' })
map: MapLibrary | null;
}

View File

@@ -0,0 +1,34 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Unique } from 'typeorm';
import { License } from './license.entity';
@Entity('store_categories')
@Unique(['license_id', 'slug'])
export class StoreCategory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'varchar', length: 100 })
slug: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'integer', default: 0 })
display_order: number;
@Column({ type: 'boolean', default: true })
visible: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,42 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
@Entity('store_config')
export class StoreConfig {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', unique: true })
license_id: string;
@Column({ type: 'varchar', length: 200 })
store_name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'varchar', length: 3, default: 'USD' })
currency: string;
@Column({ type: 'varchar', length: 255, nullable: true })
paypal_client_id: string | null;
@Column({ type: 'text', nullable: true })
paypal_client_secret: string | null;
@Column({ type: 'boolean', default: true })
sandbox_mode: boolean;
@Column({ type: 'boolean', default: false })
enabled: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,54 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { StoreCategory } from './store-category.entity';
@Entity('store_items')
@Check(`"item_type" IN ('kit', 'rank', 'currency', 'command')`)
export class StoreItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid', nullable: true })
category_id: string | null;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'decimal', precision: 10, scale: 2 })
price: number;
@Column({ type: 'text', nullable: true })
image_url: string | null;
@Column({ type: 'varchar', length: 50 })
item_type: string;
@Column({ type: 'jsonb' })
delivery_commands: Record<string, any>;
@Column({ type: 'integer', nullable: true })
limit_per_player: number | null;
@Column({ type: 'boolean', default: true })
enabled: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => StoreCategory, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'category_id' })
category: StoreCategory | null;
}

View File

@@ -0,0 +1,57 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { StoreItem } from './store-item.entity';
@Entity('store_transactions')
@Check(`"status" IN ('pending', 'paid', 'delivered', 'failed', 'refunded')`)
export class StoreTransaction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid', nullable: true })
item_id: string | null;
@Column({ type: 'varchar', length: 20 })
steam_id: string;
@Column({ type: 'varchar', length: 100, nullable: true })
player_name: string | null;
@Column({ type: 'varchar', length: 255, unique: true })
paypal_order_id: string;
@Column({ type: 'varchar', length: 255, nullable: true })
paypal_transaction_id: string | null;
@Column({ type: 'decimal', precision: 10, scale: 2 })
amount: number;
@Column({ type: 'varchar', length: 3, default: 'USD' })
currency: string;
@Column({ type: 'varchar', length: 50 })
status: string;
@Column({ type: 'boolean', default: false })
delivered: boolean;
@Column({ type: 'timestamptz', nullable: true })
delivered_at: Date | null;
@Column({ type: 'varchar', length: 255, nullable: true })
payer_email: string | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => StoreItem, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'item_id' })
item: StoreItem | null;
}

View File

@@ -0,0 +1,45 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { License } from './license.entity';
import { TeamMember } from './team-member.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 255, unique: true })
email: string;
@Column({ type: 'varchar', length: 50, unique: true })
username: string;
@Column({ type: 'text' })
password_hash: string;
@Column({ type: 'text', nullable: true })
totp_secret: string | null;
@Column({ type: 'boolean', default: false })
totp_enabled: boolean;
@Column('text', { array: true, nullable: true })
backup_codes: string[] | null;
@Column({ type: 'boolean', default: false })
email_verified: boolean;
@Column({ type: 'boolean', default: false })
is_super_admin: boolean;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', nullable: true })
last_login_at: Date | null;
@OneToMany(() => License, license => license.owner)
licenses: License[];
@OneToMany(() => TeamMember, teamMember => teamMember.user)
team_memberships: TeamMember[];
}

View File

@@ -0,0 +1,37 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
@Entity('webstore_subscriptions')
@Check(`"status" IN ('active', 'cancelled', 'suspended', 'past_due')`)
export class WebstoreSubscription {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid', unique: true })
license_id: string;
@Column({ type: 'varchar', length: 255, unique: true })
paypal_subscription_id: string;
@Column({ type: 'varchar', length: 100 })
plan_id: string;
@Column({ type: 'varchar', length: 50 })
status: string;
@Column({ type: 'timestamptz' })
current_period_start: Date;
@Column({ type: 'timestamptz' })
current_period_end: Date;
@Column({ type: 'timestamptz', nullable: true })
cancelled_at: Date | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,78 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { WipeSchedule } from './wipe-schedule.entity';
import { WipeProfile } from './wipe-profile.entity';
import { MapLibrary } from './map-library.entity';
@Entity('wipe_history')
@Check(`"wipe_type" IN ('map', 'blueprint', 'full')`)
@Check(`"trigger_type" IN ('scheduled', 'manual', 'force_wipe')`)
@Check(`"status" IN ('pending', 'pre_wipe', 'wiping', 'post_wipe', 'success', 'failed', 'rolled_back')`)
export class WipeHistory {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid', nullable: true })
wipe_schedule_id: string | null;
@Column({ type: 'uuid' })
wipe_profile_id: string;
@Column({ type: 'varchar', length: 20 })
wipe_type: string;
@Column({ type: 'varchar', length: 20 })
trigger_type: string;
@Column({ type: 'varchar', length: 20, default: 'pending' })
status: string;
@Column({ type: 'timestamptz', nullable: true })
started_at: Date | null;
@Column({ type: 'timestamptz', nullable: true })
completed_at: Date | null;
@Column({ type: 'varchar', length: 255, nullable: true })
map_used_legacy: string | null;
@Column({ type: 'uuid', nullable: true })
map_id: string | null;
@Column('text', { array: true, nullable: true })
plugins_wiped: string[] | null;
@Column('text', { array: true, nullable: true })
plugins_preserved: string[] | null;
@Column({ type: 'text', nullable: true })
backup_reference: string | null;
@Column({ type: 'text', nullable: true })
error_message: string | null;
@Column({ type: 'jsonb', default: '[]' })
execution_log: any[];
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => WipeSchedule, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'wipe_schedule_id' })
wipe_schedule: WipeSchedule | null;
@ManyToOne(() => WipeProfile)
@JoinColumn({ name: 'wipe_profile_id' })
wipe_profile: WipeProfile;
@ManyToOne(() => MapLibrary, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'map_id' })
map: MapLibrary | null;
}

View File

@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { License } from './license.entity';
@Entity('wipe_profiles')
export class WipeProfile {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'varchar', length: 100 })
profile_name: string;
@Column({ type: 'text', nullable: true })
description: string | null;
@Column({ type: 'jsonb', nullable: true })
pre_wipe_config: Record<string, any> | null;
@Column({ type: 'jsonb', nullable: true })
post_wipe_config: Record<string, any> | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
updated_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
}

View File

@@ -0,0 +1,48 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Check } from 'typeorm';
import { License } from './license.entity';
import { WipeProfile } from './wipe-profile.entity';
@Entity('wipe_schedules')
@Check(`"wipe_type" IN ('map', 'blueprint', 'full')`)
export class WipeSchedule {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
license_id: string;
@Column({ type: 'uuid' })
wipe_profile_id: string;
@Column({ type: 'varchar', length: 100 })
schedule_name: string;
@Column({ type: 'varchar', length: 20 })
wipe_type: string;
@Column({ type: 'varchar', length: 100 })
cron_expression: string;
@Column({ type: 'varchar', length: 50, default: 'America/New_York' })
timezone: string;
@Column({ type: 'boolean', default: false })
wipe_blueprints: boolean;
@Column({ type: 'boolean', default: true })
is_active: boolean;
@Column({ type: 'timestamptz', nullable: true })
next_scheduled_run: Date | null;
@Column({ type: 'timestamptz', default: () => 'NOW()' })
created_at: Date;
@ManyToOne(() => License, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'license_id' })
license: License;
@ManyToOne(() => WipeProfile, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'wipe_profile_id' })
wipe_profile: WipeProfile;
}