├── .editorconfig ├── .env.example ├── .env.testing ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .prettierrc ├── README.md ├── app ├── Actions │ └── Fortify │ │ ├── CreateNewUser.php │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ └── UpdateUserProfileInformation.php ├── Http │ ├── Controllers │ │ ├── Account │ │ │ ├── ProfileController.php │ │ │ ├── SecurityController.php │ │ │ └── SessionController.php │ │ ├── Auth │ │ │ └── ConfirmablePasswordController.php │ │ └── Controller.php │ ├── Middleware │ │ └── HandleInertiaRequests.php │ └── Requests │ │ ├── Auth │ │ └── LoginRequest.php │ │ └── ProfileUpdateRequest.php ├── Models │ ├── Session.php │ └── User.php └── Providers │ ├── AppServiceProvider.php │ └── FortifyServiceProvider.php ├── artisan ├── bootstrap ├── app.php ├── cache │ ├── packages.php │ └── services.php └── providers.php ├── components.json ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── fortify.php ├── logging.php ├── mail.php ├── queue.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2025_01_25_140923_create_personal_access_tokens_table.php │ └── 2025_01_29_181350_add_two_factor_columns_to_users_table.php └── seeders │ └── DatabaseSeeder.php ├── docs ├── backend.md ├── frontend.md └── getting-started.md ├── package.json ├── phpunit.xml ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── .htaccess ├── favicon.png ├── index.php └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── Pages │ │ ├── Auth │ │ │ ├── ForgotPassword.vue │ │ │ ├── ForgotPasswordSent.vue │ │ │ ├── Login.vue │ │ │ ├── PasswordReset.vue │ │ │ ├── Register.vue │ │ │ ├── TwoFactorChallenge.vue │ │ │ └── VerifyEmail.vue │ │ ├── Dashboard.vue │ │ ├── Profile │ │ │ ├── Partials │ │ │ │ ├── DeleteUserForm.vue │ │ │ │ ├── UpdatePasswordForm.vue │ │ │ │ └── UpdateProfileInformationForm.vue │ │ │ └── Show.vue │ │ ├── Security │ │ │ ├── Partials │ │ │ │ └── TwoFactorAuthenticationForm.vue │ │ │ └── Show.vue │ │ └── Welcome.vue │ ├── app.ts │ ├── bootstrap.ts │ ├── components │ │ ├── custom │ │ │ ├── AppBreadcrumb.vue │ │ │ ├── AppCommand.vue │ │ │ ├── AppSidebar.vue │ │ │ ├── ConfirmWithPassword.vue │ │ │ ├── ErrorFeedback.vue │ │ │ ├── NavMain.vue │ │ │ ├── NavProjectMembers.vue │ │ │ ├── NavSecondary.vue │ │ │ ├── NavUser.vue │ │ │ ├── ProjectSwitcher.vue │ │ │ └── ripple │ │ │ │ ├── Ripple.vue │ │ │ │ ├── RippleCircle.vue │ │ │ │ ├── RippleContainer.vue │ │ │ │ └── index.ts │ │ └── ui │ │ │ ├── alert-dialog │ │ │ ├── AlertDialog.vue │ │ │ ├── AlertDialogAction.vue │ │ │ ├── AlertDialogCancel.vue │ │ │ ├── AlertDialogContent.vue │ │ │ ├── AlertDialogDescription.vue │ │ │ ├── AlertDialogFooter.vue │ │ │ ├── AlertDialogHeader.vue │ │ │ ├── AlertDialogTitle.vue │ │ │ ├── AlertDialogTrigger.vue │ │ │ └── index.ts │ │ │ ├── avatar │ │ │ ├── Avatar.vue │ │ │ ├── AvatarFallback.vue │ │ │ ├── AvatarImage.vue │ │ │ └── index.ts │ │ │ ├── badge │ │ │ ├── Badge.vue │ │ │ └── index.ts │ │ │ ├── breadcrumb │ │ │ ├── Breadcrumb.vue │ │ │ ├── BreadcrumbEllipsis.vue │ │ │ ├── BreadcrumbItem.vue │ │ │ ├── BreadcrumbLink.vue │ │ │ ├── BreadcrumbList.vue │ │ │ ├── BreadcrumbPage.vue │ │ │ ├── BreadcrumbSeparator.vue │ │ │ └── index.ts │ │ │ ├── button │ │ │ ├── Button.vue │ │ │ └── index.ts │ │ │ ├── collapsible │ │ │ ├── Collapsible.vue │ │ │ ├── CollapsibleContent.vue │ │ │ ├── CollapsibleTrigger.vue │ │ │ └── index.ts │ │ │ ├── command │ │ │ ├── Command.vue │ │ │ ├── CommandDialog.vue │ │ │ ├── CommandEmpty.vue │ │ │ ├── CommandGroup.vue │ │ │ ├── CommandInput.vue │ │ │ ├── CommandItem.vue │ │ │ ├── CommandList.vue │ │ │ ├── CommandSeparator.vue │ │ │ ├── CommandShortcut.vue │ │ │ └── index.ts │ │ │ ├── dialog │ │ │ ├── Dialog.vue │ │ │ ├── DialogClose.vue │ │ │ ├── DialogContent.vue │ │ │ ├── DialogDescription.vue │ │ │ ├── DialogFooter.vue │ │ │ ├── DialogHeader.vue │ │ │ ├── DialogScrollContent.vue │ │ │ ├── DialogTitle.vue │ │ │ ├── DialogTrigger.vue │ │ │ └── index.ts │ │ │ ├── dropdown-menu │ │ │ ├── DropdownMenu.vue │ │ │ ├── DropdownMenuCheckboxItem.vue │ │ │ ├── DropdownMenuContent.vue │ │ │ ├── DropdownMenuGroup.vue │ │ │ ├── DropdownMenuItem.vue │ │ │ ├── DropdownMenuLabel.vue │ │ │ ├── DropdownMenuRadioGroup.vue │ │ │ ├── DropdownMenuRadioItem.vue │ │ │ ├── DropdownMenuSeparator.vue │ │ │ ├── DropdownMenuShortcut.vue │ │ │ ├── DropdownMenuSub.vue │ │ │ ├── DropdownMenuSubContent.vue │ │ │ ├── DropdownMenuSubTrigger.vue │ │ │ ├── DropdownMenuTrigger.vue │ │ │ └── index.ts │ │ │ ├── input │ │ │ ├── Input.vue │ │ │ └── index.ts │ │ │ ├── label │ │ │ ├── Label.vue │ │ │ └── index.ts │ │ │ ├── pin-input │ │ │ ├── PinInput.vue │ │ │ ├── PinInputGroup.vue │ │ │ ├── PinInputInput.vue │ │ │ ├── PinInputSeparator.vue │ │ │ └── index.ts │ │ │ ├── separator │ │ │ ├── Separator.vue │ │ │ └── index.ts │ │ │ ├── sheet │ │ │ ├── Sheet.vue │ │ │ ├── SheetClose.vue │ │ │ ├── SheetContent.vue │ │ │ ├── SheetDescription.vue │ │ │ ├── SheetFooter.vue │ │ │ ├── SheetHeader.vue │ │ │ ├── SheetTitle.vue │ │ │ ├── SheetTrigger.vue │ │ │ └── index.ts │ │ │ ├── sidebar │ │ │ ├── Sidebar.vue │ │ │ ├── SidebarContent.vue │ │ │ ├── SidebarFooter.vue │ │ │ ├── SidebarGroup.vue │ │ │ ├── SidebarGroupAction.vue │ │ │ ├── SidebarGroupContent.vue │ │ │ ├── SidebarGroupLabel.vue │ │ │ ├── SidebarHeader.vue │ │ │ ├── SidebarInput.vue │ │ │ ├── SidebarInset.vue │ │ │ ├── SidebarMenu.vue │ │ │ ├── SidebarMenuAction.vue │ │ │ ├── SidebarMenuBadge.vue │ │ │ ├── SidebarMenuButton.vue │ │ │ ├── SidebarMenuButtonChild.vue │ │ │ ├── SidebarMenuItem.vue │ │ │ ├── SidebarMenuSkeleton.vue │ │ │ ├── SidebarMenuSub.vue │ │ │ ├── SidebarMenuSubButton.vue │ │ │ ├── SidebarMenuSubItem.vue │ │ │ ├── SidebarProvider.vue │ │ │ ├── SidebarRail.vue │ │ │ ├── SidebarSeparator.vue │ │ │ ├── SidebarTrigger.vue │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ │ ├── skeleton │ │ │ ├── Skeleton.vue │ │ │ └── index.ts │ │ │ ├── sonner │ │ │ ├── Sonner.vue │ │ │ └── index.ts │ │ │ └── tooltip │ │ │ ├── Tooltip.vue │ │ │ ├── TooltipContent.vue │ │ │ ├── TooltipProvider.vue │ │ │ ├── TooltipTrigger.vue │ │ │ └── index.ts │ ├── layouts │ │ ├── AuthenticatedLayout.vue │ │ └── AuthenticationLayout.vue │ ├── lib │ │ └── utils.ts │ ├── ssr.ts │ └── types │ │ ├── global.d.ts │ │ ├── index.d.ts │ │ └── vite-env.d.ts └── views │ └── app.blade.php ├── routes ├── auth.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ ├── PasswordUpdateTest.php │ │ └── RegistrationTest.php │ ├── ExampleTest.php │ └── ProfileTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php ├── tsconfig.json └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Vue Inertia Laravel" 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://vue-inertia-laravel.test 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 | PHP_CLI_SERVER_WORKERS=4 16 | 17 | BCRYPT_ROUNDS=12 18 | 19 | LOG_CHANNEL=stack 20 | LOG_STACK=single 21 | LOG_DEPRECATIONS_CHANNEL=null 22 | LOG_LEVEL=debug 23 | 24 | DB_CONNECTION=sqlite 25 | # DB_HOST=127.0.0.1 26 | # DB_PORT=3306 27 | # DB_DATABASE=laravel 28 | # DB_USERNAME=root 29 | # DB_PASSWORD= 30 | 31 | SESSION_DRIVER=database 32 | SESSION_LIFETIME=120 33 | SESSION_ENCRYPT=false 34 | SESSION_PATH=/ 35 | SESSION_DOMAIN=null 36 | 37 | BROADCAST_CONNECTION=log 38 | FILESYSTEM_DISK=local 39 | QUEUE_CONNECTION=database 40 | 41 | CACHE_STORE=database 42 | CACHE_PREFIX= 43 | 44 | MEMCACHED_HOST=127.0.0.1 45 | 46 | REDIS_CLIENT=phpredis 47 | REDIS_HOST=127.0.0.1 48 | REDIS_PASSWORD=null 49 | REDIS_PORT=6379 50 | 51 | MAIL_MAILER=log 52 | MAIL_SCHEME=null 53 | MAIL_HOST=127.0.0.1 54 | MAIL_PORT=2525 55 | MAIL_USERNAME=null 56 | MAIL_PASSWORD=null 57 | MAIL_FROM_ADDRESS="hello@example.com" 58 | MAIL_FROM_NAME="${APP_NAME}" 59 | 60 | AWS_ACCESS_KEY_ID= 61 | AWS_SECRET_ACCESS_KEY= 62 | AWS_DEFAULT_REGION=us-east-1 63 | AWS_BUCKET= 64 | AWS_USE_PATH_STYLE_ENDPOINT=false 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_NAME="Laravel" 2 | APP_ENV=testing 3 | APP_KEY=base64:vdJO+cS13hn5KPCE0H2pTRdbsV1h5eAUlogToz390G4= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | DB_CONNECTION=sqlite 8 | DB_DATABASE=:memory: 9 | 10 | BROADCAST_DRIVER=log 11 | CACHE_DRIVER=file 12 | FILESYSTEM_DISK=local 13 | QUEUE_CONNECTION=sync 14 | SESSION_DRIVER=database 15 | SESSION_LIFETIME=120 16 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | }, 15 | rules: { 16 | 'vue/multi-word-component-names': 'off', 17 | 'no-undef': 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /bootstrap/ssr 3 | /node_modules 4 | /public/build 5 | /public/hot 6 | /public/storage 7 | /storage/*.key 8 | /storage/pail 9 | /vendor 10 | .env 11 | .env.backup 12 | .env.production 13 | .phpactor.json 14 | .phpunit.result.cache 15 | Homestead.json 16 | Homestead.yaml 17 | npm-debug.log 18 | yarn-error.log 19 | /auth.json 20 | /.fleet 21 | /.idea 22 | /.nova 23 | /.vscode 24 | /.zed 25 | .windsurfrules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "plugins": [ 4 | "prettier-plugin-organize-imports", 5 | "prettier-plugin-tailwindcss" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | $input 19 | */ 20 | public function create(array $input): User 21 | { 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | 'email' => [ 25 | 'required', 26 | 'string', 27 | 'email', 28 | 'max:255', 29 | Rule::unique(User::class), 30 | ], 31 | 'password' => $this->passwordRules(), 32 | ])->validate(); 33 | 34 | return User::create([ 35 | 'name' => $input['name'], 36 | 'email' => $input['email'], 37 | 'password' => Hash::make($input['password']), 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | |string> 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', Password::default(), 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function reset(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'password' => $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'current_password' => ['required', 'string', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('The provided password does not match your current password.'), 26 | ])->validateWithBag('updatePassword'); 27 | 28 | $user->forceFill([ 29 | 'password' => Hash::make($input['password']), 30 | ])->save(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserProfileInformation.php: -------------------------------------------------------------------------------- 1 | $input 17 | */ 18 | public function update(User $user, array $input): void 19 | { 20 | Validator::make($input, [ 21 | 'name' => ['required', 'string', 'max:255'], 22 | 23 | 'email' => [ 24 | 'required', 25 | 'string', 26 | 'email', 27 | 'max:255', 28 | Rule::unique('users')->ignore($user->id), 29 | ], 30 | ])->validateWithBag('updateProfileInformation'); 31 | 32 | if ($input['email'] !== $user->email && 33 | $user instanceof MustVerifyEmail) { 34 | $this->updateVerifiedUser($user, $input); 35 | } else { 36 | $user->forceFill([ 37 | 'name' => $input['name'], 38 | 'email' => $input['email'], 39 | ])->save(); 40 | } 41 | } 42 | 43 | /** 44 | * Update the given verified user's profile information. 45 | * 46 | * @param array $input 47 | */ 48 | protected function updateVerifiedUser(User $user, array $input): void 49 | { 50 | $user->forceFill([ 51 | 'name' => $input['name'], 52 | 'email' => $input['email'], 53 | 'email_verified_at' => null, 54 | ])->save(); 55 | 56 | $user->sendEmailVerificationNotification(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/Account/ProfileController.php: -------------------------------------------------------------------------------- 1 | $request->user() instanceof MustVerifyEmail, 26 | 'isUpdateProfileEnabled' => Features::enabled(Features::updateProfileInformation()), 27 | 'isUpdatePasswordEnabled' => Features::enabled(Features::updatePasswords()), 28 | ]); 29 | } 30 | 31 | /** 32 | * Update the user's profile information. 33 | */ 34 | public function update(Request $request): RedirectResponse 35 | { 36 | $input = $request->all(); 37 | 38 | Validator::make($input, [ 39 | 'name' => ['required', 'string', 'max:255'], 40 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($request->user()->id)], 41 | 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], 42 | ])->validateWithBag('updateProfileInformation'); 43 | 44 | $user = $request->user(); 45 | 46 | if (isset($input['photo'])) { 47 | $user->updateProfilePhoto($input['photo']); 48 | } 49 | 50 | if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) { 51 | $this->updateVerifiedUser($user, $input); 52 | } else { 53 | $user->forceFill([ 54 | 'name' => $input['name'], 55 | 'email' => $input['email'], 56 | ])->save(); 57 | } 58 | 59 | return Redirect::route('profile.show'); 60 | } 61 | 62 | /** 63 | * Update the given verified user's profile information. 64 | */ 65 | protected function updateVerifiedUser($user, array $input): void 66 | { 67 | $user->forceFill([ 68 | 'name' => $input['name'], 69 | 'email' => $input['email'], 70 | 'email_verified_at' => null, 71 | ])->save(); 72 | } 73 | 74 | /** 75 | * Delete the user's account. 76 | */ 77 | public function destroy(Request $request): RedirectResponse 78 | { 79 | $request->validate([ 80 | 'password' => ['required', 'current_password'], 81 | ]); 82 | 83 | $user = $request->user(); 84 | 85 | Auth::logout(); 86 | 87 | $user->delete(); 88 | 89 | $request->session()->invalidate(); 90 | $request->session()->regenerateToken(); 91 | 92 | return Redirect::to('/'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/Http/Controllers/Account/SecurityController.php: -------------------------------------------------------------------------------- 1 | Session::where('user_id', $request->user()->id)->get(), 25 | 'isTwoFactorAuthenticationFeatureEnabled' => Features::enabled(Features::twoFactorAuthentication()), 26 | ]); 27 | } 28 | 29 | /** 30 | * Update the user's profile information. 31 | */ 32 | public function update(ProfileUpdateRequest $request): RedirectResponse 33 | { 34 | $request->user()->fill($request->validated()); 35 | 36 | if ($request->user()->isDirty('email')) { 37 | $request->user()->email_verified_at = null; 38 | } 39 | 40 | $request->user()->save(); 41 | 42 | return Redirect::route('profile.show'); 43 | } 44 | 45 | /** 46 | * Delete the user's account. 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::to('/'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Http/Controllers/Account/SessionController.php: -------------------------------------------------------------------------------- 1 | validate([ 24 | 'password' => ['required', 'string', 'current_password'], 25 | ]); 26 | 27 | DB::table('sessions') 28 | ->where('user_id', Auth::id()) 29 | ->where('id', '!=', request()->session()->getId()) 30 | ->delete(); 31 | 32 | return back(303)->with('status', 'other-browser-sessions-terminated'); 33 | } 34 | 35 | /** 36 | * Destroy a specific session. 37 | */ 38 | public function destroySession(Request $request, string $id): RedirectResponse 39 | { 40 | if (config('session.driver') !== 'database') { 41 | return back(409); 42 | } 43 | 44 | $request->validate([ 45 | 'password' => ['required', 'string', 'current_password'], 46 | ]); 47 | 48 | // Don't allow destroying the current session 49 | if ($id === $request->session()->getId()) { 50 | throw ValidationException::withMessages([ 51 | 'session' => ['Cannot terminate current session'], 52 | ]); 53 | } 54 | 55 | // Verify the session belongs to the current user 56 | $session = DB::table('sessions') 57 | ->where('user_id', Auth::id()) 58 | ->where('id', $id) 59 | ->first(); 60 | 61 | if (! $session) { 62 | throw ValidationException::withMessages([ 63 | 'session' => ['Session not found or does not belong to you'], 64 | ]); 65 | } 66 | 67 | DB::table('sessions') 68 | ->where('user_id', Auth::id()) 69 | ->where('id', $id) 70 | ->delete(); 71 | 72 | return back(303)->with('status', 'browser-session-terminated'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'email' => $request->user()->email, 20 | 'password' => $request->password, 21 | ])) { 22 | throw ValidationException::withMessages([ 23 | 'password' => __('auth.password'), 24 | ]); 25 | } 26 | 27 | $request->session()->put('auth.password_confirmed_at', time()); 28 | 29 | return redirect()->intended(route('dashboard', absolute: false)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function share(Request $request): array 32 | { 33 | return [ 34 | ...parent::share($request), 35 | 'auth' => [ 36 | 'user' => $request->user(), 37 | ], 38 | 'ziggy' => fn () => [ 39 | ...(new Ziggy)->toArray(), 40 | 'location' => $request->url(), 41 | ], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | |string> 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'email' => ['required', 'string', 'email'], 31 | 'password' => ['required', 'string'], 32 | ]; 33 | } 34 | 35 | /** 36 | * Attempt to authenticate the request's credentials. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function authenticate(): void 41 | { 42 | $this->ensureIsNotRateLimited(); 43 | 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45 | RateLimiter::hit($this->throttleKey()); 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => trans('auth.failed'), 49 | ]); 50 | } 51 | 52 | RateLimiter::clear($this->throttleKey()); 53 | } 54 | 55 | /** 56 | * Ensure the login request is not rate limited. 57 | * 58 | * @throws \Illuminate\Validation\ValidationException 59 | */ 60 | public function ensureIsNotRateLimited(): void 61 | { 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63 | return; 64 | } 65 | 66 | event(new Lockout($this)); 67 | 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); 69 | 70 | throw ValidationException::withMessages([ 71 | 'email' => trans('auth.throttle', [ 72 | 'seconds' => $seconds, 73 | 'minutes' => ceil($seconds / 60), 74 | ]), 75 | ]); 76 | } 77 | 78 | /** 79 | * Get the rate limiting throttle key for the request. 80 | */ 81 | public function throttleKey(): string 82 | { 83 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | |string> 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => [ 21 | 'required', 22 | 'string', 23 | 'lowercase', 24 | 'email', 25 | 'max:255', 26 | Rule::unique(User::class)->ignore($this->user()->id), 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/Session.php: -------------------------------------------------------------------------------- 1 | 'datetime', 19 | ]; 20 | 21 | protected $appends = [ 22 | 'last_active_ago', 23 | ]; 24 | 25 | public function user(): BelongsTo 26 | { 27 | return $this->belongsTo(User::class); 28 | } 29 | 30 | public function getLastActiveAgoAttribute() 31 | { 32 | return $this->last_activity->diffForHumans(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 16 | use HasFactory, Notifiable, TwoFactorAuthenticatable; 17 | 18 | /** 19 | * The attributes that are mass assignable. 20 | * 21 | * @var list 22 | */ 23 | protected $fillable = [ 24 | 'name', 25 | 'email', 26 | 'password', 27 | ]; 28 | 29 | /** 30 | * The attributes that should be hidden for serialization. 31 | * 32 | * @var list 33 | */ 34 | protected $hidden = [ 35 | 'password', 36 | 'remember_token', 37 | 'two_factor_recovery_codes', 38 | 'two_factor_secret', 39 | ]; 40 | 41 | /** 42 | * The accessors to append to the model's array form. 43 | * 44 | * @var array 45 | */ 46 | protected $appends = [ 47 | 'profile_photo_url', 48 | ]; 49 | 50 | /** 51 | * Get the attributes that should be cast. 52 | * 53 | * @return array 54 | */ 55 | protected function casts(): array 56 | { 57 | return [ 58 | 'email_verified_at' => 'datetime', 59 | 'password' => 'hashed', 60 | ]; 61 | } 62 | 63 | /** 64 | * Update the user's profile photo. 65 | */ 66 | public function updateProfilePhoto(UploadedFile $photo): void 67 | { 68 | tap($this->profile_photo_path, function ($previous) use ($photo) { 69 | $this->forceFill([ 70 | 'profile_photo_path' => $photo->storePublicly( 71 | 'profile-photos', 72 | ['disk' => 'public'] 73 | ), 74 | ])->save(); 75 | 76 | if ($previous) { 77 | Storage::disk('public')->delete($previous); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * Get the URL to the user's profile photo. 84 | */ 85 | public function getProfilePhotoUrlAttribute(): string 86 | { 87 | return $this->profile_photo_path 88 | ? asset('storage/'.$this->profile_photo_path) 89 | : ''; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | input(Fortify::username())).'|'.$request->ip()); 38 | 39 | return Limit::perMinute(5)->by($throttleKey); 40 | }); 41 | 42 | RateLimiter::for('two-factor', function (Request $request) { 43 | return Limit::perMinute(5)->by($request->session()->get('login.id')); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | health: '/up', 11 | ) 12 | ->withMiddleware(function (Middleware $middleware) { 13 | $middleware->web(append: [ 14 | \App\Http\Middleware\HandleInertiaRequests::class, 15 | \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, 16 | ]); 17 | 18 | // 19 | }) 20 | ->withExceptions(function (Exceptions $exceptions) { 21 | // 22 | })->create(); 23 | -------------------------------------------------------------------------------- /bootstrap/cache/packages.php: -------------------------------------------------------------------------------- 1 | 3 | array ( 4 | 'providers' => 5 | array ( 6 | 0 => 'Inertia\\ServiceProvider', 7 | ), 8 | ), 9 | 'jenssegers/agent' => 10 | array ( 11 | 'aliases' => 12 | array ( 13 | 'Agent' => 'Jenssegers\\Agent\\Facades\\Agent', 14 | ), 15 | 'providers' => 16 | array ( 17 | 0 => 'Jenssegers\\Agent\\AgentServiceProvider', 18 | ), 19 | ), 20 | 'laravel/breeze' => 21 | array ( 22 | 'providers' => 23 | array ( 24 | 0 => 'Laravel\\Breeze\\BreezeServiceProvider', 25 | ), 26 | ), 27 | 'laravel/fortify' => 28 | array ( 29 | 'providers' => 30 | array ( 31 | 0 => 'Laravel\\Fortify\\FortifyServiceProvider', 32 | ), 33 | ), 34 | 'laravel/pail' => 35 | array ( 36 | 'providers' => 37 | array ( 38 | 0 => 'Laravel\\Pail\\PailServiceProvider', 39 | ), 40 | ), 41 | 'laravel/sail' => 42 | array ( 43 | 'providers' => 44 | array ( 45 | 0 => 'Laravel\\Sail\\SailServiceProvider', 46 | ), 47 | ), 48 | 'laravel/sanctum' => 49 | array ( 50 | 'providers' => 51 | array ( 52 | 0 => 'Laravel\\Sanctum\\SanctumServiceProvider', 53 | ), 54 | ), 55 | 'laravel/tinker' => 56 | array ( 57 | 'providers' => 58 | array ( 59 | 0 => 'Laravel\\Tinker\\TinkerServiceProvider', 60 | ), 61 | ), 62 | 'nesbot/carbon' => 63 | array ( 64 | 'providers' => 65 | array ( 66 | 0 => 'Carbon\\Laravel\\ServiceProvider', 67 | ), 68 | ), 69 | 'nunomaduro/collision' => 70 | array ( 71 | 'providers' => 72 | array ( 73 | 0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', 74 | ), 75 | ), 76 | 'nunomaduro/termwind' => 77 | array ( 78 | 'providers' => 79 | array ( 80 | 0 => 'Termwind\\Laravel\\TermwindServiceProvider', 81 | ), 82 | ), 83 | 'pestphp/pest-plugin-laravel' => 84 | array ( 85 | 'providers' => 86 | array ( 87 | 0 => 'Pest\\Laravel\\PestServiceProvider', 88 | ), 89 | ), 90 | 'railsware/mailtrap-php' => 91 | array ( 92 | 'providers' => 93 | array ( 94 | 0 => 'Mailtrap\\Bridge\\Laravel\\MailtrapApiProvider', 95 | ), 96 | ), 97 | 'tightenco/ziggy' => 98 | array ( 99 | 'providers' => 100 | array ( 101 | 0 => 'Tighten\\Ziggy\\ZiggyServiceProvider', 102 | ), 103 | ), 104 | ); -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | 'report' => false, 39 | ], 40 | 41 | 'public' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path('app/public'), 44 | 'url' => env('APP_URL').'/storage', 45 | 'visibility' => 'public', 46 | 'throw' => false, 47 | 'report' => false, 48 | ], 49 | 50 | 's3' => [ 51 | 'driver' => 's3', 52 | 'key' => env('AWS_ACCESS_KEY_ID'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 54 | 'region' => env('AWS_DEFAULT_REGION'), 55 | 'bucket' => env('AWS_BUCKET'), 56 | 'url' => env('AWS_URL'), 57 | 'endpoint' => env('AWS_ENDPOINT'), 58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 59 | 'throw' => false, 60 | 'report' => false, 61 | ], 62 | 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Symbolic Links 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Here you may configure the symbolic links that will be created when the 71 | | `storage:link` Artisan command is executed. The array keys should be 72 | | the locations of the links and the values should be their targets. 73 | | 74 | */ 75 | 76 | 'links' => [ 77 | public_path('storage') => storage_path('app/public'), 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->string('profile_photo_path', 2048)->nullable(); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('password_reset_tokens', function (Blueprint $table) { 26 | $table->string('email')->primary(); 27 | $table->string('token'); 28 | $table->timestamp('created_at')->nullable(); 29 | }); 30 | 31 | Schema::create('sessions', function (Blueprint $table) { 32 | $table->string('id')->primary(); 33 | $table->foreignId('user_id')->nullable()->index(); 34 | $table->string('ip_address', 45)->nullable(); 35 | $table->text('user_agent')->nullable(); 36 | $table->longText('payload'); 37 | $table->integer('last_activity')->index(); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | */ 44 | public function down(): void 45 | { 46 | Schema::dropIfExists('users'); 47 | Schema::dropIfExists('password_reset_tokens'); 48 | Schema::dropIfExists('sessions'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2025_01_25_140923_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2025_01_29_181350_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 16 | ->after('password') 17 | ->nullable(); 18 | 19 | $table->text('two_factor_recovery_codes') 20 | ->after('two_factor_secret') 21 | ->nullable(); 22 | 23 | $table->timestamp('two_factor_confirmed_at') 24 | ->after('two_factor_recovery_codes') 25 | ->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::table('users', function (Blueprint $table) { 35 | $table->dropColumn([ 36 | 'two_factor_secret', 37 | 'two_factor_recovery_codes', 38 | 'two_factor_confirmed_at', 39 | ]); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create([ 17 | 'name' => 'User', 18 | 'email' => 'user@example.com', 19 | 'email_verified_at' => now(), 20 | 'password' => bcrypt('password'), // password 21 | 'remember_token' => Str::random(10), 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-inertia-laravel-starter", 3 | "version": "0.1.0", 4 | "description": "The Vue Inertia Laravel Starter", 5 | "keywords": [ 6 | "laravel", 7 | "inertia", 8 | "vue", 9 | "tailwind", 10 | "shadcn-ui" 11 | ], 12 | "license": "MIT", 13 | "private": true, 14 | "type": "module", 15 | "scripts": { 16 | "build": "vue-tsc && vite build && vite build --ssr", 17 | "dev": "vite", 18 | "lint": "eslint resources/js --ext .js,.ts,.vue --ignore-path .gitignore --fix", 19 | "lint:fix": "eslint . --fix", 20 | "format": "prettier --write .", 21 | "format:check": "prettier --check ." 22 | }, 23 | "dependencies": { 24 | "@vueuse/core": "^12.8.2", 25 | "class-variance-authority": "^0.7.1", 26 | "clsx": "^2.1.1", 27 | "lucide-vue-next": "^0.475.0", 28 | "radix-vue": "^1.9.17", 29 | "tailwind-merge": "^3.3.0", 30 | "tailwindcss-animate": "^1.0.7", 31 | "vue-sonner": "^1.3.2" 32 | }, 33 | "devDependencies": { 34 | "@iconify-json/radix-icons": "^1.2.2", 35 | "@iconify/vue": "^4.3.0", 36 | "@inertiajs/vue3": "^2.0.11", 37 | "@rushstack/eslint-patch": "^1.11.0", 38 | "@tailwindcss/forms": "^0.5.10", 39 | "@vitejs/plugin-vue": "^5.2.4", 40 | "@vue/eslint-config-prettier": "^10.2.0", 41 | "@vue/eslint-config-typescript": "^14.5.0", 42 | "@vue/server-renderer": "^3.5.14", 43 | "autoprefixer": "^10.4.21", 44 | "axios": "^1.9.0", 45 | "concurrently": "^9.1.2", 46 | "eslint": "^9.27.0", 47 | "eslint-plugin-vue": "^9.33.0", 48 | "laravel-vite-plugin": "^1.2.0", 49 | "postcss": "^8.5.3", 50 | "prettier": "^3.5.3", 51 | "prettier-plugin-organize-imports": "^4.1.0", 52 | "prettier-plugin-tailwindcss": "^0.6.11", 53 | "tailwindcss": "^3.4.17", 54 | "typescript": "^5.8.3", 55 | "vite": "^6.3.5", 56 | "vue": "^3.5.14", 57 | "vue-tsc": "^2.2.10" 58 | }, 59 | "pnpm": { 60 | "onlyBuiltDependencies": [ 61 | "esbuild", 62 | "vue-demi" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferjal0/vue-inertia-laravel/62a163d1ab7db11d430a40e88b9448ad8f5ace27/public/favicon.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --accent-foreground: 0 0% 9%; 8 | --accent: 0 0% 96.1%; 9 | --background: 0 0% 100%; 10 | --border: 0 0% 89.8%; 11 | --card-foreground: 0 0% 3.9%; 12 | --card: 0 0% 100%; 13 | --destructive-foreground: 0 0% 98%; 14 | --destructive: 0 84.2% 60.2%; 15 | --foreground: 0 0% 3.9%; 16 | --input: 0 0% 89.8%; 17 | --muted-foreground: 0 0% 45.1%; 18 | --muted: 0 0% 96.1%; 19 | --popover-foreground: 0 0% 3.9%; 20 | --popover: 0 0% 100%; 21 | --primary-foreground: 0 0% 98%; 22 | --primary: 0 0% 9%; 23 | --radius: 0.5rem; 24 | --ring: 0 0% 3.9%; 25 | --secondary-foreground: 0 0% 9%; 26 | --secondary: 0 0% 96.1%; 27 | --sidebar-accent-foreground: 240 5.9% 10%; 28 | --sidebar-accent: 240 4.8% 95.9%; 29 | --sidebar-background: 0 0% 98%; 30 | --sidebar-border: 220 13% 91%; 31 | --sidebar-foreground: 240 5.3% 26.1%; 32 | --sidebar-primary-foreground: 0 0% 98%; 33 | --sidebar-primary: 240 5.9% 10%; 34 | --sidebar-ring: 217.2 91.2% 59.8%; 35 | } 36 | 37 | .dark { 38 | --accent-foreground: 0 0% 98%; 39 | --accent: 0 0% 14.9%; 40 | --background: 0 0% 3.9%; 41 | --border: 0 0% 14.9%; 42 | --card-foreground: 0 0% 98%; 43 | --card: 0 0% 3.9%; 44 | --destructive-foreground: 0 0% 98%; 45 | --destructive: 0 62.8% 30.6%; 46 | --foreground: 0 0% 98%; 47 | --input: 0 0% 14.9%; 48 | --muted-foreground: 0 0% 63.9%; 49 | --muted: 0 0% 14.9%; 50 | --popover-foreground: 0 0% 98%; 51 | --popover: 0 0% 3.9%; 52 | --primary-foreground: 0 0% 9%; 53 | --primary: 0 0% 98%; 54 | --ring: 0 0% 83.1%; 55 | --secondary-foreground: 0 0% 98%; 56 | --secondary: 0 0% 14.9%; 57 | --sidebar-accent-foreground: 240 4.8% 95.9%; 58 | --sidebar-accent: 240 3.7% 15.9%; 59 | --sidebar-background: 240 5.9% 10%; 60 | --sidebar-border: 240 3.7% 15.9%; 61 | --sidebar-foreground: 240 4.8% 95.9%; 62 | --sidebar-primary-foreground: 0 0% 100%; 63 | --sidebar-primary: 224.3 76.3% 48%; 64 | --sidebar-ring: 217.2 91.2% 59.8%; 65 | } 66 | } 67 | 68 | @layer base { 69 | * { 70 | @apply border-border; 71 | } 72 | body { 73 | @apply bg-background text-foreground; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 88 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPasswordSent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 45 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/TwoFactorChallenge.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 74 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/VerifyEmail.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 66 | -------------------------------------------------------------------------------- /resources/js/Pages/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 24 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Show.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /resources/js/Pages/Security/Show.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /resources/js/Pages/Welcome.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 73 | -------------------------------------------------------------------------------- /resources/js/app.ts: -------------------------------------------------------------------------------- 1 | import '../css/app.css'; 2 | import './bootstrap'; 3 | 4 | import { createInertiaApp } from '@inertiajs/vue3'; 5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 6 | import { createApp, DefineComponent, h } from 'vue'; 7 | import { ZiggyVue } from '../../vendor/tightenco/ziggy'; 8 | 9 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 10 | 11 | createInertiaApp({ 12 | title: (title) => `${title} - ${appName}`, 13 | resolve: (name) => 14 | resolvePageComponent( 15 | `./Pages/${name}.vue`, 16 | import.meta.glob('./Pages/**/*.vue'), 17 | ), 18 | setup({ el, App, props, plugin }) { 19 | createApp({ render: () => h(App, props) }) 20 | .use(plugin) 21 | .use(ZiggyVue) 22 | .mount(el); 23 | }, 24 | progress: { 25 | color: '#4B5563', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /resources/js/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | window.axios = axios; 3 | 4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | -------------------------------------------------------------------------------- /resources/js/components/custom/AppBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 60 | -------------------------------------------------------------------------------- /resources/js/components/custom/AppSidebar.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 106 | -------------------------------------------------------------------------------- /resources/js/components/custom/ErrorFeedback.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /resources/js/components/custom/NavProjectMembers.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 84 | -------------------------------------------------------------------------------- /resources/js/components/custom/NavSecondary.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /resources/js/components/custom/ripple/Ripple.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /resources/js/components/custom/ripple/RippleCircle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /resources/js/components/custom/ripple/RippleContainer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /resources/js/components/custom/ripple/index.ts: -------------------------------------------------------------------------------- 1 | import Ripple from './Ripple.vue'; 2 | import RippleContainer from './RippleContainer.vue'; 3 | 4 | export { Ripple, RippleContainer }; 5 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogAction.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogCancel.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogContent.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogDescription.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/AlertDialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AlertDialog } from './AlertDialog.vue' 2 | export { default as AlertDialogAction } from './AlertDialogAction.vue' 3 | export { default as AlertDialogCancel } from './AlertDialogCancel.vue' 4 | export { default as AlertDialogContent } from './AlertDialogContent.vue' 5 | export { default as AlertDialogDescription } from './AlertDialogDescription.vue' 6 | export { default as AlertDialogFooter } from './AlertDialogFooter.vue' 7 | export { default as AlertDialogHeader } from './AlertDialogHeader.vue' 8 | export { default as AlertDialogTitle } from './AlertDialogTitle.vue' 9 | export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /resources/js/components/ui/avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/avatar/AvatarFallback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/avatar/AvatarImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /resources/js/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Avatar } from './Avatar.vue' 4 | export { default as AvatarFallback } from './AvatarFallback.vue' 5 | export { default as AvatarImage } from './AvatarImage.vue' 6 | 7 | export const avatarVariant = cva( 8 | 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', 9 | { 10 | variants: { 11 | size: { 12 | sm: 'h-10 w-10 text-xs', 13 | base: 'h-16 w-16 text-2xl', 14 | lg: 'h-32 w-32 text-5xl', 15 | }, 16 | shape: { 17 | circle: 'rounded-full', 18 | square: 'rounded-md', 19 | }, 20 | }, 21 | }, 22 | ) 23 | 24 | export type AvatarVariants = VariantProps 25 | -------------------------------------------------------------------------------- /resources/js/components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Badge } from './Badge.vue' 4 | 5 | export const badgeVariants = cva( 6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ) 24 | 25 | export type BadgeVariants = VariantProps 26 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbEllipsis.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/BreadcrumbSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Breadcrumb } from './Breadcrumb.vue' 2 | export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue' 3 | export { default as BreadcrumbItem } from './BreadcrumbItem.vue' 4 | export { default as BreadcrumbLink } from './BreadcrumbLink.vue' 5 | export { default as BreadcrumbList } from './BreadcrumbList.vue' 6 | export { default as BreadcrumbPage } from './BreadcrumbPage.vue' 7 | export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue' 8 | -------------------------------------------------------------------------------- /resources/js/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /resources/js/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Button } from './Button.vue' 4 | 5 | export const buttonVariants = cva( 6 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 12 | destructive: 13 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 14 | outline: 15 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 16 | secondary: 17 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 18 | ghost: 'hover:bg-accent hover:text-accent-foreground', 19 | link: 'text-primary underline-offset-4 hover:underline', 20 | }, 21 | size: { 22 | default: 'h-9 px-4 py-2', 23 | sm: 'h-8 rounded-md px-3 text-xs', 24 | lg: 'h-10 rounded-md px-8', 25 | icon: 'h-9 w-9', 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default', 31 | }, 32 | }, 33 | ) 34 | 35 | export type ButtonVariants = VariantProps 36 | -------------------------------------------------------------------------------- /resources/js/components/ui/collapsible/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /resources/js/components/ui/collapsible/CollapsibleContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/collapsible/CollapsibleTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapsible } from './Collapsible.vue' 2 | export { default as CollapsibleContent } from './CollapsibleContent.vue' 3 | export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue' 4 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/Command.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandEmpty.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandGroup.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandInput.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandSeparator.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/CommandShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/command/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Command } from './Command.vue' 2 | export { default as CommandDialog } from './CommandDialog.vue' 3 | export { default as CommandEmpty } from './CommandEmpty.vue' 4 | export { default as CommandGroup } from './CommandGroup.vue' 5 | export { default as CommandInput } from './CommandInput.vue' 6 | export { default as CommandItem } from './CommandItem.vue' 7 | export { default as CommandList } from './CommandList.vue' 8 | export { default as CommandSeparator } from './CommandSeparator.vue' 9 | export { default as CommandShortcut } from './CommandShortcut.vue' 10 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuContent.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuRadioItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 42 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuSubContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /resources/js/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DropdownMenu } from './DropdownMenu.vue' 2 | 3 | export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' 4 | export { default as DropdownMenuContent } from './DropdownMenuContent.vue' 5 | export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' 6 | export { default as DropdownMenuItem } from './DropdownMenuItem.vue' 7 | export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' 8 | export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' 9 | export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' 10 | export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' 11 | export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' 12 | export { default as DropdownMenuSub } from './DropdownMenuSub.vue' 13 | export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' 14 | export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' 15 | export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' 16 | export { DropdownMenuPortal } from 'radix-vue' 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /resources/js/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /resources/js/components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /resources/js/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /resources/js/components/ui/pin-input/PinInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /resources/js/components/ui/pin-input/PinInputGroup.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /resources/js/components/ui/pin-input/PinInputInput.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | -------------------------------------------------------------------------------- /resources/js/components/ui/pin-input/PinInputSeparator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /resources/js/components/ui/pin-input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PinInput } from './PinInput.vue' 2 | export { default as PinInputGroup } from './PinInputGroup.vue' 3 | export { default as PinInputInput } from './PinInputInput.vue' 4 | export { default as PinInputSeparator } from './PinInputSeparator.vue' 5 | -------------------------------------------------------------------------------- /resources/js/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /resources/js/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/Sheet.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetContent.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 57 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/SheetTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | 3 | export { default as Sheet } from './Sheet.vue' 4 | export { default as SheetClose } from './SheetClose.vue' 5 | export { default as SheetContent } from './SheetContent.vue' 6 | export { default as SheetDescription } from './SheetDescription.vue' 7 | export { default as SheetFooter } from './SheetFooter.vue' 8 | export { default as SheetHeader } from './SheetHeader.vue' 9 | export { default as SheetTitle } from './SheetTitle.vue' 10 | export { default as SheetTrigger } from './SheetTrigger.vue' 11 | 12 | export const sheetVariants = cva( 13 | 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', 14 | { 15 | variants: { 16 | side: { 17 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', 18 | bottom: 19 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', 20 | left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', 21 | right: 22 | 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', 23 | }, 24 | }, 25 | defaultVariants: { 26 | side: 'right', 27 | }, 28 | }, 29 | ) 30 | 31 | export type SheetVariants = VariantProps 32 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 86 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarGroupAction.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarGroupContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarGroupLabel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarInset.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuAction.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuBadge.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuButton.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 50 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuButtonChild.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuSkeleton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuSub.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuSubButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarMenuSubItem.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarProvider.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 81 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarRail.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/SidebarTrigger.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'vue' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | export interface SidebarProps { 5 | side?: 'left' | 'right' 6 | variant?: 'sidebar' | 'floating' | 'inset' 7 | collapsible?: 'offcanvas' | 'icon' | 'none' 8 | class?: HTMLAttributes['class'] 9 | } 10 | 11 | export { default as Sidebar } from './Sidebar.vue' 12 | export { default as SidebarContent } from './SidebarContent.vue' 13 | export { default as SidebarFooter } from './SidebarFooter.vue' 14 | export { default as SidebarGroup } from './SidebarGroup.vue' 15 | export { default as SidebarGroupAction } from './SidebarGroupAction.vue' 16 | export { default as SidebarGroupContent } from './SidebarGroupContent.vue' 17 | export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue' 18 | export { default as SidebarHeader } from './SidebarHeader.vue' 19 | export { default as SidebarInput } from './SidebarInput.vue' 20 | export { default as SidebarInset } from './SidebarInset.vue' 21 | export { default as SidebarMenu } from './SidebarMenu.vue' 22 | export { default as SidebarMenuAction } from './SidebarMenuAction.vue' 23 | export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue' 24 | export { default as SidebarMenuButton } from './SidebarMenuButton.vue' 25 | export { default as SidebarMenuItem } from './SidebarMenuItem.vue' 26 | export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue' 27 | export { default as SidebarMenuSub } from './SidebarMenuSub.vue' 28 | export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue' 29 | export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue' 30 | export { default as SidebarProvider } from './SidebarProvider.vue' 31 | export { default as SidebarRail } from './SidebarRail.vue' 32 | export { default as SidebarSeparator } from './SidebarSeparator.vue' 33 | export { default as SidebarTrigger } from './SidebarTrigger.vue' 34 | 35 | export { useSidebar } from './utils' 36 | 37 | export const sidebarMenuButtonVariants = cva( 38 | 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', 39 | { 40 | variants: { 41 | variant: { 42 | default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', 43 | outline: 44 | 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', 45 | }, 46 | size: { 47 | default: 'h-8 text-sm', 48 | sm: 'h-7 text-xs', 49 | lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', 50 | }, 51 | }, 52 | defaultVariants: { 53 | variant: 'default', 54 | size: 'default', 55 | }, 56 | }, 57 | ) 58 | 59 | export type SidebarMenuButtonVariants = VariantProps 60 | -------------------------------------------------------------------------------- /resources/js/components/ui/sidebar/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref } from 'vue' 2 | import { createContext } from 'radix-vue' 3 | 4 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state' 5 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 6 | export const SIDEBAR_WIDTH = '16rem' 7 | export const SIDEBAR_WIDTH_MOBILE = '18rem' 8 | export const SIDEBAR_WIDTH_ICON = '3rem' 9 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b' 10 | 11 | export const [useSidebar, provideSidebarContext] = createContext<{ 12 | state: ComputedRef<'expanded' | 'collapsed'> 13 | open: Ref 14 | setOpen: (value: boolean) => void 15 | isMobile: Ref 16 | openMobile: Ref 17 | setOpenMobile: (value: boolean) => void 18 | toggleSidebar: () => void 19 | }>('Sidebar') 20 | -------------------------------------------------------------------------------- /resources/js/components/ui/skeleton/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeleton } from './Skeleton.vue' 2 | -------------------------------------------------------------------------------- /resources/js/components/ui/sonner/Sonner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /resources/js/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './Sonner.vue' 2 | -------------------------------------------------------------------------------- /resources/js/components/ui/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/components/ui/tooltip/TooltipContent.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /resources/js/components/ui/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /resources/js/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue' 2 | export { default as TooltipContent } from './TooltipContent.vue' 3 | export { default as TooltipProvider } from './TooltipProvider.vue' 4 | export { default as TooltipTrigger } from './TooltipTrigger.vue' 5 | -------------------------------------------------------------------------------- /resources/js/layouts/AuthenticatedLayout.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /resources/js/layouts/AuthenticationLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /resources/js/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /resources/js/ssr.ts: -------------------------------------------------------------------------------- 1 | import { createInertiaApp } from '@inertiajs/vue3'; 2 | import createServer from '@inertiajs/vue3/server'; 3 | import { renderToString } from '@vue/server-renderer'; 4 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 5 | import { createSSRApp, DefineComponent, h } from 'vue'; 6 | import { ZiggyVue } from '../../vendor/tightenco/ziggy'; 7 | 8 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 9 | 10 | createServer((page) => 11 | createInertiaApp({ 12 | page, 13 | render: renderToString, 14 | title: (title) => `${title} - ${appName}`, 15 | resolve: (name) => 16 | resolvePageComponent( 17 | `./Pages/${name}.vue`, 18 | import.meta.glob('./Pages/**/*.vue'), 19 | ), 20 | setup({ App, props, plugin }) { 21 | return createSSRApp({ render: () => h(App, props) }) 22 | .use(plugin) 23 | .use(ZiggyVue, { 24 | ...page.props.ziggy, 25 | location: new URL(page.props.ziggy.location), 26 | }); 27 | }, 28 | }), 29 | ); 30 | -------------------------------------------------------------------------------- /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 { PageProps as AppPageProps } from './'; 5 | 6 | declare global { 7 | interface Window { 8 | axios: AxiosInstance; 9 | } 10 | 11 | /* eslint-disable no-var */ 12 | var route: typeof ziggyRoute; 13 | } 14 | 15 | declare module 'vue' { 16 | interface ComponentCustomProperties { 17 | route: typeof ziggyRoute; 18 | } 19 | } 20 | 21 | declare module '@inertiajs/core' { 22 | interface PageProps extends InertiaPageProps, AppPageProps {} 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'ziggy-js'; 2 | 3 | export interface User { 4 | id: number; 5 | name: string; 6 | email: string; 7 | email_verified_at?: string; 8 | profile_photo_url: string | undefined; 9 | two_factor_confirmed_at: string | null; 10 | } 11 | 12 | export type PageProps< 13 | T extends Record = Record, 14 | > = T & { 15 | auth: { 16 | user: User; 17 | }; 18 | ziggy: Config & { location: string }; 19 | }; 20 | -------------------------------------------------------------------------------- /resources/js/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @routes 17 | @vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"]) 18 | @inertiaHead 19 | 20 | 21 | 22 | @inertia 23 | 24 | 25 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 10 | Route::get('/register', function () { 11 | return Inertia::render('Auth/Register', [ 12 | 'isRegisterEnabled' => Features::enabled(Features::registration()), 13 | ]); 14 | })->name('register'); 15 | 16 | Route::get('/login', function () { 17 | return Inertia::render('Auth/Login', [ 18 | 'isRegisterEnabled' => Features::enabled(Features::registration()), 19 | ]); 20 | })->name('login'); 21 | 22 | Route::get('/login/challenge', function () { 23 | return Inertia::render('Auth/TwoFactorChallenge'); 24 | })->name('two-factor.login'); 25 | 26 | Route::get('/forgot-password', function () { 27 | return Inertia::render('Auth/ForgotPassword'); 28 | })->name('auth.forgot-password'); 29 | 30 | Route::get('/reset-password/{token}', function (string $token) { 31 | return Inertia::render('Auth/PasswordReset', [ 32 | 'token' => $token, 33 | 'email' => Request::input('email'), 34 | ]); 35 | })->name('password.reset'); 36 | 37 | Route::get('/forgot-password/sent', function () { 38 | return Inertia::render('Auth/ForgotPasswordSent'); 39 | })->name('forgot-password.sent'); 40 | }); 41 | 42 | Route::middleware('auth')->group(function () { 43 | Route::post('/confirm-password', [ConfirmablePasswordController::class, 'store'])->name('password.confirm'); 44 | 45 | Route::get('/verify-email', function () { 46 | return Inertia::render('Auth/VerifyEmail'); 47 | })->name('verification.notice'); 48 | }); 49 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | Route::has('login'), 12 | 'canRegister' => Route::has('register'), 13 | 'laravelVersion' => Application::VERSION, 14 | 'phpVersion' => PHP_VERSION, 15 | ]); 16 | }); 17 | 18 | Route::middleware('auth', 'verified')->group(function () { 19 | // Dashboard 20 | Route::get('/dashboard', function () { 21 | return Inertia::render('Dashboard'); 22 | })->name('dashboard'); 23 | 24 | // Profile 25 | Route::get('/account/profile', [ProfileController::class, 'show'])->name('profile.show'); 26 | Route::patch('/account/profile', [ProfileController::class, 'update'])->name('profile.update'); 27 | Route::delete('/account/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 28 | 29 | // Security 30 | Route::get('/account/security', [SecurityController::class, 'show'])->name('security.show'); 31 | }); 32 | 33 | require __DIR__.'/auth.php'; 34 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users can not authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | 34 | test('users can logout', function () { 35 | $user = User::factory()->create(); 36 | 37 | $response = $this->actingAs($user)->post('/logout'); 38 | 39 | $this->assertGuest(); 40 | $response->assertRedirect('/'); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/verify-email'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('email can be verified', function () { 17 | $user = User::factory()->unverified()->create(); 18 | 19 | Event::fake(); 20 | 21 | $verificationUrl = URL::temporarySignedRoute( 22 | 'verification.verify', 23 | now()->addMinutes(60), 24 | ['id' => $user->id, 'hash' => sha1($user->email)] 25 | ); 26 | 27 | $response = $this->actingAs($user)->get($verificationUrl); 28 | 29 | Event::assertDispatched(Verified::class); 30 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 31 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 32 | }); 33 | 34 | test('email is not verified with invalid hash', function () { 35 | $user = User::factory()->unverified()->create(); 36 | 37 | $verificationUrl = URL::temporarySignedRoute( 38 | 'verification.verify', 39 | now()->addMinutes(60), 40 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 41 | ); 42 | 43 | $this->actingAs($user)->get($verificationUrl); 44 | 45 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 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, function ($notification) { 31 | $response = $this->get('/reset-password/'.$notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response 55 | ->assertSessionHasNoErrors() 56 | ->assertRedirect(route('dashboard')); 57 | 58 | return true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('user cannot update password with incorrect current password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrors('current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | 42 | test('new password must be different from current password', function () { 43 | $user = User::factory()->create(); 44 | 45 | $response = $this 46 | ->actingAs($user) 47 | ->from('/profile') 48 | ->put('/password', [ 49 | 'current_password' => 'password', 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response 55 | ->assertSessionHasErrors('password') 56 | ->assertRedirect('/profile'); 57 | }); 58 | 59 | test('new password must be confirmed', function () { 60 | $user = User::factory()->create(); 61 | 62 | $response = $this 63 | ->actingAs($user) 64 | ->from('/profile') 65 | ->put('/password', [ 66 | 'current_password' => 'password', 67 | 'password' => 'new-password', 68 | 'password_confirmation' => 'different-password', 69 | ]); 70 | 71 | $response 72 | ->assertSessionHasErrors('password') 73 | ->assertRedirect('/profile'); 74 | }); 75 | 76 | test('new password must be at least 8 characters', function () { 77 | $user = User::factory()->create(); 78 | 79 | $response = $this 80 | ->actingAs($user) 81 | ->from('/profile') 82 | ->put('/password', [ 83 | 'current_password' => 'password', 84 | 'password' => 'short', 85 | 'password_confirmation' => 'short', 86 | ]); 87 | 88 | $response 89 | ->assertSessionHasErrors('password') 90 | ->assertRedirect('/profile'); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | 9 | test('new users can register', function () { 10 | $response = $this->post('/register', [ 11 | 'name' => 'Test User', 12 | 'email' => 'test@example.com', 13 | 'password' => 'password', 14 | 'password_confirmation' => 'password', 15 | ]); 16 | 17 | $this->assertAuthenticated(); 18 | $response->assertRedirect(route('dashboard', absolute: false)); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve", 7 | "strict": true, 8 | "isolatedModules": true, 9 | "target": "ESNext", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "skipLibCheck": true, 14 | "paths": { 15 | "@/*": ["./resources/js/*"], 16 | "ziggy-js": ["./vendor/tightenco/ziggy"] 17 | } 18 | }, 19 | "include": ["resources/js/**/*.ts", "resources/js/**/*.vue"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import laravel from 'laravel-vite-plugin'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | vue(), 8 | laravel({ 9 | input: 'resources/js/app.ts', 10 | ssr: 'resources/js/ssr.ts', 11 | refresh: true, 12 | }), 13 | ], 14 | }); 15 | --------------------------------------------------------------------------------