Initialer Laravel Commit für BetiX
This commit is contained in:
303
app/Models/User.php
Normal file
303
app/Models/User.php
Normal file
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use App\Casts\EncryptedDecimal;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Notifications\VerifyEmail;
|
||||
use App\Notifications\ResetPassword;
|
||||
use App\Casts\SafeEncryptedString;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'email_index', // Blind Index for lookups
|
||||
'username',
|
||||
'username_index', // Blind Index for lookups
|
||||
'avatar_url', // Profile image for chat/header
|
||||
'role', // Role badge (e.g., Admin, Streamer, Co)
|
||||
'clan_tag', // Clan short tag badge
|
||||
// Profile fields
|
||||
'first_name',
|
||||
'last_name',
|
||||
'gender',
|
||||
'birthdate',
|
||||
'phone',
|
||||
'country',
|
||||
'address_line1',
|
||||
'address_line2',
|
||||
'city',
|
||||
'state',
|
||||
'postal_code',
|
||||
'currency',
|
||||
'is_adult',
|
||||
'password',
|
||||
'last_login_at',
|
||||
'last_login_ip',
|
||||
'last_login_user_agent',
|
||||
'balance', // BTX Balance
|
||||
'vip_level',
|
||||
'vault_balance',
|
||||
'vault_balances',
|
||||
'preferred_locale',
|
||||
// Social Profile Fields
|
||||
'is_public',
|
||||
'bio',
|
||||
'avatar',
|
||||
'banner',
|
||||
'is_banned', // Added
|
||||
'withdraw_cooldown_until',
|
||||
'registration_ip',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'remember_token',
|
||||
'birthdate', // Hide sensitive data
|
||||
'phone', // Hide sensitive data
|
||||
'email_index',
|
||||
'username_index',
|
||||
'last_login_ip',
|
||||
'last_login_user_agent',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
'email' => SafeEncryptedString::class,
|
||||
'is_adult' => 'boolean',
|
||||
'last_login_at' => 'datetime',
|
||||
'balance' => 'decimal:4',
|
||||
'vip_level' => 'integer',
|
||||
'vault_balance' => 'decimal:4', // Store and handle as plaintext decimal now
|
||||
'vault_balances' => 'array', // JSON map: {"BTC":"0","ETH":"0","SOL":"0"}
|
||||
'is_public' => 'boolean',
|
||||
'is_banned' => 'boolean', // Added
|
||||
// Vault PIN-related
|
||||
'vault_pin_set_at' => 'datetime',
|
||||
'vault_pin_locked_until' => 'datetime',
|
||||
'vault_pin_attempts' => 'integer',
|
||||
'withdraw_cooldown_until' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically hash email and username for blind indexing on save.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (User $user) {
|
||||
if ($user->isDirty('email')) {
|
||||
// Store email hash in lowercase for case-insensitive lookup
|
||||
$user->email_index = hash('sha256', strtolower($user->email));
|
||||
}
|
||||
if ($user->isDirty('username')) {
|
||||
// Store username hash in lowercase for case-insensitive lookup
|
||||
$user->username_index = hash('sha256', strtolower($user->username));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's stats.
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
return $this->hasOne(UserStats::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's wallets.
|
||||
*/
|
||||
public function wallets()
|
||||
{
|
||||
return $this->hasMany(Wallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's guild membership.
|
||||
*/
|
||||
public function guildMember()
|
||||
{
|
||||
return $this->hasOne(GuildMember::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all restrictions for the user.
|
||||
*/
|
||||
public function restrictions()
|
||||
{
|
||||
return $this->hasMany(UserRestriction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has an active account ban.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Casts\Attribute
|
||||
*/
|
||||
protected function isBanned(): \Illuminate\Database\Eloquent\Casts\Attribute
|
||||
{
|
||||
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
|
||||
get: fn () => $this->restrictions()->where('type', 'account_ban')->where('active', true)->exists(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email address that should be used for password reset.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEmailForPasswordReset()
|
||||
{
|
||||
return $this->email; // Use the actual email
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email verification notification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
$this->notify(new VerifyEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure MailChannel gets a plaintext, RFC-compliant email address.
|
||||
* If the stored value is still encrypted/malformed, try to decrypt
|
||||
* using configured keys. If still invalid, log and return null to skip send.
|
||||
*
|
||||
* Returning null will prevent the Mail channel from sending and avoids
|
||||
* Symfony RfcComplianceException while we fix upstream data/keys.
|
||||
*
|
||||
* @return string|array|null
|
||||
*/
|
||||
public function routeNotificationForMail(): string|array|null
|
||||
{
|
||||
// 1) Try the casted value first (should be plaintext if keys are aligned)
|
||||
$candidates = [];
|
||||
$casted = $this->email;
|
||||
if (is_string($casted)) {
|
||||
$candidates[] = $casted;
|
||||
}
|
||||
|
||||
// 2) Try decrypting the raw DB value explicitly if it looks encrypted
|
||||
$raw = (string) $this->getRawOriginal('email');
|
||||
if ($raw !== '') {
|
||||
$candidates[] = $raw;
|
||||
$decrypted = $this->tryDecrypt($raw);
|
||||
if ($decrypted !== null) {
|
||||
$candidates[] = $decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
// 3) As last resort, try decrypting the casted value too (in case a layer re-wrapped it)
|
||||
if (is_string($casted)) {
|
||||
$dec2 = $this->tryDecrypt($casted);
|
||||
if ($dec2 !== null) {
|
||||
$candidates[] = $dec2;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidates as $value) {
|
||||
if (is_string($value) && $this->isValidEmail($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// No valid address could be obtained. Log once per user session/context.
|
||||
Log::warning('Unable to derive plaintext email for mail routing; skipping send to avoid RFC error', [
|
||||
'user_id' => $this->id,
|
||||
'env' => app()->environment(),
|
||||
]);
|
||||
|
||||
// In local/dev, you may prefer to route to MAIL_FROM_ADDRESS to unblock flows:
|
||||
if (app()->environment(['local', 'development'])) {
|
||||
$fallback = (string) config('mail.from.address');
|
||||
if ($this->isValidEmail($fallback)) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Returning null prevents MailChannel from attempting a send
|
||||
return null;
|
||||
}
|
||||
|
||||
private function tryDecrypt(?string $value): ?string
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (!$this->looksEncrypted($value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Crypt::decryptString($value);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function isValidEmail(string $email): bool
|
||||
{
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
private function looksEncrypted(string $value): bool
|
||||
{
|
||||
$decoded = base64_decode($value, true);
|
||||
if ($decoded === false) {
|
||||
return false;
|
||||
}
|
||||
$json = json_decode($decoded, true);
|
||||
if (!is_array($json)) {
|
||||
return false;
|
||||
}
|
||||
return isset($json['iv'], $json['value'], $json['mac']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user