This commit is contained in:
ghassan committed 2026-08-04 11:26:25 +03:00
1 parent d0834df8df
commit 6dc35134fa
171 files changed
+19241 -49

No files matched your search

@@ -0,0 +1,24 @@
<?php
namespace App\Services\Audit;
use App\Models\AuditLog;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Request;
class AuditLogger
{
public function log(User $actor, string $action, ?Model $auditable = null, array $metadata = []): AuditLog
{
return AuditLog::create([
'user_id' => $actor->id,
'action' => $action,
'auditable_type' => $auditable?->getMorphClass(),
'auditable_id' => $auditable?->getKey(),
'ip_address' => Request::ip(),
'user_agent' => Request::userAgent(),
'metadata' => $metadata,
]);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
/**
* Scaffolded ahead of any actual upload feature (e.g. avatars) so hardening
* isn't an afterthought bolted on later. Not wired to a route yet.
*/
class FileUploadValidator
{
private const ALLOWED_MIME_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
private const MAX_BYTES = 5 * 1024 * 1024; // 5MB
public function validate(UploadedFile $file): bool
{
if ($file->getSize() > self::MAX_BYTES) {
return false;
}
// Sniff actual content type rather than trusting the client-supplied
// extension or Content-Type header.
return in_array($file->getMimeType(), self::ALLOWED_MIME_TYPES, true);
}
/**
* A randomized filename that never trusts the client-supplied name.
*/
public function randomizedFilename(UploadedFile $file): string
{
return Str::uuid().'.'.$file->extension();
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Services\Mqtt;
use PhpMqtt\Client\Facades\MQTT;
use PhpMqtt\Client\MqttClient;
class MqttPublisher
{
public static function publish(string $topic, array $payload, int $qos = MqttClient::QOS_AT_LEAST_ONCE): void
{
MQTT::connection()->publish($topic, json_encode($payload), $qos);
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Services;
use App\Models\TwoFactorRecoveryCode;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use PragmaRX\Google2FA\Google2FA;
class TwoFactorService
{
public function __construct(private readonly Google2FA $google2fa) {}
public function generateSecret(): string
{
return $this->google2fa->generateSecretKey();
}
public function otpAuthUrl(User $user, string $secret): string
{
return $this->google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secret,
);
}
public function verifyCode(string $secret, string $code): bool
{
return $this->google2fa->verifyKey($secret, $code, window: 1);
}
/**
* @return list<string> the plaintext recovery codes (shown once, never stored plaintext)
*/
public function generateRecoveryCodes(User $user): array
{
$user->twoFactorRecoveryCodes()->delete();
$plaintextCodes = [];
foreach (range(1, 10) as $_) {
$code = Str::random(10);
$plaintextCodes[] = $code;
TwoFactorRecoveryCode::create([
'user_id' => $user->id,
'code_hash' => Hash::make($code),
]);
}
return $plaintextCodes;
}
public function redeemRecoveryCode(User $user, string $code): bool
{
$recoveryCode = $user->twoFactorRecoveryCodes()
->whereNull('used_at')
->get()
->first(fn (TwoFactorRecoveryCode $recoveryCode) => Hash::check($code, $recoveryCode->code_hash));
if (! $recoveryCode) {
return false;
}
$recoveryCode->update(['used_at' => now()]);
return true;
}
}