├── .nvmrc ├── public ├── favicon.ico ├── robots.txt ├── images │ ├── logo.png │ └── logo-dark.png ├── vendor │ └── telescope │ │ ├── favicon.ico │ │ └── mix-manifest.json └── .htaccess ├── database ├── .gitignore ├── seeders │ ├── RoleSeeder.php │ ├── SettingSeeder.php │ ├── DatabaseSeeder.php │ └── FinancialMetricsSeeder.php └── migrations │ ├── 2025_12_08_071401_update_users_table_with_account_locked.php │ ├── 2025_11_27_122645_update_users_table_with_core_settings.php │ ├── 2014_10_12_100000_create_password_reset_tokens_table.php │ ├── 2025_02_02_205811_create_settings_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2025_04_26_181756_create_sessions_table.php │ ├── 2025_06_16_075701_create_financial_metrics_table.php │ ├── 2025_02_02_183354_create_personalisations_table.php │ ├── 2025_02_24_161810_create_login_history_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2024_10_22_135647_create_cache_table.php │ ├── 2025_12_04_035619_update_users_table_with_delete_settings.php │ ├── 2025_07_06_171547_create_system_notices_table.php │ ├── 2014_10_12_200000_add_two_factor_columns_to_users_table.php │ ├── 2025_12_12_092732_create_app_notification_reads_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2025_12_12_084613_create_app_notifications_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ └── 2025_06_26_082925_create_health_tables.php ├── bootstrap ├── cache │ └── .gitignore └── providers.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── private │ │ └── .gitignore │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── resources ├── views │ ├── vendor │ │ ├── authentication-log │ │ │ ├── .gitkeep │ │ │ └── emails │ │ │ │ ├── new.blade.php │ │ │ │ └── failed.blade.php │ │ ├── mail │ │ │ ├── text │ │ │ │ ├── footer.blade.php │ │ │ │ ├── panel.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ ├── table.blade.php │ │ │ │ ├── button.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── layout.blade.php │ │ │ │ └── message.blade.php │ │ │ └── html │ │ │ │ ├── table.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ ├── footer.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── panel.blade.php │ │ │ │ ├── message.blade.php │ │ │ │ ├── button.blade.php │ │ │ │ └── layout.blade.php │ │ ├── health │ │ │ ├── mail │ │ │ │ └── checkFailedNotification.blade.php │ │ │ ├── logo.blade.php │ │ │ └── list-cli.blade.php │ │ └── notifications │ │ │ └── email.blade.php │ ├── errors │ │ ├── 404.blade.php │ │ ├── 401.blade.php │ │ ├── 419.blade.php │ │ ├── 500.blade.php │ │ ├── 402.blade.php │ │ ├── 429.blade.php │ │ ├── 503.blade.php │ │ └── 403.blade.php │ └── emails │ │ ├── notifications │ │ └── cleanup-report.blade.php │ │ ├── magic-registration-link.blade.php │ │ ├── welcome.blade.php │ │ ├── verify-email-from-admin-triggered.blade.php │ │ ├── welcome-verified.blade.php │ │ ├── verify.blade.php │ │ ├── account-restored.blade.php │ │ ├── magic-link.blade.php │ │ └── goodbye-user-mail.blade.php ├── lang │ ├── en │ │ ├── app.php │ │ ├── pagination.php │ │ ├── auth.php │ │ └── passwords.php │ └── vendor │ │ ├── world │ │ ├── zh │ │ │ └── response.php │ │ ├── ja │ │ │ └── response.php │ │ ├── kr │ │ │ └── response.php │ │ ├── ro │ │ │ └── response.php │ │ ├── en │ │ │ └── response.php │ │ ├── ru │ │ │ └── response.php │ │ ├── ar │ │ │ └── response.php │ │ ├── de │ │ │ └── response.php │ │ ├── it │ │ │ └── response.php │ │ ├── pl │ │ │ └── response.php │ │ ├── tr │ │ │ └── response.php │ │ ├── bn │ │ │ └── response.php │ │ ├── br │ │ │ └── response.php │ │ ├── fr │ │ │ └── response.php │ │ ├── hr │ │ │ └── response.php │ │ ├── nl │ │ │ └── response.php │ │ ├── pt │ │ │ └── response.php │ │ ├── es │ │ │ └── response.php │ │ └── fa │ │ │ └── response.php │ │ └── authentication-log │ │ └── en │ │ └── messages.php ├── css │ ├── partials │ │ ├── cards.css │ │ ├── wells.css │ │ ├── typography.css │ │ ├── notifications.css │ │ ├── tables.css │ │ ├── tabs.css │ │ ├── pagination.css │ │ └── base.css │ └── app.css └── js │ ├── Components │ ├── Common │ │ ├── RolesBadges.vue │ │ ├── RoleBadge.vue │ │ └── NotificationTypeBadge.vue │ ├── Forms │ │ └── Switch.vue │ ├── Icons │ │ ├── FacebookIcon.vue │ │ └── GitHubIcon.vue │ └── DebouncedInput.vue │ ├── ziggy.js │ ├── echo.js │ ├── utils │ └── apiFetch.js │ ├── Pages │ ├── Admin │ │ └── Notifications │ │ │ └── AdminDeletedNotificaionsIndex.vue │ └── Auth │ │ └── ForgotPassword.vue │ └── app.js ├── postcss.config.js ├── tests ├── TestCase.php ├── CreatesApplication.php ├── Feature │ ├── AdminAuditControllerTest.php │ ├── AdminLoginHistoryControllerTest.php │ └── AdminPermissionRoleControllerTest.php └── Pest.php ├── .gitattributes ├── .prettierignore ├── app ├── Http │ ├── Controllers │ │ ├── Pages │ │ │ ├── PageController.php │ │ │ └── ChartsController.php │ │ ├── Controller.php │ │ ├── Auth │ │ │ └── LogoutController.php │ │ ├── Admin │ │ │ ├── AdminUsersVerificationController.php │ │ │ └── AdminSettingController.php │ │ └── Notifications │ │ │ └── AppNotificationPageController.php │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── ForcePasswordChange.php │ │ ├── HandleAppearance.php │ │ ├── DisableAccount.php │ │ ├── EnsureAccountNotLocked.php │ │ ├── EnsureIsLocalTesting.php │ │ ├── RequireAuthForVerification.php │ │ ├── TrustProxies.php │ │ ├── EmailVerificationCheck.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── HandleSocialiteProviders.php │ │ ├── CheckPasswordExpiry.php │ │ └── RequireTwoFactor.php │ └── Requests │ │ ├── EmailVerificationRequest.php │ │ ├── Notifications │ │ ├── ExpireNotificationsRequest.php │ │ ├── BulkNotificationsRequest.php │ │ └── ListNotificationsRequest.php │ │ └── Admin │ │ └── Notifications │ │ ├── StoreAdminAppNotificationRequest.php │ │ └── UpdateAdminAppNotificationRequest.php ├── Traits │ ├── UserCachingTrait.php │ ├── HasProtectedRoles.php │ ├── HasProtectedPermission.php │ └── PersonalisationsHelper.php ├── Listeners │ ├── SendRestoredEmail.php │ ├── LogSuccessfulLogout.php │ ├── SendWelcomeEmailVerified.php │ ├── SendGoodbyeEmail.php │ ├── SendWelcomeEmail.php │ ├── LogFailedLogin.php │ ├── LogSuccessfulLogin.php │ └── CreateAppNotification.php ├── Providers │ ├── CachedEloquentUserProvider.php │ ├── AppServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── VerifyCustomEmailServiceProvider.php │ ├── AppCachedEloquentUserServiceProvider.php │ ├── AuthServiceProvider.php │ ├── DebugBarServiceProvider.php │ ├── RouteServiceProvider.php │ ├── AppHealthServiceProvider.php │ └── AppPersonalisationServiceProvider.php ├── Events │ ├── UserRestored.php │ ├── UserDeleted.php │ ├── AppNotificationRequested.php │ ├── AppNotificationsBulkChanged.php │ └── AppNotificationStateChanged.php ├── Actions │ └── Fortify │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── RegisterResponse.php │ │ ├── UpdateUserPassword.php │ │ └── CreateNewUser.php ├── Models │ ├── Setting.php │ ├── LoginHistory.php │ ├── FinancialMetric.php │ ├── AppNotificationRead.php │ ├── Personalisation.php │ └── AppNotification.php ├── Mail │ ├── MagicRegistrationLink.php │ ├── MagicLoginLink.php │ ├── WelcomeMail.php │ ├── WelcomeMailVerified.php │ ├── RestoredUserMail.php │ ├── NotificationsCleanupReport.php │ ├── VerifyMail.php │ └── GoodbyeUserMail.php ├── Jobs │ ├── SendScheduledAppNotificationsJob.php │ ├── SoftDeleteExpiredAppNotificationsJob.php │ ├── DestroySoftDeletedUsersJob.php │ └── CleanupDeletedAppNotificationsJob.php ├── Console │ └── Commands │ │ ├── SoftDeleteExpiredAppNotificationsCommand.php │ │ ├── RunUsersAutoDestroy.php │ │ ├── SendScheduledAppNotificationsCommand.php │ │ └── CleanupDeletedAppNotificationsCommand.php ├── Services │ └── Notifications │ │ ├── AppNotificationAutoExpireService.php │ │ ├── AppNotificationCleanupService.php │ │ └── AppNotificationScheduledSendService.php ├── Observers │ ├── UserObserver.php │ └── PersonalisationObserver.php └── Policies │ └── SettingPolicy.php ├── config ├── socialite.php ├── roles.php ├── seeders.php ├── cors.php ├── view.php ├── inertia.php └── notify.php ├── artisan ├── tailwind.config.js ├── routes ├── api.php ├── console.php └── channels.php ├── .editorconfig ├── .eslintrc.json ├── LICENSE.md ├── SECURITY.md ├── .gitignore ├── phpunit.xml ├── CONTRIBUTING.md └── .prettierrc /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/views/vendor/authentication-log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /resources/lang/en/app.php: -------------------------------------------------------------------------------- 1 | 2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 | 4 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otatechie/guacpanel-tailwind/HEAD/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /resources/css/partials/cards.css: -------------------------------------------------------------------------------- 1 | .danger-card { 2 | @apply mb-4 rounded-lg border border-red-200 p-4 sm:p-6 dark:border-red-800; 3 | } 4 | -------------------------------------------------------------------------------- /resources/views/errors/404.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Not Found')) 4 | @section('code', '404') 5 | @section('message', __('Not Found')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/401.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Unauthorized')) 4 | @section('code', '401') 5 | @section('message', __('Unauthorized')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/419.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Page Expired')) 4 | @section('code', '419') 5 | @section('message', __('Page Expired')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Server Error')) 4 | @section('code', '500') 5 | @section('message', __('Server Error')) 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/errors/402.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Payment Required')) 4 | @section('code', '402') 5 | @section('message', __('Payment Required')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/429.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Too Many Requests')) 4 | @section('code', '429') 5 | @section('message', __('Too Many Requests')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Service Unavailable')) 4 | @section('code', '503') 5 | @section('message', __('Service Unavailable')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/403.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Forbidden')) 4 | @section('code', '403') 5 | @section('message', __($exception->getMessage() ?: 'Forbidden')) 6 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ Illuminate\Mail\Markdown::parse($slot) }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header ?? '') !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer ?? '') !!} 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=a04a99f77a55ffcecde23cd7304b481b", 3 | "/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9", 4 | "/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf" 5 | } 6 | -------------------------------------------------------------------------------- /resources/css/partials/wells.css: -------------------------------------------------------------------------------- 1 | .well { 2 | @apply rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800; 3 | } 4 | 5 | .danger-well { 6 | @apply rounded-lg border border-red-200 bg-red-50 p-3 sm:p-4 dark:border-red-700 dark:bg-red-900/20; 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | public/build 4 | public/hot 5 | storage 6 | bootstrap/cache 7 | *.min.js 8 | *.min.css 9 | composer.json 10 | packages.json 11 | resources/js/components/ui/* 12 | resources/views/mail/* 13 | resources/views/email/* 14 | resources/views/vendor/* 15 | 16 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/Http/Controllers/Pages/PageController.php: -------------------------------------------------------------------------------- 1 | id); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/views/vendor/health/mail/checkFailedNotification.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # Laravel Health 3 | 4 | {{ __('health::notifications.check_failed_mail_body') }} 5 | 6 | @foreach($results as $result) 7 | - {{ $result->check->getLabel() }}: {{ $result->getNotificationMessage() }} 8 | @endforeach 9 | 10 | @endcomponent 11 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['url']) 2 | 3 | 4 | 5 | @if (trim($slot) === 'Laravel') 6 | 7 | @else 8 | {{ $slot }} 9 | @endif 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 2 | import RoleBadge from '@js/Components/Common/RoleBadge.vue' 3 | 4 | const props = defineProps({ 5 | roles: { 6 | type: Array, 7 | default: () => [], 8 | }, 9 | }) 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /app/Listeners/SendRestoredEmail.php: -------------------------------------------------------------------------------- 1 | user->email)->queue(new RestoredUserMail($event->user)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/socialite.php: -------------------------------------------------------------------------------- 1 | '{provider}', 14 | ]; 15 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/zh/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => '内部服务器错误', 6 | 'record_not_found' => '没有找到:attribute!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => '电话|电话', 10 | 'country' => '国家|国家', 11 | 'city' => '城市|城市', 12 | 'state' => '县|县', 13 | 'timezone' => '时区|时区', 14 | 'currency' => '货币|货币', 15 | 'language' => '语言|语言', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Listeners/LogSuccessfulLogout.php: -------------------------------------------------------------------------------- 1 | user)) { 15 | $this->resetUserCache($event->user); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/ja/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => '内部サーバーエラー', 6 | 'record_not_found' => 'いいえ :attribute が見つかりませんでした!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => '電話|電話', 10 | 'country' => '国|国', 11 | 'city' => '市|都市', 12 | 'state' => '州|州', 13 | 'timezone' => 'タイムゾーン|時間帯', 14 | 'currency' => '通貨|通貨', 15 | 'language' => '言語|言語', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/kr/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => '인터넷 서버 오류', 6 | 'record_not_found' => '아니오 :attribute 을(를) 찾았습니다!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => '핸드폰|전화', 10 | 'country' => '국가|국가', 11 | 'city' => '도시|도시', 12 | 'state' => '군|군', 13 | 'timezone' => '시간대|시간대', 14 | 'currency' => '통화|통화', 15 | 'language' => '언어|언어', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'magic/*', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Listeners/SendWelcomeEmailVerified.php: -------------------------------------------------------------------------------- 1 | user; 14 | 15 | Mail::to($user->email)->send(new WelcomeMailVerified($user)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Providers/CachedEloquentUserProvider.php: -------------------------------------------------------------------------------- 1 | remember('user_'.$identifier, now()->addDay(), function () use ($identifier) { 12 | return parent::retrieveById($identifier); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Listeners/SendGoodbyeEmail.php: -------------------------------------------------------------------------------- 1 | user->email)->queue(new GoodbyeUserMail($event->user, $event->url)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | register(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/ro/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Internal Server Error', 6 | 'record_not_found' => 'Nu :attribute a fost găsit!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefon|telefoane', 10 | 'country' => 'țară|țări', 11 | 'city' => 'oraș|orase', 12 | 'state' => 'judet|judete', 13 | 'timezone' => 'fus orar|fusuri orare', 14 | 'currency' => 'valută|valute', 15 | 'language' => 'limba|limbi', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts(): array 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/en/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Internal Server Error', 6 | 'record_not_found' => 'No :attribute was found!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'phone|phones', 10 | 'country' => 'country|countries', 11 | 'city' => 'city|cities', 12 | 'state' => 'state|states', 13 | 'timezone' => 'timezone|timezones', 14 | 'currency' => 'currency|currencies', 15 | 'language' => 'language|languages', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/ru/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Внутренняя Ошибка Сервера', 6 | 'record_not_found' => 'Нет :attribute найдено!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'телефон|телефоны', 10 | 'country' => 'страна|страны', 11 | 'city' => 'город|города', 12 | 'state' => 'округ|графства', 13 | 'timezone' => 'часовой пояс|часовые пояса', 14 | 'currency' => 'валюта|валюты', 15 | 'language' => 'язык|языки', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/ar/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'خطأ في الخادم', 6 | 'record_not_found' => 'لم يتم العثور على :attribute!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'الهواتف|هاتف', 10 | 'country' => 'البلدان|البلد', 11 | 'city' => 'المدن|المدينة', 12 | 'state' => 'المقاطعة|المقاطعات', 13 | 'timezone' => 'المناطق الزمنية|المنطقة الزمنية', 14 | 'currency' => 'العملات|عملة', 15 | 'language' => 'اللغات|اللغة', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson() ? null : route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/de/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Interner Serverfehler', 6 | 'record_not_found' => 'Es wurde kein :attribute gefunden!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'Telefon|Telefone', 10 | 'country' => 'Land|Länder', 11 | 'city' => 'Stadt|Städte', 12 | 'state' => 'Zustand|Zustände', 13 | 'timezone' => 'Zeitzone|Zeitzonen', 14 | 'currency' => 'Währung|Währungen', 15 | 'language' => 'Sprache|Sprachen', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/it/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Errore interno del server', 6 | 'record_not_found' => 'Nessun elemento di :attribute trovato!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefono|telefoni', 10 | 'country' => 'paese|paesi', 11 | 'city' => 'città|città', 12 | 'state' => 'stato|stati', 13 | 'timezone' => 'fuso orario|fusi orari', 14 | 'currency' => 'valuta|valute', 15 | 'language' => 'lingua|lingue', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/pl/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Wewnętrzny błąd serwera', 6 | 'record_not_found' => 'Nie znaleziono :attribute!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefon|telefony', 10 | 'country' => 'kraj|kraje', 11 | 'city' => 'miasto|miasta', 12 | 'polish' => 'hrabstwo|hrabstwa', 13 | 'timezone' => 'strefa czasowa|strefy czasowe', 14 | 'currency' => 'waluta|waluty', 15 | 'language' => 'język|języki', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/tr/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'İç Sunucu Hatası', 6 | 'record_not_found' => 'Hayır :attribute bulundu!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefon|telefonlar', 10 | 'country' => 'ülke|ülkeler', 11 | 'city' => 'şehir|şehirler', 12 | 'state' => 'eyalet|eyalet', 13 | 'timezone' => 'saat dilimi|zaman dilimleri', 14 | 'currency' => 'para birimi|para birimleri', 15 | 'language' => 'dil|diller', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Events/UserRestored.php: -------------------------------------------------------------------------------- 1 | user = $user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/bn/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'অভ্যন্তরীণ সার্ভার ত্রুটি', 6 | 'record_not_found' => ':attribute রেকর্ডটি পাওয়া যায় নি!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'ফোন|ফোনগুলি', 10 | 'country' => 'দেশ|দেশগুলি', 11 | 'city' => 'শহর|শহরগুলি', 12 | 'state' => 'কাউন্টি|কাউন্টিগুলি', 13 | 'timezone' => 'সময় অঞ্চল|সময় অঞ্চলগুলো', 14 | 'currency' => 'মুদ্রা|মুদ্রাগুলি', 15 | 'language' => 'ভাষা|ভাষাগুলো', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/br/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Erro do Servidor Interno', 6 | 'record_not_found' => 'Não :attribute foi encontrado!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefone|telefones', 10 | 'country' => 'país|países', 11 | 'city' => 'cidade|cidades', 12 | 'state' => 'estado|estados', 13 | 'timezone' => 'fuso horário|fusos horários', 14 | 'currency' => 'moeda|moedas', 15 | 'language' => 'língua|línguas', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/fr/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Erreur Interne du Serveur', 6 | 'record_not_found' => 'Non :attribute a été trouvé!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'téléphone|téléphones', 10 | 'country' => 'pays|pays', 11 | 'city' => 'ville|villes', 12 | 'state' => 'état|états', 13 | 'timezone' => 'fuseau horaire|fuseaux horaires', 14 | 'currency' => 'devise|devises', 15 | 'language' => 'langue|langues', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/hr/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Interna pogreška poslužitelja', 6 | 'record_not_found' => ':attribute nije pronađeno!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefon|telefoni', 10 | 'country' => 'zemlja|zemlje', 11 | 'city' => 'grad|gradovi', 12 | 'state' => 'stanje|države', 13 | 'timezone' => 'vremenska zona|vremenske zone', 14 | 'currency' => 'valuta|valute', 15 | 'language' => 'jezik|jezici', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/nl/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Internal serverfout', 6 | 'record_not_found' => 'Geen :attribute kon gevonden worden!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefoon|telefoons', 10 | 'country' => 'land|landen', 11 | 'city' => 'plaats|plaatsen', 12 | 'state' => 'staat|staten', 13 | 'timezone' => 'tijdzone|tijdzones', 14 | 'currency' => 'munteenheid|munteenheden', 15 | 'language' => 'taal|talen', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/pt/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Erro do Servidor Interno', 6 | 'record_not_found' => 'Não :attribute foi encontrado!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'telefone|telefones', 10 | 'country' => 'país|países', 11 | 'city' => 'cidade|cidades', 12 | 'state' => 'estados|estado', 13 | 'timezone' => 'fuso horário|fusos horários', 14 | 'currency' => 'moeda|moedas', 15 | 'language' => 'língua|línguas', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /database/seeders/RoleSeeder.php: -------------------------------------------------------------------------------- 1 | first(); 17 | if ($role === null) { 18 | Role::create(['name' => $rolename]); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/es/response.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'server_error' => 'Error de servidor interno', 6 | 'record_not_found' => '¡No se encontró ningún :attribute!', 7 | ], 8 | 'attributes' => [ 9 | 'phone' => 'teléfono|telefonos', 10 | 'country' => 'país|países', 11 | 'city' => 'ciudad|ciudades', 12 | 'state' => 'estado|estados', 13 | 'timezone' => 'zona horaria|zonas horarias', 14 | 'currency' => 'divisa|monedas', 15 | 'language' => 'idioma|idiomas', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | |string> 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', Password::default(), 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /database/seeders/SettingSeeder.php: -------------------------------------------------------------------------------- 1 | false, 17 | 'password_expiry' => false, 18 | 'two_factor_authentication' => false, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import forms from "@tailwindcss/forms"; 3 | 4 | export default { 5 | darkMode: "class", 6 | content: [ 7 | "./resources/**/*.blade.php", 8 | "./resources/**/*.js", 9 | "./resources/**/*.vue", 10 | "./pages/**/*.{html,js}", 11 | "./components/**/*.{html,js}", 12 | ], 13 | theme: { 14 | extend: { 15 | screens: { 16 | xxs: "360px", 17 | xs: "480px", 18 | }, 19 | }, 20 | }, 21 | plugins: [forms], 22 | }; 23 | -------------------------------------------------------------------------------- /app/Models/Setting.php: -------------------------------------------------------------------------------- 1 | 'boolean', 16 | 'password_expiry' => 'boolean', 17 | 'passwordless_login' => 'boolean', 18 | 'two_factor_authentication' => 'boolean', 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /config/roles.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'superuser', 15 | 'user', 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/css/partials/typography.css: -------------------------------------------------------------------------------- 1 | .text-primary { 2 | color: var(--primary-color); 3 | } 4 | 5 | .text-secondary { 6 | color: var(--secondary-color); 7 | } 8 | 9 | .text-info { 10 | color: var(--info-color); 11 | } 12 | 13 | .text-success { 14 | color: var(--success-color); 15 | } 16 | 17 | .text-warning { 18 | color: var(--warning-color); 19 | } 20 | 21 | .text-danger { 22 | color: var(--error-color); 23 | } 24 | 25 | .text-xxs { 26 | font-size: 0.65em; 27 | } 28 | 29 | .text-xxxs { 30 | font-size: 0.5em; 31 | } 32 | -------------------------------------------------------------------------------- /resources/lang/vendor/world/fa/response.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'server_error' => 'خطا در سرور', 5 | 'record_not_found' => 'رکورد :attribute یافت نشد!', 6 | ], 7 | 'attributes' => [ 8 | 'phone' => 'تلفن‌ها|تلفن', 9 | 'country' => 'کشورها|کشور', 10 | 'city' => 'شهرها|شهر', 11 | 'state' => 'استان‌ها|استان', 12 | 'timezone' => 'مناطق زمانی|منطقه زمانی', 13 | 'currency' => 'ارزها|ارز', 14 | 'language' => 'زبان‌ها|زبان', 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LogoutController.php: -------------------------------------------------------------------------------- 1 | logout(); 14 | 15 | $request->session()->invalidate(); 16 | 17 | $request->session()->regenerateToken(); 18 | 19 | return redirect('/login'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Requests/EmailVerificationRequest.php: -------------------------------------------------------------------------------- 1 | route('id')); 13 | 14 | $this->setUserResolver(function () use ($user) { 15 | return $user; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/VerifyCustomEmailServiceProvider.php: -------------------------------------------------------------------------------- 1 | to($notifiable->email, $notifiable->name ?? null); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Events/UserDeleted.php: -------------------------------------------------------------------------------- 1 | user = $user; 22 | $this->url = $url; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Listeners/SendWelcomeEmail.php: -------------------------------------------------------------------------------- 1 | user; 19 | 20 | Mail::to($user->email)->send(new WelcomeMail($user)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/AppCachedEloquentUserServiceProvider.php: -------------------------------------------------------------------------------- 1 | provider('cachedEloquentUser', function (Application $application, array $config) { 13 | return new CachedEloquentUserProvider( 14 | $application['hash'], 15 | $config['model'] 16 | ); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | call([ 16 | RoleSeeder::class, 17 | SettingSeeder::class, 18 | PermissionRoleSeeder::class, 19 | UserSeeder::class, 20 | FinancialMetricsSeeder::class, 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Mail/MagicRegistrationLink.php: -------------------------------------------------------------------------------- 1 | url = $url; 19 | } 20 | 21 | public function build() 22 | { 23 | return $this->markdown('emails.magic-registration-link') 24 | ->subject('Complete your registration'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/ForcePasswordChange.php: -------------------------------------------------------------------------------- 1 | check() && auth()->user()->force_password_change) { 14 | session()->flash('warning', __('notifications.account.force_pw_change')); 15 | 16 | return redirect()->route('user.password.change'); 17 | } 18 | 19 | return $next($request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Requests/Notifications/ExpireNotificationsRequest.php: -------------------------------------------------------------------------------- 1 | user(); 12 | 13 | return (bool) $user && $user->can('manage-notifications'); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | 'ids' => ['required', 'array', 'min:1', 'max:500'], 20 | 'ids.*' => ['string'], 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Traits/HasProtectedRoles.php: -------------------------------------------------------------------------------- 1 | getProtectedRoles()); 15 | 16 | return in_array(strtolower(trim($roleName)), $protectedRoles); 17 | } 18 | 19 | protected function getProtectedRolesForValidation(): string 20 | { 21 | return implode(',', $this->getProtectedRoles()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | user(); 19 | // })->middleware('auth:sanctum'); 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleAppearance.php: -------------------------------------------------------------------------------- 1 | cookie('appearance') ?? 'system'); 20 | 21 | return $next($request); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Events/AppNotificationRequested.php: -------------------------------------------------------------------------------- 1 | check() && auth()->user()->disable_account) { 14 | auth()->logout(); 15 | session()->flash('warning', __('notifications.account.disabled', ['email' => config('guacpanel.admin.support_email')])); 16 | 17 | return redirect()->route('login'); 18 | } 19 | 20 | return $next($request); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /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 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /resources/css/partials/notifications.css: -------------------------------------------------------------------------------- 1 | .notification-badge-error { 2 | @apply border-red-200 bg-red-50 text-red-700 dark:border-red-900/50 dark:bg-red-900/20 dark:text-red-300; 3 | } 4 | 5 | .notification-badge-warning { 6 | @apply border-yellow-200 bg-yellow-50 text-yellow-800 dark:border-yellow-900/50 dark:bg-yellow-900/20 dark:text-yellow-300; 7 | } 8 | 9 | .notification-badge-success { 10 | @apply border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/50 dark:bg-emerald-900/20 dark:text-emerald-300; 11 | } 12 | 13 | .notification-badge-default { 14 | @apply border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/50 dark:bg-blue-900/20 dark:text-blue-300; 15 | } 16 | -------------------------------------------------------------------------------- /app/Jobs/SendScheduledAppNotificationsJob.php: -------------------------------------------------------------------------------- 1 | sendDue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/lang/vendor/authentication-log/en/messages.php: -------------------------------------------------------------------------------- 1 | 'Login from a new device', 17 | 'content' => 'Your :app account logged in from a new device.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'url', 3 | 'color' => 'primary', 4 | 'align' => 'center', 5 | ]) 6 | 7 | 8 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureAccountNotLocked.php: -------------------------------------------------------------------------------- 1 | user(); 14 | 15 | abort_unless($user, 404); 16 | 17 | if ($user->account_locked) { 18 | auth()->logout(); 19 | 20 | return redirect()->route('login')->with('error', __('notifications.account.locked', ['email' => config('guacpanel.admin.support_email')])); 21 | } 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Listeners/LogFailedLogin.php: -------------------------------------------------------------------------------- 1 | user)) { 13 | LoginHistory::create([ 14 | 'user_type' => get_class($event->user), 15 | 'user_id' => $event->user->id, 16 | 'ip_address' => request()->ip(), 17 | 'user_agent' => request()->userAgent(), 18 | 'login_successful' => false, 19 | 'login_at' => now(), 20 | ]); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Listeners/LogSuccessfulLogin.php: -------------------------------------------------------------------------------- 1 | user)) { 13 | LoginHistory::create([ 14 | 'user_type' => get_class($event->user), 15 | 'user_id' => $event->user->id, 16 | 'ip_address' => request()->ip(), 17 | 'user_agent' => request()->userAgent(), 18 | 'login_successful' => true, 19 | 'login_at' => now(), 20 | ]); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Mail/MagicLoginLink.php: -------------------------------------------------------------------------------- 1 | url = $url; 20 | $this->isNewUser = $isNewUser; 21 | } 22 | 23 | public function build() 24 | { 25 | return $this->markdown('emails.magic-link') 26 | ->subject($this->isNewUser ? 'Complete Your Registration' : 'Your Magic Login Link'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Mail/WelcomeMail.php: -------------------------------------------------------------------------------- 1 | user = $user; 20 | } 21 | 22 | public function build(): self 23 | { 24 | return $this 25 | ->subject(trans('emails.welcome.subject', [ 26 | 'appname' => config('app.name'), 27 | ])) 28 | ->markdown('emails.welcome'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2025_12_08_071401_update_users_table_with_account_locked.php: -------------------------------------------------------------------------------- 1 | boolean('account_locked') 12 | ->default(0) 13 | ->after('disable_account'); 14 | }); 15 | } 16 | 17 | public function down(): void 18 | { 19 | Schema::table('users', function (Blueprint $table) { 20 | $table->dropColumn('account_locked'); 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureIsLocalTesting.php: -------------------------------------------------------------------------------- 1 | environment([ 16 | 'local', 17 | 'testing', 18 | ]), $abortTo); 19 | 20 | $user = $request->user(); 21 | abort_unless($user, $abortTo); 22 | // abort_unless($user->isSuperUser, $abortTo); 23 | // abort_unless($user?->can('manage-notifications'), $abortTo); 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/RequireAuthForVerification.php: -------------------------------------------------------------------------------- 1 | check()) { 15 | session()->put('url.intended', $request->fullUrl()); 16 | 17 | return redirect() 18 | ->route('login') 19 | ->with('warning', __('notifications.verify.login')); 20 | } 21 | } 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Mail/WelcomeMailVerified.php: -------------------------------------------------------------------------------- 1 | user = $user; 20 | } 21 | 22 | public function build(): self 23 | { 24 | return $this 25 | ->subject(trans('emails.welcome-verified.subject', [ 26 | 'appname' => config('app.name'), 27 | ])) 28 | ->markdown('emails.welcome-verified'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.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 | [*.php] 12 | charset = utf-8 13 | end_of_line = lf 14 | insert_final_newline = true 15 | indent_style = space 16 | indent_size = 4 17 | trim_trailing_whitespace = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.{yml,yaml}] 23 | indent_size = 2 24 | 25 | [docker-compose.yml] 26 | indent_size = 4 27 | 28 | [*.js] 29 | indent_style = space 30 | indent_size = 2 31 | 32 | [*.ts] 33 | indent_style = space 34 | indent_size = 2 35 | 36 | [*.json] 37 | indent_style = space 38 | indent_size = 4 39 | 40 | [*.vue] 41 | indent_style = space 42 | indent_size = 2 43 | -------------------------------------------------------------------------------- /resources/views/emails/notifications/cleanup-report.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.notifications_cleanup.greeting') 4 |

5 | 6 |

7 | @lang('emails.notifications_cleanup.line_one', ['appname' => config('app.name')]) 8 |

9 | 10 | 15 | 16 |

17 | @lang('emails.notifications_cleanup.goodbye') 18 |

19 | 20 |

21 | {{ config('app.name') }} 22 |

23 |
24 | -------------------------------------------------------------------------------- /app/Jobs/SoftDeleteExpiredAppNotificationsJob.php: -------------------------------------------------------------------------------- 1 | softDeleteExpired(); 22 | 23 | return (int) $result['soft_deleted']; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/LoginHistory.php: -------------------------------------------------------------------------------- 1 | 'boolean', 24 | 'login_at' => 'datetime', 25 | 'logout_at' => 'datetime', 26 | ]; 27 | 28 | public function user(): MorphTo 29 | { 30 | return $this->morphTo(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/js/ziggy.js: -------------------------------------------------------------------------------- 1 | const Ziggy = { 2 | url: 'http:\/\/localhost', 3 | port: null, 4 | defaults: {}, 5 | routes: { 6 | 'sanctum.csrf-cookie': { 7 | uri: 'sanctum\/csrf-cookie', 8 | methods: ['GET', 'HEAD'], 9 | }, 10 | 'ignition.healthCheck': { 11 | uri: '_ignition\/health-check', 12 | methods: ['GET', 'HEAD'], 13 | }, 14 | 'ignition.executeSolution': { 15 | uri: '_ignition\/execute-solution', 16 | methods: ['POST'], 17 | }, 18 | 'ignition.updateConfig': { 19 | uri: '_ignition\/update-config', 20 | methods: ['POST'], 21 | }, 22 | }, 23 | } 24 | if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { 25 | Object.assign(Ziggy.routes, window.Ziggy.routes) 26 | } 27 | export { Ziggy } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2025_11_27_122645_update_users_table_with_core_settings.php: -------------------------------------------------------------------------------- 1 | string('profile_image_type') 12 | ->default('avatar') 13 | ->nullable() 14 | ->after('disable_account'); 15 | }); 16 | } 17 | 18 | public function down(): void 19 | { 20 | Schema::table('users', function (Blueprint $table) { 21 | $table->dropColumn('profile_image_type'); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'Sorry, Please check your credentials and try again.', 17 | 'password' => 'Sorry, The provided password is incorrect.', 18 | 'throttle' => 'Sorry, Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/css/partials/tables.css: -------------------------------------------------------------------------------- 1 | /* Container border uses variables in both themes */ 2 | 3 | .container-border-table { 4 | background-color: var(--color-surface); 5 | border: 1px solid var(--color-border-strong); 6 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 7 | padding: 0.125rem; 8 | } 9 | 10 | /* Table Styles */ 11 | .table-header { 12 | padding-left: 1.5rem; 13 | padding-right: 1.5rem; 14 | padding-top: 0.75rem; 15 | padding-bottom: 0.75rem; 16 | text-align: left; 17 | font-size: 0.75rem; 18 | line-height: 1rem; 19 | font-weight: 500; 20 | color: var(--color-text-muted); 21 | text-transform: uppercase; 22 | letter-spacing: 0.05em; 23 | } 24 | 25 | .notifications-data-table { 26 | th, 27 | td { 28 | &:nth-child(6) { 29 | @apply min-w-80; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php: -------------------------------------------------------------------------------- 1 | string('email')->primary(); 15 | $table->string('token'); 16 | $table->timestamp('created_at')->nullable(); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('password_reset_tokens'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /resources/views/vendor/authentication-log/emails/new.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # @lang('Hello!') 3 | 4 | @lang('Your :app account logged in from a new device.', ['app' => config('app.name')]) 5 | 6 | > **@lang('Account:')** {{ $account->email }}
7 | > **@lang('Time:')** {{ $time->toCookieString() }}
8 | > **@lang('IP Address:')** {{ $ipAddress }}
9 | > **@lang('Browser:')** {{ $browser }}
10 | @if ($location && $location['default'] === false) 11 | > **@lang('Location:')** {{ $location['city'] ?? __('Unknown City') }}, {{ $location['state'], __('Unknown State') }} 12 | @endif 13 | 14 | @lang('If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.') 15 | 16 | @lang('Regards,')
17 | {{ config('app.name') }} 18 | @endcomponent 19 | -------------------------------------------------------------------------------- /resources/js/Components/Common/RoleBadge.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function reset(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'password' => $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Console/Commands/SoftDeleteExpiredAppNotificationsCommand.php: -------------------------------------------------------------------------------- 1 | softDeleteExpired(); 17 | 18 | $this->info('Soft-deleted notifications: '.$result['soft_deleted']); 19 | $this->info('Cutoff (now): '.$result['cutoff']->toDateTimeString()); 20 | 21 | return self::SUCCESS; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/vendor/authentication-log/emails/failed.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # @lang('Hello!') 3 | 4 | @lang('There has been a failed login attempt to your :app account.', ['app' => config('app.name')]) 5 | 6 | > **@lang('Account:')** {{ $account->email }}
7 | > **@lang('Time:')** {{ $time->toCookieString() }}
8 | > **@lang('IP Address:')** {{ $ipAddress }}
9 | > **@lang('Browser:')** {{ $browser }}
10 | @if ($location && $location['default'] === false) 11 | > **@lang('Location:')** {{ $location['city'] ?? __('Unknown City') }}, {{ $location['state'], __('Unknown State') }} 12 | @endif 13 | 14 | @lang('If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.') 15 | 16 | @lang('Regards,')
17 | {{ config('app.name') }} 18 | @endcomponent 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/EmailVerificationCheck.php: -------------------------------------------------------------------------------- 1 | handle($request, function ($request) use ($next) { 24 | return $next($request); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Requests/Notifications/BulkNotificationsRequest.php: -------------------------------------------------------------------------------- 1 | user(); 13 | 14 | return (bool) $user && ( 15 | $user->can('edit-notifications') || 16 | $user->can('delete-notifications') 17 | ); 18 | } 19 | 20 | public function rules(): array 21 | { 22 | return [ 23 | 'action' => ['required', 'string', Rule::in(['read', 'unread', 'dismiss', 'undismiss', 'delete'])], 24 | 'ids' => ['required', 'array', 'min:1', 'max:500'], 25 | 'ids.*' => ['string'], 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | hasRole('superuser') ? true : null; 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset.', 17 | 'sent' => 'We have emailed your password reset link.', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (string) $userId; 18 | }); 19 | 20 | Broadcast::channel('system', function ($user) { 21 | // Start permissive so you can test: 22 | return true; 23 | 24 | // Later you can restrict (e.g. admins only) 25 | // return $user->hasRole('admin'); 26 | }); 27 | -------------------------------------------------------------------------------- /app/Models/FinancialMetric.php: -------------------------------------------------------------------------------- 1 | 'date', 19 | 'amount' => 'decimal:2', 20 | ]; 21 | 22 | public function toSearchableArray(): array 23 | { 24 | return array_merge($this->toArray(), [ 25 | 'id' => (string) $this->id, 26 | 'created_at' => $this->created_at->timestamp, 27 | 'collection_name' => 'financial_metrics', 28 | 'amount' => (float) $this->amount, 29 | 'category' => $this->category, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 24 | return redirect(RouteServiceProvider::HOME); 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2025_02_02_205811_create_settings_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | $table->boolean('password_expiry')->default(0); 16 | $table->boolean('passwordless_login')->default(0); 17 | $table->boolean('two_factor_authentication')->default(0); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('settings'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app/Mail/RestoredUserMail.php: -------------------------------------------------------------------------------- 1 | user = $user; 22 | $this->restoreDateRaw = $this->user?->restore_date; 23 | $this->restoreDateParsed = $this->user?->restore_date->format('F d, Y @ g:i A'); 24 | } 25 | 26 | public function build() 27 | { 28 | return $this 29 | ->subject(__('emails.restored.subject', ['appname' => config('app.name')])) 30 | ->markdown('emails.account-restored'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/css/partials/tabs.css: -------------------------------------------------------------------------------- 1 | .tab-fade-enter-active, 2 | .tab-fade-leave-active { 3 | transition: all 0.3s ease; 4 | } 5 | 6 | .tab-fade-enter-from { 7 | opacity: 0; 8 | transform: translateX(20px); 9 | } 10 | 11 | .tab-fade-leave-to { 12 | opacity: 0; 13 | transform: translateX(-20px); 14 | } 15 | 16 | .slide-down-enter-active { 17 | transition: all 0.3s ease-out; 18 | } 19 | 20 | .slide-down-leave-active { 21 | transition: all 0.2s ease-in; 22 | } 23 | 24 | .slide-down-enter-from { 25 | opacity: 0; 26 | max-height: 0; 27 | transform: translateY(-10px); 28 | } 29 | 30 | .slide-down-enter-to { 31 | opacity: 1; 32 | max-height: 1000px; 33 | transform: translateY(0); 34 | } 35 | 36 | .slide-down-leave-from { 37 | opacity: 1; 38 | max-height: 1000px; 39 | transform: translateY(0); 40 | } 41 | 42 | .slide-down-leave-to { 43 | opacity: 0; 44 | max-height: 0; 45 | transform: translateY(-10px); 46 | } 47 | -------------------------------------------------------------------------------- /config/seeders.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'superAdmin' => [ 7 | 'enabled' => env('SEED_SUPER_ADMIN_USER_ENABLED', false), 8 | 'name' => env('SEED_SUPER_ADMIN_USER_NAME', 'Ota'), 9 | 'email' => env('SEED_SUPER_ADMIN_USER_EMAIL', 'ota@example.com'), 10 | 'password' => env('SEED_SUPER_ADMIN_USER_PASSWORD', 'password'), 11 | 'role' => env('SEED_SUPER_ADMIN_USER_ROLE', 'superuser'), 12 | ], 13 | 'regular' => [ 14 | 'enabled' => env('SEED_REGULAR_USER_ENABLED', false), 15 | 'name' => env('SEED_REGULAR_USER_NAME', 'Regular User'), 16 | 'email' => env('SEED_REGULAR_USER_EMAIL', 'user@example.com'), 17 | 'password' => env('SEED_REGULAR_USER_PASSWORD', 'password'), 18 | 'role' => env('SEED_REGULAR_USER_ROLE', 'user'), 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleSocialiteProviders.php: -------------------------------------------------------------------------------- 1 | filter(fn ($provider) => $provider['enabled'] ?? false) 20 | ->toArray(); 21 | 22 | $providerConfig = config('socialite'); 23 | $providerConfig['providers'] = $providers; 24 | 25 | $request->attributes->set('providersConfig', $providerConfig); 26 | 27 | return $next($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Services/Notifications/AppNotificationAutoExpireService.php: -------------------------------------------------------------------------------- 1 | whereNull('deleted_at') 21 | ->whereNotNull('auto_expire_on') 22 | ->where('auto_expire_on', '<=', $cutoff) 23 | ->delete(); 24 | 25 | return [ 26 | 'soft_deleted' => (int) $softDeleted, 27 | 'cutoff' => $cutoff, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('uuid')->unique(); 16 | $table->text('connection'); 17 | $table->text('queue'); 18 | $table->longText('payload'); 19 | $table->longText('exception'); 20 | $table->timestamp('failed_at')->useCurrent(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('failed_jobs'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | /* @import 'tw-animate-css'; */ 3 | 4 | @theme { 5 | --breakpoint-xxs: 360px; 6 | --breakpoint-xs: 480px; 7 | } 8 | 9 | @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; 10 | @source '../../storage/framework/views/*.php'; 11 | @source '../views'; 12 | 13 | @import '@css/partials/theme.css'; 14 | @import '@css/partials/base.css'; 15 | @import '@css/partials/tables.css'; 16 | @import '@css/partials/pagination.css'; 17 | @import '@css/partials/forms.css'; 18 | @import '@css/partials/buttons.css'; 19 | @import '@css/partials/nav.css'; 20 | @import '@css/partials/datatable.css'; 21 | @import '@css/partials/badges.css'; 22 | @import '@css/partials/tabs.css'; 23 | @import '@css/partials/typography.css'; 24 | @import '@css/partials/wells.css'; 25 | @import '@css/partials/cards.css'; 26 | @import '@css/partials/notifications.css'; 27 | @import '~/nprogress/nprogress.css'; 28 | -------------------------------------------------------------------------------- /app/Mail/NotificationsCleanupReport.php: -------------------------------------------------------------------------------- 1 | deleted = $deleted; 24 | $this->cutoff = $cutoff; 25 | $this->days = $days; 26 | } 27 | 28 | public function build(): self 29 | { 30 | return $this 31 | ->subject(trans('emails.notifications_cleanup.subject', [ 32 | 'appname' => config('app.name'), 33 | ])) 34 | ->markdown('emails.notifications.cleanup-report'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Providers/DebugBarServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->isLocal()) { 15 | /** 16 | * Loader for registering facades. 17 | */ 18 | $loader = \Illuminate\Foundation\AliasLoader::getInstance(); 19 | 20 | /* 21 | * Load third party local providers 22 | */ 23 | $this->app->register(\Barryvdh\Debugbar\ServiceProvider::class); 24 | 25 | /* 26 | * Load third party local aliases 27 | */ 28 | $loader->alias('Debugbar', \Barryvdh\Debugbar\Facade::class); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Services/Notifications/AppNotificationCleanupService.php: -------------------------------------------------------------------------------- 1 | subDays($days); 19 | 20 | $deleted = AppNotification::query() 21 | ->onlyTrashed() 22 | ->where('deleted_at', '<=', $cutoff) 23 | ->forceDelete(); 24 | 25 | return [ 26 | 'deleted' => (int) $deleted, 27 | 'cutoff' => $cutoff, 28 | 'days' => $days, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2025_04_26_181756_create_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 15 | $table->foreignUlid('user_id')->nullable()->index(); 16 | $table->string('ip_address', 45)->nullable(); 17 | $table->text('user_agent')->nullable(); 18 | $table->longText('payload'); 19 | $table->integer('last_activity')->index(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('sessions'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Mail/VerifyMail.php: -------------------------------------------------------------------------------- 1 | user = $user; 21 | $this->verificationUrl = $verificationUrl; 22 | } 23 | 24 | public function build(): self 25 | { 26 | return $this 27 | ->subject(trans('emails.verify.subject', [ 28 | 'appname' => config('app.name'), 29 | ])) 30 | ->markdown('emails.verify', [ 31 | 'user' => $this->user, 32 | 'verificationUrl' => $this->verificationUrl, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "plugin:vue/vue3-essential" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "es2021": true, 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "indent": "off", 17 | "linebreak-style": "off", 18 | "quotes": "off", 19 | "semi": "off", 20 | "vue/html-indent": "off", 21 | "vue/max-attributes-per-line": "off", 22 | "vue/multiline-html-element-content-newline": "off", 23 | "vue/singleline-html-element-content-newline": "off", 24 | "vue/first-attribute-linebreak": "off", 25 | "vue/html-closing-bracket-newline": "off", 26 | "vue/html-closing-bracket-spacing": "off", 27 | "vue/html-self-closing": "off", 28 | "vue/multi-word-component-names": "off", 29 | "vue/require-default-prop": "off", 30 | "vue/no-v-html": "off", 31 | "no-unused-vars": "warn" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | by($request->user()?->id ?: $request->ip()); 19 | }); 20 | 21 | $this->routes(function () { 22 | Route::middleware('api') 23 | ->prefix('api') 24 | ->group(base_path('routes/api.php')); 25 | 26 | Route::middleware('web') 27 | ->group(base_path('routes/web.php')); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2025_06_16_075701_create_financial_metrics_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | $table->date('date'); 16 | $table->string('category'); 17 | $table->decimal('amount', 12, 2); 18 | $table->string('type')->nullable(); 19 | $table->string('description')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('financial_metrics'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /app/Listeners/CreateAppNotification.php: -------------------------------------------------------------------------------- 1 | scope, ['user', 'system', 'release'], true) 14 | ? $event->scope 15 | : 'user'; 16 | 17 | $userId = $scope === 'user' 18 | ? $event->userId 19 | : null; 20 | 21 | $notification = AppNotification::create([ 22 | 'user_id' => $userId, 23 | 'scope' => $scope, 24 | 'type' => $event->type, 25 | 'title' => $event->title, 26 | 'message' => $event->message, 27 | 'data' => $event->data ?: null, 28 | ]); 29 | 30 | AppNotificationCreated::dispatch($notification); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /app/Jobs/DestroySoftDeletedUsersJob.php: -------------------------------------------------------------------------------- 1 | chunkById(100, function (Collection $users) use (&$deletedCount): void { 26 | foreach ($users as $user) { 27 | $user->forceDelete(); 28 | $deletedCount++; 29 | } 30 | }); 31 | 32 | return $deletedCount; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /resources/css/partials/pagination.css: -------------------------------------------------------------------------------- 1 | .pagination-btn { 2 | display: inline-flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding-left: 0.75rem; 6 | padding-right: 0.75rem; 7 | padding-top: 0.5rem; 8 | padding-bottom: 0.5rem; 9 | border: 1px solid var(--color-border-strong); 10 | border-radius: 0.375rem; 11 | font-size: 0.875rem; 12 | line-height: 1.25rem; 13 | font-weight: 500; 14 | color: var(--color-text-muted); 15 | background-color: var(--color-surface); 16 | cursor: pointer; 17 | } 18 | 19 | .pagination-btn:hover { 20 | background-color: var(--color-surface-muted); 21 | } 22 | 23 | .pagination-btn:focus { 24 | outline: none; 25 | ring-offset: 2px; 26 | --tw-ring-opacity: 1; 27 | --tw-ring-color: rgba(168, 85, 247, var(--tw-ring-opacity)); 28 | --tw-ring-offset-width: 2px; 29 | } 30 | 31 | .pagination-btn:disabled { 32 | opacity: 0.5; 33 | cursor: not-allowed; 34 | } 35 | 36 | .pagination-btn:disabled:hover { 37 | background-color: var(--color-surface); 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckPasswordExpiry.php: -------------------------------------------------------------------------------- 1 | password_expiry && $user->isPasswordExpired()) { 20 | if (!$request->routeIs('user.password.expired') && !$request->routeIs('user.password.expired.update')) { 21 | session()->flash('warning', 'Your password has expired. Please change it to continue.'); 22 | 23 | return redirect()->route('user.password.expired'); 24 | } 25 | } 26 | } 27 | 28 | return $next($request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2025_02_02_183354_create_personalisations_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | $table->string('app_name')->nullable(); 16 | $table->string('app_logo')->nullable(); 17 | $table->string('app_logo_dark')->nullable(); 18 | $table->string('favicon')->nullable(); 19 | $table->string('copyright_text')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('personalisations'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /app/Console/Commands/RunUsersAutoDestroy.php: -------------------------------------------------------------------------------- 1 | info('Running DestroySoftDeletedUsersJob...'); 30 | 31 | $job = new DestroySoftDeletedUsersJob(); 32 | 33 | $deletedCount = $job->handle(); 34 | 35 | $this->info("Deleted {$deletedCount} user(s)."); 36 | 37 | return self::SUCCESS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/css/partials/base.css: -------------------------------------------------------------------------------- 1 | /* Base Styles */ 2 | textarea:focus, 3 | input:focus { 4 | outline: none; 5 | } 6 | 7 | /* FilePond Customizations */ 8 | .filepond--drop-label { 9 | font-family: var(--font-sans); 10 | } 11 | 12 | /* Utility Classes */ 13 | .main-heading, 14 | .sub-heading { 15 | font-size: 1.5rem; 16 | line-height: 2rem; 17 | font-weight: 700; 18 | color: var(--color-text); 19 | } 20 | 21 | /* Dark heading colors inherit from variables; no override needed */ 22 | 23 | .container-border { 24 | background-color: var(--color-surface); 25 | border: 1px solid var(--color-border); 26 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 27 | } 28 | 29 | .main-container { 30 | @apply rounded-[var(--main-container-radius)]; 31 | .container-border { 32 | @apply rounded-[var(--main-container-radius)]; 33 | > div:last-child { 34 | @apply rounded-b-[var(--main-container-radius)]; 35 | } 36 | } 37 | > div, 38 | > div > nav { 39 | @apply rounded-t-[var(--main-container-radius)]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/2025_02_24_161810_create_login_history_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->ulidMorphs('user'); 16 | $table->string('ip_address')->nullable(); 17 | $table->text('user_agent')->nullable(); 18 | $table->boolean('login_successful')->default(false); 19 | $table->timestamp('login_at'); 20 | $table->timestamp('logout_at')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('login_history'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->morphs('tokenable'); 16 | $table->string('name'); 17 | $table->string('token', 64)->unique(); 18 | $table->text('abilities')->nullable(); 19 | $table->timestamp('last_used_at')->nullable(); 20 | $table->timestamp('expires_at')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('personal_access_tokens'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/AdminUsersVerificationController.php: -------------------------------------------------------------------------------- 1 | middleware('permission:edit-users'); 14 | } 15 | 16 | public function toggle(Request $request, User $user) 17 | { 18 | $user->update([ 19 | 'email_verified_at' => $user->email_verified_at ? null : now(), 20 | ]); 21 | 22 | return redirect() 23 | ->back() 24 | ->with('success', 'User '.($user->email_verified_at ? 'Verified' : 'Un-Verified')); 25 | } 26 | 27 | public function send(Request $request, User $user) 28 | { 29 | $user->sendUserEmailVerificationFromAdmin(); 30 | 31 | return redirect() 32 | ->back() 33 | ->with('success', 'Verification Email Sent to '.$user->email); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2024_10_22_135647_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 15 | $table->mediumText('value'); 16 | $table->integer('expiration')->index(); 17 | }); 18 | 19 | Schema::create('cache_locks', function (Blueprint $table) { 20 | $table->string('key')->primary(); 21 | $table->string('owner'); 22 | $table->integer('expiration')->index(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('cache'); 32 | Schema::dropIfExists('cache_locks'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /resources/views/emails/magic-registration-link.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.general.greeting', ['username' => $user->name]) 4 |

5 | 6 |

Complete Your Registration Welcome! Click the button below to verify your email and access your account.

7 | 8 | 9 | @lang('emails.general.access') 10 | 11 | 12 |

If you did not request this registration, no action is required.

13 | 14 |

15 | @lang('emails.general.goodbye') 16 |

17 | 18 |

19 | {{ config('app.name') }} 20 |

21 | 22 | 23 | @lang( 24 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 25 | 'into your web browser:', 26 | [ 27 | 'actionText' => __('emails.general.access'), 28 | ] 29 | ) 30 | [{{ $url }}]({{ $url }}) 31 | 32 |
33 | -------------------------------------------------------------------------------- /resources/views/emails/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.welcome.greeting', ['username' => $user->name]) 4 |

5 | 6 |

7 | @lang('emails.welcome.line_one', ['appname' => config('app.name')]) 8 |

9 | 10 | 11 | @lang('emails.welcome.btn') 12 | 13 | 14 |

15 | @lang('emails.welcome.line_two') 16 |

17 | 18 |

19 | @lang('emails.welcome.goodbye') 20 |

21 | 22 |

23 | {{ config('app.name') }} 24 |

25 | 26 | 27 | @lang( 28 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 29 | 'into your web browser:', 30 | [ 31 | 'actionText' => __('emails.welcome.btn'), 32 | ] 33 | ) 34 | [{{ route('dashboard') }}]({{ route('dashboard') }}) 35 | 36 |
37 | -------------------------------------------------------------------------------- /app/Providers/AppHealthServiceProvider.php: -------------------------------------------------------------------------------- 1 | check()) { 16 | return $next($request); 17 | } 18 | 19 | $settings = Setting::first(); 20 | $twoFactorEnabled = Features::enabled(Features::twoFactorAuthentication()); 21 | 22 | if (!$settings || !$settings->two_factor_authentication || !$twoFactorEnabled) { 23 | return $next($request); 24 | } 25 | 26 | $user = auth()->user(); 27 | if (!$user->two_factor_secret) { 28 | session()->flash('warning', __('notifications.account.two_factor_required')); 29 | 30 | return redirect()->route('user.two.factor'); 31 | } 32 | 33 | return $next($request); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Console/Commands/SendScheduledAppNotificationsCommand.php: -------------------------------------------------------------------------------- 1 | option('dry-run'); 17 | 18 | $count = $service->sendDue($dryRun); 19 | 20 | if ($dryRun) { 21 | $this->info("Dry run: {$count} scheduled notifications are due."); 22 | 23 | return self::SUCCESS; 24 | } 25 | 26 | $this->info("Sent {$count} scheduled notifications."); 27 | 28 | return self::SUCCESS; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ato Augustine ato@tuta.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY. See Below.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you discover a security vulnerability within GuacPanel, please send an email to Ato Augustine at **ato@tuta.com**. All security vulnerabilities will be promptly addressed. 8 | 9 | ## What to Include 10 | 11 | When reporting a security vulnerability, please include: 12 | 13 | - A description of the vulnerability 14 | - Steps to reproduce the issue 15 | - Potential impact of the vulnerability 16 | - Any suggested fixes (if available) 17 | 18 | ## Supported Versions 19 | 20 | We provide security updates for the latest version of GuacPanel. Please ensure you're using the most recent release. 21 | 22 | ## Security Best Practices 23 | 24 | When using GuacPanel, please: 25 | 26 | - Keep all dependencies up to date 27 | - Use strong passwords and enable 2FA 28 | - Regularly review user permissions 29 | - Monitor authentication logs 30 | - Keep your Laravel installation updated 31 | 32 | Thank you for helping keep GuacPanel secure! 33 | -------------------------------------------------------------------------------- /app/Providers/AppPersonalisationServiceProvider.php: -------------------------------------------------------------------------------- 1 | getTable())) { 19 | $personalisation = $this->getPersonalisations(); 20 | 21 | // Share with Blade views 22 | View::share('personalisation', $personalisation); 23 | 24 | // Share with Inertia 25 | Inertia::share([ 26 | 'app' => [ 27 | 'version' => config('app.version'), 28 | 'name' => config('app.name'), 29 | ], 30 | 'personalisation' => fn () => $personalisation, 31 | ]); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2025_12_04_035619_update_users_table_with_delete_settings.php: -------------------------------------------------------------------------------- 1 | string('restore_token') 12 | ->nullable() 13 | ->after('profile_image_type'); 14 | 15 | $table->boolean('auto_destroy') 16 | ->default(1) 17 | ->after('restore_token'); 18 | 19 | $table->datetime('restore_date') 20 | ->nullable() 21 | ->after('deleted_at'); 22 | }); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::table('users', function (Blueprint $table) { 28 | $table->dropColumn('restore_token'); 29 | $table->dropColumn('auto_destroy'); 30 | $table->dropColumn('restore_date'); 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /resources/views/emails/verify-email-from-admin-triggered.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.verify_from_admin.greeting', ['username' => $notifiable->name]) 4 |

5 | 6 |

7 | @lang('emails.verify_from_admin.msg_upper_one') 8 |

9 | 10 | 11 | @lang('emails.verify_from_admin.btn') 12 | 13 | 14 |

15 | @lang('emails.verify_from_admin.msg_lower') 16 |

17 | 18 |

19 | @lang('emails.verify_from_admin.goodbye') 20 |

21 | 22 |

23 | {{ config('app.name') }} 24 |

25 | 26 | 27 | @lang( 28 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 29 | 'into your web browser:', 30 | [ 31 | 'actionText' => __('emails.verify_from_admin.btn'), 32 | ] 33 | ) 34 | [{{ $verificationUrl }}]({{ $verificationUrl }}) 35 | 36 |
37 | -------------------------------------------------------------------------------- /resources/js/echo.js: -------------------------------------------------------------------------------- 1 | import Echo from 'laravel-echo' 2 | import Pusher from 'pusher-js' 3 | 4 | window.Pusher = Pusher 5 | 6 | const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') 7 | 8 | const notificationsEnabled = import.meta.env.VITE_APP_NOTIFICATIONS_ENABLED 9 | 10 | if (notificationsEnabled?.toLowerCase() === 'true') { 11 | window.Echo = new Echo({ 12 | broadcaster: 'reverb', 13 | key: import.meta.env.VITE_REVERB_APP_KEY, 14 | 15 | wsHost: import.meta.env.VITE_REVERB_HOST ?? window.location.hostname, 16 | wsPort: Number(import.meta.env.VITE_REVERB_PORT ?? 8080), 17 | wssPort: Number(import.meta.env.VITE_REVERB_PORT ?? 8080), 18 | 19 | forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https', 20 | enabledTransports: ['ws', 'wss'], 21 | 22 | // IMPORTANT for Fortify/session auth: 23 | authEndpoint: '/broadcasting/auth', 24 | withCredentials: true, 25 | auth: { 26 | headers: { 27 | 'X-Requested-With': 'XMLHttpRequest', 28 | ...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}), 29 | }, 30 | }, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /resources/views/emails/welcome-verified.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.welcome-verified.greeting', ['username' => $user->name]) 4 |

5 | 6 |

7 | @lang('emails.welcome-verified.line_one', ['appname' => config('app.name')]) 8 |

9 | 10 | 11 | @lang('emails.welcome-verified.btn') 12 | 13 | 14 |

15 | @lang('emails.welcome-verified.line_two') 16 |

17 | 18 |

19 | @lang('emails.welcome-verified.goodbye') 20 |

21 | 22 |

23 | {{ config('app.name') }} 24 |

25 | 26 | 27 | @lang( 28 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 29 | 'into your web browser:', 30 | [ 31 | 'actionText' => __('emails.welcome-verified.btn'), 32 | ] 33 | ) 34 | [{{ route('dashboard') }}]({{ route('dashboard') }}) 35 | 36 |
37 | -------------------------------------------------------------------------------- /app/Models/AppNotificationRead.php: -------------------------------------------------------------------------------- 1 | 'datetime', 28 | 'dismissed_at' => 'datetime', 29 | 'u_del_notif_at' => 'datetime', 30 | 'deleted_at' => 'datetime', 31 | ]; 32 | 33 | public function notification(): BelongsTo 34 | { 35 | return $this->belongsTo(AppNotification::class, 'app_notification_id'); 36 | } 37 | 38 | public function user(): BelongsTo 39 | { 40 | return $this->belongsTo(User::class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Traits/HasProtectedPermission.php: -------------------------------------------------------------------------------- 1 | getProtectedPermissions()); 28 | 29 | return in_array(strtolower(trim($permissionName)), $protectedPermissions); 30 | } 31 | 32 | protected function getProtectedPermissionsForValidation(): string 33 | { 34 | return implode(',', $this->getProtectedPermissions()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/emails/verify.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.verify.greeting', ['username' => $user->name]) 4 |

5 | 6 |

7 | @lang('emails.verify.msg_upper_one', ['appname' => config('app.name')]) 8 |

9 | 10 |

11 | @lang('emails.verify.msg_upper_two') 12 |

13 | 14 | 15 | @lang('emails.verify.btn') 16 | 17 | 18 |

19 | @lang('emails.verify.msg_lower') 20 |

21 | 22 |

23 | @lang('emails.verify.goodbye') 24 |

25 | 26 |

27 | {{ config('app.name') }} 28 |

29 | 30 | 31 | @lang( 32 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 33 | 'into your web browser:', 34 | [ 35 | 'actionText' => __('emails.verify.btn'), 36 | ] 37 | ) 38 | [{{ $verificationUrl }}]({{ $verificationUrl }}) 39 | 40 |
41 | -------------------------------------------------------------------------------- /app/Actions/Fortify/RegisterResponse.php: -------------------------------------------------------------------------------- 1 | guard = $guard; 16 | } 17 | 18 | public function toResponse($request) 19 | { 20 | if (!config('guacpanel.auto_login_after_register')) { 21 | $this->guard->logout(); 22 | } 23 | 24 | if (config('guacpanel.email_verification_enabled')) { 25 | session()->flash('success', __('notifications.register.pw_success_auto_login_disabled_activation_enabled')); 26 | } else { 27 | session()->flash('success', __('notifications.register.pw_success_auto_login_disabled_activation_disabled')); 28 | } 29 | 30 | return $request->wantsJson() 31 | ? new JsonResponse('', 201) 32 | : redirect()->route('login'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Console/Commands/CleanupDeletedAppNotificationsCommand.php: -------------------------------------------------------------------------------- 1 | argument('days'); 17 | 18 | if ($days === null || $days === '') { 19 | $days = (int) config('guacpanel.notifications.auto_cleanup_deleted_days', 30); 20 | } 21 | 22 | $days = max(1, (int) $days); 23 | 24 | $result = $cleanup->cleanupDeleted($days); 25 | 26 | $this->info('Deleted notifications: '.$result['deleted']); 27 | $this->info('Cutoff date: '.$result['cutoff']->toDateTimeString().' ('.$result['days'].' days)'); 28 | 29 | return self::SUCCESS; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /resources/views/emails/account-restored.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.restored.greeting', ['username' => $user->name]) 4 |

5 | 6 |

7 | @lang('emails.restored.line_one', ['appname' => config('app.name'), 'date' => $restoreDateParsed]) 8 |

9 | 10 | 11 | @lang('emails.restored.btn') 12 | 13 | 14 |

15 | @lang('emails.restored.line_two', ['email' => config('guacpanel.admin.support_email')]) 16 |

17 | 18 |

19 | @lang('emails.restored.goodbye') 20 |

21 | 22 |

23 | {{ config('app.name') }} 24 |

25 | 26 | 27 | @lang( 28 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 29 | 'into your web browser:', 30 | [ 31 | 'actionText' => __('emails.restored.btn'), 32 | ] 33 | ) 34 | [{{ route('dashboard') }}]({{ route('dashboard') }}) 35 | 36 |
37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Typesense binaries 2 | typesense-server 3 | typesense-server-*.deb 4 | typesense-server-*.tar.gz 5 | typesense-server-*.tar.gz.* 6 | typesense-server.md5.txt 7 | typesense/data/ 8 | data/ 9 | 10 | ### OSX Vendor Configs ### 11 | .DS_Store 12 | *._DS_Store 13 | ._.DS_Store 14 | *._ 15 | ._* 16 | ._.* 17 | 18 | ### Windows Vendor Configs ### 19 | Thumbs.db 20 | 21 | ### Windows Vendor Configs ### 22 | Thumbs.db 23 | 24 | ### Editor Configs ### 25 | *.sublime-workspace 26 | .idea/* 27 | /.idea/* 28 | .phpintel/* 29 | /.phpintel/* 30 | /.vscode 31 | /.notes/* 32 | /.fleet 33 | 34 | ### Assets ### 35 | /node_modules 36 | /public/build 37 | /public/hot 38 | /public/storage 39 | /storage/*.key 40 | /vendor 41 | .env 42 | .env.backup 43 | .env.production 44 | .phpunit.result.cache 45 | Homestead.json 46 | Homestead.yaml 47 | auth.json 48 | npm-debug.log 49 | yarn-error.log 50 | /storage/debugbar 51 | /storage/framework/cache/* 52 | /storage/framework/sessions/* 53 | /storage/framework/testing/* 54 | /storage/framework/views/* 55 | /storage/pail 56 | docker-compose.override.yml 57 | 58 | ## GHA Runners require lock files. 59 | #composer.lock 60 | #package-lock.json 61 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'current_password' => ['required', 'string', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('The provided password does not match your current password.'), 26 | ])->validateWithBag('updatePassword'); 27 | 28 | $user->forceFill([ 29 | 'password' => Hash::make($input['password']), 30 | ])->save(); 31 | 32 | session()->flash('success', 'Your password has been changed successfully.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2025_07_06_171547_create_system_notices_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | $table->string('title'); 16 | $table->text('content'); 17 | $table->boolean('is_active')->default(true); 18 | $table->boolean('is_dismissible')->default(false); 19 | $table->string('type')->default('info'); 20 | $table->dateTime('visible_from')->nullable(); 21 | $table->dateTime('expires_at')->nullable(); 22 | $table->foreignUlid('created_by')->constrained('users'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('system_notices'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/seeders/FinancialMetricsSeeder.php: -------------------------------------------------------------------------------- 1 | subDays(100); 19 | 20 | // Disable Scout syncing during seeding to avoid Typesense configuration errors 21 | FinancialMetric::withoutSyncingToSearch(function () use ($faker, $startDate) { 22 | for ($i = 0; $i < 100; $i++) { 23 | FinancialMetric::create([ 24 | 'date' => $startDate->copy()->addDays($i), 25 | 'category' => $faker->randomElement(['sales', 'marketing', 'operations', 'investment']), 26 | 'amount' => $faker->randomFloat(2, 1000, 100000), 27 | 'type' => $faker->randomElement(['income', 'expense']), 28 | 'description' => $faker->sentence(), 29 | ]); 30 | } 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Requests/Admin/Notifications/StoreAdminAppNotificationRequest.php: -------------------------------------------------------------------------------- 1 | user(); 12 | 13 | return (bool) $user && $user->can('manage-notifications'); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | 'scope' => ['required', 'in:user,system,release'], 20 | 'user_id' => ['nullable', 'string', 'required_if:scope,user'], 21 | 'type' => ['required', 'in:success,info,warning,danger'], 22 | 'title' => ['required', 'string', 'max:255'], 23 | 'message' => ['required', 'string'], 24 | 'scheduled_on' => ['nullable', 'date'], 25 | 'auto_expire_on' => ['nullable', 'date'], 26 | ]; 27 | } 28 | 29 | public function messages(): array 30 | { 31 | return [ 32 | 'user_id.required_if' => 'A user is required when scope is set to user.', 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Models/Personalisation.php: -------------------------------------------------------------------------------- 1 | 'string', 29 | 'app_logo' => 'string', 30 | 'app_logo_dark' => 'string', 31 | 'favicon' => 'string', 32 | 'copyright_text' => 'string', 33 | 'created_at' => 'datetime', 34 | 'updated_at' => 'datetime', 35 | ]; 36 | 37 | protected static function booted() 38 | { 39 | // Handled in Observer 40 | static::saved(function ($settings) { 41 | // 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Requests/Admin/Notifications/UpdateAdminAppNotificationRequest.php: -------------------------------------------------------------------------------- 1 | user(); 12 | 13 | return (bool) $user && $user->can('manage-notifications'); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | 'scope' => ['required', 'in:user,system,release'], 20 | 'user_id' => ['nullable', 'string', 'required_if:scope,user'], 21 | 'type' => ['required', 'in:success,info,warning,danger'], 22 | 'title' => ['required', 'string', 'max:255'], 23 | 'message' => ['required', 'string'], 24 | 'data' => ['nullable', 'array'], 25 | 'scheduled_on' => ['nullable', 'date'], 26 | 'auto_expire_on' => ['nullable', 'date'], 27 | ]; 28 | } 29 | 30 | public function messages(): array 31 | { 32 | return [ 33 | 'user_id.required_if' => 'A user is required when scope is set to user.', 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/emails/magic-link.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | {{ $isNewUser ? 'Complete Your Registration' : 'Login to Your Account' }} 4 |

5 | 6 |

7 | @if ($isNewUser) 8 | Welcome! Click the button below to verify your email and access your account. 9 | @else 10 | Click the button below to login to your account. This link will expire in 10 minutes. 11 | @endif 12 |

13 | 14 | 15 | {{ $isNewUser ? 'Access Your Account' : 'Login Now' }} 16 | 17 | 18 |

If you did not request this link, no action is required.

19 | 20 |

21 | @lang('emails.general.goodbye') 22 |

23 | 24 |

25 | {{ config('app.name') }} 26 |

27 | 28 | {{-- Subcopy --}} 29 | 30 | @lang( 31 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 32 | 'into your web browser:', 33 | [ 34 | 'actionText' => $isNewUser ? 'Access Your Account' : 'Login Now', 35 | ] 36 | ) 37 | [{{ $url }}]({{ $url }}) 38 | 39 |
40 | -------------------------------------------------------------------------------- /app/Events/AppNotificationsBulkChanged.php: -------------------------------------------------------------------------------- 1 | userId === 'system') { 28 | return new PrivateChannel('system'); 29 | } 30 | 31 | return new PrivateChannel("users.{$this->userId}"); 32 | } 33 | 34 | public function broadcastAs(): string 35 | { 36 | return 'app-notification.bulk'; 37 | } 38 | 39 | public function broadcastWith(): array 40 | { 41 | return [ 42 | 'action' => $this->action, 43 | 'ids' => $this->ids, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Feature/AdminAuditControllerTest.php: -------------------------------------------------------------------------------- 1 | 'view-audits']); 14 | 15 | $this->adminUser = User::factory()->create(); 16 | $this->adminUser->givePermissionTo('view-audits'); 17 | 18 | $this->regularUser = User::factory()->create(); 19 | }); 20 | 21 | test('it redirects unauthenticated users to login page', function () { 22 | $this->get(route('admin.audit.index')) 23 | ->assertRedirect(route('login')); 24 | }); 25 | 26 | test('it denies access to users without audit permission', function () { 27 | $this->actingAs($this->regularUser) 28 | ->get(route('admin.audit.index')) 29 | ->assertForbidden(); 30 | }); 31 | 32 | test('it allows access to users with audit permission', function () { 33 | $this->actingAs($this->adminUser) 34 | ->get(route('admin.audit.index')) 35 | ->assertStatus(200) 36 | ->assertInertia( 37 | fn (Assert $page) => $page 38 | ->component('Admin/IndexAuditPage') 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GuacPanel 2 | 3 | Thank you for considering contributing to GuacPanel! We welcome contributions from the community. 4 | 5 | ## How to Contribute 6 | 7 | ### Reporting Issues 8 | - Use the GitHub issue tracker 9 | - Provide clear description and steps to reproduce 10 | - Include your environment details (PHP, Node.js versions) 11 | 12 | ### Submitting Changes 13 | 1. Fork the repository 14 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 15 | 3. Make your changes 16 | 4. Test your changes thoroughly 17 | 5. Commit with clear messages (`git commit -m 'Add amazing feature'`) 18 | 6. Push to your branch (`git push origin feature/amazing-feature`) 19 | 7. Open a Pull Request 20 | 21 | ### Development Setup 22 | Follow the installation instructions in the README.md, then: 23 | 24 | ```bash 25 | # Install development dependencies 26 | npm install 27 | composer install 28 | 29 | # Run tests 30 | php artisan test 31 | npm run test 32 | 33 | # Start development 34 | npm run dev 35 | php artisan serve 36 | ``` 37 | 38 | ### Code Style 39 | - Follow PSR-12 for PHP code 40 | - Use Prettier for JavaScript/Vue formatting 41 | - Write descriptive commit messages 42 | - Add tests for new features 43 | 44 | ## Questions? 45 | 46 | Feel free to open an issue for questions or join our discussions! -------------------------------------------------------------------------------- /app/Mail/GoodbyeUserMail.php: -------------------------------------------------------------------------------- 1 | user = $user; 26 | $this->url = $url; 27 | $this->restoreEnabled = (bool) config('guacpanel.user.account.restore_enabled', false); 28 | $this->daysToRestore = (int) config('guacpanel.user.account.days_to_restore', 60); 29 | $this->autoDestroyEnabled = (bool) ($user->auto_destroy ?? false); 30 | $this->autoDestroyDateRaw = $user->auto_destroy_date; 31 | $this->autoDestroyDateParsed = $user->auto_destroy_formatted; 32 | } 33 | 34 | public function build(): self 35 | { 36 | return $this 37 | ->subject(__('emails.goodbye.subject')) 38 | ->markdown('emails.goodbye-user-mail'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Observers/UserObserver.php: -------------------------------------------------------------------------------- 1 | user_slug) { 17 | $user->user_slug = 'user-'.Str::ulid(); 18 | } 19 | 20 | if (!$user->password) { 21 | $user->password = null; 22 | } 23 | } 24 | 25 | public function created(User $user): void 26 | { 27 | $this->resetUserCache($user); 28 | } 29 | 30 | public function updated(User $user): void 31 | { 32 | $this->resetUserCache($user); 33 | } 34 | 35 | public function deleted(User $user): void 36 | { 37 | $user->update([ 38 | 'restore_date' => null, 39 | ]); 40 | 41 | $this->resetUserCache($user); 42 | } 43 | 44 | public function restored(User $user): void 45 | { 46 | $user->update([ 47 | 'restore_date' => Carbon::now(), 48 | ]); 49 | 50 | $this->resetUserCache($user); 51 | } 52 | 53 | public function forceDeleted(User $user): void 54 | { 55 | $this->resetUserCache($user); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "bracketSameLine": true, 7 | "htmlWhitespaceSensitivity": "ignore", 8 | "vueIndentScriptAndStyle": false, 9 | "printWidth": 100, 10 | "trailingComma": "es5", 11 | "arrowParens": "avoid", 12 | "useTabs": false, 13 | "bracketSpacing": true, 14 | "endOfLine": "lf", 15 | "plugins": [ 16 | "prettier-plugin-tailwindcss", 17 | "@prettier/plugin-php", 18 | prettier-plugin-blade 19 | ], 20 | "overrides": [ 21 | { 22 | "files": ["*.php"], 23 | "options": { 24 | "tabWidth": 4, 25 | "printWidth": 120, 26 | "phpVersion": "8.2" 27 | } 28 | }, 29 | { 30 | "files": ["*.blade.php"], 31 | "options": { 32 | "tabWidth": 4 33 | } 34 | }, 35 | { 36 | "files": ["*.vue"], 37 | "options": { 38 | "tabWidth": 2 39 | } 40 | }, 41 | { 42 | "files": ["*.js", "*.cjs", "*.mjs", "*.ts", "*.tsx", "*.json"], 43 | "options": { 44 | "tabWidth": 2 45 | } 46 | }, 47 | { 48 | "files": ["*.css", "*.scss", "*.sass", "*.pcss"], 49 | "options": { 50 | "parser": "css", 51 | "tabWidth": 2 52 | } 53 | }, 54 | ] 55 | 56 | } 57 | -------------------------------------------------------------------------------- /resources/js/Components/Forms/Switch.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /resources/js/Components/Common/NotificationTypeBadge.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /resources/views/vendor/notifications/email.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Greeting --}} 3 | @if (! empty($greeting)) 4 | # {{ $greeting }} 5 | @else 6 | @if ($level === 'error') 7 | # @lang('Whoops!') 8 | @else 9 | # @lang('Hello!') 10 | @endif 11 | @endif 12 | 13 | {{-- Intro Lines --}} 14 | @foreach ($introLines as $line) 15 | {{ $line }} 16 | 17 | @endforeach 18 | 19 | {{-- Action Button --}} 20 | @isset($actionText) 21 | $level, 24 | default => 'primary', 25 | }; 26 | ?> 27 | 28 | {{ $actionText }} 29 | 30 | @endisset 31 | 32 | {{-- Outro Lines --}} 33 | @foreach ($outroLines as $line) 34 | {{ $line }} 35 | 36 | @endforeach 37 | 38 | {{-- Salutation --}} 39 | @if (! empty($salutation)) 40 | {{ $salutation }} 41 | @else 42 | @lang('Regards,')
43 | {{ config('app.name') }} 44 | @endif 45 | 46 | {{-- Subcopy --}} 47 | @isset($actionText) 48 | 49 | @lang( 50 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n". 51 | 'into your web browser:', 52 | [ 53 | 'actionText' => $actionText, 54 | ] 55 | ) [{{ $displayableActionUrl }}]({{ $actionUrl }}) 56 | 57 | @endisset 58 |
59 | -------------------------------------------------------------------------------- /resources/js/Components/Icons/FacebookIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | -------------------------------------------------------------------------------- /resources/views/vendor/health/logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/js/utils/apiFetch.js: -------------------------------------------------------------------------------- 1 | export default async function apiFetch(url, options = {}) { 2 | const getCookie = name => { 3 | const value = `; ${document.cookie || ''}` 4 | const parts = value.split(`; ${name}=`) 5 | if (parts.length === 2) { 6 | return parts.pop().split(';').shift() 7 | } 8 | return null 9 | } 10 | 11 | const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') 12 | 13 | // Laravel sets XSRF-TOKEN cookie URL-encoded 14 | const xsrfCookie = getCookie('XSRF-TOKEN') 15 | const xsrf = xsrfCookie ? decodeURIComponent(xsrfCookie) : null 16 | 17 | const headers = { 18 | Accept: 'application/json', 19 | 'X-Requested-With': 'XMLHttpRequest', 20 | ...(options.headers ?? {}), 21 | } 22 | 23 | const method = (options.method ?? 'GET').toUpperCase() 24 | 25 | // Only add CSRF + JSON headers for non-GET requests 26 | if (method !== 'GET') { 27 | if (!headers['Content-Type'] && !(options.body instanceof FormData)) { 28 | headers['Content-Type'] = 'application/json' 29 | } 30 | 31 | if (csrf && !headers['X-CSRF-TOKEN']) { 32 | headers['X-CSRF-TOKEN'] = csrf 33 | } 34 | 35 | if (xsrf && !headers['X-XSRF-TOKEN']) { 36 | headers['X-XSRF-TOKEN'] = xsrf 37 | } 38 | } 39 | 40 | return fetch(url, { 41 | credentials: 'same-origin', 42 | ...options, 43 | headers, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /tests/Feature/AdminLoginHistoryControllerTest.php: -------------------------------------------------------------------------------- 1 | 'view-login-history']); 14 | 15 | $this->adminUser = User::factory()->create(); 16 | $this->adminUser->givePermissionTo('view-login-history'); 17 | 18 | $this->regularUser = User::factory()->create(); 19 | }); 20 | 21 | test('it redirects unauthenticated users to login page', function () { 22 | $this->get(route('admin.login.history.index')) 23 | ->assertRedirect(route('login')); 24 | }); 25 | 26 | test('it denies access to users without login history permission', function () { 27 | $this->actingAs($this->regularUser) 28 | ->get(route('admin.login.history.index')) 29 | ->assertForbidden(); 30 | }); 31 | 32 | test('it allows access to users with login history permission', function () { 33 | $this->actingAs($this->adminUser) 34 | ->get(route('admin.login.history.index')) 35 | ->assertStatus(200) 36 | ->assertInertia( 37 | fn (Assert $page) => $page 38 | ->component('Admin/IndexLoginHistoryPage') 39 | ->has('loginHistory') 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /app/Traits/PersonalisationsHelper.php: -------------------------------------------------------------------------------- 1 | tap( 16 | Personalisation::first() ?? new Personalisation(), 17 | function (Personalisation $personalisation) { 18 | if ($personalisation->favicon && !Storage::disk('public')->exists($personalisation->favicon)) { 19 | $personalisation->favicon = null; 20 | } 21 | } 22 | ); 23 | 24 | if (!$useCaching) { 25 | return $loader(); 26 | } 27 | 28 | try { 29 | return Cache::rememberForever($this->cacheName, $loader); 30 | } catch (\Exception $e) { 31 | // If cache fails (e.g., table doesn't exist during migration), fall back to direct query 32 | return $loader(); 33 | } 34 | } 35 | 36 | protected function clearPersonalisationCache(): void 37 | { 38 | try { 39 | Cache::forget($this->cacheName); 40 | } catch (\Exception $e) { 41 | // Silently fail if cache is not available 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Models/AppNotification.php: -------------------------------------------------------------------------------- 1 | 'array', 37 | 'sent_as_scheduled' => 'boolean', 38 | 'scheduled_on' => 'datetime', 39 | 'auto_expire_on' => 'datetime', 40 | 'created_at' => 'datetime', 41 | 'updated_at' => 'datetime', 42 | 'deleted_at' => 'datetime', 43 | ]; 44 | 45 | public function user() 46 | { 47 | return $this->belongsTo(User::class); 48 | } 49 | 50 | public function reads() 51 | { 52 | return $this->hasMany(AppNotificationRead::class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Jobs/CleanupDeletedAppNotificationsJob.php: -------------------------------------------------------------------------------- 1 | cleanupDeleted($days); 27 | 28 | if (config('guacpanel.notifications.auto_clean_send_email')) { 29 | $to = (string) config('guacpanel.notifications.auto_clean_send_email_to', ''); 30 | 31 | if ($to !== '') { 32 | Mail::to($to)->send(new NotificationsCleanupReport( 33 | deleted: (int) $result['deleted'], 34 | cutoff: $result['cutoff'], 35 | days: (int) $result['days'], 36 | )); 37 | } 38 | } 39 | 40 | return (int) $result['deleted']; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Controllers/Notifications/AppNotificationPageController.php: -------------------------------------------------------------------------------- 1 | query('per_page', 25); 18 | $perPage = $perPageRaw === 'all' ? 'all' : (int) $perPageRaw; 19 | $perPage = $perPage === 'all' ? 'all' : ($perPage > 0 ? $perPage : 25); 20 | 21 | $filters = [ 22 | 'scope' => (string) $request->query('scope', 'all'), 23 | 'read' => (string) $request->query('read', 'all'), 24 | 'dismissed' => (string) $request->query('dismissed', 'all'), 25 | 'type' => (string) $request->query('type', 'all'), 26 | 'search' => (string) $request->query('search', ''), 27 | 'sort' => (string) $request->query('sort', 'newest'), 28 | 'per_page' => $perPage, 29 | ]; 30 | 31 | return Inertia::render('Notifications/NotificationsIndex', [ 32 | 'filters' => $filters, 33 | 'notifications' => fn () => $this->resolveNotifications($request, $filters['per_page'], $filters), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Services/Notifications/AppNotificationScheduledSendService.php: -------------------------------------------------------------------------------- 1 | whereNotNull('scheduled_on') 18 | ->where('scheduled_on', '<=', $now) 19 | ->where('sent_as_scheduled', false) 20 | ->whereNull('deleted_at') 21 | ->where(function ($q) use ($now) { 22 | $q->whereNull('auto_expire_on') 23 | ->orWhere('auto_expire_on', '>', $now); 24 | }) 25 | ->orderBy('scheduled_on') 26 | ->chunkById(200, function ($chunk) use (&$sent, $dryRun) { 27 | foreach ($chunk as $notification) { 28 | if ($dryRun) { 29 | $sent++; 30 | continue; 31 | } 32 | 33 | event(new AppNotificationCreated($notification)); 34 | 35 | $notification->forceFill([ 36 | 'sent_as_scheduled' => true, 37 | ])->save(); 38 | 39 | $sent++; 40 | } 41 | }); 42 | 43 | return $sent; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 16 | ->after('password') 17 | ->nullable(); 18 | 19 | $table->text('two_factor_recovery_codes') 20 | ->after('two_factor_secret') 21 | ->nullable(); 22 | 23 | if (Fortify::confirmsTwoFactorAuthentication()) { 24 | $table->timestamp('two_factor_confirmed_at') 25 | ->after('two_factor_recovery_codes') 26 | ->nullable(); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::table('users', function (Blueprint $table) { 37 | $table->dropColumn(array_merge([ 38 | 'two_factor_secret', 39 | 'two_factor_recovery_codes', 40 | ], Fortify::confirmsTwoFactorAuthentication() ? [ 41 | 'two_factor_confirmed_at', 42 | ] : [])); 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /database/migrations/2025_12_12_092732_create_app_notification_reads_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 12 | 13 | $table->foreignUlid('app_notification_id') 14 | ->constrained('app_notifications') 15 | ->cascadeOnDelete(); 16 | 17 | $table->foreignUlid('user_id') 18 | ->constrained('users') 19 | ->cascadeOnDelete(); 20 | 21 | $table->timestamp('read_at')->nullable(); 22 | $table->timestamp('dismissed_at')->nullable(); 23 | $table->timestamp('u_del_notif_at')->nullable(); 24 | $table->timestamp('deleted_at')->nullable(); 25 | $table->timestamps(); 26 | 27 | $table->unique(['app_notification_id', 'user_id']); 28 | 29 | $table->index(['user_id', 'read_at']); 30 | $table->index(['user_id', 'dismissed_at']); 31 | $table->index(['user_id', 'u_del_notif_at'], 'anr_u_d_notif_at_idx'); 32 | $table->index(['user_id', 'deleted_at']); 33 | }); 34 | } 35 | 36 | public function down(): void 37 | { 38 | Schema::dropIfExists('app_notification_reads'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /tests/Feature/AdminPermissionRoleControllerTest.php: -------------------------------------------------------------------------------- 1 | 'view-permissions-roles']); 13 | 14 | $this->adminUser = User::factory()->create(); 15 | $this->adminUser->givePermissionTo('view-permissions-roles'); 16 | 17 | $this->regularUser = User::factory()->create(); 18 | 19 | $this->testToken = 'test-token'; 20 | }); 21 | 22 | test('it redirects unauthenticated users to login page', function () { 23 | $this->get(route('admin.permission.role.index')) 24 | ->assertRedirect(route('login')); 25 | }); 26 | 27 | test('it allows access to users with manage permissions and roles permission', function () { 28 | $this->actingAs($this->adminUser) 29 | ->withSession(['_token' => $this->testToken]) 30 | ->get(route('admin.permission.role.index')) 31 | ->assertStatus(200) 32 | ->assertInertia( 33 | fn ($page) => $page->component('Admin/PermissionRole/IndexPermissionRolePage') 34 | ); 35 | }); 36 | 37 | test('it denies access to users without manage permissions and roles permission', function () { 38 | $this->actingAs($this->regularUser) 39 | ->withSession(['_token' => $this->testToken]) 40 | ->get(route('admin.permission.role.index')) 41 | ->assertForbidden(); 42 | }); 43 | -------------------------------------------------------------------------------- /app/Policies/SettingPolicy.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 15 | $table->string('name'); 16 | $table->string('user_slug')->unique()->nullable(false); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password')->nullable(); 20 | $table->string('location')->nullable(); 21 | $table->boolean('force_password_change')->default(false); 22 | $table->boolean('disable_account')->default(false); 23 | $table->string('locale')->default('en'); 24 | $table->timestamp('last_login_at')->nullable(); 25 | $table->string('last_login_ip')->nullable(); 26 | $table->timestamp('password_changed_at')->nullable(); 27 | $table->timestamp('password_expiry_at')->nullable(); 28 | $table->rememberToken(); 29 | $table->softDeletes(); 30 | $table->timestamps(); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | */ 37 | public function down(): void 38 | { 39 | Schema::dropIfExists('users'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | $input 19 | */ 20 | public function create(array $input): User 21 | { 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | 'email' => [ 25 | 'required', 26 | 'string', 27 | 'email', 28 | 'max:255', 29 | Rule::unique(User::class), 30 | ], 31 | 'password' => $this->passwordRules(), 32 | ])->validate(); 33 | 34 | $now = now(); 35 | $user = User::create([ 36 | 'name' => $input['name'], 37 | 'email' => $input['email'], 38 | 'password' => Hash::make($input['password']), 39 | 'password_changed_at' => $now, 40 | 'password_expiry_at' => $now->addMonths(3), 41 | ]); 42 | 43 | $user->assignRole(config('seeders.users.regular.role')); 44 | 45 | session()->flash('success', __('notifications.register.pw_success_auto_login_enabled')); 46 | 47 | return $user; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Observers/PersonalisationObserver.php: -------------------------------------------------------------------------------- 1 | clearPersonalisationCache(); 18 | } 19 | 20 | /** 21 | * Handle the Personalisation "updated" event. 22 | */ 23 | public function updated(Personalisation $personalisation): void 24 | { 25 | $this->clearPersonalisationCache(); 26 | } 27 | 28 | /** 29 | * same as "updated" event. 30 | */ 31 | public function saved(Personalisation $personalisation): void 32 | { 33 | // $this->clearPersonalisationCache(); 34 | } 35 | 36 | /** 37 | * Handle the Personalisation "deleted" event. 38 | */ 39 | public function deleted(Personalisation $personalisation): void 40 | { 41 | $this->clearPersonalisationCache(); 42 | } 43 | 44 | /** 45 | * Handle the Personalisation "restored" event. 46 | */ 47 | public function restored(Personalisation $personalisation): void 48 | { 49 | $this->clearPersonalisationCache(); 50 | } 51 | 52 | /** 53 | * Handle the Personalisation "force deleted" event. 54 | */ 55 | public function forceDeleted(Personalisation $personalisation): void 56 | { 57 | $this->clearPersonalisationCache(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/Pages/ChartsController.php: -------------------------------------------------------------------------------- 1 | year; 16 | 17 | $metrics = FinancialMetric::select(DB::raw('EXTRACT(MONTH FROM date) as month_number'), 'type', DB::raw('SUM(amount) as total')) 18 | ->whereYear('date', $currentYear) 19 | ->groupBy('month_number', 'type') 20 | ->orderBy('month_number') 21 | ->get(); 22 | 23 | $months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 24 | 25 | $income = array_fill_keys($months, 0); 26 | $expense = array_fill_keys($months, 0); 27 | 28 | foreach ($metrics as $metric) { 29 | $monthName = Carbon::create($currentYear, $metric->month_number)->format('M'); 30 | if ($metric->type === 'income') { 31 | $income[$monthName] = (float) $metric->total; 32 | } else { 33 | $expense[$monthName] = (float) $metric->total; 34 | } 35 | } 36 | 37 | return Inertia::render('Charts', [ 38 | 'financialMetrics' => [ 39 | 'months' => $months, 40 | 'income' => $income, 41 | 'expense' => $expense, 42 | ], 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /resources/views/emails/goodbye-user-mail.blade.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | @lang('emails.goodbye.greeting', ['username' => $user->name]) 4 |

5 | 6 |

7 | @lang('emails.goodbye.common_line_one') 8 |

9 | 10 | @if ($restoreEnabled) 11 |

12 | @lang('emails.goodbye.restore_line_one', ['days' => $daysToRestore]) 13 |

14 | 15 | 16 | @lang('emails.goodbye.restore_btn_text') 17 | 18 | @endif 19 | 20 |

21 | @if ($user->auto_destroy) 22 | @lang('emails.goodbye.common_line_two', ['date' => $autoDestroyDateParsed, 'days' => $daysToRestore]) 23 | @endif 24 | 25 | @lang('emails.goodbye.common_line_three') 26 |

27 | 28 |

29 | 30 | @lang('emails.goodbye.goodbye') 31 | 32 |

33 | 34 |

35 | {{ config('app.name') }} 36 |

37 | 38 | @if ($restoreEnabled) 39 | @isset($url) 40 | 41 | @lang( 42 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n" . 43 | 'into your web browser:', 44 | [ 45 | 'actionText' => __('emails.goodbye.restore_btn_text'), 46 | ] 47 | ) 48 | [{{ $url }}]({{ $url }}) 49 | 50 | @endisset 51 | @endif 52 |
53 | -------------------------------------------------------------------------------- /config/inertia.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'enabled' => true, 20 | 'url' => 'http://127.0.0.1:13714', 21 | // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), 22 | ], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Testing 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The values described here are used to locate Inertia components on the 30 | | filesystem. For instance, when using `assertInertia`, the assertion 31 | | attempts to locate the component as a file relative to the paths. 32 | | 33 | */ 34 | 35 | 'testing' => [ 36 | 'ensure_pages_exist' => true, 37 | 38 | 'page_paths' => [ 39 | resource_path('js/pages'), 40 | ], 41 | 42 | 'page_extensions' => [ 43 | 'js', 44 | 'jsx', 45 | 'svelte', 46 | 'ts', 47 | 'tsx', 48 | 'vue', 49 | ], 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /database/migrations/2025_12_12_084613_create_app_notifications_table.php: -------------------------------------------------------------------------------- 1 | ulid('id')->primary(); 12 | 13 | $table->foreignUlid('user_id') 14 | ->nullable() 15 | ->constrained('users') 16 | ->nullOnDelete(); 17 | 18 | $table->string('scope')->default('user'); // user|system|release 19 | $table->string('type')->default('info'); // info|success|warning|error 20 | $table->string('title')->nullable(); 21 | $table->text('message'); 22 | $table->json('data')->nullable(); 23 | 24 | $table->boolean('sent_as_scheduled')->default(false)->index(); 25 | $table->timestamp('scheduled_on')->nullable()->index(); 26 | $table->timestamp('auto_expire_on')->nullable(); 27 | $table->timestamps(); 28 | $table->softDeletes(); 29 | 30 | $table->index(['sent_as_scheduled', 'scheduled_on'], 'app_notifications_sched_send_idx'); 31 | $table->index(['scope', 'created_at']); 32 | $table->index('auto_expire_on'); 33 | $table->index('deleted_at'); 34 | }); 35 | } 36 | 37 | public function down(): void 38 | { 39 | Schema::dropIfExists('app_notifications'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('queue')->index(); 16 | $table->longText('payload'); 17 | $table->unsignedTinyInteger('attempts'); 18 | $table->unsignedInteger('reserved_at')->nullable(); 19 | $table->unsignedInteger('available_at'); 20 | $table->unsignedInteger('created_at'); 21 | }); 22 | 23 | Schema::create('job_batches', function (Blueprint $table) { 24 | $table->string('id')->primary(); 25 | $table->string('name'); 26 | $table->integer('total_jobs'); 27 | $table->integer('pending_jobs'); 28 | $table->integer('failed_jobs'); 29 | $table->longText('failed_job_ids'); 30 | $table->mediumText('options')->nullable(); 31 | $table->integer('cancelled_at')->nullable(); 32 | $table->integer('created_at'); 33 | $table->integer('finished_at')->nullable(); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | */ 40 | public function down(): void 41 | { 42 | Schema::dropIfExists('jobs'); 43 | Schema::dropIfExists('job_batches'); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /resources/js/Components/DebouncedInput.vue: -------------------------------------------------------------------------------- 1 | 56 | 60 | -------------------------------------------------------------------------------- /app/Events/AppNotificationStateChanged.php: -------------------------------------------------------------------------------- 1 | userId); 31 | } 32 | 33 | public function broadcastAs(): string 34 | { 35 | return 'app-notification.state'; 36 | } 37 | 38 | public function broadcastWith(): array 39 | { 40 | return [ 41 | 'id' => $this->notificationId, 42 | 'scope' => $this->scope, 43 | 'read_at' => $this->readAt, 44 | 'dismissed_at' => $this->dismissedAt, 45 | 'action' => $this->action, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/js/Pages/Admin/Notifications/AdminDeletedNotificaionsIndex.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import { createInertiaApp, Link, router } from '@inertiajs/vue3' 3 | import { ZiggyVue } from 'ziggy-js' 4 | import NProgress from 'nprogress' 5 | import '@css/app.css' 6 | import '@js/utils/darkMode.js' 7 | import { initializeTheme } from '@js/utils/themeInit' 8 | import InstantSearch from 'vue-instantsearch/vue3/es' 9 | import Default from '@js/Layouts/Default.vue' 10 | import Auth from '@js/Layouts/Auth.vue' 11 | import VueDOMPurifyHTML from 'vue-dompurify-html' 12 | import '@js/echo' 13 | 14 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' 15 | 16 | NProgress.configure({ showSpinner: false }) 17 | router.on('start', () => NProgress.start()) 18 | router.on('finish', () => NProgress.done()) 19 | router.on('error', () => NProgress.done()) 20 | 21 | const appName = import.meta.env.VITE_APP_NAME ?? 'GuacPanel' 22 | 23 | createInertiaApp({ 24 | progress: { 25 | delay: 50, 26 | color: '#ffa500', 27 | includeCSS: true, 28 | }, 29 | title: title => `${title} - ${appName}`, 30 | resolve: name => 31 | resolvePageComponent( 32 | `./Pages/${name}.vue`, 33 | import.meta.glob('./Pages/**/*.vue', { eager: true }) 34 | ), 35 | setup({ el, App, props, plugin }) { 36 | const app = createApp({ render: () => h(App, props) }) 37 | app 38 | .use(plugin) 39 | .use(ZiggyVue) 40 | .use(InstantSearch) 41 | .use(VueDOMPurifyHTML) 42 | .component('Link', Link) 43 | .component('Default', Default) 44 | .component('Auth', Auth) 45 | .mount(el) 46 | return app 47 | }, 48 | }) 49 | 50 | initializeTheme() 51 | -------------------------------------------------------------------------------- /resources/views/vendor/health/list-cli.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(count($checkResults?->storedCheckResults ?? [])) 3 |
4 | Laravel Health Check Results 5 | 6 | Last ran all the checks 7 | @if ($lastRanAt->diffInMinutes() < 1) 8 | just now 9 | @else 10 | {{ $lastRanAt->diffForHumans() }} 11 | @endif 12 | 13 |
14 | @foreach ($checkResults->storedCheckResults as $result) 15 |
16 | 17 | 18 | {{ ucfirst($result->status) }} 19 | 20 | 21 | {{ $result->label }} 22 | 23 | {{ $result->shortSummary }} 24 |
25 | @if ($result->notificationMessage) 26 |
27 | ⇂ {{ $result->notificationMessage }} 28 |
29 | @endif 30 | @endforeach 31 | @else 32 |
33 | No checks have run yet...
34 | Please execute this command: 35 |

36 | php artisan health:check 37 |
38 | @endif 39 |
40 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ config('app.name') }} 5 | 6 | 7 | 8 | 9 | 26 | {{ $head ?? '' }} 27 | 28 | 29 | 30 | 31 | 32 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /config/notify.php: -------------------------------------------------------------------------------- 1 | env('NOTIFY_THEME', 'light'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Notification timeout 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Defines the number of seconds during which the notification will be visible. 24 | | 25 | */ 26 | 27 | 'timeout' => 5000, 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Preset Messages 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Define any preset messages here that can be reused. 35 | | Available model: connect, drake, emotify, smiley, toast 36 | | 37 | */ 38 | 39 | 'preset-messages' => [ 40 | // An example preset 'user updated' Connectify notification. 41 | 'user-updated' => [ 42 | 'message' => 'The user has been updated successfully.', 43 | 'type' => 'success', 44 | 'model' => 'connect', 45 | 'title' => 'User Updated', 46 | ], 47 | ], 48 | 49 | ]; 50 | -------------------------------------------------------------------------------- /database/migrations/2025_06_26_082925_create_health_tables.php: -------------------------------------------------------------------------------- 1 | getConnectionName(); 13 | $tableName = EloquentHealthResultStore::getHistoryItemInstance()->getTable(); 14 | 15 | Schema::connection($connection)->create($tableName, function (Blueprint $table) { 16 | $table->id(); 17 | 18 | $table->string('check_name'); 19 | $table->string('check_label'); 20 | $table->string('status'); 21 | $table->text('notification_message')->nullable(); 22 | $table->string('short_summary')->nullable(); 23 | $table->json('meta'); 24 | $table->timestamp('ended_at'); 25 | $table->uuid('batch'); 26 | $table->timestamps(); 27 | }); 28 | 29 | Schema::connection($connection)->table($tableName, function (Blueprint $table) { 30 | $table->index('created_at'); 31 | $table->index('batch'); 32 | }); 33 | } 34 | 35 | public function down(): void 36 | { 37 | $connection = (new HealthCheckResultHistoryItem())->getConnectionName(); 38 | $tableName = EloquentHealthResultStore::getHistoryItemInstance()->getTable(); 39 | 40 | Schema::connection($connection)->dropIfExists($tableName); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /app/Http/Requests/Notifications/ListNotificationsRequest.php: -------------------------------------------------------------------------------- 1 | user(); 12 | 13 | return (bool) $user && $user->can('view-notifications'); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | 'scope' => ['sometimes', 'string', 'in:all,user,system,release'], 20 | 'read' => ['sometimes', 'string', 'in:all,read,unread'], 21 | 'dismissed' => ['sometimes', 'string', 'in:all,dismissed,undismissed'], 22 | 'type' => ['sometimes', 'string', 'in:all,info,success,warning,error'], 23 | 'search' => ['sometimes', 'nullable', 'string', 'max:255'], 24 | 'sort' => ['sometimes', 'string', 'in:newest,oldest'], 25 | 'per_page' => [ 26 | 'sometimes', 27 | function ($attribute, $value, $fail) { 28 | if ($value === 'all') { 29 | return; 30 | } 31 | 32 | if (!is_numeric($value)) { 33 | $fail('The per_page must be an integer or "all".'); 34 | 35 | return; 36 | } 37 | 38 | $n = (int) $value; 39 | 40 | if ($n < 1 || $n > 1000) { 41 | $fail('The per_page must be between 1 and 1000, or "all".'); 42 | } 43 | }, 44 | ], 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/AdminSettingController.php: -------------------------------------------------------------------------------- 1 | middleware('permission:manage-settings'); 16 | } 17 | 18 | public function index() 19 | { 20 | return Inertia::render('Admin/IndexSettingPage'); 21 | } 22 | 23 | public function show() 24 | { 25 | $systemSettings = Setting::first() ?? new Setting(); 26 | 27 | return Inertia::render('Admin/IndexManageSettingPage', [ 28 | 'systemSettings' => $systemSettings, 29 | 'canResetPassword' => Features::enabled(Features::resetPasswords()), 30 | 'canRegister' => Features::enabled(Features::registration()), 31 | 'twoFactorEnabled' => Features::enabled(Features::twoFactorAuthentication()), 32 | ]); 33 | } 34 | 35 | public function update(Request $request) 36 | { 37 | $validatedData = $request->validate([ 38 | 'password_expiry' => ['boolean'], 39 | 'passwordless_login' => ['boolean'], 40 | 'two_factor_authentication' => ['boolean'], 41 | ]); 42 | 43 | if (!config('guacpanel.mfa_enabled')) { 44 | $validatedData['two_factor_authentication'] = false; 45 | } 46 | 47 | Setting::updateOrCreate([], $validatedData); 48 | 49 | return redirect()->back()->with('success', __('notifications.admin.settings_updated_successfully')); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 61 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /resources/js/Components/Icons/GitHubIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | --------------------------------------------------------------------------------