feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea5697cb81
commit
46aa138eec
@@ -73,6 +73,14 @@ final class EmailScenarioRegistry
|
||||
'label' => 'Hosting đã khởi tạo', 'category' => self::BUSINESS, 'group' => 'SERVICE',
|
||||
'required_variables' => ['domain_name'],
|
||||
],
|
||||
'ip_allocated' => [
|
||||
'label' => 'Cấp phát địa chỉ IP', 'category' => self::BUSINESS, 'group' => 'SERVICE',
|
||||
'required_variables' => ['ip_table'],
|
||||
],
|
||||
'ip_revoked' => [
|
||||
'label' => 'Thu hồi địa chỉ IP', 'category' => self::BUSINESS, 'group' => 'SERVICE',
|
||||
'required_variables' => ['ip_rows'],
|
||||
],
|
||||
'vm_suspended' => [
|
||||
'label' => 'VPS bị tạm ngưng', 'category' => self::BUSINESS, 'group' => 'SERVICE',
|
||||
'required_variables' => ['vm_name'],
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
final class MailAttachments
|
||||
{
|
||||
public const MAX_FILES = 10;
|
||||
|
||||
public const RULE = 'file|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,zip,rar,jpg,jpeg,png,gif,webp|max:20480';
|
||||
|
||||
private const MAX_TOTAL_KB = 25 * 1024;
|
||||
|
||||
private const DISK = 'local';
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'attachments' => 'nullable|array|max:' . self::MAX_FILES,
|
||||
'attachments.*' => self::RULE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, UploadedFile> $uploads
|
||||
* @return array<int, array{disk: string, path: string, name: string}>
|
||||
*/
|
||||
public static function store(array $uploads, string $directory): array
|
||||
{
|
||||
$totalKb = array_sum(array_map(fn (UploadedFile $file) => $file->getSize() / 1024, $uploads));
|
||||
abort_if($totalKb > self::MAX_TOTAL_KB, 422, 'Tổng dung lượng tệp đính kèm không được vượt quá 25MB.');
|
||||
|
||||
return array_map(fn (UploadedFile $file) => [
|
||||
'disk' => self::DISK,
|
||||
'path' => $file->store($directory, self::DISK),
|
||||
'name' => $file->getClientOriginalName(),
|
||||
], $uploads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Http\Services\SignRSA;
|
||||
use App\Models\Setting;
|
||||
use App\Support\Cache\CacheKey;
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
final class MailSettings
|
||||
{
|
||||
private const TTL_SECONDS = 3600;
|
||||
|
||||
private const NON_DELIVERING_MAILERS = ['array', 'log'];
|
||||
|
||||
public static function cacheKey(): string
|
||||
{
|
||||
return CacheKey::build('settings', 'smtp');
|
||||
}
|
||||
|
||||
public static function apply(): void
|
||||
{
|
||||
$settings = self::resolve();
|
||||
if ($settings === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = config('mail');
|
||||
if (!in_array($config['default'], self::NON_DELIVERING_MAILERS, true)) {
|
||||
$config['default'] = $settings['transport'];
|
||||
}
|
||||
$config['from'] = [
|
||||
'address' => $settings['from_address'] ?: ($config['from']['address'] ?? null) ?: 'no-reply@example.com',
|
||||
'name' => $settings['from_name'],
|
||||
];
|
||||
$config['mailers'][$settings['transport']] = array_merge(
|
||||
$config['mailers'][$settings['transport']] ?? [],
|
||||
$settings['mailer'],
|
||||
);
|
||||
|
||||
Config::set('mail', $config);
|
||||
}
|
||||
|
||||
public static function refresh(object $mailer): void
|
||||
{
|
||||
self::apply();
|
||||
if ($mailer instanceof MailManager) {
|
||||
$mailer->forgetMailers();
|
||||
}
|
||||
}
|
||||
|
||||
private static function resolve(): ?array
|
||||
{
|
||||
return Cache::remember(self::cacheKey(), self::TTL_SECONDS, function () {
|
||||
if (!Schema::hasTable('setting')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mail = Setting::where('key', 'smtp')->first();
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
|
||||
if (!$mail || !$setting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mail = json_decode($mail->value);
|
||||
$company = json_decode($setting->value)->company ?? 'Your Company';
|
||||
|
||||
$password = '';
|
||||
if (isset($mail->password)) {
|
||||
$decrypted = (new SignRSA())->DecryptFunc($mail->password);
|
||||
$password = $decrypted ?: $mail->password;
|
||||
}
|
||||
|
||||
$transport = ($mail->transport ?? null) ?: 'smtp';
|
||||
$encryption = $mail->encryption ?? null;
|
||||
|
||||
if ($encryption === 0 || $encryption === '0' || $encryption === 'starttls') {
|
||||
$encryption = 'tls';
|
||||
}
|
||||
|
||||
return [
|
||||
'transport' => $transport,
|
||||
'from_address' => ($mail->from_address ?? null) ?: ($mail->user ?? null),
|
||||
'from_name' => ($mail->from_name ?? null) ?: $company,
|
||||
'mailer' => array_filter([
|
||||
'transport' => $transport,
|
||||
'host' => $mail->host ?? '',
|
||||
'port' => $mail->port ?? 587,
|
||||
'encryption' => $encryption,
|
||||
'username' => $mail->user ?? '',
|
||||
'password' => $password,
|
||||
], fn ($value) => $value !== null),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user