Deploy Ladill Events / deploy (push) Successful in 28s
Attendee and speaker comms use encrypted tenant relay credentials and skip platform-key fallback and Bird wallet double-debit. Co-authored-by: Cursor <cursoragent@cursor.com>
164 lines
6.1 KiB
PHP
164 lines
6.1 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Messaging;
|
||
|
||
use App\Models\MessagingCredential;
|
||
|
||
class MessagingCredentialsService
|
||
{
|
||
public function __construct(
|
||
private CustomerSmsClient $sms,
|
||
private CustomerEmailClient $email,
|
||
) {}
|
||
|
||
public function forOwner(string $ownerRef): MessagingCredential
|
||
{
|
||
return MessagingCredential::query()->firstOrCreate(
|
||
['owner_ref' => $ownerRef],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @return array{ok: bool, error?: string, credential?: MessagingCredential, senders?: list<string>}
|
||
*/
|
||
public function validateAndSaveSms(string $ownerRef, string $apiKey, string $senderId): array
|
||
{
|
||
$apiKey = trim($apiKey);
|
||
$senderId = trim($senderId);
|
||
|
||
if (! str_starts_with($apiKey, 'lsk_sms_live_')) {
|
||
return ['ok' => false, 'error' => 'SMS API keys must start with lsk_sms_live_.'];
|
||
}
|
||
|
||
if ($senderId === '' || strlen($senderId) > 11) {
|
||
return ['ok' => false, 'error' => 'Sender ID must be 1–11 characters.'];
|
||
}
|
||
|
||
$me = $this->sms->me($apiKey);
|
||
if (! ($me['ok'] ?? false)) {
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'sms_status' => MessagingCredential::STATUS_INVALID,
|
||
'sms_last_error' => $me['error'] ?? 'Invalid SMS API key.',
|
||
]);
|
||
|
||
return ['ok' => false, 'error' => $me['error'] ?? 'Invalid SMS API key.', 'credential' => $credential];
|
||
}
|
||
|
||
$senders = $this->sms->senders($apiKey);
|
||
$approved = collect($senders['data']['data'] ?? [])->pluck('sender_id')->map(fn ($id) => (string) $id)->all();
|
||
$defaultSender = (string) ($senders['data']['default_sender'] ?? '');
|
||
|
||
$allowed = array_values(array_unique(array_filter([...$approved, $defaultSender])));
|
||
if ($allowed !== [] && ! in_array($senderId, $allowed, true)) {
|
||
$error = 'Sender ID is not approved for this SMS key. Approved: '.implode(', ', $allowed);
|
||
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'sms_status' => MessagingCredential::STATUS_INVALID,
|
||
'sms_last_error' => $error,
|
||
]);
|
||
|
||
return ['ok' => false, 'error' => $error, 'credential' => $credential, 'senders' => $allowed];
|
||
}
|
||
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'sms_api_key_encrypted' => MessagingCredential::encryptKey($apiKey),
|
||
'sms_api_key_prefix' => substr($apiKey, 0, 16),
|
||
'sms_sender_id' => $senderId,
|
||
'sms_status' => MessagingCredential::STATUS_VALID,
|
||
'sms_validated_at' => now(),
|
||
'sms_last_error' => null,
|
||
]);
|
||
|
||
return ['ok' => true, 'credential' => $credential->fresh(), 'senders' => $allowed];
|
||
}
|
||
|
||
/**
|
||
* @return array{ok: bool, error?: string, credential?: MessagingCredential}
|
||
*/
|
||
public function validateAndSaveBird(string $ownerRef, string $apiKey, string $fromEmail, ?string $fromName): array
|
||
{
|
||
$apiKey = trim($apiKey);
|
||
$fromEmail = strtolower(trim($fromEmail));
|
||
$fromName = trim((string) $fromName);
|
||
|
||
if (! str_starts_with($apiKey, 'lsk_live_') && ! str_starts_with($apiKey, 'lsk_acct_')) {
|
||
return ['ok' => false, 'error' => 'Bird API keys must start with lsk_live_ or lsk_acct_.'];
|
||
}
|
||
|
||
if (! filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) {
|
||
return ['ok' => false, 'error' => 'Enter a valid from email address.'];
|
||
}
|
||
|
||
$me = $this->email->me($apiKey);
|
||
if (! ($me['ok'] ?? false)) {
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'bird_status' => MessagingCredential::STATUS_INVALID,
|
||
'bird_last_error' => $me['error'] ?? 'Invalid Bird API key.',
|
||
]);
|
||
|
||
return ['ok' => false, 'error' => $me['error'] ?? 'Invalid Bird API key.', 'credential' => $credential];
|
||
}
|
||
|
||
$domains = collect($me['data']['verified_domains'] ?? [])->map(fn ($d) => strtolower((string) $d))->all();
|
||
$fromDomain = strtolower((string) (explode('@', $fromEmail, 2)[1] ?? ''));
|
||
if ($domains !== [] && ! in_array($fromDomain, $domains, true)) {
|
||
$error = 'From email must use a verified Bird domain ('.implode(', ', $domains).').';
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'bird_status' => MessagingCredential::STATUS_INVALID,
|
||
'bird_last_error' => $error,
|
||
]);
|
||
|
||
return ['ok' => false, 'error' => $error, 'credential' => $credential];
|
||
}
|
||
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'bird_api_key_encrypted' => MessagingCredential::encryptKey($apiKey),
|
||
'bird_api_key_prefix' => substr($apiKey, 0, 16),
|
||
'bird_from_email' => $fromEmail,
|
||
'bird_from_name' => $fromName !== '' ? $fromName : null,
|
||
'bird_status' => MessagingCredential::STATUS_VALID,
|
||
'bird_validated_at' => now(),
|
||
'bird_last_error' => null,
|
||
]);
|
||
|
||
return ['ok' => true, 'credential' => $credential->fresh()];
|
||
}
|
||
|
||
public function disconnectSms(string $ownerRef): MessagingCredential
|
||
{
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'sms_api_key_encrypted' => null,
|
||
'sms_api_key_prefix' => null,
|
||
'sms_sender_id' => null,
|
||
'sms_status' => null,
|
||
'sms_validated_at' => null,
|
||
'sms_last_error' => null,
|
||
]);
|
||
|
||
return $credential->fresh();
|
||
}
|
||
|
||
public function disconnectBird(string $ownerRef): MessagingCredential
|
||
{
|
||
$credential = $this->forOwner($ownerRef);
|
||
$credential->update([
|
||
'bird_api_key_encrypted' => null,
|
||
'bird_api_key_prefix' => null,
|
||
'bird_from_email' => null,
|
||
'bird_from_name' => null,
|
||
'bird_status' => null,
|
||
'bird_validated_at' => null,
|
||
'bird_last_error' => null,
|
||
]);
|
||
|
||
return $credential->fresh();
|
||
}
|
||
}
|