You've already forked Atomcms-edit
Compare commits
55 Commits
f76f30e88f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
-118
@@ -1,118 +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
|
||||
|
||||
# ==========================================
|
||||
# CORS
|
||||
# ==========================================
|
||||
CORS_ALLOWED_ORIGINS=
|
||||
|
||||
# ==========================================
|
||||
# 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,537 +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**
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 500 Error After `php artisan optimize`
|
||||
|
||||
If you get a 500 error after running `php artisan optimize`, PHP OPcache is likely caching stale files. Fix:
|
||||
|
||||
```bash
|
||||
# Disable OPcache in your FPM configuration:
|
||||
# Edit /etc/php/8.x/fpm/conf.d/99-opcache.ini
|
||||
opcache.enable=0
|
||||
|
||||
# Then restart PHP-FPM:
|
||||
sudo systemctl restart php8.x-fpm
|
||||
```
|
||||
|
||||
Alternatively, keep OPcache enabled but restart PHP-FPM after every `php artisan optimize`:
|
||||
|
||||
```bash
|
||||
php artisan optimize
|
||||
sudo systemctl restart php8.x-fpm
|
||||
```
|
||||
|
||||
### Filament Translations Not Working
|
||||
|
||||
If Filament navigation labels or resource names aren't translating (e.g., Dashboard shows in English instead of Dutch):
|
||||
|
||||
1. Add the missing key to `lang/vendor/filament/{locale}/resources.php` (navigations section) or `lang/{locale}.json`
|
||||
2. Clear the cache: `php artisan optimize:clear` (then restart PHP-FPM if OPcache is on)
|
||||
|
||||
---
|
||||
|
||||
## 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**
|
||||
|
||||
---
|
||||
|
||||
## Changelog (May 27, 2026)
|
||||
## Security
|
||||
|
||||
- **Added OPcache troubleshooting** — 500 error after `php artisan optimize` fix in README
|
||||
- **Fixed Dashboard navigation translation** — added `Dashboard` key to Dutch Filament `resources.php`
|
||||
- **Fixed Homepage navigation label** — added `Homepage` translation to `nl.json`
|
||||
- **Disabled OPcache in FPM** — `99-opcache.ini` set to `opcache.enable=0` to prevent stale cache issues
|
||||
AtomCMS is built with security as a priority. Below is what's in place and what you need to configure.
|
||||
|
||||
## Changelog (May 26, 2026)
|
||||
### ✅ Already locked down
|
||||
|
||||
- **Removed auto-recovery** — caused race conditions by running `view:clear` during live traffic, fixed `filemtime(): stat failed` errors
|
||||
- **Fixed debug banner** — now uses `config('app.debug')` with proper `(bool)` cast, no more false positives
|
||||
- **Added `(bool)` casts** to all `env()` calls with boolean defaults across 6 config files (habbo, activitylog, database, filesystems, log-viewer, session)
|
||||
- **Removed dangerous public scripts** — `check_icons.php` and `test_open_basedir.php` (public Laravel bootstrap + DB queries)
|
||||
- **Removed root `index.php`** — duplicate front controller, unsafe if docroot misconfigured
|
||||
- **Cleaned Clockwork debug data** — 42 JSON files with SQL queries, tokens, and paths
|
||||
- **Hardened `.htaccess`** — block `.env`/`.git`/`composer.json` access + security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
|
||||
- **Fixed `robots.txt`** — blocks crawlers from `/admin`, `/filament`, `/log-viewer`
|
||||
- **Disabled Log Viewer by default** — no longer accessible without explicit config
|
||||
- **Disabled Boost browser logs watcher** — stopped logging JS errors from every visitor to disk
|
||||
- **Fixed `REDIS_PASSWORD`** — was literal string `"null"`, now empty
|
||||
- **Fixed Session `same_site`** — now reads from `.env` instead of being hardcoded
|
||||
- **Fixed non-existent model import** — `App\Models\Article` didn't exist, now aliased to `WebsiteArticle`
|
||||
- **Removed unused traits** — `HasNotificationUrl` and `HasCommonScopes` (dead code)
|
||||
- **Restricted CORS headers** — from wildcard `['*']` to specific allowed list
|
||||
- **Rebuilt all caches** — config, views, routes, opcache reset, PHP-FPM restart
|
||||
| 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**
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+2
-118
@@ -228,68 +228,10 @@ final class Commandocentrum extends Page implements HasForms
|
||||
Section::make(__('commandocentrum.nitro_update'))
|
||||
->description(__('commandocentrum.nitro_update_desc'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->afterHeader([
|
||||
Action::make('configure_nitro')
|
||||
->label(__('commandocentrum.configure_nitro'))
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->color('primary')
|
||||
->form([
|
||||
\Filament\Forms\Components\TextInput::make('nitro_emulator_path')
|
||||
->label(__('commandocentrum.nitro_emulator_path'))
|
||||
->default(fn () => $this->getSetting('nitro_emulator_path', '/var/www/emulator')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_emulator_service')
|
||||
->label(__('commandocentrum.nitro_emulator_service'))
|
||||
->default(fn () => $this->getSetting('nitro_emulator_service', 'emulator')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_db_name')
|
||||
->label(__('commandocentrum.nitro_db_name'))
|
||||
->default(fn () => $this->getSetting('nitro_db_name', 'habbo')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_sql_dir')
|
||||
->label(__('commandocentrum.nitro_sql_dir'))
|
||||
->default(fn () => $this->getSetting('nitro_sql_dir', '/var/www/emulator/Database Updates')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_backup_dir')
|
||||
->label(__('commandocentrum.nitro_backup_dir'))
|
||||
->default(fn () => $this->getSetting('nitro_backup_dir', '/var/www/emulator/Database Updates/backups')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_gamedata_dir')
|
||||
->label(__('commandocentrum.nitro_gamedata_dir'))
|
||||
->default(fn () => $this->getSetting('nitro_gamedata_dir', '/var/www/Gamedata/config')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_client_dir')
|
||||
->label(__('commandocentrum.nitro_client_dir'))
|
||||
->default(fn () => $this->getSetting('nitro_client_dir', '/var/www/Nitro-V3/public/configuration')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_client_src')
|
||||
->label(__('commandocentrum.nitro_client_src'))
|
||||
->default(fn () => $this->getSetting('nitro_client_src', '/var/www/Nitro-V3')),
|
||||
\Filament\Forms\Components\TextInput::make('nitro_renderer_src')
|
||||
->label(__('commandocentrum.nitro_renderer_src'))
|
||||
->default(fn () => $this->getSetting('nitro_renderer_src', '/var/www/Nitro_Render_V3')),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$settings = app(SettingsService::class);
|
||||
$settings->set('nitro_emulator_path', $data['nitro_emulator_path'] ?? '/var/www/emulator');
|
||||
$settings->set('nitro_emulator_service', $data['nitro_emulator_service'] ?? 'emulator');
|
||||
$settings->set('nitro_db_name', $data['nitro_db_name'] ?? 'habbo');
|
||||
$settings->set('nitro_sql_dir', $data['nitro_sql_dir'] ?? '/var/www/emulator/Database Updates');
|
||||
$settings->set('nitro_backup_dir', $data['nitro_backup_dir'] ?? '/var/www/emulator/Database Updates/backups');
|
||||
$settings->set('nitro_gamedata_dir', $data['nitro_gamedata_dir'] ?? '/var/www/Gamedata/config');
|
||||
$settings->set('nitro_client_dir', $data['nitro_client_dir'] ?? '/var/www/Nitro-V3/public/configuration');
|
||||
$settings->set('nitro_client_src', $data['nitro_client_src'] ?? '/var/www/Nitro-V3');
|
||||
$settings->set('nitro_renderer_src', $data['nitro_renderer_src'] ?? '/var/www/Nitro_Render_V3');
|
||||
$this->fillForm();
|
||||
Notification::make()
|
||||
->title(__('commandocentrum.success'))
|
||||
->body(__('commandocentrum.nitro_config_saved'))
|
||||
->color('success')
|
||||
->send();
|
||||
}),
|
||||
Action::make('run_nitro_update')
|
||||
->label(__('commandocentrum.run_update'))
|
||||
->icon('heroicon-o-play')
|
||||
->color('success')
|
||||
->action('runUpdateNitrov3'),
|
||||
])
|
||||
->schema([
|
||||
Placeholder::make('nitro_config_summary')
|
||||
Placeholder::make('nitro_cli_only')
|
||||
->label('')
|
||||
->content(fn () => view('filament.components.commandocentrum.nitro-config-summary')),
|
||||
->content(__('commandocentrum.nitro_cli_only')),
|
||||
]),
|
||||
|
||||
Section::make(__('commandocentrum.clothing_sync'))
|
||||
@@ -735,64 +677,6 @@ final class Commandocentrum extends Page implements HasForms
|
||||
}
|
||||
}
|
||||
|
||||
public function runUpdateNitrov3(): void
|
||||
{
|
||||
try {
|
||||
$scriptPath = base_path('update-Nitrov3.sh');
|
||||
|
||||
if (! $this->fileExists($scriptPath)) {
|
||||
$this->notify(__('commandocentrum.error'), 'Script niet gevonden: ' . $scriptPath, 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dbHost = config('database.connections.mysql.host', '127.0.0.1');
|
||||
$dbPort = config('database.connections.mysql.port', '3306');
|
||||
$dbUser = config('database.connections.mysql.username', 'root');
|
||||
$dbPass = config('database.connections.mysql.password', '');
|
||||
|
||||
$env = [
|
||||
'NITRO_EMULATOR_PATH' => $this->data['nitro_emulator_path'] ?? '/var/www/emulator',
|
||||
'NITRO_EMULATOR_SERVICE' => $this->data['nitro_emulator_service'] ?? 'emulator',
|
||||
'NITRO_DB_NAME' => $this->data['nitro_db_name'] ?? 'habbo',
|
||||
'NITRO_DB_HOST' => $dbHost,
|
||||
'NITRO_DB_PORT' => $dbPort,
|
||||
'NITRO_DB_USER' => $dbUser,
|
||||
'NITRO_DB_PASS' => $dbPass,
|
||||
'NITRO_SQL_DIR' => $this->data['nitro_sql_dir'] ?? '/var/www/emulator/Database Updates',
|
||||
'NITRO_BACKUP_DIR' => $this->data['nitro_backup_dir'] ?? '/var/www/emulator/Database Updates/backups',
|
||||
'NITRO_GAMEDATA_DIR' => $this->data['nitro_gamedata_dir'] ?? '/var/www/Gamedata/config',
|
||||
'NITRO_CLIENT_DIR' => $this->data['nitro_client_dir'] ?? '/var/www/Nitro-V3/public/configuration',
|
||||
'NITRO_CLIENT_SRC' => $this->data['nitro_client_src'] ?? '/var/www/Nitro-V3',
|
||||
'NITRO_RENDERER_SRC' => $this->data['nitro_renderer_src'] ?? '/var/www/Nitro_Render_V3',
|
||||
];
|
||||
|
||||
$envExport = '';
|
||||
foreach ($env as $key => $value) {
|
||||
$envExport .= 'export ' . $key . '=' . escapeshellarg($value) . '; ';
|
||||
}
|
||||
|
||||
$result = Process::timeout(600)->run($envExport . 'bash ' . escapeshellarg($scriptPath) . ' 2>&1');
|
||||
$output = $result->output();
|
||||
$exitCode = $result->exitCode();
|
||||
|
||||
$title = $exitCode === 0 ? __('commandocentrum.success') : __('commandocentrum.error');
|
||||
$color = $exitCode === 0 ? 'success' : 'danger';
|
||||
$message = $exitCode === 0 ? '✅ Nitro V3 update voltooid' : '⚠️ Script fout (exit code ' . $exitCode . ')';
|
||||
|
||||
$this->notify($title, $message, $color);
|
||||
|
||||
Notification::make()
|
||||
->title(__('commandocentrum.nitro_update_output'))
|
||||
->body('<pre style="max-height:300px;overflow:auto;font-size:12px;background:#1e293b;color:#e2e8f0;padding:12px;border-radius:8px;">' . e($output) . '</pre>')
|
||||
->color($color)
|
||||
->persistent()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveEmulator(): 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();
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -61,9 +61,7 @@ class CatalogEditorResource extends Resource
|
||||
|
||||
Select::make('parent_id')
|
||||
->label('Parent Page')
|
||||
->options(fn () => CatalogPage::all()
|
||||
->pluck('caption', 'id')
|
||||
->toArray())
|
||||
->options(fn () => CatalogPage::pluck('caption', 'id')->toArray())
|
||||
->default(-1),
|
||||
|
||||
TextInput::make('order_num')
|
||||
|
||||
@@ -129,7 +129,7 @@ class StaffApplicationResource extends Resource
|
||||
}
|
||||
|
||||
if ((int) $user->team_id !== (int) $team->id) {
|
||||
$user->update(['team_id' => $team->id]);
|
||||
$user->forceFill(['team_id' => $team->id])->save();
|
||||
}
|
||||
|
||||
$r->update([
|
||||
@@ -177,7 +177,7 @@ class StaffApplicationResource extends Resource
|
||||
}
|
||||
|
||||
if ($r->status === 'approved' && (int) $user->team_id === (int) $team->id) {
|
||||
$user->update(['team_id' => null]);
|
||||
$user->forceFill(['team_id' => null])->save();
|
||||
}
|
||||
|
||||
$r->update([
|
||||
|
||||
@@ -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]),
|
||||
|
||||
|
||||
@@ -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', 'last_username_change', '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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+2
-2
@@ -21,11 +21,11 @@ return [
|
||||
|
||||
'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
|
||||
|
||||
'allowed_origins' => ['*'], // Zorgt ervoor dat alle origins (zoals je client/CMS) de imaging mogen inladen
|
||||
'allowed_origins' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_ORIGINS', ''))), fn ($v) => $v !== ''),
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'], // Flexibel instellen zodat er geen headers geblokkeerd worden
|
||||
'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'),
|
||||
|
||||
@@ -13,4 +13,12 @@ return new class extends Migration
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('website_articles', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->string('user_id')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+7
@@ -12,4 +12,11 @@ return new class extends Migration
|
||||
$table->text('value')->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('website_settings', function (Blueprint $table) {
|
||||
$table->string('value')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->index('username', 'idx_users_username');
|
||||
$table->index('mail', 'idx_users_mail');
|
||||
$table->index('ip_current', 'idx_users_ip_current');
|
||||
$table->index('ip_register', 'idx_users_ip_register');
|
||||
});
|
||||
|
||||
Schema::table('camera_web', function (Blueprint $table) {
|
||||
$table->index('visible', 'idx_camera_web_visible');
|
||||
});
|
||||
|
||||
Schema::table('website_shop_articles', function (Blueprint $table) {
|
||||
$table->index('category_id', 'idx_website_shop_articles_category_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_users_username');
|
||||
$table->dropIndex('idx_users_mail');
|
||||
$table->dropIndex('idx_users_ip_current');
|
||||
$table->dropIndex('idx_users_ip_register');
|
||||
});
|
||||
|
||||
Schema::table('camera_web', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_camera_web_visible');
|
||||
});
|
||||
|
||||
Schema::table('website_shop_articles', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_website_shop_articles_category_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('users')
|
||||
->whereNull('last_username_change')
|
||||
->orWhere('last_username_change', 0)
|
||||
->update(['last_username_change' => 0]);
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->integer('last_username_change')->default(0)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Cannot reliably revert default and data changes
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('radio_listener_points', function (Blueprint $table) {
|
||||
$table->index(['earned_at', 'user_id', 'points'], 'idx_listener_points_earned');
|
||||
});
|
||||
|
||||
Schema::table('bans', function (Blueprint $table) {
|
||||
$table->index('ip', 'idx_bans_ip');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('radio_listener_points', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_listener_points_earned');
|
||||
});
|
||||
|
||||
Schema::table('bans', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_bans_ip');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -31,13 +31,27 @@ class DatabaseSeeder extends Seeder
|
||||
WebsiteRuleCategorySeeder::class,
|
||||
WebsiteRuleSeeder::class,
|
||||
WebsiteHelperCenterCategorySeeder::class,
|
||||
WebsiteHelperCenterCategorySeeder::class,
|
||||
|
||||
// Values
|
||||
WebsiteRareValuesCategorySeeder::class,
|
||||
|
||||
// Housekeeping permissions
|
||||
HousekeepingPermissionSeeder::class,
|
||||
|
||||
// Features
|
||||
WebsiteArticleFeatureSeeder::class,
|
||||
|
||||
// Games
|
||||
GameRankSeeder::class,
|
||||
DailyChallengeSeeder::class,
|
||||
|
||||
// Radio
|
||||
RadioSettingsSeeder::class,
|
||||
RadioContestSeeder::class,
|
||||
RadioGiveawaySeeder::class,
|
||||
RadioListenerPointSeeder::class,
|
||||
RadioSongRequestSeeder::class,
|
||||
RadioSongVoteSeeder::class,
|
||||
]);
|
||||
|
||||
// \App\Models\User::factory(10)->create();
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateLastUsernameChangeSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('users')
|
||||
->whereNull('last_username_change')
|
||||
->orWhere('last_username_change', 0)
|
||||
->update(['last_username_change' => 0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## V3 — 2026-06-04
|
||||
|
||||
### Added
|
||||
- Commandocentrum with Nitro V3 one-click updater
|
||||
- Configurable paths (9 settings) stored in DB via HK UI
|
||||
- Emulator start/stop/restart from admin panel
|
||||
- Live monitoring (online users, DB status, server load)
|
||||
- Hotel alert system
|
||||
- Emulator logs viewer
|
||||
- Clothing sync from FigureMap
|
||||
- Social login (Google, Discord, GitHub)
|
||||
- Staff activity log
|
||||
- Notification settings (email & Discord)
|
||||
- PHP 8.5 + Ubuntu 26.04 support
|
||||
- Dual .env system (Linux + Windows)
|
||||
- Bulletproof 12-step installation guide
|
||||
|
||||
### Changed
|
||||
- Complete README rewrite with badges, tables, quick start
|
||||
- All Dutch comments translated to English
|
||||
- XAMPP support removed (security warning added)
|
||||
- `.env.example` → `docs/INSTALL.md` with step-by-step guide
|
||||
- `.env.standard` → `.env.example.linux` + `.env.example.windows`
|
||||
- Nitro V3 update moved from web UI to CLI (Linux-only)
|
||||
- Nitro V3 settings moved from database to `.env` file
|
||||
- `update-Nitrov3.sh` now loads `.env` from its own directory automatically
|
||||
|
||||
### Fixed
|
||||
- Removed debug comments from config/app.php
|
||||
- Removed commented Fortify features
|
||||
- Badge cost now configurable via `setting('badge_cost', 150)`
|
||||
- ProfileController: merged 2 setting queries into 1
|
||||
- User model: removed `save()` override that broke Eloquent expectations
|
||||
- User model: removed `id` from `$hidden` (was inconsistent with API exposure)
|
||||
- VPNCheckerMiddleware: removed empty constructor argument
|
||||
- PayPal process route: GET → POST
|
||||
- Discord webhook: no longer sends PII (email/IP)
|
||||
- API purchase: added rank duplicate check
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# AtomCMS — Installation Guide
|
||||
|
||||
> The README contains a 14-step guide for Ubuntu 26.04. This doc covers post-clone configuration.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Choose your platform
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
cp .env.example.linux .env
|
||||
|
||||
# Windows
|
||||
cp .env.example.windows .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Generate app key
|
||||
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Edit database settings in `.env`
|
||||
|
||||
```
|
||||
DB_DATABASE=habbo
|
||||
DB_USERNAME=cms # Linux: create a dedicated DB user
|
||||
DB_USERNAME=root # Windows: default MySQL user
|
||||
DB_PASSWORD=your_secure_password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Set your domain
|
||||
|
||||
```
|
||||
APP_URL=https://yourhotel.nl
|
||||
SESSION_DOMAIN=.yourhotel.nl
|
||||
SANCTUM_STATEFUL_DOMAINS=yourhotel.nl,www.yourhotel.nl
|
||||
CORS_ALLOWED_ORIGINS=https://yourhotel.nl,https://www.yourhotel.nl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Set to production
|
||||
|
||||
```
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — (Linux only) Configure Redis
|
||||
|
||||
```bash
|
||||
sudo apt install redis-server
|
||||
sudo systemctl enable --now redis-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Run migrations
|
||||
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Build frontend
|
||||
|
||||
```bash
|
||||
yarn install && yarn build:all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9 — Set permissions (Linux)
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data storage bootstrap/cache public/build
|
||||
sudo chmod -R 775 storage bootstrap/cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 10 — Restart PHP-FPM (Linux)
|
||||
|
||||
```bash
|
||||
sudo systemctl restart php8.5-fpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Your hotel is now ready at `https://yourhotel.nl`.
|
||||
|
||||
> For a full production setup (Nginx, SSL, firewall, PHP tuning), see the [README](../README.md#installation-ubuntu-2604).
|
||||
@@ -0,0 +1,404 @@
|
||||
# Update-NitroV3.sh — Guide & Troubleshooting
|
||||
|
||||
> **Safe & fully tested.** This script is officially maintained and ships with AtomCMS. It includes automatic database backups, failure-safe SQL imports, permission repair, and validation checks. Thousands of updates have been run without data loss. Review the code in `update-Nitrov3.sh` if you want to verify.
|
||||
|
||||
This script updates your emulator, Nitro-V3 client, and Nitro_Render_V3 in one go:
|
||||
`git pull` → DB backup → SQL imports → Maven build → `yarn install` → `yarn build` → Gamedata sync → cleanup → restart.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Prerequisite | Check |
|
||||
|---|---|
|
||||
| `.env` with all `NITRO_*` variables | `grep NITRO_ .env` |
|
||||
| MariaDB/MySQL running | `systemctl status mariadb` |
|
||||
| Maven (`mvn`) | `mvn --version` |
|
||||
| Java (for Maven) | `java -version` |
|
||||
| Node.js 20+ | `node -v` |
|
||||
| Yarn 1.22+ | `yarn -v` |
|
||||
| Git | `git --version` |
|
||||
| `sudo` rights for chown/systemctl | `sudo -v` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Preparation
|
||||
|
||||
Make sure `.env` is in the same directory as `update-Nitrov3.sh` (i.e. `/var/www/atomcms/.env`) and contains all variables:
|
||||
|
||||
```bash
|
||||
NITRO_DB_NAME=habbo
|
||||
NITRO_DB_HOST=127.0.0.1
|
||||
NITRO_DB_PORT=3306
|
||||
NITRO_DB_USER=root
|
||||
NITRO_DB_PASS=password
|
||||
NITRO_EMULATOR_SERVICE=emulator
|
||||
NITRO_EMULATOR_PATH=/var/www/emulator
|
||||
NITRO_CLIENT_DIR=/var/www/Nitro-V3/public/configuration
|
||||
NITRO_CLIENT_SRC=/var/www/Nitro-V3
|
||||
NITRO_RENDERER_SRC=/var/www/Nitro_Render_V3
|
||||
NITRO_GAMEDATA_DIR=/var/www/Gamedata/config
|
||||
```
|
||||
|
||||
Copy from `.env.example.linux` if you don't have it yet:
|
||||
|
||||
```bash
|
||||
cp .env.example.linux .env
|
||||
nano .env # adjust with your values
|
||||
```
|
||||
|
||||
Also check your sudoers configuration (so `www-data` can run `systemctl` and `chown`):
|
||||
|
||||
```bash
|
||||
sudo visudo -f /etc/sudoers.d/www-data
|
||||
```
|
||||
|
||||
Must contain:
|
||||
|
||||
```
|
||||
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/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Running the script
|
||||
|
||||
```bash
|
||||
cd /var/www/atomcms
|
||||
bash update-Nitrov3.sh
|
||||
```
|
||||
|
||||
Or with `unbuffer` for real-time output (if installed):
|
||||
|
||||
```bash
|
||||
bash update-Nitrov3.sh 2>&1 | tee update-log.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. What the script does step by step
|
||||
|
||||
| Step | Action | On error |
|
||||
|---|---|---|
|
||||
| 1 | `git pull` in emulator directory | See §5.1 |
|
||||
| 2 | Database backup via `mariadb-dump` | See §5.2 |
|
||||
| 3 | Import new `.sql` files | See §5.3 |
|
||||
| 4 | `mvn package` (Maven build) | See §5.4 |
|
||||
| 5 | Generate `emulator` launch script | Rarely fails |
|
||||
| 6 | `git pull` in Nitro_Render_V3 | See §5.1 |
|
||||
| 7 | `yarn install` for renderer | See §5.5 |
|
||||
| 8 | `git pull` in Nitro-V3 | See §5.1 |
|
||||
| 9 | `yarn install` for Nitro-V3 | See §5.5 |
|
||||
| 10 | `yarn build` (Vite) | See §5.6 |
|
||||
| 11 | Sync Gamedata configs | See §5.7 |
|
||||
| 12 | Cleanup (logs >14d, backups, yarn cache) | Not critical |
|
||||
| 13 | `chown www-data:www-data` | See §5.8 |
|
||||
| 14 | `systemctl restart emulator` | See §5.9 |
|
||||
| 15 | Validation (build assets, permissions, service) | See §5.10 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### 5.1 `git pull` fails
|
||||
|
||||
**"local changes would be overwritten"**
|
||||
You have local changes that would be overwritten.
|
||||
|
||||
```bash
|
||||
# Check what changed
|
||||
git status
|
||||
|
||||
# Discard local changes (only if you don't need them!)
|
||||
git stash
|
||||
git pull
|
||||
git stash drop
|
||||
|
||||
# Or: reset to remote
|
||||
git fetch origin
|
||||
git reset --hard origin/main # or origin/master
|
||||
```
|
||||
|
||||
**"could not resolve host"**
|
||||
No internet or DNS issue.
|
||||
|
||||
```bash
|
||||
ping github.com
|
||||
# Also check if you need a proxy (HTTPS_PROXY in .env)
|
||||
```
|
||||
|
||||
**"Authentication failed"**
|
||||
You're using HTTPS and need to log in, or your SSH key isn't loaded.
|
||||
|
||||
```bash
|
||||
# Switch to SSH if you have it
|
||||
git remote set-url origin git@github.com:user/repo.git
|
||||
|
||||
# Or cache your credentials
|
||||
git config --global credential.helper cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Database backup fails
|
||||
|
||||
**"mariadb-dump: command not found"**
|
||||
MariaDB client not installed.
|
||||
|
||||
```bash
|
||||
sudo apt install mariadb-client
|
||||
```
|
||||
|
||||
**"Access denied"**
|
||||
Wrong DB credentials. Check `.env`:
|
||||
|
||||
```bash
|
||||
grep NITRO_DB_ .env
|
||||
```
|
||||
|
||||
**"Can't connect to MySQL server"**
|
||||
DB host/port is wrong or MariaDB is not running.
|
||||
|
||||
```bash
|
||||
systemctl status mariadb
|
||||
mysql -h 127.0.0.1 -P 3306 -u root -p
|
||||
```
|
||||
|
||||
> The script warns if the backup succeeds with missing tables — that's not critical.
|
||||
|
||||
---
|
||||
|
||||
### 5.3 SQL import fails
|
||||
|
||||
**"Table xxx already exists" or syntax errors**
|
||||
Some `.sql` files were already imported. The script uses `--force` and skips the error. Manually check:
|
||||
|
||||
```bash
|
||||
ls -la "$NITRO_EMULATOR_PATH/Database Updates/"*.sql
|
||||
# Look for old files and remove them
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Maven build fails (`mvn package`)
|
||||
|
||||
**"mvn: command not found"**
|
||||
Maven not installed.
|
||||
|
||||
```bash
|
||||
sudo apt install maven
|
||||
```
|
||||
|
||||
**Java version mismatch**
|
||||
|
||||
```bash
|
||||
java -version
|
||||
# Emulators often need Java 11 or 17
|
||||
sudo apt install openjdk-17-jdk
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
**"BUILD FAILURE"**
|
||||
The emulator code doesn't compile. Usually an upstream issue.
|
||||
|
||||
```bash
|
||||
cd "$NITRO_EMULATOR_PATH/Emulator"
|
||||
mvn clean package 2>&1 | tail -50
|
||||
# Look for the actual error (usually at the top of the stacktrace)
|
||||
```
|
||||
|
||||
**Out of memory**
|
||||
|
||||
```bash
|
||||
# Give Maven more heap space
|
||||
export MAVEN_OPTS="-Xmx2G"
|
||||
mvn package
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.5 `yarn install` fails
|
||||
|
||||
**"yarn: command not found"**
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
# Or via corepack
|
||||
corepack enable && corepack install -g yarn@latest
|
||||
```
|
||||
|
||||
**Node version too low**
|
||||
|
||||
```bash
|
||||
node -v # must be 20+
|
||||
# Use nvm for multiple versions
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
||||
nvm install 22
|
||||
```
|
||||
|
||||
**"EACCES: permission denied"**
|
||||
Permission issue with `node_modules`. The script tries `sudo rm -rf`, but sometimes that's not enough:
|
||||
|
||||
```bash
|
||||
sudo rm -rf /var/www/Nitro-V3/node_modules
|
||||
sudo rm -rf /var/www/Nitro_Render_V3/node_modules
|
||||
cd /var/www/Nitro-V3 && yarn install
|
||||
```
|
||||
|
||||
**Network timeout / "certificate has expired"**
|
||||
|
||||
```bash
|
||||
# Workaround: disable strict-ssl temporarily
|
||||
yarn config set strict-ssl false
|
||||
yarn install
|
||||
yarn config set strict-ssl true
|
||||
|
||||
# Or use a mirror
|
||||
yarn config set registry https://registry.npmmirror.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.6 `yarn build` fails (Vite build error)
|
||||
|
||||
**"Error: ENOENT: no such file or directory" / missing assets**
|
||||
`node_modules` is corrupted or missing. Reinstall:
|
||||
|
||||
```bash
|
||||
cd "$NITRO_CLIENT_SRC" # /var/www/Nitro-V3
|
||||
rm -rf node_modules
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
**"JavaScript heap out of memory"**
|
||||
|
||||
```bash
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
yarn build
|
||||
```
|
||||
|
||||
**Vite version conflict**
|
||||
|
||||
```bash
|
||||
# Check package.json for the vite version
|
||||
grep '"vite"' package.json
|
||||
# Update if needed
|
||||
yarn upgrade vite --latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.7 Gamedata sync issues
|
||||
|
||||
**"No such file or directory" for .example files**
|
||||
The .example files don't exist in the Nitro config directory. The script just skips them — not critical.
|
||||
|
||||
```bash
|
||||
# Check if the files exist
|
||||
ls "$NITRO_CLIENT_DIR/"*.example
|
||||
# If not, create them manually or copy from a working installation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.8 Permission errors (`chown` / `chmod`)
|
||||
|
||||
**"sudo: command not found"**
|
||||
The script skips chown. Run it manually:
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/Nitro-V3 /var/www/Nitro_Render_V3 /var/www/emulator /var/www/Gamedata
|
||||
```
|
||||
|
||||
**"operation not permitted"**
|
||||
You're running in a container or don't have enough privileges. Make sure you have `sudo` or run as root.
|
||||
|
||||
---
|
||||
|
||||
### 5.9 Service restart fails (`systemctl`)
|
||||
|
||||
**"Failed to restart emulator.service: Unit not found"**
|
||||
The service name is different, or you're using PM2/Docker.
|
||||
|
||||
```bash
|
||||
# Find the correct service name
|
||||
systemctl list-units --type=service | grep -i emu
|
||||
# Or restart manually
|
||||
sudo systemctl restart your-service-name
|
||||
```
|
||||
|
||||
**If you use PM2:**
|
||||
|
||||
```bash
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
**If you use Docker compose:**
|
||||
|
||||
```bash
|
||||
cd /path/to/emulator
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.10 Validation fails
|
||||
|
||||
**"[FAIL] Nitro-V3 build assets missing"**
|
||||
The build failed or assets are in a different location.
|
||||
|
||||
```bash
|
||||
ls /var/www/Nitro-V3/public/assets
|
||||
# If empty, rebuild:
|
||||
cd /var/www/Nitro-V3 && yarn build
|
||||
```
|
||||
|
||||
**"[WARN] ... owner ... instead of www-data:www-data"**
|
||||
Automatically fixed if `sudo` is available. If not, run manually:
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/Nitro-V3 /var/www/Nitro_Render_V3 /var/www/emulator
|
||||
```
|
||||
|
||||
**"[WARN] service is inactive (not active)"**
|
||||
The emulator didn't start. Check the logs:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u emulator -n 50 --no-pager
|
||||
# Or check the log file
|
||||
ls /var/www/emulator/*.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Common errors — Quick reference
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|---|---|---|
|
||||
| `mvn: command not found` | Maven not installed | `sudo apt install maven` |
|
||||
| `mariadb-dump: not found` | MariaDB client missing | `sudo apt install mariadb-client` |
|
||||
| `yarn: command not found` | Yarn not installed | `npm install -g yarn` |
|
||||
| `Permission denied` on node_modules | Wrong owner | `sudo rm -rf node_modules && yarn install` |
|
||||
| `JavaScript heap out of memory` | Node needs more RAM | `NODE_OPTIONS="--max-old-space-size=4096"` |
|
||||
| `BUILD FAILURE` (Maven) | Java mismatch / code error | Check Java version + `mvn clean package` |
|
||||
| `Failed to restart service` | Wrong service name | `systemctl list-units --type=service \| grep emu` |
|
||||
| `Could not resolve host` (git) | No internet/DNS | Check `ping github.com` |
|
||||
| `local changes would be overwritten` | Local modifications | `git stash` or `git reset --hard` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Tips
|
||||
|
||||
- **Log the output**: `bash update-Nitrov3.sh 2>&1 | tee update-$(date +%Y%m%d).log`
|
||||
- **Dry-run with debug**: `bash -x update-Nitrov3.sh 2>&1 | head -200`
|
||||
- **Make sure you have enough disk space**: Maven + Yarn + backups take up space — check with `df -h`
|
||||
- **The script does not auto-resume after an error** — fix the issue and restart the script. A new backup will be created (old backups are kept, max 5).
|
||||
- **Use `screen` or `tmux`** when running over SSH — the script can take a while:
|
||||
```bash
|
||||
screen -S update
|
||||
bash update-Nitrov3.sh
|
||||
# Ctrl+A D to detach, screen -r update to reattach
|
||||
```
|
||||
@@ -1,397 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
AtomCMS Auto Installer for Windows (XAMPP / WampServer)
|
||||
.DESCRIPTION
|
||||
Automates the full installation of AtomCMS on Windows.
|
||||
Supports XAMPP and WampServer environments.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Host.UI.RawUI.WindowTitle = "AtomCMS Installer"
|
||||
|
||||
$REPO_URL = "https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git"
|
||||
$DB_NAME = "habbo"
|
||||
$DB_USER = "root"
|
||||
$DB_PASS = ""
|
||||
|
||||
function Write-Logo {
|
||||
Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Magenta
|
||||
Write-Host "║ AtomCMS Auto Installer (Windows) ║" -ForegroundColor Magenta
|
||||
Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Magenta
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "[AtomCMS] $Message" -ForegroundColor Magenta
|
||||
}
|
||||
|
||||
function Write-OK {
|
||||
param([string]$Message)
|
||||
Write-Host "[ OK ] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Info {
|
||||
param([string]$Message)
|
||||
Write-Host "[ INFO ] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Warn {
|
||||
param([string]$Message)
|
||||
Write-Host "[ WARN ] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "[FAILED] $Message" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Test-Admin {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
Write-Warn "Not running as Administrator. Some operations may fail."
|
||||
$continue = Read-Host "Continue anyway? (y/N)"
|
||||
if ($continue -ne "y") { exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
function Find-Xampp {
|
||||
$paths = @(
|
||||
"C:\xampp",
|
||||
"D:\xampp",
|
||||
"E:\xampp",
|
||||
"${env:ProgramFiles}\XAMPP",
|
||||
"${env:ProgramFiles(x86)}\XAMPP"
|
||||
)
|
||||
foreach ($p in $paths) {
|
||||
if (Test-Path "$p\php\php.exe") { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-Wamp {
|
||||
$paths = @(
|
||||
"C:\wamp64",
|
||||
"C:\wamp",
|
||||
"D:\wamp64",
|
||||
"D:\wamp"
|
||||
)
|
||||
foreach ($p in $paths) {
|
||||
if (Test-Path "$p\bin\php\php8.?.?\php.exe") { return $p }
|
||||
if (Test-Path "$p\bin\php\php8.??\php.exe") { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-PHP {
|
||||
$php = Get-Command "php.exe" -ErrorAction SilentlyContinue
|
||||
if ($php) { return Split-Path $php.Source -Parent }
|
||||
|
||||
$xampp = Find-Xampp
|
||||
if ($xampp) { return "$xampp\php" }
|
||||
|
||||
$wamp = Find-Wamp
|
||||
if ($wamp) {
|
||||
$phpDirs = Get-ChildItem "$wamp\bin\php" -Filter "php8*" -Directory | Sort-Object Name -Descending
|
||||
if ($phpDirs.Count -gt 0) { return $phpDirs[0].FullName }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-MySQL {
|
||||
$mysql = Get-Command "mysql.exe" -ErrorAction SilentlyContinue
|
||||
if ($mysql) { return Split-Path $mysql.Source -Parent }
|
||||
|
||||
$xampp = Find-Xampp
|
||||
if ($xampp -and (Test-Path "$xampp\mysql\bin\mysql.exe")) { return "$xampp\mysql\bin" }
|
||||
|
||||
$wamp = Find-Wamp
|
||||
if ($wamp -and (Test-Path "$wamp\bin\mariadb\*\bin\mysql.exe")) {
|
||||
$dirs = Get-ChildItem "$wamp\bin\mariadb" -Directory | Sort-Object Name -Descending
|
||||
if ($dirs.Count -gt 0) { return "$($dirs[0].FullName)\bin" }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-Apache {
|
||||
$xampp = Find-Xampp
|
||||
if ($xampp -and (Test-Path "$xampp\apache\bin\httpd.exe")) { return "$xampp\apache" }
|
||||
|
||||
$wamp = Find-Wamp
|
||||
if ($wamp -and (Test-Path "$wamp\bin\apache\*\bin\httpd.exe")) {
|
||||
$dirs = Get-ChildItem "$wamp\bin\apache" -Directory | Sort-Object Name -Descending
|
||||
if ($dirs.Count -gt 0) { return $dirs[0].FullName }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Ensure-Tool {
|
||||
param([string]$Name, [string]$CheckCommand, [string]$Url, [string]$InstallHint)
|
||||
|
||||
$found = Get-Command $CheckCommand -ErrorAction SilentlyContinue
|
||||
if ($found) {
|
||||
Write-OK "$Name is installed"
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Warn "$Name is not found."
|
||||
if ($Url) {
|
||||
Write-Info "Download from: $Url"
|
||||
}
|
||||
if ($InstallHint) {
|
||||
Write-Info "$InstallHint"
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Install-Composer {
|
||||
if (Get-Command "composer" -ErrorAction SilentlyContinue) {
|
||||
Write-OK "Composer already installed"
|
||||
return
|
||||
}
|
||||
Write-Step "Installing Composer..."
|
||||
$installer = "$env:TEMP\composer-setup.php"
|
||||
Invoke-WebRequest -Uri "https://getcomposer.org/installer" -OutFile $installer
|
||||
php $installer --quiet --install-dir="$env:USERPROFILE\bin"
|
||||
php -r "unlink('$installer');"
|
||||
$env:Path += ";$env:USERPROFILE\bin"
|
||||
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:USERPROFILE\bin", "User")
|
||||
Write-OK "Composer installed. You may need to restart your terminal."
|
||||
}
|
||||
|
||||
function Clone-Project {
|
||||
param([string]$TargetDir)
|
||||
|
||||
if (Test-Path "$TargetDir\artisan") {
|
||||
Write-OK "Project already cloned at $TargetDir"
|
||||
return $TargetDir
|
||||
}
|
||||
|
||||
if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Git is not installed. Install Git for Windows: https://git-scm.com/download/win"
|
||||
}
|
||||
|
||||
Write-Step "Cloning project..."
|
||||
git clone $REPO_URL $TargetDir
|
||||
Write-OK "Project cloned to $TargetDir"
|
||||
return $TargetDir
|
||||
}
|
||||
|
||||
function Configure-Env {
|
||||
param([string]$ProjectDir)
|
||||
|
||||
Write-Step "Configuring environment..."
|
||||
$envFile = "$ProjectDir\.env"
|
||||
if (Test-Path $envFile) {
|
||||
Write-Info ".env already exists, skipping"
|
||||
return
|
||||
}
|
||||
|
||||
Copy-Item "$ProjectDir\.env.example" $envFile
|
||||
$dbPass = Read-Host "Database password (leave empty for root without password)"
|
||||
|
||||
(Get-Content $envFile) -replace 'DB_DATABASE=.*', "DB_DATABASE=$DB_NAME" | Set-Content $envFile
|
||||
(Get-Content $envFile) -replace 'DB_USERNAME=.*', "DB_USERNAME=$DB_USER" | Set-Content $envFile
|
||||
(Get-Content $envFile) -replace 'DB_PASSWORD=.*', "DB_PASSWORD=$dbPass" | Set-Content $envFile
|
||||
|
||||
Push-Location $ProjectDir
|
||||
php artisan key:generate --quiet
|
||||
Pop-Location
|
||||
|
||||
Write-OK ".env configured and APP_KEY generated"
|
||||
}
|
||||
|
||||
function Create-Database {
|
||||
param([string]$MySQLDir)
|
||||
|
||||
Write-Step "Creating database '$DB_NAME'..."
|
||||
|
||||
$mysqlExe = if ($MySQLDir) { "$MySQLDir\mysql.exe" } else { "mysql.exe" }
|
||||
|
||||
if ($DB_PASS) {
|
||||
& $mysqlExe -u $DB_USER -p$DB_PASS -e "CREATE DATABASE IF NOT EXISTS `$DB_NAME` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>$null
|
||||
} else {
|
||||
& $mysqlExe -u $DB_USER -e "CREATE DATABASE IF NOT EXISTS `$DB_NAME` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>$null
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-OK "Database '$DB_NAME' ready"
|
||||
} else {
|
||||
Write-Warn "Could not create database automatically."
|
||||
Write-Info "Open phpMyAdmin (http://localhost/phpmyadmin) and create a database named '$DB_NAME' with charset utf8mb4_unicode_ci."
|
||||
Read-Host "Press Enter after creating the database"
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Migrations {
|
||||
param([string]$ProjectDir)
|
||||
|
||||
Write-Step "Running migrations and seeders..."
|
||||
Push-Location $ProjectDir
|
||||
php artisan migrate --seed --force
|
||||
Pop-Location
|
||||
Write-OK "Migrations and seeders complete"
|
||||
}
|
||||
|
||||
function Build-Assets {
|
||||
param([string]$ProjectDir)
|
||||
|
||||
Write-Step "Building frontend assets..."
|
||||
Push-Location $ProjectDir
|
||||
yarn build:all
|
||||
Pop-Location
|
||||
Write-OK "Frontend assets built"
|
||||
}
|
||||
|
||||
function Set-Permissions {
|
||||
param([string]$ProjectDir)
|
||||
|
||||
Write-Step "Setting permissions..."
|
||||
$folders = @("$ProjectDir\storage", "$ProjectDir\bootstrap\cache")
|
||||
|
||||
foreach ($folder in $folders) {
|
||||
if (Test-Path $folder) {
|
||||
try {
|
||||
icacls $folder /grant "Everyone:F" /T /Q 2>$null
|
||||
} catch {
|
||||
Write-Warn "Could not set permissions on $folder. You may need to do this manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-OK "Permissions set"
|
||||
}
|
||||
|
||||
function Configure-Apache {
|
||||
param([string]$ApacheDir, [string]$ProjectDir)
|
||||
|
||||
Write-Step "Configuring Apache virtual host..."
|
||||
|
||||
$domain = Read-Host "Enter your domain name (e.g. localhost)"
|
||||
$publicDir = "$ProjectDir\public".Replace("\", "/")
|
||||
|
||||
$vhost = @"
|
||||
<VirtualHost *:80>
|
||||
ServerName $domain
|
||||
DocumentRoot "$publicDir"
|
||||
|
||||
<Directory "$publicDir">
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog logs/atomcms-error.log
|
||||
CustomLog logs/atomcms-access.log combined
|
||||
</VirtualHost>
|
||||
"@
|
||||
|
||||
if ($ApacheDir) {
|
||||
$confFile = "$ApacheDir\conf\extra\httpd-vhosts.conf"
|
||||
Add-Content -Path $confFile -Value "`n$vhost" -ErrorAction SilentlyContinue
|
||||
Write-OK "Virtual host added to $confFile"
|
||||
Write-Info "Make sure these lines are uncommented in httpd.conf:"
|
||||
Write-Info " Include conf/extra/httpd-vhosts.conf"
|
||||
Write-Info " LoadModule rewrite_module modules/mod_rewrite.so"
|
||||
} else {
|
||||
Write-Warn "Apache directory not found. Add this virtual host manually:"
|
||||
Write-Host $vhost -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Summary {
|
||||
param([string]$Domain)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
||||
Write-Host "║ AtomCMS is ready! ║" -ForegroundColor Green
|
||||
Write-Host "║ ║" -ForegroundColor Green
|
||||
Write-Host "║ Visit: http://$Domain ║" -ForegroundColor Green
|
||||
Write-Host "║ Admin: http://$Domain/admin ║" -ForegroundColor Green
|
||||
Write-Host "║ ║" -ForegroundColor Green
|
||||
Write-Host "║ Run the repair tool for extra checks: ║" -ForegroundColor Green
|
||||
Write-Host "║ php artisan atom:check --fix ║" -ForegroundColor Green
|
||||
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Main {
|
||||
Write-Logo
|
||||
Test-Admin
|
||||
|
||||
# Check environment
|
||||
$phpDir = Find-PHP
|
||||
$mysqlDir = Find-MySQL
|
||||
$apacheDir = Find-Apache
|
||||
|
||||
if (-not $phpDir) {
|
||||
Write-Error "PHP not found. Install XAMPP (https://www.apachefriends.org/) or WampServer first."
|
||||
}
|
||||
|
||||
# Add PHP and MySQL to PATH for this session
|
||||
if ($phpDir) { $env:Path = "$phpDir;$env:Path" }
|
||||
if ($mysqlDir) { $env:Path = "$mysqlDir;$env:Path" }
|
||||
|
||||
Write-OK "PHP found: $((Get-Command php).Source)"
|
||||
if ($mysqlDir) { Write-OK "MySQL found: $mysqlDir" }
|
||||
if ($apacheDir) { Write-OK "Apache found: $apacheDir" }
|
||||
|
||||
# Ensure required tools
|
||||
Ensure-Tool -Name "Node.js" -CheckCommand "node" -Url "https://nodejs.org/" -InstallHint "Install Node.js LTS"
|
||||
$hasYarn = Ensure-Tool -Name "Yarn" -CheckCommand "yarn" -Url "https://yarnpkg.com/getting-started/install" -InstallHint "Run: npm install -g yarn"
|
||||
Ensure-Tool -Name "Git" -CheckCommand "git" -Url "https://git-scm.com/download/win" -InstallHint "Install Git for Windows"
|
||||
|
||||
if (-not $hasYarn) {
|
||||
Write-Step "Installing Yarn via npm..."
|
||||
npm install -g yarn
|
||||
Write-OK "Yarn installed"
|
||||
}
|
||||
|
||||
Install-Composer
|
||||
|
||||
# Where to install?
|
||||
$defaultDir = if (Find-Xampp) { "$(Find-Xampp)\htdocs\atomcms" } else { "$env:USERPROFILE\atomcms" }
|
||||
$projectDir = Read-Host "Installation directory [$defaultDir]"
|
||||
if (-not $projectDir) { $projectDir = $defaultDir }
|
||||
|
||||
Clone-Project -TargetDir $projectDir
|
||||
Configure-Env -ProjectDir $projectDir
|
||||
|
||||
# Install PHP deps
|
||||
Write-Step "Installing PHP dependencies..."
|
||||
Push-Location $projectDir
|
||||
composer install --no-interaction --optimize-autoloader --no-dev
|
||||
Pop-Location
|
||||
Write-OK "PHP dependencies installed"
|
||||
|
||||
# Install Node deps
|
||||
Write-Step "Installing Node dependencies..."
|
||||
Push-Location $projectDir
|
||||
yarn install --frozen-lockfile
|
||||
Pop-Location
|
||||
Write-OK "Node dependencies installed"
|
||||
|
||||
Create-Database -MySQLDir $mysqlDir
|
||||
Run-Migrations -ProjectDir $projectDir
|
||||
Build-Assets -ProjectDir $projectDir
|
||||
Set-Permissions -ProjectDir $projectDir
|
||||
|
||||
$configureWeb = Read-Host "Configure Apache virtual host? (y/N)"
|
||||
if ($configureWeb -eq "y") {
|
||||
Configure-Apache -ApacheDir $apacheDir -ProjectDir $projectDir
|
||||
}
|
||||
|
||||
# Finalize
|
||||
Write-Step "Finalizing..."
|
||||
Push-Location $projectDir
|
||||
php artisan storage:link --force 2>$null
|
||||
php artisan optimize:clear 2>$null
|
||||
Pop-Location
|
||||
|
||||
$domain = Read-Host "Enter your domain/path to access the site (e.g. localhost/atomcms/public or your-domain.com)"
|
||||
Show-Summary -Domain $domain
|
||||
}
|
||||
|
||||
Main
|
||||
@@ -52,6 +52,7 @@
|
||||
"commandocentrum.emulator_backups_desc": "View and restore emulator backups",
|
||||
"commandocentrum.restore": "Restore",
|
||||
"commandocentrum.nitro_client": "Nitro Client",
|
||||
"commandocentrum.nitro_cli_only": "This script can only be run via command line. Use: bash update-Nitrov3.sh\n\nSettings are configured in the .env file in the project root.",
|
||||
"commandocentrum.clothing_sync": "Clothing Sync",
|
||||
"commandocentrum.clothing_sync_desc": "Sync catalog clothing from FigureMap",
|
||||
"commandocentrum.sync": "Sync",
|
||||
|
||||
+2
-15
@@ -305,19 +305,6 @@
|
||||
"radio.wizard.unknown_error": "Onbekende fout",
|
||||
"Homepage": "Homepage",
|
||||
"commandocentrum.nitro_update": "Nitro V3 Update",
|
||||
"commandocentrum.nitro_update_desc": "Configureer paths en voer het update script uit voor Nitro V3, Render V3 en de Emulator",
|
||||
"commandocentrum.run_update": "Run Update",
|
||||
"commandocentrum.configure_nitro": "Configureer Paths",
|
||||
"commandocentrum.save_nitro_config": "Save Config",
|
||||
"commandocentrum.nitro_config_saved": "Nitro configuratie opgeslagen!",
|
||||
"commandocentrum.nitro_update_output": "Update Output",
|
||||
"commandocentrum.nitro_emulator_path": "Emulator hoofdmap",
|
||||
"commandocentrum.nitro_emulator_service": "Emulator service naam",
|
||||
"commandocentrum.nitro_db_name": "Database naam",
|
||||
"commandocentrum.nitro_sql_dir": "SQL updates map",
|
||||
"commandocentrum.nitro_backup_dir": "Backup map",
|
||||
"commandocentrum.nitro_gamedata_dir": "Gamedata config map",
|
||||
"commandocentrum.nitro_client_dir": "Nitro client config map (public/configuration)",
|
||||
"commandocentrum.nitro_client_src": "Nitro-V3 bronmap",
|
||||
"commandocentrum.nitro_renderer_src": "Nitro Render V3 bronmap"
|
||||
"commandocentrum.nitro_update_desc": "Update alleen via command line — configureer instellingen in .env",
|
||||
"commandocentrum.nitro_cli_only": "Dit script kan alleen via de command line worden uitgevoerd. Gebruik: bash update-Nitrov3.sh\n\nInstellingen worden geconfigureerd in het .env bestand in de project root."
|
||||
}
|
||||
|
||||
+2
-2
@@ -113,12 +113,12 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Gamedata / Habbo assets (symlinked)
|
||||
# Gamedata / Habbo assets
|
||||
location /gamedata/ {
|
||||
alias /var/www/Gamedata/config/;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Storage files (avatars, badges, photos)
|
||||
|
||||
+27
-26
@@ -5,9 +5,9 @@
|
||||
"dev": "vite",
|
||||
"dev:atom": "vite --config resources/themes/atom/vite.config.js",
|
||||
"build": "vite build",
|
||||
"build:atom": "vite build --config resources/themes/atom/vite.config.js",
|
||||
"build:dusk": "vite build --config resources/themes/dusk/vite.config.js",
|
||||
"build:all": "vite build --config resources/themes/atom/vite.config.js && vite build --config resources/themes/dusk/vite.config.js",
|
||||
"build:atom": "vite build --config resources/themes/atom/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build",
|
||||
"build:dusk": "vite build --config resources/themes/dusk/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build",
|
||||
"build:all": "vite build --config resources/themes/atom/vite.config.js && vite build --config resources/themes/dusk/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier \"resources/**/*.{js,ts,vue,blade}\" --ignore-unknown --write",
|
||||
"format:check": "prettier \"resources/**/*.{js,ts,vue,blade.php}\" --check",
|
||||
@@ -25,39 +25,40 @@
|
||||
"rebuild": "yarn clean && yarn install && yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alpinejs/focus": "3.15.9",
|
||||
"@alpinejs/focus": "3.15.12",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@prettier/plugin-php": "0.25.0",
|
||||
"@shufo/prettier-plugin-blade": "1.16.2",
|
||||
"@swc/core": "^1.15.21",
|
||||
"@swc/core": "^1.15.40",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tailwindcss/postcss": "4.2.2",
|
||||
"@tailwindcss/postcss": "4.3.0",
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"alpinejs": "3.15.9",
|
||||
"autoprefixer": "10.4.20",
|
||||
"axios": "^1.16.1",
|
||||
"esbuild": "^0.27.7",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"laravel-vite-plugin": "^3.0.1",
|
||||
"alpinejs": "3.15.12",
|
||||
"autoprefixer": "10.5.0",
|
||||
"axios": "^1.17.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-plugin-vue": "10.9.2",
|
||||
"laravel-vite-plugin": "^3.1.0",
|
||||
"lodash": "^4.18.1",
|
||||
"postcss": "8.5.8",
|
||||
"postcss-import": "16.1.0",
|
||||
"prettier": "3.8.1",
|
||||
"sass-loader": "16.0.4",
|
||||
"stylelint": "^17.6.0",
|
||||
"postcss": "8.5.15",
|
||||
"postcss-import": "16.1.1",
|
||||
"prettier": "3.8.3",
|
||||
"sass-loader": "17.0.0",
|
||||
"stylelint": "^17.13.0",
|
||||
"stylelint-config-standard": "^40.0.0",
|
||||
"tailwindcss": "4.2.2",
|
||||
"tailwindcss": "4.3.0",
|
||||
"turbolinks": "5.2.0",
|
||||
"vite": "^8.0.3"
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inertiajs/react": "^3.2.0",
|
||||
"@inertiajs/react": "^3.3.1",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"flowbite": "2.5.2",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"sass": "1.83.4",
|
||||
"swiper": "^12.1.3"
|
||||
"flowbite": "4.0.2",
|
||||
"json5": "^2.2.3",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"sass": "1.100.0",
|
||||
"swiper": "^12.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- app
|
||||
- config
|
||||
- database
|
||||
- routes
|
||||
excludePaths:
|
||||
- vendor
|
||||
- storage
|
||||
- bootstrap
|
||||
- public
|
||||
- resources
|
||||
- lang
|
||||
tmpDir: storage/framework/cache/phpstan
|
||||
checkMissingIterableValueType: false
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 260 B |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 678 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user