├── .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 |
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 |
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 |
13 |
14 |
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 |
4 |
5 |
6 |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }}
8 |
9 |
10 |
11 |
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 |
9 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
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 |
11 | @lang('emails.notifications_cleanup.deleted', ['count' => $deleted])
12 | @lang('emails.notifications_cleanup.cutoff', ['date' => $cutoff->format('m/j/Y @ g:i A')])
13 | @lang('emails.notifications_cleanup.days', ['days' => $days])
14 |
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 |
33 |
34 | {{ capitalize(role.name) }}
35 |
36 |
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 |
21 |
31 |
40 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/resources/js/Components/Common/NotificationTypeBadge.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
43 | {{ label }}
44 |
45 |
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 |
15 |
20 |
21 |
24 |
25 |
40 |
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 |
57 |
59 |
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 |
19 |
20 |
21 |
22 |
23 |
27 |
28 | Back
29 |
30 |
31 |
32 |
33 |
35 |
36 | This page will be implemented next for managing soft deleted notifications. We may not
37 | need it since we have a job to clean them up.
38 |
39 |
40 |
41 |
42 |
43 |
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 |
33 |
34 | {{ $header ?? '' }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {{ Illuminate\Mail\Markdown::parse($slot) }}
44 |
45 | {{ $subcopy ?? '' }}
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{ $footer ?? '' }}
53 |
54 |
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 |
21 |
22 |
23 |
24 | Forgot password
25 |
26 |
52 |
53 |
54 | Back to
55 |
56 | login
57 |
58 |
59 |
60 |
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 |
15 |
16 |
21 |
22 |
23 |
--------------------------------------------------------------------------------