12 Commits

Author SHA1 Message Date
c68531139a Merge pull request 'Add proxy IP headers to Telegram notifications' (#9) from feature/configure-app-name into main
All checks were successful
Tests / Laravel tests (push) Successful in 12m21s
Reviewed-on: #9
2026-06-04 00:31:37 +02:00
da4bdc6bf4 Add proxy IP headers to Telegram notifications
All checks were successful
Tests / Laravel tests (pull_request) Successful in 12m3s
2026-06-04 00:30:44 +02:00
a80945bf0b Merge pull request 'Update page title to use application name from configuration' (#8) from feature/configure-app-name into main
All checks were successful
Tests / Laravel tests (push) Successful in 12m28s
Reviewed-on: #8
2026-06-04 00:10:56 +02:00
03b06632f9 Update page title to use application name from configuration
All checks were successful
Tests / Laravel tests (pull_request) Successful in 11m54s
2026-06-04 00:10:18 +02:00
f7c867e13c Merge pull request 'bugfixes/fix-cursor-on-production' (#7) from bugfixes/fix-cursor-on-production into main
All checks were successful
Tests / Laravel tests (push) Successful in 12m0s
Reviewed-on: #7
2026-06-03 23:54:43 +02:00
2f2b7866c6 Refactor visitor ID creation function for improved readability
All checks were successful
Tests / Laravel tests (pull_request) Successful in 11m55s
2026-06-03 23:51:30 +02:00
f85b596a66 Fix cursor movement functionality on page load 2026-06-03 23:47:36 +02:00
9b8c273c21 Merge pull request 'Add page visit Telegram notifications' (#6) from feature/page-visit-notifications into main
Some checks failed
Tests / Laravel tests (push) Failing after 7m34s
Reviewed-on: #6
2026-06-03 23:27:26 +02:00
bbfc64031f Add page visit Telegram notifications
Some checks failed
Tests / Laravel tests (pull_request) Failing after 6m57s
2026-06-03 23:19:06 +02:00
fdc10a0acb Merge pull request 'featute/readme-enhancement' (#5) from featute/readme-enhancement into main
All checks were successful
Tests / Laravel tests (push) Successful in 12m1s
Reviewed-on: #5
2026-06-03 23:00:57 +02:00
38943743aa Refine README content
All checks were successful
Tests / Laravel tests (pull_request) Successful in 2m26s
2026-06-03 22:59:54 +02:00
866118a86f Update project README 2026-06-03 22:59:54 +02:00
12 changed files with 607 additions and 106 deletions

123
README.md
View File

@@ -1,79 +1,106 @@
# Interactieve Laravel CV Applicatie
# CV Roberto
Deze Laravel-applicatie is gebouwd als mijn interactieve en dynamische cv. In plaats van een statisch pdf-bestand, kun je hier real-time mijn:
Dit project is mijn interactieve CV en tegelijk een voorbeeld van hoe ik een Laravel-app opzet.
- **Vaardigheden** (met beoordeling en iconen)
- **Werkervaring**
- **Opleidingen**
- **Persoonlijke gegevens**
- **Tags & kernkwaliteiten**
Live: [cv.robert.ooo](https://cv.robert.ooo)
zien — inclusief slimme automatisering, logging en Telegram-notificaties voor recruiterinteracties.
## Wat zit erin
---
- Publieke CV-pagina met werkervaring, opleidingen, skills en personalia.
- Adminomgeving om CV-data te beheren.
- Afbeeldingsuploads via Spatie Media Library.
- Contactformulier met queue job voor Telegram-notificaties.
- Klikbare verborgen personalia, zodat bots de waarde niet direct in de HTML zien.
- Feature tests voor de belangrijkste controllerflows.
- CI-checks voor tests, Larastan en formatting.
## 🧰 Techniek & Stack
## Stack
- **Framework:** Laravel 12
- **Frontend:** Tailwind CSS, Blade
- **DevOps-integraties:** Telegram alerts, Healthchecks, custom logging
- **CI/CD-ready:** Ondersteuning voor deploy hooks en jobs
- **Overige tools:** Docker, Nginx, Git, Redis, Cron, Promtail, Grafana
- Laravel 12
- PHP 8.2+
- Blade
- Tailwind CSS
- Pest
- Larastan level 7
- Laravel Pint
- Blade Formatter
- Docker / Laravel Sail
---
## Kwaliteitschecks
## ⚙️ Installatie
1. **Clone deze repo:**
Deze checks horen groen te zijn voordat een merge logisch is:
```bash
git clone https://github.com/roberto-guagliardo/cv-app.git
cd cv-app
composer test
composer analyse
composer format:check
npm run format:check
npm run build
```
2. **Installeer dependencies:**
In de workflow worden dezelfde checks afgedwongen:
```bash
composer install
npm install && npm run build
```
- PHPUnit/Pest feature tests
- Larastan op level 7
- PHP formatting via Pint
- Blade formatting via Blade Formatter
3. **Maak .env aan en configureer je database, storage en Telegram:**
Skipped tests zijn alleen acceptabel als dat bewust is, zoals disabled registratieflows. Larastan en formatting moeten gewoon groen zijn.
## Lokaal draaien
```bash
cp .env.example .env
composer install
npm ci
php artisan key:generate
```
4. **Voer migraties & seeders uit:**
```bash
php artisan migrate --seed
```
5. **Geniet van de app:**
```bash
npm run build
php artisan serve
```
---
Met Sail:
## 🌐 Live Demo
```bash
cp .env.example .env
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan migrate --seed
npm ci
npm run build
```
Wil je zien hoe het eruitziet? Bekijk mijn live cv op:
Voor de admin login kun je in `.env` deze waardes zetten en daarna opnieuw seeden:
➡️ [cv.robert.ooo](https://cv.robert.ooo)
```env
ADMIN_NAME="Admin"
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=changeme123
```
---
## Development
## 📊 Bijdragen
PHP formatter:
Deze applicatie is persoonlijk en niet bedoeld voor publieke bijdragen, maar voel je vrij om de structuur of ideeën te gebruiken voor je eigen showcase-app.
```bash
composer format
composer format:check
```
---
Blade formatter:
## 💌 Contact
```bash
npm run format
npm run format:check
```
Wil je mij benaderen voor een samenwerking of opdracht?
Gebruik het contactformulier op de site of stuur direct een bericht via [Telegram](https://t.me/robertguagliardo).
Tests en analyse:
```bash
composer test
composer analyse
```
## Contact
Gebruik het contactformulier op de site of stuur me een bericht via [Telegram](https://t.me/robertguagliardo).

View File

@@ -2,7 +2,9 @@
namespace App\Http\Controllers;
use App\Http\Requests\PageVisitRequest;
use App\Jobs\NotifyTelegramAboutContactMessage;
use App\Jobs\NotifyTelegramAboutPageVisit;
use App\Jobs\NotifyTelegramAboutPersonaliaClick;
use App\Models\Education;
use App\Models\Personalia;
@@ -30,7 +32,8 @@ class FrontendController extends Controller
NotifyTelegramAboutPersonaliaClick::dispatch(
$personalia,
request()->ip(),
request()->userAgent()
request()->userAgent(),
$this->ipHeaders(request())
);
return response()->json([
@@ -53,9 +56,36 @@ class FrontendController extends Controller
$request->ip(),
$request->userAgent(),
$validated['email'] ?? null,
$validated['phone'] ?? null
$validated['phone'] ?? null,
$this->ipHeaders($request)
);
return response()->json(['status' => 'success']);
}
public function pageVisit(PageVisitRequest $request): JsonResponse
{
NotifyTelegramAboutPageVisit::dispatch(
$request->validated(),
$request->ip(),
$request->userAgent(),
$request->header('Accept-Language'),
$this->ipHeaders($request)
);
return response()->json(['status' => 'queued']);
}
/**
* @return array<string, string|null>
*/
protected function ipHeaders(Request $request): array
{
return [
'CF-Connecting-IP' => $request->header('CF-Connecting-IP'),
'X-Forwarded-For' => $request->header('X-Forwarded-For'),
'X-Real-IP' => $request->header('X-Real-IP'),
'Forwarded' => $request->header('Forwarded'),
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PageVisitRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
'visitor_id' => ['required', 'string', 'max:100'],
'url' => ['required', 'url', 'max:2048'],
'path' => ['nullable', 'string', 'max:2048'],
'title' => ['nullable', 'string', 'max:255'],
'referrer' => ['nullable', 'string', 'max:2048'],
'language' => ['nullable', 'string', 'max:50'],
'timezone' => ['nullable', 'string', 'max:100'],
'screen' => ['nullable', 'array'],
'screen.width' => ['nullable', 'integer', 'min:0', 'max:100000'],
'screen.height' => ['nullable', 'integer', 'min:0', 'max:100000'],
'viewport' => ['nullable', 'array'],
'viewport.width' => ['nullable', 'integer', 'min:0', 'max:100000'],
'viewport.height' => ['nullable', 'integer', 'min:0', 'max:100000'],
'device_pixel_ratio' => ['nullable', 'numeric', 'min:0', 'max:100'],
'color_depth' => ['nullable', 'integer', 'min:0', 'max:1000'],
'platform' => ['nullable', 'string', 'max:255'],
'vendor' => ['nullable', 'string', 'max:255'],
'hardware_concurrency' => ['nullable', 'integer', 'min:0', 'max:1024'],
'device_memory' => ['nullable', 'numeric', 'min:0', 'max:1024'],
'cookies_enabled' => ['nullable', 'boolean'],
'online' => ['nullable', 'boolean'],
];
}
}

View File

@@ -2,12 +2,12 @@
namespace App\Jobs;
use App\Services\TelegramNotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class NotifyTelegramAboutContactMessage implements ShouldQueue
{
@@ -25,8 +25,18 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
protected ?string $phone;
public function __construct(string $name, string $message, string $ip, string $userAgent, ?string $email = null, ?string $phone = null)
{
/**
* @param array<string, string|null> $ipHeaders
*/
public function __construct(
string $name,
string $message,
string $ip,
string $userAgent,
?string $email = null,
?string $phone = null,
protected array $ipHeaders = []
) {
$this->name = $name;
$this->message = $message;
$this->ip = $ip;
@@ -35,30 +45,16 @@ class NotifyTelegramAboutContactMessage implements ShouldQueue
$this->phone = $phone;
}
public function handle(): void
public function handle(TelegramNotificationService $telegram): void
{
$email = $this->email ?? '';
$phone = $this->phone ?? '';
$text = <<<TEXT
📩 *Nieuw contactbericht ontvangen*
👤 Naam: *{$this->name}*
💬 Bericht:
{$this->message}
📧 Email: {$email}
📱 Telefoon: {$phone}
🌐 IP: {$this->ip}
🧭 User Agent: `{$this->userAgent}`
🕒 Tijdstip: *{now()->format('d-m-Y H:i')}*
TEXT;
Http::post('https://api.telegram.org/bot'.config('services.telegram.bot_token').'/sendMessage', [
'chat_id' => config('services.telegram.chat_id'),
'text' => $text,
'parse_mode' => 'Markdown',
]);
$telegram->notifyContactMessage(
$this->name,
$this->message,
$this->ip,
$this->userAgent,
$this->email,
$this->phone,
$this->ipHeaders
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Jobs;
use App\Services\TelegramNotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NotifyTelegramAboutPageVisit implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @param array<string, mixed> $visit
* @param array<string, string|null> $ipHeaders
*/
public function __construct(
protected array $visit,
protected ?string $ip,
protected ?string $userAgent,
protected ?string $acceptLanguage,
protected array $ipHeaders = []
) {}
public function handle(TelegramNotificationService $telegram): void
{
$telegram->notifyPageVisit($this->visit, $this->ip, $this->userAgent, $this->acceptLanguage, $this->ipHeaders);
}
}

View File

@@ -3,12 +3,12 @@
namespace App\Jobs;
use App\Models\Personalia;
use App\Services\TelegramNotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class NotifyTelegramAboutPersonaliaClick implements ShouldQueue
{
@@ -20,32 +20,18 @@ class NotifyTelegramAboutPersonaliaClick implements ShouldQueue
protected ?string $userAgent;
public function __construct(Personalia $personalia, ?string $ip, ?string $userAgent)
/**
* @param array<string, string|null> $ipHeaders
*/
public function __construct(Personalia $personalia, ?string $ip, ?string $userAgent, protected array $ipHeaders = [])
{
$this->personalia = $personalia;
$this->ip = $ip;
$this->userAgent = $userAgent;
}
public function handle(): void
public function handle(TelegramNotificationService $telegram): void
{
$ip = $this->ip ?? '';
$userAgent = $this->userAgent ?? '';
$message = <<<TEXT
👤 *Persoonlijke gegevens bekeken*
Naam: {$this->personalia->value}
IP: {$ip}
User Agent: `{$userAgent}`
📅 Tijdstip: *{now()->format('d-m-Y H:i')}*
TEXT;
Http::post('https://api.telegram.org/bot'.config('services.telegram.bot_token').'/sendMessage', [
'chat_id' => config('services.telegram.chat_id'),
'text' => $message,
'parse_mode' => 'Markdown',
]);
$telegram->notifyPersonaliaClick($this->personalia, $this->ip, $this->userAgent, $this->ipHeaders);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Services;
use App\Models\Personalia;
use Illuminate\Support\Facades\Http;
class TelegramNotificationService
{
/**
* @param array<string, string|null> $ipHeaders
*/
public function notifyContactMessage(
string $name,
string $message,
string $ip,
string $userAgent,
?string $email = null,
?string $phone = null,
array $ipHeaders = []
): void {
$email ??= '-';
$phone ??= '-';
$ipHeaderText = $this->formatIpHeaders($ipHeaders);
$text = <<<TEXT
Nieuw contactbericht ontvangen
Naam: {$name}
Email: {$email}
Telefoon: {$phone}
IP: {$ip}
{$ipHeaderText}
User Agent: {$userAgent}
Bericht:
{$message}
Tijdstip: {now()->format('d-m-Y H:i')}
TEXT;
$this->send($text);
}
/**
* @param array<string, string|null> $ipHeaders
*/
public function notifyPersonaliaClick(Personalia $personalia, ?string $ip, ?string $userAgent, array $ipHeaders = []): void
{
$ip ??= '-';
$userAgent ??= '-';
$ipHeaderText = $this->formatIpHeaders($ipHeaders);
$message = <<<TEXT
Persoonlijke gegevens bekeken
Naam: {$personalia->value}
IP: {$ip}
{$ipHeaderText}
User Agent: {$userAgent}
Tijdstip: {now()->format('d-m-Y H:i')}
TEXT;
$this->send($message);
}
/**
* @param array<string, mixed> $visit
* @param array<string, string|null> $ipHeaders
*/
public function notifyPageVisit(array $visit, ?string $ip, ?string $userAgent, ?string $acceptLanguage, array $ipHeaders = []): void
{
$screen = $this->formatDimensions($visit['screen'] ?? null);
$viewport = $this->formatDimensions($visit['viewport'] ?? null);
$ipHeaderText = $this->formatIpHeaders($ipHeaders);
$message = <<<TEXT
Pagina bezocht
URL: {$this->value($visit['url'] ?? null)}
Pad: {$this->value($visit['path'] ?? null)}
Titel: {$this->value($visit['title'] ?? null)}
Referrer: {$this->value($visit['referrer'] ?? null)}
Visitor ID: {$this->value($visit['visitor_id'] ?? null)}
IP: {$this->value($ip)}
{$ipHeaderText}
User Agent: {$this->value($userAgent)}
Accept-Language: {$this->value($acceptLanguage)}
Browser taal: {$this->value($visit['language'] ?? null)}
Timezone: {$this->value($visit['timezone'] ?? null)}
Platform: {$this->value($visit['platform'] ?? null)}
Vendor: {$this->value($visit['vendor'] ?? null)}
Scherm: {$screen}
Viewport: {$viewport}
DPR: {$this->value($visit['device_pixel_ratio'] ?? null)}
Color depth: {$this->value($visit['color_depth'] ?? null)}
CPU cores: {$this->value($visit['hardware_concurrency'] ?? null)}
Device memory: {$this->value($visit['device_memory'] ?? null)}
Cookies: {$this->boolean($visit['cookies_enabled'] ?? null)}
Online: {$this->boolean($visit['online'] ?? null)}
Tijdstip: {now()->format('d-m-Y H:i')}
TEXT;
$this->send($message);
}
protected function send(string $text): void
{
$botToken = config('services.telegram.bot_token');
$chatId = config('services.telegram.chat_id');
if (! is_string($botToken) || $botToken === '' || ! is_string($chatId) || $chatId === '') {
return;
}
Http::post("https://api.telegram.org/bot{$botToken}/sendMessage", [
'chat_id' => $chatId,
'text' => $text,
]);
}
/**
* @param array<string, string|null> $ipHeaders
*/
protected function formatIpHeaders(array $ipHeaders): string
{
$headers = [
'CF-Connecting-IP',
'X-Forwarded-For',
'X-Real-IP',
'Forwarded',
];
return collect($headers)
->map(fn (string $header): string => "{$header}: {$this->value($ipHeaders[$header] ?? null)}")
->implode("\n");
}
protected function value(mixed $value): string
{
if (is_bool($value)) {
return $this->boolean($value);
}
if (is_scalar($value)) {
$value = trim((string) $value);
return $value !== '' ? $value : '-';
}
return '-';
}
protected function boolean(mixed $value): string
{
return match ($value) {
true => 'ja',
false => 'nee',
default => '-',
};
}
protected function formatDimensions(mixed $dimensions): string
{
if (! is_array($dimensions)) {
return '-';
}
$width = $this->value($dimensions['width'] ?? null);
$height = $this->value($dimensions['height'] ?? null);
return "{$width}x{$height}";
}
}

View File

@@ -15,14 +15,21 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
const cursor = document.getElementById('custom-cursor');
window.addEventListener('load', () => {
const cursor = document.getElementById('custom-cursor');
const gsapInstance = window.gsap;
document.addEventListener('mousemove', (e) => {
gsap.to(cursor, {
if (!cursor || !gsapInstance) {
return;
}
document.addEventListener('mousemove', (e) => {
gsapInstance.to(cursor, {
duration: 0.2,
x: e.clientX + 20,
y: e.clientY - 15,
ease: 'power2.out'
});
});
});

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Laravel</title>
<title>{{ config('app.name') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
@@ -170,6 +170,63 @@
});
</script>
<script>
(() => {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (!csrfToken || !window.fetch) {
return;
}
const visitorKey = 'cv_visitor_id';
const createVisitorId = () => window.crypto?.randomUUID?.() ??
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
let visitorId = createVisitorId();
try {
visitorId = window.localStorage.getItem(visitorKey) || visitorId;
window.localStorage.setItem(visitorKey, visitorId);
} catch {
// Tracking should never break the page when storage is blocked.
}
window.fetch('{{ route('page-visits.store') }}', {
method: 'POST',
keepalive: true,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify({
visitor_id: visitorId,
url: window.location.href,
path: window.location.pathname,
title: document.title,
referrer: document.referrer,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: {
width: window.screen?.width,
height: window.screen?.height,
},
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
device_pixel_ratio: window.devicePixelRatio,
color_depth: window.screen?.colorDepth,
platform: navigator.platform,
vendor: navigator.vendor,
hardware_concurrency: navigator.hardwareConcurrency,
device_memory: navigator.deviceMemory,
cookies_enabled: navigator.cookieEnabled,
online: navigator.onLine,
}),
}).catch(() => {});
})();
</script>
@stack('scripts')
</body>

View File

@@ -13,6 +13,7 @@ Route::get('/dashboard', function (): \Illuminate\View\View {
})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/getPersonalia/{personalia}', [FrontendController::class, 'getPersonalia'])->name('personalia');
Route::post('/contact', [FrontendController::class, 'message'])->name('contact');
Route::post('/page-visits', [FrontendController::class, 'pageVisit'])->name('page-visits.store');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');

View File

@@ -1,6 +1,7 @@
<?php
use App\Jobs\NotifyTelegramAboutContactMessage;
use App\Jobs\NotifyTelegramAboutPageVisit;
use App\Jobs\NotifyTelegramAboutPersonaliaClick;
use App\Models\Education;
use App\Models\Personalia;
@@ -99,3 +100,55 @@ test('a contact message requires a name and message', function () {
Queue::assertNotPushed(NotifyTelegramAboutContactMessage::class);
});
test('a page visit can be submitted and is queued for notification', function () {
Queue::fake();
$this->withHeaders([
'User-Agent' => 'Pest Browser',
'Accept-Language' => 'nl-NL,nl;q=0.9',
])->postJson(route('page-visits.store'), [
'visitor_id' => 'visitor-123',
'url' => 'https://cv.robert.ooo/',
'path' => '/',
'title' => 'CV Roberto',
'referrer' => 'https://example.com',
'language' => 'nl-NL',
'timezone' => 'Europe/Amsterdam',
'screen' => [
'width' => 1920,
'height' => 1080,
],
'viewport' => [
'width' => 1440,
'height' => 900,
],
'device_pixel_ratio' => 1,
'color_depth' => 24,
'platform' => 'Linux x86_64',
'vendor' => 'Google Inc.',
'hardware_concurrency' => 8,
'device_memory' => 8,
'cookies_enabled' => true,
'online' => true,
])
->assertOk()
->assertJson([
'status' => 'queued',
]);
Queue::assertPushed(NotifyTelegramAboutPageVisit::class);
});
test('a page visit requires a visitor id and valid url', function () {
Queue::fake();
$this->postJson(route('page-visits.store'), [
'visitor_id' => '',
'url' => 'not-a-url',
])
->assertUnprocessable()
->assertJsonValidationErrors(['visitor_id', 'url']);
Queue::assertNotPushed(NotifyTelegramAboutPageVisit::class);
});

View File

@@ -0,0 +1,92 @@
<?php
use App\Models\Personalia;
use App\Services\TelegramNotificationService;
use Illuminate\Support\Facades\Http;
test('telegram service sends a contact message notification', function () {
config([
'services.telegram.bot_token' => 'telegram-token',
'services.telegram.chat_id' => 'telegram-chat',
]);
Http::fake();
app(TelegramNotificationService::class)->notifyContactMessage(
name: 'Roberto',
message: 'Hoi, ik wil graag contact opnemen.',
ip: '127.0.0.1',
userAgent: 'Pest Browser',
email: 'roberto@example.com',
phone: '+31612345678',
ipHeaders: [
'CF-Connecting-IP' => '203.0.113.10',
'X-Forwarded-For' => '203.0.113.10, 198.51.100.20',
],
);
Http::assertSent(fn ($request) => $request->url() === 'https://api.telegram.org/bottelegram-token/sendMessage'
&& $request['chat_id'] === 'telegram-chat'
&& str_contains($request['text'], 'Nieuw contactbericht ontvangen')
&& str_contains($request['text'], 'roberto@example.com')
&& str_contains($request['text'], 'CF-Connecting-IP: 203.0.113.10')
&& str_contains($request['text'], 'X-Forwarded-For: 203.0.113.10, 198.51.100.20'));
});
test('telegram service sends a personalia click notification', function () {
config([
'services.telegram.bot_token' => 'telegram-token',
'services.telegram.chat_id' => 'telegram-chat',
]);
Http::fake();
$personalia = Personalia::factory()->create([
'value' => 'roberto@example.com',
]);
app(TelegramNotificationService::class)->notifyPersonaliaClick($personalia, '127.0.0.1', 'Pest Browser', [
'CF-Connecting-IP' => '203.0.113.10',
]);
Http::assertSent(fn ($request) => str_contains($request['text'], 'Persoonlijke gegevens bekeken')
&& str_contains($request['text'], 'roberto@example.com')
&& str_contains($request['text'], 'CF-Connecting-IP: 203.0.113.10'));
});
test('telegram service sends a page visit notification', function () {
config([
'services.telegram.bot_token' => 'telegram-token',
'services.telegram.chat_id' => 'telegram-chat',
]);
Http::fake();
app(TelegramNotificationService::class)->notifyPageVisit([
'visitor_id' => 'visitor-123',
'url' => 'https://cv.robert.ooo/',
'path' => '/',
'title' => 'CV Roberto',
'referrer' => 'https://example.com',
'language' => 'nl-NL',
'timezone' => 'Europe/Amsterdam',
'screen' => [
'width' => 1920,
'height' => 1080,
],
'viewport' => [
'width' => 1440,
'height' => 900,
],
], '127.0.0.1', 'Pest Browser', 'nl-NL,nl;q=0.9', [
'CF-Connecting-IP' => '203.0.113.10',
'X-Real-IP' => '198.51.100.30',
]);
Http::assertSent(fn ($request) => str_contains($request['text'], 'Pagina bezocht')
&& str_contains($request['text'], 'Visitor ID: visitor-123')
&& str_contains($request['text'], 'Scherm: 1920x1080')
&& str_contains($request['text'], 'Viewport: 1440x900')
&& str_contains($request['text'], 'CF-Connecting-IP: 203.0.113.10')
&& str_contains($request['text'], 'X-Real-IP: 198.51.100.30'));
});