From 10a00bac0e8c8f364b422484ce19ec2b8a5c4653 Mon Sep 17 00:00:00 2001 From: dyzcdn Date: Tue, 23 Dec 2025 04:59:21 +0700 Subject: [PATCH] chore: cleanup project structure and update readme for beta release --- .gitignore | 12 + README.md | 30 +- RELEASE_NOTES.md | 32 + app/Events/DashboardStatsUpdated.php | 38 + app/Events/PingResponse.php | 44 + app/Events/TicketMessageSent.php | 65 + app/Helpers/MenuHelper.php | 163 ++- .../Admin/ContactManagementController.php | 68 + .../Admin/LegalManagementController.php | 60 + .../Controllers/Admin/RootCaController.php | 13 + .../Admin/SmtpTesterController.php | 74 + .../Admin/TicketManagementController.php | 125 ++ app/Http/Controllers/ApiKeyController.php | 8 + app/Http/Controllers/AuthController.php | 244 ++-- .../Controllers/CertificateController.php | 7 + app/Http/Controllers/ContactController.php | 35 + app/Http/Controllers/DashboardController.php | 109 +- app/Http/Controllers/LegalController.php | 25 + .../Controllers/NotificationController.php | 49 + app/Http/Controllers/PageController.php | 5 + app/Http/Controllers/SearchController.php | 177 +++ app/Http/Controllers/TicketController.php | 165 +++ app/Http/Controllers/ToolController.php | 25 + app/Http/Middleware/CheckApiKey.php | 3 + app/Mail/ContactReply.php | 64 + app/Models/ContactSubmission.php | 22 + app/Models/LegalPage.php | 23 + app/Models/LegalPageRevision.php | 25 + app/Models/Ticket.php | 31 + app/Models/TicketReply.php | 29 + app/Models/User.php | 6 + app/Notifications/NewTicketCreated.php | 51 + app/Notifications/NewTicketReply.php | 56 + app/Notifications/SystemAlert.php | 70 + app/Notifications/TicketStatusUpdated.php | 38 + bootstrap/app.php | 1 + composer.json | 4 +- composer.lock | 1242 ++++++++++++++++- config/broadcasting.php | 82 ++ config/laravolt/avatar-hd.php | 253 ++++ config/laravolt/avatar.php | 174 +++ config/mail.php | 15 + config/reverb.php | 95 ++ ..._12_22_100801_create_legal_pages_table.php | 45 + ...03941_create_contact_submissions_table.php | 33 + ...025_12_22_124707_create_tickets_tables.php | 47 + ...2_22_124710_create_notifications_table.php | 31 + ...le_id_to_string_in_notifications_table.php | 28 + database/seeders/DatabaseSeeder.php | 22 +- database/seeders/LegalPageSeeder.php | 42 + deploy.sh.example | 84 ++ package-lock.json | 212 ++- package.json | 3 + public/chat-id.html | 262 ++++ public/images/error/403-dark.svg | 20 + public/images/error/403.svg | 20 + public/images/logo/auth-logo.png | Bin 0 -> 5108 bytes public/images/logo/auth-logo.svg | 92 +- public/images/logo/logo-dark.png | Bin 0 -> 3149 bytes public/images/logo/logo-dark.svg | 92 +- public/images/logo/logo-icon.png | Bin 0 -> 1541 bytes public/images/logo/logo-icon.svg | 74 +- public/images/logo/logo.png | Bin 0 -> 3316 bytes public/images/logo/logo.svg | 92 +- public/images/og-share.png | Bin 0 -> 63101 bytes public/key-gen.html | 27 +- resources/css/app.css | 1 + resources/js/bootstrap.js | 8 + resources/js/echo.js | 14 + .../header/notification-dropdown.blade.php | 200 +-- .../views/components/public/footer.blade.php | 12 + .../views/components/public/navbar.blade.php | 127 ++ .../components/ui/global-search.blade.php | 226 +++ .../components/ui/realtime-toast.blade.php | 103 ++ .../views/emails/contact-reply.blade.php | 14 + resources/views/errors/403.blade.php | 2 +- resources/views/errors/404.blade.php | 2 +- resources/views/errors/500.blade.php | 2 +- resources/views/landing.blade.php | 217 +++ resources/views/layouts/app-header.blade.php | 38 +- resources/views/layouts/app.blade.php | 4 + .../views/layouts/fullscreen-layout.blade.php | 23 +- .../pages/admin/contacts/index.blade.php | 116 ++ .../views/pages/admin/contacts/show.blade.php | 115 ++ .../views/pages/admin/legal/edit.blade.php | 211 +++ .../views/pages/admin/legal/index.blade.php | 116 ++ .../pages/admin/smtp-tester/index.blade.php | 168 +++ .../views/pages/admin/tickets/index.blade.php | 110 ++ .../views/pages/admin/tickets/show.blade.php | 317 +++++ .../pages/auth/forgot-password.blade.php | 9 +- .../views/pages/auth/reset-password.blade.php | 8 +- .../views/pages/auth/setup-password.blade.php | 11 + resources/views/pages/auth/signin.blade.php | 19 +- resources/views/pages/auth/signup.blade.php | 15 +- .../views/pages/auth/verify-email.blade.php | 10 +- resources/views/pages/dashboard.blade.php | 215 ++- resources/views/pages/legal/show.blade.php | 48 + .../views/pages/public/contact.blade.php | 90 ++ .../public/tools/app-key-generator.blade.php | 101 ++ .../public/tools/chat-id-finder.blade.php | 185 +++ .../views/pages/support/create.blade.php | 105 ++ resources/views/pages/support/index.blade.php | 95 ++ resources/views/pages/support/show.blade.php | 299 ++++ .../views/vendor/mail/html/button.blade.php | 24 + .../views/vendor/mail/html/footer.blade.php | 11 + .../views/vendor/mail/html/header.blade.php | 8 + .../views/vendor/mail/html/layout.blade.php | 58 + .../views/vendor/mail/html/message.blade.php | 28 + .../views/vendor/mail/html/panel.blade.php | 14 + .../views/vendor/mail/html/subcopy.blade.php | 7 + .../views/vendor/mail/html/table.blade.php | 3 + .../views/vendor/mail/html/themes/default.css | 220 +++ .../views/vendor/mail/text/button.blade.php | 1 + .../views/vendor/mail/text/footer.blade.php | 1 + .../views/vendor/mail/text/header.blade.php | 1 + .../views/vendor/mail/text/layout.blade.php | 9 + .../views/vendor/mail/text/message.blade.php | 27 + .../views/vendor/mail/text/panel.blade.php | 1 + .../views/vendor/mail/text/subcopy.blade.php | 1 + .../views/vendor/mail/text/table.blade.php | 1 + routes/channels.php | 23 + routes/web.php | 104 +- 122 files changed, 8320 insertions(+), 661 deletions(-) create mode 100644 RELEASE_NOTES.md create mode 100644 app/Events/DashboardStatsUpdated.php create mode 100644 app/Events/PingResponse.php create mode 100644 app/Events/TicketMessageSent.php create mode 100644 app/Http/Controllers/Admin/ContactManagementController.php create mode 100644 app/Http/Controllers/Admin/LegalManagementController.php create mode 100644 app/Http/Controllers/Admin/SmtpTesterController.php create mode 100644 app/Http/Controllers/Admin/TicketManagementController.php create mode 100644 app/Http/Controllers/ContactController.php create mode 100644 app/Http/Controllers/LegalController.php create mode 100644 app/Http/Controllers/NotificationController.php create mode 100644 app/Http/Controllers/SearchController.php create mode 100644 app/Http/Controllers/TicketController.php create mode 100644 app/Http/Controllers/ToolController.php create mode 100644 app/Mail/ContactReply.php create mode 100644 app/Models/ContactSubmission.php create mode 100644 app/Models/LegalPage.php create mode 100644 app/Models/LegalPageRevision.php create mode 100644 app/Models/Ticket.php create mode 100644 app/Models/TicketReply.php create mode 100644 app/Notifications/NewTicketCreated.php create mode 100644 app/Notifications/NewTicketReply.php create mode 100644 app/Notifications/SystemAlert.php create mode 100644 app/Notifications/TicketStatusUpdated.php create mode 100644 config/broadcasting.php create mode 100644 config/laravolt/avatar-hd.php create mode 100644 config/laravolt/avatar.php create mode 100644 config/reverb.php create mode 100644 database/migrations/2025_12_22_100801_create_legal_pages_table.php create mode 100644 database/migrations/2025_12_22_103941_create_contact_submissions_table.php create mode 100644 database/migrations/2025_12_22_124707_create_tickets_tables.php create mode 100644 database/migrations/2025_12_22_124710_create_notifications_table.php create mode 100644 database/migrations/2025_12_22_155233_change_notifiable_id_to_string_in_notifications_table.php create mode 100644 database/seeders/LegalPageSeeder.php create mode 100644 deploy.sh.example create mode 100644 public/chat-id.html create mode 100644 public/images/error/403-dark.svg create mode 100644 public/images/error/403.svg create mode 100644 public/images/logo/auth-logo.png create mode 100644 public/images/logo/logo-dark.png create mode 100644 public/images/logo/logo-icon.png create mode 100644 public/images/logo/logo.png create mode 100644 public/images/og-share.png create mode 100644 resources/js/echo.js create mode 100644 resources/views/components/public/footer.blade.php create mode 100644 resources/views/components/public/navbar.blade.php create mode 100644 resources/views/components/ui/global-search.blade.php create mode 100644 resources/views/components/ui/realtime-toast.blade.php create mode 100644 resources/views/emails/contact-reply.blade.php create mode 100644 resources/views/landing.blade.php create mode 100644 resources/views/pages/admin/contacts/index.blade.php create mode 100644 resources/views/pages/admin/contacts/show.blade.php create mode 100644 resources/views/pages/admin/legal/edit.blade.php create mode 100644 resources/views/pages/admin/legal/index.blade.php create mode 100644 resources/views/pages/admin/smtp-tester/index.blade.php create mode 100644 resources/views/pages/admin/tickets/index.blade.php create mode 100644 resources/views/pages/admin/tickets/show.blade.php create mode 100644 resources/views/pages/legal/show.blade.php create mode 100644 resources/views/pages/public/contact.blade.php create mode 100644 resources/views/pages/public/tools/app-key-generator.blade.php create mode 100644 resources/views/pages/public/tools/chat-id-finder.blade.php create mode 100644 resources/views/pages/support/create.blade.php create mode 100644 resources/views/pages/support/index.blade.php create mode 100644 resources/views/pages/support/show.blade.php create mode 100644 resources/views/vendor/mail/html/button.blade.php create mode 100644 resources/views/vendor/mail/html/footer.blade.php create mode 100644 resources/views/vendor/mail/html/header.blade.php create mode 100644 resources/views/vendor/mail/html/layout.blade.php create mode 100644 resources/views/vendor/mail/html/message.blade.php create mode 100644 resources/views/vendor/mail/html/panel.blade.php create mode 100644 resources/views/vendor/mail/html/subcopy.blade.php create mode 100644 resources/views/vendor/mail/html/table.blade.php create mode 100644 resources/views/vendor/mail/html/themes/default.css create mode 100644 resources/views/vendor/mail/text/button.blade.php create mode 100644 resources/views/vendor/mail/text/footer.blade.php create mode 100644 resources/views/vendor/mail/text/header.blade.php create mode 100644 resources/views/vendor/mail/text/layout.blade.php create mode 100644 resources/views/vendor/mail/text/message.blade.php create mode 100644 resources/views/vendor/mail/text/panel.blade.php create mode 100644 resources/views/vendor/mail/text/subcopy.blade.php create mode 100644 resources/views/vendor/mail/text/table.blade.php create mode 100644 routes/channels.php diff --git a/.gitignore b/.gitignore index 1bb514b..2a9d403 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,15 @@ yarn-error.log* # Compiled assets mix-manifest.json + +# Local Deployment & Release +/deploy.sh +/zip-release.php +/zip-stage.php +/copy-release.php +/make-release.bat +/auth_backup.php +/.idea +/.vscode +*.bak +*.tmp diff --git a/README.md b/README.md index 50d6381..d87b92f 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ A robust, modern platform for managing Root CAs, Intermediate CAs, and Leaf Cert #### Option A: Terminal Access ```bash # Clone and enter -git clone -cd app-tail +git clone https://github.com/twinpath/app.git +cd app # Install dependencies composer install @@ -120,5 +120,31 @@ Returns Root and Intermediate CA certificates in JSON format. `GET /api/v1/certificates` Retrieves user-specific leaf certificates. Requires `X-API-KEY` header. +## 🔄 CI/CD & Automated Deployment + +The project includes an automation script for seamless deployment on aaPanel: + +### 1. Script Setup +1. Locate `deploy.sh.example` and rename it to `deploy.sh` on your server. +2. Edit `deploy.sh` and provide your specific paths and Telegram credentials. +3. Make the script executable: `chmod +x deploy.sh`. + +### 2. aaPanel Webhook Integration +1. In aaPanel, install the **Webhook** app. +2. Create a new Webhook and paste the following command: + ```bash + /bin/bash /www/wwwroot/your-project-path/deploy.sh + ``` +3. Copy the Webhook URL provided by aaPanel. + +### 3. GitHub Integration +1. Go to your GitHub repository **Settings > Webhooks**. +2. Click **Add webhook**. +3. Paste your aaPanel Webhook URL into the **Payload URL**. +4. Set **Content type** to `application/json`. +5. Select **Just the push event** and click **Add webhook**. + +Now, every time you push to the `main` branch, aaPanel will automatically pull the latest code, install dependencies, run migrations, and build assets. + ## 📦 License Refer to the [LICENSE](LICENSE) file for details. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..86743ca --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,32 @@ +# Release v1.0.0 - Initial Official Launch + +We are excited to announce the first official release of the **Certificate Authority & API Management System**. This version provides a complete foundation for managing internal PKI and secure API access. + +## ✨ Key Features +- **CA management**: Full lifecycle support for Root and Intermediate CAs. +- **Certificate Issuance**: Dynamic generation of leaf certificates with customizable SANs. +- **API Key Infrastructure**: Secure key generation, status toggling, and real-time usage tracking. +- **Advanced Dashboard**: Real-time metrics, issuance trends, and server latency monitoring. +- **Developer Experience**: Interactive API documentation with code snippets (cURL, JS, PHP, Python). +- **Hosting Friendly**: Included standalone Web Key Generator and manual database import scripts. + +## 🛠️ Included in this Release +- **Source Code**: Full Laravel 12 / Tailwind v4 source. +- **Database Schema**: Pre-configured `install.sql` in the `database/` folder. +- **Key-Gen Tool**: Standalone utility in `public/key-gen.html`. + +## 🚀 Installation Overview +For users with terminal access: +```bash +composer install && npm install && npm run build +php artisan migrate --seed +``` + +For Shared Hosting users: +1. Download the `app-v1.0.0-ready.zip` attachment. +2. Upload and extract to your server. +3. Import `database/install.sql` via phpMyAdmin. +4. Configure `.env` using our provided `key-gen.html`. + +--- +*Thank you for using DyDev TrustLab solutions!* diff --git a/app/Events/DashboardStatsUpdated.php b/app/Events/DashboardStatsUpdated.php new file mode 100644 index 0000000..4ff5a52 --- /dev/null +++ b/app/Events/DashboardStatsUpdated.php @@ -0,0 +1,38 @@ +userId = $userId; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('user.' . $this->userId), + ]; + } +} diff --git a/app/Events/PingResponse.php b/app/Events/PingResponse.php new file mode 100644 index 0000000..9a49878 --- /dev/null +++ b/app/Events/PingResponse.php @@ -0,0 +1,44 @@ +userId = $userId; + $this->timestamp = $timestamp; + } + + /** + * Get the channels the event should broadcast on. + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('user.' . $this->userId), + ]; + } + + /** + * The event's broadcast name. + */ + public function broadcastAs(): string + { + return 'PingResponse'; + } +} diff --git a/app/Events/TicketMessageSent.php b/app/Events/TicketMessageSent.php new file mode 100644 index 0000000..bcdb9be --- /dev/null +++ b/app/Events/TicketMessageSent.php @@ -0,0 +1,65 @@ +reply = $reply->load('user'); + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('ticket.' . $this->reply->ticket_id), + ]; + } + + /** + * The event's broadcast name. + */ + public function broadcastAs(): string + { + return 'ticket.message.sent'; + } + + /** + * Data to broadcast. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'id' => $this->reply->id, + 'message' => $this->reply->message, + 'user_id' => $this->reply->user_id, + 'user_name' => $this->reply->user->name, + 'is_staff' => $this->reply->user->isAdmin(), + 'created_at' => $this->reply->created_at->format('M d, Y H:i A'), + 'attachment_url' => $this->reply->attachment_path ? \Storage::url($this->reply->attachment_path) : null, + ]; + } +} diff --git a/app/Helpers/MenuHelper.php b/app/Helpers/MenuHelper.php index 6698ca5..c9b143e 100644 --- a/app/Helpers/MenuHelper.php +++ b/app/Helpers/MenuHelper.php @@ -20,39 +20,73 @@ class MenuHelper ['name' => 'Create Certificate', 'route_name' => 'certificate.create', 'pro' => false], ], ], + [ + 'name' => 'API Keys', + 'icon' => 'api-key', + 'route_name' => 'api-keys.index', + ], + ]; + } + + public static function getTemplateItems() + { + return [ [ 'name' => 'Calendar', 'icon' => 'calendar', - 'route_name' => 'calendar', + 'route_name' => 'templates.calendar', ], [ 'name' => 'Forms', 'icon' => 'forms', 'subItems' => [ - ['name' => 'Form Elements', 'route_name' => 'form-elements', 'pro' => false], + ['name' => 'Form Elements', 'route_name' => 'templates.form-elements', 'pro' => false], ], ], [ 'name' => 'Tables', 'icon' => 'tables', 'subItems' => [ - ['name' => 'Basic Tables', 'route_name' => 'basic-tables', 'pro' => false] + ['name' => 'Basic Tables', 'route_name' => 'templates.basic-tables', 'pro' => false] ], ], - [ - 'name' => 'API Keys', - 'icon' => 'api-key', - 'route_name' => 'api-keys.index', - ], [ 'name' => 'Pages', 'icon' => 'pages', 'subItems' => [ - ['name' => 'Blank Page', 'route_name' => 'blank', 'pro' => false], + ['name' => 'Blank Page', 'route_name' => 'templates.blank', 'pro' => false], ['name' => '404 Error', 'route_name' => 'error-404', 'pro' => false], // ['name' => 'API Keys', 'route_name' => 'api-keys.index', 'pro' => false, 'new' => true] ], ], + [ + 'name' => 'Charts', + 'icon' => 'charts', + 'subItems' => [ + ['name' => 'Line Chart', 'route_name' => 'templates.line-chart', 'pro' => false], + ['name' => 'Bar Chart', 'route_name' => 'templates.bar-chart', 'pro' => false] + ], + ], + [ + 'name' => 'UI Elements', + 'icon' => 'ui-elements', + 'subItems' => [ + ['name' => 'Alerts', 'route_name' => 'templates.alerts', 'pro' => false], + ['name' => 'Avatar', 'route_name' => 'templates.avatars', 'pro' => false], + ['name' => 'Badge', 'route_name' => 'templates.badges', 'pro' => false], + ['name' => 'Buttons', 'route_name' => 'templates.buttons', 'pro' => false], + ['name' => 'Images', 'route_name' => 'templates.images', 'pro' => false], + ['name' => 'Videos', 'route_name' => 'templates.videos', 'pro' => false], + ], + ], + [ + 'name' => 'Authentication', + 'icon' => 'authentication', + 'subItems' => [ + ['name' => 'Sign In', 'route_name' => 'signin', 'pro' => false], + ['name' => 'Sign Up', 'route_name' => 'signup', 'pro' => false], + ], + ], ]; } @@ -72,45 +106,34 @@ class MenuHelper ]; } - public static function getOthersItems() - { - return [ - [ - 'name' => 'Charts', - 'icon' => 'charts', - 'subItems' => [ - ['name' => 'Line Chart', 'route_name' => 'line-chart', 'pro' => false], - ['name' => 'Bar Chart', 'route_name' => 'bar-chart', 'pro' => false] - ], - ], - [ - 'name' => 'UI Elements', - 'icon' => 'ui-elements', - 'subItems' => [ - ['name' => 'Alerts', 'route_name' => 'alerts', 'pro' => false], - ['name' => 'Avatar', 'route_name' => 'avatars', 'pro' => false], - ['name' => 'Badge', 'route_name' => 'badges', 'pro' => false], - ['name' => 'Buttons', 'route_name' => 'buttons', 'pro' => false], - ['name' => 'Images', 'route_name' => 'images', 'pro' => false], - ['name' => 'Videos', 'route_name' => 'videos', 'pro' => false], - ], - ], - [ - 'name' => 'Authentication', - 'icon' => 'authentication', - 'subItems' => [ - ['name' => 'Sign In', 'route_name' => 'signin', 'pro' => false], - ['name' => 'Sign Up', 'route_name' => 'signup', 'pro' => false], - ], - ], - ]; - } - public static function getMenuGroups() { $groups = []; - // Admin Menu + // Check if we are in the template section + if (request()->is('templates*')) { + $groups[] = [ + 'title' => 'Back', + 'items' => [ + [ + 'name' => 'Back to Dashboard', + 'icon' => 'dashboard', // Or an arrow icon if available + 'route_name' => 'dashboard', + ] + ] + ]; + + $groups[] = [ + 'title' => 'Template Gallery', + 'items' => self::getTemplateItems() + ]; + + return $groups; + } + + // --- Main Admin/User Sidebar --- + + // Admin Management if (auth()->check() && auth()->user()->isAdmin()) { $groups[] = [ 'title' => 'Admin Management', @@ -125,19 +148,45 @@ class MenuHelper 'icon' => 'certificate', 'route_name' => 'admin.root-ca.index', ], + [ + 'name' => 'Ticket Management', + 'icon' => 'support-ticket', + 'route_name' => 'admin.tickets.index', + ], + [ + 'name' => 'Legal Page Management', + 'icon' => 'pages', + 'route_name' => 'admin.legal-pages.index', + ], + [ + 'name' => 'Inbox / Messages', + 'icon' => 'email', + 'route_name' => 'admin.contacts.index', + ], + [ + 'name' => 'SMTP Tester', + 'icon' => 'server-settings', + 'route_name' => 'admin.smtp-tester.index', + ], ] ]; } - // Filter Main Items based on role + // Standard Menus + // Filter Main Items based on role for non-admins (though getMainNavItems is now cleaner) $mainItems = self::getMainNavItems(); + + // Add Support Tickets to Main Nav + $mainItems[] = [ + 'name' => 'Support Tickets', + 'icon' => 'support-ticket', + 'route_name' => 'support.index', + ]; + if (!auth()->check() || !auth()->user()->isAdmin()) { - $mainItems = array_values(array_filter($mainItems, function ($item) { - return in_array($item['name'], ['Dashboard', 'Certificate', 'API Keys']); - })); + // For customers, just show what's in mainItems (Dashboard, Certs, Keys, Support) } - // Standard Menus $groups[] = [ 'title' => 'Menu', 'items' => $mainItems @@ -147,12 +196,18 @@ class MenuHelper 'title' => 'My Account', 'items' => self::getMyAccountItems() ]; - - // Others - Admin Only + + // Link to Templates for Admins if (auth()->check() && auth()->user()->isAdmin()) { $groups[] = [ - 'title' => 'Others', - 'items' => self::getOthersItems() + 'title' => 'Development', + 'items' => [ + [ + 'name' => 'View Templates', + 'icon' => 'pages', + 'route_name' => 'templates.calendar' + ] + ] ]; } @@ -216,6 +271,8 @@ class MenuHelper 'api-key' => '', + 'server-settings' => '', + ]; return $icons[$iconName] ?? ''; diff --git a/app/Http/Controllers/Admin/ContactManagementController.php b/app/Http/Controllers/Admin/ContactManagementController.php new file mode 100644 index 0000000..93c7c6f --- /dev/null +++ b/app/Http/Controllers/Admin/ContactManagementController.php @@ -0,0 +1,68 @@ +paginate(10); + return view('pages.admin.contacts.index', compact('submissions')); + } + + public function show(ContactSubmission $contactSubmission) + { + if (!$contactSubmission->is_read) { + $contactSubmission->update(['is_read' => true]); + } + + return view('pages.admin.contacts.show', compact('contactSubmission')); + } + + public function reply(Request $request, ContactSubmission $contactSubmission) + { + $validated = $request->validate([ + 'subject' => 'required|string|max:255', + 'message' => 'required|string', + ]); + + // Send using the support mailer + try { + $email = trim($contactSubmission->email); + + \Illuminate\Support\Facades\Log::info('Attempting to send ContactReply', [ + 'to' => $email, + 'subject' => $validated['subject'], + 'message_length' => strlen($validated['message']), + 'mailer' => 'support' + ]); + + Mail::mailer('support') + ->to($email) + ->send(new ContactReply(strip_tags($validated['subject']), strip_tags($validated['message']))); + + \Illuminate\Support\Facades\Log::info('ContactReply sent command executed without exception.'); + + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Failed to send ContactReply', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + return back()->with('error', 'Failed to send reply: ' . $e->getMessage()); + } + + return back()->with('success', 'Reply sent successfully to ' . $contactSubmission->email); + } + + public function destroy(ContactSubmission $contactSubmission) + { + $contactSubmission->delete(); + return redirect()->route('admin.contacts.index')->with('success', 'Message deleted successfully.'); + } +} diff --git a/app/Http/Controllers/Admin/LegalManagementController.php b/app/Http/Controllers/Admin/LegalManagementController.php new file mode 100644 index 0000000..96ed77a --- /dev/null +++ b/app/Http/Controllers/Admin/LegalManagementController.php @@ -0,0 +1,60 @@ +get(); + return view('pages.admin.legal.index', compact('pages')); + } + + public function edit(LegalPage $legalPage) + { + $legalPage->load('currentRevision'); + return view('pages.admin.legal.edit', compact('legalPage')); + } + + public function update(Request $request, LegalPage $legalPage) + { + $request->validate([ + 'content' => 'required', + 'version' => 'required_unless:update_existing,true', + 'change_log' => 'nullable|string', + ]); + + if ($request->has('update_existing') && $request->update_existing == 'true') { + $revision = $legalPage->currentRevision; + if ($revision) { + $revision->update([ + 'content' => $request->content, + 'change_log' => $request->change_log ?? $revision->change_log, + 'created_by' => Auth::id(), + ]); + return redirect()->route('admin.legal-pages.index')->with('success', 'Current version updated successfully (no new revision created).'); + } + } + + // Deactivate old revisions + LegalPageRevision::where('legal_page_id', $legalPage->id)->update(['is_active' => false]); + + // Create new revision + LegalPageRevision::create([ + 'legal_page_id' => $legalPage->id, + 'content' => $request->content, + 'version' => $request->version, + 'change_log' => $request->change_log, + 'is_active' => true, + 'created_by' => Auth::id(), + ]); + + return redirect()->route('admin.legal-pages.index')->with('success', 'Legal page updated successfully with a new version.'); + } +} diff --git a/app/Http/Controllers/Admin/RootCaController.php b/app/Http/Controllers/Admin/RootCaController.php index 6034f34..fc9a47c 100644 --- a/app/Http/Controllers/Admin/RootCaController.php +++ b/app/Http/Controllers/Admin/RootCaController.php @@ -10,6 +10,19 @@ use Illuminate\Support\Str; class RootCaController extends Controller { + public function setup() + { + if (CaCertificate::count() > 0) { + return redirect()->route('certificate.index')->with('error', 'CA already initialized.'); + } + + if (app(\App\Services\OpenSslService::class)->setupCa()) { + return redirect()->route('certificate.index')->with('success', 'Root and Intermediate CA successfully initialized.'); + } + + return redirect()->route('certificate.index')->with('error', 'Failed to initialize CA.'); + } + public function index() { $certificates = CaCertificate::all()->map(function($cert) { diff --git a/app/Http/Controllers/Admin/SmtpTesterController.php b/app/Http/Controllers/Admin/SmtpTesterController.php new file mode 100644 index 0000000..4f4edb6 --- /dev/null +++ b/app/Http/Controllers/Admin/SmtpTesterController.php @@ -0,0 +1,74 @@ + [ + 'name' => 'System/Default Mailer (smtp)', + 'host' => Config::get('mail.mailers.smtp.host'), + 'port' => Config::get('mail.mailers.smtp.port'), + 'username' => Config::get('mail.mailers.smtp.username'), + 'encryption' => Config::get('mail.mailers.smtp.encryption'), + 'from' => Config::get('mail.from.address'), + ], + 'support' => [ + 'name' => 'Support Mailer (support)', + 'host' => Config::get('mail.mailers.support.host'), + 'port' => Config::get('mail.mailers.support.port'), + 'username' => Config::get('mail.mailers.support.username'), + 'encryption' => Config::get('mail.mailers.support.encryption'), + 'from' => Config::get('mail.mailers.support.username'), // usually same as username or configured + ], + ]; + + return view('pages.admin.smtp-tester.index', compact('configs')); + } + + public function send(Request $request) + { + $request->validate([ + 'mailer' => 'required|in:smtp,support', + 'email' => 'required|email', + ]); + + $mailer = $request->mailer; + $targetEmail = $request->email; + + + try { + $start = microtime(true); + + $fromAddress = config("mail.mailers.{$mailer}.from.address") ?? config('mail.from.address'); + $fromName = config("mail.mailers.{$mailer}.from.name") ?? config('mail.from.name'); + $mode = $request->input('mode', 'raw'); + + if ($mode === 'mailable') { + Mail::mailer($mailer) + ->to($targetEmail) + ->send(new \App\Mail\ContactReply("SMTP Connection Test (Mailable Mode)", "This is a test message sent using the actual ContactReply mailable class.\n\nTime: " . now())); + } else { + Mail::mailer($mailer)->raw("This is a test email from the SMTP Tester (Raw Mode).\n\nMailer: $mailer\nFrom: $fromAddress ($fromName)\nTime: " . now(), function ($message) use ($targetEmail, $fromAddress, $fromName) { + $message->to($targetEmail) + ->from($fromAddress, $fromName) + ->subject('SMTP Connection Test (Raw Mode) - ' . config('app.name')); + }); + } + + $duration = round((microtime(true) - $start) * 1000, 2); + + return back()->with('success', "Test email sent successfully via '{$mailer}' (Mode: {$mode}) in {$duration}ms!"); + } catch (Exception $e) { + return back()->with('error', "Connection Failed: " . $e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Admin/TicketManagementController.php b/app/Http/Controllers/Admin/TicketManagementController.php new file mode 100644 index 0000000..b526dc0 --- /dev/null +++ b/app/Http/Controllers/Admin/TicketManagementController.php @@ -0,0 +1,125 @@ +has('search')) { + $search = $request->search; + $query->where(function($q) use ($search) { + $q->where('ticket_number', 'like', "%{$search}%") + ->orWhere('subject', 'like', "%{$search}%") + ->orWhereHas('user', function($u) use ($search) { + $u->where('email', 'like', "%{$search}%"); + }); + }); + } + + if ($request->has('status') && $request->status !== 'all') { + $query->where('status', $request->status); + } + + $tickets = $query->latest()->paginate(10)->withQueryString(); + + return view('pages.admin.tickets.index', [ + 'tickets' => $tickets, + 'title' => 'Support Ticket Management' + ]); + } + + public function show(Ticket $ticket) + { + $ticket->load(['replies.user', 'user.certificates', 'user.tickets' => function($q) { + $q->latest()->limit(5); + }]); + + return view('pages.admin.tickets.show', [ + 'ticket' => $ticket, + 'title' => 'Manage Ticket #' . $ticket->ticket_number + ]); + } + + public function reply(Request $request, Ticket $ticket) + { + $validated = $request->validate([ + 'message' => 'required|string|max:5000', + 'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx', + ]); + + $attachmentPath = null; + if ($request->hasFile('attachment')) { + $attachmentPath = $request->file('attachment')->store('attachments', 'public'); + } + + $reply = TicketReply::create([ + 'ticket_id' => $ticket->id, + 'user_id' => Auth::id(), + 'message' => strip_tags($validated['message']), + 'attachment_path' => $attachmentPath, + ]); + + // Auto update status to "Answered" if it was Open + if ($ticket->status === 'open') { + $ticket->update(['status' => 'answered']); + } + + $ticket->touch(); + + // Notify User + $ticket->user->notify(new NewTicketReply($reply)); + + // Broadcast for real-time + broadcast(new \App\Events\TicketMessageSent($reply))->toOthers(); + + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Reply sent successfully.', + 'reply' => [ + 'id' => $reply->id, + 'message' => $reply->message, + 'user_id' => $reply->user_id, + 'user_name' => $reply->user->name, + 'is_staff' => true, + 'created_at' => $reply->created_at->format('M d, Y H:i A'), + 'attachment_url' => $reply->attachment_path ? \Storage::url($reply->attachment_path) : null, + ] + ]); + } + + return back()->with('success', 'Reply sent successfully.'); + } + + public function updateStatus(Request $request, Ticket $ticket) + { + $validated = $request->validate([ + 'status' => 'required|in:open,answered,closed', + 'priority' => 'required|in:low,medium,high', + ]); + + $oldStatus = $ticket->status; + + $ticket->update([ + 'status' => $validated['status'], + 'priority' => $validated['priority'], + ]); + + if ($oldStatus !== $validated['status']) { + $ticket->user->notify(new TicketStatusUpdated($ticket)); + } + + return back()->with('success', 'Ticket updated successfully.'); + } +} diff --git a/app/Http/Controllers/ApiKeyController.php b/app/Http/Controllers/ApiKeyController.php index 2bd0a97..4cafc1f 100644 --- a/app/Http/Controllers/ApiKeyController.php +++ b/app/Http/Controllers/ApiKeyController.php @@ -41,6 +41,8 @@ class ApiKeyController extends Controller 'key' => $key, ]); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + return back()->with('success', 'API Key generated successfully.') ->with('generated_key', $key); } @@ -53,6 +55,8 @@ class ApiKeyController extends Controller $apiKey->delete(); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + return back()->with('success', 'API Key deleted successfully.'); } @@ -66,6 +70,8 @@ class ApiKeyController extends Controller 'is_active' => !$apiKey->is_active ]); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + if (request()->wantsJson()) { return response()->json(['success' => true, 'message' => 'API Key status updated successfully.']); } @@ -86,6 +92,8 @@ class ApiKeyController extends Controller 'last_used_at' => null, // Reset usage ]); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + if (request()->wantsJson()) { return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 5366b97..a4062ac 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -76,10 +76,14 @@ class AuthController extends Controller $roleInfo = \App\Models\Role::where('name', 'customer')->first(); + // Generate default avatar + $avatarPath = $this->generateDefaultAvatar($validated['fname'] . ' ' . ($validated['lname'] ?? ''), $validated['email']); + $user = User::create([ 'first_name' => $validated['fname'], 'last_name' => $validated['lname'], 'email' => $validated['email'], + 'avatar' => $avatarPath, 'password' => Hash::make($validated['password']), 'role_id' => $roleInfo ? $roleInfo->id : null, ]); @@ -92,164 +96,101 @@ class AuthController extends Controller } /** - * Unified social redirect method + * Redirect to social provider */ public function socialRedirect($provider, $context) { - // Store context in session for callback session(['social_auth_context' => $context]); - + return Socialite::driver($provider)->redirect(); } /** - * Unified social callback method + * Handle social provider callback */ - public function socialCallback($provider) + public function socialCallback($provider, Request $request) { - if (request()->has('error')) { - \Log::info('Social auth error: ' . request()->get('error')); - return redirect()->route('signin')->with('error', 'Authentication failed or was cancelled.'); - } - + $context = session('social_auth_context', 'signin'); + session()->forget('social_auth_context'); + try { $socialUser = Socialite::driver($provider)->user(); - \Log::info('Social user retrieved', ['provider' => $provider, 'email' => $socialUser->email]); } catch (\Exception $e) { - \Log::error('Socialite error: ' . $e->getMessage()); - return redirect()->route('signin')->with('error', 'Failed to authenticate with ' . ucfirst($provider) . '.'); + \Log::error("Social login failed for {$provider}: " . get_class($e) . ' - ' . $e->getMessage()); + \Log::error($e->getTraceAsString()); + return redirect()->route('signin')->with('error', 'Authentication failed. Please try again.'); } - $context = session('social_auth_context', 'signin'); - \Log::info('Social auth context', ['context' => $context, 'is_authenticated' => Auth::check()]); - session()->forget('social_auth_context'); + // Find user by social ID or email + $user = User::where($provider . '_id', $socialUser->getId()) + ->orWhere('email', $socialUser->getEmail()) + ->first(); - // Handle 'connect' context - user wants to link social account - if ($context === 'connect') { - if (!Auth::check()) { - \Log::warning('Connect attempt without authentication'); - return redirect()->route('signin')->with('error', 'Please sign in first to connect your social account.'); - } - \Log::info('Handling social connect from settings'); - return $this->connectSocialAccount($provider, $socialUser); - } - - // If user is already authenticated (shouldn't happen for signin/signup) - if (Auth::check()) { - \Log::info('User already authenticated, treating as connect'); - return $this->connectSocialAccount($provider, $socialUser); - } - - // Handle based on context - if ($context === 'signin') { - \Log::info('Handling social signin'); - return $this->handleSocialSignin($provider, $socialUser); - } else { - \Log::info('Handling social signup'); - return $this->handleSocialSignup($provider, $socialUser); - } - } - - /** - * Connect social account to existing authenticated user - */ - protected function connectSocialAccount($provider, $socialUser) - { - $user = Auth::user(); - - $updateData = [ - $provider . '_id' => $socialUser->id, - $provider . '_token' => $socialUser->token, - $provider . '_refresh_token' => $socialUser->refreshToken, - ]; - - // Ensure update persists - $user->forceFill($updateData)->save(); - - $this->recordLogin($user, $provider); - - \Log::info('Social account connected for user', ['user_id' => $user->id, 'provider' => $provider]); - - return redirect()->route('settings')->with('success', ucfirst($provider) . ' account connected successfully.'); - } - - /** - * Handle social signin (only for existing users) - */ - protected function handleSocialSignin($provider, $socialUser) - { - // Try to find user by provider ID first - $user = User::where($provider . '_id', $socialUser->id)->first(); - - // If user not found by ID - if (!$user) { - // Check if user exists by email just to provide a helpful error message - if ($socialUser->email && User::where('email', $socialUser->email)->exists()) { - $this->revokeSocialToken($provider, $socialUser->token); - return redirect()->route('signin')->with('error', "An account with this email exists but is not connected to " . ucfirst($provider) . ". Please log in with your password and connect your social account from Settings."); + if ($user) { + // Context: Connect + if ($context === 'connect') { + if (Auth::check()) { + $currentUser = Auth::user(); + if ($user->id !== $currentUser->id) { + return redirect()->route('settings')->with('error', "This {$provider} account is already linked to another user."); + } + } } - // Genuinely no account found - $this->revokeSocialToken($provider, $socialUser->token); - return redirect()->route('signin')->with('error', 'No account found with this ' . ucfirst($provider) . ' account. Please sign up first.'); - } - - // Proceed with login for connected user - - // Update login tracking - - - $this->recordLogin($user, $provider); - - Auth::login($user); - - return redirect()->route('dashboard'); - } - - /** - * Handle social signup (only for new users) - */ - protected function handleSocialSignup($provider, $socialUser) - { - // Check if user already exists by email - $existingUser = User::where('email', $socialUser->email)->first(); - - if ($existingUser) { - return redirect()->route('signup')->withErrors([ - 'email' => 'An account with this email already exists. Please sign in instead.', + // Update social tokens/ID if not set or changed + $user->update([ + $provider . '_id' => $socialUser->getId(), + $provider . '_token' => $socialUser->token, + $provider . '_refresh_token' => $socialUser->refreshToken ?? $user->{$provider . '_refresh_token'}, + 'email_verified_at' => $user->email_verified_at ?? now(), // Auto-verify if not already ]); + + // Login user if not already auth or if in auth context + if ($context !== 'connect') { + Auth::login($user); + $this->recordLogin($user, $provider); + return redirect()->intended('dashboard'); + } + + return redirect()->route('settings')->with('success', "{$provider} account connected successfully."); + } else { + // New User or Connect (but account doesn't exist) + + if ($context === 'connect' && Auth::check()) { + $user = Auth::user(); + $user->update([ + $provider . '_id' => $socialUser->getId(), + $provider . '_token' => $socialUser->token, + $provider . '_refresh_token' => $socialUser->refreshToken, + ]); + return redirect()->route('settings')->with('success', "{$provider} account connected successfully."); + } + + if ($context === 'signin') { + return redirect()->route('signin')->with('error', 'No account found with this email. Please sign up first.'); + } + + // Signup flow - Store in session and redirect to password setup + $nameParts = explode(' ', $socialUser->getName() ?? '', 2); + $firstName = $nameParts[0] ?? ''; + $lastName = $nameParts[1] ?? ''; + + $avatarPath = $this->downloadSocialAvatar($socialUser->getAvatar(), $socialUser->getEmail()); + + session([ + 'needs_password_setup' => true, + 'social_signup_provider' => $provider, + 'social_signup_email' => $socialUser->getEmail(), + 'social_signup_first_name' => $firstName, + 'social_signup_last_name' => $lastName, + 'social_signup_avatar_path' => $avatarPath, + 'social_signup_provider_id' => $socialUser->getId(), + 'social_signup_provider_token' => $socialUser->token, + 'social_signup_provider_refresh_token' => $socialUser->refreshToken ?? null, + ]); + + return redirect()->route('setup-password'); } - - // Download and save avatar - $avatarPath = null; - if ($socialUser->avatar) { - $avatarPath = $this->downloadSocialAvatar($socialUser->avatar, $socialUser->email); - } - - // Parse name - $nameParts = explode(' ', $socialUser->name ?? $socialUser->email, 2); - $firstName = $nameParts[0]; - $lastName = $nameParts[1] ?? ''; - - // Store user data in session for password setup - session([ - 'needs_password_setup' => true, - 'social_signup_provider' => $provider, - 'social_signup_name' => $socialUser->name ?? $socialUser->email, - 'social_signup_email' => $socialUser->email, - 'social_signup_first_name' => $firstName, - 'social_signup_last_name' => $lastName, - 'social_signup_avatar' => $avatarPath ? asset('storage/' . $avatarPath) : null, - 'social_signup_avatar_path' => $avatarPath, - 'social_signup_provider_id' => $socialUser->id, - 'social_signup_provider_token' => $socialUser->token, - 'social_signup_provider_refresh_token' => $socialUser->refreshToken, - ]); - - \Log::info('Social signup - redirecting to password setup', ['email' => $socialUser->email]); - - // Redirect to password setup page - return redirect()->route('setup-password'); } /** @@ -265,7 +206,10 @@ class AuthController extends Controller $imageContent = file_get_contents($avatarUrl); if ($imageContent === false) { - return null; + // Determine name for fallback + // We don't have the name here easily, so we use email part + $name = explode('@', $userEmail)[0]; + return $this->generateDefaultAvatar($name, $userEmail); } // Save to storage @@ -273,9 +217,26 @@ class AuthController extends Controller return $filename; } catch (\Exception $e) { - // If download fails, return null (user will have default avatar) + // If download fails, return generated avatar \Log::error('Failed to download social avatar: ' . $e->getMessage()); - return null; + $name = explode('@', $userEmail)[0]; + return $this->generateDefaultAvatar($name, $userEmail); + } + } + + /** + * Generate default avatar using Laravolt + */ + protected function generateDefaultAvatar($name, $email) + { + try { + $filename = 'avatars/' . Str::slug($email) . '_' . time() . '.png'; + $avatar = \Laravolt\Avatar\Facade::create($name)->getImageObject()->encode(new \Intervention\Image\Encoders\PngEncoder()); + Storage::disk('public')->put($filename, $avatar); + return $filename; + } catch (\Exception $e) { + \Log::error('Failed to generate default avatar: ' . $e->getMessage()); + return null; // Ultimate fallback to null } } @@ -330,6 +291,7 @@ class AuthController extends Controller $provider . '_id' => $providerId, $provider . '_token' => $providerToken, $provider . '_refresh_token' => $providerRefreshToken, + 'email_verified_at' => now(), // Auto-verify email from trusted social provider ]); $this->recordLogin($user, $provider); diff --git a/app/Http/Controllers/CertificateController.php b/app/Http/Controllers/CertificateController.php index 2b1e872..23bc3b6 100644 --- a/app/Http/Controllers/CertificateController.php +++ b/app/Http/Controllers/CertificateController.php @@ -252,6 +252,8 @@ BATCH; 'valid_to' => $result['valid_to'] ?? null, ]); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + return redirect()->route('certificate.index')->with('success', 'Certificate generated successfully.'); } catch (\Exception $e) { return redirect()->back()->withInput()->with('error', $e->getMessage()); @@ -285,6 +287,8 @@ BATCH; 'created_at' => now(), // Refresh timestamp ]); + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + return redirect()->route('certificate.index')->with('success', 'Certificate regenerated successfully.'); } catch (\Exception $e) { return redirect()->route('certificate.index')->with('error', 'Failed to regenerate: ' . $e->getMessage()); @@ -348,6 +352,9 @@ BATCH; { $this->authorizeOwner($certificate); $certificate->delete(); + + \App\Events\DashboardStatsUpdated::dispatch(Auth::id()); + return redirect()->route('certificate.index')->with('success', 'Certificate deleted successfully.'); } diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php new file mode 100644 index 0000000..093ec5c --- /dev/null +++ b/app/Http/Controllers/ContactController.php @@ -0,0 +1,35 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:255', + 'category' => 'required|string|in:Legal Inquiry,Technical Support,Partnership,Other', + 'subject' => 'required|string|max:255', + 'message' => 'required|string|max:2000', + ]); + + ContactSubmission::create([ + 'name' => strip_tags($validated['name']), + 'email' => $validated['email'], + 'category' => $validated['category'], + 'subject' => strip_tags($validated['subject']), + 'message' => strip_tags($validated['message']), + ]); + + return back()->with('success', 'Your message has been sent successfully. We will get back to you soon!'); + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 2492ad3..1b566ad 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -48,15 +48,104 @@ class DashboardController extends Controller ->count(); } - return view('pages.dashboard', compact( - 'totalApiKeys', - 'totalCertificates', - 'activeCertificates', - 'expiringSoonCount', - 'recentCertificates', - 'recentApiActivity', - 'months', - 'issuanceData' - )); + // Format data for view + $formattedCertificates = $recentCertificates->map(fn($c) => [ + 'common_name' => $c->common_name, + 'organization' => $c->organization, + 'created_at' => $c->created_at->format('M d, Y'), + 'valid_to' => $c->valid_to->format('M d, Y'), + 'is_valid' => $c->valid_to > now(), + ]); + + $formattedApiActivity = $recentApiActivity->map(fn($k) => [ + 'name' => $k->name, + 'last_used_diff' => $k->last_used_at?->diffForHumans() ?? 'None', + ]); + + return view('pages.dashboard', [ + 'totalApiKeys' => $totalApiKeys, + 'totalCertificates' => $totalCertificates, + 'activeCertificates' => $activeCertificates, + 'expiringSoonCount' => $expiringSoonCount, + 'recentCertificates' => $formattedCertificates, + 'recentApiActivity' => $formattedApiActivity, + 'months' => $months, + 'issuanceData' => $issuanceData + ]); + } + + public function stats() + { + $user = Auth::user(); + + // Basic Counts + $totalApiKeys = $user->apiKeys()->count(); + $totalCertificates = $user->certificates()->count(); + $activeCertificates = $user->certificates() + ->where('valid_to', '>', now()) + ->count(); + $expiringSoonCount = $user->certificates() + ->where('valid_to', '>', now()) + ->where('valid_to', '<=', now()->addDays(14)) + ->count(); + + // Recent Activity + $recentCertificates = $user->certificates() + ->latest() + ->limit(5) + ->get()->map(function($cert) { + return [ + 'common_name' => $cert->common_name, + 'organization' => $cert->organization, + 'created_at' => $cert->created_at->format('M d, Y'), + 'valid_to' => $cert->valid_to->format('M d, Y'), + 'is_valid' => $cert->valid_to > now(), + ]; + }); + + $recentApiActivity = $user->apiKeys() + ->whereNotNull('last_used_at') + ->orderBy('last_used_at', 'desc') + ->limit(5) + ->get()->map(function($key) { + return [ + 'name' => $key->name, + 'last_used_diff' => $key->last_used_at->diffForHumans(), + ]; + }); + + // Chart Data + $months = []; + $issuanceData = []; + for ($i = 5; $i >= 0; $i--) { + $date = now()->subMonths($i); + $months[] = $date->format('M'); + $issuanceData[] = $user->certificates() + ->whereYear('created_at', $date->year) + ->whereMonth('created_at', $date->month) + ->count(); + } + + return response()->json([ + 'totalApiKeys' => $totalApiKeys, + 'totalCertificates' => $totalCertificates, + 'activeCertificates' => $activeCertificates, + 'expiringSoonCount' => $expiringSoonCount, + 'recentCertificates' => $recentCertificates, + 'recentApiActivity' => $recentApiActivity, + 'months' => $months, + 'issuanceData' => $issuanceData, + 'maxIssuance' => max($issuanceData) ?: 1, + ]); + } + + public function ping() + { + $userId = Auth::id(); + $timestamp = now()->getTimestampMs(); + + \App\Events\PingResponse::dispatch($userId, $timestamp); + + return response()->noContent(); } } diff --git a/app/Http/Controllers/LegalController.php b/app/Http/Controllers/LegalController.php new file mode 100644 index 0000000..a110c5a --- /dev/null +++ b/app/Http/Controllers/LegalController.php @@ -0,0 +1,25 @@ +where('is_active', true) + ->with(['currentRevision']) + ->firstOrFail(); + + $revision = $page->currentRevision; + + if (!$revision) { + abort(404, 'No active content found for this page.'); + } + + return view('pages.legal.show', compact('page', 'revision')); + } +} diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php new file mode 100644 index 0000000..6d70613 --- /dev/null +++ b/app/Http/Controllers/NotificationController.php @@ -0,0 +1,49 @@ +notifications()->findOrFail($id); + $notification->markAsRead(); + + // Redirect to the target URL if available + if (isset($notification->data['action_url'])) { + return redirect($notification->data['action_url']); + } + + return back(); + } + + public function markAllRead() + { + Auth::user()->unreadNotifications->markAsRead(); + return back()->with('success', 'All notifications marked as read.'); + } + public function getUnread() + { + $notifications = Auth::user()->unreadNotifications->map(function ($notification) { + return [ + 'id' => $notification->id, + 'data' => [ + 'title' => $notification->data['title'] ?? 'Notification', + 'body' => $notification->data['body'] ?? '', + 'icon' => $notification->data['icon'] ?? null, + 'action_url' => $notification->data['action_url'] ?? null, + ], + 'created_at_human' => $notification->created_at->diffForHumans(), + 'read_url' => route('notifications.read', $notification->id), + ]; + }); + + return response()->json([ + 'notifications' => $notifications, + 'count' => $notifications->count() + ]); + } +} diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 0b5194a..d79984a 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -17,6 +17,11 @@ class PageController extends Controller return view('pages.blank', ['title' => 'Blank']); } + public function landing() + { + return view('landing', ['title' => 'Home']); + } + public function error404() { return view('pages.errors.error-404', ['title' => 'Error 404']); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php new file mode 100644 index 0000000..46fcb38 --- /dev/null +++ b/app/Http/Controllers/SearchController.php @@ -0,0 +1,177 @@ +get('q'); + $user = Auth::user(); + + if (!$query || strlen($query) < 2) { + return response()->json([ + 'Navigation' => $this->getNavigation($user, $query) + ]); + } + + $results = []; + + // 1. Navigation Search + $navigation = $this->getNavigation($user, $query); + if (!empty($navigation)) { + $results['Navigation'] = $navigation; + } + + if ($user->isAdmin()) { + // 2. Admin: User Search + $users = User::where('name', 'like', "%$query%") + ->orWhere('email', 'like', "%$query%") + ->limit(5) + ->get() + ->map(fn($u) => [ + 'label' => $u->name, + 'sublabel' => $u->email, + 'url' => route('admin.users.index') . '?search=' . $u->email, + 'icon' => 'user' + ]); + if ($users->count() > 0) $results['Users'] = $users; + + // 3. Admin: Ticket Search + $tickets = Ticket::where('ticket_id', 'like', "%$query%") + ->orWhere('subject', 'like', "%$query%") + ->limit(5) + ->get() + ->map(fn($t) => [ + 'label' => $t->subject, + 'sublabel' => '#' . $t->ticket_id, + 'url' => route('admin.tickets.show', $t->id), + 'icon' => 'ticket' + ]); + if ($tickets->count() > 0) $results['Tickets'] = $tickets; + + // 4. Admin: Root CA Search + $cas = CaCertificate::where('common_name', 'like', "%$query%") + ->orWhere('uuid', 'like', "%$query%") + ->limit(5) + ->get() + ->map(fn($ca) => [ + 'label' => $ca->common_name, + 'sublabel' => 'Type: ' . strtoupper($ca->ca_type), + 'url' => route('admin.root-ca.index'), + 'icon' => 'shield' + ]); + if ($cas->count() > 0) $results['Root CAs'] = $cas; + + } else { + // 4. Customer: Certificate Search + $certs = Certificate::where('user_id', $user->id) + ->where(function($q) use ($query) { + $q->where('common_name', 'like', "%$query%") + ->orWhere('uuid', 'like', "%$query%"); + }) + ->limit(5) + ->get() + ->map(fn($c) => [ + 'label' => $c->common_name, + 'sublabel' => 'UUID: ' . substr($c->uuid, 0, 8) . '...', + 'url' => route('certificate.index') . '?uuid=' . $c->uuid, + 'icon' => 'certificate' + ]); + if ($certs->count() > 0) $results['Certificates'] = $certs; + + // 5. Customer: Ticket Search + $tickets = Ticket::where('user_id', $user->id) + ->where(function($q) use ($query) { + $q->where('ticket_id', 'like', "%$query%") + ->orWhere('subject', 'like', "%$query%"); + }) + ->limit(5) + ->get() + ->map(fn($t) => [ + 'label' => $t->subject, + 'sublabel' => '#' . $t->ticket_id, + 'url' => route('support.show', $t->id), + 'icon' => 'ticket' + ]); + if ($tickets->count() > 0) $results['Tickets'] = $tickets; + } + + return response()->json($results); + } + + private function getNavigation($user, $query = null) + { + $menuGroups = MenuHelper::getMenuGroups(); + $allNavs = []; + + foreach ($menuGroups as $group) { + // Skip Template Gallery group explicitly just in case + if ($group['title'] === 'Template Gallery' || $group['title'] === 'Development') continue; + + foreach ($group['items'] as $item) { + // Process main item + $this->processMenuItem($item, $allNavs); + + // Process subItems if any + if (isset($item['subItems'])) { + foreach ($item['subItems'] as $subItem) { + $this->processMenuItem($subItem, $allNavs); + } + } + } + } + + return collect($allNavs) + ->filter(function($nav) use ($query) { + // Exclude templates + if (isset($nav['route_name']) && str_starts_with($nav['route_name'], 'templates.')) return false; + + // Check query + if ($query && stripos($nav['label'], $query) === false) return false; + + return true; + }) + ->unique('url') // Avoid duplicates + ->values() + ->take(8) // Give more suggestions + ->toArray(); + } + + private function processMenuItem($item, &$allNavs) + { + if (!isset($item['route_name'])) return; + + $allNavs[] = [ + 'label' => $item['name'], + 'url' => route($item['route_name']), + 'icon' => $this->mapIcon($item['icon'] ?? 'default'), + 'route_name' => $item['route_name'] + ]; + } + + private function mapIcon($sidebarIcon) + { + // Map common sidebar icons to search icons + $map = [ + 'dashboard' => 'home', + 'certificate' => 'certificate', + 'support-ticket' => 'ticket', + 'users' => 'users', + 'user-profile' => 'user', + 'settings' => 'settings', + 'api-key' => 'key', + 'server-settings' => 'shield', + ]; + + return $map[$sidebarIcon] ?? 'default'; + } +} diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php new file mode 100644 index 0000000..17e3da3 --- /dev/null +++ b/app/Http/Controllers/TicketController.php @@ -0,0 +1,165 @@ +latest() + ->paginate(10); + + return view('pages.support.index', [ + 'tickets' => $tickets, + 'title' => 'My Support Tickets' + ]); + } + + public function create() + { + return view('pages.support.create', [ + 'title' => 'Open New Ticket' + ]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'subject' => 'required|string|max:255', + 'category' => 'required|string|in:Technical,Billing,General,Feature Request,Other', + 'priority' => 'required|in:low,medium,high', + 'message' => 'required|string|max:5000', + 'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx', + ]); + + $ticket = Ticket::create([ + 'user_id' => Auth::id(), + 'ticket_number' => 'TCK-' . date('Ymd') . '-' . strtoupper(Str::random(4)), + 'subject' => strip_tags($validated['subject']), + 'category' => $validated['category'], + 'priority' => $validated['priority'], + 'status' => 'open', + ]); + + $attachmentPath = null; + if ($request->hasFile('attachment')) { + $attachmentPath = $request->file('attachment')->store('attachments', 'public'); + } + + // Create initial message as a reply/description + TicketReply::create([ + 'ticket_id' => $ticket->id, + 'user_id' => Auth::id(), + 'message' => strip_tags($validated['message']), + 'attachment_path' => $attachmentPath, + ]); + + // Notify Admins + $admins = User::whereHas('role', function($q) { + $q->where('name', 'admin'); + })->get(); + + foreach ($admins as $admin) { + $admin->notify(new NewTicketCreated($ticket)); + } + + return redirect()->route('support.show', $ticket) + ->with('success', 'Ticket created successfully.'); + } + + public function show(Ticket $ticket) + { + // Authorize + if ($ticket->user_id !== Auth::id()) { + abort(403); + } + + $ticket->load(['replies.user', 'user']); + + return view('pages.support.show', [ + 'ticket' => $ticket, + 'title' => 'Ticket #' . $ticket->ticket_number + ]); + } + + public function reply(Request $request, Ticket $ticket) + { + if ($ticket->user_id !== Auth::id()) { + abort(403); + } + + $validated = $request->validate([ + 'message' => 'required|string|max:5000', + 'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx', + ]); + + $attachmentPath = null; + if ($request->hasFile('attachment')) { + $attachmentPath = $request->file('attachment')->store('attachments', 'public'); + } + + $reply = TicketReply::create([ + 'ticket_id' => $ticket->id, + 'user_id' => Auth::id(), + 'message' => strip_tags($validated['message']), + 'attachment_path' => $attachmentPath, + ]); + + // Update ticket status if it was closed (optional, usually re-opens it) + if ($ticket->status === 'closed') { + $ticket->update(['status' => 'open']); + } else { + $ticket->touch(); // Update updated_at + } + + // Notify Admins (Logic could be refined to notify specific assignee later) + $admins = User::whereHas('role', function($q) { + $q->where('name', 'admin'); + })->get(); + + foreach ($admins as $admin) { + $admin->notify(new NewTicketReply($reply)); + } + + // Broadcast for real-time + broadcast(new \App\Events\TicketMessageSent($reply))->toOthers(); + + if ($request->ajax() || $request->wantsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Reply sent successfully.', + 'reply' => [ + 'id' => $reply->id, + 'message' => $reply->message, + 'user_id' => $reply->user_id, + 'user_name' => $reply->user->name, + 'created_at' => $reply->created_at->format('M d, Y H:i A'), + 'attachment_url' => $reply->attachment_path ? \Storage::url($reply->attachment_path) : null, + ] + ]); + } + + return back()->with('success', 'Reply sent successfully.'); + } + + public function close(Ticket $ticket) + { + if ($ticket->user_id !== Auth::id()) { + abort(403); + } + + $ticket->update(['status' => 'closed']); + + return back()->with('success', 'Ticket marked as closed.'); + } +} diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php new file mode 100644 index 0000000..097b050 --- /dev/null +++ b/app/Http/Controllers/ToolController.php @@ -0,0 +1,25 @@ +update(['last_used_at' => now()]); + // Real-time update dashboard + \App\Events\DashboardStatsUpdated::dispatch($apiKey->user_id); + // Put the user in the request context $request->merge(['authenticated_user' => $apiKey->user]); diff --git a/app/Mail/ContactReply.php b/app/Mail/ContactReply.php new file mode 100644 index 0000000..0ac9828 --- /dev/null +++ b/app/Mail/ContactReply.php @@ -0,0 +1,64 @@ +replySubject = $subject; + $this->replyMessage = $message; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + from: new \Illuminate\Mail\Mailables\Address( + config('mail.mailers.support.from.address'), + config('mail.mailers.support.from.name') + ), + subject: $this->replySubject, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.contact-reply', + with: [ + 'replyMessage' => $this->replyMessage, + ] + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/ContactSubmission.php b/app/Models/ContactSubmission.php new file mode 100644 index 0000000..17a609a --- /dev/null +++ b/app/Models/ContactSubmission.php @@ -0,0 +1,22 @@ + */ + use HasFactory, HasUlids; + + protected $fillable = [ + 'name', + 'email', + 'category', + 'subject', + 'message', + 'is_read' + ]; +} diff --git a/app/Models/LegalPage.php b/app/Models/LegalPage.php new file mode 100644 index 0000000..1b181ca --- /dev/null +++ b/app/Models/LegalPage.php @@ -0,0 +1,23 @@ +hasMany(LegalPageRevision::class); + } + + public function currentRevision() + { + return $this->hasOne(LegalPageRevision::class)->where('is_active', true)->latestOfMany(); + } +} diff --git a/app/Models/LegalPageRevision.php b/app/Models/LegalPageRevision.php new file mode 100644 index 0000000..e10251d --- /dev/null +++ b/app/Models/LegalPageRevision.php @@ -0,0 +1,25 @@ +belongsTo(LegalPage::class); + } +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php new file mode 100644 index 0000000..e05fd89 --- /dev/null +++ b/app/Models/Ticket.php @@ -0,0 +1,31 @@ +belongsTo(User::class); + } + + public function replies() + { + return $this->hasMany(TicketReply::class); + } +} diff --git a/app/Models/TicketReply.php b/app/Models/TicketReply.php new file mode 100644 index 0000000..7700b91 --- /dev/null +++ b/app/Models/TicketReply.php @@ -0,0 +1,29 @@ +belongsTo(Ticket::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index dcdf41a..f44cccb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -50,6 +50,7 @@ class User extends Authenticatable implements MustVerifyEmail protected $fillable = [ 'role_id', 'email', + 'email_verified_at', 'password', 'avatar', 'first_name', @@ -152,4 +153,9 @@ class User extends Authenticatable implements MustVerifyEmail { return $this->hasMany(Certificate::class); } + + public function tickets() + { + return $this->hasMany(Ticket::class); + } } diff --git a/app/Notifications/NewTicketCreated.php b/app/Notifications/NewTicketCreated.php new file mode 100644 index 0000000..5d952a9 --- /dev/null +++ b/app/Notifications/NewTicketCreated.php @@ -0,0 +1,51 @@ +ticket = $ticket; + } + + public function via(object $notifiable): array + { + return ['database', 'broadcast']; + } + + public function toArray(object $notifiable): array + { + return [ + 'icon' => 'ticket', + 'title' => 'New Support Ticket', + 'body' => "{$this->ticket->user->name} created ticket #{$this->ticket->ticket_number}: {$this->ticket->subject}", + 'action_url' => route('admin.tickets.show', $this->ticket->id), + 'type' => 'info', + 'created_at' => now(), + ]; + } + + public function toBroadcast(object $notifiable): BroadcastMessage + { + return new BroadcastMessage([ + 'id' => $this->id, + 'data' => $this->toArray($notifiable), + 'created_at' => now(), + 'created_at_human' => now()->diffForHumans(), // Pre-calculate for frontend + 'read_url' => route('notifications.read', $this->id), + ]); + } +} diff --git a/app/Notifications/NewTicketReply.php b/app/Notifications/NewTicketReply.php new file mode 100644 index 0000000..ab30e53 --- /dev/null +++ b/app/Notifications/NewTicketReply.php @@ -0,0 +1,56 @@ +reply = $reply; + } + + public function via(object $notifiable): array + { + return ['database', 'broadcast']; + } + + public function toArray(object $notifiable): array + { + $ticket = $this->reply->ticket; + $url = $notifiable->isAdmin() + ? route('admin.tickets.show', $ticket->id) + : route('support.show', $ticket->id); + + return [ + 'icon' => 'chat', + 'title' => "New Reply on Ticket #{$ticket->ticket_number}", + 'body' => "{$this->reply->user->name}: " . \Illuminate\Support\Str::limit($this->reply->message, 50), + 'action_url' => $url, + 'type' => 'info', + 'created_at' => now(), + ]; + } + + public function toBroadcast(object $notifiable): BroadcastMessage + { + return new BroadcastMessage([ + 'id' => $this->id, + 'data' => $this->toArray($notifiable), + 'created_at' => now(), + 'created_at_human' => now()->diffForHumans(), + 'read_url' => route('notifications.read', $this->id), + ]); + } +} diff --git a/app/Notifications/SystemAlert.php b/app/Notifications/SystemAlert.php new file mode 100644 index 0000000..dc87469 --- /dev/null +++ b/app/Notifications/SystemAlert.php @@ -0,0 +1,70 @@ +title = $title; + $this->message = $message; + $this->type = $type; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(object $notifiable): array + { + return ['database', 'broadcast']; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'icon' => 'notification', + 'title' => $this->title, + 'body' => $this->message, + 'type' => $this->type, + 'action_url' => '#', + 'created_at' => now(), + ]; + } + + /** + * Get the broadcast representation of the notification. + */ + public function toBroadcast(object $notifiable): BroadcastMessage + { + return new BroadcastMessage([ + 'id' => $this->id, + 'data' => $this->toArray($notifiable), + 'created_at' => now(), + 'created_at_human' => now()->diffForHumans(), + 'read_url' => route('notifications.read', $this->id), + ]); + } +} diff --git a/app/Notifications/TicketStatusUpdated.php b/app/Notifications/TicketStatusUpdated.php new file mode 100644 index 0000000..8957825 --- /dev/null +++ b/app/Notifications/TicketStatusUpdated.php @@ -0,0 +1,38 @@ +ticket = $ticket; + } + + public function via(object $notifiable): array + { + return ['database']; + } + + public function toArray(object $notifiable): array + { + return [ + 'icon' => 'check-circle', + 'title' => 'Ticket Status Updated', + 'body' => "Your ticket #{$this->ticket->ticket_number} has been marked as " . ucfirst($this->ticket->status), + 'action_url' => route('support.show', $this->ticket->id), + 'type' => $this->ticket->status === 'closed' ? 'success' : 'info', + 'created_at' => now(), + ]; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 776f14a..f03ad7c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -8,6 +8,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', + channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { diff --git a/composer.json b/composer.json index f29e89e..f29ff43 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,10 @@ "require": { "php": "^8.2", "laravel/framework": "^12.0", + "laravel/reverb": "^1.0", "laravel/socialite": "^5.24", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "laravolt/avatar": "^6.3" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 4cde84e..dace91d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f581c6d28518c3a47ca2d65cb9f7cc60", + "content-hash": "065b9d83adf2a17b87ed567b244f3790", "packages": [ { "name": "brick/math", @@ -135,6 +135,136 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "clue/redis-protocol", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/clue/redis-protocol.git", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/redis-protocol/zipball/6f565332f5531b7722d1e9c445314b91862f6d6c", + "reference": "6f565332f5531b7722d1e9c445314b91862f6d6c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\Redis\\Protocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.", + "homepage": "https://github.com/clue/redis-protocol", + "keywords": [ + "parser", + "protocol", + "redis", + "resp", + "serializer", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/redis-protocol/issues", + "source": "https://github.com/clue/redis-protocol/tree/v0.3.2" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-08-07T11:06:28+00:00" + }, + { + "name": "clue/redis-react", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-redis.git", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-redis/zipball/84569198dfd5564977d2ae6a32de4beb5a24bdca", + "reference": "84569198dfd5564977d2ae6a32de4beb5a24bdca", + "shasum": "" + }, + "require": { + "clue/redis-protocol": "^0.3.2", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.0 || ^1.1", + "react/promise-timer": "^1.11", + "react/socket": "^1.16" + }, + "require-dev": { + "clue/block-react": "^1.5", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Async Redis client implementation, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-redis", + "keywords": [ + "async", + "client", + "database", + "reactphp", + "redis" + ], + "support": { + "issues": "https://github.com/clue/reactphp-redis/issues", + "source": "https://github.com/clue/reactphp-redis/tree/v2.8.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2025-01-03T16:18:33+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -509,6 +639,53 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, { "name": "firebase/php-jwt", "version": "v6.11.1", @@ -1116,6 +1293,150 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-03-29T07:46:21+00:00" + }, + { + "name": "intervention/image", + "version": "3.11.6", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/5f6d27d9fd56312c47f347929e7ac15345c605a1", + "reference": "5f6d27d9fd56312c47f347929e7ac15345c605a1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" + } + ], + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.11.6" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2025-12-17T13:38:29+00:00" + }, { "name": "laravel/framework", "version": "v12.26.4", @@ -1392,6 +1713,88 @@ }, "time": "2025-07-07T14:17:42+00:00" }, + { + "name": "laravel/reverb", + "version": "v1.6.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "b97d21650bcfaa462dfa4735048dbc33359514e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/b97d21650bcfaa462dfa4735048dbc33359514e1", + "reference": "b97d21650bcfaa462dfa4735048dbc33359514e1", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0|^12.0", + "illuminate/contracts": "^10.47|^11.0|^12.0", + "illuminate/http": "^10.47|^11.0|^12.0", + "illuminate/support": "^10.47|^11.0|^12.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.4", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0", + "symfony/http-foundation": "^6.3|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.0|^3.0|^4.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Output": "Laravel\\Reverb\\Output" + }, + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.6.3" + }, + "time": "2025-11-28T20:12:49+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.4", @@ -1591,6 +1994,97 @@ }, "time": "2025-01-27T14:24:01+00:00" }, + { + "name": "laravolt/avatar", + "version": "6.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravolt/avatar.git", + "reference": "dea77b6fbff08f38e744a9e23855369c8270f2aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravolt/avatar/zipball/dea77b6fbff08f38e744a9e23855369c8270f2aa", + "reference": "dea77b6fbff08f38e744a9e23855369c8270f2aa", + "shasum": "" + }, + "require": { + "illuminate/cache": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "intervention/image": "^3.4", + "php": ">=8.2" + }, + "require-dev": { + "mockery/mockery": "^1.6.7", + "pestphp/pest": "^2.34|^3.7", + "pestphp/pest-plugin-type-coverage": "^2.8|^3.3", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^11.5.3", + "roave/security-advisories": "dev-latest" + }, + "suggest": { + "ext-gd": "Needed to support image manipulation", + "ext-imagick": "Needed to support image manipulation, better than GD" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Avatar": "Laravolt\\Avatar\\Facade" + }, + "providers": [ + "Laravolt\\Avatar\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "psr-4": { + "Laravolt\\Avatar\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bayu Hendra Winata", + "email": "uyab.exe@gmail.com", + "homepage": "https://laravolt.dev", + "role": "Developer" + } + ], + "description": "Turn name, email, and any other string into initial-based avatar or gravatar.", + "homepage": "https://github.com/laravolt/avatar", + "keywords": [ + "avatar", + "gravatar", + "laravel", + "laravolt" + ], + "support": { + "issues": "https://github.com/laravolt/avatar/issues", + "source": "https://github.com/laravolt/avatar/tree/6.3.1" + }, + "funding": [ + { + "url": "https://paypal.me/bayuhendra", + "type": "custom" + }, + { + "url": "https://ko-fi.com/bayuhendra", + "type": "ko_fi" + }, + { + "url": "https://www.patreon.com/uyab", + "type": "patreon" + } + ], + "time": "2025-08-27T08:20:27+00:00" + }, { "name": "league/commonmark", "version": "2.7.1", @@ -2841,6 +3335,102 @@ }, "time": "2020-10-15T08:29:30+00:00" }, + { + "name": "paragonie/sodium_compat", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "547e2dc4d45107440e76c17ab5a46e4252460158" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/547e2dc4d45107440e76c17ab5a46e4252460158", + "reference": "547e2dc4d45107440e76c17ab5a46e4252460158", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^7|^8|^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "ParagonIE\\Sodium\\": "namespaced/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.4.0" + }, + "time": "2025-10-06T08:47:40+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -3516,6 +4106,67 @@ }, "time": "2025-08-04T12:39:37+00:00" }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3714,6 +4365,595 @@ }, "time": "2025-06-25T14:20:11+00:00" }, + { + "name": "ratchet/rfc6455", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "psr/http-factory-implementation": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.7", + "phpunit/phpunit": "^9.5", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + }, + "time": "2025-02-24T01:18:22+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..ebc3fb9 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,82 @@ + env('BROADCAST_CONNECTION', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over WebSockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'reverb' => [ + 'driver' => 'reverb', + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', + 'port' => env('PUSHER_PORT', 443), + 'scheme' => env('PUSHER_SCHEME', 'https'), + 'encrypted' => true, + 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + + 'log' => [ + 'driver' => 'log', + ], + + 'null' => [ + 'driver' => 'null', + ], + + ], + +]; diff --git a/config/laravolt/avatar-hd.php b/config/laravolt/avatar-hd.php new file mode 100644 index 0000000..615a4a8 --- /dev/null +++ b/config/laravolt/avatar-hd.php @@ -0,0 +1,253 @@ + [ + // Enable HD mode - when true, uses higher resolution settings + 'enabled' => env('AVATAR_HD_ENABLED', true), + + // HD dimensions (default: 512x512, supports up to 2048x2048) + 'width' => env('AVATAR_HD_WIDTH', 512), + 'height' => env('AVATAR_HD_HEIGHT', 512), + + // HD font size (scales with dimensions) + 'fontSize' => env('AVATAR_HD_FONT_SIZE', 192), + + // Export quality settings + 'quality' => [ + 'png' => env('AVATAR_HD_PNG_QUALITY', 95), + 'jpg' => env('AVATAR_HD_JPG_QUALITY', 90), + 'webp' => env('AVATAR_HD_WEBP_QUALITY', 85), + ], + + // Anti-aliasing for smoother edges + 'antialiasing' => env('AVATAR_HD_ANTIALIASING', true), + + // DPI for high-quality rendering + 'dpi' => env('AVATAR_HD_DPI', 300), + ], + + /* + |-------------------------------------------------------------------------- + | Export and Storage Settings + |-------------------------------------------------------------------------- + | Configuration for image export and storage optimization + | + */ + 'export' => [ + // Default export format + 'format' => env('AVATAR_EXPORT_FORMAT', 'png'), // png, jpg, webp + + // Export path (relative to storage/app) + 'path' => env('AVATAR_EXPORT_PATH', 'avatars'), + + // Filename pattern: {name}, {initials}, {hash}, {timestamp} + 'filename_pattern' => env('AVATAR_EXPORT_FILENAME', '{hash}_{timestamp}.{format}'), + + // Enable multiple format export + 'multiple_formats' => env('AVATAR_EXPORT_MULTIPLE', false), + + // Progressive JPEG for better loading + 'progressive_jpeg' => env('AVATAR_PROGRESSIVE_JPEG', true), + + // WebP lossless compression + 'webp_lossless' => env('AVATAR_WEBP_LOSSLESS', false), + ], + + /* + |-------------------------------------------------------------------------- + | Performance and Caching + |-------------------------------------------------------------------------- + | Enhanced caching and performance settings for HD avatars + | + */ + 'performance' => [ + // Enable file-based caching in addition to memory cache + 'file_cache' => env('AVATAR_FILE_CACHE', true), + + // Cache different sizes separately + 'size_based_cache' => env('AVATAR_SIZE_CACHE', true), + + // Preload fonts for better performance + 'preload_fonts' => env('AVATAR_PRELOAD_FONTS', true), + + // Background processing for large batches + 'background_processing' => env('AVATAR_BACKGROUND_PROCESSING', false), + + // Lazy loading support + 'lazy_loading' => env('AVATAR_LAZY_LOADING', true), + + // Compression levels + 'compression' => [ + 'png' => env('AVATAR_PNG_COMPRESSION', 6), // 0-9 + 'webp' => env('AVATAR_WEBP_COMPRESSION', 80), // 0-100 + ], + ], + + /* + |-------------------------------------------------------------------------- + | Storage Management + |-------------------------------------------------------------------------- + | Configuration for storage optimization and cleanup + | + */ + 'storage' => [ + // Automatic cleanup of old files + 'auto_cleanup' => env('AVATAR_AUTO_CLEANUP', true), + + // Maximum age for cached files (in days) + 'max_age_days' => env('AVATAR_MAX_AGE_DAYS', 30), + + // Maximum storage size (in MB, 0 = unlimited) + 'max_storage_mb' => env('AVATAR_MAX_STORAGE_MB', 500), + + // Storage driver (local, s3, etc.) + 'disk' => env('AVATAR_STORAGE_DISK', 'local'), + + // CDN URL for serving images + 'cdn_url' => env('AVATAR_CDN_URL', null), + + // Enable storage metrics + 'metrics' => env('AVATAR_STORAGE_METRICS', false), + ], + + /* + |-------------------------------------------------------------------------- + | HD Themes + |-------------------------------------------------------------------------- + | Enhanced themes with HD-specific optimizations + | + */ + 'hd_themes' => [ + 'ultra-hd' => [ + 'width' => 1024, + 'height' => 1024, + 'fontSize' => 384, + 'backgrounds' => [ + '#667eea', '#764ba2', '#f093fb', '#f5576c', + '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', + '#ffecd2', '#fcb69f', '#a8edea', '#fed6e3', + ], + 'foregrounds' => ['#FFFFFF'], + 'border' => [ + 'size' => 4, + 'color' => 'foreground', + 'radius' => 8, + ], + ], + 'retina' => [ + 'width' => 512, + 'height' => 512, + 'fontSize' => 192, + 'backgrounds' => [ + '#667eea', '#764ba2', '#f093fb', '#f5576c', + '#4facfe', '#00f2fe', '#43e97b', '#38f9d7', + ], + 'foregrounds' => ['#FFFFFF'], + ], + 'material-hd' => [ + 'width' => 384, + 'height' => 384, + 'fontSize' => 144, + 'shape' => 'circle', + 'backgrounds' => [ + '#1976D2', '#388E3C', '#F57C00', '#7B1FA2', + '#5D4037', '#455A64', '#E64A19', '#00796B', + ], + 'foregrounds' => ['#FFFFFF'], + 'border' => [ + 'size' => 2, + 'color' => 'background', + 'radius' => 0, + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Responsive Sizes + |-------------------------------------------------------------------------- + | Predefined sizes for responsive avatar generation + | + */ + 'responsive_sizes' => [ + 'thumbnail' => ['width' => 64, 'height' => 64, 'fontSize' => 24], + 'small' => ['width' => 128, 'height' => 128, 'fontSize' => 48], + 'medium' => ['width' => 256, 'height' => 256, 'fontSize' => 96], + 'large' => ['width' => 512, 'height' => 512, 'fontSize' => 192], + 'xl' => ['width' => 768, 'height' => 768, 'fontSize' => 288], + 'xxl' => ['width' => 1024, 'height' => 1024, 'fontSize' => 384], + ], + + /* + |-------------------------------------------------------------------------- + | Advanced Features + |-------------------------------------------------------------------------- + | Additional HD avatar features + | + */ + 'features' => [ + // Generate avatar sprites for animations + 'sprites' => env('AVATAR_SPRITES', false), + + // Generate avatar variations (different colors/styles) + 'variations' => env('AVATAR_VARIATIONS', false), + + // Generate blur placeholder images + 'placeholders' => env('AVATAR_PLACEHOLDERS', true), + + // Generate different aspect ratios + 'aspect_ratios' => env('AVATAR_ASPECT_RATIOS', false), + + // Watermarking support + 'watermark' => [ + 'enabled' => env('AVATAR_WATERMARK', false), + 'text' => env('AVATAR_WATERMARK_TEXT', ''), + 'opacity' => env('AVATAR_WATERMARK_OPACITY', 0.3), + 'position' => env('AVATAR_WATERMARK_POSITION', 'bottom-right'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | API Settings + |-------------------------------------------------------------------------- + | Configuration for avatar API endpoints + | + */ + 'api' => [ + // Enable avatar API endpoints + 'enabled' => env('AVATAR_API_ENABLED', true), + + // Rate limiting (requests per minute) + 'rate_limit' => env('AVATAR_API_RATE_LIMIT', 60), + + // Enable CORS for API endpoints + 'cors' => env('AVATAR_API_CORS', true), + + // API authentication + 'auth' => env('AVATAR_API_AUTH', false), + + // Response headers + 'headers' => [ + 'Cache-Control' => 'public, max-age=31536000', // 1 year + 'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000).' GMT', + ], + ], +]; diff --git a/config/laravolt/avatar.php b/config/laravolt/avatar.php new file mode 100644 index 0000000..a36b4b4 --- /dev/null +++ b/config/laravolt/avatar.php @@ -0,0 +1,174 @@ + env('IMAGE_DRIVER', 'gd'), + + /* + |-------------------------------------------------------------------------- + | Cache Configuration + |-------------------------------------------------------------------------- + | Control caching behavior for avatars + | + */ + 'cache' => [ + // Set to true to enable caching, false to disable + 'enabled' => env('AVATAR_CACHE_ENABLED', true), + + // Cache prefix to avoid conflicts with other cached items + 'key_prefix' => 'avatar_', + + // Cache duration in seconds + // Set to null to cache forever, 0 to disable cache + // Default: 86400 (24 hours) + 'duration' => env('AVATAR_CACHE_DURATION', 86400), + ], + + // Initial generator class + 'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class, + + // Whether all characters supplied must be replaced with their closest ASCII counterparts + 'ascii' => false, + + // Image shape: circle or square + 'shape' => 'circle', + + // Image width, in pixel + 'width' => 100, + + // Image height, in pixel + 'height' => 100, + + // Responsive SVG, height and width attributes are not added when true + 'responsive' => false, + + // Number of characters used as initials. If name consists of single word, the first N character will be used + 'chars' => 2, + + // font size + 'fontSize' => 48, + + // convert initial letter in uppercase + 'uppercase' => false, + + // Right to Left (RTL) + 'rtl' => false, + + // Fonts used to render text. + // If contains more than one fonts, randomly selected based on name supplied + 'fonts' => [__DIR__.'/../fonts/OpenSans-Bold.ttf', __DIR__.'/../fonts/rockwell.ttf'], + + // List of foreground colors to be used, randomly selected based on name supplied + 'foregrounds' => [ + '#FFFFFF', + ], + + // List of background colors to be used, randomly selected based on name supplied + 'backgrounds' => [ + '#f44336', + '#E91E63', + '#9C27B0', + '#673AB7', + '#3F51B5', + '#2196F3', + '#03A9F4', + '#00BCD4', + '#009688', + '#4CAF50', + '#8BC34A', + '#CDDC39', + '#FFC107', + '#FF9800', + '#FF5722', + ], + + 'border' => [ + 'size' => 1, + + // border color, available value are: + // 'foreground' (same as foreground color) + // 'background' (same as background color) + // or any valid hex ('#aabbcc') + 'color' => 'background', + + // border radius, currently only work for SVG + 'radius' => 0, + ], + + // List of theme name to be used when rendering avatar + // Possible values are: + // 1. Theme name as string: 'colorful' + // 2. Or array of string name: ['grayscale-light', 'grayscale-dark'] + // 3. Or wildcard "*" to use all defined themes + 'theme' => ['colorful'], + + // Predefined themes + // Available theme attributes are: + // shape, chars, backgrounds, foregrounds, fonts, fontSize, width, height, ascii, uppercase, and border. + 'themes' => [ + 'grayscale-light' => [ + 'backgrounds' => ['#edf2f7', '#e2e8f0', '#cbd5e0'], + 'foregrounds' => ['#a0aec0'], + ], + 'grayscale-dark' => [ + 'backgrounds' => ['#2d3748', '#4a5568', '#718096'], + 'foregrounds' => ['#e2e8f0'], + ], + 'colorful' => [ + 'backgrounds' => [ + '#f44336', + '#E91E63', + '#9C27B0', + '#673AB7', + '#3F51B5', + '#2196F3', + '#03A9F4', + '#00BCD4', + '#009688', + '#4CAF50', + '#8BC34A', + '#CDDC39', + '#FFC107', + '#FF9800', + '#FF5722', + ], + 'foregrounds' => ['#FFFFFF'], + ], + 'pastel' => [ + 'backgrounds' => [ + '#ef9a9a', + '#F48FB1', + '#CE93D8', + '#B39DDB', + '#9FA8DA', + '#90CAF9', + '#81D4FA', + '#80DEEA', + '#80CBC4', + '#A5D6A7', + '#E6EE9C', + '#FFAB91', + '#FFCCBC', + '#D7CCC8', + ], + 'foregrounds' => [ + '#FFF', + ], + ], + ], +]; diff --git a/config/mail.php b/config/mail.php index 385fd44..ee5779e 100644 --- a/config/mail.php +++ b/config/mail.php @@ -50,6 +50,21 @@ return [ 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), ], + 'support' => [ + 'transport' => env('MAIL_SUPPORT_MAILER', 'smtp'), + 'host' => env('MAIL_SUPPORT_HOST', '127.0.0.1'), + 'port' => env('MAIL_SUPPORT_PORT', 2525), + 'username' => env('MAIL_SUPPORT_USERNAME'), + 'password' => env('MAIL_SUPPORT_PASSWORD'), + 'encryption' => env('MAIL_SUPPORT_ENCRYPTION', 'tls'), + 'from' => [ + 'address' => env('MAIL_SUPPORT_FROM_ADDRESS', 'support@lab.dyzulk.com'), + 'name' => env('MAIL_SUPPORT_FROM_NAME', 'DyDev Support'), + ], + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + 'ses' => [ 'transport' => 'ses', ], diff --git a/config/reverb.php b/config/reverb.php new file mode 100644 index 0000000..d7fc88e --- /dev/null +++ b/config/reverb.php @@ -0,0 +1,95 @@ + env('REVERB_SERVER', 'reverb'), + + /* + |-------------------------------------------------------------------------- + | Reverb Servers + |-------------------------------------------------------------------------- + | + | Here you may define details for each of the supported Reverb servers. + | Each server has its own configuration options that are defined in + | the array below. You should ensure all the options are present. + | + */ + + 'servers' => [ + + 'reverb' => [ + 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), + 'port' => env('REVERB_SERVER_PORT', 8080), + 'path' => env('REVERB_SERVER_PATH', ''), + 'hostname' => env('REVERB_HOST'), + 'options' => [ + 'tls' => [], + ], + 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), + 'scaling' => [ + 'enabled' => env('REVERB_SCALING_ENABLED', false), + 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), + 'server' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', '6379'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', '0'), + 'timeout' => env('REDIS_TIMEOUT', 60), + ], + ], + 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), + 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Reverb Applications + |-------------------------------------------------------------------------- + | + | Here you may define how Reverb applications are managed. If you choose + | to use the "config" provider, you may define an array of apps which + | your server will support, including their connection credentials. + | + */ + + 'apps' => [ + + 'provider' => 'config', + + 'apps' => [ + [ + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'allowed_origins' => ['*'], + 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), + 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30), + 'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'), + 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + ], + ], + + ], + +]; diff --git a/database/migrations/2025_12_22_100801_create_legal_pages_table.php b/database/migrations/2025_12_22_100801_create_legal_pages_table.php new file mode 100644 index 0000000..2ef0027 --- /dev/null +++ b/database/migrations/2025_12_22_100801_create_legal_pages_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('title'); + $table->string('slug')->unique(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + + Schema::create('legal_page_revisions', function (Blueprint $table) { + $table->id(); + $table->foreignId('legal_page_id')->constrained()->onDelete('cascade'); + $table->longText('content'); + $table->string('version')->default('1.0'); + $table->text('change_log')->nullable(); + $table->boolean('is_active')->default(true); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('legal_page_revisions'); + Schema::dropIfExists('legal_pages'); + } +}; diff --git a/database/migrations/2025_12_22_103941_create_contact_submissions_table.php b/database/migrations/2025_12_22_103941_create_contact_submissions_table.php new file mode 100644 index 0000000..f034a94 --- /dev/null +++ b/database/migrations/2025_12_22_103941_create_contact_submissions_table.php @@ -0,0 +1,33 @@ +ulid('id')->primary(); + $table->string('name'); + $table->string('email'); + $table->string('category'); + $table->string('subject'); + $table->text('message'); + $table->boolean('is_read')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contact_submissions'); + } +}; diff --git a/database/migrations/2025_12_22_124707_create_tickets_tables.php b/database/migrations/2025_12_22_124707_create_tickets_tables.php new file mode 100644 index 0000000..621e416 --- /dev/null +++ b/database/migrations/2025_12_22_124707_create_tickets_tables.php @@ -0,0 +1,47 @@ +uuid('id')->primary(); + $table->string('user_id', 32)->index(); + $table->string('ticket_number')->unique(); + $table->string('subject'); + $table->string('category'); // Technical, Billing, etc. + $table->enum('priority', ['low', 'medium', 'high'])->default('medium'); + $table->enum('status', ['open', 'answered', 'closed'])->default('open'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::create('ticket_replies', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('ticket_id')->constrained()->onDelete('cascade'); + $table->string('user_id', 32)->index(); + $table->text('message'); + $table->string('attachment_path')->nullable(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ticket_replies'); + Schema::dropIfExists('tickets'); + } +}; diff --git a/database/migrations/2025_12_22_124710_create_notifications_table.php b/database/migrations/2025_12_22_124710_create_notifications_table.php new file mode 100644 index 0000000..d738032 --- /dev/null +++ b/database/migrations/2025_12_22_124710_create_notifications_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/database/migrations/2025_12_22_155233_change_notifiable_id_to_string_in_notifications_table.php b/database/migrations/2025_12_22_155233_change_notifiable_id_to_string_in_notifications_table.php new file mode 100644 index 0000000..038f027 --- /dev/null +++ b/database/migrations/2025_12_22_155233_change_notifiable_id_to_string_in_notifications_table.php @@ -0,0 +1,28 @@ +string('notifiable_id')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('notifications', function (Blueprint $table) { + $table->unsignedBigInteger('notifiable_id')->change(); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index ae106b0..6a8d224 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -23,7 +23,14 @@ class DatabaseSeeder extends Seeder ['label' => 'Customer'] ); - // Create Admin User + // Helper to generate avatar + $generateAvatar = function ($name, $email) { + $filename = 'avatars/' . \Illuminate\Support\Str::slug($email) . '_' . time() . '.png'; + $avatar = \Laravolt\Avatar\Facade::create($name)->getImageObject()->encode(new \Intervention\Image\Encoders\PngEncoder()); + \Illuminate\Support\Facades\Storage::disk('public')->put($filename, $avatar); + return $filename; + }; + // Create Admin User User::firstOrCreate( ['email' => 'admin@dyzulk.com'], @@ -33,18 +40,20 @@ class DatabaseSeeder extends Seeder 'password' => \Illuminate\Support\Facades\Hash::make('password'), 'role_id' => $adminRole->id, 'email_verified_at' => now(), + 'avatar' => $generateAvatar('Admin User', 'admin@dyzulk.com'), ] ); - // Create Regular User // Create Regular User User::firstOrCreate( - ['email' => 'test@example.com'], + ['email' => 'user@dyzulk.com'], [ - 'first_name' => 'Test', - 'last_name' => 'User', + 'first_name' => 'User', + 'last_name' => 'Customer', 'password' => \Illuminate\Support\Facades\Hash::make('password'), 'role_id' => $customerRole->id, + 'email_verified_at' => now(), + 'avatar' => $generateAvatar('User Customer', 'user@dyzulk.com'), ] ); @@ -58,5 +67,8 @@ class DatabaseSeeder extends Seeder // 'role_id' => $customerRole->id, // ] // ); + $this->call([ + LegalPageSeeder::class, + ]); } } diff --git a/database/seeders/LegalPageSeeder.php b/database/seeders/LegalPageSeeder.php new file mode 100644 index 0000000..5436dba --- /dev/null +++ b/database/seeders/LegalPageSeeder.php @@ -0,0 +1,42 @@ + 'terms-and-conditions'], + ['title' => 'Terms and Conditions'] + ); + + LegalPageRevision::create([ + 'legal_page_id' => $terms->id, + 'version' => '1.0.0', + 'change_log' => 'Initial revision', + 'is_active' => true, + 'content' => "# Terms and Conditions\n\nWelcome to DyDev TrustLab. These terms outline the rules and regulations for the use of our services.\n\n## 1. Acceptable Use\nBy accessing this website, we assume you accept these terms and conditions. Do not continue to use TrustLab if you do not agree to all of the terms and conditions stated on this page.\n\n## 2. Intellectual Property\nUnless otherwise stated, TrustLab and/or its licensors own the intellectual property rights for all material on TrustLab.\n\n## Contact Us\nIf you have any questions about these Terms, please contact us at **info@dydev.com** or via our [Contact Form](/contact)." + ]); + + // 2. Privacy Policy + $privacy = LegalPage::firstOrCreate( + ['slug' => 'privacy-policy'], + ['title' => 'Privacy Policy'] + ); + + LegalPageRevision::create([ + 'legal_page_id' => $privacy->id, + 'version' => '1.0.0', + 'change_log' => 'Initial revision', + 'is_active' => true, + 'content' => "# Privacy Policy\n\nYour privacy is important to us. It is TrustLab's policy to respect your privacy regarding any information we may collect from you across our website.\n\n## 1. Information We Collect\nWe only ask for personal information when we truly need it to provide a service to you.\n\n## 2. Data Security\nWe protect your data within commercially acceptable means to prevent loss and theft.\n\n## Contact Us\nIf you have any questions about how we handle user data and personal information, please contact us at **privacy@dydev.com** or via our [Contact Form](/contact)." + ]); + } +} diff --git a/deploy.sh.example b/deploy.sh.example new file mode 100644 index 0000000..db9237b --- /dev/null +++ b/deploy.sh.example @@ -0,0 +1,84 @@ +#!/bin/bash +# ========================================================== +# Script Deployment Otomatis (GitHub -> aaPanel) +# Proyek: YOUR_PROJECT_NAME +# ========================================== + +# --- 1. KONFIGURASI PATH & NOTIFIKASI --- +PROJECT_PATH="/www/wwwroot/your-domain.com" +PHP_BIN="/www/server/php/83/bin/php" +NODE_BIN="/www/server/nodejs/v24.12.0/bin/node" + +# Fix Enviroment for Composer & Git +export HOME=/root +export COMPOSER_HOME=/root/.composer +git config --global --add safe.directory $PROJECT_PATH + +# Telegram Config +BOT_TOKEN="YOUR_BOT_TOKEN" +CHAT_ID="YOUR_CHAT_ID" + +function send_telegram() { + local message=$1 + curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \ + -d "chat_id=$CHAT_ID" \ + -d "text=$message" \ + -d "parse_mode=Markdown" > /dev/null +} + +echo "==== STARTING DEPLOYMENT: $(date) ====" +send_telegram "🚀 *Deployment Started* for your-domain.com on $(date)" + +cd $PROJECT_PATH || { + send_telegram "❌ *Deployment Failed*: Project path not found!"; + exit 1; +} + +# --- 2. UPDATE KODE DARI GITHUB --- +echo "[1/6] Fetching latest code from GitHub..." +git fetch --all +git reset --hard origin/main + +# --- 3. MANAJEMEN DEPENDENSI (PHP) --- +echo "[2/6] Updating PHP dependencies (Composer)..." +# Mencari lokasi composer secara dinamis +if [ -f "/usr/local/bin/composer" ]; then + COMPOSER_PATH="/usr/local/bin/composer" +elif [ -f "/usr/bin/composer" ]; then + COMPOSER_PATH="/usr/bin/composer" +else + COMPOSER_PATH=$(which composer) +fi + +$PHP_BIN $COMPOSER_PATH install --no-dev --optimize-autoloader --no-interaction || $PHP_BIN $COMPOSER_PATH update --no-dev --optimize-autoloader --no-interaction + +# --- 4. DATABASE MIGRATION --- +echo "[3/6] Running database migrations..." +# Cek apakah vendor ada sebelum jalankan artisan +if [ ! -d "vendor" ]; then + echo "ERROR: Folder vendor tidak ditemukan. Composer install gagal." + send_telegram "❌ *Deployment Failed*: Vendor folder missing (Composer error)!" + exit 1 +fi +$PHP_BIN artisan migrate --force + + +# --- 5. BUILD FRONTEND ASSETS --- +echo "[4/6] Installing & Building assets (Vite)..." +export PATH=$PATH:$(dirname $NODE_BIN) +npm install +npm run build + +# --- 6. OPTIMASI LARAVEL --- +echo "[5/6] Running optimizations..." +$PHP_BIN artisan optimize +$PHP_BIN artisan view:cache +$PHP_BIN artisan storage:link --force + +# --- 7. PERMISSION & CLEANUP --- +echo "[6/6] Fixing permissions..." +chown -R www:www $PROJECT_PATH +chmod -R 775 $PROJECT_PATH/storage $PROJECT_PATH/bootstrap/cache + +echo "==== DEPLOYMENT COMPLETED SUCCESSFULLY ====" +send_telegram "✅ *Deployment Success*: your-domain.com is now updated!" diff --git a/package-lock.json b/package-lock.json index 88e5786..e0c970f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,12 @@ { - "name": "app", + "name": "app-ca-management", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "app-ca-management", + "version": "1.0.0", "dependencies": { "@floating-ui/dom": "^1.7.4", "@fullcalendar/core": "^6.1.19", @@ -20,10 +23,13 @@ "swiper": "^12.0.3" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.12", "axios": "^1.11.0", "concurrently": "^9.0.1", + "laravel-echo": "^2.2.6", "laravel-vite-plugin": "^2.0.0", + "pusher-js": "^8.4.0", "tailwindcss": "^4.1.12", "vite": "^7.0.4" } @@ -896,6 +902,14 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@svgdotjs/svg.draggable.js": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", @@ -1214,6 +1228,19 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", @@ -1452,6 +1479,38 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1494,6 +1553,32 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -1843,6 +1928,20 @@ "integrity": "sha512-8VmL3Uuen08Es9xb2N6Wdc32TrQDGPXYCIdTB126jAhTJsYd/4r4Mc63VQA3qHxG0p4zeCI8sFO5XRsdjljMJg==", "license": "MIT" }, + "node_modules/laravel-echo": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.2.6.tgz", + "integrity": "sha512-KuCldOrE8qbm0CVDBgc6FiX3VuReDu1C1xaS891KqwEUg9NT/Op03iiZqTWeVd0/WJ4H95q2pe9QEDJlwb/FPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "pusher-js": "*", + "socket.io-client": "*" + } + }, "node_modules/laravel-vite-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", @@ -2184,6 +2283,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2252,6 +2359,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/preact": { "version": "10.12.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", @@ -2278,6 +2399,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pusher-js": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz", + "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2351,6 +2482,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2497,6 +2660,20 @@ "dev": true, "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", @@ -2614,6 +2791,39 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4d77df7..3782875 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,13 @@ "dev": "vite" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.12", "axios": "^1.11.0", "concurrently": "^9.0.1", + "laravel-echo": "^2.2.6", "laravel-vite-plugin": "^2.0.0", + "pusher-js": "^8.4.0", "tailwindcss": "^4.1.12", "vite": "^7.0.4" }, diff --git a/public/chat-id.html b/public/chat-id.html new file mode 100644 index 0000000..55b9e5c --- /dev/null +++ b/public/chat-id.html @@ -0,0 +1,262 @@ + + + + + + Telegram Chat ID Finder | DyDev Admin + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+ + +
+
+
+
+ + + +
+

+ Telegram Chat ID Finder +

+
+ +
+ +

+ Enter your Bot Token from @BotFather to see recent activity and find your CHAT_ID. +

+ +
+
+ +
+ + +
+
+
+ + +
+ + + + +
+ + +
+

Detected Chat IDs:

+
+ + + + + + + + + + + +
NameID
+
+
+ +
+

Instructions:

+
    +
  1. Send a random message (e.g., "Hello") to your Telegram Bot.
  2. +
  3. Paste your Bot Token above and click Get Updates.
  4. +
  5. Your CHAT_ID will appear in the table. Copy it and use it in your deploy.sh or .env file.
  6. +
+
+
+ +

+ © 2025 - DyDev TrustLab +

+
+ + diff --git a/public/images/error/403-dark.svg b/public/images/error/403-dark.svg new file mode 100644 index 0000000..a40b741 --- /dev/null +++ b/public/images/error/403-dark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/error/403.svg b/public/images/error/403.svg new file mode 100644 index 0000000..f849ace --- /dev/null +++ b/public/images/error/403.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/auth-logo.png b/public/images/logo/auth-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..322936b1b453cb6b0e3cf362beaaf68fbab06c2b GIT binary patch literal 5108 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
  • M-s z3CL;=x89CVWv4UKaS)t z>(UJE_%)J_=tMrXSUDYg2*uAWZM2a0C0!cD3b{!{kW%lRTT#j%b=Uv4adA1!VO0d_wsnb1MDu|KXZtaBcGl5rSW7q||Ip)JNc^}iIkI%jYFNcK%CP=>@$Hz8r1_egGD zdIVuVuSJsKuO~i^8$=Qe<{*TUKY`2u;tiORx!qQ>m~a7-VdO+9`h*V?<|5faP7Ful zmbDJbvMloME+|o`$ih|S_>e5&6LV~&ZPn+i&OU{YtH`c6qu{0dWZarCa!78$ZDrQw z=`aTUo~iS2-zC9^es38kN=RMuLN>CMNed)ST8w1>m|R#lIi~GHgLvQj*Ca1MaxQHc zS?Kp$vXFbIW8{yKctCQ(^p2I`(NhW&LdOW*fLIqc_KTYWC1iIE7C32TS8+!MD!$A1 zx+8lz)kmFYVzwZO@@RvkZh%M@707a^RLF7L;H#$OoxuH`rfPTpl)Yo&RVll5gil00 zlb8dBKj`MGO2r&$Yim}_xOuQc-Ib!aurRMGI|iI=3`PGh6*iN)tdlf;!teq3ENht? z;bVu9n9;bf@$9a>txcwjvSU(qWi9o5wbcvLYL4+#ds=WBry@dZZN{5`l{k#ym_J6^ zR+im$VPW;wGTJt%yzbUko#=yp&E^rj?=qLhX$l{}n=|hyQYeYJC1KUPSj$K|tz2S` zjka|tdyEBFDAe+)lkyU6l3bG*VFaBaJj_7XJ^$ W0fr32elt1%0000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/logo-dark.png b/public/images/logo/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..95b945d4a227b56095c941b188597726ec774b22 GIT binary patch literal 3149 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/logo-icon.png b/public/images/logo/logo-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8dae70b8d359962fcf74c456a310a77ec4fd0062 GIT binary patch literal 1541 zcmV+g2KxDlP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11(Hcb zK~z|Ut(RGBRCN@_e`n@)0ouZ}P+Fk0QwjxJK$cPg5fFkAu@<2`2oaG*6P`pPJ{VII zU(}fJ0wN_!BG6D21|$svB9S0V%T|Rhv{8XlN=vbOr_20(n3+5G&P<0woQIkFpL4$R zo#mTz?}$7xBPyK8LQu=Z6WI%P2QU-6Kg`7XcLTz5RC5kmT~w6UW)VTci7XVSfKSzKUz@}a zi(;cDs2TlL{e&WWa;%W00k;2NWCI>mtR`Z}-Hxv0C}f;~AF5IQ@1 zsJq`10xO2Z%t-^Hg=(2Gz{`u$dFQQczS{m9xl1!xJU@+|o<2VP;uLlDEy1e}r6@41zxy6aIyVo@9{`P^^-zHn}>SRVG#87;)f~AYo2@A8**51vY z!cyH=DOmqz7Skq=qOHA~&$pdn^M={9wmJFi%hQG=`WL8m;*{Kcqfo@-UU92|sTqmP znx4e*U#hSt+lU$x&e$=-xKMH%1t_H`xqO$*XA(#p8BOM-k$m&rC0<%Ifwa^(E|%Ou zDepLZRU0*7O`n{bAMD2A>T_}V`aR~%N@m)WQ5-2QCw=^Il12|>=+H>6T&qO@m&?V~ z>vd$$NFshj6k|pYrSOOAy!J{4v9W`>T2|YyKoP+@amw=i{)FOm_HzA39rNd=lAbo4 zV?S3gDLtNX_Ba$!UfH1gT`8+&&a7k-6Jm%6*En{(oE15lXqttaY7H=n7x{?PMW$r-+vjImWM#&!*=f1x3b{*aYRQ&P;%uisVT8c z%}nG@Z8P`kS^;?UZzuQaTFIW7M5;ZGU(eRi;LmTK#=lrGH zR8>3BtQOkZog6K`2?)KtE|YK5qYk>ed#S8$q^;G-yxDd{goOBL6OWfyGk@tj_`2Xb zH8pkUWSyP8=8cD7PqDG}y#+jK?qdIet8Cq}fbeiDhmQV<?t-r+OiPMD(omY&!)GtiM0*pN5>x=DZXirXTa)<$XvD0!@p(zn+ySavYUL! z;4rprKgZG~>7=H_vA^gV$4^y;#A;7nwddd^j~WtA(V;TzDK?(VN@m}ItDHDp6;w~g zoum0z9gH|w<^Y2tG#1TI<3@Qs^$%JpxpJ44xtZ+RbCKWA{}VtS&}{+9Tl>AsBsJaV zF6AZDR^+9{wcAWgxxV6c54Mk2p^e5hR_L9iZNqeX;LFW7$z`rkP8fJRLW z6f1#Pff5T!6@=P?A*(iwCN$PStYZZQy9Jc2T36_>pmK}>gU;2TShtTy+}$L^hZ z%PmTw)F5p&SdIif&jn`g-)^rpWYzj;yjbPvj_z6mxc?n{U#mCne;pSa5Sb~+2xBG2 rs-N`H+k%N_PYNE!Z)HZ<0SRs00000NkvXXu0mjf2lm^u literal 0 HcmV?d00001 diff --git a/public/images/logo/logo-icon.svg b/public/images/logo/logo-icon.svg index 11d52ca..6e0d87e 100644 --- a/public/images/logo/logo-icon.svg +++ b/public/images/logo/logo-icon.svg @@ -1,44 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/logo.png b/public/images/logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..54352752bbe81bcdfc9bd81b043a4f9797ec18cd GIT binary patch literal 3316 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/og-share.png b/public/images/og-share.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f08b342113bd4c9f99ad527276898c834b432c GIT binary patch literal 63101 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

    $$Mmr>L_<5zA*tf|if-3mFytlN{u+e6<-I%~I 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

    ^M%L3C#dEJM9p4$dU&;6unxOJ)h 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 R&m{N{-_v(dgL{PN{{YTF{)PYm literal 0 HcmV?d00001 diff --git a/public/key-gen.html b/public/key-gen.html index eaa350e..f810d5a 100644 --- a/public/key-gen.html +++ b/public/key-gen.html @@ -3,7 +3,22 @@ - Laravel Key Generator | DyDev Admin + Laravel APP_KEY Generator | DyDev Admin + + + + + + + + + + + + + + + + +
    + +
    +
    +
    + + + + + + +
    + +
    +
    +
    +
    + +
    +
    + 🚀 Unified Certificate Management +
    +

    + Secure Your Assets with
    + + Trusted Certificate Authority + +

    +

    + Issue, manage, and track SSL/TLS certificates and API keys through a powerful, developer-friendly management system. +

    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +

    Powerful Features for Modern Apps

    +

    Everything you need to manage your security layer efficiently.

    +
    + +
    + +
    +
    + + + +
    +

    Custom CA Issuance

    +

    + Issue professional Root and Intermediate CA certificates with a single click. Fully compliant with standard encryption protocols. +

    +
    + + +
    +
    + + + +
    +

    API Management

    +

    + Secure your external services with granular API keys. Track usage patterns and revoke access instantly when needed. +

    +
    + + +
    +
    + + + +
    +

    Real-time Tracking

    +

    + Monitor issuance trends and expiring certificates through intuitive analytical dashboards and automated alerts. +

    +
    +
    +
    +
    + + +
    +
    +
    +
    +

    Ready to secure your application?

    +

    Join hundreds of developers managing their security infrastructure with {{ config('app.name') }}.

    + +
    + +
    + +
    +
    +
    +
    + + + + + + +
    + +@endsection diff --git a/resources/views/layouts/app-header.blade.php b/resources/views/layouts/app-header.blade.php index d5c6f5a..e07706e 100644 --- a/resources/views/layouts/app-header.blade.php +++ b/resources/views/layouts/app-header.blade.php @@ -67,26 +67,24 @@ diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 7e38075..a0c0819 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -113,6 +113,9 @@ {{-- Flash Message Component --}} + {{-- Real-time Toast Component --}} + + {{-- preloader --}} {{-- preloader end --}} @@ -137,6 +140,7 @@ + @stack('scripts') diff --git a/resources/views/layouts/fullscreen-layout.blade.php b/resources/views/layouts/fullscreen-layout.blade.php index 0bb341a..414a8c7 100644 --- a/resources/views/layouts/fullscreen-layout.blade.php +++ b/resources/views/layouts/fullscreen-layout.blade.php @@ -6,7 +6,28 @@ - {{ $title ?? 'Dashboard' }} | {{ config('app.name') }} + {{ $title ?? 'Unified Management' }} | {{ config('app.name', 'DyDev Admin') }} + + + + + + + + + + + + + + + + + + + + + @yield('meta') @vite(['resources/css/app.css', 'resources/js/app.js']) diff --git a/resources/views/pages/admin/contacts/index.blade.php b/resources/views/pages/admin/contacts/index.blade.php new file mode 100644 index 0000000..62be7cf --- /dev/null +++ b/resources/views/pages/admin/contacts/index.blade.php @@ -0,0 +1,116 @@ +@extends('layouts.app') + +@section('content') +
    + +
    +

    + Inbox / Messages +

    + + +
    + + +
    +
    + + + + + + + + + + + @forelse ($submissions as $msg) + + + + + + + @empty + + + + @endforelse + +
    +

    Sender

    +
    +

    Category & Subject

    +
    +

    Date

    +
    +

    Actions

    +
    +
    +
    +
    + {{ substr($msg->name, 0, 1) }} +
    + @if(!$msg->is_read) + + @endif +
    +
    + + {{ $msg->name }} + + + {{ $msg->email }} + +
    +
    +
    + + {{ $msg->category }} + +

    + {{ $msg->subject }} +

    +
    +

    + {{ $msg->created_at->format('M d, Y') }} + {{ $msg->created_at->format('H:i') }} +

    +
    +
    + + + +
    + @csrf + @method('DELETE') + +
    +
    +
    +
    + +

    No messages in your inbox yet.

    +
    +
    +
    + @if($submissions->hasPages()) +
    + {{ $submissions->links() }} +
    + @endif +
    +
    +@endsection diff --git a/resources/views/pages/admin/contacts/show.blade.php b/resources/views/pages/admin/contacts/show.blade.php new file mode 100644 index 0000000..c88e00f --- /dev/null +++ b/resources/views/pages/admin/contacts/show.blade.php @@ -0,0 +1,115 @@ +@extends('layouts.app') + +@section('content') +
    + +
    +

    + Message Details +

    + + +
    + +
    + +
    +
    +
    +
    + {{ substr($contactSubmission->name, 0, 1) }} +
    +
    +

    + {{ $contactSubmission->name }} +

    +

    + {{ $contactSubmission->email }} +

    +
    +
    +
    + + {{ $contactSubmission->category }} + +

    + Received on {{ $contactSubmission->created_at->format('M d, Y \a\t H:i') }} +

    +
    +
    +
    + + +
    +

    Subject

    +

    + {{ $contactSubmission->subject }} +

    + +

    Message

    +
    + {!! nl2br(e($contactSubmission->message)) !!} +
    + + +
    +

    + + Quick Reply via Portal +

    + +
    + @csrf +
    + + +
    +
    + + +
    +
    +

    + * Sending from: support@lab.dyzulk.com +

    + +
    +
    +
    +
    + + +
    + + +
    + @csrf + @method('DELETE') + +
    +
    +
    +
    +@endsection diff --git a/resources/views/pages/admin/legal/edit.blade.php b/resources/views/pages/admin/legal/edit.blade.php new file mode 100644 index 0000000..d3cde78 --- /dev/null +++ b/resources/views/pages/admin/legal/edit.blade.php @@ -0,0 +1,211 @@ +@extends('layouts.app') + +@section('content') +
    + +
    +

    + Edit: {{ $legalPage->title }} +

    + + +
    + +
    +
    + @csrf + @method('PUT') + +
    + +
    + + +
    + + + +
    + +
    + + + + +
    + + + + + + + + + + + + +
    + +
    +
    + +

    Current active:

    +
    + +
    + + + @error('change_log') +

    {{ $message }}

    + @enderror +
    + +
    + +
    +
    + + +
    +
    + +
    + + +
    +
    + +
    + + @error('content') +

    {{ $message }}

    + @enderror +
    + +
    +
    +
    +
    + +

    + TIP: Markdown is converted to high-quality typography on the public site. +

    +
    +
    +
    +
    +
    +@endsection diff --git a/resources/views/pages/admin/legal/index.blade.php b/resources/views/pages/admin/legal/index.blade.php new file mode 100644 index 0000000..0349973 --- /dev/null +++ b/resources/views/pages/admin/legal/index.blade.php @@ -0,0 +1,116 @@ +@extends('layouts.app') + +@section('content') +
    + +
    +

    + Legal Pages Management +

    + + +
    + + + @if (session('success')) +
    +
    + + + +
    +
    +
    + Successfully +
    +

    + {{ session('success') }} +

    +
    +
    + @endif + + +
    +
    + + + + + + + + + + + + @foreach ($pages as $page) + + + + + + + + @endforeach + +
    +

    + Page Title +

    +
    +

    + Slug +

    +
    +

    + Current Version +

    +
    +

    + Last Updated +

    +
    +

    + Actions +

    +
    + + {{ $page->title }} + + + + /legal/{{ $page->slug }} + + + + v{{ $page->currentRevision->version ?? 'N/A' }} + + +

    + {{ $page->currentRevision ? $page->currentRevision->updated_at->format('M d, Y') : 'N/A' }} +

    +
    + +
    +
    +
    +
    +@endsection diff --git a/resources/views/pages/admin/smtp-tester/index.blade.php b/resources/views/pages/admin/smtp-tester/index.blade.php new file mode 100644 index 0000000..ce20a6d --- /dev/null +++ b/resources/views/pages/admin/smtp-tester/index.blade.php @@ -0,0 +1,168 @@ +@extends('layouts.app') + +@section('content') +
    + +
    +

    + SMTP Tester +

    + + +
    + +
    + +
    +
    +
    +

    + Run Connection Test +

    +
    + +
    + @csrf + +
    + +
    + + + + + + +
    +
    + +
    + + +

    + We will send a raw test email to this address. +

    +
    + +
    + + +
    +
    + + + +
    + + @if(session('success')) +
    +
    +
    + +
    +
    +

    Test Successful

    +

    {{ session('success') }}

    +
    +
    +
    + @endif + + @if(session('error')) +
    +
    +
    + +
    +
    +

    Connection Failed

    +

    {{ session('error') }}

    +

    Please check your .env configuration and ensure your SMTP server is accessible.

    +
    +
    +
    + @endif +
    + + +
    +
    +
    +

    + Current Configuration (Read-Only) +

    +
    +
    +
    + @foreach($configs as $key => $config) +
    +

    + {{ $config['name'] }} +

    +
    +
    +
    Host
    +
    {{ $config['host'] ?? 'N/A' }}
    +
    +
    +
    Port
    +
    {{ $config['port'] ?? 'N/A' }}
    +
    +
    +
    Username
    +
    {{ $config['username'] ?? 'N/A' }}
    +
    +
    +
    Encryption
    +
    {{ $config['encryption'] ?? 'None' }}
    +
    +
    +
    From Address
    +
    {{ $config['from'] ?? 'N/A' }}
    +
    +
    +
    + @endforeach +
    +
    +
    +
    +
    +
    + + +@endsection diff --git a/resources/views/pages/admin/tickets/index.blade.php b/resources/views/pages/admin/tickets/index.blade.php new file mode 100644 index 0000000..9b56fe0 --- /dev/null +++ b/resources/views/pages/admin/tickets/index.blade.php @@ -0,0 +1,110 @@ +@extends('layouts.app') + +@section('content') +
    +
    +

    + Ticket Management +

    + +
    +
    + + + +
    +
    + All + Open + Answered + Closed +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + @forelse($tickets as $ticket) + + + + + + + + + @empty + + + + @endforelse + +
    StatusTicket InfoUserPriorityUpdatedAction
    + @php + $statusClass = match($ticket->status) { + 'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400', + 'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400', + 'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400', + default => 'bg-gray-100 text-gray-600' + }; + @endphp + + {{ ucfirst($ticket->status) }} + + +
    #{{ $ticket->ticket_number }}
    +
    {{ $ticket->subject }}
    +
    {{ $ticket->category }}
    +
    +
    {{ $ticket->user->name }}
    +
    {{ $ticket->user->email }}
    +
    + + {{ ucfirst($ticket->priority) }} + + + {{ $ticket->updated_at->diffForHumans() }} + + Manage +
    +

    No tickets found matching your criteria.

    +
    +
    +
    + {{ $tickets->links() }} +
    +
    +@endsection diff --git a/resources/views/pages/admin/tickets/show.blade.php b/resources/views/pages/admin/tickets/show.blade.php new file mode 100644 index 0000000..fea2d7d --- /dev/null +++ b/resources/views/pages/admin/tickets/show.blade.php @@ -0,0 +1,317 @@ +@extends('layouts.app') + +@section('content') +
    +
    +

    + Manage Ticket #{{ $ticket->ticket_number }} +

    + +
    +
    + +
    + +
    + + +
    +
    +

    {{ $ticket->subject }}

    + + {{ $ticket->category }} + +
    +
    +
    + +
    + @foreach($ticket->replies as $reply) + @php + // In Admin view: User messages on LEFT, Admin messages (ours) on RIGHT + // But since multiple admins might exist, we check if reply user is Admin Role or specific user + // Simpler: If reply->user_id == Auth::id() -> Right (It's ME) + // If reply->user->isAdmin() -> Right (It's a Colleague) + // Else (Customer) -> Left + $isStaff = $reply->user->isAdmin(); + @endphp +
    + @if(!$isStaff) +
    +
    + {{ substr($reply->user->name, 0, 1) }} +
    +
    + @endif +
    +
    + {{ $reply->user->name }} {{ $isStaff ? '(Staff)' : '' }} • {{ $reply->created_at->format('M d, Y H:i A') }} +
    +
    +

    {{ $reply->message }}

    + @if($reply->attachment_path) + + @endif +
    +
    + @if($isStaff) +
    +
    + +
    +
    + @endif +
    + @endforeach +
    + +
    +

    Post Staff Reply

    +
    + @csrf +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    + + + +
    + @csrf + @method('PATCH') +
    +
    + + +
    +
    + + +
    + +
    +
    +
    + + + + +
    +
    + {{ substr($ticket->user->name, 0, 1) }} +
    +
    +
    {{ $ticket->user->name }}
    +
    {{ $ticket->user->email }}
    +
    +
    +
    +

    Recent Tickets

    + @if($ticket->user->tickets->count() > 1) + + @else +

    No other tickets.

    + @endif +
    +
    +
    +
    +@endsection + +@push('scripts') + + + +@endpush diff --git a/resources/views/pages/auth/forgot-password.blade.php b/resources/views/pages/auth/forgot-password.blade.php index 42a95ce..1087c63 100644 --- a/resources/views/pages/auth/forgot-password.blade.php +++ b/resources/views/pages/auth/forgot-password.blade.php @@ -1,4 +1,7 @@ -@extends('layouts.fullscreen-layout') +@extends('layouts.fullscreen-layout', ['title' => 'Reset Your Password']) + +@section('meta_description', 'Recover access to your TrustLab account. Enter your email to receive a password reset link.') +@section('robots', 'noindex, nofollow') @section('content')
    @@ -13,7 +16,7 @@ - Back to Home + Back to home
    @@ -76,7 +79,7 @@ Logo

    - Free and Open-Source Tailwind CSS Admin Dashboard Template + Professional Certificate Authority & API Management System

    diff --git a/resources/views/pages/auth/reset-password.blade.php b/resources/views/pages/auth/reset-password.blade.php index a7ee39e..7c46a32 100644 --- a/resources/views/pages/auth/reset-password.blade.php +++ b/resources/views/pages/auth/reset-password.blade.php @@ -1,4 +1,6 @@ -@extends('layouts.fullscreen-layout') +@extends('layouts.fullscreen-layout', ['title' => 'Set New Password']) + +@section('robots', 'noindex, nofollow') @section('content')
    @@ -13,7 +15,7 @@ - Back to Home + Back to home
    @@ -108,7 +110,7 @@ Logo

    - Free and Open-Source Tailwind CSS Admin Dashboard Template + Professional Certificate Authority & API Management System

    diff --git a/resources/views/pages/auth/setup-password.blade.php b/resources/views/pages/auth/setup-password.blade.php index 12c7e3f..252461d 100644 --- a/resources/views/pages/auth/setup-password.blade.php +++ b/resources/views/pages/auth/setup-password.blade.php @@ -5,6 +5,17 @@
    +
    diff --git a/resources/views/pages/auth/signin.blade.php b/resources/views/pages/auth/signin.blade.php index b4c8432..dc1235f 100644 --- a/resources/views/pages/auth/signin.blade.php +++ b/resources/views/pages/auth/signin.blade.php @@ -1,17 +1,20 @@ -@extends('layouts.fullscreen-layout') +@extends('layouts.fullscreen-layout', ['title' => 'Sign In to Portal']) + +@section('meta_description', 'Access your Certificate Authority dashboard and manage your API keys and certificates securely.') +@section('robots', 'noindex, nofollow') @section('content')
    -
    @@ -136,6 +139,12 @@ Don't have an account? Sign Up

    +

    + By signing in, you agree to our + Terms + and + Privacy Policy. +

    @@ -151,7 +160,7 @@ Logo

    - Free and Open-Source Tailwind CSS Admin Dashboard Template + Professional Certificate Authority & API Management System

    diff --git a/resources/views/pages/auth/signup.blade.php b/resources/views/pages/auth/signup.blade.php index 5e8b3ba..b4a6eab 100644 --- a/resources/views/pages/auth/signup.blade.php +++ b/resources/views/pages/auth/signup.blade.php @@ -1,4 +1,7 @@ -@extends('layouts.fullscreen-layout') +@extends('layouts.fullscreen-layout', ['title' => 'Create Your Account']) + +@section('meta_description', 'Get started with DyDev TrustLab. Create an account to manage your certificates and API keys with a unified dashboard.') +@section('robots', 'noindex, nofollow') @section('content')
    @@ -11,7 +14,7 @@ - Back to dashboard + Back to home
    @@ -157,6 +160,12 @@ Already have an account? Sign In

    +

    + By signing up, you agree to our + Terms + and + Privacy Policy. +

    @@ -170,7 +179,7 @@ Logo

    - Free and Open-Source Tailwind CSS Admin Dashboard Template + Professional Certificate Authority & API Management System

    diff --git a/resources/views/pages/auth/verify-email.blade.php b/resources/views/pages/auth/verify-email.blade.php index a70e219..307763e 100644 --- a/resources/views/pages/auth/verify-email.blade.php +++ b/resources/views/pages/auth/verify-email.blade.php @@ -1,4 +1,6 @@ -@extends('layouts.fullscreen-layout') +@extends('layouts.fullscreen-layout', ['title' => 'Verify Your Email']) + +@section('robots', 'noindex, nofollow') @section('content')
    @@ -17,7 +19,9 @@

    - Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? + Thanks for signing up! Before getting started, could you verify your email address + ({{ auth()->user()->email }}) + by clicking on the link we just emailed to you?

    If you didn't receive the email, we will gladly send you another. @@ -56,7 +60,7 @@ Logo

    - Secure Certificate Management System + Professional Certificate Authority & API Management System

    diff --git a/resources/views/pages/dashboard.blade.php b/resources/views/pages/dashboard.blade.php index f09f946..afeb0ec 100644 --- a/resources/views/pages/dashboard.blade.php +++ b/resources/views/pages/dashboard.blade.php @@ -1,24 +1,125 @@ @extends('layouts.app') @section('content') -
    +@push('scripts') + +@endpush
    @@ -50,12 +151,12 @@
    Total Certificates - {{ $totalCertificates }} + {{ $totalCertificates }} - {{ $activeCertificates }} Active Now + {{ $activeCertificates }} Active Now
    @@ -69,9 +170,9 @@
    Manageable API Keys - {{ $totalApiKeys }} + {{ $totalApiKeys }} - Latest usage: {{ $recentApiActivity->first()->last_used_at?->diffForHumans() ?? 'None' }} + Latest usage: {{ $recentApiActivity->first()?->last_used_at?->diffForHumans() ?? 'None' }}
    @@ -85,7 +186,7 @@
    Expiring Soon - {{ $expiringSoonCount }} + {{ $expiringSoonCount }} Action required within 14 days @@ -101,7 +202,16 @@
    Node Status - Operational + Operational Latency: @@ -119,23 +229,18 @@
    - @foreach($issuanceData as $index => $count) +
    @@ -143,23 +248,22 @@

    Recent API Activity

    - @forelse($recentApiActivity as $activity) + +
    +

    No recent activity detected.

    +
    @@ -167,7 +271,7 @@

    Recently Issued Certificates

    - View All + View All
    @@ -181,25 +285,21 @@ - @forelse($recentCertificates as $cert) + + + +
    No certificates found.
    @@ -207,3 +307,4 @@
    @endsection + diff --git a/resources/views/pages/legal/show.blade.php b/resources/views/pages/legal/show.blade.php new file mode 100644 index 0000000..ae94c54 --- /dev/null +++ b/resources/views/pages/legal/show.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.fullscreen-layout', ['title' => $page->title]) + +@section('content') +
    + +
    +
    + + + +
    +
    + +
    +
    + 📜 Legal Document +
    +

    + {{ $page->title }} +

    +
    + + + Last updated: {{ $revision->created_at->format('M d, Y') }} + + + + Version {{ $revision->version }} + +
    +
    + + +
    + {!! Str::markdown($revision->content) !!} +
    +
    +
    + + +
    +@endsection diff --git a/resources/views/pages/public/contact.blade.php b/resources/views/pages/public/contact.blade.php new file mode 100644 index 0000000..7dd4b14 --- /dev/null +++ b/resources/views/pages/public/contact.blade.php @@ -0,0 +1,90 @@ +@extends('layouts.fullscreen-layout', ['title' => 'Contact Us']) + +@section('content') +
    + +
    +
    + + + +
    +
    +
    +

    + Contact Our Team +

    +

    + Have a question or need legal assistance? We're here to help. +

    +
    + + @if (session('success')) +
    +
    + + + +

    + {{ session('success') }} +

    +
    +
    + @endif + +
    +
    + @csrf +
    +
    + + + @error('name')

    {{ $message }}

    @enderror +
    +
    + + + @error('email')

    {{ $message }}

    @enderror +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    + +
    +
    + + +
    +@endsection diff --git a/resources/views/pages/public/tools/app-key-generator.blade.php b/resources/views/pages/public/tools/app-key-generator.blade.php new file mode 100644 index 0000000..79eff2f --- /dev/null +++ b/resources/views/pages/public/tools/app-key-generator.blade.php @@ -0,0 +1,101 @@ +@extends('layouts.fullscreen-layout', ['title' => 'Laravel APP_KEY Generator']) + +@section('content') +
    + +
    +
    + + + +
    +
    +
    +
    + 🔐 Security Utility +
    +

    + Key Generator +

    +

    + Generate a production-ready 32-byte APP_KEY for your Laravel application securely in your browser. +

    +
    + +
    + +
    +
    + +
    +
    +
    + + + + +
    +
    + +
    + + +
    + +
    +

    + Quick Guide +

    +
      +
    • + 1. +
      Copy the generated key above.
      +
    • +
    • + 2. +
      Open your .env file in your Laravel project root.
      +
    • +
    • + 3. +
      Update the APP_KEY= variable with this key.
      +
    • +
    +
    +
    +
    + +
    +
    + + +
    +@endsection diff --git a/resources/views/pages/public/tools/chat-id-finder.blade.php b/resources/views/pages/public/tools/chat-id-finder.blade.php new file mode 100644 index 0000000..f65e8da --- /dev/null +++ b/resources/views/pages/public/tools/chat-id-finder.blade.php @@ -0,0 +1,185 @@ +@extends('layouts.fullscreen-layout', ['title' => 'Telegram Chat ID Finder']) + +@section('content') +
    + +
    +
    + + + +
    +
    +
    +
    + 🛠️ Developer Utility +
    +

    + Chat ID Finder +

    +

    + Quickly retrieve your Telegram Chat ID using your Bot Token. All processing happens locally in your browser. +

    +
    + +
    + +
    +
    + +
    + + +
    +
    + + +
    + + + + +
    + + +
    +

    + Detected Chats + +

    +
    + + + + + + + + + + + +
    Chat IdentityNumeric ID
    +
    +
    + +
    +

    + + Instructions +

    +
      +
    1. Send a message (e.g., "Hello") to your Bot from the account or group you want the ID from.
    2. +
    3. Paste your Bot Token above and click Fetch Updates.
    4. +
    5. Your CHAT_ID will appear in the table. Copy it for use in your API or integrations.
    6. +
    +
    +
    +
    + +
    +
    + + +
    +@endsection diff --git a/resources/views/pages/support/create.blade.php b/resources/views/pages/support/create.blade.php new file mode 100644 index 0000000..274a909 --- /dev/null +++ b/resources/views/pages/support/create.blade.php @@ -0,0 +1,105 @@ +@extends('layouts.app') + +@section('content') +
    +
    +

    + Open New Ticket +

    + +
    +
    + + +
    + @csrf + +
    + +
    + + + @error('subject')

    {{ $message }}

    @enderror +
    + + +
    + + + @error('category')

    {{ $message }}

    @enderror +
    + + +
    + + + @error('priority')

    {{ $message }}

    @enderror +
    + + +
    + + + @error('message')

    {{ $message }}

    @enderror +
    + + +
    + +
    + +
    + @error('attachment')

    {{ $message }}

    @enderror +
    +
    + +
    + +
    +
    +
    +@endsection diff --git a/resources/views/pages/support/index.blade.php b/resources/views/pages/support/index.blade.php new file mode 100644 index 0000000..46a872b --- /dev/null +++ b/resources/views/pages/support/index.blade.php @@ -0,0 +1,95 @@ +@extends('layouts.app') + +@section('content') +
    +
    +

    + My Support Tickets +

    + +
    + +
    + + +
    + + + + + + + + + + + + + @forelse($tickets as $ticket) + + + + + + + + + @empty + + + + @endforelse + +
    Ticket IDSubjectCategoryStatusLast UpdatedAction
    + #{{ $ticket->ticket_number }} + +
    {{ $ticket->subject }}
    +
    + + {{ $ticket->category }} + + + @php + $statusClass = match($ticket->status) { + 'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400', + 'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400', + 'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400', + default => 'bg-gray-100 text-gray-600' + }; + @endphp + + {{ ucfirst($ticket->status) }} + + + {{ $ticket->updated_at->diffForHumans() }} + + View +
    +
    + +

    No tickets found.

    +
    +
    +
    +
    + {{ $tickets->links() }} +
    +
    +@endsection diff --git a/resources/views/pages/support/show.blade.php b/resources/views/pages/support/show.blade.php new file mode 100644 index 0000000..179c05d --- /dev/null +++ b/resources/views/pages/support/show.blade.php @@ -0,0 +1,299 @@ +@extends('layouts.app') + +@section('content') +
    +
    +

    + Ticket #{{ $ticket->ticket_number }} +

    + +
    +
    + @if($ticket->status !== 'closed') +
    + @csrf + +
    + @endif +
    +
    + +
    + +
    + + +
    +

    {{ $ticket->subject }}

    +
    + + {{ $ticket->category }} + + @php + $statusClass = match($ticket->status) { + 'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400', + 'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400', + 'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400', + default => 'bg-gray-100 text-gray-600' + }; + @endphp + + {{ ucfirst($ticket->status) }} + +
    +
    +
    + +
    + @foreach($ticket->replies as $reply) + @php + $isMe = $reply->user_id === Auth::id(); + @endphp +
    + @if(!$isMe) +
    +
    + {{ substr($reply->user->name, 0, 1) }} +
    +
    + @endif +
    +
    + {{ $reply->user->name }} • {{ $reply->created_at->format('M d, Y H:i A') }} +
    +
    +

    {{ $reply->message }}

    + @if($reply->attachment_path) + + @endif +
    +
    + @if($isMe) +
    +
    + Me +
    +
    + @endif +
    + @endforeach +
    + + @if($ticket->status !== 'closed') +
    +

    Post a Reply

    +
    + @csrf +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + @else +
    + +
    + @endif +
    +
    + + +
    + + +
    +
    +
    Created
    +
    {{ $ticket->created_at->format('M d, Y') }}
    +
    +
    +
    Last Updated
    +
    {{ $ticket->updated_at->diffForHumans() }}
    +
    +
    +
    Priority
    +
    + {{ ucfirst($ticket->priority) }} +
    +
    +
    +
    +
    +
    +@endsection + +@push('scripts') + + + +@endpush diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..050e969 --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,24 @@ +@props([ + 'url', + 'color' => 'primary', + 'align' => 'center', +]) + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..ceb1981 --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,8 @@ +@props(['url']) + + + + + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..b0b28c2 --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,58 @@ + + + +{{ config('app.name') }} + + + + + +{!! $head ?? '' !!} + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..ddc6bbe --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,28 @@ + +{{-- Header --}} + + +{{ config('app.name') }} + + + +{{-- Body --}} +{!! $slot !!} + +{{-- Subcopy --}} +@isset($subcopy) + + +{!! $subcopy !!} + + +@endisset + +{{-- Footer --}} + + +© {{ date('Y') }} **{{ config('app.name') }}**. [Visit Portal]({{ config('app.url') }})
    +{{ __('Secure Certificate Management for Developers.') }} +
    +
    +
    diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
    +{{ Illuminate\Mail\Markdown::parse($slot) }} +
    diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..a9cf0c0 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,220 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #f8fafc; + color: #475569; + height: 100%; + line-height: 1.6; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.6; + text-align: left; +} + +a { + color: #3b82f6; + text-decoration: none; + font-weight: 500; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #1e293b; + font-size: 22px; + font-weight: 800; + margin-top: 0; + margin-bottom: 24px; + text-align: left; + letter-spacing: -0.025em; +} + +h2 { + color: #1e293b; + font-size: 18px; + font-weight: 700; + margin-top: 0; + text-align: left; +} + +h3 { + color: #1e293b; + font-size: 16px; + font-weight: 700; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.6em; + margin-top: 0; + margin-bottom: 16px; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + background-color: #f8fafc; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 40px 0; + text-align: center; +} + +.header a { + color: #1e293b; + font-size: 19px; + font-weight: 900; + text-decoration: none; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Logo */ + +.logo { + height: auto; + max-height: 48px; + width: auto; +} + +/* Body */ + +.body { + background-color: #f8fafc; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + background-color: #ffffff; + border-radius: 24px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + margin: 0 auto; + padding: 0; + width: 570px; +} + +.content-cell { + padding: 48px; +} + +/* Buttons */ + +.action { + margin: 32px auto; + text-align: center; +} + +.button { + border-radius: 12px; + color: #fff !important; + display: inline-block; + font-size: 15px; + font-weight: 700; + padding: 12px 32px; + text-decoration: none; + transition: all 0.2s; +} + +.button-primary { + background-color: #3b82f6; + box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.3); +} + +/* Panels */ + +.panel { + border-left: #3b82f6 solid 4px; + margin: 24px 0; + border-radius: 0 12px 12px 0; + overflow: hidden; +} + +.panel-content { + background-color: #eff6ff; + color: #1e40af; + padding: 20px; +} + +/* Footer */ + +.footer { + padding: 40px 0; + text-align: center; +} + +.footer p { + color: #94a3b8; + font-size: 13px; + text-align: center; +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + body, .wrapper, .body { + background-color: #0f172a !important; + color: #94a3b8 !important; + } + + .inner-body { + background-color: #1e293b !important; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5) !important; + } + + h1, h2, h3, .header a { + color: #f1f5f9 !important; + } + + p { + color: #94a3b8 !important; + } + + .panel-content { + background-color: #1e293b !important; + color: #60a5fa !important; + border: 1px solid #334155 !important; + } +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..ec58e83 --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..80bce21 --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('app.name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/routes/channels.php b/routes/channels.php new file mode 100644 index 0000000..88de3e0 --- /dev/null +++ b/routes/channels.php @@ -0,0 +1,23 @@ +id === $id; +}); + +Broadcast::channel('user.{id}', function ($user, $id) { + return (int) $user->id === (int) $id; +}); + +Broadcast::channel('ticket.{ticketId}', function ($user, $ticketId) { + // Ensure we convert ticketId to string if it's not already + $ticket = \App\Models\Ticket::where('id', (string) $ticketId)->first(); + + if (!$ticket) { + return false; + } + + // Allow owner or admin + return (string) $user->id === (string) $ticket->user_id || $user->isAdmin(); +}); diff --git a/routes/web.php b/routes/web.php index 55da8d0..57f9df4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,6 +13,8 @@ use App\Http\Controllers\SettingsController; use App\Http\Controllers\Auth\ForgotPasswordController; use App\Http\Controllers\Auth\ResetPasswordController; use App\Http\Controllers\Admin\RootCaController; +use App\Http\Controllers\ContactController; +use App\Http\Controllers\TicketController; // Public API Routes Route::get('/api/public/ca-certificates', [\App\Http\Controllers\Api\PublicCaController::class, 'index'])->name('api.public.ca-certificates'); @@ -28,7 +30,7 @@ Route::get('/ping', function () { // authentication pages Route::middleware('guest')->group(function () { - Route::get('/', [AuthController::class, 'signin'])->name('home'); + Route::get('/', [PageController::class, 'landing'])->name('home'); Route::get('/signin', [AuthController::class, 'signin'])->name('signin'); Route::post('/signin', [AuthController::class, 'authenticate']); Route::get('/signup', [AuthController::class, 'signup'])->name('signup'); @@ -70,6 +72,9 @@ Route::prefix('certificate')->name('certificate.')->group(function () { Route::get('/download-installer', [CertificateController::class, 'downloadInstaller'])->name('download-installer'); }); +// Legal Pages +Route::get('/legal/{slug}', [\App\Http\Controllers\LegalController::class, 'show'])->name('legal.show'); + // Email Verification (Public/Signed) Route::get('/email/verify/{id}/{hash}', [App\Http\Controllers\VerificationController::class, 'verify']) ->middleware(['signed', 'throttle:6,1']) @@ -91,8 +96,18 @@ Route::middleware(['auth', \App\Http\Middleware\EnsureUserIsActive::class])->gro // Authenticated & Verified Routes Route::middleware('verified')->group(function () { + // Notifications + Route::get('/notifications/unread', [\App\Http\Controllers\NotificationController::class, 'getUnread'])->name('notifications.unread'); + Route::get('/notifications/{id}/read', [\App\Http\Controllers\NotificationController::class, 'markAsRead'])->name('notifications.read'); + Route::post('/notifications/read-all', [\App\Http\Controllers\NotificationController::class, 'markAllRead'])->name('notifications.readAll'); + + // Global Search + Route::get('/search/global', [\App\Http\Controllers\SearchController::class, 'global'])->name('search.global'); + // dashboard pages Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('dashboard.stats'); + Route::get('/dashboard/ping', [DashboardController::class, 'ping'])->name('dashboard.ping'); // Certificate routes Route::prefix('certificate')->name('certificate.')->group(function () { @@ -108,33 +123,47 @@ Route::middleware(['auth', \App\Http\Middleware\EnsureUserIsActive::class])->gro Route::delete('/{certificate:uuid}', [CertificateController::class, 'delete'])->name('delete'); }); + // Support Tickets (Customer) + Route::prefix('support')->name('support.')->group(function () { + Route::get('/', [TicketController::class, 'index'])->name('index'); + Route::get('/create', [TicketController::class, 'create'])->name('create'); + Route::post('/', [TicketController::class, 'store'])->name('store'); + Route::get('/{ticket}', [TicketController::class, 'show'])->name('show'); + Route::post('/{ticket}/reply', [TicketController::class, 'reply'])->name('reply'); + Route::post('/{ticket}/close', [TicketController::class, 'close'])->name('close'); + }); - // Admin Only Pages (No Prefix) + + // Admin Only Pages (No Prefix) -> Moved to Templates Route::middleware('admin')->group(function () { - // calender pages - Route::get('/calendar', [PageController::class, 'calendar'])->name('calendar'); + + // Templates Group + Route::prefix('templates')->name('templates.')->group(function () { + // calender pages + Route::get('/calendar', [PageController::class, 'calendar'])->name('calendar'); - // form pages - Route::get('/form-elements', [UiController::class, 'formElements'])->name('form-elements'); + // form pages + Route::get('/form-elements', [UiController::class, 'formElements'])->name('form-elements'); - // tables pages - Route::get('/basic-tables', [UiController::class, 'basicTables'])->name('basic-tables'); + // tables pages + Route::get('/basic-tables', [UiController::class, 'basicTables'])->name('basic-tables'); - // pages - Route::get('/blank', [PageController::class, 'blank'])->name('blank'); + // pages + Route::get('/blank', [PageController::class, 'blank'])->name('blank'); - // chart pages - Route::get('/line-chart', [ChartController::class, 'lineChart'])->name('line-chart'); - Route::get('/bar-chart', [ChartController::class, 'barChart'])->name('bar-chart'); + // chart pages + Route::get('/line-chart', [ChartController::class, 'lineChart'])->name('line-chart'); + Route::get('/bar-chart', [ChartController::class, 'barChart'])->name('bar-chart'); - // ui elements pages - Route::get('/alerts', [UiController::class, 'alerts'])->name('alerts'); - Route::get('/avatars', [UiController::class, 'avatars'])->name('avatars'); - Route::get('/badge', [UiController::class, 'badges'])->name('badges'); - Route::get('/buttons', [UiController::class, 'buttons'])->name('buttons'); - Route::get('/image', [UiController::class, 'images'])->name('images'); - Route::get('/videos', [UiController::class, 'videos'])->name('videos'); + // ui elements pages + Route::get('/alerts', [UiController::class, 'alerts'])->name('alerts'); + Route::get('/avatars', [UiController::class, 'avatars'])->name('avatars'); + Route::get('/badge', [UiController::class, 'badges'])->name('badges'); + Route::get('/buttons', [UiController::class, 'buttons'])->name('buttons'); + Route::get('/image', [UiController::class, 'images'])->name('images'); + Route::get('/videos', [UiController::class, 'videos'])->name('videos'); + }); }); // profile pages @@ -162,17 +191,46 @@ Route::middleware(['auth', \App\Http\Middleware\EnsureUserIsActive::class])->gro Route::patch('/users/{user}/update-email', [App\Http\Controllers\UserManagementController::class, 'updateEmail'])->name('users.update-email'); // Root CA Management + Route::post('/setup-ca', [RootCaController::class, 'setup'])->name('setup-ca'); Route::get('/root-ca', [RootCaController::class, 'index'])->name('root-ca.index'); Route::post('/root-ca/{certificate}/renew', [RootCaController::class, 'renew'])->name('root-ca.renew'); - // Setup Route (Admin Only) - Route::post('/setup-ca', [CertificateController::class, 'setupCa'])->name('setup-ca'); + // Legal Page Management + Route::get('/legal-pages', [App\Http\Controllers\Admin\LegalManagementController::class, 'index'])->name('legal-pages.index'); + Route::get('/legal-pages/{legalPage}/edit', [App\Http\Controllers\Admin\LegalManagementController::class, 'edit'])->name('legal-pages.edit'); + Route::put('/legal-pages/{legalPage}', [App\Http\Controllers\Admin\LegalManagementController::class, 'update'])->name('legal-pages.update'); + + // Contact Management + Route::get('/contacts', [App\Http\Controllers\Admin\ContactManagementController::class, 'index'])->name('contacts.index'); + Route::get('/contacts/{contactSubmission}', [App\Http\Controllers\Admin\ContactManagementController::class, 'show'])->name('contacts.show'); + Route::post('/contacts/{contactSubmission}/reply', [App\Http\Controllers\Admin\ContactManagementController::class, 'reply'])->name('contacts.reply'); + Route::delete('/contacts/{contactSubmission}', [App\Http\Controllers\Admin\ContactManagementController::class, 'destroy'])->name('contacts.destroy'); + + // Ticket Management + Route::prefix('tickets')->name('tickets.')->group(function () { + Route::get('/', [App\Http\Controllers\Admin\TicketManagementController::class, 'index'])->name('index'); + Route::get('/{ticket}', [App\Http\Controllers\Admin\TicketManagementController::class, 'show'])->name('show'); + Route::post('/{ticket}/reply', [App\Http\Controllers\Admin\TicketManagementController::class, 'reply'])->name('reply'); + Route::patch('/{ticket}/status', [App\Http\Controllers\Admin\TicketManagementController::class, 'updateStatus'])->name('update-status'); + }); + + // SMTP Tester + Route::get('/smtp-tester', [\App\Http\Controllers\Admin\SmtpTesterController::class, 'index'])->name('smtp-tester.index'); + Route::post('/smtp-tester/send', [\App\Http\Controllers\Admin\SmtpTesterController::class, 'send'])->name('smtp-tester.send'); }); }); }); +// Public Contact Form +Route::get('/contact', [ContactController::class, 'index'])->name('contact'); +Route::post('/contact', [ContactController::class, 'store'])->name('contact.store'); + +// Public Tools +Route::get('/tools/chat-id-finder', [\App\Http\Controllers\ToolController::class, 'chatIdFinder'])->name('tools.chat-id-finder'); +Route::get('/tools/app-key-generator', [\App\Http\Controllers\ToolController::class, 'appKeyGenerator'])->name('tools.app-key-generator'); +Route::post('/tools/app-key-generator', [\App\Http\Controllers\ToolController::class, 'generateAppKey'])->name('tools.app-key-generator.generate'); + // Public / Error Pages Route::get('/error-404', [PageController::class, 'error404'])->name('error-404'); Route::get('/php', [PageController::class, 'php']); -