feat(service): đổi sản phẩm của dịch vụ, 2FA bắt buộc, đính kèm campaign mail, thu hồi IP
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6315a392c0
commit
a6b641015e
@@ -6,6 +6,7 @@ use App\Models\Customer;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\IpPoolLog;
|
||||
use App\Services\IpPool\IpAllocationService;
|
||||
use App\Services\IpPool\IpReleaseService;
|
||||
use App\Support\MailAttachments;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -46,6 +47,18 @@ trait ManagesIpPoolAllocation
|
||||
return response()->json(['error' => false, 'message' => "Đã thu hồi {$count} IP và gửi email thông báo"]);
|
||||
}
|
||||
|
||||
public function release(Request $request, IpReleaseService $release): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'ids' => 'required|array|min:1|max:1000',
|
||||
'ids.*' => 'integer',
|
||||
'note' => 'nullable|string|max:2000',
|
||||
]);
|
||||
$count = $release->release($data['ids'], $data['note'] ?? null, $request->user('admin'));
|
||||
|
||||
return response()->json(['error' => false, 'message' => "Đã giải phóng {$count} IP khỏi dịch vụ/VM"]);
|
||||
}
|
||||
|
||||
public function logs(int $id): JsonResponse
|
||||
{
|
||||
$pool = IpPool::withTrashed()->findOrFail($id);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\MailCampaign;
|
||||
use App\Models\Template;
|
||||
use App\Services\Email\MailCampaignRenderer;
|
||||
use App\Support\AutoEmailGate;
|
||||
use App\Support\MailAttachments;
|
||||
use App\Support\MailPlaceholders;
|
||||
use App\Support\MailSettings;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -20,6 +21,8 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class MailCampaignController extends Controller
|
||||
@@ -106,6 +109,7 @@ final class MailCampaignController extends Controller
|
||||
...$this->mapCampaign($campaign),
|
||||
'cc' => $campaign->cc,
|
||||
'bcc' => $campaign->bcc,
|
||||
'attachments' => collect($campaign->attachments)->pluck('name')->all(),
|
||||
'variables' => $campaign->variables ?? [],
|
||||
'logs' => $logs,
|
||||
],
|
||||
@@ -204,6 +208,17 @@ final class MailCampaignController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadAttachment(string $uuid, int $index): StreamedResponse|JsonResponse
|
||||
{
|
||||
$file = MailCampaign::where('uuid', $uuid)->first()?->attachments[$index] ?? null;
|
||||
|
||||
if (!$file || !Storage::disk($file['disk'])->exists($file['path'])) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy tệp đính kèm'], 404);
|
||||
}
|
||||
|
||||
return Storage::disk($file['disk'])->download($file['path'], $file['name']);
|
||||
}
|
||||
|
||||
public function resolveEmails(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
@@ -240,6 +255,7 @@ final class MailCampaignController extends Controller
|
||||
'name' => 'nullable|string|max:255',
|
||||
'cc' => 'nullable|string|max:1000',
|
||||
'bcc' => 'nullable|string|max:1000',
|
||||
...MailAttachments::rules(),
|
||||
], [
|
||||
...self::EMAIL_MESSAGES,
|
||||
'uuids.*.exists' => 'Khách hàng đã chọn không còn tồn tại.',
|
||||
@@ -277,6 +293,12 @@ final class MailCampaignController extends Controller
|
||||
'created_by' => $admin->id ?? null,
|
||||
])->save();
|
||||
|
||||
if (!empty($data['attachments'])) {
|
||||
$campaign->update([
|
||||
'attachments' => MailAttachments::store($data['attachments'], "mail-campaigns/{$campaign->uuid}"),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($recipients as $email => $customerId) {
|
||||
SendMailCampaignRecipient::dispatch($campaign->id, $customerId, $email);
|
||||
}
|
||||
@@ -427,13 +449,21 @@ final class MailCampaignController extends Controller
|
||||
->toBase();
|
||||
}
|
||||
|
||||
private function displaySubject(MailCampaign $campaign): string
|
||||
{
|
||||
$values = array_filter($campaign->variables ?? [], fn ($value) => $value !== null && $value !== '');
|
||||
$subject = MailPlaceholders::fill((string) $campaign->subject, $values, escapeHtml: false);
|
||||
|
||||
return trim(preg_replace('/[\s|\-–,:]+$/u', '', trim($subject)) ?? $subject);
|
||||
}
|
||||
|
||||
private function mapCampaign(MailCampaign $campaign): array
|
||||
{
|
||||
return [
|
||||
'id' => $campaign->id,
|
||||
'uuid' => $campaign->uuid,
|
||||
'name' => $campaign->name,
|
||||
'subject' => $campaign->subject,
|
||||
'subject' => $this->displaySubject($campaign),
|
||||
'template_id' => $campaign->template_id,
|
||||
'template_name' => $campaign->template->name ?? null,
|
||||
'total_recipients' => $campaign->total_recipients,
|
||||
|
||||
@@ -61,6 +61,7 @@ class SendMailCampaignRecipient implements ShouldQueue
|
||||
$rendered['subject'],
|
||||
$rendered['html'],
|
||||
$mailConfig,
|
||||
storedFiles: $campaign->attachments ?? [],
|
||||
)) === null;
|
||||
} catch (\Throwable $e) {
|
||||
$failed = true;
|
||||
|
||||
@@ -25,6 +25,7 @@ class MailCampaign extends Model
|
||||
'subject',
|
||||
'message',
|
||||
'variables',
|
||||
'attachments',
|
||||
'cc',
|
||||
'bcc',
|
||||
'recipient_type',
|
||||
@@ -38,6 +39,7 @@ class MailCampaign extends Model
|
||||
|
||||
protected $casts = [
|
||||
'variables' => 'array',
|
||||
'attachments' => 'array',
|
||||
'total_recipients' => 'integer',
|
||||
'sent_count' => 'integer',
|
||||
'failed_count' => 'integer',
|
||||
|
||||
@@ -63,7 +63,7 @@ trait FiltersBillingRecords
|
||||
}
|
||||
|
||||
if (!in_array($sortBy, $columns, true)) {
|
||||
return $query;
|
||||
return $query->orderByDesc("{$table}.id");
|
||||
}
|
||||
|
||||
return $query->orderBy("{$table}.{$sortBy}", $direction)->orderByDesc("{$table}.id");
|
||||
|
||||
@@ -105,6 +105,7 @@ class ClientAuthService
|
||||
'email_verify_code' => $code,
|
||||
'email_verify_code_expired_at' => now()->addMinutes(5),
|
||||
]);
|
||||
$client->forceFill(['two_factor_enabled' => true])->save();
|
||||
|
||||
$this->sendVerifyEmail($client, $code);
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\IpPool;
|
||||
|
||||
use App\Models\IpPool;
|
||||
use App\Models\IpPoolLog;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\User;
|
||||
use App\Models\VirtualMachines;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class IpReleaseService
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $ids
|
||||
*/
|
||||
public function release(array $ids, ?string $note, User $admin): int
|
||||
{
|
||||
return DB::transaction(function () use ($ids, $note, $admin) {
|
||||
$pools = IpPool::whereIn('id', $ids)->lockForUpdate()->get();
|
||||
$count = 0;
|
||||
|
||||
foreach ($pools as $pool) {
|
||||
$mirrors = ModuleIpAddress::where('ip', $pool->ip_address)->get();
|
||||
$vms = ModuleVmware::where('ip', $pool->ip_address)
|
||||
->orWhereIn('module_ip_address', $mirrors->pluck('id'))
|
||||
->get();
|
||||
|
||||
if ($pool->status === 'free' && $pool->service_id === null && $vms->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$before = $pool->only(['status', 'customer_id', 'customer_name', 'service_name', 'service_id']);
|
||||
$this->detach($pool, $mirrors->pluck('id')->all(), $vms->pluck('id')->all());
|
||||
IpPoolLog::record($pool, IpPoolLog::VM_RELEASED, $before['status'], [
|
||||
'customer_id' => $before['customer_id'],
|
||||
'customer_name' => $before['customer_name'],
|
||||
'service_name' => $before['service_name'],
|
||||
'service_id' => $before['service_id'],
|
||||
'vm_name' => $vms->pluck('name')->filter()->implode(', ') ?: null,
|
||||
'user_id' => $admin->id,
|
||||
'note' => $note ?: 'Giải phóng IP khỏi dịch vụ/VM',
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $mirrorIds
|
||||
* @param array<int, int> $vmIds
|
||||
*/
|
||||
private function detach(IpPool $pool, array $mirrorIds, array $vmIds): void
|
||||
{
|
||||
$pool->update([
|
||||
'status' => 'free',
|
||||
'service_id' => null,
|
||||
'customer_id' => null,
|
||||
'customer_name' => null,
|
||||
'service_name' => null,
|
||||
'contact_email' => null,
|
||||
'contact_cc' => null,
|
||||
'revoked_at' => now()->toDateString(),
|
||||
]);
|
||||
|
||||
ModuleIpAddress::whereIn('id', $mirrorIds)->update(['status' => 0, 'vm_name' => null]);
|
||||
ModuleVmware::whereIn('id', $vmIds)->update(['ip' => null, 'module_ip_address' => null]);
|
||||
VirtualMachines::where('ip_pool_id', $pool->id)
|
||||
->orWhere('ip', $pool->ip_address)
|
||||
->update(['ip_pool_id' => null, 'ip' => null]);
|
||||
}
|
||||
}
|
||||
@@ -45,13 +45,11 @@ class VmCredentialsEmailDataBuilder
|
||||
|
||||
private function billingLabel(?int $months): string
|
||||
{
|
||||
return match ($months) {
|
||||
1 => 'Hang thang',
|
||||
3 => '3 Thang',
|
||||
6 => '6 Thang',
|
||||
12 => 'Hang nam',
|
||||
24 => '2 Nam',
|
||||
default => ($months ?? 0) . ' Thang',
|
||||
return match (true) {
|
||||
$months === 1 => 'Hàng tháng',
|
||||
$months === 12 => 'Hàng năm',
|
||||
$months > 12 && $months % 12 === 0 => ($months / 12) . ' năm',
|
||||
default => ($months ?? 0) . ' tháng',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ClientPassword
|
||||
{
|
||||
public const MIN_LENGTH = 12;
|
||||
|
||||
public static function rule(): Password
|
||||
{
|
||||
return Password::min(self::MIN_LENGTH);
|
||||
}
|
||||
|
||||
public static function minMessage(): string
|
||||
{
|
||||
return 'Mật khẩu tối thiểu ' . self::MIN_LENGTH . ' ký tự';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customers', function ($table) {
|
||||
$table->boolean('two_factor_enabled')->default(true)->change();
|
||||
});
|
||||
|
||||
DB::table('customers')->where('two_factor_enabled', false)->update(['two_factor_enabled' => true]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customers', function ($table) {
|
||||
$table->boolean('two_factor_enabled')->default(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mail_campaigns', function (Blueprint $table) {
|
||||
$table->json('attachments')->nullable()->after('variables');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mail_campaigns', function (Blueprint $table) {
|
||||
$table->dropColumn('attachments');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -115,6 +115,7 @@ Route::prefix('ip-pool')
|
||||
Route::post('allocate', 'allocate')->middleware('perm:module-vcenter.manager');
|
||||
Route::post('revocation/preview', 'revocationPreview')->middleware('perm:module-vcenter.manager');
|
||||
Route::post('revoke', 'revoke')->middleware('perm:module-vcenter.manager');
|
||||
Route::post('release', 'release')->middleware('perm:module-vcenter.manager');
|
||||
Route::get('logs', 'allLogs')->middleware('perm:module-vcenter.view');
|
||||
Route::get('{id}/logs', 'logs')->middleware('perm:module-vcenter.view');
|
||||
Route::get('{id}', 'show')->middleware('perm:module-vcenter.view');
|
||||
|
||||
@@ -193,6 +193,7 @@ Route::prefix('mail-campaigns')
|
||||
Route::post('bulk-delete', 'bulkDelete')->middleware('perm:mail-campaign.manager');
|
||||
Route::get('{uuid}/preview', 'previewCampaign')->middleware('perm:mail-campaign.view');
|
||||
Route::get('{uuid}/logs/{id}', 'showLog')->whereNumber('id')->middleware('perm:mail-campaign.view');
|
||||
Route::get('{uuid}/attachments/{index}', 'downloadAttachment')->whereNumber('index')->middleware('perm:mail-campaign.view');
|
||||
Route::get('{uuid}', 'show')->middleware('perm:mail-campaign.view');
|
||||
Route::post('/', 'store')->middleware('perm:mail-campaign.create');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user