├── public
├── favicon.ico
├── robots.txt
├── index.php
└── .htaccess
├── database
├── .gitignore
├── seeders
│ └── DatabaseSeeder.php
├── migrations
│ ├── 0001_01_01_000001_create_cache_table.php
│ ├── 0001_01_01_000000_create_users_table.php
│ └── 0001_01_01_000002_create_jobs_table.php
└── factories
│ └── UserFactory.php
├── bootstrap
├── cache
│ └── .gitignore
├── providers.php
└── app.php
├── storage
├── logs
│ └── .gitignore
├── pail
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
└── framework
│ ├── testing
│ └── .gitignore
│ ├── views
│ └── .gitignore
│ ├── cache
│ ├── data
│ │ └── .gitignore
│ └── .gitignore
│ ├── sessions
│ └── .gitignore
│ └── .gitignore
├── docker
└── local
│ ├── web
│ ├── php.ini
│ ├── supervisord.conf
│ ├── start-container
│ └── Dockerfile
│ └── database
│ ├── pgsql
│ └── create-testing-database.sql
│ └── mysql
│ └── create-testing-database.sh
├── app
├── Http
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── Auth
│ │ │ ├── EmailVerificationPromptController.php
│ │ │ ├── EmailVerificationNotificationController.php
│ │ │ ├── VerifyEmailController.php
│ │ │ ├── ConfirmablePasswordController.php
│ │ │ ├── AuthenticatedSessionController.php
│ │ │ ├── RegisteredUserController.php
│ │ │ ├── PasswordResetLinkController.php
│ │ │ └── NewPasswordController.php
│ │ └── Settings
│ │ │ ├── PasswordController.php
│ │ │ └── ProfileController.php
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ └── HandleInertiaRequests.php
│ └── Requests
│ │ ├── ProfileUpdateRequest.php
│ │ └── Auth
│ │ └── LoginRequest.php
├── Exceptions
│ └── ErrorToastException.php
├── helpers.php
├── Providers
│ └── AppServiceProvider.php
└── Models
│ └── User.php
├── tests
├── TestCase.php
├── Unit
│ └── ExampleTest.php
└── Feature
│ ├── ExampleTest.php
│ ├── Auth
│ ├── RegistrationTest.php
│ ├── PasswordConfirmationTest.php
│ ├── AuthenticationTest.php
│ ├── EmailVerificationTest.php
│ └── PasswordResetTest.php
│ └── Settings
│ ├── PasswordUpdateTest.php
│ └── ProfileUpdateTest.php
├── .gitattributes
├── routes
├── console.php
├── web.php
├── settings.php
└── auth.php
├── resources
├── js
│ ├── components
│ │ ├── ClientOnly.vue
│ │ ├── ThemePresetSelector.vue
│ │ ├── InputErrors.vue
│ │ ├── NavLogoLink.vue
│ │ ├── Container.vue
│ │ ├── PageTitleSection.vue
│ │ ├── SelectColorModeButton.vue
│ │ ├── FlashMessages.vue
│ │ ├── primevue
│ │ │ └── menu
│ │ │ │ ├── TabMenu.vue
│ │ │ │ ├── Menu.vue
│ │ │ │ ├── Breadcrumb.vue
│ │ │ │ ├── ContextMenu.vue
│ │ │ │ ├── TieredMenu.vue
│ │ │ │ ├── Menubar.vue
│ │ │ │ └── PanelMenu.vue
│ │ ├── DeleteUserModal.vue
│ │ ├── ApplicationLogo.vue
│ │ └── PopupMenuButton.vue
│ ├── layouts
│ │ ├── AppLayout.vue
│ │ ├── GuestAuthLayout.vue
│ │ ├── UserSettingsLayout.vue
│ │ └── app
│ │ │ └── HeaderLayout.vue
│ ├── utils.ts
│ ├── pages
│ │ ├── Dashboard.vue
│ │ ├── settings
│ │ │ └── Appearance.vue
│ │ ├── auth
│ │ │ ├── ConfirmPassword.vue
│ │ │ ├── VerifyEmail.vue
│ │ │ ├── ForgotPassword.vue
│ │ │ ├── ResetPassword.vue
│ │ │ ├── Register.vue
│ │ │ └── Login.vue
│ │ └── Error.vue
│ ├── theme
│ │ ├── global-pt.ts
│ │ ├── bootstrap-preset.ts
│ │ ├── breeze-preset.ts
│ │ ├── warm-preset.ts
│ │ ├── noir-preset.ts
│ │ └── enterprise-preset.ts
│ ├── types
│ │ ├── global.d.ts
│ │ ├── paginiation.d.ts
│ │ └── index.d.ts
│ ├── composables
│ │ ├── useSiteColorMode.ts
│ │ ├── useThemePreset.ts
│ │ ├── useAppLayout.ts
│ │ └── usePaginatedDataTable.ts
│ ├── ssr.ts
│ └── app.ts
├── css
│ ├── app.css
│ └── tailwind.css
└── views
│ └── app.blade.php
├── phpstan.neon
├── .editorconfig
├── artisan
├── .gitignore
├── pint.json
├── README.md
├── LICENSE
├── eslint.config.ts
├── config
├── services.php
├── filesystems.php
├── cache.php
├── mail.php
├── queue.php
├── auth.php
├── app.php
└── logging.php
├── phpunit.xml
├── .env.example
├── vite.config.ts
├── .devcontainer
└── devcontainer.json
├── docker-compose.local.yml
├── package.json
└── composer.json
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/pail/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/bootstrap/providers.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
8 | })->purpose('Display an inspiring quote')->hourly();
9 |
--------------------------------------------------------------------------------
/docker/local/database/mysql/create-testing-database.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
4 | CREATE DATABASE IF NOT EXISTS testing;
5 | GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
6 | EOSQL
7 |
--------------------------------------------------------------------------------
/resources/js/components/ClientOnly.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/larastan/larastan/extension.neon
3 | - vendor/nesbot/carbon/extension.neon
4 |
5 | parameters:
6 | level: 8
7 | treatPhpDocTypesAsCertain: false
8 | paths:
9 | - app
10 | - database/factories
11 | - database/seeders
12 | - routes
13 |
--------------------------------------------------------------------------------
/tests/Unit/ExampleTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{yml,yaml}]
15 | indent_size = 2
16 |
17 | [docker-compose.yml]
18 | indent_size = 4
19 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | handleCommand(new ArgvInput);
14 |
15 | exit($status);
16 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [
15 | 'colorScheme'
16 | ];
17 | }
18 |
--------------------------------------------------------------------------------
/docker/local/web/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 | user=root
4 | logfile=/var/log/supervisor/supervisord.log
5 | pidfile=/var/run/supervisord.pid
6 |
7 | [program:php]
8 | command=%(ENV_SUPERVISOR_PHP_COMMAND)s
9 | user=%(ENV_SUPERVISOR_PHP_USER)s
10 | environment=LARAVEL_SAIL="1"
11 | stdout_logfile=/dev/stdout
12 | stdout_logfile_maxbytes=0
13 | stderr_logfile=/dev/stderr
14 | stderr_logfile_maxbytes=0
15 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | html {
2 | /* font size will determine the component/utility scaling */
3 | font-size: 14px;
4 | line-height: 1.15;
5 | }
6 |
7 | body {
8 | margin: 0 !important;
9 | padding: 0 !important;
10 | }
11 |
12 | #app {
13 | visibility: hidden;
14 | }
15 |
16 | #nprogress .bar {
17 | z-index: 9999999 !important;
18 | }
19 |
20 | .lucide {
21 | width: 16px;
22 | height: 16px;
23 | }
24 |
--------------------------------------------------------------------------------
/resources/js/layouts/AppLayout.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/resources/js/components/ThemePresetSelector.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/Feature/ExampleTest.php:
--------------------------------------------------------------------------------
1 | get('/');
16 |
17 | $response->assertStatus(200);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 |
2 | defineProps<{
3 | errors?: string[]
4 | }>()
5 |
6 |
7 |
8 |
12 |
19 | {{ errorMessage }}
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/resources/js/utils.ts:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge'
2 | import { mergeProps } from 'vue'
3 |
4 | export const ptViewMerge = (
5 | globalPTProps = {} as any,
6 | selfPTProps = {} as any,
7 | datasets: any
8 | ) => {
9 | const { class: globalClass, ...globalRest } = globalPTProps
10 | const { class: selfClass, ...selfRest } = selfPTProps
11 |
12 | return mergeProps(
13 | { class: twMerge(globalClass, selfClass) },
14 | globalRest,
15 | selfRest,
16 | datasets
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.phpunit.cache
2 | /bootstrap/ssr
3 | /node_modules
4 | /public/build
5 | /public/hot
6 | /public/storage
7 | /storage/*.key
8 | /storage/pail
9 | /resources/js/actions
10 | /resources/js/routes
11 | /resources/js/wayfinder
12 | /vendor
13 | .DS_Store
14 | .env
15 | .env.backup
16 | .env.production
17 | .phpactor.json
18 | .phpunit.result.cache
19 | Homestead.json
20 | Homestead.yaml
21 | npm-debug.log
22 | yarn-error.log
23 | /auth.json
24 | /.fleet
25 | /.idea
26 | /.nova
27 | /.vscode
28 | /.zed
29 |
30 | components.d.ts
31 | *.tsbuildinfo
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | handleRequest(Request::capture());
18 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | User::factory()->create([
19 | 'name' => 'Test User',
20 | 'email' => 'test@example.com',
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | Application::VERSION,
10 | 'phpVersion' => PHP_VERSION,
11 | ]);
12 | })->name('welcome');
13 |
14 | Route::get('/dashboard', function () {
15 | return Inertia::render('Dashboard');
16 | })->middleware(['auth', 'verified'])->name('dashboard');
17 |
18 | require __DIR__ . '/settings.php';
19 | require __DIR__ . '/auth.php';
20 |
--------------------------------------------------------------------------------
/resources/js/pages/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | You are logged in!
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "psr12",
3 | "rules": {
4 | "array_indentation": true,
5 | "array_syntax": true,
6 | "fully_qualified_strict_types": true,
7 | "method_chaining_indentation": true,
8 | "no_trailing_comma_in_singleline_function_call": true,
9 | "no_trailing_comma_in_singleline": true,
10 | "no_unused_imports": true,
11 | "no_whitespace_before_comma_in_array": true,
12 | "single_import_per_statement": true,
13 | "whitespace_after_comma_in_array": true,
14 | "trailing_comma_in_multiline": false
15 | }
16 | }
--------------------------------------------------------------------------------
/resources/js/theme/global-pt.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Global pass through styling for components
3 | * https://primevue.org/passthrough/#global
4 | */
5 | export default {
6 | dialog: {
7 | root: {
8 | class: 'm-4 sm:m-0'
9 | },
10 | },
11 | toast: {
12 | root: {
13 | // Full width/centered on mobile, bottom right desktop
14 | class: 'fixed! left-4! right-4! bottom-4! w-auto! md:right-8! md:bottom-8! sm:w-[25rem]! sm:not-fixed! sm:left-auto! sm:ml-auto!'
15 | },
16 | message: {
17 | class: 'shadow-lg mb-0 mt-4'
18 | },
19 | },
20 | }
--------------------------------------------------------------------------------
/resources/js/components/NavLogoLink.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/resources/js/components/Container.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docker/local/web/start-container:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
4 | echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
5 | exit 1
6 | fi
7 |
8 | if [ ! -z "$WWWUSER" ]; then
9 | usermod -u $WWWUSER sail
10 | fi
11 |
12 | if [ ! -d /.composer ]; then
13 | mkdir /.composer
14 | fi
15 |
16 | chmod -R ugo+rw /.composer
17 |
18 | if [ $# -gt 0 ]; then
19 | if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
20 | exec "$@"
21 | else
22 | exec gosu $WWWUSER "$@"
23 | fi
24 | else
25 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
26 | fi
27 |
--------------------------------------------------------------------------------
/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/js/types/global.d.ts:
--------------------------------------------------------------------------------
1 | import { PageProps as InertiaPageProps } from '@inertiajs/core'
2 | import { AxiosInstance } from 'axios'
3 | import { route as ziggyRoute } from 'ziggy-js'
4 | import { AppPageProps } from './'
5 |
6 | declare global {
7 | interface Window {
8 | axios: AxiosInstance;
9 | }
10 |
11 | var route: typeof ziggyRoute
12 | }
13 |
14 | declare module 'vue' {
15 | interface ComponentCustomProperties {
16 | route: typeof ziggyRoute
17 | }
18 | }
19 |
20 | declare module '@inertiajs/core' {
21 | interface PageProps extends InertiaPageProps, AppPageProps { }
22 | export interface InertiaConfig {
23 | errorValueType: string[]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationPromptController.php:
--------------------------------------------------------------------------------
1 | user()?->hasVerifiedEmail()
19 | ? redirect()->intended(route('dashboard', absolute: false))
20 | : Inertia::render('auth/VerifyEmail', ['status' => session('status')]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/resources/js/components/PageTitleSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationNotificationController.php:
--------------------------------------------------------------------------------
1 | user();
17 |
18 | if ($user?->hasVerifiedEmail()) {
19 | return redirect()->intended(route('dashboard', absolute: false));
20 | }
21 |
22 | $user?->sendEmailVerificationNotification();
23 |
24 | return back()->with('status', 'verification-link-sent');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/css/tailwind.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tailwindcss-primeui";
3 |
4 | @source '../../storage/framework/views/*.php';
5 | @source '../../resources/views/**/*.blade.php';
6 | @source '../../resources/js/**/*.vue';
7 | @source '../../resources/js/theme/*.js';
8 |
9 | @custom-variant dark (&:where(.dark, .dark *));
10 |
11 | @theme {
12 | --font-sans: Inter, sans-serif;
13 | --header-height: 4.5rem;
14 | }
15 |
16 | @utility dynamic-bg {
17 | @apply bg-surface-0 dark:bg-surface-900;
18 | }
19 |
20 | @utility dynamic-border {
21 | @apply border-surface-200 dark:border-surface-800;
22 | }
23 |
24 | @utility delete-menu-item {
25 | @apply text-red-500 dark:text-red-400 hover:bg-red-500/10 rounded-[var(--p-menu-item-border-radius)] transition-colors duration-[var(--p-menu-transition-duration)];
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Requests/ProfileUpdateRequest.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public function rules(): array
17 | {
18 | return [
19 | 'name' => ['required', 'string', 'max:255'],
20 | 'email' => [
21 | 'required',
22 | 'string',
23 | 'lowercase',
24 | 'email',
25 | 'max:255',
26 | Rule::unique(User::class)->ignore($this->user()?->id)
27 | ],
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/resources/views/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 | {{ config('app.name', 'Laravel') }}
12 |
13 |
14 |
18 |
22 |
23 |
24 | @routes
25 | @vite(['resources/js/app.ts', "resources/js/pages/{$page['component']}.vue"])
26 | @inertiaHead
27 |
28 |
29 |
30 | @inertia
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/RegistrationTest.php:
--------------------------------------------------------------------------------
1 | get('/register');
15 |
16 | $response->assertStatus(200);
17 | }
18 |
19 | public function test_new_users_can_register(): void
20 | {
21 | $response = $this->post('/register', [
22 | 'name' => 'Test User',
23 | 'email' => 'test@example.com',
24 | 'password' => 'password',
25 | 'password_confirmation' => 'password',
26 | ]);
27 |
28 | $this->assertAuthenticated();
29 | $response->assertRedirect(route('dashboard', absolute: false));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/routes/settings.php:
--------------------------------------------------------------------------------
1 | group(function () {
9 | Route::redirect('settings', '/settings/profile');
10 |
11 | Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
12 | Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
13 | Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
14 |
15 | Route::get('settings/password', [PasswordController::class, 'edit'])->name('password.edit');
16 | Route::put('settings/password', [PasswordController::class, 'update'])->name('password.update');
17 |
18 | Route::get('settings/appearance', function () {
19 | return Inertia::render('settings/Appearance');
20 | })->name('appearance');
21 | });
22 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000001_create_cache_table.php:
--------------------------------------------------------------------------------
1 | string('key')->primary();
15 | $table->mediumText('value');
16 | $table->integer('expiration');
17 | });
18 |
19 | Schema::create('cache_locks', function (Blueprint $table) {
20 | $table->string('key')->primary();
21 | $table->string('owner');
22 | $table->integer('expiration');
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 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/VerifyEmailController.php:
--------------------------------------------------------------------------------
1 | user();
19 |
20 | if ($user && $user->hasVerifiedEmail()) {
21 | return redirect()->intended(route('dashboard', absolute: false) . '?verified=1');
22 | }
23 |
24 | if ($user instanceof MustVerifyEmail && $user->markEmailAsVerified()) {
25 | event(new Verified($user));
26 | }
27 |
28 | return redirect()->intended(route('dashboard', absolute: false) . '?verified=1');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel + PrimeVue Starter Kit
2 |
3 | ## About
4 |
5 |   ![Static Badge]() ![Static Badge]() 
6 |
7 | A basic authentication starter kit using [Laravel](https://laravel.com/docs/master), [Intertia.js](https://inertiajs.com/), [PrimeVue](https://primevue.org/) components, and [Tailwind CSS](https://tailwindcss.com/).
8 |
9 | > [!TIP]
10 | > Do you need a separate Vue SPA front-end instead of using Inertia.js? Consider using the [PrimeVue SPA + Laravel API Starter Kit](https://github.com/connorabbas/laravel-api-primevue-starter-kit) instead.
11 |
12 | ## Resources
13 |
14 | [🌐 **Demo Application**](https://demo.laravel-primevue-starter-kit.com/)
15 |
16 | [📚 **Documentation**](https://connorabbas.github.io/laravel-primevue-starter-kit-docs/)
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Connor Abbas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/resources/js/types/paginiation.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Interface representing a pagination link.
3 | */
4 | export interface PaginationLink {
5 | url: string | null;
6 | label: string;
7 | active: boolean;
8 | }
9 |
10 | /**
11 | * Interface representing pagination metadata.
12 | */
13 | export interface PaginationMeta {
14 | current_page: number;
15 | from: number | null;
16 | last_page: number;
17 | path: string;
18 | per_page: number;
19 | to: number | null;
20 | total: number;
21 | }
22 |
23 | /**
24 | * Interface representing a Laravel Illuminate\Pagination\LengthAwarePaginator
25 | * @template T - The type of items in the paginator.
26 | */
27 | export interface LengthAwarePaginator {
28 | current_page: number;
29 | data: T[];
30 | first_page_url: string;
31 | from: number | null;
32 | total: number;
33 | per_page: number;
34 | last_page: number;
35 | last_page_url: string;
36 | next_page_url: string | null;
37 | path: string;
38 | to: number | null;
39 | prev_page_url: string | null;
40 | links?: PaginationLink[];
41 | meta?: PaginationMeta;
42 | }
43 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Settings/PasswordController.php:
--------------------------------------------------------------------------------
1 | validate([
29 | 'current_password' => ['required', 'current_password'],
30 | 'password' => ['required', Password::defaults(), 'confirmed'],
31 | ]);
32 |
33 | $request->user()?->update([
34 | 'password' => Hash::make($validated['password']),
35 | ]);
36 |
37 | return back();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/eslint.config.ts:
--------------------------------------------------------------------------------
1 | import vue from 'eslint-plugin-vue'
2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
3 |
4 | export default [
5 | // Global ignores
6 | {
7 | ignores: [
8 | 'node_modules',
9 | 'vendor',
10 | 'dist',
11 | 'public',
12 | 'bootstrap/ssr',
13 | ],
14 | },
15 | // Vue and TypeScript files
16 | ...defineConfigWithVueTs(
17 | vue.configs['flat/recommended'],
18 | vueTsConfigs.recommended,
19 | {
20 | rules: {
21 | 'vue/require-default-prop': 'off',
22 | 'vue/attribute-hyphenation': 'off',
23 | 'vue/v-on-event-hyphenation': 'off',
24 | 'vue/multi-word-component-names': 'off',
25 | 'vue/no-v-html': 'off',
26 | 'vue/html-indent': ['error', 4],
27 | '@typescript-eslint/no-explicit-any': 'off',
28 | indent: ['error', 4],
29 | semi: ['error', 'never'],
30 | 'linebreak-style': ['error', 'unix'],
31 | },
32 | },
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'token' => env('POSTMARK_TOKEN'),
19 | ],
20 |
21 | 'ses' => [
22 | 'key' => env('AWS_ACCESS_KEY_ID'),
23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
25 | ],
26 |
27 | 'resend' => [
28 | 'key' => env('RESEND_KEY'),
29 | ],
30 |
31 | 'slack' => [
32 | 'notifications' => [
33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
35 | ],
36 | ],
37 |
38 | ];
39 |
--------------------------------------------------------------------------------
/resources/js/components/SelectColorModeButton.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
32 |
33 | {{ option.label }}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/resources/js/pages/settings/Appearance.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 | Appearance settings
24 |
25 |
26 | Update your account's appearance settings
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/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/Models/User.php:
--------------------------------------------------------------------------------
1 | */
13 | use HasFactory;
14 | use Notifiable;
15 |
16 | /**
17 | * The attributes that are mass assignable.
18 | *
19 | * @var list
20 | */
21 | protected $fillable = [
22 | 'name',
23 | 'email',
24 | 'password',
25 | ];
26 |
27 | /**
28 | * The attributes that should be hidden for serialization.
29 | *
30 | * @var list
31 | */
32 | protected $hidden = [
33 | 'password',
34 | 'remember_token',
35 | ];
36 |
37 | /**
38 | * Get the attributes that should be cast.
39 | *
40 | * @return array
41 | */
42 | protected function casts(): array
43 | {
44 | return [
45 | 'email_verified_at' => 'datetime',
46 | 'password' => 'hashed',
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/PasswordConfirmationTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $response = $this->actingAs($user)->get('/confirm-password');
18 |
19 | $response->assertStatus(200);
20 | }
21 |
22 | public function test_password_can_be_confirmed(): void
23 | {
24 | $user = User::factory()->create();
25 |
26 | $response = $this->actingAs($user)->post('/confirm-password', [
27 | 'password' => 'password',
28 | ]);
29 |
30 | $response->assertRedirect();
31 | $response->assertSessionHasNoErrors();
32 | }
33 |
34 | public function test_password_is_not_confirmed_with_invalid_password(): void
35 | {
36 | $user = User::factory()->create();
37 |
38 | $response = $this->actingAs($user)->post('/confirm-password', [
39 | 'password' => 'wrong-password',
40 | ]);
41 |
42 | $response->assertSessionHasErrors();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ConfirmablePasswordController.php:
--------------------------------------------------------------------------------
1 | validate([
29 | 'email' => $request->user()?->email,
30 | 'password' => $request->password,
31 | ]);
32 |
33 | if (!$successfullyValidated) {
34 | throw ValidationException::withMessages([
35 | 'password' => __('auth.password'),
36 | ]);
37 | }
38 |
39 | $request->session()->put('auth.password_confirmed_at', time());
40 |
41 | return redirect()->intended(route('dashboard', absolute: false));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class UserFactory extends Factory
14 | {
15 | protected $model = User::class;
16 |
17 | /**
18 | * The current password being used by the factory.
19 | */
20 | protected static ?string $password;
21 |
22 | /**
23 | * Define the model's default state.
24 | *
25 | * @return array
26 | */
27 | public function definition(): array
28 | {
29 | return [
30 | 'name' => fake()->name(),
31 | 'email' => fake()->unique()->safeEmail(),
32 | 'email_verified_at' => now(),
33 | 'password' => static::$password ??= Hash::make('password'),
34 | 'remember_token' => Str::random(10),
35 | ];
36 | }
37 |
38 | /**
39 | * Indicate that the model's email address should be unverified.
40 | */
41 | public function unverified(): static
42 | {
43 | return $this->state(fn (array $attributes) => [
44 | 'email_verified_at' => null,
45 | ]);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME="Laravel + PrimeVue"
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_TIMEZONE=UTC
6 | APP_URL=http://localhost:8000
7 |
8 | APP_LOCALE=en
9 | APP_FALLBACK_LOCALE=en
10 | APP_FAKER_LOCALE=en_US
11 |
12 | APP_MAINTENANCE_DRIVER=file
13 | APP_MAINTENANCE_STORE=database
14 |
15 | BCRYPT_ROUNDS=12
16 |
17 | LOG_CHANNEL=stack
18 | LOG_STACK=single
19 | LOG_DEPRECATIONS_CHANNEL=null
20 | LOG_LEVEL=debug
21 |
22 | DB_CONNECTION=sqlite
23 | #DB_HOST=
24 | #DB_PORT=
25 | #DB_DATABASE=
26 | #DB_USERNAME=
27 | #DB_PASSWORD=
28 |
29 | SESSION_DRIVER=file
30 | SESSION_LIFETIME=120
31 | SESSION_ENCRYPT=false
32 | SESSION_PATH=/
33 | SESSION_DOMAIN=null
34 |
35 | BROADCAST_CONNECTION=log
36 | FILESYSTEM_DISK=local
37 | QUEUE_CONNECTION=sync
38 |
39 | CACHE_STORE=file
40 | CACHE_PREFIX=
41 |
42 | MEMCACHED_HOST=127.0.0.1
43 |
44 | REDIS_CLIENT=phpredis
45 | REDIS_HOST=127.0.0.1
46 | REDIS_PASSWORD=null
47 | REDIS_PORT=6379
48 |
49 | MAIL_MAILER=log
50 | MAIL_HOST=127.0.0.1
51 | MAIL_PORT=2525
52 | MAIL_USERNAME=null
53 | MAIL_PASSWORD=null
54 | MAIL_ENCRYPTION=null
55 | MAIL_FROM_ADDRESS="hello@example.com"
56 | MAIL_FROM_NAME="${APP_NAME}"
57 |
58 | AWS_ACCESS_KEY_ID=
59 | AWS_SECRET_ACCESS_KEY=
60 | AWS_DEFAULT_REGION=us-east-1
61 | AWS_BUCKET=
62 | AWS_USE_PATH_STYLE_ENDPOINT=false
63 |
64 | VITE_APP_NAME="${APP_NAME}"
65 |
66 | WWWGROUP=1000
67 | WWWUSER=1000
68 |
69 | APP_PORT=8000
70 | VITE_APP_PORT=5173
71 | FORWARD_DB_PORT=
72 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/AuthenticatedSessionController.php:
--------------------------------------------------------------------------------
1 | Route::has('password.request'),
23 | 'status' => session('status'),
24 | ]);
25 | }
26 |
27 | /**
28 | * Handle an incoming authentication request.
29 | */
30 | public function store(LoginRequest $request): RedirectResponse
31 | {
32 | $request->authenticate();
33 |
34 | $request->session()->regenerate();
35 |
36 | return redirect()->intended(route('dashboard', absolute: false));
37 | }
38 |
39 | /**
40 | * Destroy an authenticated session.
41 | */
42 | public function destroy(Request $request): RedirectResponse
43 | {
44 | Auth::guard('web')->logout();
45 |
46 | $request->session()->invalidate();
47 |
48 | $request->session()->regenerateToken();
49 |
50 | return redirect('/');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/resources/js/composables/useSiteColorMode.ts:
--------------------------------------------------------------------------------
1 | import { useColorMode, type BasicColorSchema, type UseColorModeOptions } from '@vueuse/core'
2 | import { useCookies } from '@vueuse/integrations/useCookies'
3 | import type { CookieSetOptions } from 'universal-cookie'
4 | import { watch } from 'vue'
5 |
6 | interface SiteColorModeOptions extends UseColorModeOptions {
7 | cookieKey?: string;
8 | cookieOpts?: CookieSetOptions;
9 | cookieColorMode?: BasicColorSchema;
10 | }
11 |
12 | export function useSiteColorMode(opts: SiteColorModeOptions = {}) {
13 | const {
14 | cookieKey = 'colorScheme',
15 | cookieOpts: userOpts,
16 | cookieColorMode,
17 | ...rest
18 | } = opts
19 |
20 | // a maxAge in seconds (365 days)
21 | const defaultOpts: CookieSetOptions = {
22 | path: '/',
23 | maxAge: 365 * 24 * 60 * 60,
24 | sameSite: 'lax',
25 | }
26 |
27 | const finalCookieOpts = { ...defaultOpts, ...userOpts }
28 |
29 | const cookies = useCookies([cookieKey])
30 | const initialValue: BasicColorSchema = typeof window === 'undefined'
31 | ? (cookieColorMode ?? 'auto')
32 | : (cookies.get(cookieKey) as BasicColorSchema) ?? 'auto'
33 |
34 | const colorMode = useColorMode({ initialValue, ...rest })
35 |
36 | if (typeof window !== 'undefined') {
37 | watch(colorMode, (mode) => {
38 | cookies.set(cookieKey, mode, finalCookieOpts)
39 | })
40 | }
41 |
42 | return colorMode
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get('/login');
16 |
17 | $response->assertStatus(200);
18 | }
19 |
20 | public function test_users_can_authenticate_using_the_login_screen(): void
21 | {
22 | $user = User::factory()->create();
23 |
24 | $response = $this->post('/login', [
25 | 'email' => $user->email,
26 | 'password' => 'password',
27 | ]);
28 |
29 | $this->assertAuthenticated();
30 | $response->assertRedirect(route('dashboard', absolute: false));
31 | }
32 |
33 | public function test_users_can_not_authenticate_with_invalid_password(): void
34 | {
35 | $user = User::factory()->create();
36 |
37 | $this->post('/login', [
38 | 'email' => $user->email,
39 | 'password' => 'wrong-password',
40 | ]);
41 |
42 | $this->assertGuest();
43 | }
44 |
45 | public function test_users_can_logout(): void
46 | {
47 | $user = User::factory()->create();
48 |
49 | $response = $this->actingAs($user)->post('/logout');
50 |
51 | $this->assertGuest();
52 | $response->assertRedirect('/');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/js/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { DataTableFilterMetaData } from 'primevue'
2 | import type { Page, Errors } from '@inertiajs/core'
3 | import type { MenuItem as PrimeVueMenuItem } from 'primevue/menuitem'
4 | import type { LucideIcon } from 'lucide-vue-next'
5 | import type { Config } from 'ziggy-js'
6 |
7 | export interface User {
8 | id: number;
9 | name: string;
10 | email: string;
11 | email_verified_at: string | null;
12 | created_at: string;
13 | updated_at: string;
14 | }
15 |
16 | export interface AuthProps {
17 | user: User;
18 | }
19 |
20 | export interface FlashProps {
21 | success?: string | null;
22 | info?: string | null;
23 | warn?: string | null;
24 | error?: string | null;
25 | message?: string | null;
26 | }
27 |
28 | export type AppPageProps = Record> = T & {
29 | colorScheme: 'auto' | 'light' | 'dark';
30 | ziggy: Config & { location: string };
31 | auth: AuthProps;
32 | flash: FlashProps;
33 | queryParams: Record;
34 | };
35 |
36 | export type PrimeVueDataFilters = {
37 | [key: string]: DataTableFilterMetaData;
38 | };
39 |
40 | export interface MenuItem extends PrimeVueMenuItem {
41 | route?: string;
42 | lucideIcon?: LucideIcon;
43 | lucideIconClass?: string;
44 | active?: boolean;
45 | }
46 |
47 | export interface InertiaRouterFetchCallbacks {
48 | onSuccess?: (page: Page) => void;
49 | onError?: (errors: Errors) => void;
50 | onFinish?: () => void;
51 | }
52 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/RegisteredUserController.php:
--------------------------------------------------------------------------------
1 | validate([
34 | 'name' => ['required', 'string', 'max:255'],
35 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
36 | 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
37 | ]);
38 |
39 | $user = User::create([
40 | 'name' => $request->name,
41 | 'email' => $request->email,
42 | 'password' => Hash::make($request->string('password')),
43 | ]);
44 |
45 | event(new Registered($user));
46 |
47 | Auth::login($user);
48 |
49 | return redirect(route('dashboard', absolute: false));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/resources/js/composables/useThemePreset.ts:
--------------------------------------------------------------------------------
1 | import { ref, Ref } from 'vue'
2 | import { usePreset } from '@primeuix/themes'
3 | import { Preset } from '@primeuix/themes/types'
4 | import { useStorage } from '@vueuse/core'
5 | import bootstrap from '@/theme/bootstrap-preset'
6 | import breeze from '@/theme/breeze-preset'
7 | import enterprise from '@/theme/enterprise-preset'
8 | import noir from '@/theme/noir-preset'
9 | import warm from '@/theme/warm-preset'
10 |
11 | interface ThemePreset {
12 | label: string,
13 | value: string,
14 | preset: Preset,
15 | }
16 |
17 | const presets = ref([
18 | { label: 'Bootstrap', value: 'bootstrap', preset: bootstrap },
19 | { label: 'Breeze', value: 'breeze', preset: breeze },
20 | { label: 'Enterprise', value: 'enterprise', preset: enterprise },
21 | { label: 'Noir', value: 'noir', preset: noir },
22 | { label: 'Warm', value: 'warm', preset: warm },
23 | ])
24 |
25 | const selectedPreset: Ref = useStorage('theme-preset', 'noir')
26 |
27 | function getCurrentPreset(): Preset {
28 | return (
29 | presets.value.find((p) => p.value === selectedPreset.value)?.preset ||
30 | presets.value[3].preset
31 | )
32 | }
33 |
34 | function setPreset(presetValue: string): void {
35 | const themePreset = presets.value.find((p) => p.value === presetValue)
36 | if (themePreset) {
37 | usePreset(themePreset.preset)
38 | }
39 | }
40 |
41 | setPreset(selectedPreset.value)
42 |
43 | export function useThemePreset() {
44 | return {
45 | presets,
46 | selectedPreset,
47 | getCurrentPreset,
48 | setPreset,
49 | }
50 | }
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/PasswordResetLinkController.php:
--------------------------------------------------------------------------------
1 | session('status'),
22 | ]);
23 | }
24 |
25 | /**
26 | * Handle an incoming password reset link request.
27 | *
28 | * @throws ValidationException
29 | */
30 | public function store(Request $request): RedirectResponse
31 | {
32 | $request->validate([
33 | 'email' => ['required', 'email'],
34 | ]);
35 |
36 | // We will send the password reset link to this user. Once we have attempted
37 | // to send the link, we will examine the response then see the message we
38 | // need to show to the user. Finally, we'll send out a proper response.
39 | $status = Password::sendResetLink(
40 | $request->only('email')
41 | );
42 |
43 | if ($status == Password::RESET_LINK_SENT) {
44 | return back()->with('status', __($status));
45 | }
46 |
47 | throw ValidationException::withMessages([
48 | 'email' => [__($status)],
49 | ]);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite'
2 | import laravel from 'laravel-vite-plugin'
3 | import vue from '@vitejs/plugin-vue'
4 | import tailwindcss from "@tailwindcss/vite"
5 | import Components from 'unplugin-vue-components/vite'
6 | import { PrimeVueResolver } from '@primevue/auto-import-resolver'
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig(({ mode }) => {
10 | // https://vite.dev/config/#using-environment-variables-in-config
11 | const env = loadEnv(mode, process.cwd(), '')
12 | const devPort = env.VITE_APP_PORT ? Number(env.VITE_APP_PORT) : 5173
13 | const hostDomain = env.VITE_HOST_DOMAIN || 'localhost'
14 |
15 | return {
16 | plugins: [
17 | laravel({
18 | input: 'resources/js/app.ts',
19 | ssr: 'resources/js/ssr.ts',
20 | refresh: true,
21 | }),
22 | vue({
23 | template: {
24 | transformAssetUrls: {
25 | base: null,
26 | includeAbsolute: false,
27 | },
28 | },
29 | }),
30 | tailwindcss(),
31 | Components({
32 | resolvers: [PrimeVueResolver()],
33 | }),
34 | ],
35 | server: {
36 | port: devPort,
37 | host: true,
38 | hmr: {
39 | host: hostDomain,
40 | },
41 | cors: true,
42 | watch: {
43 | usePolling: true,
44 | },
45 | },
46 | preview: {
47 | port: devPort,
48 | },
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/tests/Feature/Settings/PasswordUpdateTest.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | $response = $this
19 | ->actingAs($user)
20 | ->from('/settings/password')
21 | ->put('/settings/password', [
22 | 'current_password' => 'password',
23 | 'password' => 'new-password',
24 | 'password_confirmation' => 'new-password',
25 | ]);
26 |
27 | $response
28 | ->assertSessionHasNoErrors()
29 | ->assertRedirect('/settings/password');
30 |
31 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password));
32 | }
33 |
34 | public function test_correct_password_must_be_provided_to_update_password()
35 | {
36 | $user = User::factory()->create();
37 |
38 | $response = $this
39 | ->actingAs($user)
40 | ->from('/settings/password')
41 | ->put('/settings/password', [
42 | 'current_password' => 'wrong-password',
43 | 'password' => 'new-password',
44 | 'password_confirmation' => 'new-password',
45 | ]);
46 |
47 | $response
48 | ->assertSessionHasErrors('current_password')
49 | ->assertRedirect('/settings/password');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | id();
15 | $table->string('name');
16 | $table->string('email')->unique();
17 | $table->timestamp('email_verified_at')->nullable();
18 | $table->string('password');
19 | $table->rememberToken();
20 | $table->timestamps();
21 | });
22 |
23 | Schema::create('password_reset_tokens', function (Blueprint $table) {
24 | $table->string('email')->primary();
25 | $table->string('token');
26 | $table->timestamp('created_at')->nullable();
27 | });
28 |
29 | Schema::create('sessions', function (Blueprint $table) {
30 | $table->string('id')->primary();
31 | $table->foreignId('user_id')->nullable()->index();
32 | $table->string('ip_address', 45)->nullable();
33 | $table->text('user_agent')->nullable();
34 | $table->longText('payload');
35 | $table->integer('last_activity')->index();
36 | });
37 | }
38 |
39 | /**
40 | * Reverse the migrations.
41 | */
42 | public function down(): void
43 | {
44 | Schema::dropIfExists('users');
45 | Schema::dropIfExists('password_reset_tokens');
46 | Schema::dropIfExists('sessions');
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // https://aka.ms/devcontainer.json
2 | {
3 | "name": "Laravel PrimeVue Starter",
4 | "dockerComposeFile": [
5 | "../docker-compose.local.yml"
6 | ],
7 | "service": "laravel",
8 | "workspaceFolder": "/var/www/html",
9 | "mounts": [
10 | "type=bind,source=/home/${localEnv:USER}/.ssh,target=/home/sail/.ssh,readonly"
11 | ],
12 | "customizations": {
13 | "vscode": {
14 | "extensions": [
15 | "DEVSENSE.phptools-vscode",
16 | "MehediDracula.php-namespace-resolver",
17 | "laravel.vscode-laravel",
18 | "Vue.volar",
19 | "hollowtree.vue-snippets",
20 | "dbaeumer.vscode-eslint",
21 | "EditorConfig.EditorConfig",
22 | "bradlc.vscode-tailwindcss",
23 | "eamodio.gitlens",
24 | "esbenp.prettier-vscode",
25 | "mikestead.dotenv",
26 | "streetsidesoftware.code-spell-checker",
27 | "shd101wyy.markdown-preview-enhanced",
28 | "formulahendry.auto-rename-tag",
29 | "pmneo.tsimporter"
30 | ],
31 | "settings": {
32 | "html.format.wrapAttributes": "force-expand-multiline",
33 | "[vue]": {
34 | "editor.defaultFormatter": "Vue.volar",
35 | "editor.tabSize": 4
36 | }
37 | }
38 | }
39 | },
40 | "remoteUser": "sail",
41 | "postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true"
42 | // "forwardPorts": [],
43 | // "runServices": [],
44 | // "shutdownAction": "none",
45 | }
--------------------------------------------------------------------------------
/app/Http/Controllers/Settings/ProfileController.php:
--------------------------------------------------------------------------------
1 | $request->user() instanceof MustVerifyEmail,
23 | 'status' => session('status'),
24 | ]);
25 | }
26 |
27 | /**
28 | * Update the user's profile information.
29 | */
30 | public function update(ProfileUpdateRequest $request): RedirectResponse
31 | {
32 | $user = $request->user();
33 |
34 | $user?->fill($request->validated());
35 |
36 | if ($user && $user->isDirty('email')) {
37 | $user->email_verified_at = null;
38 | }
39 |
40 | $user?->save();
41 |
42 | return redirect()->route('profile.edit');
43 | }
44 |
45 | /**
46 | * Delete the user's profile.
47 | */
48 | public function destroy(Request $request): RedirectResponse
49 | {
50 | $request->validate([
51 | 'password' => ['required', 'current_password'],
52 | ]);
53 |
54 | $user = $request->user();
55 |
56 | Auth::logout();
57 |
58 | $user?->delete();
59 |
60 | $request->session()->invalidate();
61 | $request->session()->regenerateToken();
62 |
63 | return redirect()->route('welcome');
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/resources/js/theme/bootstrap-preset.ts:
--------------------------------------------------------------------------------
1 | import Preset from '@primeuix/themes/lara'
2 | import { definePreset } from '@primeuix/themes'
3 |
4 | const customThemePreset = definePreset(Preset, {
5 | semantic: {
6 | primary: {
7 | 50: '{blue.50}',
8 | 100: '{blue.100}',
9 | 200: '{blue.200}',
10 | 300: '{blue.300}',
11 | 400: '{blue.400}',
12 | 500: '{blue.500}',
13 | 600: '{blue.600}',
14 | 700: '{blue.700}',
15 | 800: '{blue.800}',
16 | 900: '{blue.900}',
17 | 950: '{blue.950}',
18 | },
19 | colorScheme: {
20 | light: {
21 | surface: {
22 | 50: '{slate.50}',
23 | 100: '{slate.100}',
24 | 200: '{slate.200}',
25 | 300: '{slate.300}',
26 | 400: '{slate.400}',
27 | 500: '{slate.500}',
28 | 600: '{slate.600}',
29 | 700: '{slate.700}',
30 | 800: '{slate.800}',
31 | 900: '{slate.900}',
32 | 950: '{slate.950}',
33 | },
34 | },
35 | dark: {
36 | surface: {
37 | 50: '{slate.50}',
38 | 100: '{slate.100}',
39 | 200: '{slate.200}',
40 | 300: '{slate.300}',
41 | 400: '{slate.400}',
42 | 500: '{slate.500}',
43 | 600: '{slate.600}',
44 | 700: '{slate.700}',
45 | 800: '{slate.800}',
46 | 900: '{slate.900}',
47 | 950: '{slate.950}',
48 | },
49 | },
50 | },
51 | },
52 | })
53 |
54 | export default customThemePreset
55 |
--------------------------------------------------------------------------------
/resources/js/theme/breeze-preset.ts:
--------------------------------------------------------------------------------
1 | import Preset from '@primeuix/themes/aura'
2 | import { definePreset } from '@primeuix/themes'
3 |
4 | const customThemePreset = definePreset(Preset, {
5 | semantic: {
6 | primary: {
7 | 50: '{indigo.50}',
8 | 100: '{indigo.100}',
9 | 200: '{indigo.200}',
10 | 300: '{indigo.300}',
11 | 400: '{indigo.400}',
12 | 500: '{indigo.500}',
13 | 600: '{indigo.600}',
14 | 700: '{indigo.700}',
15 | 800: '{indigo.800}',
16 | 900: '{indigo.900}',
17 | 950: '{indigo.950}',
18 | },
19 | colorScheme: {
20 | light: {
21 | surface: {
22 | 50: '{gray.50}',
23 | 100: '{gray.100}',
24 | 200: '{gray.200}',
25 | 300: '{gray.300}',
26 | 400: '{gray.400}',
27 | 500: '{gray.500}',
28 | 600: '{gray.600}',
29 | 700: '{gray.700}',
30 | 800: '{gray.800}',
31 | 900: '{gray.900}',
32 | 950: '{gray.950}',
33 | },
34 | },
35 | dark: {
36 | surface: {
37 | 50: '{gray.50}',
38 | 100: '{gray.100}',
39 | 200: '{gray.200}',
40 | 300: '{gray.300}',
41 | 400: '{gray.400}',
42 | 500: '{gray.500}',
43 | 600: '{gray.600}',
44 | 700: '{gray.700}',
45 | 800: '{gray.800}',
46 | 900: '{gray.900}',
47 | 950: '{gray.950}',
48 | },
49 | },
50 | },
51 | },
52 | })
53 |
54 | export default customThemePreset
55 |
--------------------------------------------------------------------------------
/resources/js/theme/warm-preset.ts:
--------------------------------------------------------------------------------
1 | import Preset from '@primeuix/themes/nora'
2 | import { definePreset } from '@primeuix/themes'
3 |
4 | const customThemePreset = definePreset(Preset, {
5 | semantic: {
6 | primary: {
7 | 50: '{orange.50}',
8 | 100: '{orange.100}',
9 | 200: '{orange.200}',
10 | 300: '{orange.300}',
11 | 400: '{orange.400}',
12 | 500: '{orange.500}',
13 | 600: '{orange.600}',
14 | 700: '{orange.700}',
15 | 800: '{orange.800}',
16 | 900: '{orange.900}',
17 | 950: '{orange.950}',
18 | },
19 | colorScheme: {
20 | light: {
21 | surface: {
22 | 50: '{stone.50}',
23 | 100: '{stone.100}',
24 | 200: '{stone.200}',
25 | 300: '{stone.300}',
26 | 400: '{stone.400}',
27 | 500: '{stone.500}',
28 | 600: '{stone.600}',
29 | 700: '{stone.700}',
30 | 800: '{stone.800}',
31 | 900: '{stone.900}',
32 | 950: '{stone.950}',
33 | },
34 | },
35 | dark: {
36 | surface: {
37 | 50: '{stone.50}',
38 | 100: '{stone.100}',
39 | 200: '{stone.200}',
40 | 300: '{stone.300}',
41 | 400: '{stone.400}',
42 | 500: '{stone.500}',
43 | 600: '{stone.600}',
44 | 700: '{stone.700}',
45 | 800: '{stone.800}',
46 | 900: '{stone.900}',
47 | 950: '{stone.950}',
48 | },
49 | },
50 | },
51 | },
52 | })
53 |
54 | export default customThemePreset
55 |
--------------------------------------------------------------------------------
/resources/js/theme/noir-preset.ts:
--------------------------------------------------------------------------------
1 | import Preset from '@primeuix/themes/aura'
2 | import { definePreset } from '@primeuix/themes'
3 |
4 | // https://primevue.org/theming/styled/#noir
5 | const customThemePreset = definePreset(Preset, {
6 | semantic: {
7 | primary: {
8 | 50: '{zinc.50}',
9 | 100: '{zinc.100}',
10 | 200: '{zinc.200}',
11 | 300: '{zinc.300}',
12 | 400: '{zinc.400}',
13 | 500: '{zinc.500}',
14 | 600: '{zinc.600}',
15 | 700: '{zinc.700}',
16 | 800: '{zinc.800}',
17 | 900: '{zinc.900}',
18 | 950: '{zinc.950}',
19 | },
20 | colorScheme: {
21 | light: {
22 | primary: {
23 | color: '{zinc.950}',
24 | inverseColor: '#ffffff',
25 | hoverColor: '{zinc.900}',
26 | activeColor: '{zinc.800}',
27 | },
28 | highlight: {
29 | background: '{zinc.950}',
30 | focusBackground: '{zinc.700}',
31 | color: '#ffffff',
32 | focusColor: '#ffffff',
33 | },
34 | },
35 | dark: {
36 | primary: {
37 | color: '{zinc.50}',
38 | inverseColor: '{zinc.950}',
39 | hoverColor: '{zinc.100}',
40 | activeColor: '{zinc.200}',
41 | },
42 | highlight: {
43 | background: 'rgba(250, 250, 250, .16)',
44 | focusBackground: 'rgba(250, 250, 250, .24)',
45 | color: 'rgba(255,255,255,.87)',
46 | focusColor: 'rgba(255,255,255,.87)',
47 | },
48 | },
49 | },
50 | },
51 | })
52 |
53 | export default customThemePreset
54 |
--------------------------------------------------------------------------------
/resources/js/theme/enterprise-preset.ts:
--------------------------------------------------------------------------------
1 | import Preset from '@primeuix/themes/material'
2 | import { definePreset } from '@primeuix/themes'
3 |
4 | const customThemePreset = definePreset(Preset, {
5 | semantic: {
6 | primary: {
7 | 50: '{teal.50}',
8 | 100: '{teal.100}',
9 | 200: '{teal.200}',
10 | 300: '{teal.300}',
11 | 400: '{teal.400}',
12 | 500: '{teal.500}',
13 | 600: '{teal.600}',
14 | 700: '{teal.700}',
15 | 800: '{teal.800}',
16 | 900: '{teal.900}',
17 | 950: '{teal.950}',
18 | },
19 | colorScheme: {
20 | light: {
21 | surface: {
22 | 50: '{neutral.50}',
23 | 100: '{neutral.100}',
24 | 200: '{neutral.200}',
25 | 300: '{neutral.300}',
26 | 400: '{neutral.400}',
27 | 500: '{neutral.500}',
28 | 600: '{neutral.600}',
29 | 700: '{neutral.700}',
30 | 800: '{neutral.800}',
31 | 900: '{neutral.900}',
32 | 950: '{neutral.950}',
33 | },
34 | },
35 | dark: {
36 | surface: {
37 | 50: '{neutral.50}',
38 | 100: '{neutral.100}',
39 | 200: '{neutral.200}',
40 | 300: '{neutral.300}',
41 | 400: '{neutral.400}',
42 | 500: '{neutral.500}',
43 | 600: '{neutral.600}',
44 | 700: '{neutral.700}',
45 | 800: '{neutral.800}',
46 | 900: '{neutral.900}',
47 | 950: '{neutral.950}',
48 | },
49 | },
50 | },
51 | },
52 | })
53 |
54 | export default customThemePreset
55 |
--------------------------------------------------------------------------------
/docker-compose.local.yml:
--------------------------------------------------------------------------------
1 | services:
2 | laravel:
3 | build:
4 | context: ./docker/local/web
5 | dockerfile: Dockerfile
6 | args:
7 | WWWGROUP: '${WWWGROUP}'
8 | image: sail-8.4/app
9 | extra_hosts:
10 | - 'host.docker.internal:host-gateway'
11 | #ports:
12 | #- '${APP_PORT:-80}:80' not required using Traefik
13 | #- '${VITE_APP_PORT:-5173}:${VITE_APP_PORT:-5173}' Not required if using dev containers (auto forwards port to localhost)
14 | environment:
15 | WWWUSER: '${WWWUSER}'
16 | LARAVEL_SAIL: 1
17 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
18 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
19 | IGNITION_LOCAL_SITES_PATH: '${PWD}'
20 | volumes:
21 | - '.:/var/www/html'
22 | labels:
23 | - "traefik.enable=true"
24 | - "traefik.http.routers.laravel-primevue.rule=Host(`laravel-primevue.localhost`)"
25 | - "traefik.http.services.laravel-primevue.loadbalancer.server.port=80"
26 | networks:
27 | - sail
28 | - proxy
29 | depends_on:
30 | - pgsql
31 |
32 | pgsql:
33 | image: 'postgres:17'
34 | ports:
35 | - '${FORWARD_DB_PORT:-5432}:5432'
36 | environment:
37 | PGPASSWORD: '${DB_PASSWORD:-secret}'
38 | POSTGRES_DB: '${DB_DATABASE}'
39 | POSTGRES_USER: '${DB_USERNAME}'
40 | POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}'
41 | volumes:
42 | - 'laravel-primevue-pgsql:/var/lib/postgresql/data'
43 | - './docker/local/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql'
44 | networks:
45 | - sail
46 | healthcheck:
47 | test: [ "CMD", "pg_isready", "-q", "-d", "${DB_DATABASE}", "-U", "${DB_USERNAME}" ]
48 | retries: 3
49 | timeout: 5s
50 |
51 | volumes:
52 | laravel-primevue-pgsql:
53 | driver: local
54 |
55 | networks:
56 | sail:
57 | driver: bridge
58 | proxy:
59 | name: traefik_network
60 | external: true
61 |
--------------------------------------------------------------------------------
/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 | Schema::create('failed_jobs', function (Blueprint $table) {
37 | $table->id();
38 | $table->string('uuid')->unique();
39 | $table->text('connection');
40 | $table->text('queue');
41 | $table->longText('payload');
42 | $table->longText('exception');
43 | $table->timestamp('failed_at')->useCurrent();
44 | });
45 | }
46 |
47 | /**
48 | * Reverse the migrations.
49 | */
50 | public function down(): void
51 | {
52 | Schema::dropIfExists('jobs');
53 | Schema::dropIfExists('job_batches');
54 | Schema::dropIfExists('failed_jobs');
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "build:ssr": "vite build && vite build --ssr",
8 | "lint": "eslint . --fix"
9 | },
10 | "dependencies": {
11 | "@inertiajs/vue3": "^2.0.5",
12 | "@primeuix/themes": "^2.0.0",
13 | "@tailwindcss/vite": "^4.0.17",
14 | "@types/lodash-es": "^4.17.12",
15 | "@vitejs/plugin-vue": "^5.2.3",
16 | "@vue/server-renderer": "^3.5.14",
17 | "@vueuse/core": "^13.0.0",
18 | "@vueuse/integrations": "^13.2.0",
19 | "globals": "^16.0.0",
20 | "laravel-vite-plugin": "^1.2.0",
21 | "lodash-es": "^4.17.21",
22 | "lucide-vue-next": "^0.485.0",
23 | "primevue": "^4.5.0",
24 | "tailwind-merge": "^3.2.0",
25 | "tailwindcss": "^4.0.17",
26 | "tailwindcss-primeui": "^0.6.1",
27 | "typescript": "^5.8.2",
28 | "universal-cookie": "^7.2.2",
29 | "unplugin-vue-components": "^28.4.1",
30 | "vite": "^6.2.3",
31 | "vue": "^3.5.13",
32 | "ziggy-js": "^2.5.2"
33 | },
34 | "devDependencies": {
35 | "@primevue/auto-import-resolver": "^4.5.0",
36 | "@tsconfig/node22": "^22.0.2",
37 | "@types/node": "^24.5.2",
38 | "@vitejs/plugin-vue": "^5.0.5",
39 | "@vue/eslint-config-typescript": "^14.6.0",
40 | "@vue/tsconfig": "^0.8.1",
41 | "eslint": "^9.18.0",
42 | "eslint-plugin-vue": "^9.32.0",
43 | "jiti": "^2.6.0",
44 | "npm-run-all2": "^8.0.4",
45 | "typescript": "^5.7.3",
46 | "typescript-eslint": "^8.19.1",
47 | "unplugin-vue-components": "^29.1.0",
48 | "vite": "^6.2",
49 | "vite-plugin-vue-devtools": "^8.0.2",
50 | "vue-tsc": "^3.0.8"
51 | },
52 | "optionalDependencies": {
53 | "@rollup/rollup-linux-x64-gnu": "4.37.0",
54 | "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
55 | "lightningcss-linux-x64-gnu": "^1.29.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/resources/js/layouts/GuestAuthLayout.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
15 |
16 |
20 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/resources/js/pages/auth/ConfirmPassword.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Confirm your password
26 |
27 |
28 |
29 |
30 |
31 | This is a secure area of the application. Please confirm your password before continuing.
32 |
33 |
34 |
35 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/resources/js/components/FlashMessages.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
19 |
25 |
26 |
27 |
28 | {{ page.props.flash.success }}
29 |
30 |
36 |
37 |
38 |
39 | {{ page.props.flash.info }}
40 |
41 |
47 |
48 |
49 |
50 | {{ page.props.flash.warn }}
51 |
52 |
58 |
59 |
60 |
61 | {{ page.props.flash.error }}
62 |
63 |
69 |
70 |
71 |
72 | {{ page.props.flash.message }}
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/EmailVerificationTest.php:
--------------------------------------------------------------------------------
1 | unverified()->create();
21 |
22 | $response = $this->actingAs($user)->get('/verify-email');
23 |
24 | $response->assertStatus(200);
25 | }
26 |
27 | public function test_email_can_be_verified(): void
28 | {
29 | $user = User::factory()->unverified()->create();
30 | $userRef = new ReflectionClass(User::class);
31 | if (!$userRef->implementsInterface(MustVerifyEmail::class)) {
32 | $this->markTestSkipped('User email verification is not enabled, skipping test.');
33 | }
34 |
35 | Event::fake();
36 |
37 | $verificationUrl = URL::temporarySignedRoute(
38 | 'verification.verify',
39 | now()->addMinutes(60),
40 | ['id' => $user->id, 'hash' => sha1($user->email)]
41 | );
42 |
43 | $response = $this->actingAs($user)->get($verificationUrl);
44 |
45 | Event::assertDispatched(Verified::class);
46 | $this->assertTrue($user->fresh()->hasVerifiedEmail());
47 | $response->assertRedirect(route('dashboard', absolute: false) . '?verified=1');
48 | }
49 |
50 | public function test_email_is_not_verified_with_invalid_hash(): void
51 | {
52 | $user = User::factory()->unverified()->create();
53 |
54 | $verificationUrl = URL::temporarySignedRoute(
55 | 'verification.verify',
56 | now()->addMinutes(60),
57 | ['id' => $user->id, 'hash' => sha1('wrong-email')]
58 | );
59 |
60 | $this->actingAs($user)->get($verificationUrl);
61 |
62 | $this->assertFalse($user->fresh()->hasVerifiedEmail());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/resources/js/pages/Error.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
37 |
38 | {{ details }}
39 |
40 |
41 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/Http/Middleware/HandleInertiaRequests.php:
--------------------------------------------------------------------------------
1 |
44 | */
45 | public function share(Request $request): array
46 | {
47 | return [
48 | ...parent::share($request),
49 | 'colorScheme' => fn () => $request->cookie('colorScheme', 'auto'),
50 | 'ziggy' => fn () => [
51 | ...(new Ziggy())->toArray(),
52 | 'location' => $request->url(),
53 | ],
54 | 'auth' => [
55 | 'user' => $request->user(),
56 | ],
57 | 'flash' => [
58 | 'success' => fn () => $request->session()->get('flash_success'),
59 | 'info' => fn () => $request->session()->get('flash_info'),
60 | 'warn' => fn () => $request->session()->get('flash_warn'),
61 | 'error' => fn () => $request->session()->get('flash_error'),
62 | 'message' => fn () => $request->session()->get('flash_message'),
63 | ],
64 | 'queryParams' => Inertia::always($request->query()),
65 | ];
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/PasswordResetTest.php:
--------------------------------------------------------------------------------
1 | get('/forgot-password');
18 |
19 | $response->assertStatus(200);
20 | }
21 |
22 | public function test_reset_password_link_can_be_requested(): void
23 | {
24 | Notification::fake();
25 |
26 | $user = User::factory()->create();
27 |
28 | $this->post('/forgot-password', ['email' => $user->email]);
29 |
30 | Notification::assertSentTo($user, ResetPassword::class);
31 | }
32 |
33 | public function test_reset_password_screen_can_be_rendered(): void
34 | {
35 | Notification::fake();
36 |
37 | $user = User::factory()->create();
38 |
39 | $this->post('/forgot-password', ['email' => $user->email]);
40 |
41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
42 | $response = $this->get('/reset-password/' . $notification->token);
43 |
44 | $response->assertStatus(200);
45 |
46 | return true;
47 | });
48 | }
49 |
50 | public function test_password_can_be_reset_with_valid_token(): void
51 | {
52 | Notification::fake();
53 |
54 | $user = User::factory()->create();
55 |
56 | $this->post('/forgot-password', ['email' => $user->email]);
57 |
58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
59 | $response = $this->post('/reset-password', [
60 | 'token' => $notification->token,
61 | 'email' => $user->email,
62 | 'password' => 'password',
63 | 'password_confirmation' => 'password',
64 | ]);
65 |
66 | $response
67 | ->assertSessionHasNoErrors()
68 | ->assertRedirect(route('login'));
69 |
70 | return true;
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/resources/js/pages/auth/VerifyEmail.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Verify email
29 |
30 |
31 |
32 |
33 |
34 | Please verify your email address by clicking on the link we just emailed to you.
35 |
36 |
37 |
38 |
42 |
47 | A new verification link has been sent to the email address you
48 | provided during registration.
49 |
50 |
51 |
52 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/routes/auth.php:
--------------------------------------------------------------------------------
1 | group(function () {
14 | Route::get('register', [RegisteredUserController::class, 'create'])
15 | ->name('register');
16 | Route::post('register', [RegisteredUserController::class, 'store']);
17 | Route::get('login', [AuthenticatedSessionController::class, 'create'])
18 | ->name('login');
19 | Route::post('login', [AuthenticatedSessionController::class, 'store']);
20 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
21 | ->name('password.request');
22 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
23 | ->name('password.email');
24 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
25 | ->name('password.reset');
26 | Route::post('reset-password', [NewPasswordController::class, 'store'])
27 | ->name('password.store');
28 | });
29 |
30 | Route::middleware('auth')->group(function () {
31 | Route::get('verify-email', EmailVerificationPromptController::class)
32 | ->name('verification.notice');
33 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
34 | ->middleware(['signed', 'throttle:6,1'])
35 | ->name('verification.verify');
36 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
37 | ->middleware('throttle:6,1')
38 | ->name('verification.send');
39 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
40 | ->name('password.confirm');
41 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
42 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
43 | ->name('logout');
44 | });
45 |
--------------------------------------------------------------------------------
/resources/js/layouts/UserSettingsLayout.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
38 | Settings
39 |
40 |
41 | Manage your profile and account settings
42 |
43 |
44 |
45 |
46 |
47 |
48 |
63 |
64 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/Http/Requests/Auth/LoginRequest.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | public function rules(): array
28 | {
29 | return [
30 | 'email' => ['required', 'string', 'email'],
31 | 'password' => ['required', 'string'],
32 | ];
33 | }
34 |
35 | /**
36 | * Attempt to authenticate the request's credentials.
37 | *
38 | * @throws ValidationException
39 | */
40 | public function authenticate(): void
41 | {
42 | $this->ensureIsNotRateLimited();
43 |
44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
45 | RateLimiter::hit($this->throttleKey());
46 |
47 | throw ValidationException::withMessages([
48 | 'email' => __('auth.failed'),
49 | ]);
50 | }
51 |
52 | RateLimiter::clear($this->throttleKey());
53 | }
54 |
55 | /**
56 | * Ensure the login request is not rate limited.
57 | *
58 | * @throws ValidationException
59 | */
60 | public function ensureIsNotRateLimited(): void
61 | {
62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
63 | return;
64 | }
65 |
66 | event(new Lockout($this));
67 |
68 | $seconds = RateLimiter::availableIn($this->throttleKey());
69 |
70 | throw ValidationException::withMessages([
71 | 'email' => __('auth.throttle', [
72 | 'seconds' => $seconds,
73 | 'minutes' => ceil($seconds / 60),
74 | ]),
75 | ]);
76 | }
77 |
78 | /**
79 | * Get the rate limiting throttle key for the request.
80 | */
81 | public function throttleKey(): string
82 | {
83 | return Str::transliterate(Str::lower($this->string('email')) . '|' . $this->ip());
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/NewPasswordController.php:
--------------------------------------------------------------------------------
1 | $request->email,
26 | 'token' => $request->route('token'),
27 | ]);
28 | }
29 |
30 | /**
31 | * Handle an incoming new password request.
32 | *
33 | * @throws ValidationException
34 | */
35 | public function store(Request $request): RedirectResponse
36 | {
37 | $request->validate([
38 | 'token' => ['required', 'string'],
39 | 'email' => ['required', 'email'],
40 | 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
41 | ]);
42 |
43 | // Here we will attempt to reset the user's password. If it is successful we
44 | // will update the password on an actual user model and persist it to the
45 | // database. Otherwise we will parse the error and return the response.
46 | /** @var string $status */
47 | $status = Password::reset(
48 | $request->only('email', 'password', 'password_confirmation', 'token'),
49 | function ($user) use ($request) {
50 | $user->forceFill([
51 | 'password' => Hash::make($request->string('password')),
52 | 'remember_token' => Str::random(60),
53 | ])->save();
54 |
55 | event(new PasswordReset($user));
56 | }
57 | );
58 |
59 | // If the password was successfully reset, we will redirect the user back to
60 | // the application's home authenticated view. If there is an error we can
61 | // redirect them back to where they came from with their error message.
62 | if ($status == Password::PASSWORD_RESET) {
63 | return redirect()->route('login')->with('status', __($status));
64 | }
65 |
66 | throw ValidationException::withMessages([
67 | 'email' => [__($status)],
68 | ]);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/config/filesystems.php:
--------------------------------------------------------------------------------
1 | env('FILESYSTEM_DISK', 'local'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Filesystem Disks
21 | |--------------------------------------------------------------------------
22 | |
23 | | Below you may configure as many filesystem disks as necessary, and you
24 | | may even configure multiple disks for the same driver. Examples for
25 | | most supported storage drivers are configured here for reference.
26 | |
27 | | Supported drivers: "local", "ftp", "sftp", "s3"
28 | |
29 | */
30 |
31 | 'disks' => [
32 |
33 | 'local' => [
34 | 'driver' => 'local',
35 | 'root' => storage_path('app'),
36 | 'throw' => false,
37 | ],
38 |
39 | 'public' => [
40 | 'driver' => 'local',
41 | 'root' => storage_path('app/public'),
42 | 'url' => env('APP_URL') . '/storage',
43 | 'visibility' => 'public',
44 | 'throw' => false,
45 | ],
46 |
47 | 's3' => [
48 | 'driver' => 's3',
49 | 'key' => env('AWS_ACCESS_KEY_ID'),
50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
51 | 'region' => env('AWS_DEFAULT_REGION'),
52 | 'bucket' => env('AWS_BUCKET'),
53 | 'url' => env('AWS_URL'),
54 | 'endpoint' => env('AWS_ENDPOINT'),
55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
56 | 'throw' => false,
57 | ],
58 |
59 | ],
60 |
61 | /*
62 | |--------------------------------------------------------------------------
63 | | Symbolic Links
64 | |--------------------------------------------------------------------------
65 | |
66 | | Here you may configure the symbolic links that will be created when the
67 | | `storage:link` Artisan command is executed. The array keys should be
68 | | the locations of the links and the values should be their targets.
69 | |
70 | */
71 |
72 | 'links' => [
73 | public_path('storage') => storage_path('app/public'),
74 | ],
75 |
76 | ];
77 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/TabMenu.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
37 |
38 |
45 |
56 |
60 |
65 | {{ item.label }}
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/resources/js/ssr.ts:
--------------------------------------------------------------------------------
1 | import { createInertiaApp } from '@inertiajs/vue3'
2 | import createServer from '@inertiajs/vue3/server'
3 |
4 | import { renderToString } from '@vue/server-renderer'
5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
6 | import { createSSRApp, DefineComponent, h } from 'vue'
7 | import { route as ziggyRoute, ZiggyVue } from 'ziggy-js'
8 |
9 | import PrimeVue from 'primevue/config'
10 | import Toast from 'primevue/toast'
11 | import ToastService from 'primevue/toastservice'
12 |
13 | import { useSiteColorMode } from '@/composables/useSiteColorMode'
14 |
15 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
16 |
17 | createServer((page) =>
18 | createInertiaApp({
19 | page,
20 | render: renderToString,
21 | title: (title) => `${title} - ${appName}`,
22 | resolve: (name) => resolvePageComponent(
23 | `./pages/${name}.vue`,
24 | import.meta.glob('./pages/**/*.vue'),
25 | ),
26 | setup({ App, props, plugin }) {
27 | // Color mode set from cookie on the server
28 | const cookieColorMode = props.initialPage.props.colorScheme
29 | const colorMode = useSiteColorMode({
30 | cookieColorMode,
31 | emitAuto: true,
32 | })
33 |
34 | // Global Toast component
35 | const Root = {
36 | setup() {
37 | return () => h('div', [
38 | h(App, props),
39 | h(Toast, { position: 'bottom-right' })
40 | ])
41 | }
42 | }
43 |
44 | // Create app
45 | const app = createSSRApp(Root)
46 |
47 | // Configure Ziggy for SSR
48 | const ziggyConfig = {
49 | ...page.props.ziggy,
50 | location: new URL(page.props.ziggy.location),
51 | }
52 | const boundRoute: typeof ziggyRoute = ((name?: any, params?: any, absolute?: boolean) => {
53 | return ziggyRoute(name, params, absolute, ziggyConfig)
54 | }) as typeof ziggyRoute
55 | app.config.globalProperties.route = boundRoute
56 | app.config.globalProperties.$route = boundRoute
57 | if (typeof globalThis !== 'undefined') {
58 | (globalThis as any).route = boundRoute
59 | }
60 |
61 | app.use(plugin)
62 | .use(ZiggyVue, ziggyConfig)
63 | .use(PrimeVue, { theme: 'none' }) // TODO: PrimeVue won't render it's styles server side
64 | .use(ToastService)
65 | .provide('colorMode', colorMode)
66 |
67 | return app
68 | },
69 | }),
70 | )
71 |
--------------------------------------------------------------------------------
/resources/js/components/DeleteUserModal.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
82 |
83 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/Menu.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
80 |
81 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/Breadcrumb.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
28 |
29 |
38 |
42 |
47 | {{ item.label }}
48 |
49 |
58 |
62 |
67 | {{ item.label }}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/ContextMenu.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
28 |
29 |
38 |
42 |
47 |
48 |
49 |
58 |
62 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://getcomposer.org/schema.json",
3 | "name": "connora/laravel-primevue-starter-kit",
4 | "type": "project",
5 | "description": "Laravel + PrimeVue Starter Kit",
6 | "keywords": [
7 | "laravel",
8 | "framework"
9 | ],
10 | "license": "MIT",
11 | "require": {
12 | "php": "^8.2",
13 | "inertiajs/inertia-laravel": "^2.0",
14 | "laravel/framework": "^12.0",
15 | "laravel/tinker": "^2.10.1",
16 | "tightenco/ziggy": "^2.5"
17 | },
18 | "require-dev": {
19 | "fakerphp/faker": "^1.23",
20 | "larastan/larastan": "^3.4",
21 | "laravel/pail": "^1.2.2",
22 | "laravel/pint": "^1.13",
23 | "laravel/sail": "^1.41",
24 | "mockery/mockery": "^1.6",
25 | "nunomaduro/collision": "^8.6",
26 | "phpstan/phpstan": "^2.1",
27 | "phpunit/phpunit": "^11.5.3"
28 | },
29 | "autoload": {
30 | "psr-4": {
31 | "App\\": "app/",
32 | "Database\\Factories\\": "database/factories/",
33 | "Database\\Seeders\\": "database/seeders/"
34 | },
35 | "files": [
36 | "app/helpers.php"
37 | ]
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "Tests\\": "tests/"
42 | }
43 | },
44 | "scripts": {
45 | "post-autoload-dump": [
46 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
47 | "@php artisan package:discover --ansi"
48 | ],
49 | "post-update-cmd": [
50 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
51 | ],
52 | "post-root-package-install": [
53 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
54 | ],
55 | "post-create-project-cmd": [
56 | "@php artisan key:generate --ansi",
57 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
58 | "@php artisan migrate --graceful --ansi"
59 | ],
60 | "analyse": "phpstan analyse --memory-limit=2G",
61 | "dev": [
62 | "Composer\\Config::disableProcessTimeout",
63 | "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
64 | ],
65 | "test": [
66 | "@php artisan config:clear --ansi",
67 | "@php artisan test"
68 | ]
69 | },
70 | "extra": {
71 | "laravel": {
72 | "dont-discover": []
73 | }
74 | },
75 | "config": {
76 | "optimize-autoloader": true,
77 | "preferred-install": "dist",
78 | "sort-packages": true,
79 | "allow-plugins": {
80 | "pestphp/pest-plugin": true,
81 | "php-http/discovery": true
82 | }
83 | },
84 | "minimum-stability": "stable",
85 | "prefer-stable": true
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Feature/Settings/ProfileUpdateTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $response = $this
18 | ->actingAs($user)
19 | ->get('/settings/profile');
20 |
21 | $response->assertOk();
22 | }
23 |
24 | public function test_profile_information_can_be_updated()
25 | {
26 | $user = User::factory()->create();
27 |
28 | $response = $this
29 | ->actingAs($user)
30 | ->patch('/settings/profile', [
31 | 'name' => 'Test User',
32 | 'email' => 'test@example.com',
33 | ]);
34 |
35 | $response
36 | ->assertSessionHasNoErrors()
37 | ->assertRedirect('/settings/profile');
38 |
39 | $user->refresh();
40 |
41 | $this->assertSame('Test User', $user->name);
42 | $this->assertSame('test@example.com', $user->email);
43 | $this->assertNull($user->email_verified_at);
44 | }
45 |
46 | public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged()
47 | {
48 | $user = User::factory()->create();
49 |
50 | $response = $this
51 | ->actingAs($user)
52 | ->patch('/settings/profile', [
53 | 'name' => 'Test User',
54 | 'email' => $user->email,
55 | ]);
56 |
57 | $response
58 | ->assertSessionHasNoErrors()
59 | ->assertRedirect('/settings/profile');
60 |
61 | $this->assertNotNull($user->refresh()->email_verified_at);
62 | }
63 |
64 | public function test_user_can_delete_their_account()
65 | {
66 | $user = User::factory()->create();
67 |
68 | $response = $this
69 | ->actingAs($user)
70 | ->delete('/settings/profile', [
71 | 'password' => 'password',
72 | ]);
73 |
74 | $response
75 | ->assertSessionHasNoErrors()
76 | ->assertRedirect('/');
77 |
78 | $this->assertGuest();
79 | $this->assertNull($user->fresh());
80 | }
81 |
82 | public function test_correct_password_must_be_provided_to_delete_account()
83 | {
84 | $user = User::factory()->create();
85 |
86 | $response = $this
87 | ->actingAs($user)
88 | ->from('/settings/profile')
89 | ->delete('/settings/profile', [
90 | 'password' => 'wrong-password',
91 | ]);
92 |
93 | $response
94 | ->assertSessionHasErrors('password')
95 | ->assertRedirect('/settings/profile');
96 |
97 | $this->assertNotNull($user->fresh());
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/resources/js/components/ApplicationLogo.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/js/app.ts:
--------------------------------------------------------------------------------
1 | import '../css/app.css'
2 | import '../css/tailwind.css'
3 |
4 | import { createInertiaApp, router } from '@inertiajs/vue3'
5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
6 | import { createSSRApp, DefineComponent, h } from 'vue'
7 | import { ZiggyVue } from 'ziggy-js'
8 |
9 | import PrimeVue from 'primevue/config'
10 | import Toast from 'primevue/toast'
11 | import ToastService from 'primevue/toastservice'
12 | import { useToast } from 'primevue/usetoast'
13 |
14 | import { useSiteColorMode } from '@/composables/useSiteColorMode'
15 | import globalPt from '@/theme/global-pt'
16 | import themePreset from '@/theme/noir-preset'
17 |
18 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
19 |
20 | createInertiaApp({
21 | title: (title) => `${title} - ${appName}`,
22 | resolve: (name) =>
23 | resolvePageComponent(
24 | `./pages/${name}.vue`,
25 | import.meta.glob('./pages/**/*.vue'),
26 | ),
27 | setup({ el, App, props, plugin }) {
28 | // Site light/dark mode
29 | const colorMode = useSiteColorMode({ emitAuto: true })
30 |
31 | // Root component with Global Toast
32 | const Root = {
33 | setup() {
34 | // show error toast instead of standard Inertia modal response
35 | const toast = useToast()
36 | router.on('invalid', (event) => {
37 | const responseBody = event.detail.response?.data
38 | if (responseBody?.error_summary && responseBody?.error_detail) {
39 | event.preventDefault()
40 | toast.add({
41 | severity: event.detail.response?.status >= 500 ? 'error' : 'warn',
42 | summary: responseBody.error_summary,
43 | detail: responseBody.error_detail,
44 | life: 5000,
45 | })
46 | }
47 | })
48 |
49 | return () => h('div', [
50 | h(App, props),
51 | h(Toast, { position: 'bottom-right' })
52 | ])
53 | }
54 | }
55 |
56 | createSSRApp(Root)
57 | .use(plugin)
58 | .use(ZiggyVue)
59 | .use(PrimeVue, {
60 | theme: {
61 | preset: themePreset,
62 | options: {
63 | darkModeSelector: '.dark',
64 | cssLayer: {
65 | name: 'primevue',
66 | order: 'theme, base, primevue',
67 | },
68 | },
69 | },
70 | pt: globalPt,
71 | })
72 | .use(ToastService)
73 | .provide('colorMode', colorMode)
74 | .mount(el);
75 |
76 | // #app content set to hidden by default
77 | // reduces jumpy initial render from SSR content (unstyled PrimeVue components)
78 | (el as HTMLElement).style.visibility = 'visible'
79 | },
80 | progress: {
81 | color: 'var(--p-primary-500)',
82 | },
83 | })
84 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/TieredMenu.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
25 |
29 |
34 |
35 |
36 |
45 |
49 |
54 |
55 |
56 |
65 |
69 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/resources/js/pages/auth/ForgotPassword.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
43 | {{ props.status }}
44 |
45 |
46 |
47 |
48 |
49 | Forgot password
50 |
51 |
52 |
53 |
54 |
55 | Enter your email address to receive a password reset link
56 |
57 |
58 |
59 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/resources/js/components/primevue/menu/Menubar.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
28 |
32 |
37 |
38 |
39 |
48 |
52 |
57 |
58 |
59 |
68 |
72 |
77 |
78 |
79 |
83 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/docker/local/web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:24.04
2 |
3 | LABEL maintainer="Taylor Otwell"
4 |
5 | ARG WWWGROUP
6 | ARG NODE_VERSION=22
7 | ARG MYSQL_CLIENT="mysql-client"
8 | ARG POSTGRES_VERSION=17
9 |
10 | WORKDIR /var/www/html
11 |
12 | ENV DEBIAN_FRONTEND=noninteractive
13 | ENV TZ=UTC
14 | ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
15 | ENV SUPERVISOR_PHP_USER="sail"
16 |
17 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
18 |
19 | RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
20 | echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
21 | echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
22 |
23 | RUN apt-get update && apt-get upgrade -y \
24 | && mkdir -p /etc/apt/keyrings \
25 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
26 | && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
27 | && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
28 | && apt-get update \
29 | && apt-get install -y php8.4-cli php8.4-dev \
30 | php8.4-pgsql php8.4-sqlite3 php8.4-gd \
31 | php8.4-curl php8.4-mongodb \
32 | php8.4-imap php8.4-mysql php8.4-mbstring \
33 | php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
34 | php8.4-intl php8.4-readline \
35 | php8.4-ldap \
36 | php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
37 | php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
38 | && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
39 | && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
40 | && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
41 | && apt-get update \
42 | && apt-get install -y nodejs \
43 | && npm install -g npm \
44 | && npm install -g pnpm \
45 | && npm install -g bun \
46 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
47 | && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
48 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
49 | && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
50 | && apt-get update \
51 | && apt-get install -y yarn \
52 | && apt-get install -y $MYSQL_CLIENT \
53 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \
54 | && apt-get -y autoremove \
55 | && apt-get clean \
56 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
57 |
58 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
59 |
60 | RUN userdel -r ubuntu
61 | RUN groupadd --force -g $WWWGROUP sail
62 | RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
63 |
64 | COPY start-container /usr/local/bin/start-container
65 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
66 | COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
67 | RUN chmod +x /usr/local/bin/start-container
68 |
69 | EXPOSE 80/tcp
70 |
71 | ENTRYPOINT ["start-container"]
72 |
--------------------------------------------------------------------------------
/resources/js/composables/useAppLayout.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
2 | import { usePage, useForm } from '@inertiajs/vue3'
3 | import { LayoutGrid, House, Info, Settings, LogOut, ExternalLink, FileSearch, FolderGit2 } from 'lucide-vue-next'
4 | import { MenuItem } from '@/types'
5 |
6 | export function useAppLayout() {
7 | const page = usePage()
8 | const currentRoute = computed(() => {
9 | // Access page.url to trigger re-computation on navigation.
10 | /* eslint-disable @typescript-eslint/no-unused-vars */
11 | const url = page.url
12 | /* eslint-enable @typescript-eslint/no-unused-vars */
13 | return route().current()
14 | })
15 |
16 | // Menu items
17 | const menuItems = computed