1 Commits

Author SHA1 Message Date
Bartłomiej Janczak
bdef0b6687 feat: add api_enabled on/off toggle in settings + update README for v1.5 2026-06-02 11:10:10 +02:00
6 changed files with 279 additions and 1 deletions

254
README.md
View File

@@ -50,6 +50,9 @@ A professional, high-performance **Short URL Manager** plugin for [Filament v5](
- 🎯 **Smart Link Targeting** — Route visitors to different destination URLs based on their device type, country, or via weighted A/B split rotation.
- 🛡️ **Rate Limiting / Bot Protection** — Configurable per-IP rate limits on redirects with automatic `429 Too Many Requests` responses.
- 📊 **Daily Stats Aggregation & Pruning** — Automatic daily summarization of raw visit logs into compact daily stats tables. Configurable retention window prevents unbounded database growth at scale.
- 🎯 **Social Retargeting Pixels** *(new in v1.5)* — Inject Meta Pixel, Google Tag, and LinkedIn Insight tracking scripts client-side via a premium glassmorphic interstitial page. Build remarketing audiences even when redirecting to external domains.
- 🔌 **Developer REST API** *(new in v1.5)* — Full REST API (`GET`, `POST`, `DELETE`) for external integrations. Secured with API Key authentication managed via the Settings panel.
- 📡 **Webhooks** *(new in v1.5)* — Real-time HTTP POST event notifications on every click, link creation, or expiration. Configure per-link or globally, dispatched asynchronously via the queue.
---
@@ -362,6 +365,246 @@ When Redis is not available and counter buffering is enabled, the `IncrementVisi
---
## Social Retargeting Pixels (new in v1.5.0)
The **Marketing & API** tab in the short URL form lets you attach client-side tracking pixels to any link. When a visitor clicks a link that has pixels configured, instead of an instant 302 redirect the plugin serves a lightweight HTML interstitial page (styled with glassmorphism) for ~250 ms. During that time the browser executes the pixel scripts — capturing cookies and building ad audiences — then the visitor is forwarded to the destination URL.
This unlocks remarketing to people who clicked your links **even when redirecting to external domains** (e.g. booking.com, amazon.com) where you cannot install your own tracking code.
### Supported Pixel Providers
| Field | Provider | Script loaded |
|---|---|---|
| **Meta Pixel ID** | Meta / Facebook Ads | `fbevents.js` via `fbq('init', ...)` |
| **Google Tag / GA4 ID** | Google Ads, GA4 | `gtag.js` via Google Tag Manager |
| **LinkedIn Partner ID** | LinkedIn Insight Tag | `insight.min.js` via LinkedIn |
> **Note:** These pixels fire **client-side** in the visitor's browser — completely separate from the server-side GA4 Measurement Protocol integration. Both systems work in parallel and do not interfere with each other.
### How to use
1. Open any short URL for editing.
2. Navigate to the **Marketing & API** tab.
3. Enter your pixel IDs in the **Retargeting Pixels** section.
4. Save. Done — every click will now trigger the configured tracking scripts.
```php
// Programmatically via model attributes
$shortUrl->update([
'pixel_meta_id' => '1234567890',
'pixel_google_id' => 'G-XXXXXXXXXX',
'pixel_linkedin_id' => '1234567',
]);
```
> **Privacy/GDPR Note:** You are responsible for ensuring that firing these pixels complies with applicable privacy regulations and your cookie consent mechanism.
---
## Developer REST API (new in v1.5.0)
The plugin exposes a REST API that allows external systems (CRMs, Zapier, Make, custom integrations) to manage short URLs programmatically.
### Enabling the API
The API is **disabled by default**. Enable it in **Settings → API & Webhooks → REST API Access → Enable Developer REST API**.
### Authentication
All API endpoints are protected. Include your API key in every request using one of these headers:
```
X-Api-Key: sh_key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
or
```
Authorization: Bearer sh_key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
**Managing API Keys:** Go to **Settings → API & Webhooks → Developer API Keys** and add named keys. Each key can be individually activated or deactivated without deleting it.
> If the API is disabled globally, all endpoints return `503 Service Unavailable` regardless of the key provided.
### Endpoints
#### `GET /api/short-url/links`
List all short URLs (paginated, 30 per page).
```bash
curl https://yourdomain.com/api/short-url/links \
-H "X-Api-Key: sh_key_your_key_here"
```
**Response:**
```json
{
"data": [
{
"id": 1,
"destination_url": "https://example.com",
"url_key": "abc123",
"short_url": "https://yourdomain.com/s/abc123",
"is_enabled": true,
"redirect_status_code": 302,
"total_visits": 47,
"unique_visits": 31,
"max_visits": null,
"activated_at": null,
"expires_at": null,
"pixel_meta_id": null,
"pixel_google_id": null,
"pixel_linkedin_id": null,
"webhook_url": null,
"notes": null,
"created_at": "2026-06-01T12:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 30,
"total": 72
}
}
```
#### `POST /api/short-url/links`
Create a new short URL.
```bash
curl -X POST https://yourdomain.com/api/short-url/links \
-H "X-Api-Key: sh_key_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://example.com/product",
"url_key": "promo26",
"notes": "Summer campaign",
"single_use": false,
"max_visits": 1000,
"pixel_meta_id": "1234567890",
"webhook_url": "https://api.mycrm.com/clicks"
}'
```
**Accepted fields:**
| Field | Type | Required | Description |
|---|---|---|---|
| `destination_url` | string (URL) | ✅ | Target URL |
| `url_key` | string | ❌ | Custom slug (auto-generated if omitted) |
| `notes` | string | ❌ | Internal notes |
| `is_enabled` | boolean | ❌ | Active status (default: `true`) |
| `redirect_status_code` | integer (301/302) | ❌ | HTTP redirect code |
| `single_use` | boolean | ❌ | Expire after first click |
| `forward_query_params` | boolean | ❌ | Forward query string to destination |
| `max_visits` | integer | ❌ | Click limit before expiry |
| `expiration_redirect_url` | string (URL) | ❌ | Fallback URL on expiry |
| `activated_at` | datetime | ❌ | Activation timestamp |
| `expires_at` | datetime | ❌ | Expiration timestamp |
| `pixel_meta_id` | string | ❌ | Meta Pixel ID |
| `pixel_google_id` | string | ❌ | Google Tag / GA4 ID |
| `pixel_linkedin_id` | string | ❌ | LinkedIn Partner ID |
| `webhook_url` | string (URL) | ❌ | Per-link webhook endpoint |
**Response:** `201 Created` with the created link object.
#### `DELETE /api/short-url/links/{id}`
Permanently delete a short URL by its ID.
```bash
curl -X DELETE https://yourdomain.com/api/short-url/links/42 \
-H "X-Api-Key: sh_key_your_key_here"
```
**Response:** `200 OK`
```json
{ "message": "Short URL deleted successfully." }
```
### Error Responses
| HTTP Code | Reason |
|---|---|
| `401 Unauthorized` | Missing or invalid API key |
| `422 Unprocessable Entity` | Validation error (see `errors` field in response) |
| `503 Service Unavailable` | REST API is disabled in Settings |
---
## Webhooks (new in v1.5.0)
Webhooks allow external systems to receive real-time HTTP POST notifications when events occur on your short URLs. Payloads are dispatched **asynchronously** via the Laravel Queue — redirects are never blocked.
### Configuration
Webhooks can be configured at two levels:
**1. Per-link webhook** — Set a `Dedicated Webhook URL` in the **Marketing & API** tab of a specific short URL. Fires for every click on that link.
**2. Global webhook** — Set a **Global Webhook URL** in **Settings → API & Webhooks → Global Webhook Configuration**. Fires for all links that don't have their own webhook URL, for the event types you select.
### Monitored Events
| Event key | When fired |
|---|---|
| `visited` | A visitor clicks the short URL |
| `created` | A new short URL is created via the REST API |
| `expired` | A link reaches its expiration date |
| `limit_reached` | A link reaches its `max_visits` click limit |
Select which events to monitor in **Settings → API & Webhooks → Monitored Webhook Events**.
### Payload Format
All webhook requests are HTTP POST with `Content-Type: application/json` and the following payload structure:
```json
{
"event": "visited",
"timestamp": "2026-06-02T10:00:00+00:00",
"short_url": {
"id": 1,
"destination_url": "https://example.com",
"url_key": "abc123",
"short_url": "https://yourdomain.com/s/abc123",
"total_visits": 48,
"unique_visits": 32
},
"visit": {
"id": 101,
"visited_at": "2026-06-02T10:00:00+00:00",
"device_type": "desktop",
"browser": "Chrome",
"browser_version": "124.0",
"operating_system": "Windows",
"operating_system_version": "10",
"country": "Poland",
"country_code": "PL",
"city": "Warsaw",
"referer_url": "https://linkedin.com",
"referer_host": "linkedin.com",
"utm_source": "linkedin",
"utm_medium": "social",
"utm_campaign": "spring_sale",
"utm_term": null,
"utm_content": null
}
}
```
### Retry Policy
If the webhook endpoint returns a non-2xx response or is unreachable, the `SendWebhookJob` will automatically retry up to **3 times** with a **10-second backoff** between attempts. Failed jobs land in your queue's failed jobs table after exhausting retries.
### Webhook Priority (per-link vs global)
The resolution order is:
1. If the short URL has its own `webhook_url` → use it (always fires, regardless of global event selection).
2. If no per-link URL is set, and a **Global Webhook URL** is configured, and the event type is in the selected **Monitored Events** list → use the global URL.
3. Otherwise no webhook is fired.
---
## Configuration Reference (.env)
You can also pre-configure all parameters via your `.env` file:
@@ -520,7 +763,16 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**:
## Changelog
### v1.4.0 (Latest)
### v1.5.1
- **REST API On/Off Toggle** — Enable or disable the entire developer REST API from Settings → API & Webhooks without touching code. Returns `503 Service Unavailable` when disabled. Toggle takes effect immediately without route cache clearing.
### v1.5.0
- **Social Retargeting Pixels** — Attach Meta Pixel, Google Tag (GA4/Ads), and LinkedIn Insight Tag to any short URL. A premium glassmorphic interstitial page executes pixel scripts in the visitor's browser before forwarding them to the destination. Enables building remarketing audiences even on external domains.
- **Developer REST API** — Full `GET /api/short-url/links`, `POST /api/short-url/links`, and `DELETE /api/short-url/links/{id}` endpoints with API Key authentication (managed via Settings UI). Supports creating links with all available attributes including pixels and webhooks.
- **Webhook System** — Real-time HTTP POST notifications on `visited`, `created`, `expired`, and `limit_reached` events. Configurable per-link or globally. Dispatched asynchronously via `SendWebhookJob` with 3-attempt retry policy and 10-second backoff.
- **Settings: API & Webhooks Tab** — New settings tab to manage global webhook URL, monitored event types, and developer API key management with name labels and per-key activation toggles.
### v1.4.0
- **Validity Date Ranges (From-To)** — Set activation dates (`activated_at` and `expires_at`) to control exactly when a short link is active.
- **Custom Visit Limit Counters** — Define a custom maximum visit limit (`max_visits`) after which a link automatically expires (e.g., active for 3 hits).
- **Custom Expiration Fallbacks** — Redirect expired/inactive visitors to a custom `expiration_redirect_url` rather than showing a static 410 Gone error page.

View File

@@ -352,6 +352,9 @@ return [
// Settings tab additions
'settings_tab_developer' => 'API & Webhooks',
'settings_section_rest_api' => 'REST API Access',
'settings_api_enabled' => 'Enable Developer REST API',
'settings_api_enabled_helper' => 'Allow external systems to create, list, and delete short URLs via the REST API. Disable to block all /api/short-url/* endpoints with a 503 response.',
'settings_section_global_webhook' => 'Global Webhook Configuration',
'settings_global_webhook_url' => 'Global Webhook URL',
'settings_global_webhook_url_helper' => 'Destination URL to dispatch event payloads asynchronously in the background for all links.',

View File

@@ -353,6 +353,9 @@ return [
// Settings tab additions
'settings_tab_developer' => 'API i Webhooki',
'settings_section_rest_api' => 'Dostęp do REST API',
'settings_api_enabled' => 'Włącz Developer REST API',
'settings_api_enabled_helper' => 'Zezwól zewnętrznym systemom na tworzenie, listowanie i usuwanie krótkich linków przez REST API. Wyłącz, aby zablokować wszystkie endpointy /api/short-url/* odpowiedzią 503.',
'settings_section_global_webhook' => 'Konfiguracja globalnego Webhooka',
'settings_global_webhook_url' => 'Globalny Webhook URL',
'settings_global_webhook_url_helper' => 'Adres URL, pod który wysyłane będą w tle powiadomienia o zdarzeniach ze wszystkich linków.',

View File

@@ -77,6 +77,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
'global_webhook_url' => $mgr->get('global_webhook_url'),
'webhook_events' => $mgr->get('webhook_events', ['visited']),
'api_keys' => $mgr->get('api_keys', []),
'api_enabled' => $mgr->get('api_enabled', false),
]);
}
@@ -545,6 +546,16 @@ class ShortUrlSettingsPage extends Page implements HasForms
Tab::make(__('filament-short-url::default.settings_tab_developer'))
->icon('heroicon-o-cpu-chip')
->schema([
Section::make(__('filament-short-url::default.settings_section_rest_api'))
->schema([
Toggle::make('api_enabled')
->label(__('filament-short-url::default.settings_api_enabled'))
->helperText(__('filament-short-url::default.settings_api_enabled_helper'))
->default(false)
->inline(false)
->columnSpanFull(),
]),
Section::make(__('filament-short-url::default.settings_section_global_webhook'))
->schema([
TextInput::make('global_webhook_url')

View File

@@ -14,6 +14,12 @@ class AuthenticateShortUrlApi
*/
public function handle(Request $request, Closure $next): Response
{
if (! config('filament-short-url.api_enabled', false)) {
return response()->json([
'error' => 'The Developer API is currently disabled. Enable it in Short URL Settings → API & Webhooks.',
], 503);
}
$apiKey = $request->header('X-Api-Key');
if (!$apiKey && $auth = $request->header('Authorization')) {

View File

@@ -79,6 +79,7 @@ class ShortUrlSettingsManager
'global_webhook_url' => null,
'webhook_events' => ['visited'],
'api_keys' => [],
'api_enabled' => false,
], $stored);
return $this->cache;
@@ -148,6 +149,7 @@ class ShortUrlSettingsManager
'global_webhook_url',
'webhook_events',
'api_keys',
'api_enabled',
];
$filtered = array_intersect_key($data, array_flip($keys));
@@ -300,6 +302,7 @@ class ShortUrlSettingsManager
'filament-short-url.qr_defaults.gradient_type' => $settings['qr_gradient_type'],
'filament-short-url.global_webhook_url' => $settings['global_webhook_url'] ?? null,
'filament-short-url.webhook_events' => $settings['webhook_events'] ?? ['visited'],
'filament-short-url.api_enabled' => (bool) ($settings['api_enabled'] ?? false),
]);
}
}