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 0000000..322936b Binary files /dev/null and b/public/images/logo/auth-logo.png differ diff --git a/public/images/logo/auth-logo.svg b/public/images/logo/auth-logo.svg index eb11cc7..e9a9347 100644 --- a/public/images/logo/auth-logo.svg +++ b/public/images/logo/auth-logo.svg @@ -1,53 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/logo-dark.png b/public/images/logo/logo-dark.png new file mode 100644 index 0000000..95b945d Binary files /dev/null and b/public/images/logo/logo-dark.png differ diff --git a/public/images/logo/logo-dark.svg b/public/images/logo/logo-dark.svg index 4b94dac..55269d3 100644 --- a/public/images/logo/logo-dark.svg +++ b/public/images/logo/logo-dark.svg @@ -1,53 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/logo/logo-icon.png b/public/images/logo/logo-icon.png new file mode 100644 index 0000000..8dae70b Binary files /dev/null and b/public/images/logo/logo-icon.png differ 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 0000000..5435275 Binary files /dev/null and b/public/images/logo/logo.png differ diff --git a/public/images/logo/logo.svg b/public/images/logo/logo.svg index 758dedd..e10c6d0 100644 --- a/public/images/logo/logo.svg +++ b/public/images/logo/logo.svg @@ -1,53 +1,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/og-share.png b/public/images/og-share.png new file mode 100644 index 0000000..a2f08b3 Binary files /dev/null and b/public/images/og-share.png differ 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']); -