- 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>
147 lines
4.6 KiB
PHP
147 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace Database\Seeders;
|
|
|
|
use App\Models\Template;
|
|
use App\Support\EmailTemplateShell;
|
|
use App\Support\MailPlaceholders;
|
|
use Database\Seeders\EmailTemplates\AccountTemplates;
|
|
use Database\Seeders\EmailTemplates\AdminTemplates;
|
|
use Database\Seeders\EmailTemplates\BillingTemplates;
|
|
use Database\Seeders\EmailTemplates\LegacyTemplates;
|
|
use Database\Seeders\EmailTemplates\MarketingTemplates;
|
|
use Database\Seeders\EmailTemplates\ServiceTemplates;
|
|
use Database\Seeders\EmailTemplates\TemplateVariables;
|
|
use Illuminate\Database\Seeder;
|
|
|
|
class TemplateSeeder extends Seeder
|
|
{
|
|
public function run(): void
|
|
{
|
|
foreach ([...self::templates(), ...self::legacyTemplates()] as $item) {
|
|
$this->upsert($item);
|
|
}
|
|
|
|
foreach (MarketingTemplates::all() as $item) {
|
|
$this->createMarketingIfMissing($item);
|
|
}
|
|
}
|
|
|
|
public function seedScenarioIfMissing(string $scenario): void
|
|
{
|
|
if (Template::query()->where('key', $scenario)->orWhere('scenario', $scenario)->exists()) {
|
|
return;
|
|
}
|
|
|
|
foreach (self::templates() as $item) {
|
|
if ($item['scenario'] === $scenario) {
|
|
$this->upsert($item);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function createMarketingIfMissing(array $item): void
|
|
{
|
|
$template = Template::firstOrNew(['name' => $item['name']]);
|
|
if ($template->exists) {
|
|
return;
|
|
}
|
|
|
|
$template->fill([
|
|
'type_id' => 0,
|
|
'format' => 'html',
|
|
'subject' => $item['subject'],
|
|
'message' => EmailTemplateShell::page($item['body']),
|
|
'status' => 1,
|
|
]);
|
|
$template->syncVariables($item['variables']);
|
|
$template->save();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{scenario: string, name: string, subject: string, message: string, types?: array<string, string>}>
|
|
*/
|
|
public static function templates(): array
|
|
{
|
|
$layout = [
|
|
'scenario' => 'layout',
|
|
'name' => 'Bố cục Header/Footer (mặc định)',
|
|
'subject' => '',
|
|
'message' => EmailTemplateShell::page('{{$content}}'),
|
|
];
|
|
|
|
$scenarios = array_map(
|
|
fn (array $item) => [...$item, 'message' => EmailTemplateShell::page($item['body'])],
|
|
[
|
|
...AccountTemplates::all(),
|
|
...ServiceTemplates::all(),
|
|
...BillingTemplates::all(),
|
|
...AdminTemplates::all(),
|
|
],
|
|
);
|
|
|
|
return [$layout, ...$scenarios];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{scenario: string, name: string, subject: string, message: string}>
|
|
*/
|
|
public static function legacyTemplates(): array
|
|
{
|
|
return array_map(
|
|
fn (array $item) => [...$item, 'message' => EmailTemplateShell::page($item['body'])],
|
|
LegacyTemplates::all(),
|
|
);
|
|
}
|
|
|
|
private function upsert(array $item): void
|
|
{
|
|
$scenario = $item['scenario'];
|
|
|
|
$template = Template::query()
|
|
->where(fn ($q) => $q->where('key', $scenario)->orWhere('scenario', $scenario))
|
|
->orderByRaw('`key` = ? desc', [$scenario])
|
|
->orderBy('id')
|
|
->first() ?? new Template(['type_id' => 0, 'status' => 1]);
|
|
|
|
$template->fill([
|
|
'key' => $scenario,
|
|
'scenario' => $scenario,
|
|
'name' => $item['name'],
|
|
'subject' => $item['subject'],
|
|
'message' => $item['message'],
|
|
'format' => 'html',
|
|
]);
|
|
|
|
$template->metadata = [
|
|
...($template->metadata ?? []),
|
|
'variables' => $this->variables($template, $item['types'] ?? []),
|
|
];
|
|
|
|
$template->save();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $typeOverrides
|
|
* @return array<int, array{key: string, label: string, type: string, example: string}>
|
|
*/
|
|
private function variables(Template $template, array $typeOverrides): array
|
|
{
|
|
$existing = $template->variables();
|
|
|
|
return collect(MailPlaceholders::extract($template->subject, $template->message))
|
|
->map(function (string $key) use ($existing, $typeOverrides) {
|
|
[$label, $type, $example] = TemplateVariables::DEFINITIONS[$key] ?? [$key, 'string', ''];
|
|
|
|
return [
|
|
'key' => $key,
|
|
'label' => $existing[$key]['label'] ?? $label,
|
|
'type' => $typeOverrides[$key] ?? $type,
|
|
'example' => $existing[$key]['example'] ?? $example,
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|