├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Console │ └── Commands │ │ ├── DeleteExpiredSites.php │ │ ├── RefreshServerStatus.php │ │ └── SendDeletionNotifications.php ├── Http │ ├── Controllers │ │ ├── Admin │ │ │ └── SettingsController.php │ │ ├── AdminController.php │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── Controller.php │ │ ├── ProfileController.php │ │ ├── ServerManagementController.php │ │ └── SiteController.php │ ├── Middleware │ │ └── RedirectIfAuthenticated.php │ └── Requests │ │ ├── Auth │ │ └── LoginRequest.php │ │ └── ProfileUpdateRequest.php ├── Mail │ ├── SiteCreated.php │ └── SiteDeletionNotification.php ├── Models │ ├── SelectedServer.php │ ├── Site.php │ ├── SystemSetting.php │ └── User.php ├── Providers │ └── AppServiceProvider.php ├── Services │ ├── CloudflareService.php │ ├── ServerAvatarService.php │ └── SystemSettingsService.php └── View │ └── Components │ ├── AppLayout.php │ └── GuestLayout.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── logging.php ├── mail.php ├── queue.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2025_03_29_023305_create_system_settings_table.php │ ├── 2025_03_29_062651_create_selected_servers_table.php │ ├── 2025_03_29_072559_create_sites_table.php │ ├── 2025_03_29_075257_add_reminder_and_email_to_sites_table.php │ ├── 2025_03_29_103446_add_application_id_to_sites_table.php │ ├── 2025_03_29_105617_add_cloudflare_record_id_to_sites_table.php │ ├── 2025_03_29_110439_add_database_id_to_sites_table.php │ ├── 2025_03_29_112452_add_database_username_to_sites_table.php │ ├── 2025_03_29_154111_add_is_public_to_sites_table.php │ ├── 2025_03_30_234312_add_deletion_notification_sent_to_sites_table.php │ └── 2025_03_31_072510_add_phpmyadmin_url_to_selected_servers_table.php ├── migrations_backup │ ├── 2025_03_29_023305_create_system_settings_table.php │ ├── 2025_03_29_062651_create_selected_servers_table.php │ ├── 2025_03_29_065935_remove_metrics_columns_from_selected_servers.php │ └── 2025_03_29_070325_add_connection_status_to_selected_servers.php └── seeders │ ├── DatabaseSeeder.php │ └── SystemSettingsSeeder.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ └── bootstrap.js └── views │ ├── admin │ ├── dashboard.blade.php │ ├── servers.blade.php │ ├── servers.blade.php.bak │ ├── settings.blade.php │ ├── settings │ │ └── index.blade.php │ ├── sites.blade.php │ └── sites │ │ ├── edit.blade.php │ │ └── show.blade.php │ ├── auth │ ├── confirm-password.blade.php │ ├── forgot-password.blade.php │ ├── login.blade.php │ ├── register.blade.php │ ├── reset-password.blade.php │ └── verify-email.blade.php │ ├── components │ ├── application-logo.blade.php │ ├── auth-session-status.blade.php │ ├── danger-button.blade.php │ ├── dropdown-link.blade.php │ ├── dropdown.blade.php │ ├── input-error.blade.php │ ├── input-label.blade.php │ ├── modal.blade.php │ ├── nav-link.blade.php │ ├── primary-button.blade.php │ ├── responsive-nav-link.blade.php │ ├── secondary-button.blade.php │ └── text-input.blade.php │ ├── dashboard.blade.php │ ├── emails │ └── sites │ │ ├── created.blade.php │ │ └── deletion-notification.blade.php │ ├── layouts │ ├── app.blade.php │ ├── guest.blade.php │ ├── main.blade.php │ └── navigation.blade.php │ ├── legal │ ├── disclaimer.blade.php │ ├── privacy.blade.php │ └── terms.blade.php │ ├── partials │ ├── footer.blade.php │ └── header.blade.php │ ├── profile │ ├── edit.blade.php │ └── partials │ │ ├── delete-user-form.blade.php │ │ ├── update-password-form.blade.php │ │ └── update-profile-information-form.blade.php │ ├── sites │ └── public │ │ └── show.blade.php │ ├── vendor │ └── mail │ │ └── html │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ └── themes │ │ └── default.css │ └── welcome.blade.php ├── routes ├── auth.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ ├── PasswordUpdateTest.php │ │ └── RegistrationTest.php │ ├── ExampleTest.php │ └── ProfileTest.php ├── TestCase.php └── Unit │ ├── ExampleTest.php │ └── SiteExpirationTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | APP_LOCALE=en 8 | APP_FALLBACK_LOCALE=en 9 | APP_FAKER_LOCALE=en_US 10 | 11 | APP_MAINTENANCE_DRIVER=file 12 | # APP_MAINTENANCE_STORE=database 13 | 14 | PHP_CLI_SERVER_WORKERS=4 15 | 16 | BCRYPT_ROUNDS=12 17 | 18 | LOG_CHANNEL=stack 19 | LOG_STACK=single 20 | LOG_DEPRECATIONS_CHANNEL=null 21 | LOG_LEVEL=debug 22 | 23 | DB_CONNECTION=sqlite 24 | # DB_HOST=127.0.0.1 25 | # DB_PORT=3306 26 | # DB_DATABASE=laravel 27 | # DB_USERNAME=root 28 | # DB_PASSWORD= 29 | 30 | SESSION_DRIVER=database 31 | SESSION_LIFETIME=120 32 | SESSION_ENCRYPT=false 33 | SESSION_PATH=/ 34 | SESSION_DOMAIN=null 35 | 36 | BROADCAST_CONNECTION=log 37 | FILESYSTEM_DISK=local 38 | QUEUE_CONNECTION=database 39 | 40 | CACHE_STORE=database 41 | # CACHE_PREFIX= 42 | 43 | MEMCACHED_HOST=127.0.0.1 44 | 45 | REDIS_CLIENT=phpredis 46 | REDIS_HOST=127.0.0.1 47 | REDIS_PASSWORD=null 48 | REDIS_PORT=6379 49 | 50 | MAIL_MAILER=log 51 | MAIL_SCHEME=null 52 | MAIL_HOST=127.0.0.1 53 | MAIL_PORT=2525 54 | MAIL_USERNAME=null 55 | MAIL_PASSWORD=null 56 | MAIL_FROM_ADDRESS="hello@example.com" 57 | MAIL_FROM_NAME="${APP_NAME}" 58 | 59 | AWS_ACCESS_KEY_ID= 60 | AWS_SECRET_ACCESS_KEY= 61 | AWS_DEFAULT_REGION=us-east-1 62 | AWS_BUCKET= 63 | AWS_USE_PATH_STYLE_ENDPOINT=false 64 | 65 | VITE_APP_NAME="${APP_NAME}" 66 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | npm-debug.log 17 | yarn-error.log 18 | /auth.json 19 | /.fleet 20 | /.idea 21 | /.nova 22 | /.vscode 23 | /.zed 24 | -------------------------------------------------------------------------------- /app/Console/Commands/RefreshServerStatus.php: -------------------------------------------------------------------------------- 1 | serverAvatarService = $serverAvatarService; 32 | } 33 | 34 | /** 35 | * Execute the console command. 36 | */ 37 | public function handle() 38 | { 39 | $this->info('Refreshing server connection status...'); 40 | 41 | // Check if the API is configured 42 | if (!$this->serverAvatarService->isConfigured()) { 43 | $this->error('ServerAvatar API is not configured. Please check your settings.'); 44 | return 1; 45 | } 46 | 47 | // Get all selected servers 48 | $servers = SelectedServer::all(); 49 | 50 | if ($servers->isEmpty()) { 51 | $this->info('No servers found in the database.'); 52 | return 0; 53 | } 54 | 55 | $this->info('Found ' . $servers->count() . ' servers in the database.'); 56 | 57 | $updatedCount = 0; 58 | $errorCount = 0; 59 | 60 | // Iterate over each server and check its status 61 | foreach ($servers as $server) { 62 | $this->info("Checking status for server: {$server->name} (ID: {$server->server_id})"); 63 | 64 | try { 65 | // Fetch the latest server information from ServerAvatar API 66 | $result = $this->serverAvatarService->getServer($server->server_id); 67 | 68 | if (!$result['success']) { 69 | $this->error("Failed to fetch server info: {$result['message']}"); 70 | $errorCount++; 71 | Log::warning('Failed to refresh server status', [ 72 | 'server_id' => $server->server_id, 73 | 'error' => $result['message'] 74 | ]); 75 | continue; 76 | } 77 | 78 | $serverData = $result['data']; 79 | 80 | // Determine connection status based on both agent_status and ssh_status 81 | $isAgentConnected = isset($serverData['agent_status']) && $serverData['agent_status'] == '1'; 82 | $isSshConnected = isset($serverData['ssh_status']) && $serverData['ssh_status'] == '1'; 83 | 84 | // Only set to connected if both agent and SSH are connected 85 | $newConnectionStatus = ($isAgentConnected && $isSshConnected) ? 'connected' : 'disconnected'; 86 | 87 | // Update the server record with the new status 88 | $oldStatus = $server->connection_status; 89 | $server->connection_status = $newConnectionStatus; 90 | $server->save(); 91 | 92 | if ($oldStatus !== $newConnectionStatus) { 93 | $this->info("Updated server status from '{$oldStatus}' to '{$newConnectionStatus}'"); 94 | 95 | Log::info('Server connection status updated', [ 96 | 'server_id' => $server->server_id, 97 | 'server_name' => $server->name, 98 | 'old_status' => $oldStatus, 99 | 'new_status' => $newConnectionStatus, 100 | 'agent_connected' => $isAgentConnected, 101 | 'ssh_connected' => $isSshConnected 102 | ]); 103 | 104 | $updatedCount++; 105 | } else { 106 | $this->line("Server status unchanged: '{$newConnectionStatus}'"); 107 | } 108 | 109 | } catch (\Exception $e) { 110 | $this->error("Error refreshing server status: " . $e->getMessage()); 111 | $errorCount++; 112 | Log::error('Exception while refreshing server status', [ 113 | 'server_id' => $server->server_id, 114 | 'server_name' => $server->name, 115 | 'exception' => $e->getMessage() 116 | ]); 117 | } 118 | } 119 | 120 | $this->info("Server status refresh completed:"); 121 | $this->info("- Total servers: " . $servers->count()); 122 | $this->info("- Statuses updated: " . $updatedCount); 123 | $this->info("- Errors encountered: " . $errorCount); 124 | 125 | return 0; 126 | } 127 | } -------------------------------------------------------------------------------- /app/Console/Commands/SendDeletionNotifications.php: -------------------------------------------------------------------------------- 1 | info('Started checking for sites to notify...'); 33 | 34 | // Log the current time 35 | $now = Carbon::now(); 36 | $threshold = $now->copy()->addMinutes(35); 37 | $this->info("Current time: {$now}, Threshold: {$threshold}"); 38 | 39 | // Get all sites first to see what we have 40 | $allSites = Site::all(); 41 | $this->info("Total sites in database: " . $allSites->count()); 42 | 43 | foreach ($allSites as $site) { 44 | $this->info("Site ID: {$site->id}, Name: {$site->name}, Expires: {$site->expires_at}, Email: " . 45 | ($site->email ?? 'none') . ", Notified: " . ($site->deletion_notification_sent ? 'yes' : 'no')); 46 | } 47 | 48 | // Find sites expiring in less than 35 minutes but haven't been notified yet 49 | $query = Site::query() 50 | ->where('expires_at', '<=', $threshold) 51 | ->where('deletion_notification_sent', false) 52 | ->whereNotNull('email'); 53 | 54 | // Log the query being executed 55 | $this->info("Executing query: " . $query->toSql()); 56 | $this->info("With bindings: " . json_encode($query->getBindings())); 57 | 58 | $sitesToNotify = $query->get(); 59 | 60 | $this->info("Found {$sitesToNotify->count()} sites to notify"); 61 | 62 | $count = 0; 63 | 64 | foreach ($sitesToNotify as $site) { 65 | $this->info("Processing site for notification: ID {$site->id}, Name: {$site->name}, Expires: {$site->expires_at}"); 66 | 67 | // Send notification email 68 | if (!empty($site->email)) { 69 | try { 70 | $this->info("Sending email to: {$site->email}"); 71 | Mail::to($site->email)->send(new SiteDeletionNotification($site)); 72 | 73 | // Update the site to mark notification as sent 74 | $site->deletion_notification_sent = true; 75 | $site->save(); 76 | 77 | $count++; 78 | 79 | $this->info("Sent deletion notification for site: {$site->name} ({$site->domain})"); 80 | } catch (\Exception $e) { 81 | $this->error("Failed to send email: " . $e->getMessage()); 82 | } 83 | } else { 84 | $this->warn("No email address for site {$site->id}"); 85 | } 86 | } 87 | 88 | $this->info("Sent {$count} deletion notifications"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/Http/Controllers/AdminController.php: -------------------------------------------------------------------------------- 1 | serverAvatarService = $serverAvatarService; 17 | $this->systemSettings = $systemSettings; 18 | } 19 | 20 | public function dashboard() 21 | { 22 | $domain = $this->systemSettings->getDomain(); 23 | return view('admin.dashboard', compact('domain')); 24 | } 25 | 26 | // Sites functionality moved to SiteController 27 | 28 | public function servers(Request $request) 29 | { 30 | $page = $request->input('page', 1); 31 | $result = $this->serverAvatarService->getServers($page); 32 | 33 | $servers = []; 34 | $pagination = null; 35 | $apiConfigured = $this->serverAvatarService->isConfigured(); 36 | $errorMessage = null; 37 | 38 | if ($result['success']) { 39 | $apiResponse = $result['data']; 40 | $servers = $apiResponse['data'] ?? []; 41 | 42 | // Cache the servers in the session for use in other controllers 43 | session(['available_servers' => $servers]); 44 | 45 | // Extract pagination info 46 | if (isset($apiResponse['current_page'])) { 47 | $pagination = [ 48 | 'current_page' => $apiResponse['current_page'], 49 | 'from' => $apiResponse['from'], 50 | 'to' => $apiResponse['to'], 51 | 'total' => $apiResponse['total'], 52 | 'last_page' => $apiResponse['last_page'], 53 | 'per_page' => $apiResponse['per_page'], 54 | 'prev_page_url' => $apiResponse['prev_page_url'], 55 | 'next_page_url' => $apiResponse['next_page_url'], 56 | ]; 57 | } 58 | } else { 59 | $errorMessage = $result['message']; 60 | } 61 | 62 | // Get selected servers 63 | $selectedServers = \App\Models\SelectedServer::all(); 64 | 65 | return view('admin.servers', compact('servers', 'selectedServers', 'pagination', 'apiConfigured', 'errorMessage')); 66 | } 67 | 68 | // Settings now handled by SettingsController 69 | } -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 28 | 29 | $request->session()->regenerate(); 30 | 31 | return redirect()->intended(route('admin.dashboard', absolute: false)); 32 | } 33 | 34 | /** 35 | * Destroy an authenticated session. 36 | */ 37 | public function destroy(Request $request): RedirectResponse 38 | { 39 | Auth::guard('web')->logout(); 40 | 41 | $request->session()->invalidate(); 42 | 43 | $request->session()->regenerateToken(); 44 | 45 | return redirect('/'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 28 | 'email' => $request->user()->email, 29 | 'password' => $request->password, 30 | ])) { 31 | throw ValidationException::withMessages([ 32 | 'password' => __('auth.password'), 33 | ]); 34 | } 35 | 36 | $request->session()->put('auth.password_confirmed_at', time()); 37 | 38 | return redirect()->intended(route('dashboard', absolute: false)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 17 | return redirect()->intended(route('dashboard', absolute: false)); 18 | } 19 | 20 | $request->user()->sendEmailVerificationNotification(); 21 | 22 | return back()->with('status', 'verification-link-sent'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 18 | ? redirect()->intended(route('dashboard', absolute: false)) 19 | : view('auth.verify-email'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request]); 24 | } 25 | 26 | /** 27 | * Handle an incoming new password request. 28 | * 29 | * @throws \Illuminate\Validation\ValidationException 30 | */ 31 | public function store(Request $request): RedirectResponse 32 | { 33 | $request->validate([ 34 | 'token' => ['required'], 35 | 'email' => ['required', 'email'], 36 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 37 | ]); 38 | 39 | // Here we will attempt to reset the user's password. If it is successful we 40 | // will update the password on an actual user model and persist it to the 41 | // database. Otherwise we will parse the error and return the response. 42 | $status = Password::reset( 43 | $request->only('email', 'password', 'password_confirmation', 'token'), 44 | function (User $user) use ($request) { 45 | $user->forceFill([ 46 | 'password' => Hash::make($request->password), 47 | 'remember_token' => Str::random(60), 48 | ])->save(); 49 | 50 | event(new PasswordReset($user)); 51 | } 52 | ); 53 | 54 | // If the password was successfully reset, we will redirect the user back to 55 | // the application's home authenticated view. If there is an error we can 56 | // redirect them back to where they came from with their error message. 57 | return $status == Password::PASSWORD_RESET 58 | ? redirect()->route('login')->with('status', __($status)) 59 | : back()->withInput($request->only('email')) 60 | ->withErrors(['email' => __($status)]); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validateWithBag('updatePassword', [ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back()->with('status', 'password-updated'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 29 | 'email' => ['required', 'email'], 30 | ]); 31 | 32 | // We will send the password reset link to this user. Once we have attempted 33 | // to send the link, we will examine the response then see the message we 34 | // need to show to the user. Finally, we'll send out a proper response. 35 | $status = Password::sendResetLink( 36 | $request->only('email') 37 | ); 38 | 39 | return $status == Password::RESET_LINK_SENT 40 | ? back()->with('status', __($status)) 41 | : back()->withInput($request->only('email')) 42 | ->withErrors(['email' => __($status)]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | first()->meta_value ?? '1'; 26 | 27 | if ($allowRegistration !== '1') { 28 | return redirect()->route('login') 29 | ->with('error', 'Registration is currently disabled by the administrator.'); 30 | } 31 | 32 | return view('auth.register'); 33 | } 34 | 35 | /** 36 | * Handle an incoming registration request. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function store(Request $request): RedirectResponse 41 | { 42 | // Check if registration is allowed in system settings 43 | $allowRegistration = SystemSetting::where('meta_key', 'allow_registration') 44 | ->first()->meta_value ?? '1'; 45 | 46 | if ($allowRegistration !== '1') { 47 | return redirect()->route('login') 48 | ->with('error', 'Registration is currently disabled by the administrator.'); 49 | } 50 | 51 | $request->validate([ 52 | 'name' => ['required', 'string', 'max:255'], 53 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 54 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 55 | ]); 56 | 57 | $user = User::create([ 58 | 'name' => $request->name, 59 | 'email' => $request->email, 60 | 'password' => Hash::make($request->password), 61 | ]); 62 | 63 | event(new Registered($user)); 64 | 65 | Auth::login($user); 66 | 67 | return redirect(route('admin.dashboard', absolute: false)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19 | } 20 | 21 | if ($request->user()->markEmailAsVerified()) { 22 | event(new Verified($request->user())); 23 | } 24 | 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $request->user(), 21 | ]); 22 | } 23 | 24 | /** 25 | * Update the user's profile information. 26 | */ 27 | public function update(ProfileUpdateRequest $request): RedirectResponse 28 | { 29 | $request->user()->fill($request->validated()); 30 | 31 | if ($request->user()->isDirty('email')) { 32 | $request->user()->email_verified_at = null; 33 | } 34 | 35 | $request->user()->save(); 36 | 37 | return Redirect::route('profile.edit')->with('status', 'profile-updated'); 38 | } 39 | 40 | /** 41 | * Delete the user's account. 42 | */ 43 | public function destroy(Request $request): RedirectResponse 44 | { 45 | $request->validateWithBag('userDeletion', [ 46 | 'password' => ['required', 'current_password'], 47 | ]); 48 | 49 | $user = $request->user(); 50 | 51 | Auth::logout(); 52 | 53 | $user->delete(); 54 | 55 | $request->session()->invalidate(); 56 | $request->session()->regenerateToken(); 57 | 58 | return Redirect::to('/'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Http/Controllers/ServerManagementController.php: -------------------------------------------------------------------------------- 1 | serverAvatarService = $serverAvatarService; 17 | } 18 | 19 | public function getSelectedServers(): JsonResponse 20 | { 21 | $servers = SelectedServer::all(); 22 | 23 | return response()->json([ 24 | 'success' => true, 25 | 'data' => $servers 26 | ]); 27 | } 28 | 29 | public function addServer(Request $request): JsonResponse 30 | { 31 | $request->validate([ 32 | 'server_id' => 'required|integer' 33 | ]); 34 | 35 | $serverId = $request->input('server_id'); 36 | 37 | $existingServer = SelectedServer::where('server_id', $serverId)->first(); 38 | if ($existingServer) { 39 | return response()->json([ 40 | 'success' => false, 41 | 'message' => 'Server already added to selected servers' 42 | ]); 43 | } 44 | 45 | $result = $this->serverAvatarService->getServer($serverId); 46 | 47 | if (!$result['success']) { 48 | return response()->json([ 49 | 'success' => false, 50 | 'message' => $result['message'] 51 | ]); 52 | } 53 | 54 | try { 55 | $serverData = $result['data']; 56 | 57 | $isAgentConnected = isset($serverData['agent_status']) && $serverData['agent_status'] == '1'; 58 | $isSshConnected = isset($serverData['ssh_status']) && $serverData['ssh_status'] == '1'; 59 | 60 | $connectionStatus = ($isAgentConnected && $isSshConnected) ? 'connected' : 'disconnected'; 61 | 62 | $server = new SelectedServer(); 63 | $server->server_id = $serverData['id']; 64 | $server->name = $serverData['name']; 65 | $server->ip_address = $serverData['ip'] ?? ''; 66 | $server->web_server = $serverData['web_server'] ?? null; 67 | $server->database_type = $serverData['database_type'] ?? null; 68 | $server->cores = isset($serverData['cores']) && is_numeric($serverData['cores']) ? $serverData['cores'] : null; 69 | $server->connection_status = $connectionStatus; 70 | 71 | $exists = SelectedServer::where('server_id', $serverData['id'])->first(); 72 | if ($exists) { 73 | $exists->delete(); 74 | } 75 | 76 | $server->save(); 77 | 78 | return response()->json([ 79 | 'success' => true, 80 | 'message' => 'Server added successfully', 81 | 'data' => $server 82 | ]); 83 | } catch (\Exception $e) { 84 | return response()->json([ 85 | 'success' => false, 86 | 'message' => 'Failed to add server: ' . $e->getMessage() 87 | ]); 88 | } 89 | } 90 | 91 | public function removeServer(Request $request): JsonResponse 92 | { 93 | $request->validate([ 94 | 'server_id' => 'required|integer' 95 | ]); 96 | 97 | $serverId = $request->input('server_id'); 98 | 99 | $server = SelectedServer::where('server_id', $serverId)->first(); 100 | 101 | if (!$server) { 102 | return response()->json([ 103 | 'success' => false, 104 | 'message' => 'Server not found in selected servers' 105 | ]); 106 | } 107 | 108 | try { 109 | $server->delete(); 110 | 111 | return response()->json([ 112 | 'success' => true, 113 | 'message' => 'Server removed successfully' 114 | ]); 115 | } catch (\Exception $e) { 116 | return response()->json([ 117 | 'success' => false, 118 | 'message' => 'Failed to remove server: ' . $e->getMessage() 119 | ]); 120 | } 121 | } 122 | 123 | /** 124 | * Update the phpMyAdmin URL for a server 125 | * 126 | * @param Request $request 127 | * @return JsonResponse 128 | */ 129 | public function updatePhpMyAdminUrl(Request $request): JsonResponse 130 | { 131 | $request->validate([ 132 | 'server_id' => 'required|integer', 133 | 'phpmyadmin_url' => 'nullable|url' 134 | ]); 135 | 136 | $serverId = $request->input('server_id'); 137 | $phpmyadminUrl = $request->input('phpmyadmin_url'); 138 | 139 | $server = SelectedServer::where('server_id', $serverId)->first(); 140 | 141 | if (!$server) { 142 | return response()->json([ 143 | 'success' => false, 144 | 'message' => 'Server not found in selected servers' 145 | ]); 146 | } 147 | 148 | try { 149 | $server->phpmyadmin_url = $phpmyadminUrl; 150 | $server->save(); 151 | 152 | return response()->json([ 153 | 'success' => true, 154 | 'message' => 'phpMyAdmin URL updated successfully' 155 | ]); 156 | } catch (\Exception $e) { 157 | return response()->json([ 158 | 'success' => false, 159 | 'message' => 'Failed to update phpMyAdmin URL: ' . $e->getMessage() 160 | ]); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 31 | return redirect($this->redirectTo($request)); 32 | } 33 | } 34 | 35 | return $next($request); 36 | } 37 | 38 | /** 39 | * Get the path the user should be redirected to when they are authenticated. 40 | */ 41 | protected function redirectTo(Request $request): ?string 42 | { 43 | return static::$redirectToCallback 44 | ? call_user_func(static::$redirectToCallback, $request) 45 | : $this->defaultRedirectUri(); 46 | } 47 | 48 | /** 49 | * Get the default URI the user should be redirected to when they are authenticated. 50 | */ 51 | protected function defaultRedirectUri(): string 52 | { 53 | foreach (['admin.dashboard', 'admin', 'home'] as $uri) { 54 | if (Route::has($uri)) { 55 | return route($uri); 56 | } 57 | } 58 | 59 | $routes = Route::getRoutes()->get('GET'); 60 | 61 | foreach (['admin', 'home'] as $uri) { 62 | if (isset($routes[$uri])) { 63 | return '/'.$uri; 64 | } 65 | } 66 | 67 | return '/'; 68 | } 69 | 70 | /** 71 | * Specify the callback that should be used to generate the redirect path. 72 | * 73 | * @param callable $redirectToCallback 74 | * @return void 75 | */ 76 | public static function redirectUsing(callable $redirectToCallback) 77 | { 78 | static::$redirectToCallback = $redirectToCallback; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | |string> 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'email' => ['required', 'string', 'email'], 31 | 'password' => ['required', 'string'], 32 | ]; 33 | } 34 | 35 | /** 36 | * Attempt to authenticate the request's credentials. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function authenticate(): void 41 | { 42 | $this->ensureIsNotRateLimited(); 43 | 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45 | RateLimiter::hit($this->throttleKey()); 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => trans('auth.failed'), 49 | ]); 50 | } 51 | 52 | RateLimiter::clear($this->throttleKey()); 53 | } 54 | 55 | /** 56 | * Ensure the login request is not rate limited. 57 | * 58 | * @throws \Illuminate\Validation\ValidationException 59 | */ 60 | public function ensureIsNotRateLimited(): void 61 | { 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63 | return; 64 | } 65 | 66 | event(new Lockout($this)); 67 | 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); 69 | 70 | throw ValidationException::withMessages([ 71 | 'email' => trans('auth.throttle', [ 72 | 'seconds' => $seconds, 73 | 'minutes' => ceil($seconds / 60), 74 | ]), 75 | ]); 76 | } 77 | 78 | /** 79 | * Get the rate limiting throttle key for the request. 80 | */ 81 | public function throttleKey(): string 82 | { 83 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | |string> 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => [ 21 | 'required', 22 | 'string', 23 | 'lowercase', 24 | 'email', 25 | 'max:255', 26 | Rule::unique(User::class)->ignore($this->user()->id), 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Mail/SiteCreated.php: -------------------------------------------------------------------------------- 1 | site = $site; 31 | } 32 | 33 | /** 34 | * Build the message. 35 | * 36 | * @return $this 37 | */ 38 | public function build() 39 | { 40 | $subject = 'Your WordPress Site Has Been Created: ' . $this->site->domain; 41 | 42 | return $this->subject($subject) 43 | ->markdown('emails.sites.created'); 44 | } 45 | } -------------------------------------------------------------------------------- /app/Mail/SiteDeletionNotification.php: -------------------------------------------------------------------------------- 1 | 48 | */ 49 | public function attachments(): array 50 | { 51 | return []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Models/SelectedServer.php: -------------------------------------------------------------------------------- 1 | $serverData['id']], 54 | [ 55 | 'name' => $serverData['name'], 56 | 'ip_address' => $serverData['ip'] ?? '', 57 | 'web_server' => $serverData['web_server'] ?? null, 58 | 'database_type' => $serverData['database_type'] ?? null, 59 | 'cores' => isset($serverData['cores']) && is_numeric($serverData['cores']) ? $serverData['cores'] : null, 60 | 'connection_status' => $connectionStatus, 61 | ] 62 | ); 63 | } 64 | 65 | /** 66 | * Get the sites that belong to this server. 67 | * 68 | * @return HasMany 69 | */ 70 | public function sites(): HasMany 71 | { 72 | return $this->hasMany(Site::class, 'selected_server_id'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Models/Site.php: -------------------------------------------------------------------------------- 1 | 'array', 54 | 'reminder' => 'boolean', 55 | 'has_dns_record' => 'boolean', 56 | 'is_public' => 'boolean', 57 | 'expires_at' => 'datetime', 58 | 'deletion_notification_sent' => 'boolean', 59 | ]; 60 | 61 | /** 62 | * Bootstrap the model and its traits. 63 | * 64 | * @return void 65 | */ 66 | protected static function boot() 67 | { 68 | parent::boot(); 69 | 70 | // Generate a UUID and set expiration time when creating a new site 71 | static::creating(function ($site) { 72 | if (!$site->uuid) { 73 | $site->uuid = Str::random(32); 74 | } 75 | 76 | // Set the expiration date based on system settings 77 | if (!$site->expires_at) { 78 | $defaultDeletionHours = SystemSetting::where('meta_key', 'default_deletion_time')->value('meta_value') ?? 72; // Default to 72 hours if not set 79 | $site->expires_at = now()->addHours((int) $defaultDeletionHours); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * Get the server that the site belongs to. 86 | * 87 | * @return BelongsTo 88 | */ 89 | public function server(): BelongsTo 90 | { 91 | return $this->belongsTo(SelectedServer::class, 'selected_server_id'); 92 | } 93 | 94 | /** 95 | * Get the subdomain portion of the domain 96 | * 97 | * @return string 98 | */ 99 | public function getSubdomainAttribute(): string 100 | { 101 | $domainParts = explode('.', $this->domain ?? ''); 102 | if (!empty($domainParts)) { 103 | return $domainParts[0]; 104 | } 105 | return ''; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/Models/SystemSetting.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory, Notifiable; 14 | 15 | /** 16 | * The attributes that are mass assignable. 17 | * 18 | * @var list 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | ]; 25 | 26 | /** 27 | * The attributes that should be hidden for serialization. 28 | * 29 | * @var list 30 | */ 31 | protected $hidden = [ 32 | 'password', 33 | 'remember_token', 34 | ]; 35 | 36 | /** 37 | * Get the attributes that should be cast. 38 | * 39 | * @return array 40 | */ 41 | protected function casts(): array 42 | { 43 | return [ 44 | 'email_verified_at' => 'datetime', 45 | 'password' => 'hashed', 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(\App\Services\SystemSettingsService::class); 19 | 20 | // Register a short alias for the service 21 | $this->app->alias(\App\Services\SystemSettingsService::class, 'system-settings'); 22 | } 23 | 24 | /** 25 | * Bootstrap any application services. 26 | */ 27 | public function boot(): void 28 | { 29 | // Skip all database operations if we're migrating 30 | if (getenv('APP_MIGRATING') === 'true') { 31 | return; 32 | } 33 | 34 | $this->configureMailFromDatabase(); 35 | } 36 | 37 | /** 38 | * Configure mail settings from database 39 | */ 40 | private function configureMailFromDatabase(): void 41 | { 42 | try { 43 | if (!Schema::hasTable('system_settings')) { 44 | return; 45 | } 46 | 47 | $mailSettings = SystemSetting::whereIn('meta_key', [ 48 | 'mail_host', 'mail_port', 'mail_username', 'mail_password', 49 | 'mail_from_name', 'mail_from_address', 'mail_encryption' 50 | ])->pluck('meta_value', 'meta_key')->toArray(); 51 | 52 | if (empty($mailSettings['mail_host']) || empty($mailSettings['mail_port'])) { 53 | return; 54 | } 55 | 56 | Config::set('mail.default', 'smtp'); 57 | Config::set('mail.mailers.smtp.host', $mailSettings['mail_host']); 58 | Config::set('mail.mailers.smtp.port', $mailSettings['mail_port']); 59 | 60 | if (!empty($mailSettings['mail_username'])) { 61 | Config::set('mail.mailers.smtp.username', $mailSettings['mail_username']); 62 | } 63 | 64 | if (!empty($mailSettings['mail_password'])) { 65 | Config::set('mail.mailers.smtp.password', $mailSettings['mail_password']); 66 | } 67 | 68 | // Set encryption (tls or null) 69 | $encryption = !empty($mailSettings['mail_encryption']) && $mailSettings['mail_encryption'] == '1' ? 'tls' : null; 70 | Config::set('mail.mailers.smtp.encryption', $encryption); 71 | 72 | if (!empty($mailSettings['mail_from_address'])) { 73 | Config::set('mail.from.address', $mailSettings['mail_from_address']); 74 | } 75 | 76 | if (!empty($mailSettings['mail_from_name'])) { 77 | Config::set('mail.from.name', $mailSettings['mail_from_name']); 78 | } 79 | } catch (\Exception $e) { 80 | // Silent fail during deployment 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/Services/SystemSettingsService.php: -------------------------------------------------------------------------------- 1 | first(); 33 | return $setting ? $setting->meta_value : $default; 34 | }); 35 | } catch (\Exception $e) { 36 | return $default; 37 | } 38 | } 39 | 40 | /** 41 | * Get the domain name from system settings or return a default 42 | * 43 | * @return string 44 | */ 45 | public function getDomain() 46 | { 47 | $domain = $this->get('domain'); 48 | 49 | // Always log what domain we're returning 50 | \Illuminate\Support\Facades\Log::debug('Getting domain from system settings', [ 51 | 'raw_domain' => $domain, 52 | 'using_fallback' => empty($domain), 53 | 'returning' => !empty($domain) ? $domain : 'example.com' 54 | ]); 55 | 56 | return $domain && !empty($domain) ? $domain : 'example.com'; 57 | } 58 | 59 | /** 60 | * Clear the cache for a specific setting 61 | * 62 | * @param string $key 63 | * @return void 64 | */ 65 | public function clearCache(string $key) 66 | { 67 | $cacheKey = 'system_setting_' . $key; 68 | Cache::forget($cacheKey); 69 | 70 | // Log the cache clearing for debugging 71 | \Illuminate\Support\Facades\Log::debug('Cleared system setting cache', [ 72 | 'key' => $key, 73 | 'cache_key' => $cacheKey, 74 | ]); 75 | } 76 | 77 | /** 78 | * Directly get the domain name from database, bypassing cache 79 | * 80 | * @return string 81 | */ 82 | public function getUncachedDomain() 83 | { 84 | // Skip during migrations 85 | if (getenv('APP_MIGRATING') === 'true') { 86 | return 'example.com'; 87 | } 88 | 89 | // Skip if table doesn't exist 90 | if (!Schema::hasTable('system_settings')) { 91 | return 'example.com'; 92 | } 93 | 94 | try { 95 | $setting = SystemSetting::where('meta_key', 'domain')->first(); 96 | return $setting ? $setting->meta_value : 'example.com'; 97 | } catch (\Exception $e) { 98 | return 'example.com'; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 16 | web: __DIR__.'/../routes/web.php', 17 | commands: __DIR__.'/../routes/console.php', 18 | health: '/up', 19 | ) 20 | ->withMiddleware(function (Middleware $middleware) { 21 | // 22 | }) 23 | ->withExceptions(function (Exceptions $exceptions) { 24 | // 25 | })->create(); 26 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | the application so that it's available within Artisan commands. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Application Timezone 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Here you may specify the default timezone for your application, which 63 | | will be used by the PHP date and date-time functions. The timezone 64 | | is set to "UTC" by default as it is suitable for most use cases. 65 | | 66 | */ 67 | 68 | 'timezone' => 'UTC', 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Application Locale Configuration 73 | |-------------------------------------------------------------------------- 74 | | 75 | | The application locale determines the default locale that will be used 76 | | by Laravel's translation / localization methods. This option can be 77 | | set to any locale for which you plan to have translation strings. 78 | | 79 | */ 80 | 81 | 'locale' => env('APP_LOCALE', 'en'), 82 | 83 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 84 | 85 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Encryption Key 90 | |-------------------------------------------------------------------------- 91 | | 92 | | This key is utilized by Laravel's encryption services and should be set 93 | | to a random, 32 character string to ensure that all encrypted values 94 | | are secure. You should do this prior to deploying the application. 95 | | 96 | */ 97 | 98 | 'cipher' => 'AES-256-CBC', 99 | 100 | 'key' => env('APP_KEY'), 101 | 102 | 'previous_keys' => [ 103 | ...array_filter( 104 | explode(',', env('APP_PREVIOUS_KEYS', '')) 105 | ), 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Maintenance Mode Driver 111 | |-------------------------------------------------------------------------- 112 | | 113 | | These configuration options determine the driver used to determine and 114 | | manage Laravel's "maintenance mode" status. The "cache" driver will 115 | | allow maintenance mode to be controlled across multiple machines. 116 | | 117 | | Supported drivers: "file", "cache" 118 | | 119 | */ 120 | 121 | 'maintenance' => [ 122 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 123 | 'store' => env('APP_MAINTENANCE_STORE', 'database'), 124 | ], 125 | 126 | ]; 127 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the amount of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'sqlite'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Database Connections 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Below are all of the database connections defined for your application. 27 | | An example configuration is provided for each database system which 28 | | is supported by Laravel. You're free to add / remove connections. 29 | | 30 | */ 31 | 32 | 'connections' => [ 33 | 34 | 'sqlite' => [ 35 | 'driver' => 'sqlite', 36 | 'url' => env('DB_URL'), 37 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 38 | 'prefix' => '', 39 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 40 | 'busy_timeout' => null, 41 | 'journal_mode' => null, 42 | 'synchronous' => null, 43 | ], 44 | 45 | 'mysql' => [ 46 | 'driver' => 'mysql', 47 | 'url' => env('DB_URL'), 48 | 'host' => env('DB_HOST', '127.0.0.1'), 49 | 'port' => env('DB_PORT', '3306'), 50 | 'database' => env('DB_DATABASE', 'laravel'), 51 | 'username' => env('DB_USERNAME', 'root'), 52 | 'password' => env('DB_PASSWORD', ''), 53 | 'unix_socket' => env('DB_SOCKET', ''), 54 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 55 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 56 | 'prefix' => '', 57 | 'prefix_indexes' => true, 58 | 'strict' => true, 59 | 'engine' => null, 60 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 61 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 62 | ]) : [], 63 | ], 64 | 65 | 'mariadb' => [ 66 | 'driver' => 'mariadb', 67 | 'url' => env('DB_URL'), 68 | 'host' => env('DB_HOST', '127.0.0.1'), 69 | 'port' => env('DB_PORT', '3306'), 70 | 'database' => env('DB_DATABASE', 'laravel'), 71 | 'username' => env('DB_USERNAME', 'root'), 72 | 'password' => env('DB_PASSWORD', ''), 73 | 'unix_socket' => env('DB_SOCKET', ''), 74 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 75 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 76 | 'prefix' => '', 77 | 'prefix_indexes' => true, 78 | 'strict' => true, 79 | 'engine' => null, 80 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 81 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 82 | ]) : [], 83 | ], 84 | 85 | 'pgsql' => [ 86 | 'driver' => 'pgsql', 87 | 'url' => env('DB_URL'), 88 | 'host' => env('DB_HOST', '127.0.0.1'), 89 | 'port' => env('DB_PORT', '5432'), 90 | 'database' => env('DB_DATABASE', 'laravel'), 91 | 'username' => env('DB_USERNAME', 'root'), 92 | 'password' => env('DB_PASSWORD', ''), 93 | 'charset' => env('DB_CHARSET', 'utf8'), 94 | 'prefix' => '', 95 | 'prefix_indexes' => true, 96 | 'search_path' => 'public', 97 | 'sslmode' => 'prefer', 98 | ], 99 | 100 | 'sqlsrv' => [ 101 | 'driver' => 'sqlsrv', 102 | 'url' => env('DB_URL'), 103 | 'host' => env('DB_HOST', 'localhost'), 104 | 'port' => env('DB_PORT', '1433'), 105 | 'database' => env('DB_DATABASE', 'laravel'), 106 | 'username' => env('DB_USERNAME', 'root'), 107 | 'password' => env('DB_PASSWORD', ''), 108 | 'charset' => env('DB_CHARSET', 'utf8'), 109 | 'prefix' => '', 110 | 'prefix_indexes' => true, 111 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 112 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 113 | ], 114 | 115 | ], 116 | 117 | /* 118 | |-------------------------------------------------------------------------- 119 | | Migration Repository Table 120 | |-------------------------------------------------------------------------- 121 | | 122 | | This table keeps track of all the migrations that have already run for 123 | | your application. Using this information, we can determine which of 124 | | the migrations on disk haven't actually been run on the database. 125 | | 126 | */ 127 | 128 | 'migrations' => [ 129 | 'table' => 'migrations', 130 | 'update_date_on_publish' => true, 131 | ], 132 | 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Redis Databases 136 | |-------------------------------------------------------------------------- 137 | | 138 | | Redis is an open source, fast, and advanced key-value store that also 139 | | provides a richer body of commands than a typical key-value system 140 | | such as Memcached. You may define your connection settings here. 141 | | 142 | */ 143 | 144 | 'redis' => [ 145 | 146 | 'client' => env('REDIS_CLIENT', 'phpredis'), 147 | 148 | 'options' => [ 149 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 150 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 151 | 'persistent' => env('REDIS_PERSISTENT', false), 152 | ], 153 | 154 | 'default' => [ 155 | 'url' => env('REDIS_URL'), 156 | 'host' => env('REDIS_HOST', '127.0.0.1'), 157 | 'username' => env('REDIS_USERNAME'), 158 | 'password' => env('REDIS_PASSWORD'), 159 | 'port' => env('REDIS_PORT', '6379'), 160 | 'database' => env('REDIS_DB', '0'), 161 | ], 162 | 163 | 'cache' => [ 164 | 'url' => env('REDIS_URL'), 165 | 'host' => env('REDIS_HOST', '127.0.0.1'), 166 | 'username' => env('REDIS_USERNAME'), 167 | 'password' => env('REDIS_PASSWORD'), 168 | 'port' => env('REDIS_PORT', '6379'), 169 | 'database' => env('REDIS_CACHE_DB', '1'), 170 | ], 171 | 172 | ], 173 | 174 | ]; 175 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | 'report' => false, 39 | ], 40 | 41 | 'public' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path('app/public'), 44 | 'url' => env('APP_URL').'/storage', 45 | 'visibility' => 'public', 46 | 'throw' => false, 47 | 'report' => false, 48 | ], 49 | 50 | 's3' => [ 51 | 'driver' => 's3', 52 | 'key' => env('AWS_ACCESS_KEY_ID'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 54 | 'region' => env('AWS_DEFAULT_REGION'), 55 | 'bucket' => env('AWS_BUCKET'), 56 | 'url' => env('AWS_URL'), 57 | 'endpoint' => env('AWS_ENDPOINT'), 58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 59 | 'throw' => false, 60 | 'report' => false, 61 | ], 62 | 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Symbolic Links 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Here you may configure the symbolic links that will be created when the 71 | | `storage:link` Artisan command is executed. The array keys should be 72 | | the locations of the links and the values should be their targets. 73 | | 74 | */ 75 | 76 | 'links' => [ 77 | public_path('storage') => storage_path('app/public'), 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Deprecations Log Channel 26 | |-------------------------------------------------------------------------- 27 | | 28 | | This option controls the log channel that should be used to log warnings 29 | | regarding deprecated PHP and library features. This allows you to get 30 | | your application ready for upcoming major versions of dependencies. 31 | | 32 | */ 33 | 34 | 'deprecations' => [ 35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false), 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Log Channels 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Here you may configure the log channels for your application. Laravel 45 | | utilizes the Monolog PHP logging library, which includes a variety 46 | | of powerful log handlers and formatters that you're free to use. 47 | | 48 | | Available drivers: "single", "daily", "slack", "syslog", 49 | | "errorlog", "monolog", "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 55 | 'stack' => [ 56 | 'driver' => 'stack', 57 | 'channels' => explode(',', env('LOG_STACK', 'single')), 58 | 'ignore_exceptions' => false, 59 | ], 60 | 61 | 'single' => [ 62 | 'driver' => 'single', 63 | 'path' => storage_path('logs/laravel.log'), 64 | 'level' => env('LOG_LEVEL', 'debug'), 65 | 'replace_placeholders' => true, 66 | ], 67 | 68 | 'daily' => [ 69 | 'driver' => 'daily', 70 | 'path' => storage_path('logs/laravel.log'), 71 | 'level' => env('LOG_LEVEL', 'debug'), 72 | 'days' => env('LOG_DAILY_DAYS', 14), 73 | 'replace_placeholders' => true, 74 | ], 75 | 76 | 'slack' => [ 77 | 'driver' => 'slack', 78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), 80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 81 | 'level' => env('LOG_LEVEL', 'critical'), 82 | 'replace_placeholders' => true, 83 | ], 84 | 85 | 'papertrail' => [ 86 | 'driver' => 'monolog', 87 | 'level' => env('LOG_LEVEL', 'debug'), 88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 89 | 'handler_with' => [ 90 | 'host' => env('PAPERTRAIL_URL'), 91 | 'port' => env('PAPERTRAIL_PORT'), 92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 93 | ], 94 | 'processors' => [PsrLogMessageProcessor::class], 95 | ], 96 | 97 | 'stderr' => [ 98 | 'driver' => 'monolog', 99 | 'level' => env('LOG_LEVEL', 'debug'), 100 | 'handler' => StreamHandler::class, 101 | 'handler_with' => [ 102 | 'stream' => 'php://stderr', 103 | ], 104 | 'formatter' => env('LOG_STDERR_FORMATTER'), 105 | 'processors' => [PsrLogMessageProcessor::class], 106 | ], 107 | 108 | 'syslog' => [ 109 | 'driver' => 'syslog', 110 | 'level' => env('LOG_LEVEL', 'debug'), 111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 112 | 'replace_placeholders' => true, 113 | ], 114 | 115 | 'errorlog' => [ 116 | 'driver' => 'errorlog', 117 | 'level' => env('LOG_LEVEL', 'debug'), 118 | 'replace_placeholders' => true, 119 | ], 120 | 121 | 'null' => [ 122 | 'driver' => 'monolog', 123 | 'handler' => NullHandler::class, 124 | ], 125 | 126 | 'emergency' => [ 127 | 'path' => storage_path('logs/laravel.log'), 128 | ], 129 | 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'scheme' => env('MAIL_SCHEME'), 43 | 'url' => env('MAIL_URL'), 44 | 'host' => env('MAIL_HOST', '127.0.0.1'), 45 | 'port' => env('MAIL_PORT', 2525), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | 90 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_023305_create_system_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('meta_key')->unique(); 17 | $table->text('meta_value')->nullable(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('system_settings'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_062651_create_selected_servers_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedInteger('server_id')->comment('Server ID from ServerAvatar API'); 17 | $table->string('name'); 18 | $table->string('ip_address'); 19 | $table->string('web_server')->nullable(); 20 | $table->string('database_type')->nullable(); 21 | $table->unsignedInteger('cores')->nullable(); 22 | $table->string('connection_status')->default('unknown') 23 | ->comment('Server connection status: connected, disconnected, or unknown'); 24 | $table->timestamps(); 25 | 26 | // Ensure we don't add the same server twice 27 | $table->unique('server_id'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('selected_servers'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_072559_create_sites_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid', 32)->unique()->comment('32 character unique identifier for the site'); 17 | $table->string('name'); 18 | $table->string('domain'); 19 | $table->text('description')->nullable(); 20 | $table->foreignId('selected_server_id')->constrained('selected_servers')->onDelete('cascade'); 21 | $table->string('php_version')->nullable(); 22 | $table->string('server_id')->comment('External server ID from API'); 23 | $table->string('status')->default('active'); 24 | $table->json('site_data')->nullable()->comment('Additional site configuration'); 25 | $table->timestamp('expires_at')->nullable()->comment('When this site will be automatically deleted'); 26 | $table->timestamps(); 27 | $table->index('uuid'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('sites'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_075257_add_reminder_and_email_to_sites_table.php: -------------------------------------------------------------------------------- 1 | boolean('reminder')->default(false)->after('site_data'); 16 | $table->string('email')->nullable()->after('reminder'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('sites', function (Blueprint $table) { 26 | $table->dropColumn(['reminder', 'email']); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_103446_add_application_id_to_sites_table.php: -------------------------------------------------------------------------------- 1 | string('application_id')->nullable()->comment('ServerAvatar application ID'); 16 | $table->string('system_username')->nullable()->comment('ServerAvatar system username'); 17 | $table->string('wp_username')->nullable()->comment('WordPress admin username'); 18 | $table->string('database_name')->nullable()->comment('MySQL database name'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('sites', function (Blueprint $table) { 28 | $table->dropColumn([ 29 | 'application_id', 30 | 'system_username', 31 | 'wp_username', 32 | 'database_name' 33 | ]); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_105617_add_cloudflare_record_id_to_sites_table.php: -------------------------------------------------------------------------------- 1 | string('cloudflare_record_id')->nullable()->comment('Cloudflare DNS record ID'); 16 | $table->boolean('has_dns_record')->default(false)->comment('Flag to indicate if a DNS record has been created'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('sites', function (Blueprint $table) { 26 | $table->dropColumn('cloudflare_record_id'); 27 | $table->dropColumn('has_dns_record'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_110439_add_database_id_to_sites_table.php: -------------------------------------------------------------------------------- 1 | string('database_id')->nullable()->comment('Database ID from ServerAvatar API'); 16 | // database_user_id column removed as it's not needed 17 | $table->string('database_password')->nullable()->comment('Database Password'); 18 | $table->string('database_host')->nullable()->comment('Database Host'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('sites', function (Blueprint $table) { 28 | $table->dropColumn('database_id'); 29 | // database_user_id column removed from down method as well 30 | $table->dropColumn('database_password'); 31 | $table->dropColumn('database_host'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_112452_add_database_username_to_sites_table.php: -------------------------------------------------------------------------------- 1 | string('database_username')->nullable()->comment('Database Username')->after('database_id'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('sites', function (Blueprint $table) { 25 | $table->dropColumn('database_username'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_03_29_154111_add_is_public_to_sites_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_public')->default(false)->after('status') 16 | ->comment('Whether the site details can be publicly accessed without authentication'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('sites', function (Blueprint $table) { 26 | $table->dropColumn('is_public'); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2025_03_30_234312_add_deletion_notification_sent_to_sites_table.php: -------------------------------------------------------------------------------- 1 | boolean('deletion_notification_sent')->default(false); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('sites', function (Blueprint $table) { 25 | $table->dropColumn('deletion_notification_sent'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_03_31_072510_add_phpmyadmin_url_to_selected_servers_table.php: -------------------------------------------------------------------------------- 1 | string('phpmyadmin_url')->nullable()->after('connection_status'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('selected_servers', function (Blueprint $table) { 25 | $table->dropColumn('phpmyadmin_url'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations_backup/2025_03_29_023305_create_system_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('meta_key')->unique(); 17 | $table->text('meta_value')->nullable(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('system_settings'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations_backup/2025_03_29_062651_create_selected_servers_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedInteger('server_id')->comment('Server ID from ServerAvatar API'); 17 | $table->string('name'); 18 | $table->string('ip_address'); 19 | $table->string('web_server')->nullable(); 20 | $table->string('database_type')->nullable(); 21 | $table->unsignedInteger('cores')->nullable(); 22 | $table->string('status')->default('active'); 23 | $table->float('load_average')->nullable()->comment('CPU load average in percentage'); 24 | $table->float('memory_usage')->nullable()->comment('Memory usage in percentage'); 25 | $table->float('disk_usage')->nullable()->comment('Disk usage in percentage'); 26 | $table->json('server_data')->nullable()->comment('Additional server data from API'); 27 | $table->timestamps(); 28 | 29 | // Ensure we don't add the same server twice 30 | $table->unique('server_id'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | */ 37 | public function down(): void 38 | { 39 | Schema::dropIfExists('selected_servers'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /database/migrations_backup/2025_03_29_065935_remove_metrics_columns_from_selected_servers.php: -------------------------------------------------------------------------------- 1 | dropColumn('load_average'); 16 | $table->dropColumn('memory_usage'); 17 | $table->dropColumn('disk_usage'); 18 | $table->dropColumn('server_data'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('selected_servers', function (Blueprint $table) { 28 | $table->float('load_average')->nullable()->comment('CPU load average in percentage'); 29 | $table->float('memory_usage')->nullable()->comment('Memory usage in percentage'); 30 | $table->float('disk_usage')->nullable()->comment('Disk usage in percentage'); 31 | $table->json('server_data')->nullable()->comment('Additional server data from API'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations_backup/2025_03_29_070325_add_connection_status_to_selected_servers.php: -------------------------------------------------------------------------------- 1 | string('connection_status')->default('unknown') 16 | ->comment('Server connection status: connected, disconnected, or unknown'); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('selected_servers', function (Blueprint $table) { 26 | $table->dropColumn('connection_status'); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'test@example.com', 21 | ]); 22 | 23 | $this->call([ 24 | SystemSettingsSeeder::class, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/seeders/SystemSettingsSeeder.php: -------------------------------------------------------------------------------- 1 | 'default_deletion_time'], 18 | ['meta_value' => '24'] // Default to 24 hours 19 | ); 20 | 21 | SystemSetting::updateOrCreate( 22 | ['meta_key' => 'allow_site_creation'], 23 | ['meta_value' => '0'] // Default to disabled 24 | ); 25 | 26 | SystemSetting::updateOrCreate( 27 | ['meta_key' => 'allow_registration'], 28 | ['meta_value' => '1'] // Default to enabled 29 | ); 30 | 31 | // Cloudflare Integration Settings 32 | SystemSetting::updateOrCreate( 33 | ['meta_key' => 'zone_id'], 34 | ['meta_value' => ''] 35 | ); 36 | 37 | SystemSetting::updateOrCreate( 38 | ['meta_key' => 'cloudflare_api_key'], 39 | ['meta_value' => ''] 40 | ); 41 | 42 | SystemSetting::updateOrCreate( 43 | ['meta_key' => 'domain'], 44 | ['meta_value' => ''] 45 | ); 46 | 47 | SystemSetting::updateOrCreate( 48 | ['meta_key' => 'ssl_certificate'], 49 | ['meta_value' => ''] 50 | ); 51 | 52 | SystemSetting::updateOrCreate( 53 | ['meta_key' => 'private_key'], 54 | ['meta_value' => ''] 55 | ); 56 | 57 | // ServerAvatar API Settings 58 | SystemSetting::updateOrCreate( 59 | ['meta_key' => 'api_url'], 60 | ['meta_value' => ''] 61 | ); 62 | 63 | SystemSetting::updateOrCreate( 64 | ['meta_key' => 'api_key'], 65 | ['meta_value' => ''] 66 | ); 67 | 68 | SystemSetting::updateOrCreate( 69 | ['meta_key' => 'organisation_id'], 70 | ['meta_value' => ''] 71 | ); 72 | 73 | // SMTP Configuration Settings 74 | SystemSetting::updateOrCreate( 75 | ['meta_key' => 'mail_host'], 76 | ['meta_value' => ''] 77 | ); 78 | 79 | SystemSetting::updateOrCreate( 80 | ['meta_key' => 'mail_port'], 81 | ['meta_value' => ''] 82 | ); 83 | 84 | SystemSetting::updateOrCreate( 85 | ['meta_key' => 'mail_username'], 86 | ['meta_value' => ''] 87 | ); 88 | 89 | SystemSetting::updateOrCreate( 90 | ['meta_key' => 'mail_password'], 91 | ['meta_value' => ''] 92 | ); 93 | 94 | SystemSetting::updateOrCreate( 95 | ['meta_key' => 'mail_from_name'], 96 | ['meta_value' => ''] 97 | ); 98 | 99 | SystemSetting::updateOrCreate( 100 | ['meta_key' => 'mail_from_address'], 101 | ['meta_value' => ''] 102 | ); 103 | 104 | SystemSetting::updateOrCreate( 105 | ['meta_key' => 'mail_encryption'], 106 | ['meta_value' => '0'] // Default to disabled 107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.2", 10 | "@tailwindcss/vite": "^4.0.0", 11 | "alpinejs": "^3.4.2", 12 | "autoprefixer": "^10.4.2", 13 | "axios": "^1.8.2", 14 | "concurrently": "^9.0.1", 15 | "laravel-vite-plugin": "^1.2.0", 16 | "postcss": "^8.4.31", 17 | "tailwindcss": "^3.1.0", 18 | "vite": "^6.0.11" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adarshsojitra/sandbox/9dee40259f2153067920e612c38aa319a6694195/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | 3 | import Alpine from 'alpinejs'; 4 | 5 | window.Alpine = Alpine; 6 | 7 | Alpine.start(); 8 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | window.axios = axios; 3 | 4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 4 |
5 | 6 |
7 | @csrf 8 | 9 | 10 |
11 | 12 | 13 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | {{ __('Confirm') }} 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /resources/views/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 4 |
5 | 6 | 7 | 8 | 9 |
10 | @csrf 11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | {{ __('Email Password Reset Link') }} 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | @csrf 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 23 | 24 | 25 |
26 | 27 | 28 |
29 | 33 |
34 | 35 |
36 | @if (Route::has('password.request')) 37 | 38 | {{ __('Forgot your password?') }} 39 | 40 | @endif 41 | 42 | 43 | {{ __('Log in') }} 44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /resources/views/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | @csrf 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 | 27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | 35 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | {{ __('Already registered?') }} 45 | 46 | 47 | 48 | {{ __('Register') }} 49 | 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | @csrf 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | {{ __('Reset Password') }} 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /resources/views/auth/verify-email.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 4 |
5 | 6 | @if (session('status') == 'verification-link-sent') 7 |
8 | {{ __('A new verification link has been sent to the email address you provided during registration.') }} 9 |
10 | @endif 11 | 12 |
13 |
14 | @csrf 15 | 16 |
17 | 18 | {{ __('Resend Verification Email') }} 19 | 20 |
21 |
22 | 23 |
24 | @csrf 25 | 26 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /resources/views/components/application-logo.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | Sandbox 4 | by ServerAvatar 5 |
6 |
-------------------------------------------------------------------------------- /resources/views/components/auth-session-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |
merge(['class' => 'font-medium text-sm text-green-600']) }}> 5 | {{ $status }} 6 |
7 | @endif 8 | -------------------------------------------------------------------------------- /resources/views/components/danger-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) 2 | 3 | @php 4 | $alignmentClasses = match ($align) { 5 | 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', 6 | 'top' => 'origin-top', 7 | default => 'ltr:origin-top-right rtl:origin-top-left end-0', 8 | }; 9 | 10 | $width = match ($width) { 11 | '48' => 'w-48', 12 | default => $width, 13 | }; 14 | @endphp 15 | 16 |
17 |
18 | {{ $trigger }} 19 |
20 | 21 | 35 |
36 | -------------------------------------------------------------------------------- /resources/views/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props(['messages']) 2 | 3 | @if ($messages) 4 |
    merge(['class' => 'text-sm text-red-600 space-y-1']) }}> 5 | @foreach ((array) $messages as $message) 6 |
  • {{ $message }}
  • 7 | @endforeach 8 |
9 | @endif 10 | -------------------------------------------------------------------------------- /resources/views/components/input-label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name', 3 | 'show' => false, 4 | 'maxWidth' => '2xl' 5 | ]) 6 | 7 | @php 8 | $maxWidth = [ 9 | 'sm' => 'sm:max-w-sm', 10 | 'md' => 'sm:max-w-md', 11 | 'lg' => 'sm:max-w-lg', 12 | 'xl' => 'sm:max-w-xl', 13 | '2xl' => 'sm:max-w-2xl', 14 | ][$maxWidth]; 15 | @endphp 16 | 17 |
52 |
63 |
64 |
65 | 66 |
76 | {{ $slot }} 77 |
78 |
79 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/primary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/text-input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}> 4 | -------------------------------------------------------------------------------- /resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Dashboard') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | {{ __("You're logged in!") }} 13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /resources/views/emails/sites/created.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Your WordPress Site Has Been Created 3 | 4 | Your WordPress site **{{ $site->domain }}** has been successfully deployed and is ready to use! 5 | 6 | ## Site Information 7 | - **Site URL:** [{{ 'https://' . $site->domain }}]({{ 'https://' . $site->domain }}) 8 | - **WordPress Admin:** [{{ 'https://' . $site->domain }}/wp-admin]({{ 'https://' . $site->domain }}/wp-admin) 9 | - **Username:** {{ $site->wp_username ?? $site->site_data['wp_username'] ?? 'admin' }} 10 | - **Password:** {{ $site->site_data['wp_password'] ?? '*****' }} 11 | 12 | ## Database Information 13 | - **Database Name:** {{ $site->database_name ?? $site->site_data['database_name'] ?? 'N/A' }} 14 | - **Database Username:** {{ $site->database_username ?? $site->site_data['database_username'] ?? 'N/A' }} 15 | - **Database Password:** {{ $site->database_password ?? $site->site_data['database_password'] ?? '*****' }} 16 | - **Database Host:** {{ $site->database_host ?? $site->site_data['database_host'] ?? 'localhost' }} 17 | 18 | @if($site->is_public) 19 | ## Public Information Page 20 | A public information page for your site is available at: 21 | [{{ route('sites.public.show', $site->uuid) }}]({{ route('sites.public.show', $site->uuid) }}) 22 | @endif 23 | 24 | ## Site Expiration 25 | This site will expire and be automatically deleted on **{{ $site->expires_at->format('F j, Y, g:i a') }}**. 26 | 27 | @component('mail::button', ['url' => 'https://' . $site->domain . '/wp-admin']) 28 | Visit WordPress Admin 29 | @endcomponent 30 | 31 | Thank you for using our service! 32 | 33 | Regards, 34 | {{ config('app.name') }} 35 | @endcomponent -------------------------------------------------------------------------------- /resources/views/emails/sites/deletion-notification.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Site Deletion Notification 3 | 4 | Dear User, 5 | 6 | Your site **{{ $site->name }}** ({{ $site->domain }}) will be automatically deleted in about 30 minutes. 7 | 8 | If you want to continue using this site, please log in to your account and extend its expiration time. 9 | 10 | @component('mail::button', ['url' => url('/login')]) 11 | Log in to your account 12 | @endcomponent 13 | 14 | Thank you for using our service. 15 | 16 | Regards,
17 | Sandbox by ServerAvatar 18 | @endcomponent -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @vite(['resources/css/app.css', 'resources/js/app.js']) 19 | 20 | 21 |
22 | @include('layouts.navigation') 23 | 24 | 25 | @isset($header) 26 |
27 |
28 | {{ $header }} 29 |
30 |
31 | @endisset 32 | 33 | 34 |
35 | {{ $slot }} 36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 |
26 | {{ $slot }} 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/views/layouts/navigation.blade.php: -------------------------------------------------------------------------------- 1 | 119 | -------------------------------------------------------------------------------- /resources/views/legal/disclaimer.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.main') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |

Disclaimer

8 | 9 |
10 |

Last Updated: March 30, 2025

11 |
12 | 13 |
14 |

This disclaimer ("Disclaimer") sets forth the general guidelines, disclosures, and terms of your use of the Throwaway WordPress Sites service ("Service", "we", "us" or "our").

15 |
16 | 17 |
18 |

1. Representation

19 |

Any views or information found on our Service are not intended to offend, but if you find anything offensive or inaccurate, please contact us immediately so we can address it.

20 |

The information provided by our Service is for general informational purposes only. All information on the Service is provided in good faith, however we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability, or completeness of any information on the Service.

21 |
22 | 23 |
24 |

2. External Links Disclaimer

25 |

The Service may contain links to external websites that are not provided or maintained by or in any way affiliated with us. Please note that we do not guarantee the accuracy, relevance, timeliness, or completeness of any information on these external websites.

26 |
27 | 28 |
29 |

3. Errors and Omissions Disclaimer

30 |

The information given by the Service is for general guidance on matters of interest. Even if the Service takes every precaution to ensure that the content is both current and accurate, errors can occur. Plus, given the changing nature of laws, rules and regulations, there may be delays, omissions or inaccuracies in the information contained on the Service.

31 |

We are not responsible for any errors or omissions, or for the results obtained from the use of this information.

32 |
33 | 34 |
35 |

4. Fair Use Disclaimer

36 |

The Service may contain copyrighted material the use of which has not always been specifically authorized by the copyright owner. We are making such material available in our efforts to advance understanding of environmental, political, human rights, economic, democracy, scientific, and social justice issues, etc. We believe this constitutes a 'fair use' of any such copyrighted material as provided for in section 107 of the US Copyright Law.

37 |

If you wish to use copyrighted material from the Service for your own purposes that go beyond fair use, you must obtain permission from the copyright owner.

38 |
39 | 40 |
41 |

5. No Responsibility Disclaimer

42 |

The information on the Service is provided with the understanding that we are not herein engaged in rendering legal, accounting, tax, or other professional advice and services. As such, it should not be used as a substitute for consultation with professional accounting, tax, legal or other competent advisers.

43 |

In no event shall we or our suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever arising out of or in connection with your access or use or inability to access or use the Service.

44 |
45 | 46 |
47 |

6. "Use at Your Own Risk" Disclaimer

48 |

All information in the Service is provided "as is", with no guarantee of completeness, accuracy, timeliness or of the results obtained from the use of this information, and without warranty of any kind, express or implied, including, but not limited to warranties of performance, merchantability, and fitness for a particular purpose.

49 |

We will not be liable to you or anyone else for any decision made or action taken in reliance on the information given by the Service or for any consequential, special or similar damages, even if advised of the possibility of such damages.

50 |
51 | 52 |
53 |

7. Temporary Website Disclaimer

54 |

The temporary websites created through our Service are intended for testing, demonstration, and other short-term purposes. We make no guarantees about the availability, security, or performance of these temporary sites beyond their specified expiration period.

55 |

Users are solely responsible for all content placed on their temporary sites and for ensuring that such content complies with all applicable laws and regulations. We assume no responsibility for user-generated content on temporary sites created through our Service.

56 |
57 | 58 |
59 |

8. Contact Us

60 |

If you have any questions about this Disclaimer, please contact us at disclaimer@example.com.

61 |
62 | 63 |
64 | Return to Homepage 65 |
66 |
67 |
68 |
69 | @endsection -------------------------------------------------------------------------------- /resources/views/legal/terms.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.main') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |

Terms of Service

8 | 9 |
10 |

Last Updated: March 30, 2025

11 |
12 | 13 |
14 |

Please read these Terms of Service ("Terms") carefully before using the Throwaway WordPress Sites service ("Service").

15 |

By accessing or using the Service, you agree to be bound by these Terms. If you disagree with any part of the terms, then you may not access the Service.

16 |
17 | 18 |
19 |

1. Service Description

20 |

Throwaway WordPress Sites provides temporary WordPress sites that are automatically deleted after a specified period. These sites are intended for testing, demonstrations, and temporary use cases.

21 |
22 | 23 |
24 |

2. Account Terms

25 |

When you create a temporary site on our platform, you may provide an email address for notifications. You are responsible for maintaining the security of your site credentials and are fully responsible for all activities that occur under your account.

26 |

You must immediately notify us of any unauthorized use of your account or any other breach of security.

27 |
28 | 29 |
30 |

3. Acceptable Use

31 |

Your use of the Service must not violate any applicable laws, including copyright or trademark laws, export control laws, or other laws in your jurisdiction. You are responsible for making sure that your use of the Service is in compliance with laws and any applicable regulations.

32 |

You agree not to use the Service for:

33 |
    34 |
  • Illegal activities or to promote illegal activities
  • 35 |
  • Distributing malware or other harmful code
  • 36 |
  • Phishing, spamming, or any form of unsolicited communication
  • 37 |
  • Impersonating others or providing inaccurate information
  • 38 |
  • Activities that interfere with or disrupt the Service or servers
  • 39 |
  • Storing or transmitting material that infringes on the rights of others
  • 40 |
41 |
42 | 43 |
44 |

4. Content Ownership

45 |

You retain ownership of any intellectual property rights that you hold in content you upload to your temporary site. By uploading content, you grant us a worldwide, royalty-free license to use, reproduce, modify, and distribute that content for the purpose of providing the Service.

46 |
47 | 48 |
49 |

5. Service Termination

50 |

We may terminate or suspend your access to the Service immediately, without prior notice or liability, for any reason, including, without limitation, if you breach the Terms.

51 |

All temporary sites are scheduled for automatic deletion after a specified period (as indicated during site creation). We make no guarantees about the availability of your site beyond the specified period.

52 |
53 | 54 |
55 |

6. Limitation of Liability

56 |

In no event shall we be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your access to or use of or inability to access or use the Service.

57 |
58 | 59 |
60 |

7. Disclaimer

61 |

Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.

62 |
63 | 64 |
65 |

8. Changes to Terms

66 |

We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will try to provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.

67 |
68 | 69 |
70 |

9. Contact Us

71 |

If you have any questions about these Terms, please contact us at support@example.com.

72 |
73 | 74 |
75 | Return to Homepage 76 |
77 |
78 |
79 |
80 | @endsection -------------------------------------------------------------------------------- /resources/views/partials/footer.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | Sandbox 8 | by ServerAvatar 9 |
10 |

An open-source platform for creating instant, self-destructing WordPress sites.

11 |

Perfect for testing, client demos, and temporary projects.

12 |

Powered by ServerAvatar's API.

13 |
14 |
15 |
16 | 24 |
25 |
26 |
27 |
28 |

© 2025 Sandbox by ServerAvatar. All rights reserved.

29 | 34 |
35 |
36 |
-------------------------------------------------------------------------------- /resources/views/partials/header.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 17 |
18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /resources/views/profile/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Profile') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | @include('profile.partials.update-profile-information-form') 13 |
14 |
15 | 16 |
17 |
18 | @include('profile.partials.update-password-form') 19 |
20 |
21 | 22 |
23 |
24 | @include('profile.partials.delete-user-form') 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /resources/views/profile/partials/delete-user-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Delete Account') }} 5 |

6 | 7 |

8 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 9 |

10 |
11 | 12 | {{ __('Delete Account') }} 16 | 17 | 18 |
19 | @csrf 20 | @method('delete') 21 | 22 |

23 | {{ __('Are you sure you want to delete your account?') }} 24 |

25 | 26 |

27 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 28 |

29 | 30 |
31 | 32 | 33 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | {{ __('Cancel') }} 47 | 48 | 49 | 50 | {{ __('Delete Account') }} 51 | 52 |
53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /resources/views/profile/partials/update-password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Update Password') }} 5 |

6 | 7 |

8 | {{ __('Ensure your account is using a long, random password to stay secure.') }} 9 |

10 |
11 | 12 |
13 | @csrf 14 | @method('put') 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | {{ __('Save') }} 36 | 37 | @if (session('status') === 'password-updated') 38 |

{{ __('Saved.') }}

45 | @endif 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /resources/views/profile/partials/update-profile-information-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Profile Information') }} 5 |

6 | 7 |

8 | {{ __("Update your account's profile information and email address.") }} 9 |

10 |
11 | 12 |
13 | @csrf 14 |
15 | 16 |
17 | @csrf 18 | @method('patch') 19 | 20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) 32 |
33 |

34 | {{ __('Your email address is unverified.') }} 35 | 36 | 39 |

40 | 41 | @if (session('status') === 'verification-link-sent') 42 |

43 | {{ __('A new verification link has been sent to your email address.') }} 44 |

45 | @endif 46 |
47 | @endif 48 |
49 | 50 |
51 | {{ __('Save') }} 52 | 53 | @if (session('status') === 'profile-updated') 54 |

{{ __('Saved.') }}

61 | @endif 62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Sandbox
5 | by ServerAvatar 6 |
7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/themes/default.css: -------------------------------------------------------------------------------- 1 | /* Base */ 2 | 3 | body, 4 | body *:not(html):not(style):not(br):not(tr):not(code) { 5 | box-sizing: border-box; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 7 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 8 | position: relative; 9 | } 10 | 11 | body { 12 | -webkit-text-size-adjust: none; 13 | background-color: #ffffff; 14 | color: #718096; 15 | height: 100%; 16 | line-height: 1.4; 17 | margin: 0; 18 | padding: 0; 19 | width: 100% !important; 20 | } 21 | 22 | p, 23 | ul, 24 | ol, 25 | blockquote { 26 | line-height: 1.4; 27 | text-align: left; 28 | } 29 | 30 | a { 31 | color: #4f46e5; 32 | } 33 | 34 | a img { 35 | border: none; 36 | } 37 | 38 | /* Typography */ 39 | 40 | h1 { 41 | color: #3d4852; 42 | font-size: 18px; 43 | font-weight: bold; 44 | margin-top: 0; 45 | text-align: left; 46 | } 47 | 48 | h2 { 49 | font-size: 16px; 50 | font-weight: bold; 51 | margin-top: 0; 52 | text-align: left; 53 | } 54 | 55 | h3 { 56 | font-size: 14px; 57 | font-weight: bold; 58 | margin-top: 0; 59 | text-align: left; 60 | } 61 | 62 | p { 63 | font-size: 16px; 64 | line-height: 1.5em; 65 | margin-top: 0; 66 | text-align: left; 67 | } 68 | 69 | p.sub { 70 | font-size: 12px; 71 | } 72 | 73 | img { 74 | max-width: 100%; 75 | } 76 | 77 | /* Layout */ 78 | 79 | .wrapper { 80 | -premailer-cellpadding: 0; 81 | -premailer-cellspacing: 0; 82 | -premailer-width: 100%; 83 | background-color: #edf2f7; 84 | margin: 0; 85 | padding: 0; 86 | width: 100%; 87 | } 88 | 89 | .content { 90 | -premailer-cellpadding: 0; 91 | -premailer-cellspacing: 0; 92 | -premailer-width: 100%; 93 | margin: 0; 94 | padding: 0; 95 | width: 100%; 96 | } 97 | 98 | /* Header */ 99 | 100 | .header { 101 | padding: 25px 0; 102 | text-align: center; 103 | } 104 | 105 | .header a { 106 | color: #4f46e5; 107 | font-size: 19px; 108 | font-weight: bold; 109 | text-decoration: none; 110 | } 111 | 112 | /* Logo */ 113 | 114 | .logo { 115 | height: 75px; 116 | max-height: 75px; 117 | width: auto; 118 | } 119 | 120 | /* Body */ 121 | 122 | .body { 123 | -premailer-cellpadding: 0; 124 | -premailer-cellspacing: 0; 125 | -premailer-width: 100%; 126 | background-color: #edf2f7; 127 | border-bottom: 1px solid #edf2f7; 128 | border-top: 1px solid #edf2f7; 129 | margin: 0; 130 | padding: 0; 131 | width: 100%; 132 | } 133 | 134 | .inner-body { 135 | -premailer-cellpadding: 0; 136 | -premailer-cellspacing: 0; 137 | -premailer-width: 570px; 138 | background-color: #ffffff; 139 | border-color: #e8e5ef; 140 | border-radius: 2px; 141 | border-width: 1px; 142 | box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); 143 | margin: 0 auto; 144 | padding: 0; 145 | width: 570px; 146 | } 147 | 148 | /* Subcopy */ 149 | 150 | .subcopy { 151 | border-top: 1px solid #e8e5ef; 152 | margin-top: 25px; 153 | padding-top: 25px; 154 | } 155 | 156 | .subcopy p { 157 | font-size: 14px; 158 | } 159 | 160 | /* Footer */ 161 | 162 | .footer { 163 | -premailer-cellpadding: 0; 164 | -premailer-cellspacing: 0; 165 | -premailer-width: 570px; 166 | margin: 0 auto; 167 | padding: 0; 168 | text-align: center; 169 | width: 570px; 170 | } 171 | 172 | .footer p { 173 | color: #b0adc5; 174 | font-size: 12px; 175 | text-align: center; 176 | } 177 | 178 | .footer a { 179 | color: #b0adc5; 180 | text-decoration: underline; 181 | } 182 | 183 | /* Tables */ 184 | 185 | .table table { 186 | -premailer-cellpadding: 0; 187 | -premailer-cellspacing: 0; 188 | -premailer-width: 100%; 189 | margin: 30px auto; 190 | width: 100%; 191 | } 192 | 193 | .table th { 194 | border-bottom: 1px solid #edeff2; 195 | margin: 0; 196 | padding-bottom: 8px; 197 | } 198 | 199 | .table td { 200 | color: #74787e; 201 | font-size: 15px; 202 | line-height: 18px; 203 | margin: 0; 204 | padding: 10px 0; 205 | } 206 | 207 | .content-cell { 208 | max-width: 100vw; 209 | padding: 32px; 210 | } 211 | 212 | /* Buttons */ 213 | 214 | .action { 215 | -premailer-cellpadding: 0; 216 | -premailer-cellspacing: 0; 217 | -premailer-width: 100%; 218 | margin: 30px auto; 219 | padding: 0; 220 | text-align: center; 221 | width: 100%; 222 | } 223 | 224 | .button { 225 | -webkit-text-size-adjust: none; 226 | border-radius: 4px; 227 | color: #fff; 228 | display: inline-block; 229 | overflow: hidden; 230 | text-decoration: none; 231 | } 232 | 233 | .button-blue, 234 | .button-primary { 235 | background-color: #4f46e5; 236 | border-bottom: 8px solid #4f46e5; 237 | border-left: 18px solid #4f46e5; 238 | border-right: 18px solid #4f46e5; 239 | border-top: 8px solid #4f46e5; 240 | } 241 | 242 | .button-green, 243 | .button-success { 244 | background-color: #48bb78; 245 | border-bottom: 8px solid #48bb78; 246 | border-left: 18px solid #48bb78; 247 | border-right: 18px solid #48bb78; 248 | border-top: 8px solid #48bb78; 249 | } 250 | 251 | .button-red, 252 | .button-error { 253 | background-color: #e53e3e; 254 | border-bottom: 8px solid #e53e3e; 255 | border-left: 18px solid #e53e3e; 256 | border-right: 18px solid #e53e3e; 257 | border-top: 8px solid #e53e3e; 258 | } 259 | 260 | /* Panels */ 261 | 262 | .panel { 263 | border-left: #4f46e5 solid 4px; 264 | margin: 21px 0; 265 | } 266 | 267 | .panel-content { 268 | background-color: #f7fafc; 269 | color: #718096; 270 | padding: 16px; 271 | } 272 | 273 | .panel-content p { 274 | color: #718096; 275 | } 276 | 277 | .panel-item { 278 | padding: 0; 279 | } 280 | 281 | .panel-item p:last-of-type { 282 | margin-bottom: 0; 283 | padding-bottom: 0; 284 | } 285 | 286 | /* Utilities */ 287 | 288 | .break-all { 289 | word-break: break-all; 290 | } -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('register', [RegisteredUserController::class, 'create']) 16 | ->name('register'); 17 | 18 | Route::post('register', [RegisteredUserController::class, 'store']); 19 | 20 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 21 | ->name('login'); 22 | 23 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 | 25 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26 | ->name('password.request'); 27 | 28 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29 | ->name('password.email'); 30 | 31 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32 | ->name('password.reset'); 33 | 34 | Route::post('reset-password', [NewPasswordController::class, 'store']) 35 | ->name('password.store'); 36 | }); 37 | 38 | Route::middleware('auth')->group(function () { 39 | Route::get('verify-email', EmailVerificationPromptController::class) 40 | ->name('verification.notice'); 41 | 42 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43 | ->middleware(['signed', 'throttle:6,1']) 44 | ->name('verification.verify'); 45 | 46 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47 | ->middleware('throttle:6,1') 48 | ->name('verification.send'); 49 | 50 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51 | ->name('password.confirm'); 52 | 53 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54 | 55 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56 | 57 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58 | ->name('logout'); 59 | }); 60 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 9 | })->purpose('Display an inspiring quote'); 10 | 11 | // Schedule the delete expired sites command to run every hour 12 | Schedule::command('sites:delete-expired')->everyMinute(); 13 | 14 | // Schedule the refresh server status command to run every 5 minutes 15 | Schedule::command('servers:refresh-status')->everyFiveMinutes(); 16 | 17 | // Schedule the send deletion notifications command to run every 5 minutes 18 | Schedule::command('app:send-deletion-notifications')->everyFiveMinutes(); -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('home'); 13 | 14 | // Site creation from homepage 15 | Route::post('/', [App\Http\Controllers\SiteController::class, 'store'])->name('home.sites.store'); 16 | 17 | // Public site details route 18 | Route::get('/s/{uuid}', [SiteController::class, 'publicShow'])->name('sites.public.show'); 19 | 20 | // Legal pages 21 | Route::view('/terms', 'legal.terms')->name('legal.terms'); 22 | Route::view('/privacy', 'legal.privacy')->name('legal.privacy'); 23 | Route::view('/disclaimer', 'legal.disclaimer')->name('legal.disclaimer'); 24 | 25 | // Debug route for form submission testing 26 | if (config('app.debug')) { 27 | Route::post('/debug-form', function (\Illuminate\Http\Request $request) { 28 | \Illuminate\Support\Facades\Log::debug('Debug form submission', [ 29 | 'request' => $request->all(), 30 | 'ip' => $request->ip(), 31 | 'method' => $request->method(), 32 | 'url' => $request->url() 33 | ]); 34 | return response()->json(['success' => true, 'data' => $request->all()]); 35 | })->name('debug.form'); 36 | } 37 | 38 | // Admin Routes 39 | Route::middleware(['auth', 'verified'])->prefix('admin')->group(function () { 40 | Route::get('/', [AdminController::class, 'dashboard'])->name('admin.dashboard'); 41 | Route::get('/sites', [SiteController::class, 'index'])->name('admin.sites.index'); 42 | // Custom routes for sites to use UUID for viewing 43 | Route::get('/sites/create', [SiteController::class, 'create'])->name('admin.sites.create'); 44 | Route::post('/sites', [SiteController::class, 'store'])->name('admin.sites.store'); 45 | Route::get('/sites/{uuid}', [SiteController::class, 'show'])->name('admin.sites.show'); 46 | Route::post('/sites/{site}/toggle-public', [SiteController::class, 'togglePublic'])->name('admin.sites.toggle-public'); 47 | Route::delete('/sites/{site}', [SiteController::class, 'destroy'])->name('admin.sites.destroy'); 48 | Route::get('/servers', [AdminController::class, 'servers'])->name('admin.servers'); 49 | 50 | // Server Management Routes 51 | Route::prefix('server-management')->name('admin.server-management.')->group(function () { 52 | Route::get('/selected-servers', [ServerManagementController::class, 'getSelectedServers'])->name('get-selected'); 53 | Route::post('/add-server', [ServerManagementController::class, 'addServer'])->name('add'); 54 | Route::post('/remove-server', [ServerManagementController::class, 'removeServer'])->name('remove'); 55 | Route::post('/update-phpmyadmin-url', [ServerManagementController::class, 'updatePhpMyAdminUrl'])->name('update-phpmyadmin-url'); 56 | }); 57 | 58 | // Settings routes 59 | Route::get('/settings', [SettingsController::class, 'index'])->name('admin.settings.index'); 60 | Route::post('/settings', [SettingsController::class, 'update'])->name('admin.settings.update'); 61 | Route::post('/settings/test-email', [SettingsController::class, 'testEmail'])->name('admin.settings.test-email'); 62 | Route::post('/settings/test-serveravatar-api', [SettingsController::class, 'testServerAvatarApi'])->name('admin.settings.test-serveravatar-api'); 63 | Route::post('/settings/test-cloudflare-api', [SettingsController::class, 'testCloudflareApi'])->name('admin.settings.test-cloudflare-api'); 64 | 65 | // Profile Routes 66 | Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 67 | Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 68 | Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 69 | }); 70 | 71 | require __DIR__.'/auth.php'; 72 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | ], 11 | 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 16 | }, 17 | }, 18 | }, 19 | 20 | plugins: [forms], 21 | }; 22 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | 20 | public function test_users_can_authenticate_using_the_login_screen(): void 21 | { 22 | $user = User::factory()->create(); 23 | 24 | $response = $this->post('/login', [ 25 | 'email' => $user->email, 26 | 'password' => 'password', 27 | ]); 28 | 29 | $this->assertAuthenticated(); 30 | $response->assertRedirect(route('dashboard', absolute: false)); 31 | } 32 | 33 | public function test_users_can_not_authenticate_with_invalid_password(): void 34 | { 35 | $user = User::factory()->create(); 36 | 37 | $this->post('/login', [ 38 | 'email' => $user->email, 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $this->assertGuest(); 43 | } 44 | 45 | public function test_users_can_logout(): void 46 | { 47 | $user = User::factory()->create(); 48 | 49 | $response = $this->actingAs($user)->post('/logout'); 50 | 51 | $this->assertGuest(); 52 | $response->assertRedirect('/'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 19 | 20 | $response = $this->actingAs($user)->get('/verify-email'); 21 | 22 | $response->assertStatus(200); 23 | } 24 | 25 | public function test_email_can_be_verified(): void 26 | { 27 | $user = User::factory()->unverified()->create(); 28 | 29 | Event::fake(); 30 | 31 | $verificationUrl = URL::temporarySignedRoute( 32 | 'verification.verify', 33 | now()->addMinutes(60), 34 | ['id' => $user->id, 'hash' => sha1($user->email)] 35 | ); 36 | 37 | $response = $this->actingAs($user)->get($verificationUrl); 38 | 39 | Event::assertDispatched(Verified::class); 40 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 41 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 42 | } 43 | 44 | public function test_email_is_not_verified_with_invalid_hash(): void 45 | { 46 | $user = User::factory()->unverified()->create(); 47 | 48 | $verificationUrl = URL::temporarySignedRoute( 49 | 'verification.verify', 50 | now()->addMinutes(60), 51 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 52 | ); 53 | 54 | $this->actingAs($user)->get($verificationUrl); 55 | 56 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->get('/confirm-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_password_can_be_confirmed(): void 23 | { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response->assertRedirect(); 31 | $response->assertSessionHasNoErrors(); 32 | } 33 | 34 | public function test_password_is_not_confirmed_with_invalid_password(): void 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_reset_password_link_can_be_requested(): void 23 | { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class); 31 | } 32 | 33 | public function test_reset_password_screen_can_be_rendered(): void 34 | { 35 | Notification::fake(); 36 | 37 | $user = User::factory()->create(); 38 | 39 | $this->post('/forgot-password', ['email' => $user->email]); 40 | 41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 42 | $response = $this->get('/reset-password/'.$notification->token); 43 | 44 | $response->assertStatus(200); 45 | 46 | return true; 47 | }); 48 | } 49 | 50 | public function test_password_can_be_reset_with_valid_token(): void 51 | { 52 | Notification::fake(); 53 | 54 | $user = User::factory()->create(); 55 | 56 | $this->post('/forgot-password', ['email' => $user->email]); 57 | 58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 59 | $response = $this->post('/reset-password', [ 60 | 'token' => $notification->token, 61 | 'email' => $user->email, 62 | 'password' => 'password', 63 | 'password_confirmation' => 'password', 64 | ]); 65 | 66 | $response 67 | ->assertSessionHasNoErrors() 68 | ->assertRedirect(route('login')); 69 | 70 | return true; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | $response = $this 19 | ->actingAs($user) 20 | ->from('/profile') 21 | ->put('/password', [ 22 | 'current_password' => 'password', 23 | 'password' => 'new-password', 24 | 'password_confirmation' => 'new-password', 25 | ]); 26 | 27 | $response 28 | ->assertSessionHasNoErrors() 29 | ->assertRedirect('/profile'); 30 | 31 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 32 | } 33 | 34 | public function test_correct_password_must_be_provided_to_update_password(): void 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this 39 | ->actingAs($user) 40 | ->from('/profile') 41 | ->put('/password', [ 42 | 'current_password' => 'wrong-password', 43 | 'password' => 'new-password', 44 | 'password_confirmation' => 'new-password', 45 | ]); 46 | 47 | $response 48 | ->assertSessionHasErrorsIn('updatePassword', 'current_password') 49 | ->assertRedirect('/profile'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 15 | 16 | $response->assertStatus(200); 17 | } 18 | 19 | public function test_new_users_can_register(): void 20 | { 21 | $response = $this->post('/register', [ 22 | 'name' => 'Test User', 23 | 'email' => 'test@example.com', 24 | 'password' => 'password', 25 | 'password_confirmation' => 'password', 26 | ]); 27 | 28 | $this->assertAuthenticated(); 29 | $response->assertRedirect(route('dashboard', absolute: false)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this 18 | ->actingAs($user) 19 | ->get('/profile'); 20 | 21 | $response->assertOk(); 22 | } 23 | 24 | public function test_profile_information_can_be_updated(): void 25 | { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->patch('/profile', [ 31 | 'name' => 'Test User', 32 | 'email' => 'test@example.com', 33 | ]); 34 | 35 | $response 36 | ->assertSessionHasNoErrors() 37 | ->assertRedirect('/profile'); 38 | 39 | $user->refresh(); 40 | 41 | $this->assertSame('Test User', $user->name); 42 | $this->assertSame('test@example.com', $user->email); 43 | $this->assertNull($user->email_verified_at); 44 | } 45 | 46 | public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void 47 | { 48 | $user = User::factory()->create(); 49 | 50 | $response = $this 51 | ->actingAs($user) 52 | ->patch('/profile', [ 53 | 'name' => 'Test User', 54 | 'email' => $user->email, 55 | ]); 56 | 57 | $response 58 | ->assertSessionHasNoErrors() 59 | ->assertRedirect('/profile'); 60 | 61 | $this->assertNotNull($user->refresh()->email_verified_at); 62 | } 63 | 64 | public function test_user_can_delete_their_account(): void 65 | { 66 | $user = User::factory()->create(); 67 | 68 | $response = $this 69 | ->actingAs($user) 70 | ->delete('/profile', [ 71 | 'password' => 'password', 72 | ]); 73 | 74 | $response 75 | ->assertSessionHasNoErrors() 76 | ->assertRedirect('/'); 77 | 78 | $this->assertGuest(); 79 | $this->assertNull($user->fresh()); 80 | } 81 | 82 | public function test_correct_password_must_be_provided_to_delete_account(): void 83 | { 84 | $user = User::factory()->create(); 85 | 86 | $response = $this 87 | ->actingAs($user) 88 | ->from('/profile') 89 | ->delete('/profile', [ 90 | 'password' => 'wrong-password', 91 | ]); 92 | 93 | $response 94 | ->assertSessionHasErrorsIn('userDeletion', 'password') 95 | ->assertRedirect('/profile'); 96 | 97 | $this->assertNotNull($user->fresh()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/SiteExpirationTest.php: -------------------------------------------------------------------------------- 1 | 'default_deletion_time'], 22 | ['meta_value' => '24'] // 24 hours 23 | ); 24 | 25 | // Create a selected server first (to satisfy foreign key constraints) 26 | $server = \App\Models\SelectedServer::create([ 27 | 'name' => 'Test Server', 28 | 'server_id' => 123, // Must be an integer 29 | 'ip_address' => '192.168.1.1', 30 | 'connection_status' => 'connected', 31 | ]); 32 | 33 | // Create a new site without setting expires_at 34 | $site = Site::create([ 35 | 'name' => 'Test Site', 36 | 'domain' => 'test.example.com', 37 | 'status' => 'active', 38 | 'selected_server_id' => $server->id, 39 | 'server_id' => 'server-123', 40 | 'site_data' => ['test' => true], 41 | ]); 42 | 43 | // Check that expires_at was set and is approximately 24 hours from now 44 | $this->assertNotNull($site->expires_at); 45 | 46 | // Allow 5 second tolerance for test execution 47 | $difference = now()->addHours(24)->diffInSeconds($site->expires_at, false); 48 | $this->assertLessThanOrEqual(5, abs($difference), 'Expiration time should be 24 hours from now.'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: ['resources/css/app.css', 'resources/js/app.js'], 8 | refresh: true, 9 | }), 10 | ], 11 | }); 12 | --------------------------------------------------------------------------------