Files
portal-api/app/Models/PopupBanner.php
baodev0andClaude Sonnet 5 12d5b4951c feat: popup quảng cáo, đặt lại mật khẩu bằng token 80 ký tự, 2FA email, vá lỗ hổng bảo mật
- Popup/banner quảng cáo cấu hình từ admin (loại, link, lịch hiển thị)
- Đặt lại mật khẩu: token 80 ký tự kèm check digit Luhn thay cho token+email lộ trong URL, chỉ lưu bản băm, một lần dùng, hết hạn 60 phút
- Xác thực 2 lớp qua email khi đăng nhập (mã 6 số, ghi nhớ thiết bị 90 ngày)
- ApiAuthCheck chặn ngay tài khoản bị khoá dù JWT còn hạn (trước đây dùng được tới hết TTL)
- Handler trả 404 thay vì 500 khi ModelNotFoundException không bọc HttpException
- Test ClientTamperingTest: xác nhận IDOR/giả mạo dữ liệu từ client đều bị chặn

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-13 13:23:33 +07:00

71 lines
1.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use App\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class PopupBanner extends Model
{
use LogsActivity;
public const DISK = 'public';
public const TYPES = [
'advertisement' => 'Quảng cáo',
'promotion' => 'Chương trình khuyến mãi',
];
public const PUBLIC_FIELDS = ['id', 'title', 'type', 'image_url', 'link_url', 'open_in_new_tab'];
protected $fillable = [
'title',
'type',
'image_path',
'link_url',
'open_in_new_tab',
'is_active',
'starts_at',
'ends_at',
'sort_order',
];
protected $casts = [
'open_in_new_tab' => 'boolean',
'is_active' => 'boolean',
'starts_at' => 'datetime',
'ends_at' => 'datetime',
'sort_order' => 'integer',
];
protected $appends = ['image_url'];
public function getImageUrlAttribute(): string
{
return Storage::disk(self::DISK)->url($this->image_path);
}
public function scopeVisible(Builder $query): Builder
{
return $query->where('is_active', true)
->where(fn (Builder $q) => $q->whereNull('starts_at')->orWhere('starts_at', '<=', now()))
->where(fn (Builder $q) => $q->whereNull('ends_at')->orWhere('ends_at', '>=', now()))
->orderBy('sort_order')
->orderBy('id');
}
/**
* @return list<array<string, mixed>>
*/
public static function publicList(): array
{
return self::visible()->get()
->map(fn (self $banner) => $banner->only(self::PUBLIC_FIELDS))
->all();
}
}