Initial commit

This commit is contained in:
root
2026-05-09 17:28:23 +02:00
commit 9d73f82529
5575 changed files with 281989 additions and 0 deletions
@@ -0,0 +1,66 @@
<?php
namespace App\Actions\Fortify\Controllers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Events\RecoveryCodeReplaced;
use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest;
use Laravel\Fortify\Http\Responses\TwoFactorLoginResponse;
class TwoFactorAuthenticatedSessionController extends Controller
{
/**
* The guard implementation.
*
* @var StatefulGuard
*/
protected $guard;
/**
* Create a new controller instance.
*/
public function __construct(StatefulGuard $guard)
{
$this->guard = $guard;
}
/**
* Attempt to authenticate a new session using the two factor authentication code.
*/
public function store(TwoFactorLoginRequest $request): TwoFactorLoginResponse
{
$user = $request->challengedUser();
$authenticatableUser = $user instanceof Authenticatable ? $user : null;
if ($code = $request->validRecoveryCode()) {
if ($user !== null && is_object($user) && method_exists($user, 'replaceRecoveryCode')) {
$user->replaceRecoveryCode($code);
}
if ($authenticatableUser instanceof Authenticatable) {
event(new RecoveryCodeReplaced($authenticatableUser, $code));
}
} elseif (! $request->hasValidCode()) {
throw ValidationException::withMessages([
'code' => __('Invalid Two Factor Authentication code'),
]);
}
if ($authenticatableUser instanceof Authenticatable) {
$this->guard->login($authenticatableUser, $request->remember());
}
$request->session()->regenerate();
if ($user !== null && is_object($user) && method_exists($user, 'update')) {
$user->update([
'ip_current' => $request->ip(),
]);
}
return app(TwoFactorLoginResponse::class);
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
namespace App\Actions\Fortify;
use App\Actions\Fortify\Rules\PasswordValidationRules;
use App\Models\Miscellaneous\WebsiteBetaCode;
use App\Models\User;
use App\Rules\BetaCodeRule;
use App\Rules\GoogleRecaptchaRule;
use App\Rules\WebsiteWordfilterRule;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, mixed> $input
*/
public function create(array $input): User
{
if ((setting('disable_registration') ?: '0') == '1') {
throw ValidationException::withMessages([
'registration' => __('Registration is disabled.'),
]);
}
$ip = request()->ip() ?? '127.0.0.1';
if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
$ip = '127.0.0.1';
}
$matchingIpCount = User::query()
->where('ip_current', '=', $ip)
->orWhere('ip_register', '=', $ip)
->count();
$maxAccountsPerIpSetting = setting('max_accounts_per_ip');
setting('hotel_home_room');
$maxAccountsPerIp = (int) (is_string($maxAccountsPerIpSetting) ? $maxAccountsPerIpSetting : '99');
if ($matchingIpCount >= $maxAccountsPerIp) {
throw ValidationException::withMessages([
'registration' => __('You have reached the max amount of allowed account'),
]);
}
$this->validate($input);
$startCreditsSetting = setting('start_credits');
$hotelHomeRoomSetting = setting('hotel_home_room');
/** @var User $user */
$user = User::create([
'username' => $input['username'],
'mail' => $input['mail'],
'password' => Hash::make(is_string($input['password']) ? $input['password'] : ''),
'account_created' => time(),
'last_login' => time(),
'motto' => setting('start_motto') ?: 'Welcome to the hotel!',
'look' => $input['look'] ?? setting('start_look') ?: 'hr-100-61.hd-180-1.ch-210-66.lg-270-110.sh-305-62',
'credits' => (int) (is_string($startCreditsSetting) ? $startCreditsSetting : '1000'),
'auth_ticket' => '',
'home_room' => (int) (is_string($hotelHomeRoomSetting) ? $hotelHomeRoomSetting : '0'),
'ip_register' => $ip,
'ip_current' => $ip,
]);
$user->update([
'referral_code' => sprintf('%d%s', $user->id, Str::random(8)),
]);
if (setting('requires_beta_code')) {
WebsiteBetaCode::where('code', '=', $input['beta_code'])->update([
'user_id' => $user->id,
]);
}
// Referral
if (isset($input['referral_code'])) {
/** @var User|null $referralUser */
$referralUser = User::query()
->where('referral_code', '=', $input['referral_code'])
->first();
// Only process referral if users have different IPs
if ($referralUser !== null && ($referralUser->ip_current != $user->ip_current && $referralUser->ip_register != $user->ip_register)) {
$referralUser->referrals()->updateOrCreate(['user_id' => $referralUser->id], [
'referrals_total' => $referralUser->referrals !== null ? $referralUser->referrals->referrals_total + 1 : 1,
]);
$referralUser->userReferrals()->create([
'referred_user_id' => $user->id,
'referred_user_ip' => $ip,
]);
}
}
if (setting('enable_discord_webhook') === '1') {
$discordRanksSetting = setting('discord_webhook_ranks', '[]');
$discordRanks = json_decode($discordRanksSetting, true) ?? [];
$shouldNotify = false;
if (! empty($discordRanks)) {
$shouldNotify = in_array($user->rank, $discordRanks);
} else {
$minStaffRank = (int) setting('min_staff_rank', 3);
$shouldNotify = $user->rank >= $minStaffRank;
}
if ($shouldNotify) {
$this->sendDiscordWebhook($user->username, $ip, $user->mail);
}
}
return $user;
}
/**
* @param array<string, mixed> $inputs
*
* @return array<string, mixed>
*/
private function validate(array $inputs): array
{
$usernameRegexSetting = setting('username_regex');
$usernameRegex = is_string($usernameRegexSetting) ? $usernameRegexSetting : '/^[a-zA-Z0-9_.-]+$/';
$rules = [
'username' => ['required', 'string', 'regex:' . $usernameRegex, 'max:25', Rule::unique('users'), new WebsiteWordfilterRule],
'mail' => ['required', 'string', 'email', 'max:255', Rule::unique('users')],
'password' => $this->passwordRules(),
'beta_code' => ['sometimes', 'string', new BetaCodeRule],
'terms' => ['required', 'accepted'],
'g-recaptcha-response' => ['sometimes', 'string', new GoogleRecaptchaRule],
'look' => ['sometimes', 'string'],
];
if (! empty($inputs['cf-turnstile-response'])) {
$rules['cf-turnstile-response'] = [app(Turnstile::class)];
}
$messages = [
'g-recaptcha-response.required' => __('The Google recaptcha must be completed'),
'g-recaptcha-response.string' => __('The google recaptcha was submitted with an invalid type'),
];
return Validator::make($inputs, $rules, $messages)->validate();
}
private function sendDiscordWebhook(string $username, string $ip, string $email): void
{
if (setting('discord_webhook_url') === '') {
Log::error('Discord webhook url not provided', ['Please provide a discord webhook url before being able to send any webhook requests.']);
return;
}
$discordWebhookUrl = setting('discord_webhook_url');
$hotelNameSetting = setting('hotel_name');
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}",
]);
} catch (\Exception $e) {
Log::error('Failed to send Discord webhook notification', [
'username' => $username,
'ip' => $ip,
'email' => $email,
'error' => $e->getMessage(),
]);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\User;
class DisableTwoFactorAuthentication extends \Laravel\Fortify\Actions\DisableTwoFactorAuthentication
{
#[\Override]
public function __invoke($user): void
{
if ($user instanceof User) {
$user->forceFill([
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed' => false,
])->save();
}
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use App\Rules\GoogleRecaptchaRule;
use Illuminate\Auth\Events\Failed;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Events\TwoFactorAuthenticationChallenged;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\LoginRateLimiter;
use Laravel\Fortify\TwoFactorAuthenticatable;
use RyanChandler\LaravelCloudflareTurnstile\Rules\Turnstile;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfTwoFactorAuthenticatable
{
/**
* The guard implementation.
*
* @var StatefulGuard
*/
protected $guard;
/**
* The login rate limiter instance.
*
* @var LoginRateLimiter
*/
protected $limiter;
/**
* Create a new controller instance.
*/
public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter)
{
$this->guard = $guard;
$this->limiter = $limiter;
}
/**
* Handle the incoming request.
*
* @return mixed
*/
public function handle(Request $request, callable $next)
{
$user = $this->validateCredentials($request);
if (Fortify::confirmsTwoFactorAuthentication()) {
if ($user instanceof User &&
property_exists($user, 'two_factor_secret') && $user->two_factor_secret &&
property_exists($user, 'two_factor_confirmed_at') && ! is_null($user->two_factor_confirmed_at) &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) {
return $this->twoFactorChallengeResponse($request, $user);
} else {
return $next($request);
}
}
if ($user instanceof User &&
property_exists($user, 'two_factor_secret') && $user->two_factor_secret &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) {
return $this->twoFactorChallengeResponse($request, $user);
}
return $next($request);
}
/**
* Attempt to validate the incoming credentials.
*
* @return mixed
*/
protected function validateCredentials(Request $request)
{
if (Fortify::$authenticateUsingCallback) {
return tap(call_user_func(Fortify::$authenticateUsingCallback, $request), function ($user) use ($request) {
if (! $user) {
$this->fireFailedEvent($request);
$this->throwFailedAuthenticationException($request);
}
});
}
$providerModel = config('auth.providers.users.model');
/** @var Model $model */
$model = new $providerModel;
return tap($model->newQuery()->where(Fortify::username(), $request->{Fortify::username()})->first(), function ($user) use ($request): void {
// Update the users password to bcrypt, if they previously used md5
if ($user instanceof User && config('habbo.site.convert_passwords')) {
$password = $request->input('password');
$this->convertUserPassword($user, is_string($password) ? $password : '');
}
/** @var Guard|StatefulGuard $guard */
$guard = $this->guard;
$passwordInput = $request->input('password');
$passwordString = is_string($passwordInput) ? $passwordInput : '';
if (! $user instanceof User || ! $guard->validate(['password' => $passwordString])) {
$this->fireFailedEvent($request, $user instanceof User ? $user : null);
$this->throwFailedAuthenticationException($request);
}
$this->validate($request);
/** @var User|null $user */
$user = User::query()
->select(['id', 'password', 'rank'])
->where('username', $request->input('username'))
->first();
if (setting('maintenance_enabled') === '1' && setting('min_maintenance_login_rank') > ($user->rank ?? 0)) {
throw ValidationException::withMessages([
'username' => __('Only staff can login during maintenance!'),
]);
}
});
}
/**
* Throw a failed authentication validation exception.
*
*
* @throws ValidationException
*/
protected function throwFailedAuthenticationException(Request $request): void
{
$this->limiter->increment($request);
throw ValidationException::withMessages([
Fortify::username() => [trans('auth.failed')],
])->errorBag('login');
}
/**
* Fire the failed authentication attempt event with the given arguments.
*/
protected function fireFailedEvent(Request $request, ?Authenticatable $user = null): void
{
$guardName = config('fortify.guard');
$username = $request->{Fortify::username()};
$password = $request->input('password');
event(new Failed(is_string($guardName) ? $guardName : '', $user, [
Fortify::username() => is_string($username) ? $username : '',
'password' => is_string($password) ? $password : '',
]));
}
/**
* Get the two factor authentication enabled response.
*
* @param mixed $user
*/
protected function twoFactorChallengeResponse(Request $request, $user): Response
{
$request->session()->put([
'login.id' => $user instanceof User ? $user->getKey() : null,
'login.remember' => $request->filled('remember'),
]);
TwoFactorAuthenticationChallenged::dispatch($user);
return $request->wantsJson()
? response()->json(['two_factor' => true])
: redirect()->route('two-factor.login');
}
private function convertUserPassword(User $user, string $password): void
{
if ($user->password == md5($password)) {
$user->update([
'password' => Hash::make($password),
]);
}
}
/**
* @return array<array<mixed>>
*/
private function validate(Request $request): array
{
$rules = [];
if (setting('google_recaptcha_enabled')) {
$rules['g-recaptcha-response'] = ['required', 'string', new GoogleRecaptchaRule];
}
if (setting('cloudflare_turnstile_enabled')) {
$rules['cf-turnstile-response'] = ['required', app(Turnstile::class)];
}
$messages = [
'g-recaptcha-response.required' => __('The Google reCAPTCHA must be completed'),
'g-recaptcha-response.string' => __('The Google reCAPTCHA was submitted with an invalid type'),
'cf-turnstile-response.required' => __('The Cloudflare Turnstile response is required'),
];
return Validator::make($request->all(), $rules, $messages)->validate();
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
class RedirectIfTwoFactorConfirmed extends RedirectIfTwoFactorAuthenticatable
{
// This class can use the default behavior from the parent class
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Actions\Fortify\Rules\PasswordValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* @param array<string, mixed> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$password = $input['password'] ?? '';
$user->forceFill([
'password' => Hash::make(is_string($password) ? $password : ''),
])->save();
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Fortify\Rules;
use App\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*/
/**
* @return array<int, mixed>
*/
protected function passwordRules(): array
{
return ['required', 'string', new Password, 'confirmed'];
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Actions\Fortify\Rules\PasswordValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* @param array<string, mixed> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$password = $input['password'] ?? '';
$user->forceFill([
'password' => Hash::make(is_string($password) ? $password : ''),
])->save();
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* @param array<string, mixed> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->mail &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'mail' => $input['email'],
])->save();
}
}
/**
* @param array<string, mixed> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'mail' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Actions;
use App\Enums\CurrencyTypes;
use App\Models\User;
use App\Services\RconService;
class SendCurrency
{
public function __construct(protected RconService $rcon) {}
public function execute(User $user, string $type, ?int $amount): bool
{
if (! $amount || $amount <= 0) {
return false;
}
if ($this->rcon->isConnected) {
match ($type) {
'credits' => $this->rcon->giveCredits($user, $amount),
'duckets' => $this->rcon->giveDuckets($user, $amount),
'diamonds' => $this->rcon->giveDiamonds($user, $amount),
'points' => $this->rcon->giveGotw($user, $amount),
default => false,
};
} else {
match ($type) {
'credits' => $user->update(['credits' => $user->credits + $amount]),
'duckets' => $user->currencies()->where('type', CurrencyTypes::Duckets)->increment('amount', $amount),
'diamonds' => $user->currencies()->where('type', CurrencyTypes::Diamonds)->increment('amount', $amount),
'points' => $user->currencies()->where('type', CurrencyTypes::Points)->increment('amount', $amount),
default => false,
};
}
return true;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Actions;
use App\Models\User;
use App\Services\RconService;
class SendFurniture
{
public function __construct(private readonly RconService $rcon) {}
/**
* @param array<int, array{item_id: int, amount: int}> $furniture
*/
public function execute(User $user, array $furniture): void
{
foreach ($furniture as $furni) {
if ($this->rcon->isConnected) {
for ($i = 0; $i < $furni['amount']; $i++) {
$hotelName = setting('hotel_name');
$this->rcon->sendGift($user, $furni['item_id'], 'Thank you for supporting ' . (is_scalar($hotelName) ? (string) $hotelName : ''));
}
} else {
for ($i = 0; $i < $furni['amount']; $i++) {
$user->items()->create([
'item_id' => $furni['item_id'],
]);
}
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Actions;
use App\Models\User;
class UserActions
{
public function updateUsername(User $user, string $username): void
{
$user->update([
'username' => $username,
]);
}
public function updateEmail(User $user, string $email): void
{
$user->update([
'mail' => $email,
]);
}
public function updateMotto(User $user, string $motto): void
{
$user->update([
'motto' => $motto,
]);
}
public function updateField(User $user, string $field, ?string $value): void
{
$user->update([
$field => $value,
]);
}
}