Compare commits

...

10 Commits

Author SHA1 Message Date
idevakk
c312ec3325 feat: Prepare Zemailnator for Dokploy deployment
- Add highly optimized Dockerfile with Nginx and PHP-FPM 8.4
- Add docker-compose.yml configured with Redis and MariaDB 10.11
- Implement entrypoint.sh and supervisord.conf for background workers
- Refactor legacy IMAP scripts into scheduled Artisan Commands
- Secure app by removing old routes with hardcoded basic auth credentials
- Configure email attachments to use Laravel Storage instead of insecure public/tmp
2026-02-28 23:17:39 +05:30
idevakk
bf5b797cd8 fix(deps): bump dependencies version 2026-02-12 23:00:34 +05:30
idevakk
50797b25e9 fix(page): correct page content layout from horizontal to vertical stacking 2025-12-15 13:51:56 +05:30
idevakk
bd5eade3d2 fix(addon): correct blog array access in template
Change object property access to array access for blog data in add-on template.
  The config('app.blogs') returns arrays not objects, causing "Attempt to read
  property 'slug' on array" error on /temp-gmail route.

  Fixes $blog->slug, $blog->post_image, and $blog->post to use array syntax
  in resources/views/livewire/add-on.blade.php:607,614,616
2025-12-10 03:13:47 -08:00
idevakk
77c6c5f73d fix(blog): correct post content layout from horizontal to vertical stacking
Remove flex items-center from blog post content container that was causing
  content elements to display horizontally instead of their natural vertical
  flow. This ensures proper text and element stacking within individual
  blog posts.

  Fixes layout issue in resources/views/livewire/blog.blade.php
2025-12-10 03:06:04 -08:00
idevakk
29b20a3856 refactor: improve sandbox configuration parsing 2025-12-10 03:28:00 +05:30
idevakk
9307b4e3e4 refactor(users): use unified subscription scopes in filter
- Replace manual subscription queries with dedicated User model scopes
  - Use withActiveSubscription() and withoutActiveSubscription() methods
  - Improve consistency and maintainability of subscription logic
  - Remove Stripe-specific field references for multi-provider support
2025-12-09 11:44:19 -08:00
idevakk
4028a9a21e feat(usernames): update unique constraint to composite with type and provider
- Remove single unique constraint on username
  - Add composite unique constraint on username, username_type, and provider_type
  - Update Filament username form validation with custom unique rule
  - Add descriptive validation message for duplicate usernames
2025-12-09 10:14:50 -08:00
idevakk
1c4298cdaf feat(domains): update unique constraint to composite with type and provider
- Remove single unique constraint on domain name
  - Add composite unique constraint on name, domain_type, and provider_type
  - Update Filament domain form validation with custom unique rule
  - Add descriptive validation message for duplicate domains
2025-12-08 10:57:56 -08:00
idevakk
8d8657cc5c chore: add ADMIN_EMAIL key in .env.example 2025-12-08 09:55:27 -08:00
108 changed files with 1867 additions and 1387 deletions

17
.docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -e
echo "Storage symlink..."
php artisan storage:link || true
echo "Optimize cache..."
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "Running migrations..."
php artisan migrate --force || true
# Pass hand off to supervisor
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

32
.docker/nginx.conf Normal file
View File

@@ -0,0 +1,32 @@
server {
listen 80;
server_name _;
root /var/www/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

51
.docker/supervisord.conf Normal file
View File

@@ -0,0 +1,51 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm -F
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
[program:laravel-scheduler]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan schedule:work
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0

View File

@@ -4,6 +4,13 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
ADMIN_EMAIL=
#Feature Flags Start
MAIL_HANDLER_API=true
#Feature Flags End
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
@@ -77,8 +84,8 @@ NOTIFY_TG_CHAT_ID=
FORCE_DB_MAIL=false
AUTO_FETCH_MAIL=false
FETCH_FETCH_FOR_DB=true
FETCH_FROM_REMOTE_DB=true
FETCH_FETCH_FOR_DB=false
FETCH_FROM_REMOTE_DB=false
MOVE_OR_DELETE=delete
#Provide Mailbox Folder Name to Which want to move, else put 'delete' to remove

76
Project.md Normal file
View File

@@ -0,0 +1,76 @@
---
description: Zemailnator - AI-First Project Documentation
---
# Zemailnator Project Documentation
> **AI INSTRUCTION**: This `Project.md` file contains critical context for the Zemailnator (Disposable Email Generator) application. When interacting with this project, ALWAYS load and review this file as part of your system instructions. Treat these guidelines, architectural details, and known security flaws as your primary working context.
## 1. Project Overview & Architecture
Zemailnator is a scalable disposable temporary email service (like Mailinator or 10 Minute Mail) built on the Laravel framework. It provides users with instant, disposable email addresses to prevent spam, with premium tiers offering custom domains, 10-minute validity enhancements, and bulk generation capabilities.
**Core Mechanics:**
- The platform uses IMAP (`ddeboer/imap` via PHP `ext-imap`) to fetch emails from a master mailbox.
- `App\Models\Email::fetchProcessStoreEmail()` processes incoming messages, parses HTML/Text bodies, extracts allowed attachments to `public/tmp/attachments`, and stores the records in the database (or a remote database if `config('app.fetch_from_remote_db')` is true).
- External cron/scripts (like `dropmail.php`) are used to automatically purge old messages and attachments to keep the platform lightweight.
## 2. Technology Stack
- **Framework:** Laravel 12.x
- **PHP Version:** >= 8.2
- **Frontend / UI:** Livewire v3, Flux UI Free, Tailwind CSS v4, Vite
- **Admin Panel:** Filament v4
- **Billing:** Laravel Cashier (Stripe) v15, alongside a custom Unified Payment System (OxaPay, etc.)
- **Testing:** Pest v3
- **Email Parsing:** PHP `ext-imap`, `ddeboer/imap`
## 3. Key Features
- **Disposable Email Generation:** Quick generation of random or temporary emails via Livewire components (e.g., `/mailbox`, `/gmailnator`).
- **Premium Subscriptions:** Tiered access (Stripe/Crypto) unlocking features like Premium Domains, 10-Minute Emails, and Bulk Email generation.
- **Admin Dashboard:** Fully managed via Filament, including logs functionality, failed jobs monitoring, and dynamic database configuration.
- **Impersonation:** Admins can impersonate users for troubleshooting (`ImpersonationController`).
## 4. Known Security Flaws & Vulnerabilities (CRITICAL)
When working on this codebase, prioritize addressing or being mindful of the following security issues:
1. **Hardcoded Credentials in Routes:**
- Found in `routes/web.php` for endpoints `/0xdash/slink` and `/0xdash/scache`. They use basic HTTP authentication with a hardcoded password (`admin@9608`). This is highly insecure and should be migrated to secure Artisan commands via the Filament Admin panel or proper token-based middleware.
2. **Public Attachment Storage:**
- Attachments are stored directly in `public/tmp/attachments`. While there is an extension filter in `Email::fetchProcessStoreEmail()`, any bypass of this filter (e.g., uploading a `.php` file masquerading as another or zero-byte vulnerabilities) can lead to direct Remote Code Execution (RCE) since the directory is publicly accessible.
3. **No Strict Rate Limiting on Generation Endpoints:**
- Temporary email and bulk email endpoints in `routes/web.php` appear to lack specific rate limiting, making the application vulnerable to Denial of Service (DoS) and excessive spam generation.
4. **Standalone PHP Scripts:**
- Scripts like `dropmail.php`, `closeTicket.php`, and `cleanCron.php` inside the public/root directory bootstrap Laravel manually. They lack proper authentication and could potentially be executed repeatedly by unauthorized entities, causing database or IMAP server exhaustion. They should be migrated to Laravel Console Commands (`app/Console/Commands`).
## 5. Pros & Cons
**Pros:**
- **Modern Stack:** Utilizes bleeding-edge tools (Laravel 12, Livewire 3, Tailwind 4) resulting in a highly responsive SPA-like feel.
- **Robust Admin Panel:** Leveraging Filament provides out-of-the-box CRUD, logging, and mail monitoring.
- **Flexible Monetization:** Mix of traditional Stripe and unified payments (crypto via OxaPay) allows for diverse revenue streams.
**Cons:**
- **Technical Debt:** Legacy payment routes (`checkout/{plan}`) exist alongside the unified payment system, leading to code fragmentation.
- **Synchronous IMAP Polling:** Running IMAP fetch directly or via sequential cron is slow. It should be offloaded to robust Laravel Queue workers.
- **File Management:** Relying on basic `file_put_contents` for attachments locally limits horizontal scalability (e.g., scaling across multiple Docker pods/servers). S3 or equivalent Object Storage should be used.
## 6. Development Guide & Setup Best Practices
1. Ensure both `ext-imap` and `ext-curl` are enabled in your `php.ini`.
2. Run `npm run dev` in tandem with `php artisan serve` if working on Frontend/FluxUI adjustments.
3. To test email fetching locally without an active IMAP server, consider writing Pest tests mocking the `ConnectionInterface`.
4. Always use `php artisan test` before committing. The project uses Pest. Make sure datasets and mocking are used properly per the `CLAUDE.md` guidelines.
## 7. Examples / Workflows
**Example: Fetching Emails**
The `Email` model has a primary function `fetchProcessStoreEmail()`:
```php
// Call this to manually pull emails
App\Models\Email::fetchProcessStoreEmail();
```
**Example: Reading Emails (Livewire)**
The `Mailbox` component fetches emails for the active user session or session ID, parsing them via:
```php
$emails = App\Models\Email::parseEmail($currentUserEmailAddress, $deletedArray);
```
## 8. AI Auto-Load Instruction
**For AI Agents:** Whenever you receive a new chat prompt within this workspace, your implicit first step must be to cross-reference the user's request against this `Project.md` document. Pay special attention to the Laravel 12 / Livewire 3 structures and strictly avoid executing basic auth routes or standard PHP scripts directly if Laravel-native alternatives exist. Use Pest for all tests. ALWAYS rely on the `CLAUDE.md` guidelines as your secondary set of standard rules.

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Email;
use Illuminate\Console\Command;
class CleanAttachmentsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'attachments:clean';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean old email attachments from disk';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting to clean attachments...');
Email::deleteBulkAttachments();
$this->info('Finished cleaning attachments.');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Email;
use Illuminate\Console\Command;
class CleanMailboxCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mailbox:clean';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clean old messages from the IMAP mailbox';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting mailbox cleanup...');
$result = Email::cleanMailbox();
$this->info($result);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Email;
use Illuminate\Console\Command;
class FetchEmailsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'emails:fetch';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch, process, and store emails from the IMAP server';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting to fetch emails from IMAP server...');
Email::fetchProcessStoreEmail();
$this->info('Finished fetching emails.');
}
}

View File

@@ -13,7 +13,6 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Inerba\DbConfig\AbstractPageSettings;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
class ImapSettings extends AbstractPageSettings
{
@@ -145,7 +144,7 @@ class ImapSettings extends AbstractPageSettings
$fields = ['host', 'port', 'username', 'password', 'encryption', 'validate_cert', 'protocol'];
foreach ($fields as $field) {
$key = $sectionName . '.' . $field;
$key = $sectionName.'.'.$field;
// Try different data structure approaches
$value = null;
@@ -186,8 +185,8 @@ class ImapSettings extends AbstractPageSettings
return [
'section' => ucfirst($sectionName),
'success' => false,
'message' => "Missing required fields: " . $missingFields->join(', '),
'details' => null
'message' => 'Missing required fields: '.$missingFields->join(', '),
'details' => null,
];
}
@@ -198,7 +197,7 @@ class ImapSettings extends AbstractPageSettings
'section' => ucfirst($sectionName),
'success' => false,
'message' => 'IMAP extension is not loaded in your web server. Please check your Herd PHP configuration or restart your server.',
'details' => null
'details' => null,
];
}
@@ -210,7 +209,7 @@ class ImapSettings extends AbstractPageSettings
'password' => $config['password'],
'encryption' => $config['encryption'] ?? 'none',
'validate_cert' => $config['validate_cert'] ?? false,
'protocol' => $config['protocol'] ?? 'imap'
'protocol' => $config['protocol'] ?? 'imap',
];
// Test connection using the existing ZEmail::connectMailBox method
@@ -224,8 +223,8 @@ class ImapSettings extends AbstractPageSettings
'host' => $config['host'],
'port' => $config['port'],
'encryption' => $config['encryption'] ?? 'none',
'protocol' => $config['protocol'] ?? 'imap'
]
'protocol' => $config['protocol'] ?? 'imap',
],
];
} catch (\Exception $e) {
@@ -244,13 +243,12 @@ class ImapSettings extends AbstractPageSettings
'host' => $config['host'] ?? null,
'port' => $config['port'] ?? null,
'encryption' => $config['encryption'] ?? 'none',
'protocol' => $config['protocol'] ?? 'imap'
]
'protocol' => $config['protocol'] ?? 'imap',
],
];
}
}
/**
* Send appropriate notification based on test results.
*/
@@ -294,6 +292,7 @@ class ImapSettings extends AbstractPageSettings
$details[] = "{$result['section']}: {$result['details']['messages']} messages";
}
}
return implode(' | ', $details);
}
@@ -306,6 +305,7 @@ class ImapSettings extends AbstractPageSettings
foreach ($results as $result) {
$details[] = "{$result['section']}: {$result['message']}";
}
return implode(' | ', $details);
}
@@ -319,6 +319,7 @@ class ImapSettings extends AbstractPageSettings
$status = $result['success'] ? '✅' : '❌';
$details[] = "{$status} {$result['section']}";
}
return implode(' | ', $details);
}
}

View File

@@ -27,7 +27,6 @@ use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use UnitEnum;
class ImpersonationLogViewer extends Page implements HasForms, HasTable
@@ -132,10 +131,10 @@ class ImpersonationLogViewer extends Page implements HasForms, HasTable
TextColumn::make('duration_in_minutes')
->label('Duration')
->formatStateUsing(function ($record) {
return match(true) {
!$record->duration_in_minutes => 'Active',
return match (true) {
! $record->duration_in_minutes => 'Active',
$record->duration_in_minutes < 60 => "{$record->duration_in_minutes}m",
default => round($record->duration_in_minutes / 60, 1) . 'h',
default => round($record->duration_in_minutes / 60, 1).'h',
};
})
->sortable()
@@ -324,7 +323,7 @@ class ImpersonationLogViewer extends Page implements HasForms, HasTable
->latest('start_time')
->get();
$filename = 'impersonation_logs_' . now()->format('Y_m_d_H_i_s') . '.csv';
$filename = 'impersonation_logs_'.now()->format('Y_m_d_H_i_s').'.csv';
// Create a temporary file
$handle = fopen('php://temp', 'r+');
@@ -376,7 +375,7 @@ class ImpersonationLogViewer extends Page implements HasForms, HasTable
$filename,
[
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]
);
}

View File

@@ -2,13 +2,12 @@
namespace App\Filament\Resources;
use BackedEnum;
use UnitEnum;
use App\Filament\Resources\BlogResource\Pages\CreateBlog;
use App\Filament\Resources\BlogResource\Pages\EditBlog;
use App\Filament\Resources\BlogResource\Pages\ListBlogs;
use App\Models\Blog;
use App\Models\Category;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
@@ -30,6 +29,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use UnitEnum;
class BlogResource extends Resource
{

View File

@@ -2,12 +2,11 @@
namespace App\Filament\Resources;
use BackedEnum;
use UnitEnum;
use App\Filament\Resources\CategoryResource\Pages\CreateCategory;
use App\Filament\Resources\CategoryResource\Pages\EditCategory;
use App\Filament\Resources\CategoryResource\Pages\ListCategories;
use App\Models\Category;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
@@ -23,6 +22,7 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use UnitEnum;
class CategoryResource extends Resource
{
@@ -60,7 +60,7 @@ class CategoryResource extends Resource
TextColumn::make('slug'),
TextColumn::make('blogs_count')
->label('Blogs')
->getStateUsing(fn(Category $record): int => $record->blogs()->count()),
->getStateUsing(fn (Category $record): int => $record->blogs()->count()),
IconColumn::make('is_active')->label('Active')->boolean(),
])
->filters([

View File

@@ -9,7 +9,9 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Unique;
class DomainForm
{
@@ -20,7 +22,19 @@ class DomainForm
TextInput::make('name')
->columnSpan(1)
->helperText('Domain name: example.com')
->required(),
->required()
->unique(
table: 'domains',
ignoreRecord: true,
modifyRuleUsing: function (Unique $rule, Get $get) {
return $rule
->where('domain_type', $get('domain_type'))
->where('provider_type', $get('provider_type'));
},
)
->validationMessages([
'unique' => 'The domain name already exists for this type and provider.',
]),
TextInput::make('daily_mailbox_limit')
->integer()
->minValue(1)

View File

@@ -2,12 +2,11 @@
namespace App\Filament\Resources;
use BackedEnum;
use UnitEnum;
use App\Filament\Resources\MenuResource\Pages\CreateMenu;
use App\Filament\Resources\MenuResource\Pages\EditMenu;
use App\Filament\Resources\MenuResource\Pages\ListMenus;
use App\Models\Menu;
use BackedEnum;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
@@ -21,6 +20,7 @@ use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use UnitEnum;
class MenuResource extends Resource
{

View File

@@ -2,12 +2,11 @@
namespace App\Filament\Resources;
use BackedEnum;
use UnitEnum;
use App\Filament\Resources\PageResource\Pages\CreatePage;
use App\Filament\Resources\PageResource\Pages\EditPage;
use App\Filament\Resources\PageResource\Pages\ListPages;
use App\Models\Page;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
@@ -29,6 +28,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use UnitEnum;
class PageResource extends Resource
{

View File

@@ -2,12 +2,12 @@
namespace App\Filament\Resources\PaymentProviders\Tables;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;

View File

@@ -301,6 +301,7 @@ class PlanResource extends Resource
// Halt the bulk deletion process
$action->halt();
return;
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Filament\Resources;
use BackedEnum;
use UnitEnum;
use App\Filament\Resources\TicketResource\Pages\CreateTicket;
use App\Filament\Resources\TicketResource\Pages\EditTicket;
use App\Filament\Resources\TicketResource\Pages\ListTickets;
@@ -11,6 +9,7 @@ use App\Filament\Resources\TicketResource\RelationManagers\ResponsesRelationMana
use App\Mail\TicketResponseNotification;
use App\Models\Ticket;
use App\Models\TicketResponse;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
@@ -33,6 +32,7 @@ use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\HtmlString;
use UnitEnum;
class TicketResource extends Resource
{
@@ -120,7 +120,7 @@ class TicketResource extends Resource
DatePicker::make('created_from')->label('Created From'),
DatePicker::make('created_until')->label('Created Until'),
])
->query(fn($query, array $data) => $query
->query(fn ($query, array $data) => $query
->when($data['created_from'], fn ($query, $date) => $query->whereDate('created_at', '>=', $date))
->when($data['created_until'], fn ($query, $date) => $query->whereDate('created_at', '<=', $date))),
])
@@ -134,7 +134,7 @@ class TicketResource extends Resource
Action::make('view')
->label('View & Respond')
->icon('heroicon-o-eye')
->schema(fn(Ticket $ticket): array => [
->schema(fn (Ticket $ticket): array => [
TextArea::make('response')
->label('Your Response')
->required()

View File

@@ -34,6 +34,7 @@ class TrialExtensionForm
if ($subscription->trial_ends_at) {
$label .= " ({$subscription->trial_ends_at->format('M j, Y')})";
}
return [$subscription->id => $label];
})
->toArray();
@@ -158,12 +159,14 @@ class TrialExtensionForm
if (! $subscriptionId || ! $extensionDays) {
$set('new_trial_ends_at', null);
return;
}
$subscription = Subscription::find($subscriptionId);
if (! $subscription) {
$set('new_trial_ends_at', null);
return;
}

View File

@@ -96,7 +96,7 @@ class TrialExtensionsTable
->label('View Subscription')
->icon('heroicon-o-rectangle-stack')
->color('blue')
->url(fn ($record) => route('filament.' . filament()->getCurrentPanel()->getId() . '.resources.subscriptions.edit', $record->subscription_id))
->url(fn ($record) => route('filament.'.filament()->getCurrentPanel()->getId().'.resources.subscriptions.edit', $record->subscription_id))
->openUrlInNewTab(),
])
->toolbarActions([

View File

@@ -7,7 +7,6 @@ use App\Filament\Resources\TrialExtensions\Pages\EditTrialExtension;
use App\Filament\Resources\TrialExtensions\Pages\ListTrialExtensions;
use App\Filament\Resources\TrialExtensions\Schemas\TrialExtensionForm;
use App\Filament\Resources\TrialExtensions\Tables\TrialExtensionsTable;
use App\Models\Subscription;
use App\Models\TrialExtension;
use BackedEnum;
use Filament\Resources\Resource;

View File

@@ -130,15 +130,9 @@ class UserResource extends Resource
])
->query(function ($query, array $data): void {
if ($data['value'] === 'subscribed') {
$query->whereHas('subscriptions', function ($query): void {
$query->where('stripe_status', 'active')
->orWhere('stripe_status', 'trialing');
});
$query->withActiveSubscription();
} elseif ($data['value'] === 'not_subscribed') {
$query->whereDoesntHave('subscriptions', function ($query): void {
$query->where('stripe_status', 'active')
->orWhere('stripe_status', 'trialing');
});
$query->withoutActiveSubscription();
}
}),
SelectFilter::make('email_verified')

View File

@@ -9,7 +9,9 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Unique;
class UsernameForm
{
@@ -21,7 +23,19 @@ class UsernameForm
->columnSpan(1)
->alphaDash()
->helperText('Email: myusername@gmail.com | Username: myusername')
->required(),
->required()
->unique(
table: 'usernames',
ignoreRecord: true,
modifyRuleUsing: function (Unique $rule, Get $get) {
return $rule
->where('username_type', $get('username_type'))
->where('provider_type', $get('provider_type'));
},
)
->validationMessages([
'unique' => 'The username already exists for this type and provider.',
]),
TextInput::make('daily_mailbox_limit')
->integer()
->minValue(1)

View File

@@ -2,8 +2,6 @@
namespace App\Filament\Widgets;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use App\Models\Log;
use App\Models\Meta;
use App\Models\PremiumEmail;
@@ -11,6 +9,8 @@ use App\Models\Ticket;
use App\Models\User;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
class StatsOverview extends BaseWidget
{

View File

@@ -14,7 +14,7 @@ class ArrayHelper
$keys = explode('.', $key);
foreach ($keys as $segment) {
if (!isset($array[$segment])) {
if (! isset($array[$segment])) {
return null;
}
$array = $array[$segment];
@@ -32,7 +32,7 @@ class ArrayHelper
);
} catch (\JsonException $e) {
// Optional: Log the error
Log::error("JSON encode failed: " . $e->getMessage());
Log::error('JSON encode failed: '.$e->getMessage());
// Fallback: return empty object instead of crashing
return '{}';

View File

@@ -2,11 +2,10 @@
namespace App\Http\Controllers;
use Session;
use Illuminate\Routing\Redirector;
use Illuminate\Http\RedirectResponse;
use App\Models\Premium;
use App\Models\ZEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
class AppController extends Controller
{
@@ -56,6 +55,7 @@ class AppController extends Controller
return to_route('mailbox');
}
return to_route('home');
}
@@ -86,6 +86,7 @@ class AppController extends Controller
return to_route('dashboard.premium');
}
return to_route('dashboard');
}

View File

@@ -18,7 +18,7 @@ class PaymentCancelController extends Controller
Log::info('PaymentCancelController: Cancellation page accessed', [
'user_id' => auth()->id(),
'session_token' => $sessionToken ? substr($sessionToken, 0, 20) . '...' : 'none',
'session_token' => $sessionToken ? substr($sessionToken, 0, 20).'...' : 'none',
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
@@ -48,4 +48,4 @@ class PaymentCancelController extends Controller
'recentSubscription' => $recentSubscription,
]);
}
}
}

View File

@@ -2,9 +2,9 @@
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Auth;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class CheckUserBanned

View File

@@ -2,8 +2,8 @@
namespace App\Livewire\Actions;
use Illuminate\Routing\Redirector;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;

View File

@@ -2,10 +2,10 @@
namespace App\Livewire;
use Illuminate\Support\Facades\Session;
use App\Models\ZEmail;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use App\Models\ZEmail;
use Illuminate\Support\Facades\Session;
use Livewire\Component;
class AddOn extends Component
@@ -31,6 +31,7 @@ class AddOn extends Component
if (count($messages['data']) > 0) {
return to_route('mailbox');
}
return null;
}
@@ -48,7 +49,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -66,7 +67,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -85,7 +86,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -103,7 +104,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -121,7 +122,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -139,7 +140,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [
@@ -157,7 +158,7 @@ class AddOn extends Component
$this->faqSchema = [
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => collect($this->faqs)->map(fn(array $faq): array => [
'mainEntity' => collect($this->faqs)->map(fn (array $faq): array => [
'@type' => 'Question',
'name' => $faq['title'],
'acceptedAnswer' => [

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Dashboard;
use App\Models\Subscription;
use App\Models\UsageLog;
use Exception;
use Illuminate\Http\Request;
@@ -10,7 +11,6 @@ use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
use Stripe\StripeClient;
use App\Models\Subscription;
class Dashboard extends Component
{

View File

@@ -2,11 +2,11 @@
namespace App\Livewire\Dashboard;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Request;
use App\Models\Ticket;
use App\Models\TicketResponse;
use Exception;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Livewire\Component;
class Support extends Component
@@ -129,9 +129,9 @@ class Support extends Component
public function updateTicketCounts(): void
{
$this->open = $this->tickets->filter(fn($ticket): bool => in_array($ticket->status, ['open', 'pending']))->count();
$this->open = $this->tickets->filter(fn ($ticket): bool => in_array($ticket->status, ['open', 'pending']))->count();
$this->closed = $this->tickets->filter(fn($ticket): bool => $ticket->status === 'closed')->count();
$this->closed = $this->tickets->filter(fn ($ticket): bool => $ticket->status === 'closed')->count();
}
protected function getClientIp()

View File

@@ -2,9 +2,9 @@
namespace App\Livewire\Frontend;
use Illuminate\Routing\Redirector;
use Illuminate\Http\RedirectResponse;
use App\Models\ZEmail;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Livewire\Component;
class Email extends Component

View File

@@ -40,6 +40,7 @@ class Mailbox extends Component
if (! ZEmail::getEmail()) {
return to_route('home');
}
return null;
}

View File

@@ -2,9 +2,9 @@
namespace App\Livewire;
use App\Models\ZEmail;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use App\Models\ZEmail;
use Livewire\Component;
class Home extends Component
@@ -18,6 +18,7 @@ class Home extends Component
if (count($messages['data']) > 0) {
return to_route('mailbox');
}
return null;
}

View File

@@ -18,9 +18,7 @@ class TicketResponseNotification extends Mailable
/**
* Create a new message instance.
*/
public function __construct(public Ticket $ticket, public Collection $responses)
{
}
public function __construct(public Ticket $ticket, public Collection $responses) {}
/**
* Get the message envelope.

View File

@@ -2,18 +2,17 @@
namespace App\Models;
use DateTimeImmutable;
use Illuminate\Support\Facades\Date;
use App\ColorPicker;
use Carbon\CarbonImmutable;
use DateTime;
use DateTimeImmutable;
use Ddeboer\Imap\ConnectionInterface;
use Ddeboer\Imap\Search\Date\Since;
use Ddeboer\Imap\Server;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
@@ -78,7 +77,7 @@ class Email extends Model
$sender = $message->getFrom();
$date = $message->getDate();
if (!$date instanceof DateTimeImmutable) {
if (! $date instanceof DateTimeImmutable) {
$date = new DateTime;
if ($message->getHeaders()->get('udate')) {
$date->setTimestamp($message->getHeaders()->get('udate'));
@@ -100,11 +99,11 @@ class Email extends Model
$obj = [];
$to = $message->getHeaders()->get('To') ? array_map(fn($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('To')) : [];
$to = $message->getHeaders()->get('To') ? array_map(fn ($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('To')) : [];
$cc = $message->getHeaders()->get('Cc') ? array_map(fn($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('Cc')) : [];
$cc = $message->getHeaders()->get('Cc') ? array_map(fn ($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('Cc')) : [];
$bcc = $message->getHeaders()->get('Bcc') ? array_map(fn($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('Bcc')) : [];
$bcc = $message->getHeaders()->get('Bcc') ? array_map(fn ($entry): string => $entry->mailbox.'@'.$entry->host, $message->getHeaders()->get('Bcc')) : [];
$messageTime = $message->getDate();
$utcTime = CarbonImmutable::instance($messageTime)->setTimezone('UTC')->toDateTimeString();
@@ -127,28 +126,27 @@ class Email extends Model
if ($message->hasAttachments()) {
$attachments = $message->getAttachments();
$directory = './tmp/attachments/'.$obj['id'].'/';
$directoryPath = 'attachments/'.$obj['id'];
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
foreach ($attachments as $attachment) {
$filenameArray = explode('.', (string) $attachment->getFilename());
$extension = $filenameArray[count($filenameArray) - 1];
if (in_array($extension, $allowed)) {
if (! file_exists($directory.$attachment->getFilename())) {
$filePath = $directoryPath.'/'.$attachment->getFilename();
if (! \Illuminate\Support\Facades\Storage::disk('public')->exists($filePath)) {
try {
file_put_contents(
$directory.$attachment->getFilename(),
\Illuminate\Support\Facades\Storage::disk('public')->put(
$filePath,
$attachment->getDecodedContent()
);
} catch (Exception $e) {
Log::error($e->getMessage());
}
}
if ($attachment->getFilename() !== 'undefined') {
$url = config('app.settings.app_base_url').str_replace('./', '/', $directory.$attachment->getFilename());
$url = config('app.settings.app_base_url').'/storage/'.$filePath;
$structure = $attachment->getStructure();
if (isset($structure->id) && str_contains($obj['content'], trim($structure->id, '<>'))) {
$obj['content'] = str_replace('cid:'.trim($structure->id, '<>'), $url, $obj['content']);
@@ -159,9 +157,7 @@ class Email extends Model
];
}
}
}
}
$response['data'][] = $obj;
@@ -354,11 +350,9 @@ class Email extends Model
public static function deleteBulkAttachments(): void
{
$dir = public_path('/tmp/attachments');
try {
if (File::exists($dir)) {
File::cleanDirectory($dir);
if (\Illuminate\Support\Facades\Storage::disk('public')->exists('attachments')) {
\Illuminate\Support\Facades\Storage::disk('public')->deleteDirectory('attachments');
}
} catch (Exception $e) {
Log::error($e->getMessage());
@@ -431,6 +425,7 @@ class Email extends Model
$currentTime = Date::now('UTC');
$lastRecordTime = Date::parse($latestRecord->timestamp);
return $lastRecordTime->diffInMinutes($currentTime) < 5;
}

View File

@@ -2,9 +2,9 @@
namespace App\Models;
use Illuminate\Support\Facades\Date;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Date;
class Log extends Model
{

View File

@@ -2,10 +2,10 @@
namespace App\Models;
use DateTimeImmutable;
use App\ColorPicker;
use Carbon\Carbon;
use DateTime;
use DateTimeImmutable;
use Ddeboer\Imap\Search\Email\Cc;
use Ddeboer\Imap\Search\Email\To;
use Ddeboer\Imap\SearchExpression;
@@ -41,7 +41,7 @@ class Message extends Model
$message->attachments = $request->get('attachment-info');
$message->save();
$directory = './attachments/'.$message->id;
if (!is_dir($directory)) {
if (! is_dir($directory)) {
mkdir($directory, 0777, true);
}
$attachment_ids = json_decode((string) $request->get('attachment-info'));
@@ -146,7 +146,7 @@ class Message extends Model
$blocked = false;
$sender = $message->getFrom();
$date = $message->getDate();
if (!$date instanceof DateTimeImmutable) {
if (! $date instanceof DateTimeImmutable) {
$date = new DateTime;
if ($message->getHeaders()->get('udate')) {
$date->setTimestamp($message->getHeaders()->get('udate'));
@@ -193,7 +193,7 @@ class Message extends Model
if ($message->hasAttachments() && ! $blocked) {
$attachments = $message->getAttachments();
$directory = './tmp/attachments/'.$obj['id'].'/';
if (!is_dir($directory)) {
if (! is_dir($directory)) {
mkdir($directory, 0777, true);
}
foreach ($attachments as $attachment) {

View File

@@ -2,11 +2,11 @@
namespace App\Models;
use Illuminate\Support\Facades\Date;
use App\ColorPicker;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Validator;
class PremiumEmail extends Model

View File

@@ -2,9 +2,9 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Ddeboer\Imap\ConnectionInterface;
use Ddeboer\Imap\Server;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cookie;
@@ -14,6 +14,7 @@ class ZEmail extends Model
{
use HasFactory;
use HasFactory;
public static function connectMailBox($imap = null): ConnectionInterface
{
if ($imap === null) {
@@ -38,6 +39,7 @@ class ZEmail extends Model
if (Email::mailToDBStatus()) {
return Email::parseEmail($email, $deleted);
}
return Message::fetchMessages($email, $type, $deleted);
}
@@ -57,6 +59,7 @@ class ZEmail extends Model
if (Cookie::has('email')) {
return Cookie::get('email');
}
return $generate ? ZEmail::generateRandomEmail() : null;
}
@@ -65,6 +68,7 @@ class ZEmail extends Model
if (Cookie::has('emails')) {
return unserialize(Cookie::get('emails'));
}
return [];
}

View File

@@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider
$this->appConfig = [
'website_settings' => [],
'imap_settings' => [],
'configuration_settings' => []
'configuration_settings' => [],
];
Log::error($e->getMessage());
}
@@ -114,40 +114,40 @@ class AppServiceProvider extends ServiceProvider
private function loadLegacySettings(): array
{
return [
"app_name" => $this->getConfig('website_settings.app_name'),
"app_version" => $this->getConfig('website_settings.app_version'),
"app_base_url" => $this->getConfig('website_settings.app_base_url'),
"app_admin" => $this->getConfig('website_settings.app_admin'),
"app_title" => $this->getConfig('website_settings.app_title'),
"app_description" => $this->getConfig('website_settings.app_description'),
"app_keywords" => $this->getConfig('website_settings.app_keywords'),
"app_contact" => $this->getConfig('website_settings.app_contact'),
"app_meta" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_meta')),
"app_social" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_social')),
"app_header" => $this->getConfig('website_settings.app_header'),
"app_footer" => $this->getConfig('website_settings.app_footer'),
"imap_settings" => ArrayHelper::jsonEncodeSafe([
"host" => $this->getConfig('imap_settings.public.host'),
"port" => $this->getConfig('imap_settings.public.port'),
"username" => $this->getConfig('imap_settings.public.username'),
"password" => $this->getConfig('imap_settings.public.password'),
"encryption" => $this->getConfig('imap_settings.public.encryption'),
"validate_cert" => $this->getConfig('imap_settings.public.validate_cert'),
"default_account" => $this->getConfig('imap_settings.public.default_account'),
"protocol" => $this->getConfig('imap_settings.public.protocol'),
"cc_check" => $this->getConfig('imap_settings.public.cc_check'),
"premium_host" => $this->getConfig('imap_settings.premium.host'),
"premium_port" => $this->getConfig('imap_settings.premium.port'),
"premium_username" => $this->getConfig('imap_settings.premium.username'),
"premium_password" => $this->getConfig('imap_settings.premium.password'),
"premium_encryption" => $this->getConfig('imap_settings.premium.premium_encryption'),
"premium_validate_cert" => $this->getConfig('imap_settings.premium.validate_cert'),
"premium_default_account" => $this->getConfig('imap_settings.premium.default_account'),
"premium_protocol" => $this->getConfig('imap_settings.premium.protocol'),
"premium_cc_check" => $this->getConfig('imap_settings.premium.cc_check'),
'app_name' => $this->getConfig('website_settings.app_name'),
'app_version' => $this->getConfig('website_settings.app_version'),
'app_base_url' => $this->getConfig('website_settings.app_base_url'),
'app_admin' => $this->getConfig('website_settings.app_admin'),
'app_title' => $this->getConfig('website_settings.app_title'),
'app_description' => $this->getConfig('website_settings.app_description'),
'app_keywords' => $this->getConfig('website_settings.app_keywords'),
'app_contact' => $this->getConfig('website_settings.app_contact'),
'app_meta' => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_meta')),
'app_social' => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_social')),
'app_header' => $this->getConfig('website_settings.app_header'),
'app_footer' => $this->getConfig('website_settings.app_footer'),
'imap_settings' => ArrayHelper::jsonEncodeSafe([
'host' => $this->getConfig('imap_settings.public.host'),
'port' => $this->getConfig('imap_settings.public.port'),
'username' => $this->getConfig('imap_settings.public.username'),
'password' => $this->getConfig('imap_settings.public.password'),
'encryption' => $this->getConfig('imap_settings.public.encryption'),
'validate_cert' => $this->getConfig('imap_settings.public.validate_cert'),
'default_account' => $this->getConfig('imap_settings.public.default_account'),
'protocol' => $this->getConfig('imap_settings.public.protocol'),
'cc_check' => $this->getConfig('imap_settings.public.cc_check'),
'premium_host' => $this->getConfig('imap_settings.premium.host'),
'premium_port' => $this->getConfig('imap_settings.premium.port'),
'premium_username' => $this->getConfig('imap_settings.premium.username'),
'premium_password' => $this->getConfig('imap_settings.premium.password'),
'premium_encryption' => $this->getConfig('imap_settings.premium.premium_encryption'),
'premium_validate_cert' => $this->getConfig('imap_settings.premium.validate_cert'),
'premium_default_account' => $this->getConfig('imap_settings.premium.default_account'),
'premium_protocol' => $this->getConfig('imap_settings.premium.protocol'),
'premium_cc_check' => $this->getConfig('imap_settings.premium.cc_check'),
]),
"configuration_settings" => ArrayHelper::jsonEncodeSafe($this->appConfig['configuration_settings']),
"ads_settings" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.ads_settings')),
'configuration_settings' => ArrayHelper::jsonEncodeSafe($this->appConfig['configuration_settings']),
'ads_settings' => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.ads_settings')),
];
}

View File

@@ -35,7 +35,7 @@ class OxapayProvider implements PaymentProviderContract
$config = array_merge($dbConfig, $config);
$this->config = $config;
$this->sandbox = (bool) ($config['sandbox'] ?? false);
$this->sandbox = $config['sandbox'] === 'true' ?? false;
$this->merchantApiKey = $this->sandbox ? ($config['sandbox_merchant_api_key'] ?? '') : ($config['merchant_api_key'] ?? '');
$this->baseUrl = 'https://api.oxapay.com/v1';

View File

@@ -10,13 +10,13 @@ $app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
try {
// Run the Artisan command 'ping'
$exitCode = $kernel->call('cleanMail');
// Run the Artisan command
$exitCode = $kernel->call('mailbox:clean');
// Get the output of the command
$output = $kernel->output();
echo "Artisan command 'schedule:run' executed successfully. Exit code: $exitCode\n";
echo "Artisan command executed successfully. Exit code: $exitCode\n";
echo "Output:\n$output";
} catch (\Exception $e) {

1793
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ return [
'pattern' => [
'prefix' => 'laravel-',
'date' => '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]',
'extension' => '.log'
'extension' => '.log',
],
/* -----------------------------------------------------------------

View File

@@ -1,8 +1,8 @@
<?php
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [

View File

@@ -2,8 +2,8 @@
namespace Database\Factories;
use App\Models\TicketResponse;
use App\Models\Ticket;
use App\Models\TicketResponse;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

View File

@@ -12,7 +12,7 @@ return new class extends Migration
$table->longText('html')->nullable()->change();
$table->longText('text')->nullable()->change();
});
}
public function down()

View File

@@ -11,7 +11,7 @@ return new class extends Migration
Schema::table(config('mails.database.tables.events', 'mail_events'), function (Blueprint $table) {
$table->longText('link')->nullable()->change();
});
}
public function down()

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateActivityLogTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddEventColumnToActivityLogTable extends Migration
{

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBatchUuidColumnToActivityLogTable extends Migration
{

View File

@@ -2,8 +2,8 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
@@ -18,7 +18,7 @@ return new class extends Migration
try {
// MySQL automatically names check constraints in different ways,
// so we attempt a generic drop and ignore failures.
DB::statement("ALTER TABLE `payment_providers` DROP CHECK `payment_providers.configuration`;");
DB::statement('ALTER TABLE `payment_providers` DROP CHECK `payment_providers.configuration`;');
} catch (\Throwable $e) {
// ignore if constraint does not exist
}
@@ -37,7 +37,7 @@ return new class extends Migration
AND pg_get_constraintdef(pg_constraint.oid) LIKE '%configuration%json%';
");
if (!empty($constraint->conname)) {
if (! empty($constraint->conname)) {
DB::statement("ALTER TABLE payment_providers DROP CONSTRAINT {$constraint->conname};");
}
} catch (\Throwable $e) {

View File

@@ -0,0 +1,37 @@
<?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('domains', function (Blueprint $table) {
// Drop the existing unique constraint on 'name' column
$table->dropUnique('domains_name_unique');
// Add a composite unique constraint on name, domain_type, and provider_type
// This allows the same domain name with different type combinations
$table->unique(['name', 'domain_type', 'provider_type'], 'domains_name_type_provider_unique');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('domains', function (Blueprint $table) {
// Drop the composite unique constraint
$table->dropUnique('domains_name_type_provider_unique');
// Restore the original single unique constraint on 'name'
$table->unique('name', 'domains_name_unique');
});
}
};

View File

@@ -0,0 +1,37 @@
<?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('usernames', function (Blueprint $table) {
// Drop the existing unique constraint on 'username' column
$table->dropUnique('usernames_username_unique');
// Add a composite unique constraint on username, username_type, and provider_type
// This allows the same username with different type combinations
$table->unique(['username', 'username_type', 'provider_type'], 'usernames_username_type_provider_unique');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('usernames', function (Blueprint $table) {
// Drop the composite unique constraint
$table->dropUnique('usernames_username_type_provider_unique');
// Restore the original single unique constraint on 'username'
$table->unique('username', 'usernames_username_unique');
});
}
};

View File

@@ -2,9 +2,9 @@
namespace Database\Seeders;
use stdClass;
use App\Models\Meta;
use Illuminate\Database\Seeder;
use stdClass;
class MetaSeeder extends Seeder
{

View File

@@ -28,15 +28,15 @@ class PaymentProviderSeeder extends Seeder
'secret_key' => env('STRIPE_SECRET') ?: 'sk_test_placeholder',
'publishable_key' => env('STRIPE_PUBLISHABLE_KEY') ?: 'pk_test_placeholder',
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET') ?: 'whsec_placeholder',
'webhook_url' => env('APP_URL', 'https://example.com') . '/webhook/stripe',
'success_url' => env('APP_URL', 'https://example.com') . '/payment/success',
'cancel_url' => env('APP_URL', 'https://example.com') . '/payment/cancel',
'webhook_url' => env('APP_URL', 'https://example.com').'/webhook/stripe',
'success_url' => env('APP_URL', 'https://example.com').'/payment/success',
'cancel_url' => env('APP_URL', 'https://example.com').'/payment/cancel',
'currency' => env('CASHIER_CURRENCY', 'USD'),
],
'supports_recurring' => true,
'supports_one_time' => true,
'supported_currencies' => [
'USD' => 'US Dollar'
'USD' => 'US Dollar',
],
'fee_structure' => [
'fixed_fee' => '0.50',
@@ -56,14 +56,14 @@ class PaymentProviderSeeder extends Seeder
'api_key' => env('LEMON_SQUEEZY_API_KEY', 'lsk_...'),
'store_id' => env('LEMON_SQUEEZY_STORE_ID', '...'),
'webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', 'whsec_...'),
'webhook_url' => env('APP_URL') . '/webhook/lemon-squeezy',
'success_url' => env('APP_URL') . '/payment/success',
'cancel_url' => env('APP_URL') . '/payment/cancel',
'webhook_url' => env('APP_URL').'/webhook/lemon-squeezy',
'success_url' => env('APP_URL').'/payment/success',
'cancel_url' => env('APP_URL').'/payment/cancel',
],
'supports_recurring' => true,
'supports_one_time' => true,
'supported_currencies' => [
'USD' => 'US Dollar'
'USD' => 'US Dollar',
],
'fee_structure' => [
'fixed_fee' => '0.50',
@@ -86,9 +86,9 @@ class PaymentProviderSeeder extends Seeder
'sandbox_api_key' => env('POLAR_SANDBOX_API_KEY', 'pol_test_...'),
'sandbox_webhook_secret' => env('POLAR_SANDBOX_WEBHOOK_SECRET', 'whsec_test_...'),
'access_token' => env('POLAR_ACCESS_TOKEN', 'polar_...'),
'webhook_url' => env('APP_URL') . '/webhook/polar',
'success_url' => env('APP_URL') . '/payment/success',
'cancel_url' => env('APP_URL') . '/payment/cancel',
'webhook_url' => env('APP_URL').'/webhook/polar',
'success_url' => env('APP_URL').'/payment/success',
'cancel_url' => env('APP_URL').'/payment/cancel',
],
'supports_recurring' => true,
'supports_one_time' => true,
@@ -115,9 +115,9 @@ class PaymentProviderSeeder extends Seeder
'sandbox_merchant_api_key' => env('OXAPAY_SANDBOX_MERCHANT_API_KEY', 'merchant_sb_...'),
'payout_api_key' => env('OXAPAY_PAYOUT_API_KEY', 'payout_...'),
'sandbox_payout_api_key' => env('OXAPAY_SANDBOX_PAYOUT_API_KEY', 'payout_sb_...'),
'callback_url' => env('OXAPAY_CALLBACK_URL', env('APP_URL') . '/webhook/oxapay'),
'success_url' => env('APP_URL') . '/payment/success',
'cancel_url' => env('APP_URL') . '/payment/cancel',
'callback_url' => env('OXAPAY_CALLBACK_URL', env('APP_URL').'/webhook/oxapay'),
'success_url' => env('APP_URL').'/payment/success',
'cancel_url' => env('APP_URL').'/payment/cancel',
'sandbox' => env('OXAPAY_SANDBOX', true),
'currency' => env('OXAPAY_CURRENCY', 'USD'), // string
'lifetime' => env('OXAPAY_LIFETIME', 30), // integer · min: 15 · max: 2880
@@ -157,9 +157,9 @@ class PaymentProviderSeeder extends Seeder
'exchange_rate_provider' => env('CRYPTO_EXCHANGE_RATE_PROVIDER', 'coingecko'),
'coingecko_api_key' => env('COINGECKO_API_KEY'),
'blockchair_api_key' => env('BLOCKCHAIR_API_KEY'),
'webhook_url' => env('APP_URL') . '/webhook/crypto',
'success_url' => env('APP_URL') . '/payment/success',
'cancel_url' => env('APP_URL') . '/payment/cancel',
'webhook_url' => env('APP_URL').'/webhook/crypto',
'success_url' => env('APP_URL').'/payment/success',
'cancel_url' => env('APP_URL').'/payment/cancel',
'supported_wallets' => [
'btc' => ['bitcoin', 'lightning'],
'eth' => ['ethereum', 'erc20'],
@@ -227,6 +227,7 @@ class PaymentProviderSeeder extends Seeder
}
} catch (\Exception $e) {
$this->command->error("❌ Error seeding provider {$providerData['name']}: {$e->getMessage()}");
continue;
}
}

61
docker-compose.yml Normal file
View File

@@ -0,0 +1,61 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "${APP_PORT:-8000}:80"
environment:
- APP_ENV=${APP_ENV:-production}
- APP_DEBUG=${APP_DEBUG:-false}
- APP_URL=${APP_URL}
- DB_CONNECTION=${DB_CONNECTION:-mysql}
- DB_HOST=${DB_HOST:-mariadb}
- DB_PORT=${DB_PORT:-3306}
- DB_DATABASE=${DB_DATABASE:-zemail}
- DB_USERNAME=${DB_USERNAME:-zemail_user}
- DB_PASSWORD=${DB_PASSWORD:-secret}
- CACHE_STORE=${CACHE_STORE:-redis}
- QUEUE_CONNECTION=${QUEUE_CONNECTION:-redis}
- SESSION_DRIVER=${SESSION_DRIVER:-redis}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PASSWORD=${REDIS_PASSWORD:-null}
- REDIS_PORT=${REDIS_PORT:-6379}
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost/health" ]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
- app-storage:/var/www/storage
- app-public-tmp:/var/www/public/tmp
depends_on:
- redis
- mariadb
redis:
image: redis:alpine
restart: unless-stopped
volumes:
- redis-data:/data
mariadb:
image: mariadb:10.11
restart: unless-stopped
environment:
- MYSQL_DATABASE=${DB_DATABASE:-zemail}
- MYSQL_USER=${DB_USERNAME:-zemail_user}
- MYSQL_PASSWORD=${DB_PASSWORD:-secret}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-rootsecret}
volumes:
- mariadb-data:/var/lib/mysql
volumes:
app-storage:
app-public-tmp:
redis-data:
mariadb-data:

View File

@@ -11,71 +11,11 @@ $kernel->bootstrap();
set_time_limit(0);
$newTimezone = 'Europe/London';
date_default_timezone_set($newTimezone);
$imapDB = json_decode(config('app.settings.imap_settings') ?: '{}', true);
// Mailbox credentials
$hostname = '{'.($imapDB['host'] ?? 'localhost').':'.($imapDB['port'] ?? '993').'/ssl}INBOX';
$username = $imapDB['username'] ?? '';
$password = $imapDB['password'] ?? '';
// Connect to mailbox
$inbox = imap_open($hostname, $username, $password);
// Check for connection errors
if (! $inbox) {
exit('Could not connect to mailbox: '.imap_last_error());
try {
// Run the new Artisan command fallback
$exitCode = $kernel->call('mailbox:clean');
$output = $kernel->output();
echo $output;
} catch (\Exception $e) {
echo 'Error running Artisan command: '.$e->getMessage();
}
// Get current time in Unix timestamp
$current_time = time();
// Search for messages older than one day
// $search_criteria = 'BEFORE "' . date('d-M-Y', strtotime('-3 hours', $current_time)) . '"';
// $messages = imap_search($inbox, $search_criteria);
$messages = imap_search($inbox, 'ALL');
$batch_size = 10;
$deleted_count = 0;
// if ($messages) {
// $chunks = array_chunk($messages, $batch_size);
// foreach ($chunks as $chunk) {
// foreach ($chunk as $message_number) {
// imap_delete($inbox, $message_number);
// }
// imap_expunge($inbox);
// $deleted_count += count($chunk);
// }
// echo $deleted_count . ' messages older than specified time have been deleted.';
// } else {
// echo 'No messages older than specified time found in mailbox.';
// }
if ($messages) {
$chunks = array_chunk($messages, $batch_size);
foreach ($chunks as $chunk) {
foreach ($chunk as $message_number) {
// Get message header to fetch internal date
$header = imap_headerinfo($inbox, $message_number);
$date_str = $header->date;
$msg_time = strtotime($date_str);
// Check if message is older than 3 hours
if ($msg_time !== false && ($current_time - $msg_time) > 2 * 3600) {
imap_delete($inbox, $message_number);
$deleted_count++;
}
}
imap_expunge($inbox);
}
echo $deleted_count.' messages older than 2 hours have been deleted.';
} else {
echo 'No messages found in mailbox.';
}
// Close mailbox connection
imap_close($inbox);

162
laravel_webhook_handover.md Normal file
View File

@@ -0,0 +1,162 @@
# MailOps Webhook Handover Document
This document provides the exact specifications needed to implement the receiving end of the MailOps email synchronization system within the Laravel application.
## 1. Webhook Endpoint Specification
The MailOps worker will push new emails to this exact endpoint on your Laravel server:
* **URL:** `POST https://your-laravel-app.com/api/webhooks/incoming_email`
* **Headers:**
* `Content-Type: application/json`
* `Authorization: Bearer <CONFIGURED_WEBHOOK_SECRET>` (You must configure this secret in both MailOps and Laravel).
### A. Expected JSON Payload (With Attachments)
```json
{
"hash": "a1b2c3d4e5f6g7h8i9j0...",
"metadata": {
"hash": "a1b2c3d4e5f6g7h8i9j0...",
"recipientEmail": "user@example.com",
"recipientName": "John Doe",
"senderEmail": "alert@service.com",
"senderName": "Service Alerts",
"domain": "example.com",
"subject": "Important Notification",
"received_at": "2026-02-26T17:35:00Z",
"attachments": [
{
"filename": "invoice.pdf",
"mimeType": "application/pdf",
"size": 102400,
"s3_path": "mail-attachments/2026/02/26/hash_invoice.pdf"
}
],
"attachmentSize": 102400
},
"bodyText": "Plain text content...",
"bodyHtml": "<html>HTML content...</html>"
}
```
*(Note: `received_at` is in ISO 8601 format ending with `Z` to explicitly denote UTC. `bodyHtml` and `bodyText` are completely separated from the metadata to optimize database payload sizes).*
### B. Expected JSON Payload (NO Attachments)
When an email has no attachments, the `attachments` array will be empty and `attachmentSize` will be zero. Also, depending on the email client, `bodyHtml` or `bodyText` might be `null`.
```json
{
"hash": "b2c3d4e5f6g7h8i9j0a1...",
"metadata": {
"hash": "b2c3d4e5f6g7h8i9j0a1...",
"recipientEmail": "user@example.com",
"recipientName": "",
"senderEmail": "friend@service.com",
"senderName": "Friend",
"domain": "example.com",
"subject": "Quick Question",
"received_at": "2026-02-26T17:38:12Z",
"attachments": [],
"attachmentSize": 0
},
"bodyText": "Hey, are we still fast approaching the deadline?",
"bodyHtml": null
}
```
---
## 2. Laravel Implementation Checklist
When you switch to the Laravel project, you need to build the following:
### Step 1: Route & Middleware
Define the API route and protect it with a simple Bearer token check.
```php
// routes/api.php
Route::post('/webhooks/incoming_email', [EmailWebhookController::class, 'handle'])
->middleware('verify.webhook.secret');
```
### Step 2: The Controller
The controller persists the metadata to MariaDB and the heavy body to MongoDB. **Crucially**, it also checks if the MongoDB TTL index exists, and if not, automatically creates it using the value defined in your Laravel `.env` file (e.g., `EMAIL_BODY_TTL_SECONDS=259200`).
```php
// app/Http/Controllers/EmailWebhookController.php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
public function handle(Request $request)
{
$payload = $request->all();
$meta = $payload['metadata'];
$hash = $payload['hash'];
// 1. Auto-Setup MongoDB TTL Index (Executes only once via Cache)
$this->ensureMongoTtlIndexExists();
// 2. MariaDB: Save Metadata
Email::updateOrCreate(
['unique_id_hash' => $hash],
[
'recipient_email' => $meta['recipientEmail'],
'sender_email' => $meta['senderEmail'],
'subject' => $meta['subject'] ?? '',
'is_read' => false,
// Parse the ISO 8601 UTC timestamp format explicitly for SQL
'received_at' => Carbon::parse($meta['received_at'])->setTimezone('UTC')->toDateTimeString(),
// Store attachments JSON. If empty, ensure it's saved as an empty array '[]'
'attachments' => !empty($meta['attachments']) ? json_encode($meta['attachments']) : '[]',
'attachment_size' => $meta['attachmentSize'] ?? 0
]
);
// 3. MongoDB: Save the heavy body with TTL
// Assuming you have the jenssegers/mongodb package installed
RecentEmailBody::updateOrCreate(
['unique_id_hash' => $hash],
[
// Handle cases where the sender only sends Text or only HTML
'body_text' => $payload['bodyText'] ?? '',
'body_html' => $payload['bodyHtml'] ?? '',
'created_at' => new \MongoDB\BSON\UTCDateTime(now()->timestamp * 1000), // BSON required for TTL
]
);
return response()->json(['status' => 'success'], 200);
}
/**
* Ensures the TTL index is created on the MongoDB collection.
* Uses Laravel Cache to avoid checking the database on every single webhook.
*/
private function ensureMongoTtlIndexExists()
{
Cache::rememberForever('mongo_ttl_index_created', function () {
// Fetch TTL from Laravel .env (Default: 72 hours / 259200 seconds)
$ttlSeconds = (int) env('EMAIL_BODY_TTL_SECONDS', 259200);
$collection = DB::connection('mongodb')->getCollection('recent_email_bodies');
// Background creation prevents locking the database during webhook execution
$collection->createIndex(
['created_at' => 1],
[
'expireAfterSeconds' => $ttlSeconds,
'background' => true,
'name' => 'ttl_created_at_index' // Named index prevents duplicate recreation errors
]
);
return true;
});
}
```
---
## 3. Resiliency Notes
* **Idempotency:** The MailOps worker might retry a webhook if a network timeout occurs even after Laravel successfully saved it. Your Laravel code MUST use `updateOrCreate` or `INSERT IGNORE` (like the example above) so it doesn't create duplicate emails if the same payload hash is received twice.
* **Timeouts:** The MailOps worker expects a response within 5 to 10 seconds. Do not perform long-running synchronous tasks (like connecting to external APIs or sending heavy push notifications) inside the webhook controller. Dispatch those to a Laravel Queue instead.

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
(()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex)})},syncActionModals(t){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}if(this.actionNestingIndex!==null&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();
(()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex,t.detail.shouldOverlayParentActions??!1)})},syncActionModals(t,i=!1){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}let s=this.actionNestingIndex!==null&&t!==null&&t>this.actionNestingIndex;if(this.actionNestingIndex!==null&&!(i&&s)&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",unsubscribeLivewireHook:null,visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))},destroy(){this.unsubscribeLivewireHook?.()}}}export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function h({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{h as default};
function a({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{if(!Array.isArray(e))return;let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{a as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
function n({initialHeight:e,shouldAutosize:i,state:h}){return{state:h,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=e+"rem")},resize(){if(this.$el.scrollHeight<=0)return;let t=this.$el.style.height;this.$el.style.height="0px";let r=this.$el.scrollHeight;this.$el.style.height=t;let l=parseFloat(e)*parseFloat(getComputedStyle(document.documentElement).fontSize),s=Math.max(r,l)+"px";this.wrapperEl.style.height!==s&&(this.wrapperEl.style.height=s)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{n as default};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function u({activeTab:a,isTabPersistedInQueryString:e,livewireId:h,tab:o,tabQueryStringKey:s}){return{tab:o,init(){let t=this.getTabs(),i=new URLSearchParams(window.location.search);e&&i.has(s)&&t.includes(i.get(s))&&(this.tab=i.get(s)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[a-1]),Livewire.hook("commit",({component:r,commit:f,succeed:c,fail:l,respond:b})=>{c(({snapshot:d,effect:m})=>{this.$nextTick(()=>{if(r.id!==h)return;let n=this.getTabs();n.includes(this.tab)||(this.tab=n[a-1]??this.tab)})})})},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!e)return;let t=new URL(window.location.href);t.searchParams.set(s,this.tab),history.replaceState(null,document.title,t.toString())}}}export{u as default};
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,unsubscribeLivewireHook:null,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:b,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let a=this.getTabs();a.includes(this.tab)||(this.tab=a[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),b=t.map(i=>{let a=i.querySelector(".fi-tabs-item-label"),s=i.querySelector(".fi-badge"),o=Math.ceil(a.clientWidth),l=s?Math.ceil(s.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let a=u.slice(0,i+1).reduce((p,y)=>p+y,0),s=i*n,o=b.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(p=>p.total)):0,D=l?r+W+d+h+n:0;if(a+s+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(s=>s.style.display);n.forEach(s=>s.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),b=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),a=this.findOverflowIndex(n,r,h,b,i,u);n.forEach((s,o)=>s.style.display=d[o]),a!==-1&&(this.withinDropdownIndex=a),this.withinDropdownMounted=!0},destroy(){this.unsubscribeLivewireHook?.(),this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};

View File

@@ -1 +1 @@
function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};
function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToStep(t){let e=this.getStepIndex(t);e<=-1||!s&&e>this.getStepIndex(this.step)||(this.step=t,this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:d,respond:u})=>{n(({snapshot:f,effect:h})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e}}}export{o as default};
function o({name:i,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:r,succeed:a,fail:u,respond:d})=>{a(({snapshot:h,effect:l})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};

View File

@@ -1 +1 @@
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -604,16 +604,16 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 p-1">
@foreach(collect(config('app.blogs'))->take(6) as $blog)
<a href="{{ route('blog', $blog->slug) }}">
<a href="{{ route('blog', $blog['slug']) }}">
<div class="flex items-center">
<div class="group relative mx-auto w-96 overflow-hidden rounded-[16px] dark:bg-zinc-800 bg-zinc-200 p-[1px] ease-in-out hover:bg-gradient-to-r hover:from-zinc-600 hover:via-zinc-800 hover:to-zinc-700">
<div class="group-hover:animate-spin-slow invisible absolute -top-40 -bottom-40 left-10 right-10 bg-gradient-to-r from-transparent via-gray-600 to-transparent group-hover:visible"></div>
<div class="relative rounded-[15px] dark:bg-zinc-900 bg-zinc-100 dark:text-white text-accent-content p-6">
<div class="space-y-4">
<p class="font-md text-slate-500">
<img src="{{ asset('storage/'.$blog->post_image) }}" class="card-img-top" alt="{{ $blog->slug }}">
<img src="{{ asset('storage/'.$blog['post_image']) }}" class="card-img-top" alt="{{ $blog['slug'] }}">
</p>
<p class="text-lg font-semibold dark:text-white text-accent-content truncate">{{ $blog->post }}</p>
<p class="text-lg font-semibold dark:text-white text-accent-content truncate">{{ $blog['post'] }}</p>
</div>
</div>
</div>

View File

@@ -25,7 +25,7 @@
echo $adsSettings->two ?? '';
@endphp
</div>
<div class="flex w-full items-center justify-center px-4 py-2 sm:px-6">
<div class="w-full px-4 py-2 sm:px-6">
<flux:text>{!! $postDetail->content !!}</flux:text>
</div>
<div class="mt-3"></div>

View File

@@ -24,7 +24,7 @@
<flux:heading class="mb-3 truncate" size="xl" level="1">{{ $page->title }}</flux:heading>
<div class="mb-3"></div>
<div class="block rounded-lg bg-white shadow-md dark:bg-zinc-700 items-center p-1">
<div class="flex w-full items-center justify-center px-4 py-4 sm:px-6">
<div class="w-full px-4 py-2 sm:px-6">
<flux:text>{!! $page->content !!}</flux:text>
</div>
</div>

View File

@@ -3,4 +3,4 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', fn(Request $request) => $request->user())->middleware('auth:sanctum');
Route::get('/user', fn (Request $request) => $request->user())->middleware('auth:sanctum');

View File

@@ -1,7 +1,7 @@
<?php
use App\Livewire\Actions\Logout;
use App\Http\Controllers\Auth\VerifyEmailController;
use App\Livewire\Actions\Logout;
use App\Livewire\Auth\ConfirmPassword;
use App\Livewire\Auth\ForgotPassword;
use App\Livewire\Auth\Login;

View File

@@ -1,27 +1,22 @@
<?php
use App\Models\Log;
use App\Models\Email;
use App\Models\Log;
use App\Models\Ticket;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function (): void {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
// Schedule::call(function () {
// Email::fetchProcessStoreEmail();
// })->everyMinute();
Schedule::call(function (): void {
Email::deleteBulkAttachments();
})->daily();
// Schedule::call(function () {
// Email::deleteBulkMailboxes();
// })->everyMinute();
// Schedule the new commands instead of closures
Schedule::command('emails:fetch')->everyMinute();
Schedule::command('attachments:clean')->daily();
Schedule::command('mailbox:clean')->everyTwoHours();
// Keep other necessary schedules
Schedule::call(function (): void {
Email::deleteMessagesFromDB();
})->everyTwoHours();
@@ -30,10 +25,7 @@ Schedule::call(function (): void {
Log::deleteLogsFromDB();
})->everyThreeHours();
Schedule::call(function (): void {
Email::cleanMailbox();
});
// Preserve existing commands
Artisan::command('cleanMail', function (): void {
$this->comment(Email::cleanMailbox());
});

View File

@@ -1,7 +1,6 @@
<?php
use App\Http\Controllers\AppController;
use App\Http\Controllers\ImpersonationController;
use App\Http\Controllers\WebhookController;
use App\Http\Middleware\CheckPageSlug;
@@ -22,11 +21,14 @@ use App\Livewire\Settings\Billing;
use App\Livewire\Settings\Password;
use App\Livewire\Settings\Profile;
use App\Models\Email;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
return response('OK', 200);
});
Route::get('/', Home::class)->name('home');
Route::get('/mailbox', Mailbox::class)->name('mailbox');
Route::get('/mailbox/{email?}', [AppController::class, 'mailbox'])->name('mailboxFromURL');
@@ -71,76 +73,12 @@ Route::middleware(['auth', 'verified', CheckUserBanned::class])->group(function
Route::get('dashboard/compose-email', Dashboard::class)->name('dashboard.compose');
Route::get('dashboard/support', Support::class)->name('dashboard.support');
// LEGACY: Old Stripe Cashier checkout route (deprecated - use unified payment system)
Route::get('checkout/{plan}', function ($pricing_id) {
$plans = config('app.plans');
$pricingData = [];
foreach ($plans as $plan) {
$pricingData[] = $plan['pricing_id'];
}
if (in_array($pricing_id, $pricingData)) {
return auth()->user()
->newSubscription('default', $pricing_id)
->allowPromotionCodes()
->checkout([
'billing_address_collection' => 'required',
'success_url' => route('checkout.success'),
'cancel_url' => route('checkout.cancel'),
]);
}
abort(404);
})->name('checkout');
// LEGACY: Payment status routes (used by both legacy and unified systems)
Route::get('dashboard/success', [Dashboard::class, 'paymentStatus'])->name('checkout.success')->defaults('status', 'success');
Route::get('dashboard/cancel', [Dashboard::class, 'paymentStatus'])->name('checkout.cancel')->defaults('status', 'cancel');
Route::get('dashboard/billing', fn () => auth()->user()->redirectToBillingPortal(route('dashboard')))->name('billing');
Route::get('0xdash/slink', function (Request $request) {
$validUser = 'admin';
$validPass = 'admin@9608'; // 🔐 Change this to something secure
if (! isset($_SERVER['PHP_AUTH_USER']) ||
Request::server('PHP_AUTH_USER') !== $validUser ||
Request::server('PHP_AUTH_PW') !== $validPass) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
echo 'Unauthorized';
exit;
}
Artisan::call('storage:link');
$output = Artisan::output();
return response()->json([
'message' => trim($output),
]);
})->name('storageLink');
Route::get('0xdash/scache', function (Request $request) {
$validUser = 'admin';
$validPass = 'admin@9608'; // 🔐 Change this to something secure
if (! isset($_SERVER['PHP_AUTH_USER']) ||
Request::server('PHP_AUTH_USER') !== $validUser ||
Request::server('PHP_AUTH_PW') !== $validPass) {
header('WWW-Authenticate: Basic realm="Restricted Area"');
header('HTTP/1.0 401 Unauthorized');
echo 'Unauthorized';
exit;
}
Artisan::call('cache:clear');
$output = Artisan::output();
return response()->json([
'message' => trim($output),
]);
})->name('cacheClear');
// Impersonation Routes
Route::prefix('impersonation')->name('impersonation.')->group(function (): void {
Route::post('/stop', [ImpersonationController::class, 'stop'])->name('stop');

View File

@@ -2,11 +2,11 @@
namespace Tests\Concerns;
use Exception;
use Illuminate\Support\Collection;
use App\Models\Blog;
use App\Models\Menu;
use App\Models\Plan;
use Exception;
use Illuminate\Support\Collection;
trait LoadsApplicationData
{
@@ -64,7 +64,7 @@ trait LoadsApplicationData
try {
$menus = cache()->remember('app_menus', now()->addHours(6), Menu::all(...));
$blogs = cache()->remember('app_blogs', now()->addHours(6), fn() => Blog::query()->where('is_published', 1)->get());
$blogs = cache()->remember('app_blogs', now()->addHours(6), fn () => Blog::query()->where('is_published', 1)->get());
$plans = cache()->remember('app_plans', now()->addHours(6), Plan::all(...));
} catch (Exception) {

View File

@@ -2,30 +2,30 @@
namespace Tests\Feature\Filament;
use App\Filament\Resources\TicketResource\Pages\ListTickets;
use App\Filament\Resources\TicketResource\Pages\EditTicket;
use App\Filament\Resources\TicketResource\RelationManagers\ResponsesRelationManager;
use App\Filament\Resources\TicketResource;
use App\Filament\Resources\PlanResource\Pages\ListPlans;
use App\Filament\Resources\PlanResource\Pages\EditPlan;
use App\Filament\Resources\BlogResource\Pages\ListBlogs;
use App\Filament\Resources\BlogResource;
use App\Filament\Resources\BlogResource\Pages\CreateBlog;
use App\Filament\Resources\BlogResource\Pages\EditBlog;
use App\Filament\Resources\CategoryResource\Pages\ListCategories;
use App\Filament\Resources\BlogResource\Pages\ListBlogs;
use App\Filament\Resources\CategoryResource\Pages\CreateCategory;
use App\Filament\Resources\CategoryResource\Pages\EditCategory;
use App\Filament\Resources\PageResource\Pages\ListPages;
use App\Filament\Resources\CategoryResource\Pages\ListCategories;
use App\Filament\Resources\MenuResource\Pages\CreateMenu;
use App\Filament\Resources\MenuResource\Pages\ListMenus;
use App\Filament\Resources\PageResource\Pages\CreatePage;
use App\Filament\Resources\PageResource\Pages\EditPage;
use App\Filament\Resources\MenuResource\Pages\ListMenus;
use App\Filament\Resources\MenuResource\Pages\CreateMenu;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\PageResource\Pages\ListPages;
use App\Filament\Resources\PlanResource;
use App\Filament\Resources\BlogResource;
use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\BlogResource\Pages\CreateBlog;
use App\Filament\Resources\PlanResource\Pages\CreatePlan;
use App\Filament\Resources\PlanResource\Pages\EditPlan;
use App\Filament\Resources\PlanResource\Pages\ListPlans;
use App\Filament\Resources\TicketResource;
use App\Filament\Resources\TicketResource\Pages\CreateTicket;
use App\Filament\Resources\TicketResource\Pages\EditTicket;
use App\Filament\Resources\TicketResource\Pages\ListTickets;
use App\Filament\Resources\TicketResource\RelationManagers\ResponsesRelationManager;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Models\Blog;
use App\Models\Category;
use App\Models\Menu;
@@ -40,6 +40,7 @@ use Tests\TestCase;
class ResourcesTest extends TestCase
{
public $adminUser;
protected function setUp(): void
{
parent::setUp();

View File

@@ -2,12 +2,12 @@
namespace Tests\Feature\Filament;
use App\Filament\Resources\UserResource\RelationManagers\LogsRelationManager;
use App\Filament\Resources\UserResource\RelationManagers\UsageLogsRelationManager;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource\RelationManagers\LogsRelationManager;
use App\Filament\Resources\UserResource\RelationManagers\UsageLogsRelationManager;
use App\Models\Log;
use App\Models\User;
use Livewire\Livewire;
@@ -16,6 +16,7 @@ use Tests\TestCase;
class UserResourceTest extends TestCase
{
public $adminUser;
protected function setUp(): void
{
parent::setUp();

View File

@@ -13,6 +13,7 @@ use Tests\TestCase;
class LoginTest extends TestCase
{
public $user;
protected function setUp(): void
{
parent::setUp();

View File

@@ -12,6 +12,7 @@ use Tests\TestCase;
class DashboardTest extends TestCase
{
public $user;
protected function setUp(): void
{
parent::setUp();

View File

@@ -2,13 +2,13 @@
namespace Tests\Feature\Livewire;
use App\Livewire\Home;
use App\Livewire\Frontend\Mailbox;
use Exception;
use App\Models\Blog;
use App\Livewire\Home;
use App\Livewire\ListBlog;
use App\Models\Blog;
use App\Models\Page;
use App\Models\ZEmail;
use Exception;
use Illuminate\Support\Facades\Cookie;
use Livewire\Livewire;
use Tests\TestCase;

View File

@@ -1,7 +1,7 @@
<?php
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/*
|--------------------------------------------------------------------------
@@ -30,7 +30,7 @@ pest()->extend(TestCase::class)
|
*/
expect()->extend('toBeOne', fn() => $this->toBe(1));
expect()->extend('toBeOne', fn () => $this->toBe(1));
/*
|--------------------------------------------------------------------------

View File

@@ -9,6 +9,7 @@ use Tests\TestCase;
class ActivationKeyTest extends TestCase
{
public $user;
protected function setUp(): void
{
parent::setUp();

View File

@@ -2,17 +2,19 @@
namespace Tests\Unit\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use App\Models\Blog;
use App\Models\Category;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Tests\TestCase;
class BlogTest extends TestCase
{
private User|Collection $user;
public $category;
protected function setUp(): void
{
parent::setUp();

View File

@@ -2,9 +2,9 @@
namespace Tests\Unit\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\Blog;
use App\Models\Category;
use Illuminate\Database\Eloquent\Model;
use Tests\TestCase;
class CategoryTest extends TestCase

View File

@@ -2,10 +2,10 @@
namespace Tests\Unit\Models;
use Illuminate\Support\Facades\Date;
use App\Models\Email;
use Carbon\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Date;
use Tests\TestCase;
class EmailTest extends TestCase

Some files were not shown because too many files have changed in this diff Show More