You've already forked Atomcms-edit
Compare commits
67 Commits
769abddeca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5288dbd917 | |||
| 5d58e12fc7 | |||
| a7bd30fd34 | |||
| 430d502e2a | |||
| 0b6f14d5bf | |||
| b6fb43cba1 | |||
| 4094f0fb14 | |||
| 6eeb85fcf2 | |||
| 9bb2e4246b | |||
| 4ba236249d | |||
| 392bb96ea0 | |||
| ff04270b41 | |||
| 482996429d | |||
| 4a4110a478 | |||
| 2f363eb106 | |||
| 618615a8b1 | |||
| ef559ce64b | |||
| 1db80e76fe | |||
| 51e3876ed9 | |||
| 483342602f | |||
| ccbd200464 | |||
| 834908df9f | |||
| 434f58d5aa | |||
| 4c085ac7eb | |||
| bbefdce290 | |||
| 5b8d1cda7d | |||
| ea2160132a | |||
| 2ac1264b93 | |||
| 0f0de43da5 | |||
| 1ee60405ce | |||
| 78704190bc | |||
| f109b1f764 | |||
| af2f76d9db | |||
| 6f11cae3ca | |||
| 16b2f5bbb7 | |||
| 6e437be6d1 | |||
| 4f4f40ac99 | |||
| be6a578f5e | |||
| 4d8d22f40a | |||
| 8a324b3082 | |||
| f7fe86efeb | |||
| 36887244e6 | |||
| 9b5c655c68 | |||
| b2bb1811d0 | |||
| 4b6872e5e0 | |||
| 66cbd46f37 | |||
| 889cec60e9 | |||
| 8ce3fcca85 | |||
| b8a15c8412 | |||
| 82d00ad11d | |||
| c468040792 | |||
| 4487084614 | |||
| e96e2a0fd3 | |||
| 4378144f45 | |||
| 59188a5f2c | |||
| ff364992ca | |||
| 2997486662 | |||
| f76f30e88f | |||
| 1f04979ffe | |||
| 1f9af5279a | |||
| 7814176358 | |||
| 56616d084c | |||
| e34300a8a1 | |||
| e2422e9a16 | |||
| a65db47c85 | |||
| af2c79ac59 | |||
| e754858c84 |
-113
@@ -1,113 +0,0 @@
|
||||
# AtomCMS Environment Configuration
|
||||
# Works on Linux (Nginx/Apache) and Windows (XAMPP/IIS)
|
||||
# Copy to .env and adjust values for your setup
|
||||
|
||||
# ==========================================
|
||||
# APPLICATION
|
||||
# ==========================================
|
||||
APP_NAME="AtomCMS"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
# ==========================================
|
||||
# DATABASE - Works on Linux & Windows
|
||||
# ==========================================
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=127.0.0.1 # Linux: 127.0.0.1, Windows XAMPP: localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=habbo
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# Windows IIS (SQL Server) - uncomment if using IIS/SQL Server:
|
||||
# DB_CONNECTION=sqlsrv
|
||||
# DB_HOST=(local)
|
||||
# DB_DATABASE=habbo
|
||||
# DB_USERNAME=sa
|
||||
# DB_PASSWORD=your_password
|
||||
|
||||
# ==========================================
|
||||
# SESSION & CACHE (Windows compatible)
|
||||
# ==========================================
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# ==========================================
|
||||
# MAIL (Windows compatible)
|
||||
# ==========================================
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# ==========================================
|
||||
# REAL-TIME & PUSHER
|
||||
# ==========================================
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
# ==========================================
|
||||
# SECURITY
|
||||
# ==========================================
|
||||
# Cloudflare Turnstile (optional)
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Google Recaptcha (optional)
|
||||
NOCAPTCHA_SECRET=
|
||||
NOCAPTCHA_SITEKEY=
|
||||
|
||||
# ==========================================
|
||||
# WEBHOOKS
|
||||
# ==========================================
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
# ==========================================
|
||||
# NITRO CLIENT PATHS
|
||||
# ==========================================
|
||||
# Linux: /var/www/atomcms/nitro-client
|
||||
# Windows: C:\path\to\atomcms\nitro-client
|
||||
NITRO_CLIENT_PATH=/var/www/atomcms/nitro-client
|
||||
NITRO_RENDERER_PATH=/var/www/atomcms/nitro-renderer
|
||||
NITRO_BUILD_PATH=/var/www/atomcms/nitro-client/dist
|
||||
NITRO_WEBROOT=/Client
|
||||
GAMEDATA_PATH=/var/Gamedata
|
||||
|
||||
# ==========================================
|
||||
# LOGGING
|
||||
# ==========================================
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# ==========================================
|
||||
# BROADCAST & FILESYSTEMS
|
||||
# ==========================================
|
||||
BROADCAST_DRIVER=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# ==========================================
|
||||
# LOCALIZATION
|
||||
# ==========================================
|
||||
APP_LOCALE=nl
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=nl_NL
|
||||
|
||||
# ==========================================
|
||||
# SECURITY SETTINGS
|
||||
# ==========================================
|
||||
APP_SECURE=true
|
||||
SESSION_DOMAIN=localhost
|
||||
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
|
||||
@@ -0,0 +1,109 @@
|
||||
APP_NAME="YourHotel"
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://yourhotel.nl
|
||||
|
||||
# --- LOGGING ---
|
||||
LOG_CHANNEL=daily
|
||||
LOG_MAX_FILES=14
|
||||
LOG_LEVEL=error
|
||||
|
||||
# --- DATABASE ---
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=habbo
|
||||
DB_USERNAME=cms
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_STRICT_MODE=false
|
||||
DB_ENGINE=InnoDB
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
DB_DUMP_BINARY_PATH=/usr/bin/
|
||||
|
||||
# --- CACHE & SESSION (Redis recommended on Linux) ---
|
||||
BROADCAST_DRIVER=redis
|
||||
CACHE_STORE=redis
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=
|
||||
REDIS_PORT=6379
|
||||
|
||||
REDIS_DB=0
|
||||
REDIS_CACHE_DB=1
|
||||
REDIS_QUEUE_DB=2
|
||||
|
||||
REDIS_PERSISTENT=true
|
||||
REDIS_READ_TIMEOUT=1.0
|
||||
REDIS_CONNECT_TIMEOUT=1.0
|
||||
|
||||
# --- FILAMENT ---
|
||||
FILAMENT_FILESYSTEM_DISK=public
|
||||
FILAMENT_AUTH_GUARD=web
|
||||
|
||||
# --- MAIL ---
|
||||
MAIL_MAILER=log
|
||||
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# --- SESSION SECURITY ---
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_DOMAIN=.yourhotel.nl
|
||||
SESSION_SECURE_COOKIE=true
|
||||
SESSION_HTTP_ONLY=true
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_COOKIE=__secure_session
|
||||
|
||||
# --- SANCTUM ---
|
||||
SANCTUM_STATEFUL_DOMAINS=yourhotel.nl,www.yourhotel.nl,localhost:3000
|
||||
|
||||
# --- EMULATOR ---
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
RCON_HOST=127.0.0.1
|
||||
RCON_PORT=3001
|
||||
EMULATOR_IP=127.0.0.1
|
||||
EMULATOR_PORT=3000
|
||||
|
||||
# --- NITRO UPDATE (for update-Nitrov3.sh) ---
|
||||
NITRO_EMULATOR_PATH=/var/www/emulator
|
||||
NITRO_EMULATOR_SERVICE=emulator
|
||||
NITRO_DB_HOST=127.0.0.1
|
||||
NITRO_DB_PORT=3306
|
||||
NITRO_DB_NAME=habbo
|
||||
NITRO_DB_USER=root
|
||||
NITRO_DB_PASS=
|
||||
NITRO_SQL_DIR=/var/www/emulator/Database Updates
|
||||
NITRO_BACKUP_DIR=/var/www/emulator/Database Updates/backups
|
||||
NITRO_GAMEDATA_DIR=/var/www/Gamedata/config
|
||||
NITRO_CLIENT_DIR=/var/www/Nitro-V3/public/configuration
|
||||
NITRO_CLIENT_SRC=/var/www/Nitro-V3
|
||||
NITRO_RENDERER_SRC=/var/www/Nitro_Render_V3
|
||||
|
||||
# --- CORS ---
|
||||
CORS_ALLOWED_ORIGINS=https://yourhotel.nl,https://www.yourhotel.nl,http://localhost:3000
|
||||
|
||||
# --- STAFF DEFAULTS ---
|
||||
MIN_STAFF_RANK=7
|
||||
MIN_STAFF_RANK_LOGIN=3
|
||||
DEFAULT_AVATAR_LOOK=hr-100-61.hd-180-1.ch-210-66
|
||||
|
||||
# --- CDN URLs (override if self-hosting) ---
|
||||
FANCYBOX_JS_URL=https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.umd.js
|
||||
FANCYBOX_CSS_URL=https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.css
|
||||
SWEETALERT2_JS_URL=//cdn.jsdelivr.net/npm/sweetalert2@11
|
||||
ALPINE_JS_URL=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js
|
||||
FONTSOURCE_INTER_CSS_URL=https://cdn.jsdelivr.net/npm/@fontsource/inter@4.x/400-700.css
|
||||
FONTAWESOME_CSS_URL=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.2.0/css/all.min.css
|
||||
HTML2CANVAS_JS_URL=https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.3/html2canvas.min.js
|
||||
|
||||
# --- LOCALIZATION ---
|
||||
APP_LOCALE=nl
|
||||
FORCE_HTTPS=true
|
||||
PASSWORD_RESET_TOKEN_TIME=15
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
APP_NAME="YourHotel"
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
|
||||
# --- LOGGING ---
|
||||
LOG_CHANNEL=daily
|
||||
LOG_MAX_FILES=14
|
||||
LOG_LEVEL=error
|
||||
|
||||
# --- DATABASE ---
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=habbo
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_STRICT_MODE=false
|
||||
DB_ENGINE=InnoDB
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
|
||||
# --- CACHE & SESSION (File-based for Windows compat) ---
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_STORE=file
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# --- MAIL ---
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# --- SESSION SECURITY ---
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_DOMAIN=localhost
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_HTTP_ONLY=true
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_COOKIE=__secure_session
|
||||
|
||||
# --- EMULATOR ---
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
RCON_HOST=127.0.0.1
|
||||
RCON_PORT=3001
|
||||
EMULATOR_IP=127.0.0.1
|
||||
EMULATOR_PORT=3000
|
||||
|
||||
# --- CORS ---
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||||
|
||||
# --- STAFF DEFAULTS ---
|
||||
MIN_STAFF_RANK=7
|
||||
MIN_STAFF_RANK_LOGIN=3
|
||||
DEFAULT_AVATAR_LOOK=hr-100-61.hd-180-1.ch-210-66
|
||||
|
||||
# --- CDN URLs (override if self-hosting) ---
|
||||
FANCYBOX_JS_URL=https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.umd.js
|
||||
FANCYBOX_CSS_URL=https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.css
|
||||
SWEETALERT2_JS_URL=//cdn.jsdelivr.net/npm/sweetalert2@11
|
||||
ALPINE_JS_URL=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js
|
||||
FONTSOURCE_INTER_CSS_URL=https://cdn.jsdelivr.net/npm/@fontsource/inter@4.x/400-700.css
|
||||
FONTAWESOME_CSS_URL=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.2.0/css/all.min.css
|
||||
HTML2CANVAS_JS_URL=https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.3/html2canvas.min.js
|
||||
|
||||
# --- LOCALIZATION ---
|
||||
APP_LOCALE=nl
|
||||
FORCE_HTTPS=false
|
||||
PASSWORD_RESET_TOKEN_TIME=15
|
||||
|
||||
# --- NITRO PATHS (Windows) ---
|
||||
NITRO_CLIENT_PATH=C:\Nitro-V3
|
||||
NITRO_RENDERER_PATH=C:\Nitro_Render_V3
|
||||
NITRO_BUILD_PATH=C:\Nitro-V3\dist
|
||||
NITRO_WEBROOT=/Client
|
||||
GAMEDATA_PATH=C:\Gamedata
|
||||
@@ -1,64 +0,0 @@
|
||||
APP_NAME="Epicnabbo Hotel"
|
||||
APP_ENV=production
|
||||
APP_KEY=base64:YOUR_APP_KEY_HERE
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://epicnabbo.nl
|
||||
|
||||
# --- LOGGING ---
|
||||
LOG_CHANNEL=daily
|
||||
LOG_MAX_FILES=14
|
||||
LOG_LEVEL=error
|
||||
|
||||
# --- DATABASE ---
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=YOUR_DB_HOST
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=YOUR_DB_NAME
|
||||
DB_USERNAME=YOUR_DB_USER
|
||||
DB_PASSWORD=YOUR_DB_PASSWORD
|
||||
DB_STRICT_MODE=true
|
||||
DB_ENGINE=InnoDB
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
DB_DUMP_BINARY_PATH=/usr/bin/
|
||||
|
||||
# --- REDIS TURBO ---
|
||||
BROADCAST_DRIVER=redis
|
||||
CACHE_STORE=redis
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
REDIS_DB=0
|
||||
REDIS_CACHE_DB=1
|
||||
REDIS_QUEUE_DB=2
|
||||
|
||||
REDIS_PERSISTENT=true
|
||||
REDIS_READ_TIMEOUT=1.0
|
||||
REDIS_CONNECT_TIMEOUT=1.0
|
||||
|
||||
# --- SESSION SECURITY ---
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_DOMAIN=.epicnabbo.nl
|
||||
SESSION_SECURE_COOKIE=true
|
||||
SESSION_HTTP_ONLY=true
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_COOKIE=__epicnabbo_secure_session
|
||||
|
||||
# --- BEVEILIGING & EMULATOR ---
|
||||
TURNSTILE_SITE_KEY=YOUR_TURNSTILE_SITE_KEY
|
||||
TURNSTILE_SECRET_KEY=YOUR_TURNSTILE_SECRET_KEY
|
||||
RCON_HOST=127.0.0.1
|
||||
RCON_PORT=3001
|
||||
EMULATOR_IP=127.0.0.1
|
||||
EMULATOR_PORT=3000
|
||||
|
||||
# --- EXTRA HARDENING ---
|
||||
APP_LOCALE=nl
|
||||
FORCE_HTTPS=true
|
||||
PASSWORD_RESET_TOKEN_TIME=15
|
||||
@@ -1,87 +0,0 @@
|
||||
# Windows/XAMPP/IIS .env configuration
|
||||
# Copy this to .env and adjust the values for your Windows setup
|
||||
|
||||
# Application
|
||||
APP_NAME="Epicnabbo"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:EXAMPLE_KEY_CHANGE_ME
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
# Database - Windows/XAMPP Settings
|
||||
# For XAMPP use: DB_HOST=localhost
|
||||
# For IIS use: DB_HOST=127.0.0.1 or localhost
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=habbo
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# For Windows IIS with named pipes, use:
|
||||
# DB_CONNECTION=sqlsrv
|
||||
# DB_HOST=(local)
|
||||
# DB_DATABASE=habbo
|
||||
# DB_USERNAME=sa
|
||||
# DB_PASSWORD=your_password
|
||||
|
||||
# Session (Windows compatible)
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
# Cache (Windows compatible)
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
# Mail (Windows compatible - use Mailtrap for testing)
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS="noreply@epicnabbo.nl"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# Pusher (for real-time features)
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
# Cloudflare Turnstile (optional)
|
||||
TURNSTILE_SITE_KEY=
|
||||
TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Google Recaptcha (optional)
|
||||
NOCAPTCHA_SECRET=
|
||||
NOCAPTCHA_SITEKEY=
|
||||
|
||||
# Discord Webhook (optional)
|
||||
DISCORD_WEBHOOK_URL=
|
||||
|
||||
# Nitro Client Path (Windows paths use backslashes or forward slashes)
|
||||
NITRO_CLIENT_PATH=C:\path\to\nitro-client
|
||||
NITRO_RENDERER_PATH=C:\path\to\nitro-renderer
|
||||
|
||||
# Logging
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Broadcast & Filesystems (Windows compatible)
|
||||
BROADCAST_DRIVER=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# Advanced (usually no changes needed)
|
||||
APP_LOCALE=nl
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_LOCALE=nl
|
||||
APP_FAKER_LOCALE=nl_NL
|
||||
|
||||
# Security
|
||||
APP_SECURE=true
|
||||
SESSION_DOMAIN=localhost
|
||||
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
|
||||
+33
-26
@@ -1,42 +1,49 @@
|
||||
# Negeer wachtwoorden en database-instellingen
|
||||
# --- Environment ---
|
||||
.env
|
||||
.env.backup
|
||||
.env.testing
|
||||
config.php
|
||||
wp-config.php
|
||||
/uploads/
|
||||
/temp/
|
||||
*.log
|
||||
.env.production
|
||||
|
||||
# Geen zware afhankelijkheden pushen
|
||||
# --- Dependencies ---
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# UITZONDERING: Vertaalbestanden in lang/vendor wel pushen
|
||||
/lang/vendor/
|
||||
# --- Build artifacts (hashed filenames) ---
|
||||
public/build/assets/
|
||||
public/build/hot
|
||||
|
||||
# Systeembestanden (Mac/Windows troep)
|
||||
# --- Storage (uploaded files, cache, logs, sessions) ---
|
||||
storage/app/livewire-tmp/
|
||||
storage/app/public/
|
||||
storage/clockwork/
|
||||
storage/debugbar/
|
||||
storage/framework/views/
|
||||
storage/framework/cache/
|
||||
storage/framework/sessions/
|
||||
storage/framework/testing/
|
||||
storage/logs/
|
||||
|
||||
# --- IDE & OS ---
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# VPS automation scripts (niet pushen naar GitLab)
|
||||
watch.sh
|
||||
check-updates.sh
|
||||
|
||||
# Cache bestanden (niet pushen naar GitLab)
|
||||
/storage/framework/views/
|
||||
/storage/framework/cache/
|
||||
/storage/framework/sessions/
|
||||
/storage/logs/
|
||||
/storage/debugbar/rr
|
||||
# --- Config (no default .env push) ---
|
||||
config.php
|
||||
.rr.yaml
|
||||
|
||||
# Lockfiles (kies 1 package manager)
|
||||
package-lock.json
|
||||
|
||||
# Overgebleven test/temp bestanden
|
||||
# --- Tests & Temp ---
|
||||
ci_test.txt
|
||||
cookies.txt
|
||||
.phpunit.result.cache
|
||||
|
||||
# GitHub workflows (pushen naar GitLab)
|
||||
!/.github/workflows/
|
||||
# --- Lock files (yarn.lock is tracked, package-lock.json is not) ---
|
||||
package-lock.json
|
||||
|
||||
# --- Scripts (local-only) ---
|
||||
watch.sh
|
||||
check-updates.sh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright 2023 ObjectRetros
|
||||
Copyright 2026 Remco (Epicnabbo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,480 +1,329 @@
|
||||
# AtomCMS — Remco Epicnabbo Edition
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/9ePNdJ4.png" alt="Atom CMS" width="200"/>
|
||||
[](https://discord.gg/pP6HyZedAj)
|
||||
[](https://laravel.com)
|
||||
[](https://php.net)
|
||||
[](#)
|
||||
|
||||
### The Ultimate Retro Hotel CMS
|
||||
A modern Habbo retro CMS powered by Laravel 13, Filament 5, React 19, and Nitro. Forked and maintained by Remco (Epicnabbo).
|
||||
|
||||
**Modern • Fast • Self-Repairing**
|
||||
---
|
||||
|
||||
[](https://discord.gg/pP6HyZedAj)
|
||||
[](https://laravel.com)
|
||||
[](https://php.net)
|
||||
[](https://github.com/atom-retros/atomcms)
|
||||
## What's New in V3
|
||||
|
||||
</div>
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Commandocentrum** | Central admin dashboard with Nitro, emulator & hotel monitoring |
|
||||
| **Nitro V3 Update System** | Auto-update emulator, Nitro client & renderer via CLI (Linux `.env`) |
|
||||
| **Configurable Paths** | 13 paths fully adjustable via `.env` (no database needed) |
|
||||
| **Emulator Control** | Start, stop, restart & check status from the admin panel |
|
||||
| **Live Monitoring** | Online users, emulator status, DB status, server load, diagnostics |
|
||||
| **Hotel Alerts** | Send messages to all online users in real-time |
|
||||
| **Emulator Log Viewer** | Live logs directly in the browser |
|
||||
| **Clothing Sync** | Sync catalog clothing from FigureMap with one click |
|
||||
| **Social Login** | OAuth login via Google, Discord & GitHub |
|
||||
| **Notification Settings** | Email & Discord webhook alerts with rank filtering |
|
||||
| **Staff Activity Log** | Full audit trail of all housekeeping actions |
|
||||
| **Bulletproof Installation** | 12-step guide for Ubuntu 26.04 with Redis, SSL, firewall & PHP tuning |
|
||||
| **PHP 8.5 + Ubuntu 26.04** | Fully compatible with the latest PHP and Ubuntu LTS |
|
||||
| **Dual .env System** | Separate configs for Linux (Redis) and Windows (file-based) |
|
||||
| **XAMPP Blocked** | Explicitly unsupported — we prioritise security |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://git.your-server.com/remco/Atomcms-edit.git /var/www/atomcms
|
||||
cd /var/www/atomcms
|
||||
cp .env.example.linux .env
|
||||
php artisan key:generate
|
||||
# Edit .env with your DB credentials, then:
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php artisan migrate --seed
|
||||
yarn install && yarn build:all
|
||||
```
|
||||
|
||||
> **Full installation guide** → `docs/INSTALL.md` or scroll down to [Installation](#installation-ubuntu-2604)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Radio Station
|
||||
- DJ applications & rank system
|
||||
- Live DJ sessions with schedule
|
||||
- Song requests & voting
|
||||
- Shoutbox
|
||||
- Listener points & leaderboard
|
||||
- Contests & giveaways
|
||||
- Radio banners & history
|
||||
| Module | What it does |
|
||||
|--------|-------------|
|
||||
| **Commandocentrum** | Nitro V3 one-click updater, emulator start/stop/restart, hotel alerts, live monitoring, log viewer, clothing sync, social login (Google/Discord/GitHub) |
|
||||
| **Radio** | DJ apps, live sessions, song requests, shoutbox, leaderboard, contests |
|
||||
| **Shop** | Product catalog, virtual currency, vouchers, PayPal |
|
||||
| **Community** | Articles, photo gallery, leaderboard, teams, rare values, badge lottery |
|
||||
| **Users** | Public profiles, 2FA, referrals, session logs |
|
||||
| **Help** | Ticket system, FAQ, rules |
|
||||
| **Filament Admin** | Users, bans, radio, shop, articles, emulator settings/texts/catalog, chatlogs, word filters, permissions, navigation |
|
||||
| **Themes** | Atom (light) & Dusk (dark) |
|
||||
|
||||
### Shop
|
||||
- Product catalog with categories
|
||||
- Virtual currency purchases
|
||||
- Voucher/promo codes
|
||||
- PayPal integration
|
||||
- Order history
|
||||
---
|
||||
|
||||
### Community
|
||||
- Articles with comments, reactions & tags
|
||||
- Photo gallery
|
||||
- Leaderboard
|
||||
- Teams & team applications
|
||||
- Staff page & staff applications
|
||||
- Rare values tracker
|
||||
- Badge draw/lottery system
|
||||
## Nitro V3 Update (Linux-only)
|
||||
|
||||
### Hotel Client
|
||||
- Nitro (HTML5) client support
|
||||
- Flash client support
|
||||
- FindRetros integration
|
||||
- VPN/proxy checker
|
||||
> ⚠️ **CLI only.** The web UI button has been removed. The script is configured via `.env` variables.
|
||||
|
||||
### User System
|
||||
- Public profiles with guestbook
|
||||
- Two-Factor Authentication (2FA)
|
||||
- Referral system with rewards
|
||||
- Account & password settings
|
||||
- Session logs
|
||||
**What it does:** `git pull` emulator → DB backup → SQL imports → Maven build → `git pull` Nitro_Render_V3 + Nitro-V3 → `yarn build` → sync Gamedata → cleanup → restart emulator.
|
||||
|
||||
### Help Center
|
||||
- Support ticket system with replies & status management
|
||||
- Website rules
|
||||
- FAQ with categories
|
||||
**Usage:**
|
||||
|
||||
### Themes
|
||||
- **Atom** — Default light theme
|
||||
- **Dusk** — Dark theme
|
||||
```bash
|
||||
# Make sure .env contains all NITRO_* variables (see .env.example.linux)
|
||||
cd /var/www/atomcms
|
||||
bash update-Nitrov3.sh
|
||||
```
|
||||
|
||||
### Filament Admin Panel
|
||||
- User & ban management
|
||||
- Radio management (applications, schedules, ranks, shouts, history, banners)
|
||||
- Shop & order management with charts
|
||||
- Article & tag management
|
||||
- Emulator settings, texts & catalog editors
|
||||
- Chatlog viewer (rooms & private messages)
|
||||
- Word filters & moderation tools
|
||||
- Housekeeping permissions
|
||||
- Website navigation & settings
|
||||
- Camera/photo management
|
||||
**Configurable via `.env`:**
|
||||
|
||||
### API Endpoints
|
||||
- `GET /api/user/{username}` — Fetch user data
|
||||
- `GET /api/online-users` — Online users list
|
||||
- `GET /api/online-count` — Online user count
|
||||
- `GET /api/radio/current-dj` — Current DJ info
|
||||
- `GET /api/radio/now-playing` — Current song
|
||||
- `GET /api/radio/listeners` — Listener count
|
||||
- `GET /api/radio/shouts` — Recent shouts
|
||||
- `GET /api/radio/points` — Points data
|
||||
- `GET /api/radio/points/leaderboard` — Points leaderboard
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `NITRO_EMULATOR_PATH` | `/var/www/emulator` | Emulator root directory |
|
||||
| `NITRO_EMULATOR_SERVICE` | `emulator` | Systemd service name |
|
||||
| `NITRO_DB_HOST` | `127.0.0.1` | Database host |
|
||||
| `NITRO_DB_PORT` | `3306` | Database port |
|
||||
| `NITRO_DB_NAME` | `habbo` | Database name |
|
||||
| `NITRO_DB_USER` | `root` | Database user |
|
||||
| `NITRO_DB_PASS` | — | Database password |
|
||||
| `NITRO_SQL_DIR` | `{emulator}/Database Updates` | SQL updates directory |
|
||||
| `NITRO_BACKUP_DIR` | `{emulator}/Database Updates/backups` | Backup directory |
|
||||
| `NITRO_GAMEDATA_DIR` | `/var/www/Gamedata/config` | Gamedata config directory |
|
||||
| `NITRO_CLIENT_DIR` | `{nitro}/public/configuration` | Nitro client config directory |
|
||||
| `NITRO_CLIENT_SRC` | `/var/www/Nitro-V3` | Nitro-V3 source directory |
|
||||
| `NITRO_RENDERER_SRC` | `/var/www/Nitro_Render_V3` | Nitro Render V3 source directory |
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Component | Requirement |
|
||||
| ------------- | ------------------------------- |
|
||||
| **PHP** | 8.1 or higher |
|
||||
| Component | Version |
|
||||
|-----------|---------|
|
||||
| **PHP** | 8.5+ |
|
||||
| **Database** | MariaDB 10.6+ or MySQL 8.0+ |
|
||||
| **Web Server**| Apache (mod_rewrite) or Nginx |
|
||||
| **Node.js** | 20 or higher |
|
||||
| **Yarn** | 1.22+ or 4.x (Berry) |
|
||||
| **Web Server** | Nginx or Apache |
|
||||
| **Node.js** | 20+ |
|
||||
| **Yarn** | 1.22+ |
|
||||
| **Composer** | 2.x |
|
||||
| **Redis** | Recommended (Linux) |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Environment Files
|
||||
|
||||
### Linux (Ubuntu / Debian)
|
||||
| File | Use | Cache | DB |
|
||||
|------|-----|-------|----|
|
||||
| `docs/INSTALL.md` | Step-by-step setup guide | — | — |
|
||||
| `.env.example.linux` | Linux production | Redis | MariaDB |
|
||||
| `.env.example.windows` | Windows development | File | MySQL |
|
||||
|
||||
```bash
|
||||
cp .env.example.linux .env
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
> ⚠️ **XAMPP is not supported.** Extremely unsafe for production.
|
||||
|
||||
---
|
||||
|
||||
## Installation (Ubuntu 26.04)
|
||||
|
||||
```bash
|
||||
# 1. System dependencies
|
||||
sudo apt update
|
||||
sudo apt install -y git curl wget unzip nginx mariadb-server \
|
||||
php8.3 php8.3-cli php8.3-fpm php8.3-mysql php8.3-xml \
|
||||
php8.3-mbstring php8.3-curl php8.3-zip php8.3-bcmath \
|
||||
php8.3-gd php8.3-sockets php8.3-intl
|
||||
sudo apt install -y git curl wget unzip nginx mariadb-server redis-server \
|
||||
php8.5 php8.5-{cli,fpm,mysql,xml,mbstring,curl,zip,bcmath,gd,sockets,intl} \
|
||||
build-essential
|
||||
|
||||
# 2. Install Composer
|
||||
# 2. Composer
|
||||
curl -sS https://getcomposer.org/installer | php
|
||||
sudo mv composer.phar /usr/local/bin/composer
|
||||
|
||||
# 3. Install Node.js & Yarn
|
||||
# 3. Node.js + Yarn
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
sudo corepack enable
|
||||
corepack install -g yarn@latest
|
||||
|
||||
# 4. Clone the project
|
||||
git clone https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git /var/www/atomcms
|
||||
# 4. Secure MariaDB
|
||||
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_root_password'; FLUSH PRIVILEGES;"
|
||||
|
||||
# 5. Clone
|
||||
git clone https://git.your-server.com/remco/Atomcms-edit.git /var/www/atomcms
|
||||
cd /var/www/atomcms
|
||||
|
||||
# 5. Configure environment
|
||||
cp .env.example .env
|
||||
# 6. Configure
|
||||
cp .env.example.linux .env
|
||||
# EDIT .env first: set DB_PASSWORD, APP_URL, SESSION_DOMAIN
|
||||
nano .env
|
||||
php artisan key:generate
|
||||
# Edit .env with your database credentials:
|
||||
# DB_DATABASE=habbo
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=yourpassword
|
||||
|
||||
# 6. Install PHP dependencies
|
||||
# 7. Create database + user
|
||||
sudo mysql -e "CREATE DATABASE IF NOT EXISTS habbo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
sudo mysql -e "CREATE USER IF NOT EXISTS 'cms'@'localhost' IDENTIFIED BY 'your_db_password';"
|
||||
sudo mysql -e "GRANT ALL ON habbo.* TO 'cms'@'localhost'; FLUSH PRIVILEGES;"
|
||||
|
||||
# 8. Install PHP & JS deps
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# 7. Install frontend dependencies
|
||||
yarn install
|
||||
|
||||
# 8. Create database
|
||||
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS habbo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# 9. Run migrations & seeders (or use the auto-repair tool)
|
||||
# 9. Migrate, seed & cache
|
||||
php artisan migrate --seed
|
||||
# Or use the interactive repair tool:
|
||||
# php artisan atom:check --fix
|
||||
php artisan optimize
|
||||
php artisan filament:optimize
|
||||
|
||||
# 10. Build frontend assets
|
||||
# 10. Build frontend
|
||||
yarn build:all
|
||||
|
||||
# 11. Set permissions
|
||||
# 11. Permissions
|
||||
sudo chown -R www-data:www-data storage bootstrap/cache public/build
|
||||
sudo chmod -R 775 storage bootstrap/cache
|
||||
|
||||
# 12. Configure web server
|
||||
# 12. Sudoers (for update-Nitrov3.sh — sudo chown + systemctl)
|
||||
sudo tee /etc/sudoers.d/www-data << 'EOF'
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart emulator
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl status emulator
|
||||
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown -R www-data\:www-data /var/www/*
|
||||
EOF
|
||||
sudo chmod 440 /etc/sudoers.d/www-data
|
||||
|
||||
# --- Nginx ---
|
||||
sudo nano /etc/nginx/sites-available/atomcms
|
||||
# 13. Start services
|
||||
sudo systemctl enable --now redis-server
|
||||
|
||||
# 14. PHP tuning
|
||||
sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.5/fpm/php.ini
|
||||
sudo sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/8.5/fpm/php.ini
|
||||
sudo sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php/8.5/fpm/php.ini
|
||||
sudo sed -i 's/max_execution_time = .*/max_execution_time = 300/' /etc/php/8.5/fpm/php.ini
|
||||
|
||||
# 16. Restart & verify
|
||||
sudo systemctl restart php8.5-fpm redis-server nginx
|
||||
php artisan about # should show green "Application" line
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /var/www/atomcms/public;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.php;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
location / { try_files $uri $uri/ /index.php?$query_string; }
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
location ~ /\.(?!well-known).* { deny all; }
|
||||
location ~ /(\.env|\.git|composer\.(json|lock)) { deny all; }
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/atomcms /etc/nginx/sites-enabled/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
# --- Apache ---
|
||||
sudo a2enmod rewrite
|
||||
sudo nano /etc/apache2/sites-available/atomcms.conf
|
||||
sudo systemctl restart php8.5-fpm redis-server
|
||||
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw --force enable
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName your-domain.com
|
||||
DocumentRoot /var/www/atomcms/public
|
||||
|
||||
<Directory /var/www/atomcms/public>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
### SSL (recommended)
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/atomcms-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/atomcms-access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
```bash
|
||||
sudo a2ensite atomcms
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
# 13. Restart PHP-FPM to clear opcache
|
||||
sudo systemctl restart php8.3-fpm
|
||||
|
||||
# 14. Visit http://your-domain.com in your browser
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Windows (XAMPP / WampServer)
|
||||
|
||||
#### Prerequisites
|
||||
- [XAMPP](https://www.apachefriends.org/) (PHP 8.1+, Apache, MariaDB) or [WampServer](https://www.wampserver.com/)
|
||||
- [Node.js](https://nodejs.org/) (20 LTS or higher)
|
||||
- [Yarn](https://yarnpkg.com/getting-started/install) (`npm install -g yarn` after Node.js)
|
||||
- [Composer](https://getcomposer.org/download/) (Windows installer)
|
||||
- [Git for Windows](https://git-scm.com/download/win)
|
||||
|
||||
#### Steps
|
||||
|
||||
```powershell
|
||||
# 1. Open PowerShell or CMD as Administrator
|
||||
|
||||
# 2. Clone the project
|
||||
git clone https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git C:\xampp\htdocs\atomcms
|
||||
cd C:\xampp\htdocs\atomcms
|
||||
|
||||
# 3. Configure environment
|
||||
copy .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
# Edit .env with your XAMPP database credentials:
|
||||
# DB_CONNECTION=mariadb
|
||||
# DB_HOST=localhost
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=habbo
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
# 4. Start XAMPP services
|
||||
# Open XAMPP Control Panel → Start Apache & MySQL
|
||||
|
||||
# 5. Create database
|
||||
# Open phpMyAdmin (http://localhost/phpmyadmin)
|
||||
# → New → Database name: habbo → Charset: utf8mb4_unicode_ci → Create
|
||||
|
||||
# 6. Install PHP dependencies
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# 7. Install frontend dependencies
|
||||
yarn install
|
||||
|
||||
# 8. Run migrations & seeders
|
||||
php artisan migrate --seed
|
||||
# Or use the interactive repair tool:
|
||||
# php artisan atom:check --fix
|
||||
|
||||
# 9. Build frontend assets
|
||||
yarn build:all
|
||||
|
||||
# 10. Set permissions (required for storage & cache)
|
||||
# Right-click storage\ and bootstrap\cache\ folders
|
||||
# → Properties → Security → Edit → Add "Everyone" → Full Control
|
||||
# Or run in PowerShell as Admin:
|
||||
icacls storage /grant Everyone:F /T
|
||||
icacls bootstrap/cache /grant Everyone:F /T
|
||||
icacls public/build /grant Everyone:F /T
|
||||
|
||||
# 11. Enable Apache mod_rewrite
|
||||
# Open XAMPP Control Panel → Apache → Config → httpd.conf
|
||||
# Uncomment: LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
# 12. Visit http://localhost/atomcms/public in your browser
|
||||
```
|
||||
|
||||
> **Note:** If you use IIS on Windows, the repair tool supports auto-detection:
|
||||
> `php artisan atom:check --fix`
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Repair Tool)
|
||||
## Yarn Scripts
|
||||
|
||||
```bash
|
||||
# Interactive mode (asks for confirmation before each step)
|
||||
php artisan atom:check --fix
|
||||
|
||||
# Auto mode (fixes everything automatically without asking)
|
||||
php artisan atom:check --auto
|
||||
|
||||
# Force platform detection
|
||||
php artisan atom:check --platform=nginx
|
||||
|
||||
# Dutch language output
|
||||
php artisan atom:check --fix --lang=nl
|
||||
yarn build:all # Build all themes
|
||||
yarn build:atom # Atom theme only
|
||||
yarn build:dusk # Dusk theme only
|
||||
yarn dev # Vite dev server
|
||||
yarn lint # Lint JS/Vue
|
||||
yarn format # Format code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Check only (no changes)
|
||||
php artisan atom:check
|
||||
|
||||
# Interactive fix (asks before each step)
|
||||
php artisan atom:check --fix
|
||||
|
||||
# Automatic fix (no questions)
|
||||
php artisan atom:check --auto
|
||||
|
||||
# Fix gamedata symlinks for bundled assets
|
||||
php artisan atom:fix-gamedata-symlinks
|
||||
|
||||
# Preview symlinks (dry-run)
|
||||
php artisan atom:fix-gamedata-symlinks --dry-run
|
||||
```
|
||||
|
||||
### Yarn Scripts
|
||||
|
||||
```bash
|
||||
# Build all themes
|
||||
yarn build:all
|
||||
|
||||
# Build single theme
|
||||
yarn build:atom # Atom theme
|
||||
yarn build:dusk # Dusk theme
|
||||
|
||||
# Development
|
||||
yarn dev # Start Vite dev server
|
||||
yarn dev:atom # Dev server for Atom theme
|
||||
|
||||
# Linting & Formatting
|
||||
yarn lint # Check JS/Vue
|
||||
yarn lint:fix # Fix JS/Vue
|
||||
yarn lint:css # Check CSS
|
||||
yarn lint:css:fix # Fix CSS
|
||||
yarn format # Format everything
|
||||
yarn format:check # Check formatting
|
||||
|
||||
# Full check
|
||||
yarn check # Lint + format check
|
||||
yarn check:php # PHP syntax check
|
||||
yarn check:security # Composer & npm audit
|
||||
yarn check:deps # Check outdated packages
|
||||
|
||||
# Cache
|
||||
yarn clean # Clear Vite cache
|
||||
yarn rebuild # Clean + install + build
|
||||
```
|
||||
|
||||
### PHP Commands
|
||||
|
||||
```bash
|
||||
# Auto-fix code style
|
||||
php artisan atom:fix-code
|
||||
|
||||
# Static analysis (PHPStan Level 3)
|
||||
./vendor/bin/phpstan analyse
|
||||
|
||||
# Build frontend assets
|
||||
yarn build:atom
|
||||
yarn build:dusk
|
||||
|
||||
# Development mode
|
||||
yarn dev:atom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repair Tool Details
|
||||
|
||||
The `atom:check` command performs over **100 deep system checks** and offers to fix them automatically.
|
||||
|
||||
### Diagnostic Checks
|
||||
|
||||
| Section | What it Checks |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| **Environment** | .env, APP_KEY, Debug mode, Composer security |
|
||||
| **Database** | Tables, columns, migrations, seeders, settings, radio, admin |
|
||||
| **PHP Stack** | Extensions, php.ini, config cache, session |
|
||||
| **Web Server**| Apache/Nginx/IIS config, SSL |
|
||||
| **System** | Permissions, firewall ports |
|
||||
| **Assets** | Frontend, Redis, Cron, Queue, Supervisor, Vite |
|
||||
| **HTTP Errors** | 400, 401, 403, 404, 419, 429, 500, 502, 503, 504 |
|
||||
|
||||
### Auto-Fix Steps
|
||||
|
||||
| Step | Action |
|
||||
| ---- | ---------------------------------- |
|
||||
| 1 | Environment (.env, APP_KEY) |
|
||||
| 2 | Clear all caches |
|
||||
| 3 | Fix permissions |
|
||||
| 4 | Run migrations |
|
||||
| 5 | Run seeders |
|
||||
| 6 | Fix storage (symlink, directories) |
|
||||
| 6b | Fix Radio tables |
|
||||
| 7 | Fix Gamedata symlinks |
|
||||
| 8 | Create admin user |
|
||||
| 9 | Web server config (.htaccess) |
|
||||
| 10 | PHP config & extensions |
|
||||
| 11 | Build assets (npm) |
|
||||
| 12 | Fix HTTP errors |
|
||||
|
||||
Auto-detects: **Linux, Windows IIS, XAMPP, WAMP, Apache, Nginx**
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
| Optimization | Description |
|
||||
| ------------------- | -------------------------------- |
|
||||
| **Vite 8** | Fastest build tool |
|
||||
| **esbuild** | Faster minification |
|
||||
| **Better chunking** | Optimal code splitting |
|
||||
| **Gzip + Brotli** | Compression (~70% smaller) |
|
||||
| **Resource hints** | DNS prefetch, preconnect, preload|
|
||||
| **HTTP/2** | Faster network requests |
|
||||
| **Console removed** | Smaller JS bundles |
|
||||
| **Caching** | Vite cache + optimizedeps |
|
||||
|
||||
### Gamedata Symlinks
|
||||
|
||||
The `atom:fix-gamedata-symlinks` command creates symlinks in the Gamedata directory to point to optimized bundled assets, significantly improving game client load times.
|
||||
|
||||
| Symlink | Target | Purpose |
|
||||
| ---------------- | ------------------- | ---------------- |
|
||||
| `effect` | `bundled/effect` | Avatar effects |
|
||||
| `furniture` | `bundled/furniture` | Furniture assets |
|
||||
| `generic` | `bundled/generic` | Generic assets |
|
||||
| `pet` | `bundled/pet` | Pet assets |
|
||||
| `figure` | `bundled/figure` | Avatar figures |
|
||||
| `generic_custom` | `bundled/generic` | Custom generic |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** Laravel 13
|
||||
- **Frontend:** React 19 + Alpine.js
|
||||
- **Build:** Vite 8
|
||||
- **CSS:** TailwindCSS 4
|
||||
- **Admin Panel:** Filament 5
|
||||
- **Database:** MariaDB / MySQL
|
||||
- **Linting:** ESLint + Stylelint + Prettier
|
||||
**Laravel 13 · React 19 + Alpine.js · Vite 8 · TailwindCSS 4 · Filament 5 · MariaDB/MySQL · Redis**
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
AtomCMS is built with security as a priority. Below is what's in place and what you need to configure.
|
||||
|
||||
### ✅ Already locked down
|
||||
|
||||
| Measure | Details |
|
||||
|---------|---------|
|
||||
| **Mass assignment protection** | User model restricted to 21 fillable fields (sensitive fields like `rank`, `credits`, `online` require explicit `forceFill`) |
|
||||
| **API authentication** | Sanctum tokens, Bearer-only (no query-string API keys accepted) |
|
||||
| **PayPal credentials** | Loaded from `env()`, never hardcoded |
|
||||
| **CORS** | Must be explicitly set via `CORS_ALLOWED_ORIGINS` env (no wildcard default) |
|
||||
| **Debug mode** | `APP_DEBUG=false` by default |
|
||||
| **PHP debugging** | No `dd()`, `dump()`, or `var_dump()` in production code |
|
||||
| **Password flashing** | Exception handler excludes passwords from session flash |
|
||||
| **File uploads** | MIME validation (Laravel `image` rule + `finfo` on logos) |
|
||||
| **2FA** | Two-factor authentication available |
|
||||
| **SQL injection** | All queries use parameterized binding or Eloquent ORM |
|
||||
| **Command injection** | All `exec()`/`shell_exec()` calls use `escapeshellarg()` or hardcoded values |
|
||||
| **CSRF** | Sanctum CSRF protection on all stateful routes |
|
||||
| **Insecure deserialization** | No `unserialize()` calls exist |
|
||||
|
||||
### ⚠️ You must configure
|
||||
|
||||
| Item | What to do |
|
||||
|------|------------|
|
||||
| **`.env` file** | Restrict file permissions (`chmod 600 .env`), ensure Nginx blocks access (already in the provided config) |
|
||||
| **`CORS_ALLOWED_ORIGINS`** | Set to your exact frontend domain(s) in `.env` (included in the example files) |
|
||||
| **Database password** | Use a strong, unique password (not `your_db_password`) |
|
||||
| **APP_KEY** | Run `php artisan key:generate` after cloning |
|
||||
| **Session domain** | Set `SESSION_DOMAIN` to your hotel domain in `.env` |
|
||||
| **SSL** | Required for production — use the Certbot instructions above |
|
||||
| **Admin accounts** | Only grant high-rank access to trusted users |
|
||||
| **Log retention** | Check `LOG_MAX_FILES` in `.env` (default 14 days) |
|
||||
|
||||
### 🔒 Sudoers safety
|
||||
|
||||
The `sudoers.d/www-data` configuration grants passwordless `systemctl` and `chown` to `www-data`. This is **safe by design**:
|
||||
|
||||
- Each command is pinned to a specific binary path (`/usr/bin/systemctl`, `/usr/bin/chown`)
|
||||
- `chown` is restricted to `/var/www/*`
|
||||
- No shell (`/bin/sh`, `/bin/bash`) is granted
|
||||
- No arbitrary binaries can be executed
|
||||
- In a worst-case web compromise, the attacker still cannot read `/etc/shadow`, install packages, or run arbitrary commands
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Discord:** [Join our server](https://discord.gg/pP6HyZedAj)
|
||||
- **Issues:** Report bugs via the project issue tracker
|
||||
- **Contributions:** Fork & submit merge requests — all help is welcome!
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- **Remco (Epicnabbo)** — Core Maintainer, System Architecture
|
||||
- **Kasja** — Design & Themes
|
||||
- **Kani** — RCON & API
|
||||
- **Atom Community** — Testing & Feedback
|
||||
**Remco (Epicnabbo)** — Core Maintainer · **Kasja** — Design & Themes · **Kani** — RCON & API · **Atom Community** — Testing & Feedback
|
||||
|
||||
<div align="center">
|
||||
<i>Made with love for the Retro Community</i>
|
||||
</div>
|
||||
<div align="center"><i>Made with love for the Retro Community</i></div>
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
# AtomCMS (epicnabbo edit) - v1.0 Ultimate Setup Guide
|
||||
|
||||
**AtomCMS** is a powerful Habbo retro CMS. This guide ensures a **100% working installation** on **any system**: Linux (Nginx/Apache), Windows (IIS/XAMPP), and macOS.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
git clone ssh://git@localhost:8422/RemcoEpic/atomcms-edit.git
|
||||
cd atomcms-edit
|
||||
composer install --no-dev
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
# Configure your .env (DB settings below)
|
||||
php artisan migrate --seed
|
||||
php artisan storage:link
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 System Requirements
|
||||
|
||||
### Universal Requirements
|
||||
|
||||
- **PHP**: 8.1, 8.2, or 8.3 (8.2 Recommended)
|
||||
- **Extensions**: `pdo_mysql`, `curl`, `mbstring`, `openssl`, `tokenizer`, `xml`, `zip`
|
||||
- **Database**: MySQL 5.7+ or MariaDB 10.3+
|
||||
- **Composer**: 2.x
|
||||
|
||||
### Linux (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y php8.2 php8.2-{mysql,curl,mbstring,xml,zip,fpm} nginx mysql-server
|
||||
```
|
||||
|
||||
### Windows (XAMPP/IIS)
|
||||
|
||||
- Download **XAMPP** with PHP 8.2 or install **PHP 8.2** manually.
|
||||
- Ensure `php.ini` has `extension=pdo_mysql`, `extension=curl`, etc. enabled.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 1: Clone the Repository
|
||||
|
||||
```bash
|
||||
# For RemcoEpic epicnabbo edit version:
|
||||
git clone ssh://git@localhost:8422/RemcoEpic/atomcms-edit.git
|
||||
cd atomcms-edit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 2: Install Dependencies
|
||||
|
||||
Run composer to install all required packages. The system is optimized to handle missing extensions automatically via the `SystemCheck` middleware.
|
||||
|
||||
```bash
|
||||
composer install --optimize-autoloader
|
||||
```
|
||||
|
||||
_Windows Note: Run CMD/PowerShell as Administrator if you get permission errors._
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Step 3: Environment Configuration
|
||||
|
||||
### 3.1 Create .env
|
||||
|
||||
If `.env` is missing, the system tries to create it automatically. You can also do it manually:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 3.2 Generate App Key
|
||||
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
### 3.3 Database Settings
|
||||
|
||||
Edit `.env` and configure your database connection:
|
||||
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=atomcms
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
APP_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
_Windows IIS Note: If `127.0.0.1` fails, try `localhost`._
|
||||
|
||||
---
|
||||
|
||||
## 💾 Step 4: Database Setup
|
||||
|
||||
### 4.1 Create Database
|
||||
|
||||
Login to MySQL and create the database:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE atomcms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'atomcms_user'@'localhost' IDENTIFIED BY 'strong_password';
|
||||
GRANT ALL PRIVILEGES ON atomcms.* TO 'atomcms_user'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
### 4.2 Run Migrations
|
||||
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Step 5: Storage Link (Crucial!)
|
||||
|
||||
The system uses a **Bulletproof Middleware (`SystemCheck`)** that attempts to create the storage symlink automatically on the first request.
|
||||
|
||||
**Manual Fallback (if auto-fails):**
|
||||
|
||||
_Linux:_
|
||||
|
||||
```bash
|
||||
php artisan storage:link
|
||||
sudo chmod -R 755 storage bootstrap/cache
|
||||
```
|
||||
|
||||
_Windows (CMD as Admin):_
|
||||
|
||||
```cmd
|
||||
mklink /J "C:\path\to\atomcms-edit\public\storage" "C:\path\to\atomcms-edit\storage\app\public"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Step 6: Web Server Configuration
|
||||
|
||||
### Option A: PHP Built-in (Testing)
|
||||
|
||||
```bash
|
||||
php artisan serve --host=0.0.0.0 --port=8000
|
||||
```
|
||||
|
||||
Visit: `http://localhost:8000`
|
||||
|
||||
### Option B: Nginx (Linux Production)
|
||||
|
||||
Edit `/etc/nginx/sites-available/atomcms`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name epicnabbo.nl; # Your domain
|
||||
root /var/www/atomcms/public;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable: `sudo ln -s /etc/nginx/sites-available/atomcms /etc/nginx/sites-enabled/ && sudo systemctl restart nginx`
|
||||
|
||||
### Option C: Windows IIS
|
||||
|
||||
1. Open **IIS Manager** -> **Sites** -> **Add Website**.
|
||||
2. Physical Path: `C:\inetpub\wwwroot\atomcms-edit\public`
|
||||
3. Install **URL Rewrite Module**.
|
||||
4. Add `web.config` in `public/`:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Laravel Routes" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.php" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Step 7: Production Optimization
|
||||
|
||||
```bash
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Step 8: Verify & Test
|
||||
|
||||
1. Open `http://your-domain.com` (or `http://localhost:8000`).
|
||||
2. Test **Registration** at `/register`.
|
||||
- _Note:_ v1.0 fixes the "500 Error" on registration (Cloudflare Turnstile & IP fields).
|
||||
3. Check logs if needed: `tail -f storage/logs/laravel.log`
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting (v1.0 Fixes)
|
||||
|
||||
### 500 Error on Registration?
|
||||
|
||||
**Fixed in v1.0!**
|
||||
|
||||
- ✅ Cloudflare Turnstile null-response handled.
|
||||
- ✅ `ip_register` and `ip_current` added to User model.
|
||||
- ✅ MySQL Strict Mode disabled for Windows/IIS/XAMPP compatibility.
|
||||
|
||||
### "Missing .env" or "No Application Key"
|
||||
|
||||
The `SystemCheck` middleware (auto-runs on every request) will attempt to:
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Generate `APP_KEY` automatically.
|
||||
|
||||
### Storage Link Issues
|
||||
|
||||
The system auto-creates the symlink. If it fails on Windows, ensure you run the server with **Administrator** privileges.
|
||||
|
||||
### Database Connection Failed
|
||||
|
||||
1. Check `.env` credentials.
|
||||
2. Ensure MySQL service is running (`systemctl status mysql` or Windows Services).
|
||||
3. Use `127.0.0.1` instead of `localhost` if socket errors occur.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 What's new in v1.0 (epicnabbo edit)?
|
||||
|
||||
This version is **Bulletproof**. It is designed to work immediately after `composer install` on any environment.
|
||||
|
||||
- ✅ **Cross-Platform**: Works on Linux (Nginx/Apache), Windows (IIS/XAMPP).
|
||||
- ✅ **Auto-Recovery**: `SystemCheck` middleware fixes missing `.env`, `APP_KEY`, and storage links automatically.
|
||||
- ✅ **Zero 500 Errors**: Registration bugs (Turnstile & IP fields) fixed.
|
||||
- ✅ **MySQL Strict Mode**: Disabled for maximum compatibility.
|
||||
- ✅ **Windows Path Fix**: `index.php` handles Windows backslash paths.
|
||||
|
||||
---
|
||||
|
||||
**© 2026 RemcoEpic - epicnabbo.nl**
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Commandocentrum;
|
||||
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\RconService;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -13,7 +12,6 @@ class EmulatorControlAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SettingsService $settings,
|
||||
private readonly EmulatorUpdateService $updateService,
|
||||
) {}
|
||||
|
||||
public function start(): array
|
||||
@@ -59,29 +57,4 @@ class EmulatorControlAction
|
||||
|
||||
return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!'];
|
||||
}
|
||||
|
||||
public function build(): array
|
||||
{
|
||||
return $this->updateService->buildFromSource();
|
||||
}
|
||||
|
||||
public function update(): array
|
||||
{
|
||||
return $this->updateService->updateEmulator();
|
||||
}
|
||||
|
||||
public function runSqlUpdates(): array
|
||||
{
|
||||
return $this->updateService->runSqlUpdates();
|
||||
}
|
||||
|
||||
public function getBackups(): array
|
||||
{
|
||||
return $this->updateService->getBackupList();
|
||||
}
|
||||
|
||||
public function restoreBackup(string $backupName): array
|
||||
{
|
||||
return $this->updateService->restoreBackup($backupName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Commandocentrum;
|
||||
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class NitroControlAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SettingsService $settings,
|
||||
) {}
|
||||
|
||||
public function pullUpdates(string $clientPath, string $rendererPath, string $branch): array
|
||||
{
|
||||
$this->runGitPull($clientPath, $branch);
|
||||
$this->runGitPull($rendererPath, $branch);
|
||||
|
||||
return ['success' => true, 'message' => 'Nitro bijgewerkt van GitHub'];
|
||||
}
|
||||
|
||||
public function build(string $clientPath, string $rendererPath, string $branch): array
|
||||
{
|
||||
$this->runGitPull($clientPath, $branch);
|
||||
$this->runGitPull($rendererPath, $branch);
|
||||
|
||||
Process::timeout(120)->run('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1');
|
||||
Process::timeout(120)->run('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1');
|
||||
|
||||
$exitCode = Artisan::call('build:theme');
|
||||
|
||||
return [
|
||||
'success' => $exitCode === 0,
|
||||
'message' => $exitCode === 0 ? 'Nitro build succesvol!' : 'Build gestart - controleer handmatig',
|
||||
];
|
||||
}
|
||||
|
||||
public function generateConfigs(string $siteUrl, string $webroot, string $gamedataPath): array
|
||||
{
|
||||
if (! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
|
||||
return ['success' => false, 'message' => 'Voer een geldige URL in'];
|
||||
}
|
||||
|
||||
$existingConfigs = $this->readExistingConfigs($webroot, $gamedataPath);
|
||||
|
||||
$exitCode = Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]);
|
||||
|
||||
if ($existingConfigs !== [] && $exitCode === 0) {
|
||||
$this->mergeExistingConfigs($webroot, $existingConfigs);
|
||||
}
|
||||
|
||||
$this->settings->set('nitro_last_checked', now()->toIso8601String());
|
||||
|
||||
return [
|
||||
'success' => $exitCode === 0,
|
||||
'message' => $exitCode === 0 ? 'Configs gegenereerd & bestaande instellingen behouden!' : 'Config gegenereerd (controleer handmatig)',
|
||||
];
|
||||
}
|
||||
|
||||
private function runGitPull(string $path, string $branch): void
|
||||
{
|
||||
Process::timeout(60)->run('cd ' . escapeshellarg($path) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1');
|
||||
}
|
||||
|
||||
private function readExistingConfigs(string $webroot, string $gamedataPath): array
|
||||
{
|
||||
$configs = [];
|
||||
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$path = $webroot . '/' . $file;
|
||||
$content = @file_get_contents($path);
|
||||
if ($content) {
|
||||
$decoded = json_decode($content, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$configs[$file] = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($gamedataPath !== '') {
|
||||
$gamedataConfigs = [
|
||||
'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json',
|
||||
'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json',
|
||||
'ProductData.json' => $gamedataPath . '/config/ProductData.json',
|
||||
'FigureData.json' => $gamedataPath . '/config/FigureData.json',
|
||||
];
|
||||
|
||||
foreach ($gamedataConfigs as $key => $path) {
|
||||
$content = @file_get_contents($path);
|
||||
if ($content) {
|
||||
$decoded = json_decode($content, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$configs['gamedata.' . $key] = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $configs;
|
||||
}
|
||||
|
||||
private function mergeExistingConfigs(string $webroot, array $existingConfigs): void
|
||||
{
|
||||
if (isset($existingConfigs['renderer-config.json'])) {
|
||||
$newPath = $webroot . '/renderer-config.json';
|
||||
$newContent = @file_get_contents($newPath);
|
||||
$newConfig = json_decode($newContent, true);
|
||||
|
||||
if ($newConfig && json_last_error() === JSON_ERROR_NONE) {
|
||||
$merged = array_merge($existingConfigs['renderer-config.json'], $newConfig);
|
||||
@file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
if (! empty($discordRanks)) {
|
||||
$shouldNotify = in_array($user->rank, $discordRanks);
|
||||
} else {
|
||||
$minStaffRank = (int) setting('min_staff_rank', 3);
|
||||
$minStaffRank = (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank_login'));
|
||||
$shouldNotify = $user->rank >= $minStaffRank;
|
||||
}
|
||||
|
||||
@@ -171,13 +171,11 @@ class CreateNewUser implements CreatesNewUsers
|
||||
try {
|
||||
Http::asJson()->post(is_string($discordWebhookUrl) ? $discordWebhookUrl : '', [
|
||||
'username' => sprintf('%s Bot', is_string($hotelNameSetting) ? $hotelNameSetting : 'Hotel'),
|
||||
'content' => "User: {$username} has just registered, with the IP: {$ip} and E-mail: {$email}",
|
||||
'content' => "User: {$username} has just registered.",
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send Discord webhook notification', [
|
||||
'username' => $username,
|
||||
'ip' => $ip,
|
||||
'email' => $email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class DisableTwoFactorAuthentication extends \Laravel\Fortify\Actions\DisableTwo
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed' => false,
|
||||
'two_factor_confirmed_at' => null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AutoUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'update:auto
|
||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
||||
{--sql-only : Alleen SQL updates draaien}
|
||||
{--emu-only : Alleen emulator updates draaien}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische emulator en SQL updates uitvoeren';
|
||||
|
||||
private const string CACHE_KEY_LAST_EMU_UPDATE = 'auto_update_last_emu';
|
||||
|
||||
private const string CACHE_KEY_LAST_SQL_UPDATE = 'auto_update_last_sql';
|
||||
|
||||
public function handle(AlertService $alertService, EmulatorUpdateService $updateService): int
|
||||
{
|
||||
$this->info('Automatische update check...');
|
||||
|
||||
$isForced = $this->option('force');
|
||||
$sqlOnly = $this->option('sql-only');
|
||||
$emuOnly = $this->option('emu-only');
|
||||
|
||||
if (! $isForced && ! $this->isScheduledTime()) {
|
||||
$this->line('Niet de geplande tijd, overslaan.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $sqlOnly) {
|
||||
$this->runEmulatorUpdate($alertService, $updateService);
|
||||
}
|
||||
|
||||
if (! $emuOnly) {
|
||||
$this->runSqlUpdates($alertService, $updateService);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function isScheduledTime(): bool
|
||||
{
|
||||
$enabled = setting('auto_update_enabled', true);
|
||||
|
||||
if (! $enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$scheduleTime = setting('auto_update_schedule', '03:00');
|
||||
$scheduleDays = setting('auto_update_days', '0,1,2,3,4,5,6');
|
||||
|
||||
$now = now();
|
||||
$currentTime = $now->format('H:i');
|
||||
$currentDay = (int) $now->dayOfWeek;
|
||||
|
||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
||||
|
||||
if (! in_array($currentDay, $allowedDays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($currentTime !== $scheduleTime) {
|
||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
||||
if ($minuteDiff > 5) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function runEmulatorUpdate(AlertService $alertService, EmulatorUpdateService $updateService): void
|
||||
{
|
||||
$check = $updateService->checkForUpdates();
|
||||
|
||||
if (! ($check['update_available'] ?? false)) {
|
||||
$this->line('Emulator is al up-to-date.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn('Nieuwe emulator versie beschikbaar: ' . ($check['latest_version'] ?? 'onbekend'));
|
||||
$this->info('Emulator updaten...');
|
||||
|
||||
$result = $updateService->updateEmulator();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info('Emulator succesvol geüpdatet!');
|
||||
$alertService->sendEmulatorUpdate($result['version'] ?? 'onbekend', $result['message'] ?? '');
|
||||
Log::info('[AutoUpdate] Emulator updated to v' . ($result['version'] ?? 'unknown'));
|
||||
} else {
|
||||
$this->error('Emulator update mislukt: ' . ($result['error'] ?? 'onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator Auto-Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_EMU_UPDATE, now()->toIso8601String());
|
||||
}
|
||||
|
||||
private function runSqlUpdates(AlertService $alertService, EmulatorUpdateService $updateService): void
|
||||
{
|
||||
$check = $updateService->checkForSqlUpdates();
|
||||
|
||||
if (! ($check['has_updates'] ?? false)) {
|
||||
$this->line('Geen SQL updates beschikbaar.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn($check['message']);
|
||||
$this->info('SQL updates uitvoeren...');
|
||||
|
||||
$result = $updateService->runSqlUpdates();
|
||||
|
||||
if ($result['success'] && ! empty($result['files_run'])) {
|
||||
$count = count($result['files_run']);
|
||||
$this->info("{$count} SQL updates succesvol uitgevoerd!");
|
||||
$alertService->sendSqlUpdate($count, $result['message'] ?? '');
|
||||
Log::info('[AutoUpdate] SQL updates completed', ['files' => $result['files_run']]);
|
||||
} elseif (! empty($result['errors'])) {
|
||||
$this->warn('SQL updates met fouten: ' . implode(', ', $result['errors']));
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'SQL Updates Met Fouten: ' . implode(', ', $result['errors']),
|
||||
['files' => $result['errors']],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_SQL_UPDATE, now()->toIso8601String());
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EmulatorUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'emulator:update
|
||||
{--check : Alleen controleren op updates}
|
||||
{--force : Forceer update ook al is er geen nieuwe versie}
|
||||
{--repair : Probeer emulator te repareren}
|
||||
{--rebuild : Forceer build vanaf source}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Update de emulator vanaf GitHub';
|
||||
|
||||
public function handle(EmulatorUpdateService $updateService, AlertService $alertService): int
|
||||
{
|
||||
if ($this->option('repair')) {
|
||||
return $this->repairEmulator($updateService, $alertService);
|
||||
}
|
||||
|
||||
if (! $updateService->isConfigured()) {
|
||||
$this->error('Geen GitHub URL geconfigureerd voor emulator updates.');
|
||||
$this->info('Configureer dit in Filament > Settings > Emulator');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Emulator update service gestart...');
|
||||
|
||||
$checkResult = $updateService->checkForUpdates();
|
||||
|
||||
if (isset($checkResult['error'])) {
|
||||
$this->error($checkResult['error']);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Property', 'Value'],
|
||||
[
|
||||
['Huidige Versie', $checkResult['current_version']],
|
||||
['Nieuwste Versie', $checkResult['latest_version']],
|
||||
['Update Beschikbaar', $checkResult['update_available'] ? 'JA' : 'NEE'],
|
||||
['Type', $checkResult['update_type'] ?? 'N/A'],
|
||||
],
|
||||
);
|
||||
|
||||
if ($this->option('check')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$force = $this->option('force');
|
||||
$rebuild = $this->option('rebuild');
|
||||
|
||||
if (! $checkResult['update_available'] && ! $force && ! $rebuild) {
|
||||
$this->info('Emulator is al up-to-date!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $checkResult['update_available'] && ($force || $rebuild)) {
|
||||
$this->warn('Geen nieuwe versie beschikbaar, maar force/rebuild aangevraagd.');
|
||||
}
|
||||
|
||||
if (! $force && ! $rebuild && ! $this->confirm('Wil je de emulator updaten naar v' . $checkResult['latest_version'] . '?')) {
|
||||
$this->info('Update geannuleerd.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Emulator wordt geüpdatet...');
|
||||
|
||||
try {
|
||||
if ($rebuild || $checkResult['type'] === 'source_build') {
|
||||
$this->info('Build vanaf source...');
|
||||
$result = $updateService->buildFromSource($force);
|
||||
} else {
|
||||
$result = $updateService->updateEmulator();
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info($result['message'] ?? 'Emulator succesvol geüpdatet!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Emulator succesvol geüpdatet naar v' . ($result['version'] ?? 'onbekend'),
|
||||
[
|
||||
'version' => $result['version'] ?? 'onbekend',
|
||||
'jar' => $result['jar'] ?? 'N/A',
|
||||
],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::info('[EmulatorUpdateCommand] Update successful', $result);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('Update mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator update mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::error('[EmulatorUpdateCommand] Update failed', $result);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Update exception: ' . $e->getMessage());
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'Emulator update exception: ' . $e->getMessage(),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::error('[EmulatorUpdateCommand] Exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function repairEmulator(EmulatorUpdateService $updateService, AlertService $alertService): int
|
||||
{
|
||||
$this->warn('🔧 Emulator repair modus gestart...');
|
||||
$this->info('Dit zal de emulator status controleren en proberen te repareren.');
|
||||
|
||||
try {
|
||||
$repairResult = $updateService->repairEmulator();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info('✅ Repair succesvol!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Emulator gerepareerd',
|
||||
['actions' => $repairResult['actions'] ?? []],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,796 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class GenerateNitroConfigs extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'app:generate-nitro-configs
|
||||
{--site-url= : The site URL to use}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Generate Nitro configuration files (renderer-config.json, ui-config.json, UITexts.json)';
|
||||
|
||||
private array $checks = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$siteUrl = $this->option('site-url') ?? setting('nitro_site_url', config('app.url', 'https://epicnabbo.nl'));
|
||||
|
||||
$this->info('🔧 Nitro Config Generator');
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
// Check 1: Validate URL
|
||||
$this->addCheck(function () use ($siteUrl) {
|
||||
if (empty($siteUrl) || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
|
||||
throw new \Exception("Invalid URL: {$siteUrl}");
|
||||
}
|
||||
$this->line(" ✓ URL: {$siteUrl}");
|
||||
});
|
||||
|
||||
$nitroService = new NitroUpdateService;
|
||||
$status = $nitroService->getStatus();
|
||||
$webroot = $status['webroot'] ?? '/var/www/Client';
|
||||
$buildPath = $status['build_path'] ?? '/var/www/atomcms/nitro-client/dist';
|
||||
|
||||
// Check 2: Webroot exists
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $webroot));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Webroot does not exist: {$webroot}");
|
||||
}
|
||||
$this->line(" ✓ Webroot exists: {$webroot}");
|
||||
});
|
||||
|
||||
// Check 3: Webroot is writable
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(5)->run('test -w ' . escapeshellarg((string) $webroot));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Webroot is not writable: {$webroot}");
|
||||
}
|
||||
$this->line(' ✓ Webroot is writable');
|
||||
});
|
||||
|
||||
// Check 4: Build path exists
|
||||
$this->addCheck(function () use ($buildPath) {
|
||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $buildPath));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Build path does not exist: {$buildPath}");
|
||||
}
|
||||
$this->line(" ✓ Build path exists: {$buildPath}");
|
||||
});
|
||||
|
||||
// Check both buildPath and webroot for example files
|
||||
$this->addCheck(function () use ($buildPath, $webroot) {
|
||||
// Map .example to .json fallback
|
||||
$fileMap = [
|
||||
'renderer-config.example' => 'renderer-config.json',
|
||||
'ui-config.example' => 'ui-config.json',
|
||||
'UITexts.example' => 'UITexts.example',
|
||||
];
|
||||
|
||||
$allFound = true;
|
||||
|
||||
foreach ($fileMap as $exampleFile => $jsonFile) {
|
||||
// Check .example file first
|
||||
$buildPathResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $exampleFile));
|
||||
$webrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $exampleFile));
|
||||
|
||||
if ($buildPathResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in build path: {$exampleFile}");
|
||||
} elseif ($webrootResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in webroot: {$exampleFile}");
|
||||
} elseif ($jsonFile !== $exampleFile) {
|
||||
// Check json fallback for .example files
|
||||
$jsonBuildResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $jsonFile));
|
||||
$jsonWebrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $jsonFile));
|
||||
if ($jsonBuildResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in build path: {$jsonFile}");
|
||||
} elseif ($jsonWebrootResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in webroot: {$jsonFile}");
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$exampleFile} or {$jsonFile}");
|
||||
$allFound = false;
|
||||
}
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$exampleFile}");
|
||||
$allFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $allFound) {
|
||||
throw new \Exception('Some example files are missing');
|
||||
}
|
||||
});
|
||||
|
||||
// Check 6: Check disk space
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(10)->run("df -h {$webroot} | tail -1 | awk '{print $4}'");
|
||||
if ($result->successful()) {
|
||||
$free = trim($result->output());
|
||||
$this->line(" ✓ Free space: {$free}");
|
||||
}
|
||||
});
|
||||
|
||||
// Check 7: Validate bundled/config gamedata files
|
||||
$this->addCheck(function () {
|
||||
$bundledConfigDir = '/var/www/Gamedata/bundled/config';
|
||||
$configDir = '/var/www/Gamedata/config';
|
||||
$requiredFiles = ['HabboAvatarActions.json', 'FurnitureData.json', 'ExternalTexts.json', 'ProductData.json', 'FigureData.json', 'FigureMap.json', 'EffectMap.json'];
|
||||
|
||||
// Create bundled/config if it doesn't exist
|
||||
if (! is_dir($bundledConfigDir)) {
|
||||
mkdir($bundledConfigDir, 0755, true);
|
||||
$this->line(" 📁 Created directory: {$bundledConfigDir}");
|
||||
}
|
||||
|
||||
// Copy missing files from config to bundled/config
|
||||
foreach ($requiredFiles as $file) {
|
||||
$targetPath = $bundledConfigDir . '/' . $file;
|
||||
if (! file_exists($targetPath)) {
|
||||
$sourcePath = $configDir . '/' . $file;
|
||||
if (file_exists($sourcePath)) {
|
||||
copy($sourcePath, $targetPath);
|
||||
$this->line(" 📋 Copied: {$file} to bundled/config");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' ✓ Gamedata bundled/config files ready');
|
||||
});
|
||||
|
||||
// Check 8: Validate existing config files
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$configFiles = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
||||
foreach ($configFiles as $file) {
|
||||
$path = $webroot . '/' . $file;
|
||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
||||
if ($result->exitCode() === 0) {
|
||||
// Validate JSON
|
||||
$jsonCheck = Process::timeout(5)->run('python3 -c "import json; json.load(open(\'' . $path . '\'))" 2>&1 || echo "INVALID"');
|
||||
if (str_contains($jsonCheck->output(), 'INVALID')) {
|
||||
$this->warn(" ⚠ Invalid JSON: {$file}");
|
||||
} else {
|
||||
$this->line(" ✓ Valid JSON: {$file}");
|
||||
}
|
||||
} else {
|
||||
$this->line(" - New file: {$file}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Run all checks
|
||||
$this->line('');
|
||||
$this->info('Running pre-flight checks...');
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
|
||||
try {
|
||||
foreach ($this->checks as $check) {
|
||||
$check();
|
||||
}
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
$this->info('✅ All checks passed!');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Check failed: ' . $e->getMessage());
|
||||
$this->newLine();
|
||||
$this->error('Please fix the issue before generating configs.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Generate configs
|
||||
$this->newLine();
|
||||
$this->info('Generating configuration files...');
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
|
||||
$protocol = str_starts_with((string) $siteUrl, 'https') ? 'https' : 'http';
|
||||
$host = preg_replace('/^https?:\/\//', '', (string) $siteUrl);
|
||||
|
||||
try {
|
||||
// Step 0: Sync latest example files from GitHub
|
||||
$this->syncExampleFromGithub($buildPath, $webroot);
|
||||
|
||||
$rendererConfig = $this->generateRendererConfig($protocol, $host, $buildPath, $webroot);
|
||||
$uiConfig = $this->generateUiConfig($protocol, $host, $buildPath, $webroot);
|
||||
|
||||
// Compare with existing deployed configs
|
||||
$this->compareConfigs($rendererConfig, 'renderer-config', $webroot);
|
||||
$this->compareConfigs($uiConfig, 'ui-config', $webroot);
|
||||
|
||||
// Validate generated JSON
|
||||
$this->validateJson($rendererConfig, 'renderer-config');
|
||||
$this->validateJson($uiConfig, 'ui-config');
|
||||
|
||||
$tempFile = '/tmp/nitro_config_' . uniqid();
|
||||
file_put_contents($tempFile . '_renderer', json_encode($rendererConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
file_put_contents($tempFile . '_ui', json_encode($uiConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// Copy generated configs
|
||||
$commands = [
|
||||
'cp "' . $tempFile . '_renderer" "' . $webroot . '/renderer-config.json"',
|
||||
'cp "' . $tempFile . '_ui" "' . $webroot . '/ui-config.json"',
|
||||
];
|
||||
|
||||
// Copy UITexts.json if it exists (check both buildPath and webroot)
|
||||
$uitextsInBuild = Process::timeout(5)->run('test -f "' . $buildPath . '/UITexts.example"')->exitCode() === 0;
|
||||
$uitextsInWebroot = Process::timeout(5)->run('test -f "' . $webroot . '/UITexts.example"')->exitCode() === 0;
|
||||
if ($uitextsInBuild) {
|
||||
$commands[] = 'cp "' . $buildPath . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
||||
$this->line(' ✓ Generated: UITexts.json (from build path)');
|
||||
} elseif ($uitextsInWebroot) {
|
||||
$commands[] = 'cp "' . $webroot . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
||||
$this->line(' ✓ Generated: UITexts.json (from webroot)');
|
||||
}
|
||||
|
||||
$commands[] = 'rm "' . $tempFile . '_renderer" "' . $tempFile . '_ui"';
|
||||
|
||||
Process::timeout(10)->run(implode(' && ', $commands));
|
||||
|
||||
// Set proper ownership
|
||||
Process::timeout(10)->run("chown www-data:www-data {$webroot}/renderer-config.json {$webroot}/ui-config.json {$webroot}/UITexts.json 2>/dev/null");
|
||||
|
||||
setting('nitro_config_generated_at', now()->toIso8601String());
|
||||
|
||||
$this->line(' ✓ Generated: renderer-config.json');
|
||||
$this->line(' ✓ Generated: ui-config.json');
|
||||
|
||||
// Verify generated files
|
||||
$this->verifyGeneratedFiles($webroot);
|
||||
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
$this->info('✅ Configs generated successfully!');
|
||||
|
||||
Log::info('[NitroConfig] Generated successfully', [
|
||||
'webroot' => $webroot,
|
||||
'host' => $host,
|
||||
]);
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Generation failed: ' . $e->getMessage());
|
||||
Log::error('[NitroConfig] Generation failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private function addCheck(callable $check): void
|
||||
{
|
||||
$this->checks[] = function () use ($check) {
|
||||
$check();
|
||||
};
|
||||
}
|
||||
|
||||
private function validateJson(array $data, string $name): void
|
||||
{
|
||||
$encoded = json_encode($data, JSON_THROW_ON_ERROR);
|
||||
$decoded = json_decode($encoded, true);
|
||||
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception("Invalid JSON generated for {$name}");
|
||||
}
|
||||
|
||||
$this->line(" ✓ Validated: {$name}");
|
||||
}
|
||||
|
||||
private function verifyGeneratedFiles(string $webroot): void
|
||||
{
|
||||
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
||||
$allValid = true;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$path = $webroot . '/' . $file;
|
||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
||||
|
||||
if ($result->exitCode() === 0) {
|
||||
$size = Process::timeout(5)->run('stat -c%s ' . escapeshellarg($path));
|
||||
$sizeStr = trim($size->output()) . ' bytes';
|
||||
$this->line(" ✓ Verified: {$file} ({$sizeStr})");
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$file}");
|
||||
$allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $allValid) {
|
||||
throw new \Exception('Some files were not generated');
|
||||
}
|
||||
}
|
||||
|
||||
private function generateRendererConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
||||
{
|
||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
||||
$wsHost = 'ws.' . $host;
|
||||
|
||||
$rendererConfig = [];
|
||||
|
||||
// Check build path first, then webroot for .example file
|
||||
$examplePath = $buildPath . '/renderer-config.example';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$examplePath = $webroot . '/renderer-config.example';
|
||||
}
|
||||
|
||||
// Fallback to .json version (newer Nitro-V3 format)
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$jsonPath = $buildPath . '/renderer-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
} else {
|
||||
$jsonPath = $webroot . '/renderer-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load COMPLETE example file as base
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
||||
$content = file_get_contents($examplePath);
|
||||
// Fix invalid escape sequences (literal \n, \r, \t in JSON values)
|
||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
||||
$rendererConfig = @json_decode($content, true) ?: [];
|
||||
$this->line(' ✓ Loaded renderer config from: ' . basename($examplePath) . ' (' . count($rendererConfig) . ' keys)');
|
||||
} else {
|
||||
$this->warn(' ⚠ renderer-config.example/json not found');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Recursively replace ALL URLs in the entire config
|
||||
$rendererConfig = $this->replaceUrlsRecursively($rendererConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
||||
|
||||
// Auto-detect asset directory names on disk and fix paths
|
||||
$rendererConfig = $this->autoDetectAssetPaths($rendererConfig, $webroot);
|
||||
|
||||
// Special handling for socket.url - use ws subdomain
|
||||
if (isset($rendererConfig['socket.url'])) {
|
||||
$rendererConfig['socket.url'] = $wssProtocol . '://ws.' . $host;
|
||||
}
|
||||
|
||||
// Auto-detect local asset paths from Gamedata directory
|
||||
$gamedataBase = $httpProtocol . '://' . $host;
|
||||
|
||||
// Check what directories exist in Gamedata and set URLs accordingly
|
||||
$gamedataPath = '/var/www/Gamedata';
|
||||
if (is_dir($gamedataPath)) {
|
||||
// Check for bundled directory - serve via /gamedata/bundled (nginx alias)
|
||||
if (is_dir($gamedataPath . '/bundled')) {
|
||||
$rendererConfig['asset.url'] = $gamedataBase . '/gamedata/bundled';
|
||||
}
|
||||
// JSON config files are in /gamedata/config/
|
||||
if (is_dir($gamedataPath . '/config')) {
|
||||
$rendererConfig['gamedata.url'] = $gamedataBase . '/gamedata/config';
|
||||
}
|
||||
// Check for c_images directory - serve via /gamedata/c_images
|
||||
if (is_dir($gamedataPath . '/c_images')) {
|
||||
$rendererConfig['image.library.url'] = $gamedataBase . '/gamedata/c_images/';
|
||||
// Use icons folder for furni icons
|
||||
if (is_dir($gamedataPath . '/icons')) {
|
||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/icons';
|
||||
} else {
|
||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/c_images/dcr/hof_furni';
|
||||
}
|
||||
}
|
||||
// Fix furni icon path - icons are directly in hof.furni folder, not in icons subfolder
|
||||
if (isset($rendererConfig['furni.asset.icon.url'])) {
|
||||
$rendererConfig['furni.asset.icon.url'] = '${hof.furni.url}/%libname%%param%_icon.png';
|
||||
}
|
||||
// Fix sound machine samples path - sounds are in /gamedata/sounds/
|
||||
if (isset($rendererConfig['external.samples.url'])) {
|
||||
$rendererConfig['external.samples.url'] = $gamedataBase . '/gamedata/sounds/sound_machine_sample_%sample%.mp3';
|
||||
}
|
||||
// Check for images directory
|
||||
if (is_dir($gamedataPath . '/images')) {
|
||||
$rendererConfig['images.url'] = $gamedataBase . '/gamedata/images';
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing keys that might not be in the example
|
||||
if (! isset($rendererConfig['external.plugins'])) {
|
||||
$rendererConfig['external.plugins'] = [];
|
||||
}
|
||||
|
||||
// Add YouTube API key from settings
|
||||
$youtubeApiKey = setting('youtube_api_key', '');
|
||||
if (! empty($youtubeApiKey)) {
|
||||
$rendererConfig['youtube.api.key'] = $youtubeApiKey;
|
||||
$this->line(' ✓ Added YouTube API key to renderer config');
|
||||
}
|
||||
|
||||
// Ensure pet.types matches the exact required list in order
|
||||
if (isset($rendererConfig['pet.types'])) {
|
||||
$requiredPetTypes = [
|
||||
'dog',
|
||||
'cat',
|
||||
'croco',
|
||||
'terrier',
|
||||
'bear',
|
||||
'pig',
|
||||
'lion',
|
||||
'rhino',
|
||||
'spider',
|
||||
'turtle',
|
||||
'chicken',
|
||||
'frog',
|
||||
'dragon',
|
||||
'monster',
|
||||
'monkey',
|
||||
'horse',
|
||||
'monsterplant',
|
||||
'bunnyeaster',
|
||||
'bunnyevil',
|
||||
'bunnydepressed',
|
||||
'bunnylove',
|
||||
'pigeongood',
|
||||
'pigeonevil',
|
||||
'demonmonkey',
|
||||
'bearbaby',
|
||||
'terrierbaby',
|
||||
'gnome',
|
||||
'leprechaun',
|
||||
'kittenbaby',
|
||||
'puppybaby',
|
||||
'pigletbaby',
|
||||
'haloompa',
|
||||
'fools',
|
||||
'pterosaur',
|
||||
'velociraptor',
|
||||
'cow',
|
||||
'dragondog',
|
||||
'pkmshaymin2',
|
||||
'LeetEendjes',
|
||||
'pkmnentei',
|
||||
'squirtle',
|
||||
'LeetBH',
|
||||
'LeetCaviaaa',
|
||||
'LeetFantj',
|
||||
'LeetHotelMario',
|
||||
'LeetUil',
|
||||
'LeetWolf',
|
||||
'pokemon_mewblu',
|
||||
'LeetBB',
|
||||
'LeetHotelMari1',
|
||||
'LeetMewtw',
|
||||
'LeetPikachu',
|
||||
'LeetYos',
|
||||
'LeetE',
|
||||
'LeetMewt1',
|
||||
'LeetPen',
|
||||
'slendermn',
|
||||
'pkmnPAPI0',
|
||||
'pkmnPAPI1',
|
||||
'pkmnPAPI2',
|
||||
'pkmnPAPI3',
|
||||
'pkmnPAPI4',
|
||||
'pokmn_mew',
|
||||
'pkmashhhpet',
|
||||
'pkmbeautfly',
|
||||
'pkmcelebipe',
|
||||
'pkmdarkraip',
|
||||
'pkmeeveepet',
|
||||
'pkmjirachip',
|
||||
'pkmpichupet',
|
||||
'pkmriolupet',
|
||||
'pkmshayminp',
|
||||
'pkmtogepipe',
|
||||
'pkmvictinip',
|
||||
'slenderm1',
|
||||
'LeetEendj16',
|
||||
'pkmnente1',
|
||||
'LeetBa',
|
||||
'babymeisje',
|
||||
'babyBH',
|
||||
'bb_hbx',
|
||||
'LeetUi1',
|
||||
];
|
||||
|
||||
$rendererConfig['pet.types'] = $requiredPetTypes;
|
||||
$this->line(' ✓ Set pet.types to exact required list');
|
||||
}
|
||||
|
||||
return $rendererConfig;
|
||||
}
|
||||
|
||||
private function generateUiConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
||||
{
|
||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
||||
$wsHost = 'ws.' . $host;
|
||||
|
||||
$uiConfig = [];
|
||||
|
||||
// Check build path first, then webroot for .example file
|
||||
$examplePath = $buildPath . '/ui-config.example';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$examplePath = $webroot . '/ui-config.example';
|
||||
}
|
||||
|
||||
// Fallback to .json version (newer Nitro-V3 format)
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$jsonPath = $buildPath . '/ui-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
} else {
|
||||
$jsonPath = $webroot . '/ui-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load COMPLETE example file as base
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
||||
$content = file_get_contents($examplePath);
|
||||
// Fix invalid escape sequences
|
||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
||||
$uiConfig = @json_decode($content, true) ?: [];
|
||||
$this->line(' ✓ Loaded ui config from: ' . basename($examplePath) . ' (' . count($uiConfig) . ' keys)');
|
||||
} else {
|
||||
$this->warn(' ⚠ ui-config.example/json not found');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Recursively replace ALL URLs in the entire config
|
||||
$uiConfig = $this->replaceUrlsRecursively($uiConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
||||
|
||||
return $uiConfig;
|
||||
}
|
||||
|
||||
private function fixInvalidJsonEscapeSequences(string $content): string
|
||||
{
|
||||
// The example file has literal backslash + any letter in JSON values
|
||||
// which breaks JSON. We need to escape all of these.
|
||||
|
||||
$backslash = chr(92);
|
||||
$letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
|
||||
|
||||
foreach ($letters as $letter) {
|
||||
$content = str_replace($backslash . $letter, $backslash . $backslash . $letter, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function replaceUrlsRecursively(array $config, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): array
|
||||
{
|
||||
foreach ($config as &$value) {
|
||||
if (is_array($value)) {
|
||||
// Handle nested arrays (like navigator.room.models)
|
||||
if ($this->isAssociativeArray($value)) {
|
||||
$value = $this->replaceUrlsRecursively($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
} else {
|
||||
// Handle arrays of URLs
|
||||
$value = array_map(function ($item) use ($httpProtocol, $wsProtocol, $host, $wsHost) {
|
||||
if (is_string($item)) {
|
||||
return $this->replaceUrl($item, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, $value);
|
||||
}
|
||||
} elseif (is_string($value)) {
|
||||
$value = $this->replaceUrl($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function replaceUrl(string $url, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): string
|
||||
{
|
||||
// Replace ws/wss URLs with ws subdomain
|
||||
if (str_starts_with($url, 'ws://') || str_starts_with($url, 'wss://')) {
|
||||
$path = parse_url($url, PHP_URL_PATH) ?? '/';
|
||||
|
||||
return $wsProtocol . '://' . $wsHost . ($path !== '/' ? $path : '');
|
||||
}
|
||||
|
||||
// Replace localhost in all URLs
|
||||
$url = preg_replace('#https?://localhost(?::\d+)?#', $httpProtocol . '://' . $host, $url);
|
||||
$url = preg_replace('#wss?://localhost(?::\d+)?#', $wsProtocol . '://' . $wsHost, (string) $url);
|
||||
$url = preg_replace('#localhost(?::\d+)?#', $host, (string) $url);
|
||||
|
||||
// Fix broken escape sequences in URL paths (from invalid JSON in example files)
|
||||
// Pattern: /public\nitro-assets\gamedata or any variation
|
||||
$url = preg_replace('#/public\\n[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/public\\\\[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/nitro-assets\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/nitro\\\\[a-z_-]+#i', '/gamedata', (string) $url);
|
||||
|
||||
// Fix known asset path patterns
|
||||
$url = str_replace('/public/nitro-assets/gamedata', '/gamedata', $url);
|
||||
$url = str_replace('/swf/gamedata', '/gamedata', $url);
|
||||
|
||||
// Clean up any remaining backslashes in URLs
|
||||
$url = str_replace('\\', '/', $url);
|
||||
|
||||
// Clean up any remaining double slashes (but keep protocol slashes)
|
||||
$url = preg_replace('#([^:])//+#', '$1/', $url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function isAssociativeArray(array $arr): bool
|
||||
{
|
||||
if ($arr === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
private function autoDetectAssetPaths(array $config, string $webroot): array
|
||||
{
|
||||
// Check multiple possible gamedata locations
|
||||
$possiblePaths = [
|
||||
$webroot . '/gamedata',
|
||||
'/var/www/Gamedata',
|
||||
'/var/www/gamedata',
|
||||
];
|
||||
$gamedataPath = array_find($possiblePaths, fn ($path) => is_dir($path));
|
||||
|
||||
if (! $gamedataPath) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$assetDir = opendir($gamedataPath);
|
||||
$actualDirs = [];
|
||||
while (($entry = readdir($assetDir)) !== false) {
|
||||
if ($entry !== '.' && $entry !== '..' && is_dir($gamedataPath . '/' . $entry)) {
|
||||
$actualDirs[strtolower($entry)] = $entry;
|
||||
}
|
||||
}
|
||||
closedir($assetDir);
|
||||
|
||||
$pathChecks = [
|
||||
'pet.asset.url' => 'pets',
|
||||
'furni.asset.url' => 'furniture',
|
||||
'avatar.asset.url' => 'clothes',
|
||||
'avatar.asset.effect.url' => 'effect',
|
||||
'generic.asset.url' => 'generic_custom',
|
||||
];
|
||||
|
||||
foreach ($config as $key => &$value) {
|
||||
if (! is_string($value) || ! isset($pathChecks[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expectedDir = $pathChecks[$key];
|
||||
$lowerExpected = strtolower($expectedDir);
|
||||
$actualName = null;
|
||||
|
||||
// Special case: "figure" is often used instead of "clothes" for avatars
|
||||
if ($lowerExpected === 'clothes' && isset($actualDirs['figure'])) {
|
||||
$actualName = $actualDirs['figure'];
|
||||
} elseif (isset($actualDirs[$lowerExpected])) {
|
||||
$actualName = $actualDirs[$lowerExpected];
|
||||
} else {
|
||||
foreach ($actualDirs as $actualLower => $actual) {
|
||||
if (str_starts_with($actualLower, rtrim($lowerExpected, 's')) ||
|
||||
str_starts_with(rtrim($actualLower, 's'), rtrim($lowerExpected, 's'))) {
|
||||
$actualName = $actual;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($actualName && $actualName !== $expectedDir) {
|
||||
$value = str_replace("/{$expectedDir}/", "/{$actualName}/", $value);
|
||||
$this->line(" 🔍 Auto-detected: {$key} -> /{$actualName}/ (was /{$expectedDir}/)");
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function syncExampleFromGithub(string $buildPath, string $webroot): void
|
||||
{
|
||||
$this->info('Syncing latest examples from GitHub...');
|
||||
|
||||
$rendererRepo = setting('nitro_github_url', '');
|
||||
$repo = $this->parseRepoFromUrl($rendererRepo);
|
||||
if (! $repo) {
|
||||
$this->warn(' ⚠ No GitHub repo configured, skipping sync');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$branch = setting('nitro_github_branch', 'main');
|
||||
|
||||
$examples = [
|
||||
'renderer-config.example' => 'renderer-config.example',
|
||||
'ui-config.example' => 'ui-config.example',
|
||||
'UITexts.example' => 'UITexts.json',
|
||||
];
|
||||
|
||||
foreach (array_keys($examples) as $remoteFile) {
|
||||
$tempFile = '/tmp/' . $remoteFile . '_' . uniqid();
|
||||
$fetched = false;
|
||||
|
||||
// Try multiple paths: root, public/, nitro-client/dist/, Nitro-V3 paths
|
||||
$paths = ['', 'public/', 'nitro-client/dist/', 'dist/', 'src/', 'assets/'];
|
||||
foreach ($paths as $path) {
|
||||
$url = "https://raw.githubusercontent.com/{$repo}/{$branch}/{$path}{$remoteFile}";
|
||||
$result = Process::timeout(15)->run("curl -sL -o {$tempFile} '{$url}'");
|
||||
|
||||
if ($result->successful() && file_exists($tempFile) && filesize($tempFile) > 10) {
|
||||
$content = file_get_contents($tempFile);
|
||||
$data = @json_decode($content, true);
|
||||
|
||||
if (is_array($data) && $data !== []) {
|
||||
// Save to both buildPath and webroot
|
||||
file_put_contents($buildPath . '/' . $remoteFile, $content);
|
||||
file_put_contents($webroot . '/' . $remoteFile, $content);
|
||||
$this->line(" ✓ Synced: {$remoteFile} (" . count($data) . " keys from {$path}{$remoteFile})");
|
||||
$fetched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
if (! $fetched) {
|
||||
$this->line(" - Skipped: {$remoteFile} (not found in any path)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseRepoFromUrl(string $url): ?string
|
||||
{
|
||||
if (preg_match('/github\.com\/([^\/]+\/[^\/\?#]+)/', $url, $matches)) {
|
||||
return rtrim($matches[1], '/');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function compareConfigs(array $generated, string $name, string $webroot): void
|
||||
{
|
||||
$currentPath = $webroot . '/' . $name . '.json';
|
||||
if (! file_exists($currentPath)) {
|
||||
$this->line(" ℹ {$name}: Geen bestaande config — nieuwe generatie");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$current = @json_decode(file_get_contents($currentPath), true);
|
||||
if (! is_array($current)) {
|
||||
$this->warn(" ⚠ {$name}: Bestaande config is ongeldig JSON");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$newKeys = array_diff(array_keys($generated), array_keys($current));
|
||||
$removedKeys = array_diff(array_keys($current), array_keys($generated));
|
||||
$changedKeys = [];
|
||||
|
||||
foreach (array_intersect(array_keys($generated), array_keys($current)) as $key) {
|
||||
if ($generated[$key] !== $current[$key]) {
|
||||
$changedKeys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($newKeys !== []) {
|
||||
$this->line(" 🆕 {$name}: " . count($newKeys) . ' nieuwe key(s): ' . implode(', ', array_slice($newKeys, 0, 10)));
|
||||
if (count($newKeys) > 10) {
|
||||
$this->line(' ... en ' . (count($newKeys) - 10) . ' meer');
|
||||
}
|
||||
}
|
||||
if ($removedKeys !== []) {
|
||||
$this->line(" 🗑 {$name}: " . count($removedKeys) . ' verwijderde key(s): ' . implode(', ', array_slice($removedKeys, 0, 5)));
|
||||
}
|
||||
if ($changedKeys !== []) {
|
||||
$this->line(" 🔄 {$name}: " . count($changedKeys) . ' gewijzigde key(s): ' . implode(', ', array_slice($changedKeys, 0, 5)));
|
||||
}
|
||||
if ($newKeys === [] && $removedKeys === [] && $changedKeys === []) {
|
||||
$this->line(" ✓ {$name}: Geen wijzigingen");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class NitroUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'nitro:auto
|
||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
||||
{--build-only : Alleen build en deploy uitvoeren}
|
||||
{--full : Volledige reset en reinstall}
|
||||
{--repair : Probeer Nitro te repareren}
|
||||
{--diagnose : Toon diagnose informatie}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische Nitro client en renderer updates';
|
||||
|
||||
private const string CACHE_KEY_LAST_UPDATE = 'nitro_last_update';
|
||||
|
||||
public function handle(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('Nitro update check...');
|
||||
|
||||
if ($this->option('diagnose')) {
|
||||
return $this->diagnose($nitroService);
|
||||
}
|
||||
|
||||
if ($this->option('repair')) {
|
||||
return $this->repair($alertService, $nitroService);
|
||||
}
|
||||
|
||||
$isForced = $this->option('force');
|
||||
$buildOnly = $this->option('build-only');
|
||||
$full = $this->option('full');
|
||||
|
||||
if ($buildOnly) {
|
||||
return $this->buildOnly($alertService, $nitroService);
|
||||
}
|
||||
|
||||
if (! $isForced && ! $this->isScheduledTime()) {
|
||||
$this->line('Niet de geplande tijd, overslaan.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$repo = setting('nitro_github_url', 'duckietm/Nitro-V3 (default)');
|
||||
$this->info("GitHub: {$repo}");
|
||||
|
||||
if ($full) {
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
$status = $nitroService->getStatus();
|
||||
|
||||
$this->info('Client: ' . ($status['client_installed'] ? '✅' : '❌'));
|
||||
$this->info('Renderer: ' . ($status['renderer_installed'] ? '✅' : '❌'));
|
||||
$this->info('Build: ' . ($status['build_exists'] ? '✅' : '❌'));
|
||||
$this->info('Deployed: ' . ($status['deployed'] ? '✅' : '❌'));
|
||||
|
||||
if (! $status['client_installed'] || ! $status['renderer_installed']) {
|
||||
$this->warn('Nitro niet volledig geïnstalleerd. Voer --full uit voor volledige installatie.');
|
||||
if ($this->confirm('Wil je nu volledig installeren?')) {
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->line('✅ Nitro client is up-to-date.');
|
||||
|
||||
if ($isForced) {
|
||||
$this->warn('Force update aangevraagd...');
|
||||
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_UPDATE, now()->toIso8601String());
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function diagnose(NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('🔍 Nitro diagnose...');
|
||||
$diagnosis = $nitroService->diagnose();
|
||||
|
||||
$this->newLine();
|
||||
$this->info('=== Controle Resultaten ===');
|
||||
|
||||
$checks = $diagnosis['checks'] ?? [];
|
||||
foreach ($checks as $key => $value) {
|
||||
if (is_bool($value)) {
|
||||
$this->line(($value ? '✅' : '❌') . ' ' . $key);
|
||||
} elseif (is_array($value)) {
|
||||
$this->line($key . ': ' . count($value) . ' items');
|
||||
} else {
|
||||
$this->line($key . ': ' . $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($diagnosis['issues'])) {
|
||||
$this->newLine();
|
||||
$this->error('=== Problemen ===');
|
||||
foreach ($diagnosis['issues'] as $issue) {
|
||||
$this->line('❌ ' . $issue);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($diagnosis['recommendations'])) {
|
||||
$this->newLine();
|
||||
$this->info('=== Aanbevelingen ===');
|
||||
foreach ($diagnosis['recommendations'] as $rec) {
|
||||
$this->line('💡 ' . $rec);
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function repair(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->warn('🔧 Nitro repair modus gestart...');
|
||||
$this->info('Dit zal de Nitro installatie controleren en repareren.');
|
||||
|
||||
try {
|
||||
$repairResult = $nitroService->repair();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info('✅ Repair succesvol!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client gerepareerd',
|
||||
['actions' => $repairResult['actions'] ?? []],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line('Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
if (! empty($repairResult['errors'])) {
|
||||
$this->line('Fouten:');
|
||||
foreach ($repairResult['errors'] as $error) {
|
||||
$this->line(' ❌ ' . $error);
|
||||
}
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildOnly(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('Build en deploy uitvoeren...');
|
||||
|
||||
$buildResult = $nitroService->buildClient();
|
||||
|
||||
if (! $buildResult['success']) {
|
||||
$this->error('Build mislukt: ' . ($buildResult['error'] ?? 'Onbekend'));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$nitroService->deployClient();
|
||||
$nitroService->generateConfigs();
|
||||
|
||||
$this->info('Client succesvol gedeployed!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client opnieuw gedeployed',
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function fullReinstall(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->warn('Volledige reinstall wordt uitgevoerd...');
|
||||
|
||||
try {
|
||||
$result = $nitroService->updateNitro();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info('Nitro succesvol opnieuw geïnstalleerd!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client succesvol geüpdatet en gedeployed',
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('Reinstall mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'Nitro Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Reinstall exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function isScheduledTime(): bool
|
||||
{
|
||||
$scheduleTime = setting('nitro_auto_update_schedule', '03:00');
|
||||
$scheduleDays = setting('nitro_auto_update_days', '0,6');
|
||||
$enabled = setting('nitro_auto_update_enabled', false);
|
||||
|
||||
if (! $enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$currentTime = $now->format('H:i');
|
||||
$currentDay = (int) $now->dayOfWeek;
|
||||
|
||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
||||
|
||||
if (! in_array($currentDay, $allowedDays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($currentTime !== $scheduleTime) {
|
||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
||||
if ($minuteDiff > 5) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\NitroUpdateService;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SwitchNitroBranch extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'app:switch-nitro-branch {--branch=main}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Switch Nitro to a specific branch (runs in background)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$branch = $this->option('branch') ?? 'main';
|
||||
|
||||
$this->info("🔄 Switching Nitro to branch: {$branch}");
|
||||
|
||||
try {
|
||||
$nitroService = new NitroUpdateService;
|
||||
$result = $nitroService->updateNitro(true);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
$this->info("✅ Switched to {$branch} successfully!");
|
||||
$this->info($result['message'] ?? '');
|
||||
Log::info('[NitroSwitch] Success', ['branch' => $branch, 'message' => $result['message'] ?? '']);
|
||||
} else {
|
||||
$this->error('❌ Switch failed: ' . ($result['error'] ?? 'Unknown error'));
|
||||
Log::error('[NitroSwitch] Failed', ['branch' => $branch, 'error' => $result['error'] ?? 'Unknown']);
|
||||
}
|
||||
|
||||
Cache::forget('website_settings');
|
||||
SettingsService::clearCache();
|
||||
|
||||
return ($result['success'] ?? false) ? 0 : 1;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Exception: ' . $e->getMessage());
|
||||
Log::error('[NitroSwitch] Exception', ['branch' => $branch, 'error' => $e->getMessage()]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SystemHealthCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'system:health
|
||||
{--json : Output als JSON}
|
||||
{--details : Toon meer details}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Controleer systeem gezondheid';
|
||||
|
||||
public function handle(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$json = $this->option('json');
|
||||
$this->option('details');
|
||||
|
||||
$health = $this->performHealthCheck($emuService, $nitroService);
|
||||
|
||||
if ($json) {
|
||||
$this->line(json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $health['status'] === 'healthy' ? 0 : 1;
|
||||
}
|
||||
|
||||
$this->displayHealthCheck($health);
|
||||
|
||||
return $health['status'] === 'healthy' ? 0 : 1;
|
||||
}
|
||||
|
||||
private function performHealthCheck(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): array
|
||||
{
|
||||
$checks = [];
|
||||
$issues = [];
|
||||
$warnings = [];
|
||||
|
||||
$checks['timestamp'] = now()->toIso8601String();
|
||||
|
||||
$checks['php'] = [
|
||||
'version' => PHP_VERSION,
|
||||
'status' => 'ok',
|
||||
];
|
||||
|
||||
$checks['system'] = [
|
||||
'os' => PHP_OS,
|
||||
'user' => posix_getpwuid(posix_geteuid())['name'] ?? 'unknown',
|
||||
];
|
||||
|
||||
$diskFree = @disk_free_space('/');
|
||||
$diskTotal = @disk_total_space('/');
|
||||
$diskPercent = $diskTotal > 0 ? round(($diskFree / $diskTotal) * 100, 1) : 0;
|
||||
$checks['disk'] = [
|
||||
'free' => $this->formatBytes($diskFree),
|
||||
'total' => $this->formatBytes($diskTotal),
|
||||
'percent_free' => $diskPercent,
|
||||
'status' => $diskPercent > 10 ? 'ok' : 'critical',
|
||||
];
|
||||
if ($diskPercent < 10) {
|
||||
$issues[] = 'Disk space critically low: ' . $diskPercent . '% free';
|
||||
} elseif ($diskPercent < 20) {
|
||||
$warnings[] = 'Disk space low: ' . $diskPercent . '% free';
|
||||
}
|
||||
|
||||
try {
|
||||
$emuDiagnosis = $emuService->diagnose();
|
||||
$checks['emulator'] = [
|
||||
'configured' => $emuDiagnosis['checks']['is_configured'] ?? false,
|
||||
'jar_exists' => $emuDiagnosis['checks']['jar_exists'] ?? false,
|
||||
'service_running' => $emuDiagnosis['checks']['service_running'] ?? false,
|
||||
'db_connected' => $emuDiagnosis['checks']['emulator_db_connected'] ?? false,
|
||||
'status' => empty($emuDiagnosis['issues']) ? 'ok' : 'issues',
|
||||
'issues' => $emuDiagnosis['issues'] ?? [],
|
||||
];
|
||||
|
||||
if (! empty($emuDiagnosis['issues'])) {
|
||||
foreach ($emuDiagnosis['issues'] as $issue) {
|
||||
$issues[] = 'Emulator: ' . $issue;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['emulator'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
$issues[] = 'Emulator check failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$nitroDiagnosis = $nitroService->diagnose();
|
||||
$checks['nitro'] = [
|
||||
'client_installed' => $nitroDiagnosis['checks']['client_installed'] ?? false,
|
||||
'renderer_installed' => $nitroDiagnosis['checks']['renderer_installed'] ?? false,
|
||||
'deployed' => $nitroDiagnosis['checks']['deployed'] ?? false,
|
||||
'status' => empty($nitroDiagnosis['issues']) ? 'ok' : 'issues',
|
||||
'issues' => $nitroDiagnosis['issues'] ?? [],
|
||||
];
|
||||
|
||||
if (! empty($nitroDiagnosis['issues'])) {
|
||||
foreach ($nitroDiagnosis['issues'] as $issue) {
|
||||
$issues[] = 'Nitro: ' . $issue;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['nitro'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
$issues[] = 'Nitro check failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$sqlDiagnosis = $emuService->diagnoseSqlUpdates();
|
||||
$checks['sql_updates'] = [
|
||||
'table_exists' => $sqlDiagnosis['table_exists'] ?? false,
|
||||
'applied' => $sqlDiagnosis['applied_count'] ?? 0,
|
||||
'pending' => $sqlDiagnosis['pending_count'] ?? 0,
|
||||
'status' => ($sqlDiagnosis['pending_count'] ?? 0) > 0 ? 'pending' : 'ok',
|
||||
];
|
||||
|
||||
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
|
||||
$warnings[] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['sql_updates'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$webserverCheck = Process::timeout(5)->run('which nginx || which apache2 || which httpd');
|
||||
$checks['webserver'] = [
|
||||
'installed' => $webserverCheck->successful(),
|
||||
'status' => $webserverCheck->successful() ? 'ok' : 'unknown',
|
||||
];
|
||||
|
||||
$mysqlCheck = Process::timeout(5)->run('which mysql || which mariadb');
|
||||
$checks['database'] = [
|
||||
'client_installed' => $mysqlCheck->successful(),
|
||||
'status' => $mysqlCheck->successful() ? 'ok' : 'unknown',
|
||||
];
|
||||
|
||||
$nodeCheck = Process::timeout(5)->run('which node && which yarn');
|
||||
$checks['node'] = [
|
||||
'installed' => $nodeCheck->successful(),
|
||||
'status' => $nodeCheck->successful() ? 'ok' : 'missing',
|
||||
];
|
||||
if (! $nodeCheck->successful()) {
|
||||
$warnings[] = 'Node.js/Yarn niet geïnstalleerd';
|
||||
}
|
||||
|
||||
$status = $issues === [] ? 'healthy' : ($warnings === [] ? 'warning' : 'degraded');
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'checks' => $checks,
|
||||
'issues' => $issues,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
private function displayHealthCheck(array $health): void
|
||||
{
|
||||
$status = $health['status'];
|
||||
|
||||
$statusIcon = match ($status) {
|
||||
'healthy' => '✅',
|
||||
'warning' => '⚠️',
|
||||
'degraded' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
|
||||
$this->info("{$statusIcon} Systeem Gezondheid: " . strtoupper($status));
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
$checks = $health['checks'] ?? [];
|
||||
|
||||
$this->line('📦 Systeem:');
|
||||
$this->line(' PHP: ' . ($checks['php']['version'] ?? '?'));
|
||||
$this->line(' User: ' . ($checks['system']['user'] ?? '?'));
|
||||
|
||||
$diskStatus = $checks['disk']['status'] ?? '?';
|
||||
$diskIcon = $diskStatus === 'ok' ? '✅' : '❌';
|
||||
$this->line(" {$diskIcon} Disk: " . ($checks['disk']['percent_free'] ?? '?') . '% vrij');
|
||||
|
||||
$emuStatus = $checks['emulator']['status'] ?? '?';
|
||||
$emuIcon = $emuStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$emuIcon} Emulator: " . ucfirst($emuStatus));
|
||||
|
||||
$nitroStatus = $checks['nitro']['status'] ?? '?';
|
||||
$nitroIcon = $nitroStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$nitroIcon} Nitro: " . ucfirst($nitroStatus));
|
||||
|
||||
$sqlStatus = $checks['sql_updates']['status'] ?? '?';
|
||||
$sqlIcon = $sqlStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$sqlIcon} SQL Updates: " . ucfirst($sqlStatus) . ' (' . ($checks['sql_updates']['applied'] ?? 0) . ' toegepast)');
|
||||
|
||||
$nodeStatus = $checks['node']['status'] ?? '?';
|
||||
$nodeIcon = $nodeStatus === 'ok' ? '✅' : '❌';
|
||||
$this->line(" {$nodeIcon} Node.js: " . ucfirst($nodeStatus));
|
||||
|
||||
if (! empty($health['issues'])) {
|
||||
$this->line('');
|
||||
$this->error('❌ Problemen:');
|
||||
foreach ($health['issues'] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($health['warnings'])) {
|
||||
$this->line('');
|
||||
$this->warn('⚠️ Waarschuwingen:');
|
||||
foreach ($health['warnings'] as $warning) {
|
||||
$this->line(' - ' . $warning);
|
||||
}
|
||||
}
|
||||
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
if ($status === 'healthy') {
|
||||
$this->info('✅ Alles werkt correct!');
|
||||
} else {
|
||||
$this->warn('Run "php artisan system:repair" om problemen op te lossen');
|
||||
}
|
||||
}
|
||||
|
||||
private function formatBytes(?float $bytes): string
|
||||
{
|
||||
if ($bytes === null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$i = 0;
|
||||
|
||||
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||
$bytes /= 1024;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$i];
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SystemRepairCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'system:repair
|
||||
{--emu : Alleen emulator repareren}
|
||||
{--nitro : Alleen Nitro repareren}
|
||||
{--check : Alleen controleren, niet repareren}
|
||||
{--force : Forceer reparatie ook al is alles OK}
|
||||
{--full : Volledige reset en reinstall}
|
||||
{--nuke : Alles verwijderen en opnieuw installeren (gevaarlijk!)}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische systeem reparatie voor emulator en Nitro';
|
||||
|
||||
private const string CACHE_KEY_LAST_REPAIR = 'system_last_repair';
|
||||
|
||||
public function handle(AlertService $alertService, EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('🔧 System Repair Service gestart...');
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
$checkOnly = $this->option('check');
|
||||
$force = $this->option('force');
|
||||
$full = $this->option('full');
|
||||
$emuOnly = $this->option('emu');
|
||||
$nitroOnly = $this->option('nitro');
|
||||
|
||||
$results = [
|
||||
'emulator' => null,
|
||||
'nitro' => null,
|
||||
];
|
||||
|
||||
if (! $nitroOnly) {
|
||||
$results['emulator'] = $this->repairEmulator($emuService, $checkOnly, $force, $full);
|
||||
}
|
||||
|
||||
if (! $emuOnly) {
|
||||
$results['nitro'] = $this->repairNitro($nitroService, $checkOnly, $force, $full);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_REPAIR, now()->toIso8601String());
|
||||
|
||||
$emuOk = $results['emulator'] === null || ($results['emulator']['success'] ?? false);
|
||||
$nitroOk = $results['nitro'] === null || ($results['nitro']['success'] ?? false);
|
||||
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
if ($emuOk && $nitroOk) {
|
||||
$this->info('✅ Alle systemen OK');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
private function ensureBaseDirectories(): void
|
||||
{
|
||||
$basePaths = [
|
||||
'/var/www',
|
||||
'/var/www/atomcms',
|
||||
storage_path('app'),
|
||||
];
|
||||
|
||||
foreach ($basePaths as $path) {
|
||||
if (! is_dir($path)) {
|
||||
@mkdir($path, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
Process::timeout(5)->run('chown -R www-data:www-data /var/www/atomcms 2>/dev/null || true');
|
||||
Process::timeout(5)->run('chmod -R 755 /var/www/atomcms 2>/dev/null || true');
|
||||
}
|
||||
|
||||
private function repairEmulator(EmulatorUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
||||
{
|
||||
$this->info('Emulator controleren...');
|
||||
|
||||
$this->ensureBaseDirectories();
|
||||
|
||||
try {
|
||||
$diagnosis = $service->diagnose();
|
||||
|
||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
||||
$this->line(' ✅ Emulator is OK');
|
||||
|
||||
return ['success' => true, 'status' => 'ok'];
|
||||
}
|
||||
|
||||
if ($checkOnly) {
|
||||
$this->warn(' ⚠️ Emulator problemen gevonden:');
|
||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
||||
$this->line(' 💡 ' . $rec);
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
||||
}
|
||||
|
||||
$this->warn(' 🔧 Emulator wordt gerepareerd...');
|
||||
|
||||
if ($full) {
|
||||
$this->line(' 📦 Volledige reset...');
|
||||
$repairResult = $service->repairEmulator();
|
||||
} else {
|
||||
$repairResult = $service->repairEmulator();
|
||||
}
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info(' ✅ Emulator gerepareerd!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
|
||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
||||
}
|
||||
|
||||
$this->error(' ❌ Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line(' Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ❌ Emulator exception: ' . $e->getMessage());
|
||||
Log::error('[SystemRepair] Emulator exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function repairNitro(NitroUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
||||
{
|
||||
$this->info('Nitro controleren...');
|
||||
|
||||
$this->ensureBaseDirectories();
|
||||
|
||||
try {
|
||||
$diagnosis = $service->diagnose();
|
||||
|
||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
||||
$this->line(' ✅ Nitro is OK');
|
||||
|
||||
return ['success' => true, 'status' => 'ok'];
|
||||
}
|
||||
|
||||
if ($checkOnly) {
|
||||
$this->warn(' ⚠️ Nitro problemen gevonden:');
|
||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
||||
$this->line(' 💡 ' . $rec);
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
||||
}
|
||||
|
||||
$this->warn(' 🔧 Nitro wordt gerepareerd...');
|
||||
|
||||
if ($full) {
|
||||
$this->line(' 📦 Volledige reset...');
|
||||
$repairResult = $service->updateNitro();
|
||||
} else {
|
||||
$repairResult = $service->repair();
|
||||
}
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info(' ✅ Nitro gerepareerd!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
|
||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
||||
}
|
||||
|
||||
$this->error(' ❌ Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line(' Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ❌ Nitro exception: ' . $e->getMessage());
|
||||
Log::error('[SystemRepair] Nitro exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\AutoUpdateCommand;
|
||||
use App\Console\Commands\DDoSDetectionCommand;
|
||||
use App\Console\Commands\EmulatorMonitorCommand;
|
||||
use App\Console\Commands\EmulatorUpdateCommand;
|
||||
use App\Console\Commands\FixCodeCommand;
|
||||
use App\Console\Commands\GenerateNitroConfigs;
|
||||
use App\Console\Commands\NitroUpdateCommand;
|
||||
use App\Console\Commands\SystemCheckCommand;
|
||||
use App\Console\Commands\SystemHealthCommand;
|
||||
use App\Console\Commands\SystemRepairCommand;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
@@ -28,9 +22,6 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('monitor:ddos')->everyFiveMinutes()->withoutOverlapping();
|
||||
$schedule->command('update:auto')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('nitro:auto')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('system:repair')->everyTenMinutes()->withoutOverlapping();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
@@ -42,12 +33,6 @@ class Kernel extends ConsoleKernel
|
||||
$this->commands[] = FixCodeCommand::class;
|
||||
$this->commands[] = EmulatorMonitorCommand::class;
|
||||
$this->commands[] = DDoSDetectionCommand::class;
|
||||
$this->commands[] = EmulatorUpdateCommand::class;
|
||||
$this->commands[] = AutoUpdateCommand::class;
|
||||
$this->commands[] = NitroUpdateCommand::class;
|
||||
$this->commands[] = SystemRepairCommand::class;
|
||||
$this->commands[] = SystemHealthCommand::class;
|
||||
$this->commands[] = GenerateNitroConfigs::class;
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ final class ThemeSettings extends Page implements HasForms
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static ?string $title = 'Theme & Button Settings';
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('Theme & Button Settings');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected string $view = 'filament.pages.general.theme-settings';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Executable → Regular
+14
-326
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Pages\Monitoring;
|
||||
|
||||
use App\Actions\Commandocentrum\EmulatorControlAction;
|
||||
use App\Actions\Commandocentrum\NitroControlAction;
|
||||
use App\Enums\AlertSeverity;
|
||||
use App\Models\Miscellaneous\WebsitePermission;
|
||||
use App\Models\StaffActivity;
|
||||
@@ -16,7 +15,6 @@ use App\Services\Diagnostics\DiagnosticRunner;
|
||||
use App\Services\GitHubService;
|
||||
use App\Services\RconService;
|
||||
use App\Services\SettingsService;
|
||||
use App\Services\UpdateHistoryService;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@@ -106,21 +104,15 @@ final class Commandocentrum extends Page implements HasForms
|
||||
'emulator_database_username' => $this->getSetting('emulator_database_username', ''),
|
||||
'emulator_database_password' => $this->getSetting('emulator_database_password', ''),
|
||||
'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'),
|
||||
'auto_update_enabled' => $this->getSettingBool('auto_update_enabled'),
|
||||
'auto_update_schedule' => $this->getSetting('auto_update_schedule', '03:00'),
|
||||
'auto_update_days' => $this->getSetting('auto_update_days', '0,6'),
|
||||
'nitro_client_path' => $this->getSetting('nitro_client_path', $paths['nitro_client_path']),
|
||||
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', $paths['nitro_renderer_path']),
|
||||
'nitro_build_path' => $this->getSetting('nitro_build_path', $paths['nitro_build_path']),
|
||||
'nitro_webroot' => $this->getSetting('nitro_webroot', $paths['nitro_webroot']),
|
||||
'gamedata_path' => $this->getSetting('gamedata_path', $paths['gamedata_path']),
|
||||
'nitro_github_branch' => $this->getSetting('nitro_github_branch', 'main'),
|
||||
'nitro_github_url' => $this->getSetting('nitro_github_url', ''),
|
||||
'nitro_site_url' => $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()),
|
||||
'nitro_auto_update_configs' => $this->getSettingBool('nitro_auto_update_configs'),
|
||||
'nitro_auto_update_enabled' => $this->getSettingBool('nitro_auto_update_enabled'),
|
||||
'nitro_auto_update_schedule' => $this->getSetting('nitro_auto_update_schedule', '03:00'),
|
||||
'nitro_auto_update_days' => $this->getSetting('nitro_auto_update_days', '0,6'),
|
||||
'nitro_emulator_path' => $this->getSetting('nitro_emulator_path', '/var/www/emulator'),
|
||||
'nitro_emulator_service' => $this->getSetting('nitro_emulator_service', 'emulator'),
|
||||
'nitro_db_name' => $this->getSetting('nitro_db_name', 'habbo'),
|
||||
'nitro_sql_dir' => $this->getSetting('nitro_sql_dir', '/var/www/emulator/Database Updates'),
|
||||
'nitro_backup_dir' => $this->getSetting('nitro_backup_dir', '/var/www/emulator/Database Updates/backups'),
|
||||
'nitro_gamedata_dir' => $this->getSetting('nitro_gamedata_dir', '/var/www/Gamedata/config'),
|
||||
'nitro_client_dir' => $this->getSetting('nitro_client_dir', '/var/www/Nitro-V3/public/configuration'),
|
||||
'nitro_client_src' => $this->getSetting('nitro_client_src', '/var/www/Nitro-V3'),
|
||||
'nitro_renderer_src' => $this->getSetting('nitro_renderer_src', '/var/www/Nitro_Render_V3'),
|
||||
'hotel_alert_message' => '',
|
||||
];
|
||||
}
|
||||
@@ -233,90 +225,13 @@ final class Commandocentrum extends Page implements HasForms
|
||||
->content(fn () => $this->renderEmulatorInfoView()),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.emulator_updates'))
|
||||
->description(__('commandocentrum.emulator_updates_desc'))
|
||||
->icon('heroicon-o-arrow-down-circle')
|
||||
->afterHeader([
|
||||
Action::make('check_updates')
|
||||
->label(__('commandocentrum.check_updates'))
|
||||
->color('info')
|
||||
->action('checkEmulatorUpdates'),
|
||||
Action::make('build_emulator')
|
||||
->label('🔨 ' . __('commandocentrum.build'))
|
||||
->color('success')
|
||||
->action('buildEmulator'),
|
||||
Action::make('run_sql')
|
||||
->label(__('commandocentrum.sql_updates'))
|
||||
->color('purple')
|
||||
->action('runSqlUpdates'),
|
||||
Action::make('save_emulator')
|
||||
->label(__('commandocentrum.save'))
|
||||
->color('primary')
|
||||
->action('saveEmulator'),
|
||||
])
|
||||
Section::make(__('commandocentrum.nitro_update'))
|
||||
->description(__('commandocentrum.nitro_update_desc'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->schema([
|
||||
Placeholder::make('emulator_settings')
|
||||
Placeholder::make('nitro_cli_only')
|
||||
->label('')
|
||||
->content(fn () => $this->renderEmulatorSettingsView()),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.emulator_backups'))
|
||||
->description(__('commandocentrum.emulator_backups_desc'))
|
||||
->icon('heroicon-s-archive-box')
|
||||
->schema([
|
||||
Placeholder::make('backups_list')
|
||||
->label('')
|
||||
->content(fn () => $this->renderBackupsListView()),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.nitro_client'))
|
||||
->description(__('commandocentrum.nitro_client_desc'))
|
||||
->icon('heroicon-o-cloud-arrow-down')
|
||||
->afterHeader([
|
||||
Action::make('detect_paths')
|
||||
->label('🔍 ' . __('commandocentrum.auto_detect'))
|
||||
->color('success')
|
||||
->action('detectAndSavePaths'),
|
||||
Action::make('check_nitro')
|
||||
->label(__('commandocentrum.check'))
|
||||
->color('info')
|
||||
->action('checkNitroUpdates'),
|
||||
Action::make('build_nitro')
|
||||
->label(__('commandocentrum.build'))
|
||||
->color('pink')
|
||||
->action('buildNitro'),
|
||||
Action::make('generate_configs')
|
||||
->label(__('commandocentrum.generate_configs'))
|
||||
->color('indigo')
|
||||
->action('generateNitroConfigs'),
|
||||
Action::make('save_nitro')
|
||||
->label(__('commandocentrum.save'))
|
||||
->color('primary')
|
||||
->action('saveNitro'),
|
||||
])
|
||||
->schema([
|
||||
Placeholder::make('nitro_settings')
|
||||
->label('')
|
||||
->content(fn () => $this->renderNitroSettingsView()),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.auto_updates'))
|
||||
->description(__('commandocentrum.auto_updates_desc'))
|
||||
->icon('heroicon-o-clock')
|
||||
->columns(2)
|
||||
->afterHeader([
|
||||
Action::make('save_auto')
|
||||
->label(__('commandocentrum.save'))
|
||||
->color('primary')
|
||||
->action('saveAutoUpdate'),
|
||||
])
|
||||
->schema([
|
||||
Toggle::make('auto_update_enabled')
|
||||
->label(__('commandocentrum.enable_auto_updates')),
|
||||
TextInput::make('auto_update_schedule')
|
||||
->label(__('commandocentrum.schedule')),
|
||||
TextInput::make('auto_update_days')
|
||||
->label(__('commandocentrum.days')),
|
||||
->content(__('commandocentrum.nitro_cli_only')),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.clothing_sync'))
|
||||
@@ -367,15 +282,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
->helperText(__('commandocentrum.discord_ranks_helper')),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.update_history'))
|
||||
->description(__('commandocentrum.update_history_desc'))
|
||||
->icon('heroicon-o-clock')
|
||||
->schema([
|
||||
Placeholder::make('history')
|
||||
->label('')
|
||||
->content(fn () => $this->renderUpdateHistoryView()),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.social_login'))
|
||||
->description(__('commandocentrum.social_login_desc'))
|
||||
->icon('heroicon-o-user-circle')
|
||||
@@ -491,100 +397,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderEmulatorSettingsView(): View
|
||||
{
|
||||
return view('filament.components.commandocentrum.emulator-settings', [
|
||||
'emulatorBranchesHtml' => $this->getEmulatorBranchesHtml(),
|
||||
'emulatorStatusHtml' => $this->renderEmulatorStatusView()->render(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderEmulatorStatusView(): View
|
||||
{
|
||||
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
|
||||
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
|
||||
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source');
|
||||
$githubUrl = $this->getSetting('emulator_github_url', '');
|
||||
$branch = $this->getSetting('emulator_github_branch', 'main');
|
||||
|
||||
$jarExists = $this->fileExists($jarPath);
|
||||
$sourceExists = $this->fileExists($sourcePath);
|
||||
$sourceCommit = $this->getGitCommit($sourcePath);
|
||||
$remoteVersion = $githubUrl !== '' && $githubUrl !== '0' ? $this->getRemoteCommit($githubUrl, $branch) : 'N/A';
|
||||
|
||||
$canBuild = false;
|
||||
$checkDirs = [
|
||||
$sourcePath,
|
||||
$sourcePath . '/Emulator',
|
||||
$sourcePath . '/Emulator/Emulator',
|
||||
$sourcePath . '/emulator',
|
||||
$sourcePath . '/emulator/emulator',
|
||||
];
|
||||
foreach ($checkDirs as $dir) {
|
||||
$check = $this->runCommand('test -f ' . escapeshellarg($dir . '/pom.xml') . ' && echo yes');
|
||||
if ($check && trim($check) === 'yes') {
|
||||
$canBuild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return view('filament.components.commandocentrum.emulator-status', [
|
||||
'emulatorOnline' => $this->getEmulatorStatusText() === 'Online',
|
||||
'jarExists' => $jarExists,
|
||||
'serviceName' => $serviceName,
|
||||
'sourceCommit' => $sourceCommit,
|
||||
'remoteVersion' => $remoteVersion,
|
||||
'canBuild' => $canBuild,
|
||||
'jarPath' => $jarPath,
|
||||
'sourcePath' => $sourcePath,
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderNitroSettingsView(): View
|
||||
{
|
||||
return view('filament.components.commandocentrum.nitro-settings', [
|
||||
'nitroBranchesHtml' => $this->getNitroBranchesHtml(),
|
||||
'nitroStatusHtml' => $this->renderNitroStatusView()->render(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderNitroStatusView(): View
|
||||
{
|
||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
||||
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
|
||||
$clientGithubUrl = $this->getSetting('nitro_github_url', '');
|
||||
$rendererGithubUrl = $this->getSetting('nitro_renderer_github_url', 'https://github.com/duckietm/Nitro_Render_V3');
|
||||
|
||||
$clientCommit = $this->getGitCommit($clientPath);
|
||||
$rendererCommit = $this->getGitCommit($rendererPath);
|
||||
$clientRemote = $clientGithubUrl !== '' && $clientGithubUrl !== '0' ? $this->getRemoteCommit($clientGithubUrl, $this->getSetting('nitro_github_branch', 'main')) : 'N/A';
|
||||
$rendererRemote = $rendererGithubUrl !== '' && $rendererGithubUrl !== '0' ? $this->getRemoteCommit($rendererGithubUrl, $this->getSetting('nitro_renderer_github_branch', 'main')) : 'N/A';
|
||||
|
||||
return view('filament.components.commandocentrum.nitro-status', [
|
||||
'clientExists' => $this->checkPathExists($clientPath),
|
||||
'rendererExists' => $this->checkPathExists($rendererPath),
|
||||
'webrootExists' => $this->checkPathExists($webroot),
|
||||
'clientCommit' => $clientCommit,
|
||||
'rendererCommit' => $rendererCommit,
|
||||
'clientRemote' => $clientRemote,
|
||||
'rendererRemote' => $rendererRemote,
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderBackupsListView(): View
|
||||
{
|
||||
try {
|
||||
$backups = app(EmulatorControlAction::class)->getBackups();
|
||||
} catch (Exception) {
|
||||
$backups = [];
|
||||
}
|
||||
|
||||
return view('filament.components.commandocentrum.backups-list', [
|
||||
'backups' => $backups,
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderClothingStatusView(): View
|
||||
{
|
||||
try {
|
||||
@@ -614,19 +426,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
]);
|
||||
}
|
||||
|
||||
private function renderUpdateHistoryView(): View
|
||||
{
|
||||
try {
|
||||
$history = app(UpdateHistoryService::class)->getRecent(10);
|
||||
} catch (Exception) {
|
||||
$history = [];
|
||||
}
|
||||
|
||||
return view('filament.components.commandocentrum.update-history', [
|
||||
'history' => $history,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getSetting(string $key, string $default = ''): string
|
||||
{
|
||||
try {
|
||||
@@ -878,33 +677,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
}
|
||||
}
|
||||
|
||||
public function checkEmulatorUpdates(): void
|
||||
{
|
||||
$result = app(EmulatorControlAction::class)->update();
|
||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
||||
Cache::forget('all_updates_check');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
public function buildEmulator(): void
|
||||
{
|
||||
$result = app(EmulatorControlAction::class)->build();
|
||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
||||
}
|
||||
|
||||
public function runSqlUpdates(): void
|
||||
{
|
||||
$result = app(EmulatorControlAction::class)->runSqlUpdates();
|
||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
||||
}
|
||||
|
||||
public function restoreBackup(string $backupName): void
|
||||
{
|
||||
$result = app(EmulatorControlAction::class)->restoreBackup($backupName);
|
||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), $result['success'] ? 'success' : 'danger');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
public function saveEmulator(): void
|
||||
{
|
||||
try {
|
||||
@@ -924,38 +696,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
}
|
||||
}
|
||||
|
||||
public function checkNitroUpdates(): void
|
||||
{
|
||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
||||
$branch = $this->getSetting('nitro_github_branch', 'main');
|
||||
|
||||
$result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
|
||||
$this->notify(__('commandocentrum.success'), $result['message'], 'success');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
public function buildNitro(): void
|
||||
{
|
||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
||||
$branch = $this->getSetting('nitro_github_branch', 'main');
|
||||
|
||||
$result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
|
||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.warning'), $result['message'], $result['success'] ? 'success' : 'warning');
|
||||
}
|
||||
|
||||
public function generateNitroConfigs(): void
|
||||
{
|
||||
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
|
||||
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
|
||||
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
|
||||
|
||||
$result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath);
|
||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
public function syncClothing(): void
|
||||
{
|
||||
try {
|
||||
@@ -972,58 +712,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
}
|
||||
}
|
||||
|
||||
public function detectAndSavePaths(): void
|
||||
{
|
||||
try {
|
||||
$paths = $this->autoDetectPaths();
|
||||
$settings = app(SettingsService::class);
|
||||
|
||||
$settings->set('nitro_client_path', $paths['nitro_client_path']);
|
||||
$settings->set('nitro_renderer_path', $paths['nitro_renderer_path']);
|
||||
$settings->set('nitro_build_path', $paths['nitro_build_path']);
|
||||
$settings->set('nitro_webroot', $paths['nitro_webroot']);
|
||||
$settings->set('gamedata_path', $paths['gamedata_path']);
|
||||
$settings->set('emulator_jar_path', $paths['emulator_jar_path']);
|
||||
$settings->set('emulator_source_path', $paths['emulator_source_path']);
|
||||
|
||||
$this->fillForm();
|
||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.paths_detected'), 'success');
|
||||
} catch (Exception $e) {
|
||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveNitro(): void
|
||||
{
|
||||
try {
|
||||
$settings = app(SettingsService::class);
|
||||
$settings->set('nitro_client_path', $this->data['nitro_client_path'] ?? '/var/www/atomcms/nitro-client');
|
||||
$settings->set('nitro_renderer_path', $this->data['nitro_renderer_path'] ?? '/var/www/atomcms/nitro-renderer');
|
||||
$settings->set('nitro_build_path', $this->data['nitro_build_path'] ?? '/var/www/atomcms/nitro-client/dist');
|
||||
$settings->set('nitro_webroot', $this->data['nitro_webroot'] ?? '/var/www/Client');
|
||||
$settings->set('gamedata_path', $this->data['gamedata_path'] ?? '/var/www/Gamedata');
|
||||
$settings->set('nitro_github_url', $this->data['nitro_github_url'] ?? '');
|
||||
$settings->set('nitro_github_branch', $this->data['nitro_github_branch'] ?? 'main');
|
||||
$settings->set('nitro_site_url', $this->data['nitro_site_url'] ?? '');
|
||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.nitro_settings_saved'), 'success');
|
||||
} catch (Exception $e) {
|
||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveAutoUpdate(): void
|
||||
{
|
||||
try {
|
||||
$settings = app(SettingsService::class);
|
||||
$settings->set('auto_update_enabled', ($this->data['auto_update_enabled'] ?? false) ? '1' : '0');
|
||||
$settings->set('auto_update_schedule', $this->data['auto_update_schedule'] ?? '03:00');
|
||||
$settings->set('auto_update_days', $this->data['auto_update_days'] ?? '0,6');
|
||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.auto_update_saved'), 'success');
|
||||
} catch (Exception $e) {
|
||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveAlerts(): void
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Filament\Pages\Radio;
|
||||
|
||||
use App\Models\RadioApiKey;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
@@ -82,23 +81,19 @@ final class ApiKeys extends Page implements HasTable
|
||||
->label('Aangemaakt')
|
||||
->dateTime('d-m-Y H:i'),
|
||||
])
|
||||
->recordActions(function ($record) {
|
||||
return [
|
||||
ActionGroup::make([
|
||||
->actions([
|
||||
Action::make('toggle')
|
||||
->label($record->is_active ? 'Deactiveren' : 'Activeren')
|
||||
->icon($record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||
->action(fn () => $this->toggleKey($record)),
|
||||
->label(fn ($record) => $record->is_active ? 'Deactiveren' : 'Activeren')
|
||||
->icon(fn ($record) => $record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||
->action(fn ($record) => $this->toggleKey($record)),
|
||||
Action::make('delete')
|
||||
->label('Verwijderen')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn () => $record->delete()),
|
||||
]),
|
||||
];
|
||||
})
|
||||
->toolbarActions([
|
||||
->action(fn ($record) => $record->delete()),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create')
|
||||
->label('Nieuwe API Sleutel')
|
||||
->icon('heroicon-o-plus')
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Filament\Pages\Radio;
|
||||
|
||||
use App\Models\RadioAutoDjTrack;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
@@ -74,31 +73,27 @@ final class AutoDjPlaylist extends Page implements HasTable
|
||||
->trueColor('success')
|
||||
->falseColor('danger'),
|
||||
])
|
||||
->recordActions(function ($record) {
|
||||
return [
|
||||
ActionGroup::make([
|
||||
->actions([
|
||||
Action::make('toggle_active')
|
||||
->label($record->is_active ? 'Deactiveren' : 'Activeren')
|
||||
->icon($record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||
->action(fn () => $this->toggleActive($record)),
|
||||
->label(fn ($record) => $record->is_active ? 'Deactiveren' : 'Activeren')
|
||||
->icon(fn ($record) => $record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||
->action(fn ($record) => $this->toggleActive($record)),
|
||||
Action::make('move_up')
|
||||
->label('Omhoog')
|
||||
->icon('heroicon-o-chevron-up')
|
||||
->action(fn () => $this->moveUp($record)),
|
||||
->action(fn ($record) => $this->moveUp($record)),
|
||||
Action::make('move_down')
|
||||
->label('Omlaag')
|
||||
->icon('heroicon-o-chevron-down')
|
||||
->action(fn () => $this->moveDown($record)),
|
||||
->action(fn ($record) => $this->moveDown($record)),
|
||||
Action::make('delete')
|
||||
->label('Verwijderen')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn () => $record->delete()),
|
||||
]),
|
||||
];
|
||||
})
|
||||
->toolbarActions([
|
||||
->action(fn ($record) => $record->delete()),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create')
|
||||
->label('Track Toevoegen')
|
||||
->icon('heroicon-o-plus')
|
||||
|
||||
@@ -50,6 +50,7 @@ final class EmbedCode extends Page implements HasForms
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->statePath('data')
|
||||
->components([
|
||||
Section::make('Embed Configuratie')
|
||||
->description('Pas het uiterlijk van de embed player aan')
|
||||
|
||||
@@ -196,7 +196,7 @@ final class PointsSettings extends Page implements HasForms
|
||||
|
||||
public function resetLeaderboard(): void
|
||||
{
|
||||
User::where('radio_points', '>', 0)->update(['radio_points' => 0]);
|
||||
User::query()->where('radio_points', '>', 0)->each(fn (User $u) => $u->forceFill(['radio_points' => 0])->save());
|
||||
RadioListenerPoint::query()->delete();
|
||||
|
||||
$this->pointsService->clearLeaderboardCache();
|
||||
|
||||
@@ -30,10 +30,16 @@ class BadgeTextEditorResource extends Resource
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-pencil-square';
|
||||
|
||||
#[\Override]
|
||||
protected static ?string $navigationLabel = 'Badge Editor';
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('Badge Editor');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static ?string $modelLabel = 'Badge Text';
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('Badge Text');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static ?string $slug = 'hotel/badge-text-editor';
|
||||
@@ -45,16 +51,16 @@ class BadgeTextEditorResource extends Resource
|
||||
->components([
|
||||
TextInput::make('badge_key')
|
||||
->required()
|
||||
->label('Badge Key - Expl. ATOM101')
|
||||
->placeholder('This is the badge code'),
|
||||
->label(__('Badge Key - Expl. ATOM101'))
|
||||
->placeholder(__('This is the badge code')),
|
||||
TextInput::make('badge_name')
|
||||
->required()
|
||||
->label('Badge Name')
|
||||
->placeholder('This is the name of the badge: Expl. The ATOM Badge'),
|
||||
->label(__('Badge Name'))
|
||||
->placeholder(__('This is the name of the badge: Expl. The ATOM Badge')),
|
||||
Textarea::make('badge_description')
|
||||
->required()
|
||||
->label('Badge Description')
|
||||
->placeholder('Please add a description for the badge.'),
|
||||
->label(__('Badge Description'))
|
||||
->placeholder(__('Please add a description for the badge.')),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -67,7 +73,7 @@ class BadgeTextEditorResource extends Resource
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('badge_key')
|
||||
->label('Badge Image')
|
||||
->label(__('Badge Image'))
|
||||
->getStateUsing(function ($record) use ($badgesPath) {
|
||||
$badgeName = str_replace('badge_desc_', '', $record->badge_key);
|
||||
|
||||
@@ -76,7 +82,7 @@ class BadgeTextEditorResource extends Resource
|
||||
->width(50)
|
||||
->height(50),
|
||||
TextColumn::make('badge_name')
|
||||
->label('Badge Code & Name')
|
||||
->label(__('Badge Code & Name'))
|
||||
->formatStateUsing(fn ($record) => $record->badge_key . ' : ' . $record->badge_name)
|
||||
->searchable(query: function ($query, $search) {
|
||||
$query->where('badge_key', 'like', "%{$search}%")
|
||||
@@ -84,7 +90,7 @@ class BadgeTextEditorResource extends Resource
|
||||
})
|
||||
->sortable(),
|
||||
TextColumn::make('badge_description')
|
||||
->label('Badge Description')
|
||||
->label(__('Badge Description'))
|
||||
->getStateUsing(fn ($record) => Str::limit($record->badge_description, 65))
|
||||
->searchable(),
|
||||
])
|
||||
|
||||
@@ -69,8 +69,7 @@ class ListBadgeTextEditors extends ListRecords
|
||||
|
||||
$jsonData = json_decode(file_get_contents($jsonPath), true);
|
||||
|
||||
$badges = WebsiteBadge::all();
|
||||
$badgeKeys = $badges->pluck('badge_key')->toArray();
|
||||
$badgeKeys = WebsiteBadge::pluck('badge_key')->toArray();
|
||||
|
||||
foreach ($jsonData as $key => $value) {
|
||||
if (
|
||||
|
||||
@@ -26,19 +26,22 @@ class CatalogEditorResource extends Resource
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Hotel';
|
||||
|
||||
#[\Override]
|
||||
protected static ?string $navigationLabel = 'Catalog Editor';
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('Catalog Editor');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')->label('ID')->sortable(),
|
||||
TextColumn::make('caption')->label('Page Name')->searchable(),
|
||||
TextColumn::make('parent_id')->label('Parent ID'),
|
||||
TextColumn::make('order_num')->label('Order'),
|
||||
IconColumn::make('visible')->boolean()->label('Visible'),
|
||||
IconColumn::make('enabled')->boolean()->label('Enabled'),
|
||||
TextColumn::make('id')->label(__('ID'))->sortable(),
|
||||
TextColumn::make('caption')->label(__('Page Name'))->searchable(),
|
||||
TextColumn::make('parent_id')->label(__('Parent ID')),
|
||||
TextColumn::make('order_num')->label(__('Order')),
|
||||
IconColumn::make('visible')->boolean()->label(__('Visible')),
|
||||
IconColumn::make('enabled')->boolean()->label(__('Enabled')),
|
||||
])
|
||||
->recordActions([])
|
||||
->toolbarActions([]);
|
||||
@@ -50,55 +53,53 @@ class CatalogEditorResource extends Resource
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('caption')
|
||||
->label('Page Name')
|
||||
->label(__('Page Name'))
|
||||
->required()
|
||||
->maxLength(128),
|
||||
|
||||
TextInput::make('caption_save')
|
||||
->label('Name TAG')
|
||||
->label(__('Name TAG'))
|
||||
->maxLength(25)
|
||||
->helperText('Lowercase letters only (a-z), no spaces.'),
|
||||
->helperText(__('Lowercase letters only (a-z), no spaces.')),
|
||||
|
||||
Select::make('parent_id')
|
||||
->label('Parent Page')
|
||||
->options(fn () => CatalogPage::all()
|
||||
->pluck('caption', 'id')
|
||||
->toArray())
|
||||
->label(__('Parent Page'))
|
||||
->options(fn () => CatalogPage::pluck('caption', 'id')->toArray())
|
||||
->default(-1),
|
||||
|
||||
TextInput::make('order_num')
|
||||
->label('Order')
|
||||
->label(__('Order'))
|
||||
->numeric()
|
||||
->default(1),
|
||||
|
||||
TextInput::make('icon_image')
|
||||
->label('Icon Number')
|
||||
->label(__('Icon Number'))
|
||||
->numeric()
|
||||
->default(1),
|
||||
|
||||
TextInput::make('page_layout')
|
||||
->label('Page Layout')
|
||||
->label(__('Page Layout'))
|
||||
->default('default_3x3'),
|
||||
|
||||
TextInput::make('min_rank')
|
||||
->label('Min Rank')
|
||||
->label(__('Min Rank'))
|
||||
->numeric()
|
||||
->default(1),
|
||||
|
||||
Toggle::make('visible')
|
||||
->label('Visible')
|
||||
->label(__('Visible'))
|
||||
->default(true),
|
||||
|
||||
Toggle::make('enabled')
|
||||
->label('Enabled')
|
||||
->label(__('Enabled'))
|
||||
->default(true),
|
||||
|
||||
Toggle::make('club_only')
|
||||
->label('Club Only (HC)')
|
||||
->label(__('Club Only (HC)'))
|
||||
->default(false),
|
||||
|
||||
Toggle::make('vip_only')
|
||||
->label('VIP Only')
|
||||
->label(__('VIP Only'))
|
||||
->default(false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class OpenPositionResource extends Resource
|
||||
return $schema
|
||||
->components([
|
||||
ToggleButtons::make('position_kind')
|
||||
->label('Type')
|
||||
->label(__('Type'))
|
||||
->inline()
|
||||
->options([
|
||||
'rank' => 'Ranks',
|
||||
|
||||
@@ -42,13 +42,13 @@ class StaffApplicationResource extends Resource
|
||||
->searchable(),
|
||||
|
||||
Select::make('rank_id')
|
||||
->label('Rank')
|
||||
->label(__('Rank'))
|
||||
->relationship('rank', 'rank_name')
|
||||
->searchable()
|
||||
->nullable(),
|
||||
|
||||
Select::make('team_id')
|
||||
->label('Team')
|
||||
->label(__('Team'))
|
||||
->relationship('team', 'rank_name')
|
||||
->searchable()
|
||||
->nullable(),
|
||||
@@ -65,12 +65,12 @@ class StaffApplicationResource extends Resource
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
->label('User')
|
||||
->label(__('User'))
|
||||
->sortable()
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('applied_for')
|
||||
->label('Applied For')
|
||||
->label(__('Applied For'))
|
||||
->state(fn (WebsiteStaffApplications $record) => $record->team_id
|
||||
? ($record->team->rank_name ?? '-')
|
||||
: ($record->rank->rank_name ?? '-'))
|
||||
@@ -82,7 +82,7 @@ class StaffApplicationResource extends Resource
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->label(__('Status'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state) => ucfirst($state ?? 'pending'))
|
||||
->color(fn (?string $state) => [
|
||||
@@ -98,22 +98,22 @@ class StaffApplicationResource extends Resource
|
||||
])
|
||||
->recordActions([
|
||||
Action::make('approveTeam')
|
||||
->label('Approve to Team')
|
||||
->label(__('Approve to Team'))
|
||||
->icon('heroicon-o-check-circle')
|
||||
->color('success')
|
||||
->visible(fn (WebsiteStaffApplications $r) => filled($r->team_id) && ($r->status === 'pending' || is_null($r->status)))
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Approve to Team')
|
||||
->modalHeading(__('Approve to Team'))
|
||||
->modalDescription(function (WebsiteStaffApplications $r): string {
|
||||
$user = $r->user;
|
||||
$targetTeam = optional($r->team)->rank_name ?? '—';
|
||||
$currentTeam = optional($user?->team)->rank_name;
|
||||
|
||||
if ($currentTeam && $user?->team_id !== $r->team_id) {
|
||||
return "This user is currently in '{$currentTeam}'. Approving will move them to '{$targetTeam}'. Continue?";
|
||||
return __('This user is currently in :team. Approving will move them to :target. Continue?', ['team' => $currentTeam, 'target' => $targetTeam]);
|
||||
}
|
||||
|
||||
return "Approve this application and assign the user to '{$targetTeam}'?";
|
||||
return __('Approve this application and assign the user to :team?', ['team' => $targetTeam]);
|
||||
})
|
||||
->action(function (WebsiteStaffApplications $r) {
|
||||
$user = $r->user;
|
||||
@@ -121,15 +121,15 @@ class StaffApplicationResource extends Resource
|
||||
|
||||
if (! $user || ! $team) {
|
||||
Notification::make()
|
||||
->danger()->title('Unable to approve')
|
||||
->body('Missing user or team on this application.')
|
||||
->danger()->title(__('Unable to approve'))
|
||||
->body(__('Missing user or team on this application.'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $user->team_id !== (int) $team->id) {
|
||||
$user->update(['team_id' => $team->id]);
|
||||
$user->forceFill(['team_id' => $team->id])->save();
|
||||
}
|
||||
|
||||
$r->update([
|
||||
@@ -141,27 +141,27 @@ class StaffApplicationResource extends Resource
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->success()->title('Approved')
|
||||
->body("{$user->username} has been added to '{$team->rank_name}'.")
|
||||
->success()->title(__('Approved'))
|
||||
->body(__(':username has been added to :team.', ['username' => $user->username, 'team' => $team->rank_name]))
|
||||
->send();
|
||||
}),
|
||||
|
||||
Action::make('rejectTeam')
|
||||
->label('Reject')
|
||||
->label(__('Reject'))
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->visible(fn (WebsiteStaffApplications $r) => filled($r->team_id) && in_array($r->status, ['pending', 'approved', null], true))
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reject Application')
|
||||
->modalHeading(__('Reject Application'))
|
||||
->modalDescription(function (WebsiteStaffApplications $r): string {
|
||||
$user = $r->user;
|
||||
$teamName = optional($r->team)->rank_name ?? '—';
|
||||
|
||||
if ($r->status === 'approved') {
|
||||
return "This will mark the application as rejected and remove {$user->username} from '{$teamName}' (if still on it). Continue?";
|
||||
return __('This will mark the application as rejected and remove :username from :team. Continue?', ['username' => $user->username, 'team' => $teamName]);
|
||||
}
|
||||
|
||||
return 'This will mark the application as rejected. Continue?';
|
||||
return __('This will mark the application as rejected. Continue?');
|
||||
})
|
||||
->action(function (WebsiteStaffApplications $r) {
|
||||
$user = $r->user;
|
||||
@@ -169,15 +169,15 @@ class StaffApplicationResource extends Resource
|
||||
|
||||
if (! $user || ! $team) {
|
||||
Notification::make()
|
||||
->danger()->title('Unable to reject')
|
||||
->body('Missing user or team on this application.')
|
||||
->danger()->title(__('Unable to reject'))
|
||||
->body(__('Missing user or team on this application.'))
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($r->status === 'approved' && (int) $user->team_id === (int) $team->id) {
|
||||
$user->update(['team_id' => null]);
|
||||
$user->forceFill(['team_id' => null])->save();
|
||||
}
|
||||
|
||||
$r->update([
|
||||
@@ -187,21 +187,21 @@ class StaffApplicationResource extends Resource
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->success()->title('Rejected')
|
||||
->success()->title(__('Rejected'))
|
||||
->body($r->status === 'approved'
|
||||
? "{$user->username} has been removed from '{$team->rank_name}' and the application marked as rejected."
|
||||
: 'Application has been marked as rejected.')
|
||||
? __(':username has been removed from :team and the application marked as rejected.', ['username' => $user->username, 'team' => $team->rank_name])
|
||||
: __('Application has been marked as rejected.'))
|
||||
->send();
|
||||
}),
|
||||
|
||||
Action::make('reopen')
|
||||
->label('Re-open')
|
||||
->label(__('Re-open'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->visible(fn (WebsiteStaffApplications $r) => $r->status === 'rejected')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Re-open Application')
|
||||
->modalDescription('This will set the application status back to pending.')
|
||||
->modalHeading(__('Re-open Application'))
|
||||
->modalDescription(__('This will set the application status back to pending.'))
|
||||
->action(function (WebsiteStaffApplications $r) {
|
||||
$r->update([
|
||||
'status' => 'pending',
|
||||
@@ -210,8 +210,8 @@ class StaffApplicationResource extends Resource
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->success()->title('Re-opened')
|
||||
->body('Application status set to pending.')
|
||||
->success()->title(__('Re-opened'))
|
||||
->body(__('Application status set to pending.'))
|
||||
->send();
|
||||
}),
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Resources\Miscellaneous\AlertLogResource;
|
||||
|
||||
use App\Models\Miscellaneous\AlertLog;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
@@ -136,14 +135,12 @@ class AlertLogResource extends Resource
|
||||
->icon('heroicon-o-trash')
|
||||
->color('gray')
|
||||
->action(function () {
|
||||
$updateService = new EmulatorUpdateService;
|
||||
$result = $updateService->clearAllLogs();
|
||||
Cache::flush();
|
||||
AlertLog::truncate();
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('🗑️ Alle Logs Geleegd!')
|
||||
->body($result['message'])
|
||||
->body('Alle logs zijn gewist.')
|
||||
->send();
|
||||
})
|
||||
->requiresConfirmation(),
|
||||
|
||||
@@ -180,7 +180,7 @@ class RadioApplicationResource extends Resource
|
||||
])
|
||||
->label('Status'),
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
Action::make('approve')
|
||||
->label('Goedkeuren')
|
||||
->icon('heroicon-o-check-circle')
|
||||
|
||||
@@ -123,11 +123,11 @@ class RadioBannerResource extends Resource
|
||||
->label('Actief')
|
||||
->boolean(),
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->headerActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -115,11 +115,11 @@ class RadioHistoryResource extends Resource
|
||||
TextColumn::make('listeners_count')
|
||||
->label('Luisteraars'),
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->headerActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -120,10 +120,10 @@ class RadioRankResource extends Resource
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->headerActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -142,11 +142,11 @@ class RadioScheduleResource extends Resource
|
||||
])
|
||||
->label('Dag'),
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
->headerActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class RadioShoutResource extends Resource
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
Action::make('report')
|
||||
->label('Melden')
|
||||
->icon('heroicon-o-flag')
|
||||
|
||||
@@ -76,7 +76,7 @@ class RadioSongPlayResource extends Resource
|
||||
->options(fn () => RadioSongPlay::distinct()->whereNotNull('artist')->pluck('artist', 'artist')->toArray())
|
||||
->searchable(),
|
||||
])
|
||||
->recordActions([
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->label('Verwijderen'),
|
||||
])
|
||||
|
||||
@@ -189,7 +189,7 @@ class EditUser extends EditRecord
|
||||
}
|
||||
|
||||
if (! $user->online) {
|
||||
$user->update(['rank' => $data['rank']]);
|
||||
$user->forceFill(['rank' => $data['rank']])->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class UserResource extends Resource
|
||||
Select::make('team_id')
|
||||
->native(false)
|
||||
->label(__('filament::resources.inputs.team_id'))
|
||||
->options(WebsiteTeam::all()->pluck('rank_name', 'id'))
|
||||
->options(WebsiteTeam::pluck('rank_name', 'id'))
|
||||
->columnSpanFull(),
|
||||
])->columns(['sm' => 2]),
|
||||
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use App\Services\RconService;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UpdateCheckerWidget extends Widget
|
||||
{
|
||||
#[\Override]
|
||||
protected string $view = 'filament.widgets.update-checker';
|
||||
|
||||
#[\Override]
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
#[\Override]
|
||||
protected static ?int $sort = 0;
|
||||
|
||||
public ?string $emulatorVersion = null;
|
||||
|
||||
public ?string $latestEmulatorVersion = null;
|
||||
|
||||
public bool $emulatorUpdate = false;
|
||||
|
||||
public ?string $nitroVersion = null;
|
||||
|
||||
public ?string $latestNitroVersion = null;
|
||||
|
||||
public bool $nitroUpdate = false;
|
||||
|
||||
public int $onlineUsers = 0;
|
||||
|
||||
public string $dbSize = '0 MB';
|
||||
|
||||
public bool $hasAnyUpdate = false;
|
||||
|
||||
public int $sqlApplied = 0;
|
||||
|
||||
public int $sqlPending = 0;
|
||||
|
||||
public bool $sqlTableExists = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->emulatorVersion = setting('emulator_version', '?');
|
||||
$this->nitroVersion = setting('nitro_client_version', '?');
|
||||
|
||||
try {
|
||||
$this->onlineUsers = (int) DB::connection('mysql')->table('users')->where('online', '1')->count();
|
||||
$sizeResult = DB::connection('mysql')->select('SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) as db_size FROM information_schema.tables WHERE table_schema = DATABASE()');
|
||||
$this->dbSize = ($sizeResult[0]->db_size ?? '0') . ' MB';
|
||||
} catch (\Exception) {
|
||||
$this->dbSize = '? MB';
|
||||
}
|
||||
|
||||
$this->loadSqlStatus();
|
||||
|
||||
$cached = Cache::get('all_updates_check');
|
||||
|
||||
if ($cached !== null) {
|
||||
$this->emulatorUpdate = $cached['emulator'] ?? false;
|
||||
$this->latestEmulatorVersion = $cached['emulator_version'] ?? null;
|
||||
$this->nitroUpdate = $cached['nitro'] ?? false;
|
||||
$this->latestNitroVersion = $cached['nitro_version'] ?? null;
|
||||
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->performCheck();
|
||||
}
|
||||
|
||||
private function loadSqlStatus(): void
|
||||
{
|
||||
try {
|
||||
$updateService = new EmulatorUpdateService;
|
||||
$sqlDiagnosis = $updateService->diagnoseSqlUpdates();
|
||||
|
||||
$this->sqlTableExists = $sqlDiagnosis['table_exists'] ?? false;
|
||||
$this->sqlApplied = $sqlDiagnosis['applied_count'] ?? 0;
|
||||
$this->sqlPending = $sqlDiagnosis['pending_count'] ?? 0;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateChecker] SQL status failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function performCheck(): void
|
||||
{
|
||||
try {
|
||||
$updateService = new EmulatorUpdateService;
|
||||
$check = $updateService->checkForUpdates();
|
||||
$this->emulatorUpdate = $check['update_available'] ?? false;
|
||||
$this->latestEmulatorVersion = $check['latest_version'] ?? null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateChecker] Emulator check failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$nitroService = new NitroUpdateService;
|
||||
$nitroCheck = $nitroService->checkForUpdates();
|
||||
$this->nitroUpdate = $nitroCheck['has_updates'] ?? false;
|
||||
if ($this->nitroUpdate) {
|
||||
$parts = [];
|
||||
if ($nitroCheck['client_update'] ?? false) {
|
||||
$parts[] = 'Client';
|
||||
}
|
||||
if ($nitroCheck['renderer_update'] ?? false) {
|
||||
$parts[] = 'Renderer';
|
||||
}
|
||||
$this->latestNitroVersion = implode(' + ', $parts);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateChecker] Nitro check failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
|
||||
|
||||
Cache::put('all_updates_check', [
|
||||
'emulator' => $this->emulatorUpdate,
|
||||
'emulator_version' => $this->latestEmulatorVersion,
|
||||
'nitro' => $this->nitroUpdate,
|
||||
'nitro_version' => $this->latestNitroVersion,
|
||||
], now()->addMinutes(15));
|
||||
}
|
||||
|
||||
public function forceCheck(): void
|
||||
{
|
||||
try {
|
||||
Cache::forget('all_updates_check');
|
||||
$updateService = new EmulatorUpdateService;
|
||||
$updateService->resetInstalledDate();
|
||||
$this->performCheck();
|
||||
|
||||
if ($this->hasAnyUpdate) {
|
||||
Notification::make()->success()->title('Updates Gevonden!')->body('Klik op "Alles Updaten" om te installeren')->send();
|
||||
} else {
|
||||
Notification::make()->info()->title('Geen Updates')->body('Alles is al up-to-date')->send();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function repairSystem(): void
|
||||
{
|
||||
try {
|
||||
Cache::forget('all_updates_check');
|
||||
|
||||
$messages = [];
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
$emuService = new EmulatorUpdateService;
|
||||
$repairResult = $emuService->repairEmulator();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$messages[] = '🖥️ Emulator: ' . ($repairResult['message'] ?? 'Gerepareerd');
|
||||
} else {
|
||||
$errors[] = 'Emulator: ' . ($repairResult['error'] ?? 'Onbekende fout');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Emulator: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$nitroService = new NitroUpdateService;
|
||||
$repairResult = $nitroService->repair();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$messages[] = '🎮 Nitro: ' . ($repairResult['message'] ?? 'Gerepareerd');
|
||||
} else {
|
||||
$errors[] = 'Nitro: ' . ($repairResult['error'] ?? 'Onbekende fout');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Nitro: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
if ($messages !== []) {
|
||||
Notification::make()->success()->title('Reparatie Voltooid!')->body(implode(' | ', $messages))->send();
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
Notification::make()->danger()->title('Reparatie Fout')->body(implode(' | ', $errors))->send();
|
||||
}
|
||||
|
||||
Cache::forget('all_updates_check');
|
||||
$this->mount();
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function diagnoseSystem(): void
|
||||
{
|
||||
try {
|
||||
$emuService = new EmulatorUpdateService;
|
||||
$nitroService = new NitroUpdateService;
|
||||
|
||||
$emuDiagnosis = $emuService->diagnose();
|
||||
$nitroDiagnosis = $nitroService->diagnose();
|
||||
|
||||
$issues = array_merge(
|
||||
$emuDiagnosis['issues'] ?? [],
|
||||
$nitroDiagnosis['issues'] ?? [],
|
||||
);
|
||||
|
||||
$recommendations = array_merge(
|
||||
$emuDiagnosis['recommendations'] ?? [],
|
||||
$nitroDiagnosis['recommendations'] ?? [],
|
||||
);
|
||||
|
||||
if ($issues === []) {
|
||||
Notification::make()->info()->title('Diagnose')->body('Geen problemen gevonden')->send();
|
||||
} else {
|
||||
$body = 'Problemen: ' . implode(', ', $issues);
|
||||
if ($recommendations !== []) {
|
||||
$body .= "\n\nAanbevelingen: " . implode(', ', $recommendations);
|
||||
}
|
||||
Notification::make()->warning()->title('Diagnose Resultaat')->body($body)->send();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateAll(): void
|
||||
{
|
||||
try {
|
||||
Cache::forget('all_updates_check');
|
||||
Cache::forget('website_settings');
|
||||
|
||||
$messages = [];
|
||||
$errors = [];
|
||||
$updateService = new EmulatorUpdateService;
|
||||
|
||||
try {
|
||||
$result = $updateService->updateEmulator();
|
||||
if ($result['success']) {
|
||||
$messages[] = '🖥️ ' . ($result['message'] ?? 'Emulator geüpdatet');
|
||||
} else {
|
||||
$error = $result['error'] ?? '';
|
||||
if (str_contains($error, 'al up-to-date')) {
|
||||
$messages[] = '🖥️ Emulator al up-to-date';
|
||||
} else {
|
||||
$errors[] = 'Emulator: ' . $error;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Emulator: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$nitroService = new NitroUpdateService;
|
||||
$nitroCheck = $nitroService->checkForUpdates();
|
||||
|
||||
if ($nitroCheck['has_updates'] ?? false) {
|
||||
$result = $nitroService->updateNitro();
|
||||
if ($result['success']) {
|
||||
$parts = [];
|
||||
if ($result['renderer_updated'] ?? false) {
|
||||
$parts[] = 'Renderer';
|
||||
}
|
||||
if ($result['client_updated'] ?? false) {
|
||||
$parts[] = 'Client';
|
||||
}
|
||||
if ($result['built'] ?? false) {
|
||||
$parts[] = 'Build';
|
||||
}
|
||||
if ($result['deployed'] ?? false) {
|
||||
$parts[] = 'Deploy';
|
||||
}
|
||||
if ($parts !== []) {
|
||||
$messages[] = '🎮 Nitro: ' . implode(', ', $parts);
|
||||
}
|
||||
} elseif (! empty($result['errors'] ?? [])) {
|
||||
$errors = array_merge($errors, $result['errors']);
|
||||
}
|
||||
} else {
|
||||
$messages[] = '🎮 Nitro al up-to-date';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Nitro: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$sqlResult = $updateService->runSqlUpdates();
|
||||
if ($sqlResult['sql_updated'] ?? false) {
|
||||
$count = count($sqlResult['files_run'] ?? []);
|
||||
if ($count > 0) {
|
||||
$messages[] = "📊 {$count} SQL updates";
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'SQL: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$updateService->restartEmulator();
|
||||
$messages[] = '🔄 Emulator herstart';
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateChecker] Restart failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$rcon = new RconService;
|
||||
$rcon->sendCommand('updatecatalog');
|
||||
$rcon->sendCommand('updatewordfilter');
|
||||
$messages[] = '📚 Catalogus + filter vernieuwd';
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[UpdateChecker] RCON failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($messages !== []) {
|
||||
Notification::make()->success()->title('Updates Voltooid!')->body(implode(' | ', $messages))->send();
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
Notification::make()->danger()->title('Sommige Updates Mislukt')->body(implode(' | ', $errors))->send();
|
||||
}
|
||||
|
||||
if ($messages === [] && $errors === []) {
|
||||
Notification::make()->info()->title('Geen Updates')->body('Alles was al up-to-date')->send();
|
||||
}
|
||||
|
||||
Cache::forget('all_updates_check');
|
||||
$this->mount();
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function canView(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioRank;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
@@ -12,6 +11,8 @@ use Illuminate\View\View;
|
||||
|
||||
class RadioSetupController extends Controller
|
||||
{
|
||||
use \App\Http\Controllers\Concerns\HasRadioDefaults;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.radio.setup');
|
||||
@@ -24,7 +25,7 @@ class RadioSetupController extends Controller
|
||||
$settings = [
|
||||
// Basic Radio Settings
|
||||
'radio_enabled' => '1',
|
||||
'radio_stream_url' => 'https://stream.radioking.com/radio/83232/radio.mp3',
|
||||
'radio_stream_url' => '',
|
||||
'radio_style' => 'dark',
|
||||
'radio_auto_play' => '0',
|
||||
|
||||
@@ -83,13 +84,7 @@ class RadioSetupController extends Controller
|
||||
'radio_auto_contest_creation' => '0',
|
||||
];
|
||||
|
||||
// Update all settings
|
||||
foreach ($settings as $key => $value) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value, 'comment' => $this->getSettingComment($key)],
|
||||
);
|
||||
}
|
||||
$this->saveRadioSettings($settings);
|
||||
|
||||
// Create default radio ranks if they don't exist
|
||||
$this->createDefaultRanks();
|
||||
@@ -107,11 +102,6 @@ class RadioSetupController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function doSetup(Request $request): RedirectResponse
|
||||
{
|
||||
return $this->setup($request);
|
||||
}
|
||||
|
||||
public function reset(): RedirectResponse
|
||||
{
|
||||
try {
|
||||
@@ -122,30 +112,14 @@ class RadioSetupController extends Controller
|
||||
Artisan::call('cache:clear');
|
||||
|
||||
return redirect()->route('admin.radio.setup')
|
||||
->with('success', 'Radio instellingen zijn gereset.');
|
||||
->with('success', 'Radio settings have been reset.');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('admin.radio.setup')
|
||||
->with('error', 'Fout bij resetten: ' . $e->getMessage());
|
||||
->with('error', 'Error during reset: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function createDefaultRanks(): void
|
||||
{
|
||||
$ranks = [
|
||||
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
|
||||
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
|
||||
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
|
||||
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
|
||||
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
|
||||
];
|
||||
|
||||
foreach ($ranks as $rank) {
|
||||
RadioRank::updateOrCreate(
|
||||
['name' => $rank['name']],
|
||||
$rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getSettingComment(string $key): string
|
||||
{
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioRank;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -14,6 +12,8 @@ use Illuminate\View\View;
|
||||
|
||||
class RadioWizardController extends Controller
|
||||
{
|
||||
use \App\Http\Controllers\Concerns\HasRadioDefaults;
|
||||
|
||||
private const SESSION_KEY = 'radio_wizard';
|
||||
|
||||
public function __construct(
|
||||
@@ -68,7 +68,7 @@ class RadioWizardController extends Controller
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
'other' => 'Anders',
|
||||
'other' => 'Other',
|
||||
];
|
||||
|
||||
return view('admin.radio.wizard.step-2', [
|
||||
@@ -99,7 +99,7 @@ class RadioWizardController extends Controller
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
session()->put(self::SESSION_KEY . '.stream_url', $validated['stream_url']);
|
||||
session()->put(self::SESSION_KEY . '.stream_name', $validated['stream_name'] ?? 'Mijn Radio');
|
||||
session()->put(self::SESSION_KEY . '.stream_name', $validated['stream_name'] ?? 'My Radio');
|
||||
|
||||
if ($platform === 'azurecast') {
|
||||
session()->put(self::SESSION_KEY . '.azurecast_base_url', $validated['azurecast_base_url'] ?? '');
|
||||
@@ -208,7 +208,7 @@ class RadioWizardController extends Controller
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
'other' => 'Anders',
|
||||
'other' => 'Other',
|
||||
];
|
||||
|
||||
$testResults = null;
|
||||
@@ -240,7 +240,7 @@ class RadioWizardController extends Controller
|
||||
if (! $data || empty($data['stream_url'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Geen stream URL gevonden. Start de wizard opnieuw.',
|
||||
'error' => 'No stream URL found. Please restart the wizard.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class RadioWizardController extends Controller
|
||||
|
||||
if (! $data) {
|
||||
return redirect()->route('admin.radio.wizard')
|
||||
->with('error', 'Geen wizard data gevonden. Start opnieuw.');
|
||||
->with('error', 'No wizard data found. Please start over.');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -275,10 +275,10 @@ class RadioWizardController extends Controller
|
||||
session()->forget(self::SESSION_KEY);
|
||||
|
||||
return redirect()->route('admin.radio.setup')
|
||||
->with('success', 'Radio systeem is succesvol geïnstalleerd en geconfigureerd!');
|
||||
->with('success', 'Radio system has been successfully installed and configured!');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('admin.radio.wizard.step', ['step' => 5])
|
||||
->with('error', 'Fout tijdens opslaan: ' . $e->getMessage());
|
||||
->with('error', 'Error while saving: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ class RadioWizardController extends Controller
|
||||
$settings = [
|
||||
'radio_enabled' => '1',
|
||||
'radio_stream_url' => $data['stream_url'] ?? '',
|
||||
'radio_stream_name' => $data['stream_name'] ?? 'Mijn Radio',
|
||||
'radio_stream_name' => $data['stream_name'] ?? 'My Radio',
|
||||
'radio_style' => 'dark',
|
||||
'radio_auto_play' => '0',
|
||||
'radio_stream_platform' => $platform,
|
||||
@@ -349,42 +349,19 @@ class RadioWizardController extends Controller
|
||||
$settings['radio_discord_song_changes'] = '1';
|
||||
}
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) $value, 'comment' => 'Radio wizard configuratie'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createDefaultRanks(): void
|
||||
{
|
||||
$ranks = [
|
||||
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
|
||||
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
|
||||
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
|
||||
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
|
||||
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
|
||||
];
|
||||
|
||||
foreach ($ranks as $rank) {
|
||||
RadioRank::updateOrCreate(
|
||||
['name' => $rank['name']],
|
||||
$rank,
|
||||
);
|
||||
}
|
||||
$this->saveRadioSettings($settings);
|
||||
}
|
||||
|
||||
private function buildSettingsList(array $data): array
|
||||
{
|
||||
$list = [];
|
||||
$list['Stream URL'] = $data['stream_url'] ?? '-';
|
||||
$list['Stream Naam'] = $data['stream_name'] ?? 'Mijn Radio';
|
||||
$list['Stream Name'] = $data['stream_name'] ?? 'My Radio';
|
||||
$list['Platform'] = match ($data['platform'] ?? 'other') {
|
||||
'shoutcast' => 'SHOUTcast',
|
||||
'icecast' => 'Icecast',
|
||||
'azurecast' => 'AzureCast',
|
||||
default => 'Anders',
|
||||
default => 'Other',
|
||||
};
|
||||
|
||||
if (! empty($data['now_playing_api'])) {
|
||||
@@ -395,19 +372,19 @@ class RadioWizardController extends Controller
|
||||
$list['Listeners API'] = $data['listeners_api'];
|
||||
}
|
||||
|
||||
$list['Nu Afspelen'] = ($data['enable_now_playing'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Luisteraars'] = ($data['enable_listeners'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Huidige DJ'] = ($data['enable_current_dj'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Shouts'] = ($data['enable_shouts'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['DJ Aanmeldingen'] = ($data['enable_applications'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Radio Widget'] = ($data['enable_widget'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Widget Overal'] = ($data['enable_widget_globally'] ?? true) ? 'Ja' : 'Nee';
|
||||
$list['Widget Positie'] = $data['widget_position'] ?? 'bottom-right';
|
||||
$list['Punten Systeem'] = ($data['enable_points'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Song Verzoeken'] = ($data['enable_requests'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Contesten'] = ($data['enable_contests'] ?? true) ? 'Aan' : 'Uit';
|
||||
$list['Giveaways'] = ($data['enable_giveaways'] ?? false) ? 'Aan' : 'Uit';
|
||||
$list['Discord'] = ($data['enable_discord'] ?? false) ? 'Aan' : 'Uit';
|
||||
$list['Now Playing'] = ($data['enable_now_playing'] ?? true) ? 'On' : 'Off';
|
||||
$list['Listeners'] = ($data['enable_listeners'] ?? true) ? 'On' : 'Off';
|
||||
$list['Current DJ'] = ($data['enable_current_dj'] ?? true) ? 'On' : 'Off';
|
||||
$list['Shouts'] = ($data['enable_shouts'] ?? true) ? 'On' : 'Off';
|
||||
$list['DJ Applications'] = ($data['enable_applications'] ?? true) ? 'On' : 'Off';
|
||||
$list['Radio Widget'] = ($data['enable_widget'] ?? true) ? 'On' : 'Off';
|
||||
$list['Widget Everywhere'] = ($data['enable_widget_globally'] ?? true) ? 'Yes' : 'No';
|
||||
$list['Widget Position'] = $data['widget_position'] ?? 'bottom-right';
|
||||
$list['Points System'] = ($data['enable_points'] ?? true) ? 'On' : 'Off';
|
||||
$list['Song Requests'] = ($data['enable_requests'] ?? true) ? 'On' : 'Off';
|
||||
$list['Contests'] = ($data['enable_contests'] ?? true) ? 'On' : 'Off';
|
||||
$list['Giveaways'] = ($data['enable_giveaways'] ?? false) ? 'On' : 'Off';
|
||||
$list['Discord'] = ($data['enable_discord'] ?? false) ? 'On' : 'Off';
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\ArticleResource;
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ArticleApiController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$articles = WebsiteArticle::with(['user:id,username,look'])
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => ArticleResource::collection($articles),
|
||||
'meta' => [
|
||||
'current_page' => $articles->currentPage(),
|
||||
'last_page' => $articles->lastPage(),
|
||||
'per_page' => $articles->perPage(),
|
||||
'total' => $articles->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $slug): JsonResponse
|
||||
{
|
||||
$article = WebsiteArticle::with(['user:id,username,look', 'comments.user:id,username,look'])
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json(['data' => new ArticleResource($article)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Game\Furniture\CatalogItem;
|
||||
use App\Models\Game\Furniture\CatalogPage;
|
||||
use App\Models\Game\Guild\Guild;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Services\Community\StaffService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ContentApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StaffService $staffService,
|
||||
) {}
|
||||
|
||||
public function settings(): JsonResponse
|
||||
{
|
||||
$settings = Cache::remember('api_all_settings', 60, fn () => WebsiteSetting::pluck('value', 'key'));
|
||||
|
||||
return response()->json(['data' => $settings]);
|
||||
}
|
||||
|
||||
public function staff(): JsonResponse
|
||||
{
|
||||
return response()->json(['data' => $this->staffService->fetchStaffPositions()]);
|
||||
}
|
||||
|
||||
public function teams(): JsonResponse
|
||||
{
|
||||
$teams = Guild::with('members')->latest('id')->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => $teams->items(),
|
||||
'meta' => [
|
||||
'current_page' => $teams->currentPage(),
|
||||
'last_page' => $teams->lastPage(),
|
||||
'per_page' => $teams->perPage(),
|
||||
'total' => $teams->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rareValues(Request $request): JsonResponse
|
||||
{
|
||||
$query = CatalogItem::query();
|
||||
|
||||
if ($categoryId = $request->query('category')) {
|
||||
$query->where('page_id', $categoryId);
|
||||
}
|
||||
|
||||
$items = $query->with('itemBase')->latest('id')->paginate(24);
|
||||
|
||||
return response()->json([
|
||||
'data' => $items->items(),
|
||||
'meta' => [
|
||||
'current_page' => $items->currentPage(),
|
||||
'last_page' => $items->lastPage(),
|
||||
'per_page' => $items->perPage(),
|
||||
'total' => $items->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rareValuesCategories(): JsonResponse
|
||||
{
|
||||
$categories = CatalogPage::where('catalog_name', '!=', '')
|
||||
->where('visible', 1)
|
||||
->orderBy('order_number')
|
||||
->get(['id', 'catalog_name', 'icon']);
|
||||
|
||||
return response()->json(['data' => $categories]);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Game\Furniture\CatalogItem;
|
||||
use App\Models\Game\Furniture\CatalogPage;
|
||||
use App\Models\Game\Furniture\ItemBase;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FurniEditorController extends Controller
|
||||
{
|
||||
private function checkAdmin(): void
|
||||
{
|
||||
if (! Auth::check() || Auth::user()->rank < (int) setting('min_staff_rank', 7)) {
|
||||
if (! Auth::check() || Auth::user()->rank < (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank'))) {
|
||||
abort(403, 'Forbidden');
|
||||
}
|
||||
}
|
||||
@@ -25,10 +29,10 @@ class FurniEditorController extends Controller
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
|
||||
return response()->json(['error' => "{$action} mislukt"], 500);
|
||||
return response()->json(['error' => "{$action} failed"], 500);
|
||||
}
|
||||
|
||||
private function formatItemData(\stdClass $item): array
|
||||
private function formatItemData(ItemBase $item): array
|
||||
{
|
||||
return [
|
||||
'id' => $item->id,
|
||||
@@ -59,17 +63,17 @@ class FurniEditorController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
private function formatCatalogItemData(\stdClass $r): array
|
||||
private function formatCatalogItemData(CatalogItem $item): array
|
||||
{
|
||||
return [
|
||||
'id' => $r->id,
|
||||
'catalogName' => $r->catalog_name,
|
||||
'costCredits' => $r->cost_credits,
|
||||
'costPoints' => $r->cost_points,
|
||||
'pointsType' => $r->points_type,
|
||||
'pageId' => $r->page_id,
|
||||
'pageName' => $r->page_id
|
||||
? DB::table('catalog_pages')->where('id', $r->page_id)->value('caption') ?? ''
|
||||
'id' => $item->id,
|
||||
'catalogName' => $item->catalog_name,
|
||||
'costCredits' => $item->cost_credits,
|
||||
'costPoints' => $item->cost_points,
|
||||
'pointsType' => $item->points_type,
|
||||
'pageId' => $item->page_id,
|
||||
'pageName' => $item->page_id
|
||||
? CatalogPage::where('id', $item->page_id)->value('caption') ?? ''
|
||||
: '',
|
||||
];
|
||||
}
|
||||
@@ -84,7 +88,7 @@ class FurniEditorController extends Controller
|
||||
$page = max(1, (int) $request->input('page', 1));
|
||||
$limit = min(50, max(1, (int) $request->input('limit', 20)));
|
||||
|
||||
$query = DB::table('items_base');
|
||||
$query = ItemBase::query();
|
||||
|
||||
if ($q !== '' && $q !== '0') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
@@ -105,7 +109,7 @@ class FurniEditorController extends Controller
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get()
|
||||
->map(fn ($r) => [
|
||||
->map(fn (ItemBase $r) => [
|
||||
'id' => $r->id,
|
||||
'spriteId' => $r->sprite_id,
|
||||
'itemName' => $r->item_name,
|
||||
@@ -128,7 +132,7 @@ class FurniEditorController extends Controller
|
||||
'page' => $page,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Zoeken', $e);
|
||||
return $this->handleApiError('Search', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,15 +146,14 @@ class FurniEditorController extends Controller
|
||||
return response()->json(['error' => 'Missing id'], 400);
|
||||
}
|
||||
|
||||
$item = DB::table('items_base')->where('id', $id)->first();
|
||||
$item = ItemBase::where('id', $id)->first();
|
||||
if (! $item) {
|
||||
return response()->json(['error' => 'Item not found'], 404);
|
||||
}
|
||||
|
||||
$catalog = DB::table('catalog_items')
|
||||
->whereRaw('FIND_IN_SET(?, item_ids)', [$id])
|
||||
$catalog = CatalogItem::whereRaw('FIND_IN_SET(?, item_ids)', [$id])
|
||||
->get()
|
||||
->map(fn ($r) => $this->formatCatalogItemData($r));
|
||||
->map(fn (CatalogItem $r) => $this->formatCatalogItemData($r));
|
||||
|
||||
return response()->json([
|
||||
'item' => array_merge($this->formatItemData($item), [
|
||||
@@ -161,7 +164,7 @@ class FurniEditorController extends Controller
|
||||
'furniDataEntry' => null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Item laden', $e);
|
||||
return $this->handleApiError('Load item', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +185,7 @@ class FurniEditorController extends Controller
|
||||
return response()->json(['error' => 'No fields to update'], 400);
|
||||
}
|
||||
|
||||
DB::table('items_base')->where('id', $id)->update($update);
|
||||
ItemBase::where('id', $id)->update($update);
|
||||
|
||||
Log::info('[Audit] FurniEditor update', [
|
||||
'user_id' => Auth::id(),
|
||||
@@ -192,7 +195,7 @@ class FurniEditorController extends Controller
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Actie', $e);
|
||||
return $this->handleApiError('Update', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +206,7 @@ class FurniEditorController extends Controller
|
||||
try {
|
||||
$data = $request->json()->all();
|
||||
|
||||
$id = DB::table('items_base')->insertGetId($this->buildInsertData($data));
|
||||
$id = ItemBase::insertGetId($this->buildInsertData($data));
|
||||
|
||||
Log::info('[Audit] FurniEditor create', [
|
||||
'user_id' => Auth::id(),
|
||||
@@ -212,7 +215,7 @@ class FurniEditorController extends Controller
|
||||
|
||||
return response()->json(['id' => $id]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Actie', $e);
|
||||
return $this->handleApiError('Create', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +229,7 @@ class FurniEditorController extends Controller
|
||||
return response()->json(['error' => 'Missing id'], 400);
|
||||
}
|
||||
|
||||
DB::table('items_base')->where('id', $id)->delete();
|
||||
ItemBase::where('id', $id)->delete();
|
||||
|
||||
Log::warning('[Audit] FurniEditor delete', [
|
||||
'user_id' => Auth::id(),
|
||||
@@ -235,7 +238,7 @@ class FurniEditorController extends Controller
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Actie', $e);
|
||||
return $this->handleApiError('Delete', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +247,7 @@ class FurniEditorController extends Controller
|
||||
$this->checkAdmin();
|
||||
|
||||
try {
|
||||
$rows = DB::table('items_base')
|
||||
$rows = ItemBase::query()
|
||||
->select('interaction_type')
|
||||
->groupBy('interaction_type')
|
||||
->orderBy('interaction_type')
|
||||
@@ -252,7 +255,7 @@ class FurniEditorController extends Controller
|
||||
|
||||
return response()->json(['interactions' => $rows]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Actie', $e);
|
||||
return $this->handleApiError('Interactions', $e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,14 +269,14 @@ class FurniEditorController extends Controller
|
||||
return response()->json(['error' => 'Missing spriteId'], 400);
|
||||
}
|
||||
|
||||
$item = DB::table('items_base')->where('sprite_id', $spriteId)->first();
|
||||
$item = ItemBase::where('sprite_id', $spriteId)->first();
|
||||
if (! $item) {
|
||||
return response()->json(['error' => 'Item not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['id' => $item->id]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->handleApiError('Actie', $e);
|
||||
return $this->handleApiError('By sprite', $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\HelpTicketReplyRequest;
|
||||
use App\Http\Requests\Api\HelpTicketRequest;
|
||||
use App\Http\Resources\Api\HelpTicketResource;
|
||||
use App\Models\Help\WebsiteHelpCenterTicket;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HelpApiController extends Controller
|
||||
{
|
||||
public function tickets(Request $request): JsonResponse
|
||||
{
|
||||
$tickets = WebsiteHelpCenterTicket::with('user:id,username,look')
|
||||
->when($request->user(), fn ($q) => $q->where('user_id', $request->user()->id))
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'data' => HelpTicketResource::collection($tickets),
|
||||
'meta' => [
|
||||
'current_page' => $tickets->currentPage(),
|
||||
'last_page' => $tickets->lastPage(),
|
||||
'total' => $tickets->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
$ticket = WebsiteHelpCenterTicket::with(['user:id,username,look', 'replies.user:id,username,look'])
|
||||
->where('id', $id)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json(['data' => new HelpTicketResource($ticket)]);
|
||||
}
|
||||
|
||||
public function create(HelpTicketRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$ticket = WebsiteHelpCenterTicket::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'subject' => $validated['subject'],
|
||||
'category' => $validated['category'],
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
$ticket->replies()->create([
|
||||
'user_id' => $request->user()->id,
|
||||
'message' => $validated['message'],
|
||||
]);
|
||||
|
||||
return response()->json(['data' => new HelpTicketResource($ticket)], 201);
|
||||
}
|
||||
|
||||
public function reply(HelpTicketReplyRequest $request, string $id): JsonResponse
|
||||
{
|
||||
$ticket = WebsiteHelpCenterTicket::where('id', $id)
|
||||
->where('user_id', $request->user()->id)
|
||||
->firstOrFail();
|
||||
|
||||
$validated = $request->validated();
|
||||
|
||||
$reply = $ticket->replies()->create([
|
||||
'user_id' => $request->user()->id,
|
||||
'message' => $validated['message'],
|
||||
]);
|
||||
|
||||
return response()->json(['data' => $reply->load('user:id,username,look')], 201);
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\HelpTicketReplyRequest;
|
||||
use App\Http\Requests\Api\HelpTicketRequest;
|
||||
use App\Http\Requests\Api\PhotoUploadRequest;
|
||||
use App\Http\Resources\Api\ArticleResource;
|
||||
use App\Http\Resources\Api\HelpTicketResource;
|
||||
use App\Http\Resources\Api\LeaderboardUserResource;
|
||||
use App\Http\Resources\Api\PhotoResource;
|
||||
use App\Http\Resources\Api\ShopPackageResource;
|
||||
use App\Models\Articles\WebsiteArticle;
|
||||
use App\Models\Game\Furniture\CatalogItem;
|
||||
use App\Models\Game\Furniture\CatalogPage;
|
||||
use App\Models\Game\Guild\Guild;
|
||||
use App\Models\Help\WebsiteHelpCenterTicket;
|
||||
use App\Models\Miscellaneous\CameraWeb;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\Shop\WebsiteShopArticle;
|
||||
use App\Models\User;
|
||||
use App\Services\Community\StaffService;
|
||||
use App\Services\User\UserApiService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class HotelApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserApiService $userApiService,
|
||||
private readonly StaffService $staffService,
|
||||
) {}
|
||||
|
||||
public function fetchUser(string $username): JsonResponse
|
||||
{
|
||||
$user = $this->userApiService->fetchUser($username, ['username', 'motto', 'look']);
|
||||
|
||||
return response()->json([
|
||||
'data' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
public function onlineUsers(): JsonResponse
|
||||
{
|
||||
$users = $this->userApiService->onlineUsers(['username', 'motto', 'look'], true);
|
||||
|
||||
return response()->json([
|
||||
'data' => $users,
|
||||
]);
|
||||
}
|
||||
|
||||
public function onlineUserCount(): JsonResponse
|
||||
{
|
||||
$count = $this->userApiService->onlineUserCount();
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'onlineCount' => $count,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function articles(): JsonResponse
|
||||
{
|
||||
$articles = WebsiteArticle::with(['user:id,username,look'])
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => ArticleResource::collection($articles),
|
||||
'meta' => [
|
||||
'current_page' => $articles->currentPage(),
|
||||
'last_page' => $articles->lastPage(),
|
||||
'per_page' => $articles->perPage(),
|
||||
'total' => $articles->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function article(string $slug): JsonResponse
|
||||
{
|
||||
$article = WebsiteArticle::with(['user:id,username,look', 'comments.user:id,username,look'])
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'data' => new ArticleResource($article),
|
||||
]);
|
||||
}
|
||||
|
||||
public function photos(): JsonResponse
|
||||
{
|
||||
$photos = CameraWeb::query()
|
||||
->where('visible', true)
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => PhotoResource::collection($photos),
|
||||
'meta' => [
|
||||
'current_page' => $photos->currentPage(),
|
||||
'last_page' => $photos->lastPage(),
|
||||
'per_page' => $photos->perPage(),
|
||||
'total' => $photos->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function staff(): JsonResponse
|
||||
{
|
||||
$employees = $this->staffService->fetchStaffPositions();
|
||||
|
||||
return response()->json([
|
||||
'data' => $employees,
|
||||
]);
|
||||
}
|
||||
|
||||
public function shopPackages(Request $request): JsonResponse
|
||||
{
|
||||
$packages = WebsiteShopArticle::latest('id')->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => ShopPackageResource::collection($packages),
|
||||
'meta' => [
|
||||
'current_page' => $packages->currentPage(),
|
||||
'last_page' => $packages->lastPage(),
|
||||
'per_page' => $packages->perPage(),
|
||||
'total' => $packages->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function shopCategories(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => ['furniture', 'badges', 'ranks'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function teams(): JsonResponse
|
||||
{
|
||||
$teams = Guild::with('members')
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => $teams->items(),
|
||||
'meta' => [
|
||||
'current_page' => $teams->currentPage(),
|
||||
'last_page' => $teams->lastPage(),
|
||||
'per_page' => $teams->perPage(),
|
||||
'total' => $teams->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function leaderboard(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->query('type', 'credits');
|
||||
$limit = (int) $request->query('limit', 100);
|
||||
|
||||
$validTypes = ['credits', 'pixels'];
|
||||
if (! in_array($type, $validTypes)) {
|
||||
$type = 'credits';
|
||||
}
|
||||
|
||||
$users = User::orderByDesc($type)
|
||||
->limit($limit)
|
||||
->get(['id', 'username', 'look', 'motto', 'credits', 'pixels']);
|
||||
|
||||
return response()->json([
|
||||
'data' => LeaderboardUserResource::collection($users),
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rareValues(Request $request): JsonResponse
|
||||
{
|
||||
$categoryId = $request->query('category');
|
||||
|
||||
$query = CatalogItem::query();
|
||||
|
||||
if ($categoryId) {
|
||||
$query->where('page_id', $categoryId);
|
||||
}
|
||||
|
||||
$items = $query->with('itemBase')
|
||||
->latest('id')
|
||||
->paginate(24);
|
||||
|
||||
return response()->json([
|
||||
'data' => $items->items(),
|
||||
'meta' => [
|
||||
'current_page' => $items->currentPage(),
|
||||
'last_page' => $items->lastPage(),
|
||||
'per_page' => $items->perPage(),
|
||||
'total' => $items->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rareValuesCategories(): JsonResponse
|
||||
{
|
||||
$categories = CatalogPage::where('catalog_name', '!=', '')
|
||||
->where('visible', 1)
|
||||
->orderBy('order_number')
|
||||
->get(['id', 'catalog_name', 'icon']);
|
||||
|
||||
return response()->json([
|
||||
'data' => $categories,
|
||||
]);
|
||||
}
|
||||
|
||||
public function settings(): JsonResponse
|
||||
{
|
||||
$settings = Cache::remember('api_all_settings', 60, fn () => WebsiteSetting::all()->pluck('value', 'key'));
|
||||
|
||||
return response()->json([
|
||||
'data' => $settings,
|
||||
]);
|
||||
}
|
||||
|
||||
public function userProfile(string $username): JsonResponse
|
||||
{
|
||||
$user = User::where('username', $username)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'look' => $user->look,
|
||||
'motto' => $user->motto,
|
||||
'account_created' => $user->account_created,
|
||||
'online' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function helpTickets(Request $request): JsonResponse
|
||||
{
|
||||
$tickets = WebsiteHelpCenterTicket::with('user:id,username,look')
|
||||
->when($request->user(), fn ($q) => $q->where('user_id', $request->user()->id))
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'data' => HelpTicketResource::collection($tickets),
|
||||
'meta' => [
|
||||
'current_page' => $tickets->currentPage(),
|
||||
'last_page' => $tickets->lastPage(),
|
||||
'total' => $tickets->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function helpTicket(string $id): JsonResponse
|
||||
{
|
||||
$ticket = WebsiteHelpCenterTicket::with(['user:id,username,look', 'replies.user:id,username,look'])
|
||||
->where('id', $id)
|
||||
->firstOrFail();
|
||||
|
||||
return response()->json(['data' => new HelpTicketResource($ticket)]);
|
||||
}
|
||||
|
||||
public function helpTicketCreate(HelpTicketRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$ticket = WebsiteHelpCenterTicket::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'subject' => $validated['subject'],
|
||||
'category' => $validated['category'],
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
$ticket->replies()->create([
|
||||
'user_id' => $request->user()->id,
|
||||
'message' => $validated['message'],
|
||||
]);
|
||||
|
||||
return response()->json(['data' => new HelpTicketResource($ticket)], 201);
|
||||
}
|
||||
|
||||
public function helpTicketReply(HelpTicketReplyRequest $request, string $id): JsonResponse
|
||||
{
|
||||
$ticket = WebsiteHelpCenterTicket::where('id', $id)
|
||||
->where('user_id', $request->user()->id)
|
||||
->firstOrFail();
|
||||
|
||||
$reply = $ticket->replies()->create([
|
||||
'user_id' => $request->user()->id,
|
||||
'message' => $request->input('message'),
|
||||
]);
|
||||
|
||||
return response()->json(['data' => $reply->load('user:id,username,look')], 201);
|
||||
}
|
||||
|
||||
public function uploadPhoto(PhotoUploadRequest $request): JsonResponse
|
||||
{
|
||||
$path = $request->file('image')->store('photos', 'public');
|
||||
|
||||
$photo = CameraWeb::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'image' => '/storage/' . $path,
|
||||
'visible' => true,
|
||||
]);
|
||||
|
||||
return response()->json(['data' => new PhotoResource($photo)], 201);
|
||||
}
|
||||
|
||||
public function purchasePackage(Request $request, int $packageId): JsonResponse
|
||||
{
|
||||
$package = WebsiteShopArticle::findOrFail($packageId);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$cost = $package->costs;
|
||||
|
||||
if ($user->credits < $cost) {
|
||||
return response()->json(['error' => 'Not enough credits'], 400);
|
||||
}
|
||||
|
||||
$user->decrement('credits', $cost);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Purchase successful']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\PhotoUploadRequest;
|
||||
use App\Http\Resources\Api\PhotoResource;
|
||||
use App\Models\Miscellaneous\CameraWeb;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class MediaApiController extends Controller
|
||||
{
|
||||
public function photos(): JsonResponse
|
||||
{
|
||||
$photos = CameraWeb::query()
|
||||
->where('visible', true)
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => PhotoResource::collection($photos),
|
||||
'meta' => [
|
||||
'current_page' => $photos->currentPage(),
|
||||
'last_page' => $photos->lastPage(),
|
||||
'per_page' => $photos->perPage(),
|
||||
'total' => $photos->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function upload(PhotoUploadRequest $request): JsonResponse
|
||||
{
|
||||
$path = $request->file('image')->store('photos', 'public');
|
||||
|
||||
$photo = CameraWeb::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'image' => '/storage/' . $path,
|
||||
'visible' => true,
|
||||
]);
|
||||
|
||||
return response()->json(['data' => new PhotoResource($photo)], 201);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\ShopPackageResource;
|
||||
use App\Models\Shop\WebsiteShopArticle;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShopApiController extends Controller
|
||||
{
|
||||
public function packages(): JsonResponse
|
||||
{
|
||||
$packages = WebsiteShopArticle::latest('id')->paginate(12);
|
||||
|
||||
return response()->json([
|
||||
'data' => ShopPackageResource::collection($packages),
|
||||
'meta' => [
|
||||
'current_page' => $packages->currentPage(),
|
||||
'last_page' => $packages->lastPage(),
|
||||
'per_page' => $packages->perPage(),
|
||||
'total' => $packages->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function categories(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => ['furniture', 'badges', 'ranks'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function purchase(Request $request, int $packageId): JsonResponse
|
||||
{
|
||||
$package = WebsiteShopArticle::findOrFail($packageId);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if ($package->give_rank && $user->rank >= $package->give_rank) {
|
||||
return response()->json(['error' => 'You already have this or a higher rank'], 400);
|
||||
}
|
||||
|
||||
if ($user->credits < $package->costs) {
|
||||
return response()->json(['error' => 'Not enough credits'], 400);
|
||||
}
|
||||
|
||||
$user->decrement('credits', $package->costs);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Purchase successful']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\LeaderboardUserResource;
|
||||
use App\Models\User;
|
||||
use App\Services\User\UserApiService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserApiService $userApiService,
|
||||
) {}
|
||||
|
||||
public function fetchUser(string $username): JsonResponse
|
||||
{
|
||||
$user = $this->userApiService->fetchUser($username, ['username', 'motto', 'look']);
|
||||
|
||||
return response()->json(['data' => $user]);
|
||||
}
|
||||
|
||||
public function userProfile(string $username): JsonResponse
|
||||
{
|
||||
$user = User::where('username', $username)->first();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json(['data' => null], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'look' => $user->look,
|
||||
'motto' => $user->motto,
|
||||
'account_created' => $user->account_created,
|
||||
'online' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function onlineUsers(): JsonResponse
|
||||
{
|
||||
$users = $this->userApiService->onlineUsers(['username', 'motto', 'look'], true);
|
||||
|
||||
return response()->json(['data' => $users]);
|
||||
}
|
||||
|
||||
public function onlineUserCount(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => ['onlineCount' => $this->userApiService->onlineUserCount()],
|
||||
]);
|
||||
}
|
||||
|
||||
public function leaderboard(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->query('type', 'credits');
|
||||
$limit = (int) $request->query('limit', 100);
|
||||
|
||||
if (! in_array($type, ['credits', 'pixels'])) {
|
||||
$type = 'credits';
|
||||
}
|
||||
|
||||
$users = User::orderByDesc($type)
|
||||
->limit($limit)
|
||||
->get(['id', 'username', 'look', 'motto', 'credits', 'pixels']);
|
||||
|
||||
return response()->json([
|
||||
'data' => LeaderboardUserResource::collection($users),
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ class ArticleController extends Controller
|
||||
|
||||
public function show(WebsiteArticle $article): View
|
||||
{
|
||||
$article->load(['user:id,username,look', 'comments.user:id,username,look']);
|
||||
|
||||
$reactions = $article->reactions()
|
||||
->with('user:id,username')
|
||||
->get();
|
||||
|
||||
@@ -86,7 +86,7 @@ class SocialAuthController extends Controller
|
||||
'account_created' => time(),
|
||||
'last_login' => time(),
|
||||
'motto' => 'New player',
|
||||
'look' => 'hr-100-61.hd-180-1.ch-210-66',
|
||||
'look' => config('habbo.defaults.avatar_look'),
|
||||
'ip_register' => request()->ip(),
|
||||
'ip_current' => request()->ip(),
|
||||
]);
|
||||
|
||||
@@ -12,7 +12,7 @@ class BadgeController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
{
|
||||
$cost = 150;
|
||||
$cost = (int) setting('badge_cost', 150);
|
||||
$currencyType = 'credits';
|
||||
$folderError = false;
|
||||
$errorMessage = '';
|
||||
@@ -60,7 +60,7 @@ class BadgeController extends Controller
|
||||
return redirect()->route('login')->with('error', 'You must be logged in to purchase badges.');
|
||||
}
|
||||
|
||||
$cost = 150;
|
||||
$cost = (int) setting('badge_cost', 150);
|
||||
|
||||
if (property_exists($user, 'credits') && $user->credits !== null && $user->credits < $cost) {
|
||||
return redirect()->back()->with('error', 'You don\'t have enough credits to purchase a badge.');
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Community;
|
||||
|
||||
use App\Enums\RadioSettings;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioApplication;
|
||||
use App\Models\RadioBanner;
|
||||
@@ -20,6 +21,7 @@ use Illuminate\View\View;
|
||||
|
||||
class RadioController extends Controller
|
||||
{
|
||||
use HasRadioSettings;
|
||||
public function __construct(
|
||||
private readonly RadioStreamService $streamService,
|
||||
private readonly RadioScheduleService $scheduleService,
|
||||
@@ -120,7 +122,8 @@ class RadioController extends Controller
|
||||
]);
|
||||
|
||||
if ($validated['rank_id']) {
|
||||
$rank = Cache::remember("radio_rank_{$validated['rank_id']}", 300, fn () => RadioRank::find($validated['rank_id']));
|
||||
$acceptingRanks = Cache::remember('radio_ranks_accepting', 300, fn () => RadioRank::where('accepts_applications', true)->get()->keyBy('id'));
|
||||
$rank = $acceptingRanks->get((int) $validated['rank_id']);
|
||||
|
||||
if (! $rank || ! $rank->accepts_applications) {
|
||||
return back()->withErrors([
|
||||
@@ -161,6 +164,7 @@ class RadioController extends Controller
|
||||
]);
|
||||
|
||||
Cache::forget('radio_shouts_recent');
|
||||
Cache::forget('api_radio_shouts');
|
||||
|
||||
return redirect()->route('radio.shouts')->with('success', __('radio.shout_sent'));
|
||||
}
|
||||
@@ -264,7 +268,7 @@ class RadioController extends Controller
|
||||
|
||||
if ($activeSession) {
|
||||
return response()->json([
|
||||
'error' => 'Je hebt al een actieve sessie',
|
||||
'error' => 'You already have an active session',
|
||||
'session_id' => $activeSession->id,
|
||||
], 400);
|
||||
}
|
||||
@@ -276,7 +280,7 @@ class RadioController extends Controller
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Sessie gestart',
|
||||
'message' => 'Session started',
|
||||
'session_id' => $session->id,
|
||||
]);
|
||||
}
|
||||
@@ -288,13 +292,13 @@ class RadioController extends Controller
|
||||
$activeSession = RadioHistory::where('user_id', $userId)->whereNull('ended_at')->first();
|
||||
|
||||
if (! $activeSession) {
|
||||
return response()->json(['error' => 'Geen actieve sessie gevonden'], 404);
|
||||
return response()->json(['error' => 'No active session found'], 404);
|
||||
}
|
||||
|
||||
$activeSession->endSession();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Sessie beëindigd',
|
||||
'message' => 'Session ended',
|
||||
'duration' => $activeSession->duration,
|
||||
]);
|
||||
}
|
||||
@@ -302,16 +306,16 @@ class RadioController extends Controller
|
||||
public function getShouts(): JsonResponse
|
||||
{
|
||||
if (! $this->getSetting(RadioSettings::ShoutsEnabled)) {
|
||||
return response()->json(['error' => 'Shouts zijn uitgeschakeld', 'shouts' => []], 403);
|
||||
return response()->json(['error' => 'Shouts are disabled', 'shouts' => []], 403);
|
||||
}
|
||||
|
||||
$shouts = Cache::remember('radio_shouts_recent', 30, fn () => RadioShout::with('user:id,username')
|
||||
$shouts = Cache::remember('api_radio_shouts', 30, fn () => RadioShout::with('user:id,username')
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(50)
|
||||
->get()
|
||||
->map(fn ($shout) => [
|
||||
'id' => $shout->id,
|
||||
'username' => $shout->user?->username ?? 'Anoniem',
|
||||
'username' => $shout->user?->username ?? 'Anonymous',
|
||||
'message' => $shout->message,
|
||||
'created_at' => $shout->created_at->diffForHumans(),
|
||||
]));
|
||||
@@ -336,13 +340,4 @@ class RadioController extends Controller
|
||||
return WebsiteSetting::whereIn('key', $stringKeys)->pluck('value', 'key')->all();
|
||||
});
|
||||
}
|
||||
|
||||
private function getSetting(RadioSettings $setting, mixed $default = null): mixed
|
||||
{
|
||||
return Cache::remember("setting_{$setting->value}", 60, function () use ($setting, $default): mixed {
|
||||
$websiteSetting = WebsiteSetting::where('key', $setting->value)->first();
|
||||
|
||||
return $websiteSetting?->value ?? $default;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Concerns;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\RadioRank;
|
||||
|
||||
trait HasRadioDefaults
|
||||
{
|
||||
private function createDefaultRanks(): void
|
||||
{
|
||||
$ranks = [
|
||||
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
|
||||
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
|
||||
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
|
||||
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
|
||||
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
|
||||
];
|
||||
|
||||
foreach ($ranks as $rank) {
|
||||
RadioRank::updateOrCreate(
|
||||
['name' => $rank['name']],
|
||||
$rank,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveRadioSettings(array $settings): void
|
||||
{
|
||||
foreach ($settings as $key => $value) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) $value, 'comment' => 'Radio setting'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Concerns;
|
||||
|
||||
use App\Enums\RadioSettings;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
trait HasRadioSettings
|
||||
{
|
||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
||||
|
||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
||||
$setting = WebsiteSetting::where('key', $keyStr)->first();
|
||||
|
||||
return $setting?->value ?? $default;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,14 @@ class InstallationController extends Controller
|
||||
'installation_key' => ['required', 'string', 'max:255', new ValidateInstallationKeyRule],
|
||||
]);
|
||||
|
||||
WebsiteInstallation::first()->update([
|
||||
try {
|
||||
WebsiteInstallation::firstOrFail()->update([
|
||||
'step' => 1,
|
||||
'user_ip' => $request->ip(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['message' => 'Installation record not found. Please restart.']);
|
||||
}
|
||||
|
||||
return to_route('installation.show-step', 1);
|
||||
}
|
||||
@@ -55,25 +59,31 @@ class InstallationController extends Controller
|
||||
{
|
||||
$this->updateSettings($request);
|
||||
|
||||
WebsiteInstallation::first()->increment('step');
|
||||
$installation = WebsiteInstallation::firstOrFail();
|
||||
$installation->increment('step');
|
||||
|
||||
return to_route('installation.show-step', WebsiteInstallation::first()->step);
|
||||
return to_route('installation.show-step', $installation->step);
|
||||
}
|
||||
|
||||
public function previousStep(): RedirectResponse
|
||||
{
|
||||
WebsiteInstallation::first()->decrement('step');
|
||||
$installation = WebsiteInstallation::firstOrFail();
|
||||
$installation->decrement('step');
|
||||
|
||||
return to_route('installation.show-step', WebsiteInstallation::first()->step);
|
||||
return to_route('installation.show-step', $installation->step);
|
||||
}
|
||||
|
||||
public function restartInstallation(): RedirectResponse
|
||||
{
|
||||
WebsiteInstallation::first()->update([
|
||||
try {
|
||||
WebsiteInstallation::firstOrFail()->update([
|
||||
'step' => 0,
|
||||
'installation_key' => Str::uuid(),
|
||||
'user_ip' => null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return to_route('installation.index');
|
||||
}
|
||||
|
||||
WebsiteSetting::where('key', 'theme')->update([
|
||||
'value' => 'atom',
|
||||
@@ -87,9 +97,13 @@ class InstallationController extends Controller
|
||||
Cache::forget('website_permissions');
|
||||
Cache::forget('website_settings');
|
||||
|
||||
WebsiteInstallation::latest()->first()->update([
|
||||
try {
|
||||
WebsiteInstallation::latest()->firstOrFail()->update([
|
||||
'completed' => true,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return to_route('installation.index');
|
||||
}
|
||||
|
||||
return to_route('welcome');
|
||||
}
|
||||
@@ -111,7 +125,8 @@ class InstallationController extends Controller
|
||||
|
||||
private function getSettingsForStep(int $step): Collection
|
||||
{
|
||||
$settingsData = array_chunk(WebsiteSetting::all()->pluck('key')->toArray(), (int) ceil(WebsiteSetting::count() / 4));
|
||||
$allKeys = WebsiteSetting::pluck('key')->toArray();
|
||||
$settingsData = array_chunk($allKeys, (int) ceil(count($allKeys) / 4));
|
||||
|
||||
$settings = match ($step) {
|
||||
1 => $settingsData[0] ?? [],
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class LogoGeneratorController extends Controller
|
||||
@@ -24,9 +25,25 @@ class LogoGeneratorController extends Controller
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['logo' => 'required|image|mimes:jpeg,png,gif,webp|max:5120']);
|
||||
$request->validate([
|
||||
'logo' => [
|
||||
'required',
|
||||
'image',
|
||||
'mimes:jpeg,png,gif,webp',
|
||||
'max:5120',
|
||||
],
|
||||
]);
|
||||
|
||||
$path = $request->file('logo')->store('generated-logos', 'public');
|
||||
$file = $request->file('logo');
|
||||
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file->getPathname());
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (! in_array($mime, $allowedMimes, true)) {
|
||||
return response()->json(['success' => false, 'message' => 'Invalid file type.'], 422);
|
||||
}
|
||||
|
||||
$filename = 'logo_' . Str::random(16) . '.' . $file->getClientOriginalExtension();
|
||||
$path = $file->storeAs('generated-logos', $filename, 'public');
|
||||
|
||||
$setting = WebsiteSetting::where('key', 'cms_logo')->first();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
|
||||
|
||||
use App\Enums\RadioSettings;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -15,6 +15,7 @@ use Illuminate\View\View;
|
||||
|
||||
class EmbedController extends Controller
|
||||
{
|
||||
use HasRadioSettings;
|
||||
public function __construct(
|
||||
private readonly RadioStreamService $streamService,
|
||||
) {}
|
||||
@@ -53,14 +54,4 @@ class EmbedController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
||||
|
||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
||||
$websiteSetting = WebsiteSetting::where('key', $keyStr)->first();
|
||||
|
||||
return $websiteSetting?->value ?? $default;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
|
||||
|
||||
use App\Enums\RadioSettings;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||
use App\Services\Community\RadioScheduleService;
|
||||
use App\Services\Community\RadioStreamService;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class SseController extends Controller
|
||||
{
|
||||
use HasRadioSettings;
|
||||
private const int SSE_KEEPALIVE = 15;
|
||||
|
||||
public function __construct(
|
||||
@@ -179,15 +180,4 @@ class SseController extends Controller
|
||||
|
||||
return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId;
|
||||
}
|
||||
|
||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
||||
|
||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
||||
$setting = WebsiteSetting::where('key', $keyStr)->first();
|
||||
|
||||
return $setting?->value ?? $default;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Http\Requests\RadioSongRequestFormRequest;
|
||||
use App\Models\RadioSongRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -12,7 +12,7 @@ class RadioSongRequestController extends Controller
|
||||
{
|
||||
private function isRequestsEnabled(): bool
|
||||
{
|
||||
return Cache::remember('radio_requests_enabled', 60, fn () => (bool) WebsiteSetting::where('key', 'radio_requests_enabled')->first()?->value);
|
||||
return Cache::remember('radio_requests_enabled', 60, fn () => (bool) \App\Models\Miscellaneous\WebsiteSetting::where('key', 'radio_requests_enabled')->first()?->value);
|
||||
}
|
||||
|
||||
public function index(): View|RedirectResponse
|
||||
|
||||
@@ -28,7 +28,7 @@ class ShopController extends Controller
|
||||
}
|
||||
|
||||
return view('shop.shop', [
|
||||
'articles' => $query->with(['rank:id,rank_name', 'features'])->get(),
|
||||
'articles' => $query->with(['rank:id,rank_name', 'features'])->paginate(50),
|
||||
'categories' => WebsiteShopCategory::whereHas('articles')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -40,13 +40,15 @@ class AccountSettingsController extends Controller
|
||||
return redirect()->back()->withErrors('User not found');
|
||||
}
|
||||
|
||||
if ($user->mail !== $request->input('mail')) {
|
||||
$this->userService->updateField($user, 'mail', $request->input('mail'));
|
||||
$validated = $request->validated();
|
||||
|
||||
if ($user->mail !== $validated['mail']) {
|
||||
$this->userService->updateField($user, 'mail', $validated['mail']);
|
||||
}
|
||||
|
||||
if ($user->motto !== $request->input('motto')) {
|
||||
$this->rconService->setMotto($user, $request->input('motto'));
|
||||
$this->userService->updateField($user, 'motto', $request->input('motto'));
|
||||
if ($user->motto !== $validated['motto']) {
|
||||
$this->rconService->setMotto($user, $validated['motto']);
|
||||
$this->userService->updateField($user, 'motto', $validated['motto']);
|
||||
}
|
||||
|
||||
return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated'));
|
||||
|
||||
@@ -17,9 +17,11 @@ class GuestbookController extends Controller
|
||||
{
|
||||
$this->validateGuestbookPost($user, $request);
|
||||
|
||||
$validated = $request->validated();
|
||||
|
||||
$user->profileGuestbook()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'message' => $request->input('message'),
|
||||
'message' => $validated['message'],
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', __('Your message has been posted.'));
|
||||
@@ -27,7 +29,11 @@ class GuestbookController extends Controller
|
||||
|
||||
public function destroy(User $user, WebsiteUserGuestbook $guestbook): RedirectResponse
|
||||
{
|
||||
if ($guestbook->user_id !== Auth::id() && $guestbook->profile_id !== $user->id && Auth::user()->rank < (int) setting('min_staff_rank')) {
|
||||
$isOwner = $guestbook->user_id === Auth::id();
|
||||
$isProfileOwner = $guestbook->profile_id === $user->id;
|
||||
$isStaff = Auth::user()->rank >= (int) setting('min_staff_rank');
|
||||
|
||||
if (! $isOwner && ! ($isProfileOwner && $isStaff)) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => __('Do do not have permission to delete this message'),
|
||||
]);
|
||||
|
||||
@@ -20,8 +20,10 @@ class ProfileController extends Controller
|
||||
'badges',
|
||||
]);
|
||||
|
||||
$showStats = (bool) (WebsiteSetting::where('key', 'profile_show_stats')->first()?->value ?? '1');
|
||||
$showOnline = (bool) (WebsiteSetting::where('key', 'profile_show_online_status')->first()?->value ?? '1');
|
||||
$settings = WebsiteSetting::whereIn('key', ['profile_show_stats', 'profile_show_online_status'])
|
||||
->pluck('value', 'key');
|
||||
$showStats = (bool) ($settings['profile_show_stats'] ?? '1');
|
||||
$showOnline = (bool) ($settings['profile_show_online_status'] ?? '1');
|
||||
|
||||
return view('user.profile', [
|
||||
'user' => $user,
|
||||
|
||||
@@ -20,7 +20,7 @@ class AdminSecurityMiddleware
|
||||
}
|
||||
|
||||
// Check 2: Must have admin rank
|
||||
$minRank = (int) setting('min_staff_rank', 7);
|
||||
$minRank = (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank'));
|
||||
if ($user->rank < $minRank) {
|
||||
Log::warning('[Security] Unauthorized API access attempt', [
|
||||
'user_id' => $user->id,
|
||||
|
||||
@@ -16,12 +16,12 @@ class ForceStaffTwoFactorMiddleware
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$urls = [
|
||||
'user/settings/two-factor',
|
||||
'user/settings/2fa-verify',
|
||||
$allowedRoutes = [
|
||||
'settings.two-factor',
|
||||
'two-factor.verify',
|
||||
];
|
||||
|
||||
if (($user->rank >= setting('min_staff_rank') && ! $user->two_factor_confirmed) && ! in_array(request()->path(), $urls)) {
|
||||
if (($user->rank >= setting('min_staff_rank', config('habbo.defaults.min_staff_rank')) && ! $user->two_factor_confirmed) && ! in_array(request()->route()?->getName(), $allowedRoutes)) {
|
||||
return to_route('settings.two-factor');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class LogStaffActivity
|
||||
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
if (auth()->check() && auth()->user()->rank >= (int) setting('min_staff_rank', 3)) {
|
||||
if (auth()->check() && auth()->user()->rank >= (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank_login'))) {
|
||||
$this->logRequest($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ class RadioApiKey
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $permission = '*'): Response
|
||||
{
|
||||
$key = $request->bearerToken() ?? $request->query('api_key');
|
||||
$key = $request->bearerToken();
|
||||
|
||||
if (empty($key)) {
|
||||
return response()->json([
|
||||
'error' => 'API key is verplicht. Gebruik Authorization: Bearer <key> of ?api_key=<key>',
|
||||
'error' => 'API key is required. Use Authorization: Bearer <key>',
|
||||
], 401);
|
||||
}
|
||||
|
||||
@@ -25,19 +25,19 @@ class RadioApiKey
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => 'API key is ongeldig of verlopen',
|
||||
'error' => 'API key is invalid or expired',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (! $apiKey->isAllowedIp($request->ip())) {
|
||||
return response()->json([
|
||||
'error' => 'IP-adres niet toegestaan voor deze API key',
|
||||
'error' => 'IP address not allowed for this API key',
|
||||
], 403);
|
||||
}
|
||||
|
||||
if (! $apiKey->hasPermission($permission)) {
|
||||
return response()->json([
|
||||
'error' => 'Geen toestemming voor deze actie',
|
||||
'error' => 'No permission for this action',
|
||||
], 403);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class VPNCheckerMiddleware
|
||||
return $this->denyAccess($request);
|
||||
}
|
||||
|
||||
$ipService = new IpLookupService('');
|
||||
$ipService = new IpLookupService;
|
||||
|
||||
$countryInfo = $ipService->getCountryInfo($userIp);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class LogStaffLogin
|
||||
public function handle(Login $event): void
|
||||
{
|
||||
$user = $event->user;
|
||||
$minStaffRank = (int) setting('min_staff_rank', 3);
|
||||
$minStaffRank = (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank_login'));
|
||||
|
||||
if ($user && $user->rank >= $minStaffRank) {
|
||||
StaffActivity::logLogin($user->id);
|
||||
|
||||
@@ -44,10 +44,6 @@ class WebsiteArticle extends Model
|
||||
use BelongsToUser;
|
||||
use HasSlug, \Illuminate\Database\Eloquent\Factories\HasFactory, SoftDeletes;
|
||||
|
||||
/** @var array<int, string> */
|
||||
#[\Override]
|
||||
protected $with = ['user'];
|
||||
|
||||
protected static function newFactory()
|
||||
{
|
||||
return WebsiteArticleFactory::new();
|
||||
|
||||
@@ -41,12 +41,8 @@ class WebsiteHelpCenterTicket extends Model
|
||||
#[\Override]
|
||||
protected $guarded = ['id', 'created_at', 'updated_at', 'user_id', 'status', 'subject', 'category_id'];
|
||||
|
||||
/** @var array<int, string> */
|
||||
#[\Override]
|
||||
protected $with = ['user', 'category'];
|
||||
|
||||
#[\Override]
|
||||
public $timestamps = false;
|
||||
public $timestamps = true;
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
@@ -75,7 +71,7 @@ class WebsiteHelpCenterTicket extends Model
|
||||
|
||||
public function isOpen()
|
||||
{
|
||||
return $this->open || hasPermission('manage_website_tickets');
|
||||
return (bool) $this->open;
|
||||
}
|
||||
|
||||
public function getContentAttribute($value)
|
||||
|
||||
@@ -13,10 +13,6 @@ class RadioShout extends Model
|
||||
use BelongsToUser;
|
||||
use HasFactory;
|
||||
|
||||
/** @var array<int, string> */
|
||||
#[\Override]
|
||||
protected $with = ['user'];
|
||||
|
||||
#[\Override]
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
|
||||
+3
-16
@@ -125,10 +125,10 @@ class User extends Authenticatable implements FilamentUser, HasName
|
||||
public $timestamps = false;
|
||||
|
||||
#[\Override]
|
||||
protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'team_id', 'avatar_background', 'home_background', 'pincode', 'secret_key', 'extra_rank', 'is_hidden', 'background_id', 'background_stand_id', 'background_overlay_id', 'radio_points', 'pixels', 'points', 'online', 'gender', 'rank', 'mail_verified', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'];
|
||||
protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'last_username_change', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'avatar_background', 'home_background', 'background_id', 'background_stand_id', 'background_overlay_id', 'gender'];
|
||||
|
||||
#[\Override]
|
||||
protected $hidden = ['id', 'password', 'remember_token'];
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
@@ -361,7 +361,7 @@ class User extends Authenticatable implements FilamentUser, HasName
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->update(['two_factor_confirmed' => true]);
|
||||
$this->forceFill(['two_factor_confirmed_at' => now()])->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -394,19 +394,6 @@ class User extends Authenticatable implements FilamentUser, HasName
|
||||
->logOnlyDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
#[\Override]
|
||||
public function save(array $options = []): bool
|
||||
{
|
||||
if (! $this->isDirty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::save($options);
|
||||
}
|
||||
|
||||
public function hasAppliedForTeam(int $teamId): bool
|
||||
{
|
||||
if ($teamId === 0) {
|
||||
|
||||
@@ -18,7 +18,7 @@ readonly class StaffService
|
||||
return Cache::get('staff_positions');
|
||||
}
|
||||
|
||||
$minStaffRank = (int) setting('min_staff_rank', 3);
|
||||
$minStaffRank = (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank_login'));
|
||||
$minRankToSeeHidden = (int) setting('min_rank_to_see_hidden_staff', 7);
|
||||
$userRank = Auth::check() ? Auth::user()->rank : 0;
|
||||
|
||||
@@ -49,7 +49,7 @@ readonly class StaffService
|
||||
return Cache::get('staff_ids');
|
||||
}
|
||||
|
||||
$minRank = (int) setting('min_staff_rank', 3);
|
||||
$minRank = (int) setting('min_staff_rank', config('habbo.defaults.min_staff_rank_login'));
|
||||
|
||||
$staffIds = User::query()->select('id')
|
||||
->where('rank', '>=', $minRank)
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Emulator;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class EmulatorBuildService
|
||||
{
|
||||
use EmulatorConfiguration;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfiguration();
|
||||
}
|
||||
|
||||
public function buildFromSource(bool $force = false): array
|
||||
{
|
||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
|
||||
if (! $repo) {
|
||||
return ['success' => false, 'error' => 'Geen source repo geconfigureerd'];
|
||||
}
|
||||
|
||||
$sourcePath = $this->emulatorSourcePath;
|
||||
$serviceName = $this->emulatorService;
|
||||
|
||||
Log::info('[EmulatorBuild] Starting source build', [
|
||||
'repo' => $repo,
|
||||
'branch' => $branch,
|
||||
'path' => $sourcePath,
|
||||
]);
|
||||
|
||||
$this->ensureJavaInstalled();
|
||||
|
||||
Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)));
|
||||
Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath));
|
||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)));
|
||||
|
||||
$maxRetries = 2;
|
||||
$lastError = '';
|
||||
|
||||
for ($retry = 0; $retry <= $maxRetries; $retry++) {
|
||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true');
|
||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true');
|
||||
|
||||
if ($retry > 0) {
|
||||
Log::info('[EmulatorBuild] Retry attempt', ['attempt' => $retry]);
|
||||
Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath));
|
||||
}
|
||||
|
||||
try {
|
||||
$existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'");
|
||||
$sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists';
|
||||
|
||||
$gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'");
|
||||
$isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git';
|
||||
|
||||
if ($sourceExists && $isGitRepo) {
|
||||
Log::info('[EmulatorBuild] Pulling latest changes');
|
||||
$commands = [
|
||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin',
|
||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch),
|
||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch),
|
||||
];
|
||||
} else {
|
||||
Log::info('[EmulatorBuild] Cloning repository');
|
||||
$commands = [
|
||||
'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath),
|
||||
'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)),
|
||||
'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath),
|
||||
'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath),
|
||||
];
|
||||
}
|
||||
|
||||
$command = implode(' && ', $commands);
|
||||
$result = Process::timeout(300)->run($command);
|
||||
|
||||
if ($result->failed()) {
|
||||
$lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300);
|
||||
Log::warning('[EmulatorBuild] Git operation failed', ['error' => $lastError, 'attempt' => $retry]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$buildCommands = $this->getBuildCommands($sourcePath);
|
||||
|
||||
Log::info('[EmulatorBuild] Running build', ['command' => $buildCommands]);
|
||||
$buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $buildCommands);
|
||||
|
||||
$hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal');
|
||||
|
||||
if ($hasSignalError) {
|
||||
Log::warning('[EmulatorBuild] Build process received signal, checking if JAR was built anyway');
|
||||
}
|
||||
|
||||
$jarPath = $this->findBuiltJar($sourcePath);
|
||||
|
||||
if ($jarPath) {
|
||||
Log::info('[EmulatorBuild] JAR found despite build status', ['jar' => $jarPath]);
|
||||
} elseif ($buildResult->failed() && ! $hasSignalError) {
|
||||
$lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500);
|
||||
Log::warning('[EmulatorBuild] Build failed', ['error' => $lastError, 'attempt' => $retry]);
|
||||
|
||||
if ($retry < $maxRetries) {
|
||||
$cleanCommands = [
|
||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true',
|
||||
];
|
||||
Process::timeout(60)->run(implode(' && ', $cleanCommands));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $jarPath) {
|
||||
$lastError = 'Build succeeded but JAR not found. Check build output.';
|
||||
Log::warning('[EmulatorBuild] JAR not found', ['attempt' => $retry]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return $this->deployJar($jarPath, $serviceName);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$lastError = $e->getMessage();
|
||||
Log::error('[EmulatorBuild] Source build exception', ['error' => $lastError, 'attempt' => $retry]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError,
|
||||
];
|
||||
}
|
||||
|
||||
public function getBuildCommands(string $sourcePath): string
|
||||
{
|
||||
$pomPath = $sourcePath;
|
||||
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
|
||||
|
||||
if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') {
|
||||
$pomPath = $sourcePath . '/Emulator';
|
||||
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
|
||||
}
|
||||
|
||||
$gradlewPath = $sourcePath . '/gradlew';
|
||||
$gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'");
|
||||
|
||||
$gradlePath = $sourcePath . '/build.gradle';
|
||||
$gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'");
|
||||
|
||||
if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') {
|
||||
return "cd {$pomPath} && mvn clean package -DskipTests 2>&1";
|
||||
}
|
||||
|
||||
if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') {
|
||||
return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1";
|
||||
}
|
||||
|
||||
if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') {
|
||||
return "cd {$sourcePath} && gradle clean build -x test 2>&1";
|
||||
}
|
||||
|
||||
return "cd {$sourcePath} && ls -la";
|
||||
}
|
||||
|
||||
public function findBuiltJar(string $sourcePath): ?string
|
||||
{
|
||||
$patterns = [
|
||||
$sourcePath . '/target/*.jar',
|
||||
$sourcePath . '/build/libs/*.jar',
|
||||
$sourcePath . '/*/*.jar',
|
||||
$sourcePath . '/*.jar',
|
||||
$sourcePath . '/Emulator/target/*.jar',
|
||||
$sourcePath . '/Emulator/build/libs/*.jar',
|
||||
$sourcePath . '/Emulator/*/*.jar',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1');
|
||||
if ($result->successful()) {
|
||||
$jarPath = trim($result->output());
|
||||
if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) {
|
||||
return $jarPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function deployJar(string $jarPath, string $serviceName): array
|
||||
{
|
||||
$jarName = basename($jarPath);
|
||||
$version = $this->extractVersionFromFilename($jarName);
|
||||
if ($version === '' || $version === '0') {
|
||||
$version = date('Y.m.d');
|
||||
}
|
||||
|
||||
$deployCommands = [
|
||||
'mkdir -p ' . escapeshellarg((string) $this->jarPath),
|
||||
'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath),
|
||||
'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi',
|
||||
'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
||||
'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
||||
'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
||||
'ls -la ' . escapeshellarg((string) $this->jarPath) . '/',
|
||||
'systemctl restart ' . escapeshellarg((string) $serviceName) . ' 2>&1 || service ' . escapeshellarg((string) $serviceName) . ' restart 2>&1 || true',
|
||||
];
|
||||
|
||||
$deployCommand = implode(' && ', $deployCommands);
|
||||
|
||||
Log::info('[EmulatorBuild] Deploying jar', [
|
||||
'source' => $jarPath,
|
||||
'destination' => "{$this->jarPath}/{$jarName}",
|
||||
]);
|
||||
|
||||
$deployResult = Process::timeout(60)->run($deployCommand);
|
||||
|
||||
if (! $deployResult->successful()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300),
|
||||
];
|
||||
}
|
||||
|
||||
$sourceService = new EmulatorSourceService;
|
||||
$sourceInfo = $sourceService->checkForUpdates();
|
||||
$installedDate = $sourceInfo['latest_timestamp'] ?? time();
|
||||
|
||||
$this->settings->set('emulator_version', $version);
|
||||
$this->settings->set('emulator_jar_installed_date', (string) $installedDate);
|
||||
$this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown');
|
||||
$this->settings->set('emulator_source_date', (string) $installedDate);
|
||||
|
||||
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
$this->settings->set('emulator_installed_branch', $currentBranch);
|
||||
|
||||
$sqlService = new EmulatorSqlService;
|
||||
$sqlService->runUpdates();
|
||||
|
||||
Log::info('[EmulatorBuild] Source build successful');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'version' => $version,
|
||||
'jar' => $jarName,
|
||||
'built' => true,
|
||||
'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart",
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureJavaInstalled(): void
|
||||
{
|
||||
$javaCheck = Process::timeout(5)->run('java -version 2>&1');
|
||||
if (! $javaCheck->successful()) {
|
||||
Log::warning('[EmulatorBuild] Java not found, attempting to install');
|
||||
Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1');
|
||||
}
|
||||
|
||||
$mavenCheck = Process::timeout(5)->run('which mvn');
|
||||
if (! $mavenCheck->successful()) {
|
||||
Log::warning('[EmulatorBuild] Maven not found, attempting to install');
|
||||
Process::timeout(180)->run('apt-get install -y maven 2>&1');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Emulator;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EmulatorJarService
|
||||
{
|
||||
use EmulatorConfiguration;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfiguration();
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
|
||||
}
|
||||
|
||||
public function checkForUpdates(): array
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return [
|
||||
'update_available' => false,
|
||||
'error' => 'Configureer een GitHub URL of directe .jar URL',
|
||||
];
|
||||
}
|
||||
|
||||
$sourceService = new EmulatorSourceService;
|
||||
$sourceInfo = $sourceService->checkForUpdates();
|
||||
$hasSourceUpdates = $sourceInfo && $sourceInfo['has_update'];
|
||||
|
||||
if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) {
|
||||
return $this->checkDirectUrlUpdates($sourceInfo, $hasSourceUpdates);
|
||||
}
|
||||
|
||||
return $this->checkGitHubFolderUpdates($sourceInfo, $hasSourceUpdates);
|
||||
}
|
||||
|
||||
public function performUpdate(array $check): array
|
||||
{
|
||||
$jarUrl = $check['jar_url'];
|
||||
$jarName = $check['jar_name'];
|
||||
$version = $check['latest_version'];
|
||||
$serviceName = $this->emulatorService;
|
||||
|
||||
$tempDir = '/tmp/emulator-update-' . Str::random(8);
|
||||
$tempJar = $tempDir . '/' . $jarName;
|
||||
|
||||
$updateScript = $this->buildUpdateScript($jarUrl, $jarName, $tempDir, $tempJar, $serviceName);
|
||||
|
||||
$scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh';
|
||||
file_put_contents($scriptPath, $updateScript);
|
||||
chmod($scriptPath, 0755);
|
||||
|
||||
Log::info('[EmulatorJar] Starting update', [
|
||||
'version' => $version,
|
||||
'jar' => $jarName,
|
||||
'url' => $jarUrl,
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1');
|
||||
@unlink($scriptPath);
|
||||
|
||||
if ($result->exitCode() !== 0) {
|
||||
Log::error('[EmulatorJar] Update failed', [
|
||||
'output' => $result->output(),
|
||||
'error' => $result->errorOutput(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Update mislukt: ' . substr($result->output(), 0, 300),
|
||||
];
|
||||
}
|
||||
|
||||
$this->storeUpdateInfo($version, $check);
|
||||
|
||||
Log::info('[EmulatorJar] Update successful');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'version' => $version,
|
||||
'jar' => $jarName,
|
||||
'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart",
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[EmulatorJar] Exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function findLatestJar(): ?array
|
||||
{
|
||||
if (! $this->githubRepo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$branch = $this->githubBranch ?: 'main';
|
||||
|
||||
$commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar'];
|
||||
|
||||
foreach ($commonNames as $name) {
|
||||
try {
|
||||
$apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}";
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
||||
])
|
||||
->get($apiUrl);
|
||||
|
||||
if (! $response->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = json_decode($response->body(), true);
|
||||
|
||||
if (! isset($data['sha']) || ! isset($data['download_url'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitDate = null;
|
||||
$commitSha = $data['sha'];
|
||||
|
||||
try {
|
||||
$commitsResponse = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
||||
])
|
||||
->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
|
||||
'path' => "Latest_Compiled_Version/{$name}",
|
||||
'sha' => $branch,
|
||||
'per_page' => 1,
|
||||
]);
|
||||
|
||||
if ($commitsResponse->successful()) {
|
||||
$commits = $commitsResponse->json();
|
||||
if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) {
|
||||
$commitDate = strtotime($commits[0]['commit']['committer']['date']);
|
||||
$commitSha = $commits[0]['sha'] ?? $data['sha'];
|
||||
}
|
||||
}
|
||||
} catch (\Exception) {
|
||||
Log::debug('[EmulatorJar] Could not fetch commit date for ' . $name);
|
||||
}
|
||||
|
||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
||||
$installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null);
|
||||
|
||||
$isUpdate = false;
|
||||
if ($installedJarCommit !== null) {
|
||||
$isUpdate = $installedJarCommit !== $commitSha;
|
||||
} elseif ($installedDate !== null && $commitDate !== null) {
|
||||
$isUpdate = (int) $installedDate < $commitDate;
|
||||
} elseif ($installedDate === null && $commitDate !== null) {
|
||||
$isUpdate = true;
|
||||
}
|
||||
|
||||
$version = $this->extractVersionFromFilename($name);
|
||||
if ($version === '' || $version === '0') {
|
||||
$version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d');
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'url' => $data['download_url'],
|
||||
'version' => $version,
|
||||
'commit' => $commitSha,
|
||||
'commit_date' => $commitDate,
|
||||
'is_update' => $isUpdate,
|
||||
'installed_date' => $installedDate,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorJar] Error checking JAR file ' . $name, ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function checkDirectUrlUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
|
||||
{
|
||||
$jarInfo = $this->validateDirectUrl($this->jarDirectUrl);
|
||||
|
||||
if ($jarInfo) {
|
||||
if (! empty($jarInfo['version'])) {
|
||||
$currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
|
||||
if ($jarInfo['version'] !== $currentVersion) {
|
||||
$this->settings->set('emulator_version', $jarInfo['version']);
|
||||
}
|
||||
}
|
||||
|
||||
$hasJarUpdates = $jarInfo['is_update'] ?? false;
|
||||
|
||||
if ($hasSourceUpdates && ! $hasJarUpdates) {
|
||||
return [
|
||||
'update_available' => true,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'],
|
||||
'release_name' => 'Source Update',
|
||||
'jar_url' => null,
|
||||
'jar_name' => null,
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => 'source_build',
|
||||
'source_info' => $sourceInfo,
|
||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'update_available' => $hasJarUpdates,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $jarInfo['version'],
|
||||
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL',
|
||||
'jar_url' => $this->jarDirectUrl,
|
||||
'jar_name' => $jarInfo['name'],
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => $hasSourceUpdates ? 'source_build' : 'direct_url',
|
||||
'commit' => $jarInfo['commit_sha'] ?? null,
|
||||
'source_info' => $sourceInfo,
|
||||
'has_source_updates' => $hasSourceUpdates,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasSourceUpdates) {
|
||||
return [
|
||||
'update_available' => true,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
|
||||
'release_name' => 'Source Update',
|
||||
'jar_url' => null,
|
||||
'jar_name' => null,
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => 'source_build',
|
||||
'source_info' => $sourceInfo,
|
||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'update_available' => false,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'error' => 'Directe .jar URL is niet bereikbaar',
|
||||
];
|
||||
}
|
||||
|
||||
private function checkGitHubFolderUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
|
||||
{
|
||||
$jarInfo = $this->findLatestJar();
|
||||
|
||||
if (! $jarInfo) {
|
||||
if ($hasSourceUpdates) {
|
||||
return [
|
||||
'update_available' => true,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
|
||||
'release_name' => 'Source Update',
|
||||
'jar_url' => null,
|
||||
'jar_name' => null,
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => 'source_build',
|
||||
'source_info' => $sourceInfo,
|
||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'update_available' => false,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => 'Onbekend',
|
||||
'error' => 'Kon geen .jar bestand vinden',
|
||||
'type' => 'not_found',
|
||||
'source_available' => $sourceInfo !== null,
|
||||
];
|
||||
}
|
||||
|
||||
$version = $jarInfo['version'];
|
||||
$hasJarUpdates = $jarInfo['is_update'] ?? false;
|
||||
|
||||
if ($hasSourceUpdates && ! $hasJarUpdates) {
|
||||
return [
|
||||
'update_available' => true,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version,
|
||||
'release_name' => 'Source Update',
|
||||
'jar_url' => null,
|
||||
'jar_name' => null,
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => 'source_build',
|
||||
'source_info' => $sourceInfo,
|
||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'update_available' => $hasJarUpdates,
|
||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
||||
'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version,
|
||||
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub',
|
||||
'jar_url' => $jarInfo['url'],
|
||||
'jar_name' => $jarInfo['name'],
|
||||
'jar_size' => 'Onbekend',
|
||||
'type' => $hasSourceUpdates ? 'source_build' : 'github_folder',
|
||||
'commit' => $jarInfo['commit'] ?? null,
|
||||
'commit_date' => $jarInfo['commit_date'] ?? null,
|
||||
'source_info' => $sourceInfo,
|
||||
'has_source_updates' => $hasSourceUpdates,
|
||||
];
|
||||
}
|
||||
|
||||
private function validateDirectUrl(string $url): ?array
|
||||
{
|
||||
if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$jarName = basename(parse_url($url, PHP_URL_PATH));
|
||||
$version = $this->extractVersionFromFilename($jarName);
|
||||
$storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
|
||||
|
||||
$lastModified = null;
|
||||
$commitSha = null;
|
||||
$gitHubInfoAvailable = false;
|
||||
|
||||
if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) {
|
||||
$owner = $matches[1];
|
||||
$repo = $matches[2];
|
||||
$branch = $matches[3];
|
||||
$path = $matches[4];
|
||||
|
||||
try {
|
||||
$apiResponse = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
||||
])
|
||||
->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [
|
||||
'ref' => $branch,
|
||||
]);
|
||||
|
||||
if ($apiResponse->successful()) {
|
||||
$data = $apiResponse->json();
|
||||
|
||||
if (isset($data['sha']) && ! isset($data['message'])) {
|
||||
$gitHubInfoAvailable = true;
|
||||
$commitSha = $data['sha'];
|
||||
|
||||
if (isset($data['commit']['committer']['date'])) {
|
||||
$lastModified = strtotime($data['commit']['committer']['date']);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception) {
|
||||
Log::debug('[EmulatorJar] Could not fetch GitHub commit info for direct URL');
|
||||
}
|
||||
}
|
||||
|
||||
if ($lastModified === null) {
|
||||
$response = Http::timeout(10)->head($url);
|
||||
if ($response->successful()) {
|
||||
$modifiedSince = $response->header('Last-Modified');
|
||||
if ($modifiedSince) {
|
||||
$lastModified = strtotime($modifiedSince);
|
||||
if ($lastModified === false) {
|
||||
$lastModified = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$isUpdate = false;
|
||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
||||
|
||||
$storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url));
|
||||
if ($storedData !== null && is_string($storedData)) {
|
||||
$storedDataArray = json_decode($storedData, true);
|
||||
if (is_array($storedDataArray)) {
|
||||
if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) {
|
||||
$isUpdate = $commitSha !== $storedDataArray['commit_sha'];
|
||||
} elseif ($lastModified !== null && $installedDate !== null) {
|
||||
$isUpdate = (int) $installedDate < $lastModified;
|
||||
} elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) {
|
||||
$isUpdate = $lastModified > $storedDataArray['last_modified'];
|
||||
} elseif (! in_array($version, ['', '0', $storedVersion], true)) {
|
||||
$isUpdate = version_compare($version, $storedVersion) > 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$isUpdate = true;
|
||||
}
|
||||
|
||||
$infoToStore = [
|
||||
'last_checked' => time(),
|
||||
'version' => $version,
|
||||
];
|
||||
if ($lastModified) {
|
||||
$infoToStore['last_modified'] = $lastModified;
|
||||
}
|
||||
if ($commitSha) {
|
||||
$infoToStore['commit_sha'] = $commitSha;
|
||||
}
|
||||
$this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore));
|
||||
|
||||
return [
|
||||
'name' => $jarName,
|
||||
'version' => $version,
|
||||
'last_modified' => $lastModified,
|
||||
'commit_sha' => $commitSha,
|
||||
'is_update' => $isUpdate,
|
||||
'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorJar] Direct URL not reachable', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function buildUpdateScript(string $jarUrl, string $jarName, string $tempDir, string $tempJar, string $serviceName): string
|
||||
{
|
||||
return <<<BASH
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
JAR_URL='{$jarUrl}'
|
||||
JAR_NAME='{$jarName}'
|
||||
JAR_PATH='{$this->jarPath}'
|
||||
SERVICE='{$serviceName}'
|
||||
TEMP_DIR='{$tempDir}'
|
||||
TEMP_JAR='{$tempJar}'
|
||||
|
||||
mkdir -p "\$TEMP_DIR" "\$JAR_PATH"
|
||||
|
||||
if ls "\$JAR_PATH"/*.jar 1>/dev/null 2>&1; then
|
||||
mkdir -p "\$JAR_PATH/backup"
|
||||
mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
download_success=false
|
||||
for attempt in 1 2 3; do
|
||||
if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then
|
||||
FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null)
|
||||
JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0)
|
||||
|
||||
if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then
|
||||
download_success=true
|
||||
break
|
||||
else
|
||||
rm -f "\$TEMP_JAR" 2>/dev/null || true
|
||||
sleep 3
|
||||
fi
|
||||
else
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "\$download_success" = false ]; then
|
||||
if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then
|
||||
mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "\$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "\$TEMP_JAR" "\$JAR_PATH/"
|
||||
chown -R www-data:www-data "\$JAR_PATH"
|
||||
chmod 755 "\$JAR_PATH/\$JAR_NAME"
|
||||
|
||||
systemctl restart "\$SERVICE" 2>&1 || service "\$SERVICE" restart 2>&1 || true
|
||||
|
||||
rm -rf "\$TEMP_DIR" 2>/dev/null || true
|
||||
BASH;
|
||||
}
|
||||
|
||||
private function storeUpdateInfo(string $version, array $check): void
|
||||
{
|
||||
setting('emulator_version', $version);
|
||||
|
||||
$commitDate = $check['commit_date'] ?? time();
|
||||
$this->settings->set('emulator_jar_installed_date', (string) $commitDate);
|
||||
$this->settings->set('emulator_jar_commit', $check['commit'] ?? null);
|
||||
|
||||
$sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null;
|
||||
$sourceDate = $check['source_info']['latest_timestamp'] ?? ($check['commit_date'] ?? time());
|
||||
if ($sourceSha) {
|
||||
$this->settings->set('emulator_source_commit', $sourceSha);
|
||||
}
|
||||
if ($sourceDate) {
|
||||
$this->settings->set('emulator_source_date', (string) $sourceDate);
|
||||
}
|
||||
|
||||
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
$this->settings->set('emulator_installed_branch', $currentBranch);
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Emulator;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EmulatorSourceService
|
||||
{
|
||||
use EmulatorConfiguration;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfiguration();
|
||||
}
|
||||
|
||||
public function checkForUpdates(): ?array
|
||||
{
|
||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
|
||||
if (! $repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$localCheck = $this->checkLocalSourceUpdates();
|
||||
if ($localCheck !== null) {
|
||||
return $localCheck;
|
||||
}
|
||||
|
||||
return $this->checkRemoteSourceUpdates($repo, $branch);
|
||||
}
|
||||
|
||||
public function isSourceBuildAvailable(): bool
|
||||
{
|
||||
$hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true);
|
||||
$hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true);
|
||||
|
||||
if (! $hasRepo || ! $hasPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'");
|
||||
|
||||
return trim($result->output()) === 'exists';
|
||||
} catch (\Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkLocalSourceUpdates(): ?array
|
||||
{
|
||||
$sourcePath = $this->emulatorSourcePath;
|
||||
|
||||
$existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'");
|
||||
if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') {
|
||||
return $this->checkSourceByCloning();
|
||||
}
|
||||
|
||||
try {
|
||||
$gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null");
|
||||
if (! $gitCheck->successful()) {
|
||||
return $this->checkSourceByCloning();
|
||||
}
|
||||
|
||||
$currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD");
|
||||
if (! $currentCommitResult->successful()) {
|
||||
return null;
|
||||
}
|
||||
$currentSha = trim($currentCommitResult->output());
|
||||
|
||||
$fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1");
|
||||
if ($fetchResult->failed()) {
|
||||
Log::debug('[EmulatorSource] Git fetch failed, trying local check only');
|
||||
}
|
||||
|
||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
$latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD");
|
||||
if (! $latestResult->successful()) {
|
||||
return null;
|
||||
}
|
||||
$latestSha = trim($latestResult->output());
|
||||
|
||||
$dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}");
|
||||
$latestDate = null;
|
||||
$latestTimestamp = time();
|
||||
if ($dateResult->successful()) {
|
||||
$latestDate = trim($dateResult->output(), "'");
|
||||
if ($latestDate !== '' && $latestDate !== '0') {
|
||||
$latestTimestamp = strtotime($latestDate);
|
||||
}
|
||||
}
|
||||
|
||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
||||
|
||||
$msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}");
|
||||
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
|
||||
|
||||
$authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}");
|
||||
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
|
||||
|
||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
||||
|
||||
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
|
||||
if ($storedSha === null) {
|
||||
$isUpdate = true;
|
||||
}
|
||||
|
||||
return [
|
||||
'has_update' => $isUpdate,
|
||||
'latest_sha' => $latestSha,
|
||||
'latest_date' => $latestDate,
|
||||
'latest_timestamp' => $latestTimestamp,
|
||||
'latest_message' => $latestMessage,
|
||||
'latest_author' => $latestAuthor,
|
||||
'stored_sha' => $storedSha,
|
||||
'stored_date' => $storedDate,
|
||||
'source' => 'local',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('[EmulatorSource] Local source check failed: ' . $e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkSourceByCloning(): ?array
|
||||
{
|
||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
||||
|
||||
if (! $repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo);
|
||||
$repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo);
|
||||
$repo = rtrim($repo, '/');
|
||||
|
||||
try {
|
||||
$tempDir = '/tmp/emulator-source-check-' . Str::random(8);
|
||||
|
||||
$cloneResult = Process::timeout(120)->run(
|
||||
"git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1",
|
||||
);
|
||||
|
||||
if ($cloneResult->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD");
|
||||
$latestSha = $shaResult->successful() ? trim($shaResult->output()) : '';
|
||||
|
||||
$dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'");
|
||||
$latestDate = null;
|
||||
$latestTimestamp = time();
|
||||
if ($dateResult->successful()) {
|
||||
$latestDate = trim($dateResult->output());
|
||||
if ($latestDate !== '' && $latestDate !== '0') {
|
||||
$latestTimestamp = strtotime($latestDate);
|
||||
}
|
||||
}
|
||||
|
||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
||||
|
||||
$msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'");
|
||||
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
|
||||
|
||||
$authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'");
|
||||
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
|
||||
|
||||
Process::timeout(10)->run("rm -rf {$tempDir}");
|
||||
|
||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
||||
|
||||
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
|
||||
if ($storedSha === null) {
|
||||
$isUpdate = true;
|
||||
}
|
||||
|
||||
return [
|
||||
'has_update' => $isUpdate,
|
||||
'latest_sha' => $latestSha,
|
||||
'latest_date' => $latestDate,
|
||||
'latest_timestamp' => $latestTimestamp,
|
||||
'latest_message' => $latestMessage,
|
||||
'latest_author' => $latestAuthor,
|
||||
'stored_sha' => $storedSha,
|
||||
'stored_date' => $storedDate,
|
||||
'source' => 'cloned',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('[EmulatorSource] Source clone check failed: ' . $e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkRemoteSourceUpdates(string $repo, string $branch): ?array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
||||
])
|
||||
->get("https://api.github.com/repos/{$repo}/commits", [
|
||||
'sha' => $branch,
|
||||
'per_page' => 1,
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$commits = $response->json();
|
||||
|
||||
if (empty($commits) || ! isset($commits[0]['sha'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latestCommit = $commits[0];
|
||||
$latestSha = $latestCommit['sha'];
|
||||
$latestDate = $latestCommit['commit']['committer']['date'] ?? null;
|
||||
$latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time();
|
||||
|
||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
||||
|
||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
||||
|
||||
if ($storedSha !== null && $storedSha === $latestSha) {
|
||||
$isUpdate = false;
|
||||
} elseif ($storedSha !== null && $storedSha !== $latestSha) {
|
||||
$isUpdate = true;
|
||||
} elseif ($installedDate !== null) {
|
||||
$installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate);
|
||||
$isUpdate = $installedTimestamp < $latestTimestamp;
|
||||
} else {
|
||||
$isUpdate = false;
|
||||
}
|
||||
|
||||
return [
|
||||
'has_update' => $isUpdate,
|
||||
'latest_sha' => $latestSha,
|
||||
'latest_date' => $latestDate,
|
||||
'latest_timestamp' => $latestTimestamp,
|
||||
'latest_message' => $latestCommit['commit']['message'] ?? '',
|
||||
'latest_author' => $latestCommit['commit']['author']['name'] ?? '',
|
||||
'stored_sha' => $storedSha,
|
||||
'stored_date' => $storedDate,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorSource] Could not check source updates via GitHub API', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function persistSourceCommitInfo(string $sha, int $timestamp): void
|
||||
{
|
||||
$this->settings->set('emulator_source_commit', $sha);
|
||||
$this->settings->set('emulator_source_date', (string) $timestamp);
|
||||
}
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Emulator;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class EmulatorSqlService
|
||||
{
|
||||
use EmulatorConfiguration;
|
||||
|
||||
private const string SQL_TABLE = 'emulator_sql_updates';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfiguration();
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
|
||||
}
|
||||
|
||||
public function checkForUpdates(bool $recentOnly = true): array
|
||||
{
|
||||
if (! $this->githubRepo) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'error' => 'Geen GitHub repo geconfigureerd',
|
||||
];
|
||||
}
|
||||
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
$result = $this->fetchSqlFilesFromGitHub($recentOnly);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'error' => $result['error'],
|
||||
];
|
||||
}
|
||||
|
||||
$sqlFiles = $result;
|
||||
|
||||
if ($sqlFiles === []) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'message' => $recentOnly
|
||||
? 'Geen SQL updates van de afgelopen week gevonden'
|
||||
: 'Geen SQL updates gevonden',
|
||||
];
|
||||
}
|
||||
|
||||
$appliedHashes = $this->getAppliedSqlHashes();
|
||||
$newSqlFiles = [];
|
||||
$alreadyApplied = [];
|
||||
|
||||
foreach ($sqlFiles as $file) {
|
||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
||||
if (in_array($hash, $appliedHashes)) {
|
||||
$alreadyApplied[] = $file['name'];
|
||||
|
||||
continue;
|
||||
}
|
||||
$newSqlFiles[] = $file;
|
||||
}
|
||||
|
||||
if ($newSqlFiles === []) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)',
|
||||
'applied_count' => count($alreadyApplied),
|
||||
];
|
||||
}
|
||||
|
||||
usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name']));
|
||||
|
||||
return [
|
||||
'has_updates' => true,
|
||||
'count' => count($newSqlFiles),
|
||||
'files' => $newSqlFiles,
|
||||
'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week',
|
||||
'applied_count' => count($alreadyApplied),
|
||||
];
|
||||
}
|
||||
|
||||
public function runUpdates(): array
|
||||
{
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
|
||||
if (! ($sqlCheck['has_updates'] ?? false)) {
|
||||
return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates'];
|
||||
}
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'sql_updated' => true,
|
||||
'files_run' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
foreach ($sqlCheck['files'] as $file) {
|
||||
$sqlResult = $this->downloadAndRunSql($file);
|
||||
|
||||
if ($sqlResult['success']) {
|
||||
$results['files_run'][] = $file['name'];
|
||||
$this->markSqlAsApplied($file);
|
||||
} else {
|
||||
$results['errors'][] = $file['name'] . ': ' . $sqlResult['error'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($results['errors']) > 0) {
|
||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten';
|
||||
} else {
|
||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!';
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getAppliedUpdates(): array
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
return DB::table(self::SQL_TABLE)
|
||||
->orderBy('applied_at', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function diagnose(): array
|
||||
{
|
||||
$diagnosis = [
|
||||
'table_exists' => false,
|
||||
'applied_count' => 0,
|
||||
'pending_count' => 0,
|
||||
'error' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE);
|
||||
|
||||
if ($diagnosis['table_exists']) {
|
||||
$diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count();
|
||||
}
|
||||
|
||||
if ($this->githubRepo && $diagnosis['table_exists']) {
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
if (isset($sqlCheck['count'])) {
|
||||
$diagnosis['pending_count'] = $sqlCheck['count'];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$diagnosis['error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $diagnosis;
|
||||
}
|
||||
|
||||
public function repair(): array
|
||||
{
|
||||
$actions = [];
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
$this->ensureSqlTableExists();
|
||||
$actions[] = 'SQL update tabel gecontroleerd';
|
||||
|
||||
$appliedCount = DB::table(self::SQL_TABLE)->count();
|
||||
$actions[] = "SQL updates tabel OK ({$appliedCount} records)";
|
||||
|
||||
if ($this->githubRepo) {
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
|
||||
if (isset($sqlCheck['error'])) {
|
||||
$actions[] = 'SQL update check: ' . $sqlCheck['error'];
|
||||
} elseif (! ($sqlCheck['has_updates'] ?? false)) {
|
||||
$actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date');
|
||||
} else {
|
||||
$count = $sqlCheck['count'] ?? 0;
|
||||
$actions[] = "{$count} nieuwe SQL updates beschikbaar";
|
||||
|
||||
if ($this->isConfigured()) {
|
||||
$actions[] = 'SQL updates worden toegepast...';
|
||||
$runResult = $this->runUpdates();
|
||||
|
||||
if ($runResult['success'] ?? false) {
|
||||
$filesRun = count($runResult['files_run'] ?? []);
|
||||
$actions[] = "{$filesRun} SQL updates toegepast";
|
||||
} else {
|
||||
$errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'SQL repair: ' . $e->getMessage();
|
||||
Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $errors === [],
|
||||
'actions' => $actions,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureSqlTableExists(): void
|
||||
{
|
||||
if (Schema::hasTable(self::SQL_TABLE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create(self::SQL_TABLE, function ($table) {
|
||||
$table->id();
|
||||
$table->string('file_name');
|
||||
$table->string('file_hash')->unique();
|
||||
$table->timestamp('applied_at');
|
||||
$table->text('sql_content')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
private function getAppliedSqlHashes(): array
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
return DB::table(self::SQL_TABLE)
|
||||
->pluck('file_hash')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function markSqlAsApplied(array $file): void
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
||||
|
||||
DB::table(self::SQL_TABLE)->updateOrInsert(
|
||||
['file_hash' => $hash],
|
||||
[
|
||||
'file_name' => $file['name'],
|
||||
'applied_at' => now(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array
|
||||
{
|
||||
if (! $this->githubRepo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$branch = $this->githubBranch ?: 'main';
|
||||
|
||||
$folderNames = [
|
||||
'Database%20Updates',
|
||||
'Database Updates',
|
||||
'database_updates',
|
||||
'database/updates',
|
||||
'sql/updates',
|
||||
'sql',
|
||||
'updates',
|
||||
];
|
||||
|
||||
$knownSqlFiles = [
|
||||
'07012026_UpdateDatabase_to_4-0-1.sql',
|
||||
'09012026_UpdateDatabase_to_4-0-2.sql',
|
||||
'12012026_Battle Banzai.sql',
|
||||
'12012026_Breeding Fixes.sql',
|
||||
'12012026_ChatBubbles.sql',
|
||||
'16032026_updateall_command.sql',
|
||||
'17032026_allow_underpass.sql',
|
||||
'19032026_hotel_timezone.sql',
|
||||
'21022026_user_prefixes.sql',
|
||||
'Default_Camera.sql',
|
||||
'UpdateDatabase_Allow_diagonale.sql',
|
||||
'UpdateDatabase_BOT.sql',
|
||||
'UpdateDatabase_Banners.sql',
|
||||
'UpdateDatabase_DanceCMD.sql',
|
||||
'UpdateDatabase_Happiness.sql',
|
||||
'UpdateDatabase_Websocket.sql',
|
||||
'UpdateDatabase_unignorable.sql',
|
||||
];
|
||||
|
||||
try {
|
||||
if ($recentOnly) {
|
||||
return $this->fetchRecentSqlFiles($branch);
|
||||
}
|
||||
|
||||
$sqlFiles = [];
|
||||
|
||||
foreach ($knownSqlFiles as $filename) {
|
||||
foreach ($folderNames as $folderName) {
|
||||
$encodedFilename = str_replace(' ', '%20', $filename);
|
||||
$url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}";
|
||||
$response = Http::timeout(10)->get($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
$sqlFiles[] = [
|
||||
'name' => $filename,
|
||||
'url' => $url,
|
||||
'sha' => md5($response->body()),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sqlFiles !== []) {
|
||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $sqlFiles;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchRecentSqlFiles(string $branch): array
|
||||
{
|
||||
$weekAgo = now()->subDays(7)->toIso8601String();
|
||||
|
||||
$githubToken = setting('github_token', '');
|
||||
$headers = [
|
||||
'Accept' => 'application/vnd.github+json',
|
||||
'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0',
|
||||
];
|
||||
if (! empty($githubToken)) {
|
||||
$headers['Authorization'] = 'Bearer ' . $githubToken;
|
||||
}
|
||||
|
||||
$folderNames = ['Database Updates', 'database_updates', 'sql', 'updates'];
|
||||
|
||||
try {
|
||||
foreach ($folderNames as $folderName) {
|
||||
$response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
|
||||
'path' => $folderName,
|
||||
'sha' => $branch,
|
||||
'since' => $weekAgo,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$commits = $response->json();
|
||||
|
||||
if (! is_array($commits) || $commits === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sqlFiles = [];
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
$sha = $commit['sha'] ?? null;
|
||||
$commitDate = $commit['commit']['committer']['date'] ?? null;
|
||||
|
||||
if (! $sha) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}");
|
||||
|
||||
if (! $commitResponse->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitData = $commitResponse->json();
|
||||
$files = $commitData['files'] ?? [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = $file['filename'] ?? '';
|
||||
|
||||
if (! str_ends_with(strtolower((string) $filename), '.sql')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = basename((string) $filename);
|
||||
$sqlFiles[] = [
|
||||
'name' => $name,
|
||||
'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}",
|
||||
'sha' => $sha,
|
||||
'date' => $commitDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($sqlFiles !== []) {
|
||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $sqlFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadAndRunSql(array $file): array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(60)->get($file['url']);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return ['success' => false, 'error' => 'Download mislukt'];
|
||||
}
|
||||
|
||||
$sql = $this->cleanSql($response->body());
|
||||
|
||||
if (in_array(trim($sql), ['', '0'], true)) {
|
||||
return ['success' => true, 'message' => 'Lege SQL file overgeslagen'];
|
||||
}
|
||||
|
||||
$host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1'));
|
||||
$port = setting('emulator_database_port', config('database.connections.emulator.port', '3306'));
|
||||
$name = setting('emulator_database_name', config('database.connections.emulator.database', ''));
|
||||
$username = setting('emulator_database_username', config('database.connections.emulator.username', ''));
|
||||
$password = setting('emulator_database_password', config('database.connections.emulator.password', ''));
|
||||
|
||||
if (empty($name) || empty($username)) {
|
||||
return ['success' => false, 'error' => 'Emulator database niet geconfigureerd'];
|
||||
}
|
||||
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
|
||||
$username,
|
||||
$password,
|
||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
|
||||
);
|
||||
|
||||
$statements = $this->splitSqlStatements($sql);
|
||||
$successCount = 0;
|
||||
$skipCount = 0;
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim((string) $statement);
|
||||
if ($statement === '' || $statement === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->exec($statement);
|
||||
$successCount++;
|
||||
} catch (\PDOException $e) {
|
||||
if ($this->isDuplicateError($e)) {
|
||||
$skipCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::warning('[SQL Update] Statement error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)");
|
||||
|
||||
return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanSql(string $sql): string
|
||||
{
|
||||
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||
$sql = preg_replace('/#.*$/m', '', (string) $sql);
|
||||
$sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql);
|
||||
|
||||
return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql);
|
||||
}
|
||||
|
||||
private function splitSqlStatements(string $sql): array
|
||||
{
|
||||
$parts = explode(';', $sql);
|
||||
|
||||
return array_filter($parts, fn ($part) => trim((string) $part) !== '');
|
||||
}
|
||||
|
||||
private function isDuplicateError(\PDOException $e): bool
|
||||
{
|
||||
$message = strtolower($e->getMessage());
|
||||
$patterns = [
|
||||
'duplicate entry',
|
||||
"table '",
|
||||
'already exists',
|
||||
"can't create",
|
||||
'duplicate key',
|
||||
'duplicate index',
|
||||
'error 1061',
|
||||
'error 1062',
|
||||
'error 1826',
|
||||
];
|
||||
|
||||
return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern));
|
||||
}
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Services\Emulator\EmulatorBackupService;
|
||||
use App\Services\Emulator\EmulatorBuildService;
|
||||
use App\Services\Emulator\EmulatorJarService;
|
||||
use App\Services\Emulator\EmulatorSourceService;
|
||||
use App\Services\Emulator\EmulatorSqlService;
|
||||
use App\Services\Emulator\EmulatorStatusService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class EmulatorUpdateService
|
||||
{
|
||||
private readonly EmulatorStatusService $statusService;
|
||||
|
||||
private readonly EmulatorJarService $jarService;
|
||||
|
||||
private readonly EmulatorSourceService $sourceService;
|
||||
|
||||
private readonly EmulatorBuildService $buildService;
|
||||
|
||||
private readonly EmulatorSqlService $sqlService;
|
||||
|
||||
private readonly EmulatorBackupService $backupService;
|
||||
|
||||
private readonly SettingsService $settings;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->statusService = new EmulatorStatusService;
|
||||
$this->jarService = new EmulatorJarService;
|
||||
$this->sourceService = new EmulatorSourceService;
|
||||
$this->buildService = new EmulatorBuildService;
|
||||
$this->sqlService = new EmulatorSqlService;
|
||||
$this->backupService = new EmulatorBackupService;
|
||||
$this->settings = app(SettingsService::class);
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->statusService->isConfigured();
|
||||
}
|
||||
|
||||
public function getStatus(): array
|
||||
{
|
||||
$status = $this->statusService->getStatus();
|
||||
$updateCheck = $this->jarService->checkForUpdates();
|
||||
$sourceInfo = $this->sourceService->checkForUpdates();
|
||||
|
||||
return array_merge($status, [
|
||||
'update_available' => $updateCheck['update_available'] ?? false,
|
||||
'current_version' => $updateCheck['current_version'] ?? setting('emulator_version', 'N/A'),
|
||||
'latest_version' => $updateCheck['latest_version'] ?? 'N/A',
|
||||
'update_type' => $updateCheck['type'] ?? 'unknown',
|
||||
'has_source_updates' => $sourceInfo['has_update'] ?? false,
|
||||
'latest_sha' => $sourceInfo['latest_sha'] ?? null,
|
||||
'latest_message' => $sourceInfo['latest_message'] ?? null,
|
||||
'latest_author' => $sourceInfo['latest_author'] ?? null,
|
||||
'latest_date' => $sourceInfo['latest_date'] ?? null,
|
||||
'stored_sha' => $sourceInfo['stored_sha'] ?? null,
|
||||
'stored_date' => $sourceInfo['stored_date'] ?? null,
|
||||
'source_info' => $sourceInfo,
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkForUpdates(): array
|
||||
{
|
||||
return $this->jarService->checkForUpdates();
|
||||
}
|
||||
|
||||
public function checkForSqlUpdates(bool $recentOnly = true): array
|
||||
{
|
||||
return $this->sqlService->checkForUpdates($recentOnly);
|
||||
}
|
||||
|
||||
public function runSqlUpdates(): array
|
||||
{
|
||||
return $this->sqlService->runUpdates();
|
||||
}
|
||||
|
||||
public function getAppliedSqlUpdates(): array
|
||||
{
|
||||
return $this->sqlService->getAppliedUpdates();
|
||||
}
|
||||
|
||||
public function updateEmulator(): array
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return ['success' => false, 'error' => 'Geen GitHub URL geconfigureerd'];
|
||||
}
|
||||
|
||||
$check = $this->checkForUpdates();
|
||||
|
||||
if (! ($check['update_available'] ?? false)) {
|
||||
if ($check['type'] === 'not_found' && ($check['source_available'] ?? false)) {
|
||||
return $this->buildFromSource();
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'Emulator is al up-to-date'];
|
||||
}
|
||||
|
||||
$hasSourceUpdates = ($check['has_source_updates'] ?? false) || ($check['type'] ?? '') === 'source_build';
|
||||
|
||||
if ($hasSourceUpdates && $this->sourceService->isSourceBuildAvailable()) {
|
||||
return $this->buildFromSource();
|
||||
}
|
||||
|
||||
if ($check['type'] === 'source_build') {
|
||||
return $this->buildFromSource();
|
||||
}
|
||||
|
||||
if (! ($check['jar_url'] ?? null)) {
|
||||
return ['success' => false, 'error' => 'Geen .jar gevonden'];
|
||||
}
|
||||
|
||||
$result = $this->jarService->performUpdate($check);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->runSqlUpdates();
|
||||
|
||||
if ($this->restartEmulator()) {
|
||||
$result['restarted'] = true;
|
||||
$result['message'] = ($result['message'] ?? '') . ' | 🔄 Emulator herstart';
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function performUpdate(array $check): array
|
||||
{
|
||||
return $this->jarService->performUpdate($check);
|
||||
}
|
||||
|
||||
public function buildFromSource(bool $force = false): array
|
||||
{
|
||||
return $this->buildService->buildFromSource($force);
|
||||
}
|
||||
|
||||
public function restartEmulator(): bool
|
||||
{
|
||||
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'emulator');
|
||||
|
||||
try {
|
||||
Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName);
|
||||
|
||||
$result = Process::timeout(30)->run("systemctl restart {$serviceName} 2>&1");
|
||||
if ($result->successful()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = Process::timeout(30)->run("service {$serviceName} restart 2>&1");
|
||||
|
||||
return $result->successful();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getBackupList(): array
|
||||
{
|
||||
return $this->backupService->getList();
|
||||
}
|
||||
|
||||
public function restoreBackup(string $backupName): array
|
||||
{
|
||||
return $this->backupService->restore($backupName);
|
||||
}
|
||||
|
||||
public function getInstalledVersion(): string
|
||||
{
|
||||
return $this->statusService->getInstalledVersion();
|
||||
}
|
||||
|
||||
public function getInstalledJar(): ?string
|
||||
{
|
||||
return $this->statusService->getInstalledJar();
|
||||
}
|
||||
|
||||
public function getInstalledJarInfo(): array
|
||||
{
|
||||
return $this->statusService->getInstalledJarInfo();
|
||||
}
|
||||
|
||||
public function getLastSqlUpdate(): ?string
|
||||
{
|
||||
return setting('emulator_last_sql_update');
|
||||
}
|
||||
|
||||
public function debugStatus(): array
|
||||
{
|
||||
return Cache::remember('emulator_debug_status', 120, function () {
|
||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
||||
$sourceCommit = $this->settings->getOrDefault('emulator_source_commit', null);
|
||||
$sourceDate = $this->settings->getOrDefault('emulator_source_date', null);
|
||||
$emulatorVersion = $this->settings->getOrDefault('emulator_version', null);
|
||||
$jarFiles = $this->statusService->getInstalledJarInfo();
|
||||
|
||||
return [
|
||||
'github_url' => $this->settings->getOrDefault('emulator_github_url', ''),
|
||||
'github_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
|
||||
'github_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
|
||||
'source_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
|
||||
'source_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
|
||||
'installed_date' => $installedDate,
|
||||
'installed_date_formatted' => $installedDate ? date('Y-m-d H:i:s', (int) $installedDate) : null,
|
||||
'source_commit' => $sourceCommit,
|
||||
'source_date' => $sourceDate,
|
||||
'source_date_formatted' => $sourceDate ? date('Y-m-d H:i:s', (int) $sourceDate) : null,
|
||||
'emulator_version' => $emulatorVersion,
|
||||
'jar_files' => $jarFiles,
|
||||
'installed_branch' => $this->settings->getOrDefault('emulator_installed_branch', null),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function resetInstalledDate(): void
|
||||
{
|
||||
WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete();
|
||||
WebsiteSetting::where('key', 'emulator_source_commit')->delete();
|
||||
WebsiteSetting::where('key', 'emulator_source_date')->delete();
|
||||
}
|
||||
|
||||
public function clearAllLogs(): array
|
||||
{
|
||||
$cleared = [];
|
||||
$paths = [
|
||||
storage_path('logs') => 'Laravel Logs',
|
||||
storage_path('logs/emulator.log') => 'Emulator Log',
|
||||
'/tmp/emulator-update-*' => 'Emulator Update Temp',
|
||||
'/tmp/nitro-switch-*' => 'Nitro Switch Logs',
|
||||
'/tmp/nitro_*' => 'Nitro Temp',
|
||||
'/var/www/Emulator/logs' => 'Emulator Folder Logs',
|
||||
];
|
||||
|
||||
foreach ($paths as $path => $label) {
|
||||
try {
|
||||
if (str_contains($path, '*')) {
|
||||
Process::timeout(10)->run("rm -f {$path} 2>/dev/null || true");
|
||||
$cleared[] = $label;
|
||||
} elseif (is_dir($path)) {
|
||||
Process::timeout(10)->run("find {$path} -name '*.log' -mtime +1 -delete 2>/dev/null || true");
|
||||
$cleared[] = $label;
|
||||
} elseif (is_file($path)) {
|
||||
@unlink($path);
|
||||
$cleared[] = $label;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$laravelLog = storage_path('logs/laravel.log');
|
||||
if (is_file($laravelLog)) {
|
||||
file_put_contents($laravelLog, '');
|
||||
$cleared[] = 'laravel.log';
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
|
||||
Process::timeout(10)->run("find /tmp -name 'emulator_*' -mtime +1 -delete 2>/dev/null || true");
|
||||
Process::timeout(10)->run("find /tmp -name 'nitro_*' -mtime +1 -delete 2>/dev/null || true");
|
||||
Process::timeout(10)->run("find /tmp -name 'deploy_*' -mtime +1 -delete 2>/dev/null || true");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'cleared' => $cleared,
|
||||
'message' => count($cleared) . ' log locaties geleegd',
|
||||
];
|
||||
}
|
||||
|
||||
public function repairEmulator(): array
|
||||
{
|
||||
$actions = [];
|
||||
$errors = [];
|
||||
|
||||
Log::info('[EmulatorUpdate] Starting repair process');
|
||||
|
||||
try {
|
||||
$status = $this->getStatus();
|
||||
|
||||
if (! ($status['jar_exists'] ?? false)) {
|
||||
$actions[] = 'JAR bestand ontbreekt - downloaden...';
|
||||
$updateResult = $this->updateEmulator();
|
||||
if (! $updateResult['success']) {
|
||||
$errors[] = 'Kon JAR niet herstellen: ' . ($updateResult['error'] ?? 'Onbekende fout');
|
||||
} else {
|
||||
$actions[] = 'JAR bestand hersteld';
|
||||
}
|
||||
}
|
||||
|
||||
if (! ($status['service_running'] ?? false)) {
|
||||
$actions[] = 'Emulator service niet actief - starten...';
|
||||
if ($this->restartEmulator()) {
|
||||
$actions[] = 'Emulator service gestart';
|
||||
} else {
|
||||
$errors[] = 'Kon emulator service niet starten';
|
||||
}
|
||||
}
|
||||
|
||||
$sqlRepairResult = $this->sqlService->repair();
|
||||
if (! empty($sqlRepairResult['actions'])) {
|
||||
$actions = array_merge($actions, $sqlRepairResult['actions']);
|
||||
}
|
||||
if (! empty($sqlRepairResult['errors'])) {
|
||||
$errors = array_merge($errors, $sqlRepairResult['errors']);
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return [
|
||||
'success' => false,
|
||||
'actions' => $actions,
|
||||
'errors' => $errors,
|
||||
'error' => implode('; ', $errors),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'actions' => $actions,
|
||||
'message' => count($actions) . ' acties uitgevoerd',
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[EmulatorUpdate] Repair exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'actions' => $actions,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function diagnoseSqlUpdates(): array
|
||||
{
|
||||
return $this->sqlService->diagnose();
|
||||
}
|
||||
|
||||
public function diagnose(): array
|
||||
{
|
||||
$diagnosis = [
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'checks' => [],
|
||||
'issues' => [],
|
||||
'recommendations' => [],
|
||||
];
|
||||
|
||||
try {
|
||||
$status = $this->getStatus();
|
||||
|
||||
$diagnosis['checks']['jar_exists'] = $status['jar_exists'] ?? false;
|
||||
$diagnosis['checks']['jar_files'] = $status['jar_files'] ?? [];
|
||||
$diagnosis['checks']['service_running'] = $status['service_running'] ?? false;
|
||||
$diagnosis['checks']['source_exists'] = $status['source_exists'] ?? false;
|
||||
$diagnosis['checks']['emulator_db_connected'] = $status['emulator_db_connected'] ?? false;
|
||||
$diagnosis['checks']['is_configured'] = $this->isConfigured();
|
||||
$diagnosis['checks']['update_available'] = $status['update_available'] ?? false;
|
||||
|
||||
$sqlDiagnosis = $this->sqlService->diagnose();
|
||||
$diagnosis['checks']['sql_table_exists'] = $sqlDiagnosis['table_exists'] ?? false;
|
||||
$diagnosis['checks']['sql_updates_applied'] = $sqlDiagnosis['applied_count'] ?? 0;
|
||||
$diagnosis['checks']['sql_pending'] = $sqlDiagnosis['pending_count'] ?? 0;
|
||||
|
||||
if (! ($status['jar_exists'] ?? false)) {
|
||||
$diagnosis['issues'][] = 'JAR bestand ontbreekt';
|
||||
$diagnosis['recommendations'][] = 'Voer emulator:update uit om de JAR te downloaden';
|
||||
}
|
||||
|
||||
if (! ($status['service_running'] ?? false)) {
|
||||
$diagnosis['issues'][] = 'Emulator service draait niet';
|
||||
$diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->settings->getOrDefault('emulator_service_name', 'emulator');
|
||||
}
|
||||
|
||||
if (! ($status['emulator_db_connected'] ?? false)) {
|
||||
$diagnosis['issues'][] = 'Emulator database niet bereikbaar';
|
||||
$diagnosis['recommendations'][] = 'Controleer de database credentials in de settings';
|
||||
}
|
||||
|
||||
if (! ($status['source_exists'] ?? false) && $this->sourceService->isSourceBuildAvailable()) {
|
||||
$diagnosis['issues'][] = 'Source code niet gevonden';
|
||||
$diagnosis['recommendations'][] = 'Voer emulator:update --rebuild uit om vanaf source te bouwen';
|
||||
}
|
||||
|
||||
if (! ($sqlDiagnosis['table_exists'] ?? false)) {
|
||||
$diagnosis['issues'][] = 'SQL update tabel ontbreekt';
|
||||
$diagnosis['recommendations'][] = 'Reparatie zal de tabel aanmaken';
|
||||
}
|
||||
|
||||
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
|
||||
$diagnosis['issues'][] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
|
||||
$diagnosis['recommendations'][] = 'Voer reparatie uit om SQL updates toe te passen';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$diagnosis['error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $diagnosis;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ class PurchaseService
|
||||
$this->rconService->setRank($user, $package->give_rank);
|
||||
$this->rconService->disconnectUser($user);
|
||||
} else {
|
||||
$user->update(['rank' => $package->give_rank]);
|
||||
$user->forceFill(['rank' => $package->give_rank])->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,14 @@ class RconService
|
||||
'ip' => setting('rcon_ip'),
|
||||
'port' => (int) setting('rcon_port'),
|
||||
];
|
||||
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->isConnected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
|
||||
if ($this->socket === false) {
|
||||
@@ -40,7 +42,7 @@ class RconService
|
||||
Log::error("RCON initialization failed: {$error}");
|
||||
$this->closeConnection();
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) {
|
||||
@@ -48,10 +50,17 @@ class RconService
|
||||
Log::error("RCON connection failed: {$error}");
|
||||
$this->closeConnection();
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->isConnected = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
private function closeConnection(): void
|
||||
@@ -66,6 +75,10 @@ class RconService
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
if (! $this->isConnected) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
return $this->isConnected;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class SettingsService
|
||||
|
||||
public function getLanguages(): Collection
|
||||
{
|
||||
return Cache::rememberForever(self::LANGUAGES_CACHE_KEY, function (): Collection {
|
||||
return Cache::remember(self::LANGUAGES_CACHE_KEY, 86400, function (): Collection {
|
||||
try {
|
||||
if (! Schema::hasTable('website_languages')) {
|
||||
return collect();
|
||||
@@ -75,7 +75,7 @@ class SettingsService
|
||||
return $this->fetchSettings();
|
||||
}
|
||||
|
||||
$this->cachedSettings = collect(Cache::rememberForever(self::CACHE_KEY, fn () => $this->fetchSettings()->toArray()));
|
||||
$this->cachedSettings = collect(Cache::remember(self::CACHE_KEY, 86400, fn () => $this->fetchSettings()->toArray()));
|
||||
|
||||
return $this->cachedSettings;
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateHistoryService
|
||||
{
|
||||
private const string TABLE = 'update_history';
|
||||
|
||||
public function ensureTableExists(): void
|
||||
{
|
||||
if (! Schema::hasTable(self::TABLE)) {
|
||||
Schema::create(self::TABLE, function ($table) {
|
||||
$table->id();
|
||||
$table->string('type'); // nitro, emulator, sql, config
|
||||
$table->string('action'); // update, build, deploy, fix
|
||||
$table->string('item')->nullable(); // filename, version, etc
|
||||
$table->string('status'); // success, failed, pending
|
||||
$table->text('message')->nullable();
|
||||
$table->string('user')->nullable();
|
||||
$table->string('ip')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->index(['type', 'created_at']);
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function log(string $type, string $action, ?string $item = null, string $status = 'success', ?string $message = null): void
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
DB::table(self::TABLE)->insert([
|
||||
'type' => $type,
|
||||
'action' => $action,
|
||||
'item' => $item,
|
||||
'status' => $status,
|
||||
'message' => $message,
|
||||
'user' => auth()->user()?->name ?? 'System',
|
||||
'ip' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRecent(int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getByType(string $type, int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->where('type', $type)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getByStatus(string $status, int $limit = 50): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
return DB::table(self::TABLE)
|
||||
->where('status', $status)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
$total = DB::table(self::TABLE)->count();
|
||||
$success = DB::table(self::TABLE)->where('status', 'success')->count();
|
||||
$failed = DB::table(self::TABLE)->where('status', 'failed')->count();
|
||||
|
||||
$lastUpdate = DB::table(self::TABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
$byType = DB::table(self::TABLE)
|
||||
->select('type', DB::raw('count(*) as count'))
|
||||
->groupBy('type')
|
||||
->pluck('count', 'type')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'success' => $success,
|
||||
'failed' => $failed,
|
||||
'success_rate' => $total > 0 ? round(($success / $total) * 100) : 100,
|
||||
'last_update' => $lastUpdate,
|
||||
'by_type' => $byType,
|
||||
];
|
||||
}
|
||||
|
||||
public function getHtml(): string
|
||||
{
|
||||
$this->ensureTableExists();
|
||||
|
||||
$updates = $this->getRecent(20);
|
||||
$stats = $this->getStats();
|
||||
|
||||
$html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">';
|
||||
|
||||
// Stats
|
||||
$html .= '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['total'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Totaal</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['success'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Geslaagd</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #f87171;">' . $stats['failed'] . '</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Mislukt</div></div>';
|
||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #60a5fa;">' . $stats['success_rate'] . '%</div>';
|
||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Succes</div></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// History list
|
||||
if ($updates === []) {
|
||||
$html .= '<div style="text-align: center; padding: 24px; color: #64748b;">';
|
||||
$html .= '<div style="font-size: 32px; margin-bottom: 8px;">📋</div>';
|
||||
$html .= 'Nog geen update geschiedenis</div>';
|
||||
} else {
|
||||
$html .= '<div style="max-height: 400px; overflow-y: auto;">';
|
||||
foreach ($updates as $update) {
|
||||
$icon = match ($update->status) {
|
||||
'success' => '✅',
|
||||
'failed' => '❌',
|
||||
'pending' => '⏳',
|
||||
default => '⚪'
|
||||
};
|
||||
|
||||
$typeColor = match ($update->type) {
|
||||
'nitro' => '#4ade80',
|
||||
'emulator' => '#60a5fa',
|
||||
'sql' => '#fbbf24',
|
||||
'config' => '#a78bfa',
|
||||
default => '#94a3b8'
|
||||
};
|
||||
|
||||
$time = Carbon::parse($update->created_at)->diffForHumans();
|
||||
|
||||
$html .= '<div style="display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 6px;">';
|
||||
$html .= '<div style="font-size: 16px;">' . $icon . '</div>';
|
||||
$html .= '<div style="flex: 1;">';
|
||||
$html .= '<div style="display: flex; align-items: center; gap: 8px;">';
|
||||
$html .= '<span style="background: ' . $typeColor . '20; color: ' . $typeColor . '; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 600; text-transform: uppercase;">' . e($update->type) . '</span>';
|
||||
$html .= '<span style="color: #e2e8f0; font-size: 13px;">' . e($update->action) . '</span>';
|
||||
if ($update->item) {
|
||||
$html .= '<span style="color: #94a3b8; font-size: 12px;">' . e($update->item) . '</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($update->message) {
|
||||
$html .= '<div style="color: #64748b; font-size: 11px; margin-top: 2px;">' . e($update->message) . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<div style="text-align: right;">';
|
||||
$html .= '<div style="color: #64748b; font-size: 10px;">' . e($update->user) . '</div>';
|
||||
$html .= '<div style="color: #475569; font-size: 9px;">' . e($time) . '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
return $html . '</div>';
|
||||
}
|
||||
}
|
||||
Executable → Regular
+24
-24
@@ -12,43 +12,43 @@
|
||||
"require": {
|
||||
"php": "^8.1|^8.2|^8.3|^8.4|^8.5",
|
||||
"ext-sockets": "*",
|
||||
"doctrine/dbal": "^4.0",
|
||||
"filament/filament": "^5.0",
|
||||
"doctrine/dbal": "^4.4",
|
||||
"filament/filament": "^5.6",
|
||||
"flowframe/laravel-trend": "0.4.99",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"inertiajs/inertia-laravel": "^3.1",
|
||||
"laravel/fortify": "^1.16",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/fortify": "^1.37",
|
||||
"laravel/framework": "^13.11",
|
||||
"laravel/octane": "^2.17",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/socialite": "^5.27",
|
||||
"laravel/tinker": "^3.0",
|
||||
"livewire/livewire": "^4.0",
|
||||
"opcodesio/log-viewer": "^3.0",
|
||||
"livewire/livewire": "^4.3",
|
||||
"opcodesio/log-viewer": "^3.24",
|
||||
"qirolab/laravel-themer": "dev-master",
|
||||
"ryangjchandler/laravel-cloudflare-turnstile": "^3.0",
|
||||
"spatie/laravel-activitylog": "^5.0",
|
||||
"spatie/laravel-sluggable": "^4.0",
|
||||
"spiral/roadrunner-cli": "^2.6.0",
|
||||
"spiral/roadrunner-http": "^4.0",
|
||||
"spiral/roadrunner-cli": "^2.7",
|
||||
"spiral/roadrunner-http": "^4.1",
|
||||
"srmklive/paypal": "3.0.99",
|
||||
"stevebauman/purify": "^6.0"
|
||||
"stevebauman/purify": "^6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"filament/upgrade": "^5.0",
|
||||
"fruitcake/laravel-debugbar": "^4.0",
|
||||
"itsgoingd/clockwork": "^5.0",
|
||||
"laravel/boost": "^2.0",
|
||||
"laravel/pint": "^v1.14",
|
||||
"laravel/sail": "^1.0",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"pestphp/pest": "^4.0",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"filament/upgrade": "^5.6",
|
||||
"fruitcake/laravel-debugbar": "^4.2",
|
||||
"itsgoingd/clockwork": "^5.3",
|
||||
"laravel/boost": "^2.4",
|
||||
"laravel/pint": "^v1.29",
|
||||
"laravel/sail": "^1.60",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.9",
|
||||
"pestphp/pest": "^4.7",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"rector/rector": "^2.0",
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"rector/rector": "^2.4",
|
||||
"spatie/laravel-ignition": "^2.12",
|
||||
"whichbrowser/parser": "^2.1"
|
||||
},
|
||||
"autoload": {
|
||||
|
||||
Generated
+501
-464
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Auto-push test
|
||||
use App\Providers\AppServiceProvider;
|
||||
use App\Providers\EventServiceProvider;
|
||||
use App\Providers\Filament\AdminFilamentPanelProvider;
|
||||
@@ -246,6 +245,4 @@ return [
|
||||
'aliases' => Facade::defaultAliases()->merge([
|
||||
// 'ExampleClass' => App\Example\ExampleClass::class,
|
||||
])->toArray(),
|
||||
|
||||
];
|
||||
// test
|
||||
|
||||
Regular → Executable
+2
-2
@@ -17,7 +17,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie', 'client/*', 'imaging/*'],
|
||||
|
||||
'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
|
||||
|
||||
@@ -25,7 +25,7 @@ return [
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
'allowed_headers' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_HEADERS', 'Content-Type,Authorization,X-Requested-With'))), fn ($v) => $v !== ''),
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
|
||||
@@ -147,14 +147,9 @@ return [
|
||||
|
||||
'features' => [
|
||||
Features::registration(),
|
||||
// Features::resetPasswords(),
|
||||
// Features::emailVerification(),
|
||||
// Features::updateProfileInformation(),
|
||||
// Features::updatePasswords(),
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
// 'window' => 0,
|
||||
]),
|
||||
],
|
||||
|
||||
|
||||
@@ -54,6 +54,22 @@ return [
|
||||
'external_override_texts' => env('EXTERNAL_OVERRIDE_TEXTS'),
|
||||
],
|
||||
|
||||
'defaults' => [
|
||||
'avatar_look' => env('DEFAULT_AVATAR_LOOK', 'hr-100-61.hd-180-1.ch-210-66'),
|
||||
'min_staff_rank' => env('MIN_STAFF_RANK', 7),
|
||||
'min_staff_rank_login' => env('MIN_STAFF_RANK_LOGIN', 3),
|
||||
],
|
||||
|
||||
'cdn' => [
|
||||
'fancybox_js' => env('FANCYBOX_JS_URL', 'https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.umd.js'),
|
||||
'fancybox_css' => env('FANCYBOX_CSS_URL', 'https://cdn.jsdelivr.net/npm/@fancyapps/ui@4/dist/fancybox.css'),
|
||||
'sweetalert2_js' => env('SWEETALERT2_JS_URL', '//cdn.jsdelivr.net/npm/sweetalert2@11'),
|
||||
'alpine_js' => env('ALPINE_JS_URL', 'https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js'),
|
||||
'fontsource_inter_css' => env('FONTSOURCE_INTER_CSS_URL', 'https://cdn.jsdelivr.net/npm/@fontsource/inter@4.x/400-700.css'),
|
||||
'fontawesome_css' => env('FONTAWESOME_CSS_URL', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.2.0/css/all.min.css'),
|
||||
'html2canvas_js' => env('HTML2CANVAS_JS_URL', 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.3/html2canvas.min.js'),
|
||||
],
|
||||
|
||||
'findretros' => [
|
||||
'enabled' => (bool) env('FINDRETROS_ENABLED', false),
|
||||
'name' => env('FINDRETROS_NAME', 'Example'),
|
||||
|
||||
+7
-7
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'sandbox' => [
|
||||
'client_id' => 'test_client_id',
|
||||
'client_secret' => 'test_client_secret',
|
||||
'app_id' => 'APP-80W284485P519543T',
|
||||
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
|
||||
'client_secret' => env('PAYPAL_SANDBOX_CLIENT_SECRET', ''),
|
||||
'app_id' => env('PAYPAL_SANDBOX_APP_ID', 'APP-80W284485P519543T'),
|
||||
'settings' => [
|
||||
'mode' => 'sandbox',
|
||||
'http.ConnectionTimeOut' => 30,
|
||||
@@ -20,9 +20,9 @@ return [
|
||||
],
|
||||
|
||||
'live' => [
|
||||
'client_id' => 'test_client_id',
|
||||
'client_secret' => 'test_client_secret',
|
||||
'app_id' => 'AYo1u2z7N3rQ2i2b3c4d5e6f7g8h9i0j',
|
||||
'client_id' => env('PAYPAL_LIVE_CLIENT_ID', ''),
|
||||
'client_secret' => env('PAYPAL_LIVE_CLIENT_SECRET', ''),
|
||||
'app_id' => env('PAYPAL_LIVE_APP_ID', ''),
|
||||
'settings' => [
|
||||
'mode' => 'live',
|
||||
'http.ConnectionTimeOut' => 30,
|
||||
@@ -36,7 +36,7 @@ return [
|
||||
],
|
||||
|
||||
'settings' => [
|
||||
'mode' => 'sandbox',
|
||||
'mode' => env('PAYPAL_MODE', 'sandbox'),
|
||||
'http.ConnectionTimeOut' => 30,
|
||||
'log.LogEnabled' => false,
|
||||
'log.FileName' => storage_path('logs/paypal.log'),
|
||||
|
||||
@@ -17,6 +17,7 @@ class UserFactory extends Factory
|
||||
'last_login' => time(),
|
||||
'look' => setting('start_look') ?: 'hr-100-61.hd-180-1.ch-210-66.lg-270-110.sh-305-62',
|
||||
'credits' => setting('start_credits') ?: 1000,
|
||||
'last_username_change' => 0,
|
||||
'ip_register' => '127.0.0.1',
|
||||
'ip_current' => '127.0.0.1',
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user