Compare commits

..

23 Commits

Author SHA1 Message Date
idevakk
42e2f1bf0a fix: force https scheme in production to resolve mixed-content browser blocking in dokploy 2026-03-01 09:02:40 +05:30
idevakk
2989613b75 fix: remove forced migrations from entrypoint as it halts container boot on partially migrated databases 2026-03-01 08:54:29 +05:30
idevakk
285b995afb fix: auto-run safe migrations in entrypoint for missing cache/queue tables 2026-03-01 08:51:19 +05:30
idevakk
0d9a524267 fix: auto-create database.sqlite gracefully in existing folder to prevent laravel fallback crashes 2026-03-01 08:45:46 +05:30
idevakk
b30da6614a feat: setup custom webdevops Dockerfile with explicit PHP 8.4 Zemailnator extensions and configs 2026-03-01 08:36:49 +05:30
idevakk
0aed57bc2c fix: remove docker-compose.yml in favor of pure Dockerfile deployment for simplicity 2026-03-01 00:08:55 +05:30
idevakk
6d24d1f6fc fix: add zip extension to php-extension-installer 2026-02-28 23:50:35 +05:30
idevakk
189c2d7c58 fix: use php-extension-installer for PHP 8.4 IMAP PECL compilation 2026-02-28 23:46:27 +05:30
idevakk
7f58ca9f45 fix: migrate Dockerfile to php:8.4-fpm-alpine to restore native IMAP support 2026-02-28 23:43:51 +05:30
idevakk
66b5d2f89e docs: move MailOps webhook handover document to the docs directory 2026-02-28 23:38:18 +05:30
idevakk
a04222dcf3 fix: restore missing Dockerfile 2026-02-28 23:38:00 +05:30
idevakk
58aa4d2dbc docs: update Project.md with Dokploy architecture and resolved tech debt 2026-02-28 23:28:01 +05:30
idevakk
e342c2bdae chore: remove redis and mariadb from compose for standalone deployment 2026-02-28 23:26:19 +05:30
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
111 changed files with 1940 additions and 1388 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_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
ADMIN_EMAIL=
#Feature Flags Start
MAIL_HANDLER_API=true
#Feature Flags End
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
@@ -77,8 +84,8 @@ NOTIFY_TG_CHAT_ID=
FORCE_DB_MAIL=false FORCE_DB_MAIL=false
AUTO_FETCH_MAIL=false AUTO_FETCH_MAIL=false
FETCH_FETCH_FOR_DB=true FETCH_FETCH_FOR_DB=false
FETCH_FROM_REMOTE_DB=true FETCH_FROM_REMOTE_DB=false
MOVE_OR_DELETE=delete MOVE_OR_DELETE=delete
#Provide Mailbox Folder Name to Which want to move, else put 'delete' to remove #Provide Mailbox Folder Name to Which want to move, else put 'delete' to remove

69
Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# 1. Use the specific PHP 8.4 Alpine image
FROM webdevops/php-nginx:8.4-alpine
# 2. Environment Configuration
ENV WEB_DOCUMENT_ROOT=/code/public
ENV APP_ENV=production
ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_PROCESS_TIMEOUT=2000
# 3. Install System Dependencies & Extensions
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN apk add --no-cache \
nodejs \
npm \
sqlite \
shadow \
&& install-php-extensions pdo_sqlite mongodb redis imap pdo_mysql
# 4. Copy Custom Configs
COPY ./dockerizer/php.ini /opt/docker/etc/php/php.ini
COPY ./dockerizer/vhost.conf /opt/docker/etc/nginx/vhost.conf
COPY ./dockerizer/supervisor.laravel.conf /opt/docker/etc/supervisor.d/laravel.conf
# 5. Set Working Directory
WORKDIR /code
# CRITICAL FIX: Transfer ownership of /code to the 'application' user
# WORKDIR creates folders as 'root' by default, which blocks Composer later.
RUN chown application:application /code
# 6. Install PHP Dependencies (Layered)
COPY --chown=application:application composer.json composer.lock ./
USER application
RUN composer install --no-interaction --no-scripts --no-autoloader --prefer-dist --no-dev
# 7. Install Node Dependencies
COPY --chown=application:application package.json package-lock.json* ./
RUN npm install
# 8. Copy Application Code
COPY --chown=application:application . .
# 9. Final Builds
RUN composer dump-autoload --optimize && \
composer run-script post-root-package-install && \
php artisan package:discover --ansi
RUN npm run build
# 10. Runtime Initialization Script
# This runs when the container STARTS, not when it builds.
USER root
RUN echo '#!/bin/bash' > /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'set -e' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 1. Fix Permissions \
echo 'echo "Fixing storage permissions..."' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'mkdir -p /code/storage/framework/{views,cache,sessions} /code/bootstrap/cache' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'touch /code/database/database.sqlite' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'chown -R application:application /code/storage /code/bootstrap/cache /code/database' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'chmod -R 775 /code/storage /code/bootstrap/cache /code/database' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 2. Run Optimization (As the application user) \
echo 'echo "Running Laravel Optimizations..."' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
echo 'su -s /bin/sh application -c "php artisan optimize"' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
# 3. Cache Views (Optional but recommended for production) \
echo 'su -s /bin/sh application -c "php artisan view:cache"' >> /opt/docker/provision/entrypoint.d/99-init-laravel.sh && \
chmod +x /opt/docker/provision/entrypoint.d/99-init-laravel.sh

81
Project.md Normal file
View File

@@ -0,0 +1,81 @@
---
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 the `public` Laravel Storage disk, and stores the records in the database.
- Background cleanup and email fetching are handled automatically via Supervisor running Laravel Scheduled Commands (`emails:fetch`, `mailbox:clean`, `attachments:clean`).
## 1.1 Deployment Architecture (Dokploy)
Zemailnator is containerized for zero-downtime deployment on Dokploy:
- **Application Container:** Uses `php:8.4-fpm-alpine` configured via a single `Dockerfile` with a bundled Nginx server and Supervisor to manage background queues.
- **External Services:** MariaDB and Redis run as fully separate, standalone Dokploy application instances to allow for independent backups and to prevent database restarts during application deployment.
## 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. Security & Technical Debt Improvements (Resolved)
The following critical flaws have been successfully addressed:
1. **Hardcoded Credentials in Routes:**
- *Resolved:* The highly insecure `/0xdash/slink` and `/0xdash/scache` endpoints with basic HTTP authentication have been permanently removed. Storage linking is now handled securely during deployment via `entrypoint.sh`.
2. **Public Attachment Storage (RCE Risk):**
- *Resolved:* Attachments are no longer stored directly in `public/tmp/attachments`. The `Email` model now strictly leverages Laravel's secure `Storage::disk('public')` abstraction avoiding direct `file_put_contents`.
3. **Standalone PHP Scripts:**
- *Resolved:* Legacy scripts like `dropmail.php` and `cleanCron.php` now securely dispatch the new native Laravel Console Commands (`mailbox:clean`, etc.).
4. **Legacy Checkout System:**
- *Resolved:* Old Stripe Cashier routes (`checkout/{plan}`) were removed to enforce the newer Unified Payment System.
*(Note: Ensure rate limiting is strictly applied to Livewire generation endpoints to prevent DoS.)*
## 5. Pros & Cons
**Pros:**
- **Modern Stack:** Utilizes bleeding-edge tools (Laravel 12, PHP 8.4, 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.
- **Production Ready:** Fully containerized setup tailored for Dokploy using MariaDB/Redis standalone instances for maximum scalability and zero-downtime deployments.
**Cons:**
- **Rate Limiting:** Needs comprehensive throttling on Livewire generation endpoints to completely mitigate aggressive abuse.
## 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\Schemas\Schema;
use Filament\Support\Icons\Heroicon; use Filament\Support\Icons\Heroicon;
use Inerba\DbConfig\AbstractPageSettings; use Inerba\DbConfig\AbstractPageSettings;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
class ImapSettings extends AbstractPageSettings class ImapSettings extends AbstractPageSettings
{ {
@@ -145,7 +144,7 @@ class ImapSettings extends AbstractPageSettings
$fields = ['host', 'port', 'username', 'password', 'encryption', 'validate_cert', 'protocol']; $fields = ['host', 'port', 'username', 'password', 'encryption', 'validate_cert', 'protocol'];
foreach ($fields as $field) { foreach ($fields as $field) {
$key = $sectionName . '.' . $field; $key = $sectionName.'.'.$field;
// Try different data structure approaches // Try different data structure approaches
$value = null; $value = null;
@@ -186,8 +185,8 @@ class ImapSettings extends AbstractPageSettings
return [ return [
'section' => ucfirst($sectionName), 'section' => ucfirst($sectionName),
'success' => false, 'success' => false,
'message' => "Missing required fields: " . $missingFields->join(', '), 'message' => 'Missing required fields: '.$missingFields->join(', '),
'details' => null 'details' => null,
]; ];
} }
@@ -198,7 +197,7 @@ class ImapSettings extends AbstractPageSettings
'section' => ucfirst($sectionName), 'section' => ucfirst($sectionName),
'success' => false, 'success' => false,
'message' => 'IMAP extension is not loaded in your web server. Please check your Herd PHP configuration or restart your server.', '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'], 'password' => $config['password'],
'encryption' => $config['encryption'] ?? 'none', 'encryption' => $config['encryption'] ?? 'none',
'validate_cert' => $config['validate_cert'] ?? false, 'validate_cert' => $config['validate_cert'] ?? false,
'protocol' => $config['protocol'] ?? 'imap' 'protocol' => $config['protocol'] ?? 'imap',
]; ];
// Test connection using the existing ZEmail::connectMailBox method // Test connection using the existing ZEmail::connectMailBox method
@@ -224,8 +223,8 @@ class ImapSettings extends AbstractPageSettings
'host' => $config['host'], 'host' => $config['host'],
'port' => $config['port'], 'port' => $config['port'],
'encryption' => $config['encryption'] ?? 'none', 'encryption' => $config['encryption'] ?? 'none',
'protocol' => $config['protocol'] ?? 'imap' 'protocol' => $config['protocol'] ?? 'imap',
] ],
]; ];
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -244,13 +243,12 @@ class ImapSettings extends AbstractPageSettings
'host' => $config['host'] ?? null, 'host' => $config['host'] ?? null,
'port' => $config['port'] ?? null, 'port' => $config['port'] ?? null,
'encryption' => $config['encryption'] ?? 'none', 'encryption' => $config['encryption'] ?? 'none',
'protocol' => $config['protocol'] ?? 'imap' 'protocol' => $config['protocol'] ?? 'imap',
] ],
]; ];
} }
} }
/** /**
* Send appropriate notification based on test results. * Send appropriate notification based on test results.
*/ */
@@ -294,6 +292,7 @@ class ImapSettings extends AbstractPageSettings
$details[] = "{$result['section']}: {$result['details']['messages']} messages"; $details[] = "{$result['section']}: {$result['details']['messages']} messages";
} }
} }
return implode(' | ', $details); return implode(' | ', $details);
} }
@@ -306,6 +305,7 @@ class ImapSettings extends AbstractPageSettings
foreach ($results as $result) { foreach ($results as $result) {
$details[] = "{$result['section']}: {$result['message']}"; $details[] = "{$result['section']}: {$result['message']}";
} }
return implode(' | ', $details); return implode(' | ', $details);
} }
@@ -319,6 +319,7 @@ class ImapSettings extends AbstractPageSettings
$status = $result['success'] ? '✅' : '❌'; $status = $result['success'] ? '✅' : '❌';
$details[] = "{$status} {$result['section']}"; $details[] = "{$status} {$result['section']}";
} }
return implode(' | ', $details); return implode(' | ', $details);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,9 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Unique;
class DomainForm class DomainForm
{ {
@@ -20,7 +22,19 @@ class DomainForm
TextInput::make('name') TextInput::make('name')
->columnSpan(1) ->columnSpan(1)
->helperText('Domain name: example.com') ->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') TextInput::make('daily_mailbox_limit')
->integer() ->integer()
->minValue(1) ->minValue(1)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,7 +96,7 @@ class TrialExtensionsTable
->label('View Subscription') ->label('View Subscription')
->icon('heroicon-o-rectangle-stack') ->icon('heroicon-o-rectangle-stack')
->color('blue') ->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(), ->openUrlInNewTab(),
]) ])
->toolbarActions([ ->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\Pages\ListTrialExtensions;
use App\Filament\Resources\TrialExtensions\Schemas\TrialExtensionForm; use App\Filament\Resources\TrialExtensions\Schemas\TrialExtensionForm;
use App\Filament\Resources\TrialExtensions\Tables\TrialExtensionsTable; use App\Filament\Resources\TrialExtensions\Tables\TrialExtensionsTable;
use App\Models\Subscription;
use App\Models\TrialExtension; use App\Models\TrialExtension;
use BackedEnum; use BackedEnum;
use Filament\Resources\Resource; use Filament\Resources\Resource;

View File

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

View File

@@ -9,7 +9,9 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Unique;
class UsernameForm class UsernameForm
{ {
@@ -21,7 +23,19 @@ class UsernameForm
->columnSpan(1) ->columnSpan(1)
->alphaDash() ->alphaDash()
->helperText('Email: myusername@gmail.com | Username: myusername') ->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') TextInput::make('daily_mailbox_limit')
->integer() ->integer()
->minValue(1) ->minValue(1)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,11 @@
namespace App\Livewire\Dashboard; namespace App\Livewire\Dashboard;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Request;
use App\Models\Ticket; use App\Models\Ticket;
use App\Models\TicketResponse; use App\Models\TicketResponse;
use Exception; use Exception;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
class Support extends Component class Support extends Component
@@ -129,9 +129,9 @@ class Support extends Component
public function updateTicketCounts(): void 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() protected function getClientIp()

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,7 @@ class TicketResponseNotification extends Mailable
/** /**
* Create a new message instance. * 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. * Get the message envelope.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,11 +35,14 @@ class AppServiceProvider extends ServiceProvider
$this->loadConfiguration(); $this->loadConfiguration();
$this->loadDomainUsernameData(); $this->loadDomainUsernameData();
// Only load application data when not in testing environment
if (! $this->app->environment('testing')) { if (! $this->app->environment('testing')) {
$this->loadApplicationData(); $this->loadApplicationData();
} }
if ($this->app->environment('production')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
Cashier::calculateTaxes(); Cashier::calculateTaxes();
} }
@@ -81,7 +84,7 @@ class AppServiceProvider extends ServiceProvider
$this->appConfig = [ $this->appConfig = [
'website_settings' => [], 'website_settings' => [],
'imap_settings' => [], 'imap_settings' => [],
'configuration_settings' => [] 'configuration_settings' => [],
]; ];
Log::error($e->getMessage()); Log::error($e->getMessage());
} }
@@ -114,40 +117,40 @@ class AppServiceProvider extends ServiceProvider
private function loadLegacySettings(): array private function loadLegacySettings(): array
{ {
return [ return [
"app_name" => $this->getConfig('website_settings.app_name'), 'app_name' => $this->getConfig('website_settings.app_name'),
"app_version" => $this->getConfig('website_settings.app_version'), 'app_version' => $this->getConfig('website_settings.app_version'),
"app_base_url" => $this->getConfig('website_settings.app_base_url'), 'app_base_url' => $this->getConfig('website_settings.app_base_url'),
"app_admin" => $this->getConfig('website_settings.app_admin'), 'app_admin' => $this->getConfig('website_settings.app_admin'),
"app_title" => $this->getConfig('website_settings.app_title'), 'app_title' => $this->getConfig('website_settings.app_title'),
"app_description" => $this->getConfig('website_settings.app_description'), 'app_description' => $this->getConfig('website_settings.app_description'),
"app_keywords" => $this->getConfig('website_settings.app_keywords'), 'app_keywords' => $this->getConfig('website_settings.app_keywords'),
"app_contact" => $this->getConfig('website_settings.app_contact'), 'app_contact' => $this->getConfig('website_settings.app_contact'),
"app_meta" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_meta')), 'app_meta' => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_meta')),
"app_social" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_social')), 'app_social' => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.app_social')),
"app_header" => $this->getConfig('website_settings.app_header'), 'app_header' => $this->getConfig('website_settings.app_header'),
"app_footer" => $this->getConfig('website_settings.app_footer'), 'app_footer' => $this->getConfig('website_settings.app_footer'),
"imap_settings" => ArrayHelper::jsonEncodeSafe([ 'imap_settings' => ArrayHelper::jsonEncodeSafe([
"host" => $this->getConfig('imap_settings.public.host'), 'host' => $this->getConfig('imap_settings.public.host'),
"port" => $this->getConfig('imap_settings.public.port'), 'port' => $this->getConfig('imap_settings.public.port'),
"username" => $this->getConfig('imap_settings.public.username'), 'username' => $this->getConfig('imap_settings.public.username'),
"password" => $this->getConfig('imap_settings.public.password'), 'password' => $this->getConfig('imap_settings.public.password'),
"encryption" => $this->getConfig('imap_settings.public.encryption'), 'encryption' => $this->getConfig('imap_settings.public.encryption'),
"validate_cert" => $this->getConfig('imap_settings.public.validate_cert'), 'validate_cert' => $this->getConfig('imap_settings.public.validate_cert'),
"default_account" => $this->getConfig('imap_settings.public.default_account'), 'default_account' => $this->getConfig('imap_settings.public.default_account'),
"protocol" => $this->getConfig('imap_settings.public.protocol'), 'protocol' => $this->getConfig('imap_settings.public.protocol'),
"cc_check" => $this->getConfig('imap_settings.public.cc_check'), 'cc_check' => $this->getConfig('imap_settings.public.cc_check'),
"premium_host" => $this->getConfig('imap_settings.premium.host'), 'premium_host' => $this->getConfig('imap_settings.premium.host'),
"premium_port" => $this->getConfig('imap_settings.premium.port'), 'premium_port' => $this->getConfig('imap_settings.premium.port'),
"premium_username" => $this->getConfig('imap_settings.premium.username'), 'premium_username' => $this->getConfig('imap_settings.premium.username'),
"premium_password" => $this->getConfig('imap_settings.premium.password'), 'premium_password' => $this->getConfig('imap_settings.premium.password'),
"premium_encryption" => $this->getConfig('imap_settings.premium.premium_encryption'), 'premium_encryption' => $this->getConfig('imap_settings.premium.premium_encryption'),
"premium_validate_cert" => $this->getConfig('imap_settings.premium.validate_cert'), 'premium_validate_cert' => $this->getConfig('imap_settings.premium.validate_cert'),
"premium_default_account" => $this->getConfig('imap_settings.premium.default_account'), 'premium_default_account' => $this->getConfig('imap_settings.premium.default_account'),
"premium_protocol" => $this->getConfig('imap_settings.premium.protocol'), 'premium_protocol' => $this->getConfig('imap_settings.premium.protocol'),
"premium_cc_check" => $this->getConfig('imap_settings.premium.cc_check'), 'premium_cc_check' => $this->getConfig('imap_settings.premium.cc_check'),
]), ]),
"configuration_settings" => ArrayHelper::jsonEncodeSafe($this->appConfig['configuration_settings']), 'configuration_settings' => ArrayHelper::jsonEncodeSafe($this->appConfig['configuration_settings']),
"ads_settings" => ArrayHelper::jsonEncodeSafe($this->getConfig('website_settings.ads_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); $config = array_merge($dbConfig, $config);
$this->config = $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->merchantApiKey = $this->sandbox ? ($config['sandbox_merchant_api_key'] ?? '') : ($config['merchant_api_key'] ?? '');
$this->baseUrl = 'https://api.oxapay.com/v1'; $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); $kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
try { try {
// Run the Artisan command 'ping' // Run the Artisan command
$exitCode = $kernel->call('cleanMail'); $exitCode = $kernel->call('mailbox:clean');
// Get the output of the command // Get the output of the command
$output = $kernel->output(); $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"; echo "Output:\n$output";
} catch (\Exception $e) { } 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' => [ 'pattern' => [
'prefix' => 'laravel-', 'prefix' => 'laravel-',
'date' => '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]', '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 <?php
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken; use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
return [ return [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
{ {
@@ -18,7 +18,7 @@ return new class extends Migration
try { try {
// MySQL automatically names check constraints in different ways, // MySQL automatically names check constraints in different ways,
// so we attempt a generic drop and ignore failures. // 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) { } catch (\Throwable $e) {
// ignore if constraint does not exist // 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%'; 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};"); DB::statement("ALTER TABLE payment_providers DROP CONSTRAINT {$constraint->conname};");
} }
} catch (\Throwable $e) { } 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; namespace Database\Seeders;
use stdClass;
use App\Models\Meta; use App\Models\Meta;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use stdClass;
class MetaSeeder extends Seeder class MetaSeeder extends Seeder
{ {

View File

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

6
dockerizer/php.ini Normal file
View File

@@ -0,0 +1,6 @@
date.time = UTC
display_errors = Off
memory_limit = 128M
max_execution_time = 60
post_max_size = 32M
upload_max_filesize = 16M

View File

@@ -0,0 +1,25 @@
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /code/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=application
numprocs=1
redirect_stderr=true
stdout_logfile=/docker.stdout
stdout_logfile_maxbytes=0
[program:laravel-scheduler]
process_name=%(program_name)s_%(process_num)02d
command=php /code/artisan schedule:work
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=application
numprocs=1
redirect_stderr=true
stdout_logfile=/docker.stdout
stdout_logfile_maxbytes=0

25
dockerizer/vhost.conf Normal file
View File

@@ -0,0 +1,25 @@
server {
listen 80 default_server;
server_name _;
root "/code/public";
index index.php;
client_max_body_size 50m;
access_log /docker.stdout;
error_log /docker.stderr warn;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_read_timeout 600;
}
}

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.

View File

@@ -11,71 +11,11 @@ $kernel->bootstrap();
set_time_limit(0); set_time_limit(0);
$newTimezone = 'Europe/London'; try {
date_default_timezone_set($newTimezone); // Run the new Artisan command fallback
$exitCode = $kernel->call('mailbox:clean');
$imapDB = json_decode(config('app.settings.imap_settings') ?: '{}', true); $output = $kernel->output();
echo $output;
// Mailbox credentials } catch (\Exception $e) {
$hostname = '{'.($imapDB['host'] ?? 'localhost').':'.($imapDB['port'] ?? '993').'/ssl}INBOX'; echo 'Error running Artisan command: '.$e->getMessage();
$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());
} }
// 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);

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"> <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) @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="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 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="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="relative rounded-[15px] dark:bg-zinc-900 bg-zinc-100 dark:text-white text-accent-content p-6">
<div class="space-y-4"> <div class="space-y-4">
<p class="font-md text-slate-500"> <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>
<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> </div>
</div> </div>

View File

@@ -25,7 +25,7 @@
echo $adsSettings->two ?? ''; echo $adsSettings->two ?? '';
@endphp @endphp
</div> </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> <flux:text>{!! $postDetail->content !!}</flux:text>
</div> </div>
<div class="mt-3"></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> <flux:heading class="mb-3 truncate" size="xl" level="1">{{ $page->title }}</flux:heading>
<div class="mb-3"></div> <div class="mb-3"></div>
<div class="block rounded-lg bg-white shadow-md dark:bg-zinc-700 items-center p-1"> <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> <flux:text>{!! $page->content !!}</flux:text>
</div> </div>
</div> </div>

View File

@@ -3,4 +3,4 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; 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 <?php
use App\Livewire\Actions\Logout;
use App\Http\Controllers\Auth\VerifyEmailController; use App\Http\Controllers\Auth\VerifyEmailController;
use App\Livewire\Actions\Logout;
use App\Livewire\Auth\ConfirmPassword; use App\Livewire\Auth\ConfirmPassword;
use App\Livewire\Auth\ForgotPassword; use App\Livewire\Auth\ForgotPassword;
use App\Livewire\Auth\Login; use App\Livewire\Auth\Login;

View File

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

View File

@@ -1,7 +1,6 @@
<?php <?php
use App\Http\Controllers\AppController; use App\Http\Controllers\AppController;
use App\Http\Controllers\ImpersonationController; use App\Http\Controllers\ImpersonationController;
use App\Http\Controllers\WebhookController; use App\Http\Controllers\WebhookController;
use App\Http\Middleware\CheckPageSlug; use App\Http\Middleware\CheckPageSlug;
@@ -22,11 +21,14 @@ use App\Livewire\Settings\Billing;
use App\Livewire\Settings\Password; use App\Livewire\Settings\Password;
use App\Livewire\Settings\Profile; use App\Livewire\Settings\Profile;
use App\Models\Email; use App\Models\Email;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/health', function () {
return response('OK', 200);
});
Route::get('/', Home::class)->name('home'); Route::get('/', Home::class)->name('home');
Route::get('/mailbox', Mailbox::class)->name('mailbox'); Route::get('/mailbox', Mailbox::class)->name('mailbox');
Route::get('/mailbox/{email?}', [AppController::class, 'mailbox'])->name('mailboxFromURL'); 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/compose-email', Dashboard::class)->name('dashboard.compose');
Route::get('dashboard/support', Support::class)->name('dashboard.support'); 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) // 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/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/cancel', [Dashboard::class, 'paymentStatus'])->name('checkout.cancel')->defaults('status', 'cancel');
Route::get('dashboard/billing', fn () => auth()->user()->redirectToBillingPortal(route('dashboard')))->name('billing'); 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 // Impersonation Routes
Route::prefix('impersonation')->name('impersonation.')->group(function (): void { Route::prefix('impersonation')->name('impersonation.')->group(function (): void {
Route::post('/stop', [ImpersonationController::class, 'stop'])->name('stop'); Route::post('/stop', [ImpersonationController::class, 'stop'])->name('stop');

View File

@@ -2,11 +2,11 @@
namespace Tests\Concerns; namespace Tests\Concerns;
use Exception;
use Illuminate\Support\Collection;
use App\Models\Blog; use App\Models\Blog;
use App\Models\Menu; use App\Models\Menu;
use App\Models\Plan; use App\Models\Plan;
use Exception;
use Illuminate\Support\Collection;
trait LoadsApplicationData trait LoadsApplicationData
{ {
@@ -64,7 +64,7 @@ trait LoadsApplicationData
try { try {
$menus = cache()->remember('app_menus', now()->addHours(6), Menu::all(...)); $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(...)); $plans = cache()->remember('app_plans', now()->addHours(6), Plan::all(...));
} catch (Exception) { } catch (Exception) {

View File

@@ -2,30 +2,30 @@
namespace Tests\Feature\Filament; namespace Tests\Feature\Filament;
use App\Filament\Resources\TicketResource\Pages\ListTickets; use App\Filament\Resources\BlogResource;
use App\Filament\Resources\TicketResource\Pages\EditTicket; use App\Filament\Resources\BlogResource\Pages\CreateBlog;
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\Pages\EditBlog; 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\CreateCategory;
use App\Filament\Resources\CategoryResource\Pages\EditCategory; 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\CreatePage;
use App\Filament\Resources\PageResource\Pages\EditPage; use App\Filament\Resources\PageResource\Pages\EditPage;
use App\Filament\Resources\MenuResource\Pages\ListMenus; use App\Filament\Resources\PageResource\Pages\ListPages;
use App\Filament\Resources\MenuResource\Pages\CreateMenu;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource;
use App\Filament\Resources\PlanResource; 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\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\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\Blog;
use App\Models\Category; use App\Models\Category;
use App\Models\Menu; use App\Models\Menu;
@@ -40,6 +40,7 @@ use Tests\TestCase;
class ResourcesTest extends TestCase class ResourcesTest extends TestCase
{ {
public $adminUser; public $adminUser;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<?php <?php
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase; 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 class ActivationKeyTest extends TestCase
{ {
public $user; public $user;
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();

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