Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdef0b6687 | ||
|
|
e0b7e3f28c | ||
|
|
e29c3ef24e | ||
|
|
b1416fbe86 | ||
|
|
59e4fc0e6b | ||
|
|
0e371ad139 |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/art export-ignore
|
||||
/tests export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
287
README.md
287
README.md
@@ -15,6 +15,21 @@
|
||||
|
||||
A professional, high-performance **Short URL Manager** plugin for [Filament v5](https://filamentphp.com). Built from scratch with cutting-edge practices, proxy resistance, offline Geo-IP engines, enterprise-grade smart targeting, and zero external shortening API dependencies.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p align="center">
|
||||
<table align="center" style="border-collapse: collapse; border: none;">
|
||||
<tr style="border: none;">
|
||||
<td width="50%" style="border: none; padding: 5px;"><img src="art/1.png" alt="Dashboard Stats" style="border-radius: 8px;"></td>
|
||||
<td width="50%" style="border: none; padding: 5px;"><img src="art/2.png" alt="Short URL Creation Form" style="border-radius: 8px;"></td>
|
||||
</tr>
|
||||
<tr style="border: none;">
|
||||
<td width="50%" style="border: none; padding: 5px;"><img src="art/3.png" alt="Targeting and Rules" style="border-radius: 8px;"></td>
|
||||
<td width="50%" style="border: none; padding: 5px;"><img src="art/4.png" alt="Interactive Settings Panel" style="border-radius: 8px;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
@@ -26,7 +41,7 @@ A professional, high-performance **Short URL Manager** plugin for [Filament v5](
|
||||
- ⚡ **Ultra-Fast Redirects** — Redirections resolve in milliseconds. Analytical tasks, event dispatching, and GA4 payloads are processed asynchronously via Laravel Queue jobs.
|
||||
- 🎯 **Google Analytics 4 server-side tracking** — Native integration with the GA4 Measurement Protocol to bypass client-side AdBlockers completely.
|
||||
- ⚙️ **Dual-way UTM Campaign Builder** — Built-in form builder synchronizes UTM parameters with your destination URLs in real-time (two-way binding).
|
||||
- 🔒 **Link Rules & Expiry** — Disable links manually, set expiration dates, or activate single-use links that deactivate automatically after the first click.
|
||||
- 🔒 **Link Validity Ranges & Expiry** — Set activation date ranges (From - To), custom visit limit counters (e.g. active for 3 clicks then expires), single-use restrictions, and custom fallback redirect URLs on expiration instead of static 410 Gone errors.
|
||||
- ➡️ **Query Parameter Forwarding** — Dynamically forward client query parameters (e.g. ad tokens, discount codes) to the destination URL.
|
||||
- 🛠️ **Dedicated Settings GUI** — Manage global configuration (routing, Geo-IP, GA4, cache, rate limiting, aggregation) directly inside the Filament panel without modifying files or `.env` files.
|
||||
- 💻 **Fluent Developer Builder** — Native model query builder pattern and robust programmatic generation APIs.
|
||||
@@ -35,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.
|
||||
|
||||
---
|
||||
|
||||
@@ -347,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:
|
||||
@@ -436,10 +694,14 @@ echo $shortUrl->getShortUrl(); // https://yourdomain.com/s/promo2026
|
||||
| `notes()` | `string` | Appends internal notes for administrators. |
|
||||
| `singleUse()` | `bool` (default `true`) | Makes the short URL expire immediately after the first successful click. |
|
||||
| `forwardQueryParams()` | `bool` (default `true`) | Forwards client-side incoming parameters (e.g. `?gclid=xxx`) to the final target. |
|
||||
| `expiresAt()` | `DateTimeInterface\|Carbon\|null` | Automatically sets a timestamp after which the URL returns a `410 Gone` error. |
|
||||
| `expiresAt()` | `DateTimeInterface\|Carbon\|null` | Automatically sets an expiration timestamp after which the URL is inactive. |
|
||||
| `activatedAt()` | `DateTimeInterface\|Carbon\|null` | Sets the timestamp from which the URL is active. |
|
||||
| `deactivatedAt()` | `DateTimeInterface\|Carbon\|null` | Sets the timestamp after which the URL is inactive. |
|
||||
| `maxVisits()` | `int\|null` | Sets a custom limit of total visits allowed. |
|
||||
| `expirationRedirectUrl()` | `string\|null` | Sets a custom URL to redirect to when expired/inactive. |
|
||||
| `trackVisits()` | `bool` (default `true`) | Toggles statistical logging for this link. |
|
||||
| `withTracing()` | `array` | Appends non-empty tracking parameters (like UTM codes) directly to the target URL. |
|
||||
| `create()` | `void` | Persists the model to the database and returns the `ShortUrl` instance. |
|
||||
| `create()` | `ShortUrl` | Persists the model to the database and returns the `ShortUrl` instance. |
|
||||
|
||||
---
|
||||
|
||||
@@ -501,7 +763,24 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**:
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.3.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.
|
||||
- **Reactive Validity Controls** — Master switch to toggle date limits, including bidirectional datetime picker constraints (Active From cannot exceed Expires At and vice versa) and custom UI field visibility.
|
||||
- **Smart Model Observers** — Automatic cleanup of unused parameters (like clearing `max_visits` if `single_use` is enabled, and clearing expiration fallbacks if date limits are off) to guarantee database consistency.
|
||||
- **Fluent Builder APIs** — Fluent method additions (`activatedAt()`, `deactivatedAt()`, `maxVisits()`, `expirationRedirectUrl()`) in the developer query builder.
|
||||
|
||||
### v1.3.0
|
||||
- **Automatic Scheduler Registration** — Zero-configuration task registration within the ServiceProvider booted phase (dynamically honors Settings toggles).
|
||||
- **Interactive Settings Validators** — Adds real-time "Test connection" verify action for GA4 Measurement Protocol and "Verify file" action for MaxMind database paths.
|
||||
- **Robust Table Row Copy Action** — High-reliability, conflict-free click-to-copy in table rows with built-in fallback helper for non-HTTPS (secure context) browser environments.
|
||||
|
||||
@@ -93,6 +93,15 @@ return [
|
||||
*/
|
||||
'queue_connection' => env('SHORT_URL_QUEUE', 'sync'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Name
|
||||
|--------------------------------------------------------------------------
|
||||
| The queue name to which visit tracking and counter jobs are dispatched.
|
||||
|
|
||||
*/
|
||||
'queue_name' => env('SHORT_URL_QUEUE_NAME', 'default'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redirect Cache TTL
|
||||
@@ -127,8 +136,8 @@ return [
|
||||
'foreground_color' => '#000000',
|
||||
'background_color' => '#ffffff',
|
||||
'gradient_enabled' => false,
|
||||
'gradient_from' => '#000000',
|
||||
'gradient_to' => '#ffffff',
|
||||
'gradient_from' => '#4f46e5',
|
||||
'gradient_to' => '#06b6d4',
|
||||
'gradient_type' => 'linear',
|
||||
],
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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('short_urls', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('max_visits')->nullable();
|
||||
$table->string('expiration_redirect_url', 2048)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('short_urls', function (Blueprint $table): void {
|
||||
$table->dropColumn(['max_visits', 'expiration_redirect_url']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('short_urls', function (Blueprint $table) {
|
||||
$table->string('pixel_meta_id', 100)->nullable();
|
||||
$table->string('pixel_google_id', 100)->nullable();
|
||||
$table->string('pixel_linkedin_id', 100)->nullable();
|
||||
$table->string('webhook_url', 2048)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('short_urls', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'pixel_meta_id',
|
||||
'pixel_google_id',
|
||||
'pixel_linkedin_id',
|
||||
'webhook_url',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
253
resources/lang/en/countries.php
Normal file
253
resources/lang/en/countries.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'AF' => 'Afghanistan',
|
||||
'AX' => 'Åland Islands',
|
||||
'AL' => 'Albania',
|
||||
'DZ' => 'Algeria',
|
||||
'AS' => 'American Samoa',
|
||||
'AD' => 'Andorra',
|
||||
'AO' => 'Angola',
|
||||
'AI' => 'Anguilla',
|
||||
'AQ' => 'Antarctica',
|
||||
'AG' => 'Antigua and Barbuda',
|
||||
'AR' => 'Argentina',
|
||||
'AM' => 'Armenia',
|
||||
'AW' => 'Aruba',
|
||||
'AU' => 'Australia',
|
||||
'AT' => 'Austria',
|
||||
'AZ' => 'Azerbaijan',
|
||||
'BS' => 'Bahamas',
|
||||
'BH' => 'Bahrain',
|
||||
'BD' => 'Bangladesh',
|
||||
'BB' => 'Barbados',
|
||||
'BY' => 'Belarus',
|
||||
'BE' => 'Belgium',
|
||||
'BZ' => 'Belize',
|
||||
'BJ' => 'Benin',
|
||||
'BM' => 'Bermuda',
|
||||
'BT' => 'Bhutan',
|
||||
'BO' => 'Bolivia',
|
||||
'BQ' => 'Bonaire, Sint Eustatius and Saba',
|
||||
'BA' => 'Bosnia and Herzegovina',
|
||||
'BW' => 'Botswana',
|
||||
'BV' => 'Bouvet Island',
|
||||
'BR' => 'Brazil',
|
||||
'IO' => 'British Indian Ocean Territory',
|
||||
'BN' => 'Brunei Darussalam',
|
||||
'BG' => 'Bulgaria',
|
||||
'BF' => 'Burkina Faso',
|
||||
'BI' => 'Burundi',
|
||||
'CV' => 'Cabo Verde',
|
||||
'KH' => 'Cambodia',
|
||||
'CM' => 'Cameroon',
|
||||
'CA' => 'Canada',
|
||||
'KY' => 'Cayman Islands',
|
||||
'CF' => 'Central African Republic',
|
||||
'TD' => 'Chad',
|
||||
'CL' => 'Chile',
|
||||
'CN' => 'China',
|
||||
'CX' => 'Christmas Island',
|
||||
'CC' => 'Cocos (Keeling) Islands',
|
||||
'CO' => 'Colombia',
|
||||
'KM' => 'Comoros',
|
||||
'CD' => 'Congo (Democratic Republic)',
|
||||
'CG' => 'Congo (Republic)',
|
||||
'CK' => 'Cook Islands',
|
||||
'CR' => 'Costa Rica',
|
||||
'CI' => 'Côte d\'Ivoire',
|
||||
'HR' => 'Croatia',
|
||||
'CU' => 'Cuba',
|
||||
'CW' => 'Curaçao',
|
||||
'CY' => 'Cyprus',
|
||||
'CZ' => 'Czech Republic',
|
||||
'DK' => 'Denmark',
|
||||
'DJ' => 'Djibouti',
|
||||
'DM' => 'Dominica',
|
||||
'DO' => 'Dominican Republic',
|
||||
'EC' => 'Ecuador',
|
||||
'EG' => 'Egypt',
|
||||
'SV' => 'El Salvador',
|
||||
'GQ' => 'Equatorial Guinea',
|
||||
'ER' => 'Eritrea',
|
||||
'EE' => 'Estonia',
|
||||
'SZ' => 'Eswatini',
|
||||
'ET' => 'Ethiopia',
|
||||
'FK' => 'Falkland Islands',
|
||||
'FO' => 'Faroe Islands',
|
||||
'FJ' => 'Fiji',
|
||||
'FI' => 'Finland',
|
||||
'FR' => 'France',
|
||||
'GF' => 'French Guiana',
|
||||
'PF' => 'French Polynesia',
|
||||
'TF' => 'French Southern Territories',
|
||||
'GA' => 'Gabon',
|
||||
'GM' => 'Gambia',
|
||||
'GE' => 'Georgia',
|
||||
'DE' => 'Germany',
|
||||
'GH' => 'Ghana',
|
||||
'GI' => 'Gibraltar',
|
||||
'GR' => 'Greece',
|
||||
'GL' => 'Greenland',
|
||||
'GD' => 'Grenada',
|
||||
'GP' => 'Guadeloupe',
|
||||
'GU' => 'Guam',
|
||||
'GT' => 'Guatemala',
|
||||
'GG' => 'Guernsey',
|
||||
'GN' => 'Guinea',
|
||||
'GW' => 'Guinea-Bissau',
|
||||
'GY' => 'Guyana',
|
||||
'HT' => 'Haiti',
|
||||
'HM' => 'Heard Island and McDonald Islands',
|
||||
'VA' => 'Holy See',
|
||||
'HN' => 'Honduras',
|
||||
'HK' => 'Hong Kong',
|
||||
'HU' => 'Hungary',
|
||||
'IS' => 'Iceland',
|
||||
'IN' => 'India',
|
||||
'ID' => 'Indonesia',
|
||||
'IR' => 'Iran',
|
||||
'IQ' => 'Iraq',
|
||||
'IE' => 'Ireland',
|
||||
'IM' => 'Isle of Man',
|
||||
'IL' => 'Israel',
|
||||
'IT' => 'Italy',
|
||||
'JM' => 'Jamaica',
|
||||
'JP' => 'Japan',
|
||||
'JE' => 'Jersey',
|
||||
'JO' => 'Jordan',
|
||||
'KZ' => 'Kazakhstan',
|
||||
'KE' => 'Kenya',
|
||||
'KI' => 'Kiribati',
|
||||
'KP' => 'North Korea',
|
||||
'KR' => 'South Korea',
|
||||
'KW' => 'Kuwait',
|
||||
'KG' => 'Kyrgyzstan',
|
||||
'LA' => 'Lao People\'s Democratic Republic',
|
||||
'LV' => 'Latvia',
|
||||
'LB' => 'Lebanon',
|
||||
'LS' => 'Lesotho',
|
||||
'LR' => 'Liberia',
|
||||
'LY' => 'Libya',
|
||||
'LI' => 'Liechtenstein',
|
||||
'LT' => 'Lithuania',
|
||||
'LU' => 'Luxembourg',
|
||||
'MO' => 'Macao',
|
||||
'MG' => 'Madagascar',
|
||||
'MW' => 'Malawi',
|
||||
'MY' => 'Malaysia',
|
||||
'MV' => 'Maldives',
|
||||
'ML' => 'Mali',
|
||||
'MT' => 'Malta',
|
||||
'MH' => 'Marshall Islands',
|
||||
'MQ' => 'Martinique',
|
||||
'MR' => 'Mauritania',
|
||||
'MU' => 'Mauritius',
|
||||
'YT' => 'Mayotte',
|
||||
'MX' => 'Mexico',
|
||||
'FM' => 'Micronesia',
|
||||
'MD' => 'Moldova',
|
||||
'MC' => 'Monaco',
|
||||
'MN' => 'Mongolia',
|
||||
'ME' => 'Montenegro',
|
||||
'MS' => 'Montserrat',
|
||||
'MA' => 'Morocco',
|
||||
'MZ' => 'Mozambique',
|
||||
'MM' => 'Myanmar',
|
||||
'NA' => 'Namibia',
|
||||
'NR' => 'Nauru',
|
||||
'NP' => 'Nepal',
|
||||
'NL' => 'Netherlands',
|
||||
'NC' => 'New Caledonia',
|
||||
'NZ' => 'New Zealand',
|
||||
'NI' => 'Nicaragua',
|
||||
'NE' => 'Niger',
|
||||
'NG' => 'Nigeria',
|
||||
'NU' => 'Niue',
|
||||
'NF' => 'Norfolk Island',
|
||||
'MK' => 'North Macedonia',
|
||||
'MP' => 'Northern Mariana Islands',
|
||||
'NO' => 'Norway',
|
||||
'OM' => 'Oman',
|
||||
'PK' => 'Pakistan',
|
||||
'PW' => 'Palau',
|
||||
'PS' => 'Palestine',
|
||||
'PA' => 'Panama',
|
||||
'PG' => 'Papua New Guinea',
|
||||
'PY' => 'Paraguay',
|
||||
'PE' => 'Peru',
|
||||
'PH' => 'Philippines',
|
||||
'PN' => 'Pitcairn',
|
||||
'PL' => 'Poland',
|
||||
'PT' => 'Portugal',
|
||||
'PR' => 'Puerto Rico',
|
||||
'QA' => 'Qatar',
|
||||
'RE' => 'Réunion',
|
||||
'RO' => 'Romania',
|
||||
'RU' => 'Russian Federation',
|
||||
'RW' => 'Rwanda',
|
||||
'BL' => 'Saint Barthélemy',
|
||||
'SH' => 'Saint Helena, Ascension and Tristan da Cunha',
|
||||
'KN' => 'Saint Kitts and Nevis',
|
||||
'LC' => 'Saint Lucia',
|
||||
'MF' => 'Saint Martin (French part)',
|
||||
'PM' => 'Saint Pierre and Miquelon',
|
||||
'VC' => 'Saint Vincent and the Grenadines',
|
||||
'WS' => 'Samoa',
|
||||
'SM' => 'San Marino',
|
||||
'ST' => 'Sao Tome and Principe',
|
||||
'SA' => 'Saudi Arabia',
|
||||
'SN' => 'Senegal',
|
||||
'RS' => 'Serbia',
|
||||
'SC' => 'Seychelles',
|
||||
'SL' => 'Sierra Leone',
|
||||
'SG' => 'Singapore',
|
||||
'SX' => 'Sint Maarten (Dutch part)',
|
||||
'SK' => 'Slovakia',
|
||||
'SI' => 'Slovenia',
|
||||
'SB' => 'Solomon Islands',
|
||||
'SO' => 'Somalia',
|
||||
'ZA' => 'South Africa',
|
||||
'GS' => 'South Georgia and the South Sandwich Islands',
|
||||
'SS' => 'South Sudan',
|
||||
'ES' => 'Spain',
|
||||
'LK' => 'Sri Lanka',
|
||||
'SD' => 'Sudan',
|
||||
'SR' => 'Suriname',
|
||||
'SJ' => 'Svalbard and Jan Mayen',
|
||||
'SE' => 'Sweden',
|
||||
'CH' => 'Switzerland',
|
||||
'SY' => 'Syrian Arab Republic',
|
||||
'TW' => 'Taiwan',
|
||||
'TJ' => 'Tajikistan',
|
||||
'TZ' => 'Tanzania',
|
||||
'TH' => 'Thailand',
|
||||
'TL' => 'Timor-Leste',
|
||||
'TG' => 'Togo',
|
||||
'TK' => 'Tokelau',
|
||||
'TO' => 'Tonga',
|
||||
'TT' => 'Trinidad and Tobago',
|
||||
'TN' => 'Tunisia',
|
||||
'TR' => 'Turkey',
|
||||
'TM' => 'Turkmenistan',
|
||||
'TC' => 'Turks and Caicos Islands',
|
||||
'TV' => 'Tuvalu',
|
||||
'UG' => 'Uganda',
|
||||
'UA' => 'Ukraine',
|
||||
'AE' => 'United Arab Emirates',
|
||||
'GB' => 'United Kingdom',
|
||||
'UM' => 'United States Minor Outlying Islands',
|
||||
'US' => 'United States',
|
||||
'UY' => 'Uruguay',
|
||||
'UZ' => 'Uzbekistan',
|
||||
'VU' => 'Vanuatu',
|
||||
'VE' => 'Venezuela',
|
||||
'VN' => 'Viet Nam',
|
||||
'VG' => 'Virgin Islands (British)',
|
||||
'VI' => 'Virgin Islands (U.S.)',
|
||||
'WF' => 'Wallis and Futuna',
|
||||
'EH' => 'Western Sahara',
|
||||
'YE' => 'Yemen',
|
||||
'ZM' => 'Zambia',
|
||||
'ZW' => 'Zimbabwe',
|
||||
];
|
||||
@@ -24,6 +24,13 @@ return [
|
||||
'forward_query_params' => 'Forward Query Parameters',
|
||||
'forward_query_params_helper' => 'Append incoming query parameters (like UTM tags) to the destination URL.',
|
||||
'expires_at' => 'Expires At',
|
||||
'activated_at' => 'Active From',
|
||||
'max_visits' => 'Max Visits Limit',
|
||||
'max_visits_helper' => 'Optional limit of total visits allowed before this link is deactivated.',
|
||||
'expiration_redirect_url' => 'Expiration Redirect URL',
|
||||
'expiration_redirect_url_helper' => 'Fallback URL to redirect to when the link is expired or inactive (leaves blank for 410 Gone error page).',
|
||||
'form_section_validity' => 'Validity & Limits',
|
||||
'use_date_validity' => 'Set Date Range Limits',
|
||||
'notes' => 'Internal Notes',
|
||||
'ga_tracking_id' => 'Google Analytics 4 Measurement ID',
|
||||
'ga_tracking_id_helper' => 'Optional G-XXXXXXXXXX ID to track redirects server-side.',
|
||||
@@ -288,6 +295,39 @@ return [
|
||||
'settings_rate_limiting_decay_seconds' => 'Decay Window (Seconds)',
|
||||
'settings_rate_limiting_decay_seconds_helper' => 'Time period for the rate limiter.',
|
||||
|
||||
// Queue Settings Additions
|
||||
'settings_queue_name' => 'Queue Name',
|
||||
'settings_queue_name_helper' => 'The target queue to which tracking and buffering sync jobs are dispatched. Default: "default".',
|
||||
'settings_geoip_stats_cache_ttl' => 'Stats Dashboard Cache TTL',
|
||||
'settings_geoip_stats_cache_ttl_helper' => 'Number of seconds to cache query metrics on the dashboard statistics page (prevents database load). Default: 300 (5 minutes).',
|
||||
|
||||
// Default Tracking Tab
|
||||
'settings_tab_tracking_defaults' => 'Default Tracking',
|
||||
'settings_section_tracking_defaults' => 'Default Visitor Tracking Settings',
|
||||
'settings_section_tracking_defaults_helper' => 'These settings govern the default tracking status for newly created short URLs.',
|
||||
'settings_track_visits_default' => 'Enable Tracking by Default',
|
||||
'settings_track_ip_default' => 'Default IP Address Tracking',
|
||||
'settings_track_browser_default' => 'Default Browser Tracking',
|
||||
'settings_track_browser_version_default' => 'Default Browser Version Tracking',
|
||||
'settings_track_os_default' => 'Default OS Tracking',
|
||||
'settings_track_os_version_default' => 'Default OS Version Tracking',
|
||||
'settings_track_referer_default' => 'Default Referer URL Tracking',
|
||||
'settings_track_device_type_default' => 'Default Device Type Tracking',
|
||||
|
||||
// QR Defaults Tab
|
||||
'settings_tab_qr' => 'QR Defaults',
|
||||
'settings_section_qr_defaults' => 'Default QR Code Design Options',
|
||||
'settings_section_qr_defaults_helper' => 'Configure standard design styling automatically loaded for newly generated QR codes.',
|
||||
'settings_qr_size' => 'Default Size (px)',
|
||||
'settings_qr_margin' => 'Default Margin',
|
||||
'settings_qr_dot_style' => 'Default Dot Style',
|
||||
'settings_qr_foreground_color' => 'Default Foreground Color',
|
||||
'settings_qr_background_color' => 'Default Background Color',
|
||||
'settings_qr_gradient_enabled' => 'Enable Gradient by Default',
|
||||
'settings_qr_gradient_from' => 'Gradient Color From',
|
||||
'settings_qr_gradient_to' => 'Gradient Color To',
|
||||
'settings_qr_gradient_type' => 'Default Gradient Type',
|
||||
|
||||
// Views
|
||||
'password_title' => 'Password Required',
|
||||
'password_description' => 'This link is password-protected. Please enter the correct password to continue.',
|
||||
@@ -298,4 +338,36 @@ return [
|
||||
'warning_description' => 'You are leaving this secure portal and being redirected to an external target link. Please ensure you trust the address below:',
|
||||
'warning_btn_continue' => 'Continue to Destination',
|
||||
'warning_btn_back' => 'Go Back',
|
||||
|
||||
// Marketing & API Tab
|
||||
'tab_marketing' => 'Marketing & API',
|
||||
'marketing_pixels_title' => 'Retargeting Pixels (Client-Side)',
|
||||
'marketing_pixels_desc' => 'Add ad tracking scripts to capture visitor cookies and build remarketing lists.',
|
||||
'pixel_meta' => 'Meta Pixel ID',
|
||||
'pixel_google' => 'Google Tag / GA4 ID',
|
||||
'pixel_linkedin' => 'LinkedIn Partner ID',
|
||||
'marketing_webhooks_title' => 'Link Webhook Integration',
|
||||
'marketing_webhooks_desc' => 'Configure a custom destination URL to send a real-time HTTP POST notification on each click.',
|
||||
'webhook_url' => 'Dedicated Webhook URL',
|
||||
|
||||
// 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.',
|
||||
'settings_webhook_events' => 'Monitored Webhook Events',
|
||||
'settings_webhook_events_helper' => 'Select which events should trigger a POST notification.',
|
||||
'webhook_event_visited' => 'Link Visited (Click)',
|
||||
'webhook_event_created' => 'Link Created',
|
||||
'webhook_event_expired' => 'Link Expired (Date)',
|
||||
'webhook_event_limit_reached' => 'Link Click Limit Reached',
|
||||
'settings_section_api_keys' => 'Developer API Keys',
|
||||
'settings_api_keys_description' => 'Manage API keys to integrate with external systems (e.g. CRM, Zapier, Make). Authenticate requests using the X-Api-Key header.',
|
||||
'settings_api_keys' => 'Active API Keys',
|
||||
'api_key_name' => 'Key Name (Description)',
|
||||
'api_key' => 'API Key',
|
||||
'active' => 'Active',
|
||||
];
|
||||
|
||||
253
resources/lang/pl/countries.php
Normal file
253
resources/lang/pl/countries.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'AF' => 'Afganistan',
|
||||
'AX' => 'Wyspy Alandzkie',
|
||||
'AL' => 'Albania',
|
||||
'DZ' => 'Algieria',
|
||||
'AS' => 'Samoa Amerykańskie',
|
||||
'AD' => 'Andora',
|
||||
'AO' => 'Angola',
|
||||
'AI' => 'Anguilla',
|
||||
'AQ' => 'Antarktyda',
|
||||
'AG' => 'Antigua i Barbuda',
|
||||
'AR' => 'Argentyna',
|
||||
'AM' => 'Armenia',
|
||||
'AW' => 'Aruba',
|
||||
'AU' => 'Australia',
|
||||
'AT' => 'Austria',
|
||||
'AZ' => 'Azerbejdżan',
|
||||
'BS' => 'Bahamy',
|
||||
'BH' => 'Bahrajn',
|
||||
'BD' => 'Bangladesz',
|
||||
'BB' => 'Barbados',
|
||||
'BY' => 'Białoruś',
|
||||
'BE' => 'Belgia',
|
||||
'BZ' => 'Belize',
|
||||
'BJ' => 'Benin',
|
||||
'BM' => 'Bermudy',
|
||||
'BT' => 'Bhutan',
|
||||
'BO' => 'Boliwia',
|
||||
'BQ' => 'Bonaire, Sint Eustatius i Saba',
|
||||
'BA' => 'Bośnia i Hercegowina',
|
||||
'BW' => 'Botswana',
|
||||
'BV' => 'Wyspa Bouveta',
|
||||
'BR' => 'Brazylia',
|
||||
'IO' => 'Brytyjskie Terytorium Oceanu Indyjskiego',
|
||||
'BN' => 'Brunei',
|
||||
'BG' => 'Bułgaria',
|
||||
'BF' => 'Burkina Faso',
|
||||
'BI' => 'Burundi',
|
||||
'CV' => 'Cabo Verde',
|
||||
'KH' => 'Kambodża',
|
||||
'CM' => 'Kamerun',
|
||||
'CA' => 'Kanada',
|
||||
'KY' => 'Kajmany',
|
||||
'CF' => 'Republika Środkowoafrykańska',
|
||||
'TD' => 'Czad',
|
||||
'CL' => 'Chile',
|
||||
'CN' => 'Chiny',
|
||||
'CX' => 'Wyspa Bożego Narodzenia',
|
||||
'CC' => 'Wyspy Kokosowe',
|
||||
'CO' => 'Kolumbia',
|
||||
'KM' => 'Komory',
|
||||
'CD' => 'Demokratyczna Republika Konga',
|
||||
'CG' => 'Kongo',
|
||||
'CK' => 'Wyspy Cooka',
|
||||
'CR' => 'Kostaryka',
|
||||
'CI' => 'Wybrzeże Kości Słoniowej',
|
||||
'HR' => 'Chorwacja',
|
||||
'CU' => 'Kuba',
|
||||
'CW' => 'Curaçao',
|
||||
'CY' => 'Cypr',
|
||||
'CZ' => 'Czechy',
|
||||
'DK' => 'Dania',
|
||||
'DJ' => 'Dżibuti',
|
||||
'DM' => 'Dominika',
|
||||
'DO' => 'Dominikana',
|
||||
'EC' => 'Ekwador',
|
||||
'EG' => 'Egipt',
|
||||
'SV' => 'Salwador',
|
||||
'GQ' => 'Gwinea Równikowa',
|
||||
'ER' => 'Erytrea',
|
||||
'EE' => 'Estonia',
|
||||
'SZ' => 'Eswatini',
|
||||
'ET' => 'Etiopia',
|
||||
'FK' => 'Falklandy',
|
||||
'FO' => 'Wyspy Owcze',
|
||||
'FJ' => 'Fidżi',
|
||||
'FI' => 'Finlandia',
|
||||
'FR' => 'Francja',
|
||||
'GF' => 'Gujana Francuska',
|
||||
'PF' => 'Polinezja Francuska',
|
||||
'TF' => 'Francuskie Terytoria Południowe',
|
||||
'GA' => 'Gabon',
|
||||
'GM' => 'Gambia',
|
||||
'GE' => 'Gruzja',
|
||||
'DE' => 'Niemcy',
|
||||
'GH' => 'Ghana',
|
||||
'GI' => 'Gibraltar',
|
||||
'GR' => 'Grecja',
|
||||
'GL' => 'Grenlandia',
|
||||
'GD' => 'Grenada',
|
||||
'GP' => 'Gwadelupa',
|
||||
'GU' => 'Guam',
|
||||
'GT' => 'Gwatemala',
|
||||
'GG' => 'Guernsey',
|
||||
'GN' => 'Gwinea',
|
||||
'GW' => 'Gwinea Bissau',
|
||||
'GY' => 'Gujana',
|
||||
'HT' => 'Haiti',
|
||||
'HM' => 'Wyspy Heard i McDonalda',
|
||||
'VA' => 'Watykan',
|
||||
'HN' => 'Honduras',
|
||||
'HK' => 'Hongkong',
|
||||
'HU' => 'Węgry',
|
||||
'IS' => 'Islandia',
|
||||
'IN' => 'Indie',
|
||||
'ID' => 'Indonezja',
|
||||
'IR' => 'Iran',
|
||||
'IQ' => 'Irak',
|
||||
'IE' => 'Irlandia',
|
||||
'IM' => 'Wyspa Man',
|
||||
'IL' => 'Izrael',
|
||||
'IT' => 'Włochy',
|
||||
'JM' => 'Jamajka',
|
||||
'JP' => 'Japonia',
|
||||
'JE' => 'Jersey',
|
||||
'JO' => 'Jordania',
|
||||
'KZ' => 'Kazachstan',
|
||||
'KE' => 'Kenia',
|
||||
'KI' => 'Kiribati',
|
||||
'KP' => 'Korea Północna',
|
||||
'KR' => 'Korea Południowa',
|
||||
'KW' => 'Kuwejt',
|
||||
'KG' => 'Kirgistan',
|
||||
'LA' => 'Laos',
|
||||
'LV' => 'Łotwa',
|
||||
'LB' => 'Liban',
|
||||
'LS' => 'Lesotho',
|
||||
'LR' => 'Liberia',
|
||||
'LY' => 'Libia',
|
||||
'LI' => 'Liechtenstein',
|
||||
'LT' => 'Litwa',
|
||||
'LU' => 'Luksemburg',
|
||||
'MO' => 'Makau',
|
||||
'MG' => 'Madagaskar',
|
||||
'MW' => 'Malawi',
|
||||
'MY' => 'Malezja',
|
||||
'MV' => 'Malediwy',
|
||||
'ML' => 'Mali',
|
||||
'MT' => 'Malta',
|
||||
'MH' => 'Wyspy Marshalla',
|
||||
'MQ' => 'Martynika',
|
||||
'MR' => 'Mauretania',
|
||||
'MU' => 'Mauritius',
|
||||
'YT' => 'Majotta',
|
||||
'MX' => 'Meksyk',
|
||||
'FM' => 'Mikronezja',
|
||||
'MD' => 'Mołdawia',
|
||||
'MC' => 'Monako',
|
||||
'MN' => 'Mongolia',
|
||||
'ME' => 'Czarnogóra',
|
||||
'MS' => 'Montserrat',
|
||||
'MA' => 'Maroko',
|
||||
'MZ' => 'Mozambik',
|
||||
'MM' => 'Mjanma',
|
||||
'NA' => 'Namibia',
|
||||
'NR' => 'Nauru',
|
||||
'NP' => 'Nepal',
|
||||
'NL' => 'Holandia',
|
||||
'NC' => 'Nowa Kaledonia',
|
||||
'NZ' => 'Nowa Zelandia',
|
||||
'NI' => 'Nikaragua',
|
||||
'NE' => 'Niger',
|
||||
'NG' => 'Nigeria',
|
||||
'NU' => 'Niue',
|
||||
'NF' => 'Norfolk',
|
||||
'MK' => 'Macedonia Północna',
|
||||
'MP' => 'Mariany Północne',
|
||||
'NO' => 'Norwegia',
|
||||
'OM' => 'Oman',
|
||||
'PK' => 'Pakistan',
|
||||
'PW' => 'Palau',
|
||||
'PS' => 'Palestyna',
|
||||
'PA' => 'Panama',
|
||||
'PG' => 'Papua-Nowa Gwinea',
|
||||
'PY' => 'Paragwaj',
|
||||
'PE' => 'Peru',
|
||||
'PH' => 'Filipiny',
|
||||
'PN' => 'Pitcairn',
|
||||
'PL' => 'Polska',
|
||||
'PT' => 'Portugalia',
|
||||
'PR' => 'Portoryko',
|
||||
'QA' => 'Katar',
|
||||
'RE' => 'Reunion',
|
||||
'RO' => 'Rumunia',
|
||||
'RU' => 'Rosja',
|
||||
'RW' => 'Rwanda',
|
||||
'BL' => 'Saint-Barthélemy',
|
||||
'SH' => 'Święta Helena, Wyspa Wniebowstąpienia i Tristan da Cunha',
|
||||
'KN' => 'Saint Kitts i Nevis',
|
||||
'LC' => 'Saint Lucia',
|
||||
'MF' => 'Saint-Martin',
|
||||
'PM' => 'Saint-Pierre i Miquelon',
|
||||
'VC' => 'Saint Vincent i Grenadyny',
|
||||
'WS' => 'Samoa',
|
||||
'SM' => 'San Marino',
|
||||
'ST' => 'Wyspy Świętego Tomasza i Książęca',
|
||||
'SA' => 'Arabia Saudyjska',
|
||||
'SN' => 'Senegal',
|
||||
'RS' => 'Serbia',
|
||||
'SC' => 'Seszele',
|
||||
'SL' => 'Sierra Leone',
|
||||
'SG' => 'Singapur',
|
||||
'SX' => 'Sint Maarten',
|
||||
'SK' => 'Słowacja',
|
||||
'SI' => 'Słowenia',
|
||||
'SB' => 'Wyspy Salomona',
|
||||
'SO' => 'Somalia',
|
||||
'ZA' => 'Południowa Afryka',
|
||||
'GS' => 'Georgia Południowa i Sandwich Południowy',
|
||||
'SS' => 'Sudan Południowy',
|
||||
'ES' => 'Hiszpania',
|
||||
'LK' => 'Sri Lanka',
|
||||
'SD' => 'Sudan',
|
||||
'SR' => 'Surinam',
|
||||
'SJ' => 'Svalbard i Jan Mayen',
|
||||
'SE' => 'Szwecja',
|
||||
'CH' => 'Szwajcaria',
|
||||
'SY' => 'Syria',
|
||||
'TW' => 'Tajwan',
|
||||
'TJ' => 'Tadżykistan',
|
||||
'TZ' => 'Tanzania',
|
||||
'TH' => 'Tajlandia',
|
||||
'TL' => 'Timor Wschodni',
|
||||
'TG' => 'Togo',
|
||||
'TK' => 'Tokelau',
|
||||
'TO' => 'Tonga',
|
||||
'TT' => 'Trynidad i Tobago',
|
||||
'TN' => 'Tunezja',
|
||||
'TR' => 'Turcja',
|
||||
'TM' => 'Turkmenistan',
|
||||
'TC' => 'Turks i Caicos',
|
||||
'TV' => 'Tuvalu',
|
||||
'UG' => 'Uganda',
|
||||
'UA' => 'Ukraina',
|
||||
'AE' => 'Zjednoczone Emiraty Arabskie',
|
||||
'GB' => 'Wielka Brytania',
|
||||
'UM' => 'Dalekie Wyspy Mniejsze Stanów Zjednoczonych',
|
||||
'US' => 'Stany Zjednoczone',
|
||||
'UY' => 'Urugwaj',
|
||||
'UZ' => 'Uzbekistan',
|
||||
'VU' => 'Vanuatu',
|
||||
'VE' => 'Wenezuela',
|
||||
'VN' => 'Wietnam',
|
||||
'VG' => 'Brytyjskie Wyspy Dziewicze',
|
||||
'VI' => 'Wyspy Dziewicze Stanów Zjednoczonych',
|
||||
'WF' => 'Wallis i Futuna',
|
||||
'EH' => 'Sahara Zachodnia',
|
||||
'YE' => 'Jemen',
|
||||
'ZM' => 'Zambia',
|
||||
'ZW' => 'Zimbabwe',
|
||||
];
|
||||
@@ -24,6 +24,13 @@ return [
|
||||
'forward_query_params' => 'Przekaż parametry URL',
|
||||
'forward_query_params_helper' => 'Dołącz parametry wejściowe (np. tagi UTM) do docelowego adresu URL.',
|
||||
'expires_at' => 'Wygasa dnia',
|
||||
'activated_at' => 'Aktywny od',
|
||||
'max_visits' => 'Maksymalny limit wejść',
|
||||
'max_visits_helper' => 'Opcjonalny limit liczby wejść, po osiągnięciu którego link zostanie automatycznie wyłączony.',
|
||||
'expiration_redirect_url' => 'URL przekierowania po wygaśnięciu',
|
||||
'expiration_redirect_url_helper' => 'Adres URL, na który nastąpi przekierowanie, gdy link wygaśnie lub będzie nieaktywny (pozostaw puste, aby wyświetlić błąd 410 Gone).',
|
||||
'form_section_validity' => 'Ważność i limity',
|
||||
'use_date_validity' => 'Ustaw limity datowe',
|
||||
'notes' => 'Wewnętrzne notatki',
|
||||
'ga_tracking_id' => 'Identyfikator śledzenia Google Analytics 4',
|
||||
'ga_tracking_id_helper' => 'Opcjonalny identyfikator G-XXXXXXXXXX do śledzenia przekierowań po stronie serwera.',
|
||||
@@ -289,6 +296,39 @@ return [
|
||||
'settings_rate_limiting_decay_seconds' => 'Okno wygasania (sekundy)',
|
||||
'settings_rate_limiting_decay_seconds_helper' => 'Przedział czasowy dla ograniczenia liczby żądań.',
|
||||
|
||||
// Queue Settings Additions
|
||||
'settings_queue_name' => 'Nazwa kolejki',
|
||||
'settings_queue_name_helper' => 'Docelowa nazwa kolejki, do której wysyłane są zadania śledzenia i synchronizacji liczników. Domyślnie: "default".',
|
||||
'settings_geoip_stats_cache_ttl' => 'TTL cache statystyk panelu',
|
||||
'settings_geoip_stats_cache_ttl_helper' => 'Liczba sekund buforowania wyników zapytań na stronie statystyk (zapobiega obciążeniu bazy danych). Domyślnie: 300 (5 minut).',
|
||||
|
||||
// Default Tracking Tab
|
||||
'settings_tab_tracking_defaults' => 'Domyślne śledzenie',
|
||||
'settings_section_tracking_defaults' => 'Domyślne ustawienia śledzenia wizyt',
|
||||
'settings_section_tracking_defaults_helper' => 'Ustawienia te określają domyślny status śledzenia dla nowo tworzonych krótkich adresów URL.',
|
||||
'settings_track_visits_default' => 'Śledź wizyty domyślnie',
|
||||
'settings_track_ip_default' => 'Domyślne śledzenie adresu IP',
|
||||
'settings_track_browser_default' => 'Domyślne śledzenie przeglądarki',
|
||||
'settings_track_browser_version_default' => 'Domyślne śledzenie wersji przeglądarki',
|
||||
'settings_track_os_default' => 'Domyślne śledzenie systemu operacyjnego',
|
||||
'settings_track_os_version_default' => 'Domyślne śledzenie wersji systemu operacyjnego',
|
||||
'settings_track_referer_default' => 'Domyślne śledzenie adresu referera',
|
||||
'settings_track_device_type_default' => 'Domyślne śledzenie typu urządzenia',
|
||||
|
||||
// QR Defaults Tab
|
||||
'settings_tab_qr' => 'Domyślne QR',
|
||||
'settings_section_qr_defaults' => 'Domyślne opcje wyglądu kodów QR',
|
||||
'settings_section_qr_defaults_helper' => 'Skonfiguruj standardowy styl wyglądu automatycznie ładowany dla nowo generowanych kodów QR.',
|
||||
'settings_qr_size' => 'Domyślny rozmiar (px)',
|
||||
'settings_qr_margin' => 'Domyślny margines',
|
||||
'settings_qr_dot_style' => 'Domyślny styl kropek',
|
||||
'settings_qr_foreground_color' => 'Domyślny kolor pierwszego planu',
|
||||
'settings_qr_background_color' => 'Domyślny kolor tła',
|
||||
'settings_qr_gradient_enabled' => 'Domyślnie włącz gradient',
|
||||
'settings_qr_gradient_from' => 'Kolor gradientu od',
|
||||
'settings_qr_gradient_to' => 'Kolor gradientu do',
|
||||
'settings_qr_gradient_type' => 'Domyślny typ gradientu',
|
||||
|
||||
// Views
|
||||
'password_title' => 'Wymagane hasło',
|
||||
'password_description' => 'Ten link jest chroniony hasłem. Wprowadź poprawne hasło, aby kontynuować.',
|
||||
@@ -299,4 +339,36 @@ return [
|
||||
'warning_description' => 'Opuszczasz ten bezpieczny portal i zostajesz przekierowany na zewnętrzny link docelowy. Upewnij się, że ufasz poniższemu adresowi:',
|
||||
'warning_btn_continue' => 'Kontynuuj do celu',
|
||||
'warning_btn_back' => 'Wróć',
|
||||
|
||||
// Marketing & API Tab
|
||||
'tab_marketing' => 'Marketing i API',
|
||||
'marketing_pixels_title' => 'Piksele retargetingowe (Client-Side)',
|
||||
'marketing_pixels_desc' => 'Dodaj skrypty śledzące, aby dołączać ciasteczka odwiedzających i budować grupy remarketingowe reklam.',
|
||||
'pixel_meta' => 'Meta Pixel ID',
|
||||
'pixel_google' => 'Google Tag / GA4 ID',
|
||||
'pixel_linkedin' => 'LinkedIn Partner ID',
|
||||
'marketing_webhooks_title' => 'Integracja Webhook linku',
|
||||
'marketing_webhooks_desc' => 'Skonfiguruj niestandardowy adres URL, pod który wysłane zostanie natychmiastowe powiadomienie HTTP POST po kliknięciu.',
|
||||
'webhook_url' => 'Dedykowany Webhook URL',
|
||||
|
||||
// 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.',
|
||||
'settings_webhook_events' => 'Monitorowane zdarzenia Webhooka',
|
||||
'settings_webhook_events_helper' => 'Wybierz zdarzenia, które powinny wyzwalać wysłanie powiadomienia POST.',
|
||||
'webhook_event_visited' => 'Odwiedziny linku (Wizyta)',
|
||||
'webhook_event_created' => 'Utworzenie linku',
|
||||
'webhook_event_expired' => 'Wygaśnięcie linku (Data)',
|
||||
'webhook_event_limit_reached' => 'Osiągnięcie limitu kliknięć',
|
||||
'settings_section_api_keys' => 'Klucze API Dewelopera',
|
||||
'settings_api_keys_description' => 'Zarządzaj kluczami API do integracji z zewnętrznymi systemami (np. CRM, Zapier, Make). Autoryzacja odbywa się za pomocą nagłówka X-Api-Key.',
|
||||
'settings_api_keys' => 'Aktywne klucze API',
|
||||
'api_key_name' => 'Nazwa klucza (Opis)',
|
||||
'api_key' => 'Klucz API',
|
||||
'active' => 'Aktywny',
|
||||
];
|
||||
|
||||
160
resources/views/pixel-loading.blade.php
Normal file
160
resources/views/pixel-loading.blade.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Connecting...</title>
|
||||
|
||||
<!-- Premium Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Meta / Facebook Pixel -->
|
||||
@if(!empty($pixelMetaId))
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '{{ $pixelMetaId }}');
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
<noscript><img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id={{ $pixelMetaId }}&ev=PageView&noscript=1" /></noscript>
|
||||
@endif
|
||||
|
||||
<!-- Google Analytics / GTM -->
|
||||
@if(!empty($pixelGoogleId))
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ $pixelGoogleId }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ $pixelGoogleId }}');
|
||||
</script>
|
||||
@endif
|
||||
|
||||
<!-- LinkedIn Insight -->
|
||||
@if(!empty($pixelLinkedinId))
|
||||
<script type="text/javascript">
|
||||
_linkedin_data_partner_id = "{{ $pixelLinkedinId }}";
|
||||
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
|
||||
window._linkedin_data_partner_ids.push(_linkedin_data_partner_id);
|
||||
(function(l) {
|
||||
if (!l){window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
|
||||
window.lintrk.q=[]}
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
var b = document.createElement("script");
|
||||
b.type = "text/javascript";b.async = true;
|
||||
b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
|
||||
s.parentNode.insertBefore(b, s);})(window.lintrk);
|
||||
</script>
|
||||
<noscript><img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid={{ $pixelLinkedinId }}&fmt=gif" /></noscript>
|
||||
@endif
|
||||
|
||||
<!-- Premium Styling -->
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
|
||||
color: #f1f5f9;
|
||||
font-family: 'Outfit', -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.spinner-ring {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4px solid transparent;
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
}
|
||||
|
||||
.spinner-ring:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
border-top-color: #a855f7;
|
||||
}
|
||||
|
||||
.spinner-ring:nth-child(3) {
|
||||
animation-delay: -0.6s;
|
||||
border-top-color: #ec4899;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
background: linear-gradient(to right, #818cf8, #f472b6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.95rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="spinner">
|
||||
<div class="spinner-ring"></div>
|
||||
<div class="spinner-ring"></div>
|
||||
<div class="spinner-ring"></div>
|
||||
</div>
|
||||
<h1>Connecting Safely</h1>
|
||||
<p>Securing connection & forwarding you now...</p>
|
||||
</div>
|
||||
|
||||
<!-- Async non-blocking redirect -->
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.replace("{!! addslashes($destination) !!}");
|
||||
}, 250);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,3 +11,11 @@ Route::match(
|
||||
->name('short-url.redirect')
|
||||
->where('key', '[a-zA-Z0-9_-]+')
|
||||
->middleware(config('filament-short-url.middleware', ['web', 'throttle:120,1']));
|
||||
|
||||
Route::prefix('api/short-url')
|
||||
->middleware([\Bjanczak\FilamentShortUrl\Http\Middleware\AuthenticateShortUrlApi::class])
|
||||
->group(function () {
|
||||
Route::get('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'index']);
|
||||
Route::post('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'store']);
|
||||
Route::delete('links/{id}', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'destroy']);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource\Pages;
|
||||
use Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource;
|
||||
use Bjanczak\FilamentShortUrl\Services\ShortUrlSettingsManager;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
@@ -44,7 +45,9 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
'geo_ip_cache_ttl' => $mgr->get('geo_ip_cache_ttl', 86400),
|
||||
'geo_ip_timeout' => $mgr->get('geo_ip_timeout', 3),
|
||||
'maxmind_database_path' => $mgr->get('maxmind_database_path', storage_path('geoip/GeoLite2-Country.mmdb')),
|
||||
'geo_ip_stats_cache_ttl' => $mgr->get('geo_ip_stats_cache_ttl', 300),
|
||||
'queue_connection' => $mgr->get('queue_connection', 'sync'),
|
||||
'queue_name' => $mgr->get('queue_name', 'default'),
|
||||
'ga4_api_secret' => $mgr->get('ga4_api_secret'),
|
||||
'ga4_firebase_app_id' => $mgr->get('ga4_firebase_app_id'),
|
||||
'counter_buffering_enabled' => $mgr->get('counter_buffering_enabled', false),
|
||||
@@ -54,6 +57,27 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
'rate_limiting_enabled' => $mgr->get('rate_limiting_enabled', false),
|
||||
'rate_limiting_max_attempts' => $mgr->get('rate_limiting_max_attempts', 60),
|
||||
'rate_limiting_decay_seconds' => $mgr->get('rate_limiting_decay_seconds', 60),
|
||||
'tracking_enabled' => $mgr->get('tracking_enabled', true),
|
||||
'tracking_fields_ip_address' => $mgr->get('tracking_fields_ip_address', true),
|
||||
'tracking_fields_browser' => $mgr->get('tracking_fields_browser', true),
|
||||
'tracking_fields_browser_version' => $mgr->get('tracking_fields_browser_version', true),
|
||||
'tracking_fields_operating_system' => $mgr->get('tracking_fields_operating_system', true),
|
||||
'tracking_fields_operating_system_version' => $mgr->get('tracking_fields_operating_system_version', true),
|
||||
'tracking_fields_referer_url' => $mgr->get('tracking_fields_referer_url', true),
|
||||
'tracking_fields_device_type' => $mgr->get('tracking_fields_device_type', true),
|
||||
'qr_size' => $mgr->get('qr_size', 300),
|
||||
'qr_margin' => $mgr->get('qr_margin', 1),
|
||||
'qr_dot_style' => $mgr->get('qr_dot_style', 'square'),
|
||||
'qr_foreground_color' => $mgr->get('qr_foreground_color', '#000000'),
|
||||
'qr_background_color' => $mgr->get('qr_background_color', '#ffffff'),
|
||||
'qr_gradient_enabled' => $mgr->get('qr_gradient_enabled', false),
|
||||
'qr_gradient_from' => $mgr->get('qr_gradient_from', '#4f46e5'),
|
||||
'qr_gradient_to' => $mgr->get('qr_gradient_to', '#06b6d4'),
|
||||
'qr_gradient_type' => $mgr->get('qr_gradient_type', 'linear'),
|
||||
'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),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -91,6 +115,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_key_length'))
|
||||
->helperText(__('filament-short-url::default.settings_key_length_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(4)
|
||||
->maxValue(20)
|
||||
->required(),
|
||||
@@ -99,6 +124,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_cache_ttl'))
|
||||
->helperText(__('filament-short-url::default.settings_cache_ttl_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(0)
|
||||
->suffix('s')
|
||||
->required(),
|
||||
@@ -111,6 +137,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
]),
|
||||
|
||||
Section::make(__('filament-short-url::default.settings_section_queue'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('queue_connection')
|
||||
->label(__('filament-short-url::default.settings_queue_connection'))
|
||||
@@ -125,6 +152,11 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
];
|
||||
})
|
||||
->required(),
|
||||
|
||||
TextInput::make('queue_name')
|
||||
->label(__('filament-short-url::default.settings_queue_name'))
|
||||
->helperText(__('filament-short-url::default.settings_queue_name_helper'))
|
||||
->required(),
|
||||
]),
|
||||
|
||||
Section::make(__('filament-short-url::default.settings_section_buffering'))
|
||||
@@ -168,6 +200,18 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_geoip_cache_ttl'))
|
||||
->helperText(__('filament-short-url::default.settings_geoip_cache_ttl_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(0)
|
||||
->suffix('s')
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => (bool) $get('geo_ip_enabled')),
|
||||
|
||||
// ── Stats Cache TTL (only when geo-ip is on) ──
|
||||
TextInput::make('geo_ip_stats_cache_ttl')
|
||||
->label(__('filament-short-url::default.settings_geoip_stats_cache_ttl'))
|
||||
->helperText(__('filament-short-url::default.settings_geoip_stats_cache_ttl_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(0)
|
||||
->suffix('s')
|
||||
->required()
|
||||
@@ -178,6 +222,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_geoip_timeout'))
|
||||
->helperText(__('filament-short-url::default.settings_geoip_timeout_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->maxValue(30)
|
||||
->suffix('s')
|
||||
@@ -354,6 +399,7 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_rate_limiting_max_attempts'))
|
||||
->helperText(__('filament-short-url::default.settings_rate_limiting_max_attempts_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => (bool) $get('rate_limiting_enabled')),
|
||||
@@ -362,12 +408,199 @@ class ShortUrlSettingsPage extends Page implements HasForms
|
||||
->label(__('filament-short-url::default.settings_rate_limiting_decay_seconds'))
|
||||
->helperText(__('filament-short-url::default.settings_rate_limiting_decay_seconds_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->suffix('s')
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => (bool) $get('rate_limiting_enabled')),
|
||||
]),
|
||||
]),
|
||||
|
||||
// ── Tracking Defaults ─────────────────────────────────
|
||||
Tab::make(__('filament-short-url::default.settings_tab_tracking_defaults'))
|
||||
->icon('heroicon-o-chart-bar')
|
||||
->schema([
|
||||
Section::make(__('filament-short-url::default.settings_section_tracking_defaults'))
|
||||
->description(__('filament-short-url::default.settings_section_tracking_defaults_helper'))
|
||||
->schema([
|
||||
Toggle::make('tracking_enabled')
|
||||
->label(__('filament-short-url::default.settings_track_visits_default'))
|
||||
->live()
|
||||
->inline(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('tracking_fields_ip_address')
|
||||
->label(__('filament-short-url::default.settings_track_ip_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_browser')
|
||||
->label(__('filament-short-url::default.settings_track_browser_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_browser_version')
|
||||
->label(__('filament-short-url::default.settings_track_browser_version_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_operating_system')
|
||||
->label(__('filament-short-url::default.settings_track_os_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_operating_system_version')
|
||||
->label(__('filament-short-url::default.settings_track_os_version_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_referer_url')
|
||||
->label(__('filament-short-url::default.settings_track_referer_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
|
||||
Toggle::make('tracking_fields_device_type')
|
||||
->label(__('filament-short-url::default.settings_track_device_type_default'))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('tracking_enabled')),
|
||||
])
|
||||
->columns(4),
|
||||
]),
|
||||
|
||||
// ── QR Defaults ──────────────────────────────────────
|
||||
Tab::make(__('filament-short-url::default.settings_tab_qr'))
|
||||
->icon('heroicon-o-qr-code')
|
||||
->schema([
|
||||
Section::make(__('filament-short-url::default.settings_section_qr_defaults'))
|
||||
->description(__('filament-short-url::default.settings_section_qr_defaults_helper'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('qr_size')
|
||||
->label(__('filament-short-url::default.settings_qr_size'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(100)
|
||||
->maxValue(2000)
|
||||
->required(),
|
||||
|
||||
Select::make('qr_margin')
|
||||
->label(__('filament-short-url::default.settings_qr_margin'))
|
||||
->options(array_combine(range(0, 10), range(0, 10)))
|
||||
->required(),
|
||||
|
||||
Select::make('qr_dot_style')
|
||||
->label(__('filament-short-url::default.settings_qr_dot_style'))
|
||||
->options([
|
||||
'square' => __('filament-short-url::default.qr_option_square'),
|
||||
'dots' => __('filament-short-url::default.qr_option_dots'),
|
||||
'rounded' => __('filament-short-url::default.qr_option_rounded'),
|
||||
'classy' => __('filament-short-url::default.qr_option_classy'),
|
||||
'classy-rounded' => __('filament-short-url::default.qr_option_classy_rounded'),
|
||||
'extra-rounded' => __('filament-short-url::default.qr_option_extra_rounded'),
|
||||
])
|
||||
->required(),
|
||||
|
||||
ColorPicker::make('qr_foreground_color')
|
||||
->label(__('filament-short-url::default.settings_qr_foreground_color'))
|
||||
->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
|
||||
->placeholder('#000000')
|
||||
->required(),
|
||||
|
||||
ColorPicker::make('qr_background_color')
|
||||
->label(__('filament-short-url::default.settings_qr_background_color'))
|
||||
->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
|
||||
->placeholder('#ffffff')
|
||||
->required(),
|
||||
|
||||
Toggle::make('qr_gradient_enabled')
|
||||
->label(__('filament-short-url::default.settings_qr_gradient_enabled'))
|
||||
->live()
|
||||
->inline(false),
|
||||
|
||||
ColorPicker::make('qr_gradient_from')
|
||||
->label(__('filament-short-url::default.settings_qr_gradient_from'))
|
||||
->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
|
||||
->placeholder('#4f46e5')
|
||||
->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
|
||||
->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
|
||||
|
||||
ColorPicker::make('qr_gradient_to')
|
||||
->label(__('filament-short-url::default.settings_qr_gradient_to'))
|
||||
->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
|
||||
->placeholder('#06b6d4')
|
||||
->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
|
||||
->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
|
||||
|
||||
Select::make('qr_gradient_type')
|
||||
->label(__('filament-short-url::default.settings_qr_gradient_type'))
|
||||
->options([
|
||||
'linear' => __('filament-short-url::default.qr_gradient_linear'),
|
||||
'radial' => __('filament-short-url::default.qr_gradient_radial'),
|
||||
])
|
||||
->required(fn (Get $get): bool => (bool) $get('qr_gradient_enabled'))
|
||||
->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')),
|
||||
]),
|
||||
]),
|
||||
|
||||
// ── Developer API & Webhooks ────────────────────────
|
||||
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')
|
||||
->label(__('filament-short-url::default.settings_global_webhook_url'))
|
||||
->helperText(__('filament-short-url::default.settings_global_webhook_url_helper'))
|
||||
->url()
|
||||
->nullable()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('webhook_events')
|
||||
->label(__('filament-short-url::default.settings_webhook_events'))
|
||||
->helperText(__('filament-short-url::default.settings_webhook_events_helper'))
|
||||
->multiple()
|
||||
->options([
|
||||
'visited' => __('filament-short-url::default.webhook_event_visited'),
|
||||
'created' => __('filament-short-url::default.webhook_event_created'),
|
||||
'expired' => __('filament-short-url::default.webhook_event_expired'),
|
||||
'limit_reached' => __('filament-short-url::default.webhook_event_limit_reached'),
|
||||
])
|
||||
->default(['visited'])
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Section::make(__('filament-short-url::default.settings_section_api_keys'))
|
||||
->description(__('filament-short-url::default.settings_api_keys_description'))
|
||||
->schema([
|
||||
\Filament\Forms\Components\Repeater::make('api_keys')
|
||||
->label(__('filament-short-url::default.settings_api_keys'))
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('filament-short-url::default.api_key_name'))
|
||||
->required(),
|
||||
TextInput::make('key')
|
||||
->label(__('filament-short-url::default.api_key'))
|
||||
->disabled()
|
||||
->dehydrated()
|
||||
->default(fn () => 'sh_key_'.bin2hex(random_bytes(16))),
|
||||
Toggle::make('is_active')
|
||||
->label(__('filament-short-url::default.active'))
|
||||
->default(true),
|
||||
])
|
||||
->columns(3)
|
||||
->default([]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
|
||||
@@ -27,6 +27,7 @@ class ShortUrlForm
|
||||
static::linkTab(),
|
||||
static::targetingTab(),
|
||||
static::trackingTab(),
|
||||
static::marketingTab(),
|
||||
static::qrDesignTab(),
|
||||
])->columnSpanFull(),
|
||||
]);
|
||||
@@ -103,34 +104,89 @@ class ShortUrlForm
|
||||
302 => __('filament-short-url::default.redirect_code_302'),
|
||||
301 => __('filament-short-url::default.redirect_code_301'),
|
||||
])
|
||||
->default(302)
|
||||
->default(fn () => config('filament-short-url.redirect_status_code', 302))
|
||||
->required(),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('filament-short-url::default.form_section_options'))->schema([
|
||||
Section::make(__('filament-short-url::default.form_section_options'))->schema([
|
||||
Toggle::make('is_enabled')
|
||||
->label(__('filament-short-url::default.status'))
|
||||
->default(true)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('single_use')
|
||||
->label(__('filament-short-url::default.single_use'))
|
||||
->helperText(__('filament-short-url::default.single_use_helper'))
|
||||
->default(false)
|
||||
->inline(false),
|
||||
|
||||
Toggle::make('forward_query_params')
|
||||
->label(__('filament-short-url::default.forward_query_params'))
|
||||
->helperText(__('filament-short-url::default.forward_query_params_helper'))
|
||||
->default(false)
|
||||
->inline(false),
|
||||
|
||||
DateTimePicker::make('expires_at')
|
||||
->label(__('filament-short-url::default.expires_at'))
|
||||
->nullable()
|
||||
->native(false),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('filament-short-url::default.form_section_validity'))
|
||||
->schema([
|
||||
Toggle::make('use_date_validity')
|
||||
->label(__('filament-short-url::default.use_date_validity'))
|
||||
->dehydrated(false)
|
||||
->live()
|
||||
->afterStateHydrated(function (Toggle $component, $state, Get $get, Set $set) {
|
||||
$set('use_date_validity', $get('activated_at') !== null || $get('expires_at') !== null);
|
||||
})
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if ($state) {
|
||||
$set('activated_at', now()->startOfMinute());
|
||||
} else {
|
||||
$set('activated_at', null);
|
||||
$set('expires_at', null);
|
||||
$set('expiration_redirect_url', null);
|
||||
}
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
DateTimePicker::make('activated_at')
|
||||
->label(__('filament-short-url::default.activated_at'))
|
||||
->nullable()
|
||||
->native(false)
|
||||
->withoutSeconds()
|
||||
->live(onBlur: true)
|
||||
->required(fn (Get $get): bool => (bool) $get('use_date_validity'))
|
||||
->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
|
||||
->minDate(now()->startOfDay())
|
||||
->maxDate(fn (Get $get) => $get('expires_at')),
|
||||
|
||||
DateTimePicker::make('expires_at')
|
||||
->label(__('filament-short-url::default.expires_at'))
|
||||
->nullable()
|
||||
->native(false)
|
||||
->withoutSeconds()
|
||||
->live(onBlur: true)
|
||||
->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
|
||||
->minDate(fn (Get $get) => $get('activated_at') ?: now()->startOfDay()),
|
||||
|
||||
TextInput::make('expiration_redirect_url')
|
||||
->label(__('filament-short-url::default.expiration_redirect_url'))
|
||||
->helperText(__('filament-short-url::default.expiration_redirect_url_helper'))
|
||||
->url()
|
||||
->maxLength(2048)
|
||||
->nullable()
|
||||
->visible(fn (Get $get): bool => (bool) $get('use_date_validity'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('single_use')
|
||||
->label(__('filament-short-url::default.single_use'))
|
||||
->helperText(__('filament-short-url::default.single_use_helper'))
|
||||
->default(false)
|
||||
->inline(false)
|
||||
->live(),
|
||||
|
||||
TextInput::make('max_visits')
|
||||
->label(__('filament-short-url::default.max_visits'))
|
||||
->helperText(__('filament-short-url::default.max_visits_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->nullable()
|
||||
->hidden(fn (Get $get): bool => (bool) $get('single_use')),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('filament-short-url::default.form_section_notes'))->schema([
|
||||
Textarea::make('notes')
|
||||
->label(__('filament-short-url::default.notes'))
|
||||
@@ -183,7 +239,7 @@ class ShortUrlForm
|
||||
->schema([
|
||||
Toggle::make('track_visits')
|
||||
->label(__('filament-short-url::default.track_visits'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.enabled', true))
|
||||
->live()
|
||||
->inline(false)
|
||||
->columnSpanFull(),
|
||||
@@ -193,43 +249,43 @@ class ShortUrlForm
|
||||
->schema([
|
||||
Toggle::make('track_ip_address')
|
||||
->label(__('filament-short-url::default.track_ip'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.ip_address', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_browser')
|
||||
->label(__('filament-short-url::default.track_browser'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.browser', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_browser_version')
|
||||
->label(__('filament-short-url::default.track_browser_version'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.browser_version', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_operating_system')
|
||||
->label(__('filament-short-url::default.track_os'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.operating_system', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_operating_system_version')
|
||||
->label(__('filament-short-url::default.track_os_version'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.operating_system_version', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_device_type')
|
||||
->label(__('filament-short-url::default.track_device_type'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.device_type', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
|
||||
Toggle::make('track_referer_url')
|
||||
->label(__('filament-short-url::default.track_referer'))
|
||||
->default(true)
|
||||
->default(fn () => config('filament-short-url.tracking.fields.referer_url', true))
|
||||
->inline(false)
|
||||
->disabled(fn (Get $get): bool => ! $get('track_visits')),
|
||||
])
|
||||
@@ -378,39 +434,17 @@ class ShortUrlForm
|
||||
->schema([
|
||||
Select::make('country_code')
|
||||
->label(__('filament-short-url::default.country_code'))
|
||||
->options([
|
||||
'PL' => 'Poland',
|
||||
'US' => 'United States',
|
||||
'GB' => 'United Kingdom',
|
||||
'DE' => 'Germany',
|
||||
'FR' => 'France',
|
||||
'ES' => 'Spain',
|
||||
'IT' => 'Italy',
|
||||
'CA' => 'Canada',
|
||||
'AU' => 'Australia',
|
||||
'NL' => 'Netherlands',
|
||||
'UA' => 'Ukraine',
|
||||
'IE' => 'Ireland',
|
||||
'BE' => 'Belgium',
|
||||
'AT' => 'Austria',
|
||||
'CH' => 'Switzerland',
|
||||
'SE' => 'Sweden',
|
||||
'NO' => 'Norway',
|
||||
'DK' => 'Denmark',
|
||||
'FI' => 'Finland',
|
||||
'CZ' => 'Czech Republic',
|
||||
'SK' => 'Slovakia',
|
||||
'HU' => 'Hungary',
|
||||
'RO' => 'Romania',
|
||||
'GR' => 'Greece',
|
||||
'PT' => 'Portugal',
|
||||
'BR' => 'Brazil',
|
||||
'MX' => 'Mexico',
|
||||
'CN' => 'China',
|
||||
'JP' => 'Japan',
|
||||
'IN' => 'India',
|
||||
])
|
||||
->options(function (): array {
|
||||
$countries = __('filament-short-url::countries');
|
||||
if (is_array($countries)) {
|
||||
asort($countries, SORT_LOCALE_STRING);
|
||||
return $countries;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
->searchable()
|
||||
->optionsLimit(300)
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->required(),
|
||||
TextInput::make('url')
|
||||
->label(__('filament-short-url::default.destination_url'))
|
||||
@@ -433,6 +467,9 @@ class ShortUrlForm
|
||||
->label(__('filament-short-url::default.rotation_weight'))
|
||||
->helperText(__('filament-short-url::default.rotation_weight_helper'))
|
||||
->numeric()
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->maxValue(1000)
|
||||
->required()
|
||||
->default(50),
|
||||
])
|
||||
@@ -441,4 +478,43 @@ class ShortUrlForm
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function marketingTab(): Tab
|
||||
{
|
||||
return Tab::make(__('filament-short-url::default.tab_marketing'))
|
||||
->icon('heroicon-o-megaphone')
|
||||
->schema([
|
||||
Section::make(__('filament-short-url::default.marketing_pixels_title'))
|
||||
->description(__('filament-short-url::default.marketing_pixels_desc'))
|
||||
->schema([
|
||||
TextInput::make('pixel_meta_id')
|
||||
->label(__('filament-short-url::default.pixel_meta'))
|
||||
->placeholder('e.g., 1234567890')
|
||||
->maxLength(100)
|
||||
->nullable(),
|
||||
TextInput::make('pixel_google_id')
|
||||
->label(__('filament-short-url::default.pixel_google'))
|
||||
->placeholder('e.g., G-XXXXXXXXXX or AW-XXXXXXXXXX')
|
||||
->maxLength(100)
|
||||
->nullable(),
|
||||
TextInput::make('pixel_linkedin_id')
|
||||
->label(__('filament-short-url::default.pixel_linkedin'))
|
||||
->placeholder('e.g., 1234567')
|
||||
->maxLength(100)
|
||||
->nullable(),
|
||||
])->columns(3),
|
||||
|
||||
Section::make(__('filament-short-url::default.marketing_webhooks_title'))
|
||||
->description(__('filament-short-url::default.marketing_webhooks_desc'))
|
||||
->schema([
|
||||
TextInput::make('webhook_url')
|
||||
->label(__('filament-short-url::default.webhook_url'))
|
||||
->placeholder('https://api.yourcrm.com/webhooks/clicks')
|
||||
->url()
|
||||
->maxLength(2048)
|
||||
->nullable()
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class ShortUrlGlobalOverview extends BaseWidget
|
||||
$agg = ShortUrl::query()
|
||||
->select([
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw('SUM(CASE WHEN is_enabled = 1 THEN 1 ELSE 0 END) as active'),
|
||||
DB::raw('SUM(CASE WHEN is_enabled THEN 1 ELSE 0 END) as active'),
|
||||
])
|
||||
->first();
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ class FilamentShortUrlServiceProvider extends PackageServiceProvider
|
||||
'2026_06_01_000003_add_utm_city_referer_to_short_url_visits_table',
|
||||
'2026_06_01_000004_add_targeting_and_security_to_short_urls_table',
|
||||
'2026_06_01_000005_create_short_url_daily_stats_table',
|
||||
'2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table',
|
||||
'2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table',
|
||||
])
|
||||
->hasCommands([
|
||||
SyncBufferedCountersCommand::class,
|
||||
|
||||
137
src/Http/Controllers/ShortUrlApiController.php
Normal file
137
src/Http/Controllers/ShortUrlApiController.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Bjanczak\FilamentShortUrl\Http\Controllers;
|
||||
|
||||
use Bjanczak\FilamentShortUrl\Jobs\SendWebhookJob;
|
||||
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
|
||||
use Bjanczak\FilamentShortUrl\Services\ShortUrlService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class ShortUrlApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ShortUrlService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display a listing of short URLs.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$links = ShortUrl::orderBy('id', 'desc')->paginate(30);
|
||||
|
||||
$transformed = $links->getCollection()->map(fn ($link) => $this->transformLink($link));
|
||||
|
||||
return response()->json([
|
||||
'data' => $transformed,
|
||||
'meta' => [
|
||||
'current_page' => $links->currentPage(),
|
||||
'last_page' => $links->lastPage(),
|
||||
'per_page' => $links->perPage(),
|
||||
'total' => $links->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created short URL.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'destination_url' => 'required|url|max:2048',
|
||||
'url_key' => 'nullable|string|alpha_dash|max:50|unique:short_urls,url_key',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
'is_enabled' => 'nullable|boolean',
|
||||
'redirect_status_code' => 'nullable|integer|in:301,302',
|
||||
'single_use' => 'nullable|boolean',
|
||||
'forward_query_params' => 'nullable|boolean',
|
||||
'max_visits' => 'nullable|integer|min:1',
|
||||
'expiration_redirect_url' => 'nullable|url|max:2048',
|
||||
'activated_at' => 'nullable|date',
|
||||
'expires_at' => 'nullable|date',
|
||||
'pixel_meta_id' => 'nullable|string|max:100',
|
||||
'pixel_google_id' => 'nullable|string|max:100',
|
||||
'pixel_linkedin_id' => 'nullable|string|max:100',
|
||||
'webhook_url' => 'nullable|url|max:2048',
|
||||
]);
|
||||
|
||||
$shortUrl = $this->service->create($validated);
|
||||
|
||||
// Fire 'created' webhook if active
|
||||
$this->dispatchCreatedWebhook($shortUrl);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Short URL created successfully.',
|
||||
'data' => $this->transformLink($shortUrl),
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified short URL.
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$shortUrl = ShortUrl::findOrFail($id);
|
||||
$shortUrl->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Short URL deleted successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a ShortUrl model to API response array.
|
||||
*/
|
||||
private function transformLink(ShortUrl $link): array
|
||||
{
|
||||
return [
|
||||
'id' => $link->id,
|
||||
'destination_url' => $link->destination_url,
|
||||
'url_key' => $link->url_key,
|
||||
'short_url' => $link->getShortUrl(),
|
||||
'is_enabled' => (bool) $link->is_enabled,
|
||||
'redirect_status_code' => (int) $link->redirect_status_code,
|
||||
'total_visits' => (int) $link->total_visits,
|
||||
'unique_visits' => (int) $link->unique_visits,
|
||||
'max_visits' => $link->max_visits ? (int) $link->max_visits : null,
|
||||
'activated_at' => $link->activated_at ? $link->activated_at->toIso8601String() : null,
|
||||
'expires_at' => $link->expires_at ? $link->expires_at->toIso8601String() : null,
|
||||
'pixel_meta_id' => $link->pixel_meta_id,
|
||||
'pixel_google_id' => $link->pixel_google_id,
|
||||
'pixel_linkedin_id' => $link->pixel_linkedin_id,
|
||||
'webhook_url' => $link->webhook_url,
|
||||
'notes' => $link->notes,
|
||||
'created_at' => $link->created_at->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch webhook if global or per-link webhook is active for 'created' event.
|
||||
*/
|
||||
private function dispatchCreatedWebhook(ShortUrl $shortUrl): void
|
||||
{
|
||||
$targetUrl = $shortUrl->webhook_url;
|
||||
$globalUrl = config('filament-short-url.global_webhook_url');
|
||||
$events = config('filament-short-url.webhook_events', []);
|
||||
|
||||
if (empty($targetUrl) && !empty($globalUrl) && in_array('created', $events)) {
|
||||
$targetUrl = $globalUrl;
|
||||
}
|
||||
|
||||
if (!empty($targetUrl)) {
|
||||
$connection = config('filament-short-url.queue_connection', 'sync');
|
||||
dispatch(new SendWebhookJob(
|
||||
url: $targetUrl,
|
||||
event: 'created',
|
||||
payload: [
|
||||
'event' => 'created',
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'short_url' => $this->transformLink($shortUrl),
|
||||
]
|
||||
)->onConnection($connection ?: 'sync'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,12 @@ class ShortUrlRedirectController extends Controller
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// 410 Gone if disabled or expired
|
||||
// Redirect to custom expiration URL if defined, otherwise 410 Gone if disabled or expired
|
||||
if (! $shortUrl->isActive()) {
|
||||
if ($shortUrl->expiration_redirect_url) {
|
||||
return redirect()->away($shortUrl->expiration_redirect_url, 302);
|
||||
}
|
||||
|
||||
abort(410);
|
||||
}
|
||||
|
||||
@@ -137,6 +141,15 @@ class ShortUrlRedirectController extends Controller
|
||||
cache()->forget("filament-short-url:{$shortUrl->url_key}");
|
||||
}
|
||||
|
||||
if (! empty($shortUrl->pixel_meta_id) || ! empty($shortUrl->pixel_google_id) || ! empty($shortUrl->pixel_linkedin_id)) {
|
||||
return response(view('filament-short-url::pixel-loading', [
|
||||
'destination' => $destination,
|
||||
'pixelMetaId' => $shortUrl->pixel_meta_id,
|
||||
'pixelGoogleId' => $shortUrl->pixel_google_id,
|
||||
'pixelLinkedinId' => $shortUrl->pixel_linkedin_id,
|
||||
]))->header('Content-Type', 'text/html');
|
||||
}
|
||||
|
||||
return redirect()->away($destination, $shortUrl->redirect_status_code);
|
||||
}
|
||||
}
|
||||
|
||||
56
src/Http/Middleware/AuthenticateShortUrlApi.php
Normal file
56
src/Http/Middleware/AuthenticateShortUrlApi.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Bjanczak\FilamentShortUrl\Http\Middleware;
|
||||
|
||||
use Bjanczak\FilamentShortUrl\Services\ShortUrlSettingsManager;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuthenticateShortUrlApi
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
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')) {
|
||||
if (str_starts_with(strtolower($auth), 'bearer ')) {
|
||||
$apiKey = substr($auth, 7);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($apiKey)) {
|
||||
return response()->json([
|
||||
'error' => 'Unauthorized. API Key is missing.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$mgr = app(ShortUrlSettingsManager::class);
|
||||
$keys = $mgr->get('api_keys', []);
|
||||
|
||||
$valid = false;
|
||||
foreach ($keys as $keyObj) {
|
||||
if (($keyObj['key'] ?? '') === $apiKey && (bool) ($keyObj['is_active'] ?? false)) {
|
||||
$valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$valid) {
|
||||
return response()->json([
|
||||
'error' => 'Unauthorized. Invalid or inactive API Key.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
70
src/Jobs/SendWebhookJob.php
Normal file
70
src/Jobs/SendWebhookJob.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Bjanczak\FilamentShortUrl\Jobs;
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendWebhookJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/** @var int Max retry attempts if the webhook fails */
|
||||
public int $tries = 3;
|
||||
|
||||
/** @var int Delay between retries (seconds) */
|
||||
public int $backoff = 10;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $url,
|
||||
public readonly string $event,
|
||||
public readonly array $payload
|
||||
) {
|
||||
$this->onQueue(config('filament-short-url.queue_name', 'default'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'wYachts-ShortUrl-Webhook/1.5',
|
||||
])
|
||||
->post($this->url, $this->payload);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::warning("[FilamentShortUrl] Webhook delivery returned client/server error", [
|
||||
'url' => $this->url,
|
||||
'event' => $this->event,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
|
||||
// Throw exception to trigger queue retry
|
||||
throw new \RuntimeException("Webhook failed with status " . $response->status());
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("[FilamentShortUrl] Webhook delivery failed", [
|
||||
'url' => $this->url,
|
||||
'event' => $this->event,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,53 @@ class TrackShortUrlVisitJob implements ShouldQueue
|
||||
// Fire event for user listeners
|
||||
ShortUrlVisited::dispatch($shortUrl, $visit);
|
||||
|
||||
// Trigger Webhook if active
|
||||
$targetUrl = $shortUrl->webhook_url;
|
||||
$globalUrl = config('filament-short-url.global_webhook_url');
|
||||
$events = config('filament-short-url.webhook_events', []);
|
||||
|
||||
if (empty($targetUrl) && ! empty($globalUrl) && in_array('visited', $events)) {
|
||||
$targetUrl = $globalUrl;
|
||||
}
|
||||
|
||||
if (! empty($targetUrl)) {
|
||||
dispatch(new \Bjanczak\FilamentShortUrl\Jobs\SendWebhookJob(
|
||||
url: $targetUrl,
|
||||
event: 'visited',
|
||||
payload: [
|
||||
'event' => 'visited',
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'short_url' => [
|
||||
'id' => $shortUrl->id,
|
||||
'destination_url' => $shortUrl->destination_url,
|
||||
'url_key' => $shortUrl->url_key,
|
||||
'short_url' => $shortUrl->getShortUrl(),
|
||||
'total_visits' => (int) $shortUrl->getRealTimeTotalVisits(),
|
||||
'unique_visits' => (int) $shortUrl->unique_visits,
|
||||
],
|
||||
'visit' => [
|
||||
'id' => $visit->id,
|
||||
'visited_at' => $visit->visited_at->toIso8601String(),
|
||||
'device_type' => $visit->device_type,
|
||||
'browser' => $visit->browser,
|
||||
'browser_version' => $visit->browser_version,
|
||||
'operating_system' => $visit->operating_system,
|
||||
'operating_system_version' => $visit->operating_system_version,
|
||||
'country' => $visit->country,
|
||||
'country_code' => $visit->country_code,
|
||||
'city' => $visit->city,
|
||||
'referer_url' => $visit->referer_url,
|
||||
'referer_host' => $visit->referer_host,
|
||||
'utm_source' => $visit->utm_source,
|
||||
'utm_medium' => $visit->utm_medium,
|
||||
'utm_campaign' => $visit->utm_campaign,
|
||||
'utm_term' => $visit->utm_term,
|
||||
'utm_content' => $visit->utm_content,
|
||||
],
|
||||
]
|
||||
)->onConnection($this->connection ?: 'sync'));
|
||||
}
|
||||
|
||||
// Optional GA4 Measurement Protocol integration
|
||||
if ($shortUrl->ga_tracking_id && config('filament-short-url.ga4.api_secret')) {
|
||||
$this->sendGa4Hit($shortUrl, $visit);
|
||||
|
||||
@@ -78,6 +78,12 @@ class ShortUrl extends Model
|
||||
'targeting_rules',
|
||||
'total_visits',
|
||||
'unique_visits',
|
||||
'max_visits',
|
||||
'expiration_redirect_url',
|
||||
'pixel_meta_id',
|
||||
'pixel_google_id',
|
||||
'pixel_linkedin_id',
|
||||
'webhook_url',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
@@ -96,6 +102,7 @@ class ShortUrl extends Model
|
||||
'qr_options' => 'array',
|
||||
'show_warning_page' => 'boolean',
|
||||
'targeting_rules' => 'array',
|
||||
'max_visits' => 'integer',
|
||||
'activated_at' => 'datetime',
|
||||
'deactivated_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
@@ -124,9 +131,21 @@ class ShortUrl extends Model
|
||||
{
|
||||
return $query
|
||||
->enabled()
|
||||
->where(fn (Builder $q) => $q
|
||||
->whereNull('activated_at')
|
||||
->orWhere('activated_at', '<=', now())
|
||||
)
|
||||
->where(fn (Builder $q) => $q
|
||||
->whereNull('deactivated_at')
|
||||
->orWhere('deactivated_at', '>', now())
|
||||
)
|
||||
->where(fn (Builder $q) => $q
|
||||
->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now())
|
||||
)
|
||||
->where(fn (Builder $q) => $q
|
||||
->whereNull('max_visits')
|
||||
->orWhereRaw('total_visits < max_visits')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +182,39 @@ class ShortUrl extends Model
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(fn (self $m) => cache()->forget("filament-short-url:{$m->url_key}"));
|
||||
static::saving(function (self $m) {
|
||||
if ($m->single_use) {
|
||||
$m->max_visits = null;
|
||||
}
|
||||
|
||||
if ($m->activated_at === null && $m->expires_at === null) {
|
||||
$m->expiration_redirect_url = null;
|
||||
}
|
||||
|
||||
if (empty($m->pixel_meta_id)) {
|
||||
$m->pixel_meta_id = null;
|
||||
}
|
||||
if (empty($m->pixel_google_id)) {
|
||||
$m->pixel_google_id = null;
|
||||
}
|
||||
if (empty($m->pixel_linkedin_id)) {
|
||||
$m->pixel_linkedin_id = null;
|
||||
}
|
||||
if (empty($m->webhook_url)) {
|
||||
$m->webhook_url = null;
|
||||
}
|
||||
});
|
||||
|
||||
static::saved(function (self $m) {
|
||||
cache()->forget("filament-short-url:{$m->url_key}");
|
||||
|
||||
if ($m->wasChanged('url_key')) {
|
||||
$oldKey = $m->getOriginal('url_key');
|
||||
if ($oldKey) {
|
||||
cache()->forget("filament-short-url:{$oldKey}");
|
||||
}
|
||||
}
|
||||
});
|
||||
static::deleted(fn (self $m) => cache()->forget("filament-short-url:{$m->url_key}"));
|
||||
|
||||
// Bust the forever-cached link counts displayed in the global overview widget.
|
||||
@@ -187,7 +238,17 @@ class ShortUrl extends Model
|
||||
return app(ShortUrlService::class)->destination($url);
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
public function getRealTimeTotalVisits(): int
|
||||
{
|
||||
$total = $this->total_visits;
|
||||
|
||||
if (config('filament-short-url.counter_buffering.enabled', false)) {
|
||||
$prefix = config('filament-short-url.counter_buffering.cache_key_prefix', 'filament-short-url:buffer:');
|
||||
$total += (int) cache()->get("{$prefix}total:{$this->id}", 0);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
@@ -210,12 +271,25 @@ class ShortUrl extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Visit limit reached
|
||||
if (! $this->single_use && $this->max_visits !== null && $this->getRealTimeTotalVisits() >= $this->max_visits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at !== null && $this->expires_at->isPast();
|
||||
if ($this->expires_at !== null && $this->expires_at->isPast()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! $this->single_use && $this->max_visits !== null && $this->getRealTimeTotalVisits() >= $this->max_visits) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function trackingEnabled(): bool
|
||||
|
||||
@@ -51,6 +51,34 @@ class ShortUrlBuilder
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function activatedAt(\DateTimeInterface|Carbon|null $date): static
|
||||
{
|
||||
$this->data['activated_at'] = $date ? Carbon::instance($date) : null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function deactivatedAt(\DateTimeInterface|Carbon|null $date): static
|
||||
{
|
||||
$this->data['deactivated_at'] = $date ? Carbon::instance($date) : null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function maxVisits(?int $maxVisits): static
|
||||
{
|
||||
$this->data['max_visits'] = $maxVisits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function expirationRedirectUrl(?string $url): static
|
||||
{
|
||||
$this->data['expiration_redirect_url'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function trackVisits(bool $track = true): static
|
||||
{
|
||||
$this->data['track_visits'] = $track;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Bjanczak\FilamentShortUrl\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ShortUrlSettingsManager
|
||||
@@ -45,9 +46,11 @@ class ShortUrlSettingsManager
|
||||
'geo_ip_cache_ttl' => config('filament-short-url.geo_ip.cache_ttl', 86400),
|
||||
'geo_ip_timeout' => config('filament-short-url.geo_ip.timeout', 3),
|
||||
'maxmind_database_path' => config('filament-short-url.geo_ip.maxmind.database_path', storage_path('geoip/GeoLite2-Country.mmdb')),
|
||||
'geo_ip_stats_cache_ttl' => config('filament-short-url.geo_ip.stats_cache_ttl', 300),
|
||||
'ga4_api_secret' => config('filament-short-url.ga4.api_secret'),
|
||||
'ga4_firebase_app_id' => config('filament-short-url.ga4.firebase_app_id'),
|
||||
'queue_connection' => config('filament-short-url.queue_connection', 'sync'),
|
||||
'queue_name' => config('filament-short-url.queue_name', 'default'),
|
||||
'cache_ttl' => config('filament-short-url.cache_ttl', 3600),
|
||||
'counter_buffering_enabled' => config('filament-short-url.counter_buffering.enabled', false),
|
||||
'trust_cdn_headers' => config('filament-short-url.trust_cdn_headers', false),
|
||||
@@ -56,6 +59,27 @@ class ShortUrlSettingsManager
|
||||
'rate_limiting_enabled' => config('filament-short-url.rate_limiting.enabled', false),
|
||||
'rate_limiting_max_attempts' => config('filament-short-url.rate_limiting.max_attempts', 60),
|
||||
'rate_limiting_decay_seconds' => config('filament-short-url.rate_limiting.decay_seconds', 60),
|
||||
'tracking_enabled' => config('filament-short-url.tracking.enabled', true),
|
||||
'tracking_fields_ip_address' => config('filament-short-url.tracking.fields.ip_address', true),
|
||||
'tracking_fields_browser' => config('filament-short-url.tracking.fields.browser', true),
|
||||
'tracking_fields_browser_version' => config('filament-short-url.tracking.fields.browser_version', true),
|
||||
'tracking_fields_operating_system' => config('filament-short-url.tracking.fields.operating_system', true),
|
||||
'tracking_fields_operating_system_version' => config('filament-short-url.tracking.fields.operating_system_version', true),
|
||||
'tracking_fields_referer_url' => config('filament-short-url.tracking.fields.referer_url', true),
|
||||
'tracking_fields_device_type' => config('filament-short-url.tracking.fields.device_type', true),
|
||||
'qr_size' => config('filament-short-url.qr_defaults.size', 300),
|
||||
'qr_margin' => config('filament-short-url.qr_defaults.margin', 1),
|
||||
'qr_dot_style' => config('filament-short-url.qr_defaults.dot_style', 'square'),
|
||||
'qr_foreground_color' => config('filament-short-url.qr_defaults.foreground_color', '#000000'),
|
||||
'qr_background_color' => config('filament-short-url.qr_defaults.background_color', '#ffffff'),
|
||||
'qr_gradient_enabled' => config('filament-short-url.qr_defaults.gradient_enabled', false),
|
||||
'qr_gradient_from' => config('filament-short-url.qr_defaults.gradient_from', '#4f46e5'),
|
||||
'qr_gradient_to' => config('filament-short-url.qr_defaults.gradient_to', '#06b6d4'),
|
||||
'qr_gradient_type' => config('filament-short-url.qr_defaults.gradient_type', 'linear'),
|
||||
'global_webhook_url' => null,
|
||||
'webhook_events' => ['visited'],
|
||||
'api_keys' => [],
|
||||
'api_enabled' => false,
|
||||
], $stored);
|
||||
|
||||
return $this->cache;
|
||||
@@ -80,6 +104,8 @@ class ShortUrlSettingsManager
|
||||
File::makeDirectory($dir, 0755, true);
|
||||
}
|
||||
|
||||
$oldPrefix = $this->get('route_prefix');
|
||||
|
||||
// Keep only supported settings keys to prevent bloat
|
||||
$keys = [
|
||||
'route_prefix',
|
||||
@@ -90,9 +116,11 @@ class ShortUrlSettingsManager
|
||||
'geo_ip_cache_ttl',
|
||||
'geo_ip_timeout',
|
||||
'maxmind_database_path',
|
||||
'geo_ip_stats_cache_ttl',
|
||||
'ga4_api_secret',
|
||||
'ga4_firebase_app_id',
|
||||
'queue_connection',
|
||||
'queue_name',
|
||||
'cache_ttl',
|
||||
'counter_buffering_enabled',
|
||||
'trust_cdn_headers',
|
||||
@@ -101,6 +129,27 @@ class ShortUrlSettingsManager
|
||||
'rate_limiting_enabled',
|
||||
'rate_limiting_max_attempts',
|
||||
'rate_limiting_decay_seconds',
|
||||
'tracking_enabled',
|
||||
'tracking_fields_ip_address',
|
||||
'tracking_fields_browser',
|
||||
'tracking_fields_browser_version',
|
||||
'tracking_fields_operating_system',
|
||||
'tracking_fields_operating_system_version',
|
||||
'tracking_fields_referer_url',
|
||||
'tracking_fields_device_type',
|
||||
'qr_size',
|
||||
'qr_margin',
|
||||
'qr_dot_style',
|
||||
'qr_foreground_color',
|
||||
'qr_background_color',
|
||||
'qr_gradient_enabled',
|
||||
'qr_gradient_from',
|
||||
'qr_gradient_to',
|
||||
'qr_gradient_type',
|
||||
'global_webhook_url',
|
||||
'webhook_events',
|
||||
'api_keys',
|
||||
'api_enabled',
|
||||
];
|
||||
|
||||
$filtered = array_intersect_key($data, array_flip($keys));
|
||||
@@ -121,6 +170,9 @@ class ShortUrlSettingsManager
|
||||
if (isset($filtered['geo_ip_timeout'])) {
|
||||
$filtered['geo_ip_timeout'] = (int) $filtered['geo_ip_timeout'];
|
||||
}
|
||||
if (isset($filtered['geo_ip_stats_cache_ttl'])) {
|
||||
$filtered['geo_ip_stats_cache_ttl'] = (int) $filtered['geo_ip_stats_cache_ttl'];
|
||||
}
|
||||
if (isset($filtered['cache_ttl'])) {
|
||||
$filtered['cache_ttl'] = (int) $filtered['cache_ttl'];
|
||||
}
|
||||
@@ -146,11 +198,60 @@ class ShortUrlSettingsManager
|
||||
$filtered['rate_limiting_decay_seconds'] = (int) $filtered['rate_limiting_decay_seconds'];
|
||||
}
|
||||
|
||||
// Tracking defaults
|
||||
if (isset($filtered['tracking_enabled'])) {
|
||||
$filtered['tracking_enabled'] = (bool) $filtered['tracking_enabled'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_ip_address'])) {
|
||||
$filtered['tracking_fields_ip_address'] = (bool) $filtered['tracking_fields_ip_address'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_browser'])) {
|
||||
$filtered['tracking_fields_browser'] = (bool) $filtered['tracking_fields_browser'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_browser_version'])) {
|
||||
$filtered['tracking_fields_browser_version'] = (bool) $filtered['tracking_fields_browser_version'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_operating_system'])) {
|
||||
$filtered['tracking_fields_operating_system'] = (bool) $filtered['tracking_fields_operating_system'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_operating_system_version'])) {
|
||||
$filtered['tracking_fields_operating_system_version'] = (bool) $filtered['tracking_fields_operating_system_version'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_referer_url'])) {
|
||||
$filtered['tracking_fields_referer_url'] = (bool) $filtered['tracking_fields_referer_url'];
|
||||
}
|
||||
if (isset($filtered['tracking_fields_device_type'])) {
|
||||
$filtered['tracking_fields_device_type'] = (bool) $filtered['tracking_fields_device_type'];
|
||||
}
|
||||
|
||||
// QR defaults
|
||||
if (isset($filtered['qr_size'])) {
|
||||
$filtered['qr_size'] = (int) $filtered['qr_size'];
|
||||
}
|
||||
if (isset($filtered['qr_margin'])) {
|
||||
$filtered['qr_margin'] = (int) $filtered['qr_margin'];
|
||||
}
|
||||
if (isset($filtered['qr_gradient_enabled'])) {
|
||||
$filtered['qr_gradient_enabled'] = (bool) $filtered['qr_gradient_enabled'];
|
||||
}
|
||||
|
||||
File::put($path, json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$this->cache = null;
|
||||
|
||||
// Apply immediately to current request config
|
||||
$this->applyConfigOverrides();
|
||||
|
||||
// Clear route cache if route prefix has changed
|
||||
$newPrefix = $filtered['route_prefix'] ?? null;
|
||||
if ($oldPrefix !== null && $newPrefix !== null && $oldPrefix !== $newPrefix) {
|
||||
try {
|
||||
if (app()->routesAreCached()) {
|
||||
Artisan::call('route:clear');
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Ignore route clear errors during boot/test
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,9 +270,11 @@ class ShortUrlSettingsManager
|
||||
'filament-short-url.geo_ip.cache_ttl' => $settings['geo_ip_cache_ttl'],
|
||||
'filament-short-url.geo_ip.timeout' => $settings['geo_ip_timeout'],
|
||||
'filament-short-url.geo_ip.maxmind.database_path' => $settings['maxmind_database_path'],
|
||||
'filament-short-url.geo_ip.stats_cache_ttl' => $settings['geo_ip_stats_cache_ttl'],
|
||||
'filament-short-url.ga4.api_secret' => $settings['ga4_api_secret'],
|
||||
'filament-short-url.ga4.firebase_app_id' => $settings['ga4_firebase_app_id'],
|
||||
'filament-short-url.queue_connection' => $settings['queue_connection'],
|
||||
'filament-short-url.queue_name' => $settings['queue_name'],
|
||||
'filament-short-url.cache_ttl' => $settings['cache_ttl'],
|
||||
'filament-short-url.counter_buffering.enabled' => $settings['counter_buffering_enabled'],
|
||||
'filament-short-url.trust_cdn_headers' => $settings['trust_cdn_headers'],
|
||||
@@ -180,6 +283,26 @@ class ShortUrlSettingsManager
|
||||
'filament-short-url.rate_limiting.enabled' => $settings['rate_limiting_enabled'],
|
||||
'filament-short-url.rate_limiting.max_attempts' => $settings['rate_limiting_max_attempts'],
|
||||
'filament-short-url.rate_limiting.decay_seconds' => $settings['rate_limiting_decay_seconds'],
|
||||
'filament-short-url.tracking.enabled' => $settings['tracking_enabled'],
|
||||
'filament-short-url.tracking.fields.ip_address' => $settings['tracking_fields_ip_address'],
|
||||
'filament-short-url.tracking.fields.browser' => $settings['tracking_fields_browser'],
|
||||
'filament-short-url.tracking.fields.browser_version' => $settings['tracking_fields_browser_version'],
|
||||
'filament-short-url.tracking.fields.operating_system' => $settings['tracking_fields_operating_system'],
|
||||
'filament-short-url.tracking.fields.operating_system_version' => $settings['tracking_fields_operating_system_version'],
|
||||
'filament-short-url.tracking.fields.referer_url' => $settings['tracking_fields_referer_url'],
|
||||
'filament-short-url.tracking.fields.device_type' => $settings['tracking_fields_device_type'],
|
||||
'filament-short-url.qr_defaults.size' => $settings['qr_size'],
|
||||
'filament-short-url.qr_defaults.margin' => $settings['qr_margin'],
|
||||
'filament-short-url.qr_defaults.dot_style' => $settings['qr_dot_style'],
|
||||
'filament-short-url.qr_defaults.foreground_color' => $settings['qr_foreground_color'],
|
||||
'filament-short-url.qr_defaults.background_color' => $settings['qr_background_color'],
|
||||
'filament-short-url.qr_defaults.gradient_enabled' => $settings['qr_gradient_enabled'],
|
||||
'filament-short-url.qr_defaults.gradient_from' => $settings['qr_gradient_from'],
|
||||
'filament-short-url.qr_defaults.gradient_to' => $settings['qr_gradient_to'],
|
||||
'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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user