3711a92958
PhoneNormalizer (RU-телефон → 7XXXXXXXXXX) + Mailable RegisterEmailVerificationCode с 6-значным кодом + эндпоинты register/start|verify|resend: pending-регистрация в сессии (паттерн 2FA), email_verified_at=now() при verify, rate-limit на start + cooldown 60с на resend, лимит 5 попыток ввода кода. Телефон обязателен, нормализуется в 7XXXXXXXXXX. deptrac: разрешён Request→Service. Старый одношаговый register пока сохранён (удаляется отдельной задачей Task 6). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.8 KiB
PHP
95 lines
2.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Models;
|
||
|
||
use Database\Factories\UserFactory;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||
use Illuminate\Notifications\Notifiable;
|
||
|
||
/**
|
||
* Пользователь тенанта (менеджер, оператор, owner).
|
||
*
|
||
* Tenant-aware модель с RLS: SELECT/INSERT/UPDATE/DELETE фильтруются
|
||
* политикой `tenant_isolation` по `current_setting('app.current_tenant_id')`.
|
||
*
|
||
* Источник: db/schema.sql v8.6 §4, table `users`. **НЕ путать**
|
||
* с `saas_admin_users` (админы SaaS-портала, отдельная таблица).
|
||
*
|
||
* @mixin IdeHelperUser
|
||
*/
|
||
class User extends Authenticatable
|
||
{
|
||
/** @use HasFactory<UserFactory> */
|
||
use HasFactory, Notifiable, SoftDeletes;
|
||
|
||
protected $fillable = [
|
||
'tenant_id',
|
||
'email',
|
||
'password_hash',
|
||
'first_name',
|
||
'last_name',
|
||
'phone',
|
||
'timezone',
|
||
'avatar_path',
|
||
'totp_secret',
|
||
'totp_enabled',
|
||
'notification_preferences',
|
||
'sound_enabled',
|
||
'telegram_user_id',
|
||
'is_active',
|
||
'last_login_at',
|
||
'last_active_at',
|
||
'email_verified_at',
|
||
];
|
||
|
||
protected $hidden = [
|
||
'password_hash',
|
||
'totp_secret',
|
||
];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'totp_enabled' => 'boolean',
|
||
// ТЗ §22.5.2: totp_secret шифруется через Crypt::encryptString при сохранении,
|
||
// расшифровывается при чтении. Eloquent cast 'encrypted' делает это автоматически.
|
||
'totp_secret' => 'encrypted',
|
||
'sound_enabled' => 'boolean',
|
||
'is_active' => 'boolean',
|
||
'notification_preferences' => 'array',
|
||
'telegram_user_id' => 'integer',
|
||
'email_verified_at' => 'datetime',
|
||
'last_login_at' => 'datetime',
|
||
'last_active_at' => 'datetime',
|
||
'created_at' => 'datetime',
|
||
'updated_at' => 'datetime',
|
||
'deleted_at' => 'datetime',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Auth-интеграция: схема использует `password_hash` вместо
|
||
* стандартного Laravel-имени `password`.
|
||
*/
|
||
public function getAuthPassword(): string
|
||
{
|
||
return (string) $this->password_hash;
|
||
}
|
||
|
||
public function getAuthPasswordName(): string
|
||
{
|
||
return 'password_hash';
|
||
}
|
||
|
||
/** @return BelongsTo<Tenant, $this> */
|
||
public function tenant(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Tenant::class);
|
||
}
|
||
}
|