├── LICENSE.md ├── README.md ├── composer.json ├── src ├── Console │ └── InstallCommand.php └── SugarServiceProvider.php └── stubs ├── app └── Http │ ├── Controllers │ └── Auth │ │ ├── AuthenticatedSessionController.php │ │ ├── ConfirmablePasswordController.php │ │ ├── EmailVerificationNotificationController.php │ │ ├── EmailVerificationPromptController.php │ │ ├── NewPasswordController.php │ │ ├── PasswordResetLinkController.php │ │ ├── RegisteredUserController.php │ │ └── VerifyEmailController.php │ ├── Middleware │ └── HandleInertiaRequests.php │ └── Requests │ └── Auth │ └── LoginRequest.php ├── config └── vite.php ├── resources ├── css │ └── app.css ├── js │ ├── Components │ │ ├── Breeze.ts │ │ └── Breeze │ │ │ ├── ApplicationLogo.vue │ │ │ ├── Button.vue │ │ │ ├── Checkbox.vue │ │ │ ├── Dropdown.vue │ │ │ ├── DropdownLink.vue │ │ │ ├── Input.vue │ │ │ ├── InputError.vue │ │ │ ├── Label.vue │ │ │ ├── NavLink.vue │ │ │ ├── ResponsiveNavLink.vue │ │ │ └── ValidationErrors.vue │ ├── Hooks │ │ ├── useRoute.ts │ │ └── useUser.ts │ ├── Layouts │ │ ├── Authenticated.vue │ │ └── Guest.vue │ ├── Pages │ │ ├── Auth │ │ │ ├── ConfirmPassword.vue │ │ │ ├── ForgotPassword.vue │ │ │ ├── Login.vue │ │ │ ├── Register.vue │ │ │ ├── ResetPassword.vue │ │ │ └── VerifyEmail.vue │ │ ├── Dashboard.vue │ │ └── Welcome.vue │ ├── app.ts │ ├── global.d.ts │ ├── models.d.ts │ └── shims-vue.d.ts └── views │ └── app.blade.php ├── routes ├── auth.php └── web.php ├── tailwind.config.js ├── tests ├── Feature │ ├── AuthenticationTest.php │ ├── EmailVerificationTest.php │ ├── ExampleTest.php │ ├── PasswordConfirmationTest.php │ ├── PasswordResetTest.php │ └── RegistrationTest.php ├── Pest.php └── Unit │ └── ExampleTest.php ├── tsconfig.json └── vite.config.ts /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Boris Lepikhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sugar 2 | 3 | Sugar provides a supercharged starting point for Laravel applications. The package is built on top of the official [Laravel Breeze](https://github.com/laravel/breeze), and includes: 4 | 5 | - Vite (instead of Webpack + Mix) 6 | - Vue 3 (modern SFC setup script syntax) 7 | - TypeScript 8 | - Tailwind CSS 9 | - Inertia.js 10 | 11 | ## Installation 12 | 13 | You can install the package via composer: 14 | 15 | ```bash 16 | composer require based/sugar --dev 17 | ``` 18 | 19 | Then, publish the assets provided by Sugar, and compile them: 20 | 21 | ```bash 22 | php artisan sugar:install 23 | 24 | npm install 25 | npm run dev 26 | ``` 27 | 28 | > Be careful installing Sugar on existing projects, as it completely removes `app.js` 29 | 30 | ## Inertia.js 31 | 32 | The package comes with Inertia.js and includes components from Laravel Breeze, optimized for a better experience with Vue 3 and TypeScript. 33 | 34 | ```vue 35 | 58 | ``` 59 | 60 | ## Vite 61 | 62 | [Vite](https://vitejs.dev/) is a build tool that aims to provide a faster and leaner development experience for modern web projects. Read [Why Vite?](https://vitejs.dev/guide/why.html) for more details. 63 | 64 | The support is provided by [Laravel Vite](https://laravel-vite.innocenzi.dev/) package. 65 | 66 | ## TypeScript 67 | 68 | TypeScript provides optional static typing, which lets you structure and validate your code at the compilation stage. It also brings the IDE autocompletion and validation support along with the code navigation feature. 69 | 70 | Reimagined Breeze components utilize TypeScript. However, you're free to use the familiar syntax. 71 | 72 | ## License 73 | 74 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "based/sugar", 3 | "description": "Minimal Laravel authentication scaffolding with Vue, Vite, Typescript and Tailwind.", 4 | "keywords": [ 5 | "laravel", 6 | "auth", 7 | "breeze", 8 | "typescript", 9 | "tailwind", 10 | "vite" 11 | ], 12 | "license": "MIT", 13 | "support": { 14 | "issues": "https://github.com/lepikhinb/sugar/issues", 15 | "source": "https://github.com/lepikhinb/sugar" 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Boris Lepikhin", 20 | "email": "boris@lepikhin.com" 21 | } 22 | ], 23 | "require": { 24 | "php": "^7.3|^8.0", 25 | "illuminate/filesystem": "^8.42|^9.0", 26 | "illuminate/support": "^8.42|^9.0", 27 | "illuminate/validation": "^8.42|^9.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Based\\Sugar\\": "src/" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "1.x-dev" 37 | }, 38 | "laravel": { 39 | "providers": [ 40 | "Based\\Sugar\\SugarServiceProvider" 41 | ] 42 | } 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "minimum-stability": "dev", 48 | "prefer-stable": true 49 | } 50 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | installInertiaVueViteStack(); 35 | } 36 | 37 | /** 38 | * Install Breeze's tests. 39 | * 40 | * @return void 41 | */ 42 | protected function installTests() 43 | { 44 | $this->requireComposerPackages('pestphp/pest:^1.16', 'pestphp/pest-plugin-laravel:^1.1'); 45 | 46 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/tests/Feature', base_path('tests/Feature')); 47 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/tests/Unit', base_path('tests/Unit')); 48 | (new Filesystem)->copy(__DIR__ . '/../../stubs/tests/Pest.php', base_path('tests/Pest.php')); 49 | } 50 | 51 | /** 52 | * Install the Inertia Vue Vite Breeze stack. 53 | * 54 | * @return void 55 | */ 56 | protected function installInertiaVueViteStack() 57 | { 58 | // Install Inertia... 59 | 60 | $this->requireComposerPackages([ 61 | 'based/laravel-typescript:^0.0.1', 62 | 'inertiajs/inertia-laravel:^0.4.3', 63 | 'innocenzi/laravel-vite:^0.1.10', 64 | 'laravel/sanctum:^2.6', 65 | 'tightenco/ziggy:^1.0' 66 | ]); 67 | 68 | // NPM Packages... 69 | $this->updateNodePackages(function ($packages) { 70 | return [ 71 | '@inertiajs/inertia' => '^0.10.0', 72 | '@inertiajs/inertia-vue3' => '^0.5.1', 73 | '@inertiajs/progress' => '^0.2.6', 74 | '@tailwindcss/forms' => '^0.2.1', 75 | '@types/ziggy-js' => '^1.3.0', 76 | '@vitejs/plugin-vue' => '^1.6.0', 77 | '@vue/compiler-sfc' => '^3.0.5', 78 | 'autoprefixer' => '^10.2.4', 79 | 'laravel-vite' => '^0.0.16', 80 | 'postcss' => '^8.2.13', 81 | 'postcss-import' => '^14.0.1', 82 | 'tailwindcss' => '^2.1.2', 83 | 'vite' => '^2.5.1', 84 | 'vue' => '^3.2.6', 85 | ] + $packages; 86 | }); 87 | 88 | $this->updateNodeScripts(); 89 | 90 | // Controllers... 91 | (new Filesystem)->ensureDirectoryExists(app_path('Http/Controllers/Auth')); 92 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/app/Http/Controllers/Auth', app_path('Http/Controllers/Auth')); 93 | 94 | // Requests... 95 | (new Filesystem)->ensureDirectoryExists(app_path('Http/Requests/Auth')); 96 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/App/Http/Requests/Auth', app_path('Http/Requests/Auth')); 97 | 98 | // Middleware... 99 | $this->installMiddlewareAfter('SubstituteBindings::class', '\App\Http\Middleware\HandleInertiaRequests::class'); 100 | 101 | copy(__DIR__ . '/../../stubs/app/Http/Middleware/HandleInertiaRequests.php', app_path('Http/Middleware/HandleInertiaRequests.php')); 102 | 103 | // Views... 104 | copy(__DIR__ . '/../../stubs/resources/views/app.blade.php', resource_path('views/app.blade.php')); 105 | 106 | // Components + Pages... 107 | (new Filesystem)->ensureDirectoryExists(resource_path('js/Components')); 108 | (new Filesystem)->ensureDirectoryExists(resource_path('js/Layouts')); 109 | (new Filesystem)->ensureDirectoryExists(resource_path('js/Pages')); 110 | (new Filesystem)->ensureDirectoryExists(resource_path('js/Hooks')); 111 | 112 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/resources/js/Components', resource_path('js/Components')); 113 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/resources/js/Layouts', resource_path('js/Layouts')); 114 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/resources/js/Pages', resource_path('js/Pages')); 115 | (new Filesystem)->copyDirectory(__DIR__ . '/../../stubs/resources/js/Hooks', resource_path('js/Hooks')); 116 | 117 | // Tests... 118 | $this->installTests(); 119 | 120 | // Vite config... 121 | copy(__DIR__ . '/../../stubs/config/vite.php', base_path('config/vite.php')); 122 | 123 | // Routes... 124 | copy(__DIR__ . '/../../stubs/routes/web.php', base_path('routes/web.php')); 125 | copy(__DIR__ . '/../../stubs/routes/auth.php', base_path('routes/auth.php')); 126 | 127 | // "Dashboard" Route... 128 | $this->replaceInFile('/home', '/dashboard', resource_path('js/Pages/Welcome.vue')); 129 | $this->replaceInFile('Home', 'Dashboard', resource_path('js/Pages/Welcome.vue')); 130 | $this->replaceInFile('/home', '/dashboard', app_path('Providers/RouteServiceProvider.php')); 131 | 132 | // Tailwind / Vite... 133 | tap(new Filesystem, function ($files) { 134 | $files->delete(resource_path('js/app.js')); 135 | $files->delete(resource_path('js/bootstrap.js')); 136 | }); 137 | 138 | copy(__DIR__ . '/../../stubs/tailwind.config.js', base_path('tailwind.config.js')); 139 | copy(__DIR__ . '/../../stubs/vite.config.ts', base_path('vite.config.ts')); 140 | copy(__DIR__ . '/../../stubs/tsconfig.json', base_path('tsconfig.json')); 141 | copy(__DIR__ . '/../../stubs/resources/css/app.css', resource_path('css/app.css')); 142 | copy(__DIR__ . '/../../stubs/resources/js/app.ts', resource_path('js/app.ts')); 143 | copy(__DIR__ . '/../../stubs/resources/js/global.d.ts', resource_path('js/global.d.ts')); 144 | copy(__DIR__ . '/../../stubs/resources/js/models.d.ts', resource_path('js/models.d.ts')); 145 | copy(__DIR__ . '/../../stubs/resources/js/shims-vue.d.ts', resource_path('js/shims-vue.d.ts')); 146 | 147 | $this->info('Sugar scaffolding installed successfully.'); 148 | $this->comment('Please execute the "npm install && npm run dev" command to build your assets.'); 149 | } 150 | 151 | /** 152 | * Install the middleware to a group in the application Http Kernel. 153 | * 154 | * @param string $after 155 | * @param string $name 156 | * @param string $group 157 | * @return void 158 | */ 159 | protected function installMiddlewareAfter($after, $name, $group = 'web') 160 | { 161 | $httpKernel = file_get_contents(app_path('Http/Kernel.php')); 162 | 163 | $middlewareGroups = Str::before(Str::after($httpKernel, '$middlewareGroups = ['), '];'); 164 | $middlewareGroup = Str::before(Str::after($middlewareGroups, "'$group' => ["), '],'); 165 | 166 | if (!Str::contains($middlewareGroup, $name)) { 167 | $modifiedMiddlewareGroup = str_replace( 168 | $after . ',', 169 | $after . ',' . PHP_EOL . ' ' . $name . ',', 170 | $middlewareGroup, 171 | ); 172 | 173 | file_put_contents(app_path('Http/Kernel.php'), str_replace( 174 | $middlewareGroups, 175 | str_replace($middlewareGroup, $modifiedMiddlewareGroup, $middlewareGroups), 176 | $httpKernel 177 | )); 178 | } 179 | } 180 | 181 | /** 182 | * Installs the given Composer Packages into the application. 183 | * 184 | * @param mixed $packages 185 | * @return void 186 | */ 187 | protected function requireComposerPackages($packages) 188 | { 189 | $composer = $this->option('composer'); 190 | 191 | if ($composer !== 'global') { 192 | $command = ['php', $composer, 'require']; 193 | } 194 | 195 | $command = array_merge( 196 | $command ?? ['composer', 'require'], 197 | is_array($packages) ? $packages : func_get_args() 198 | ); 199 | 200 | (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) 201 | ->setTimeout(null) 202 | ->run(function ($type, $output) { 203 | $this->output->write($output); 204 | }); 205 | } 206 | 207 | /** 208 | * Update the "package.json" file. 209 | * 210 | * @param callable $callback 211 | * @param bool $dev 212 | * @return void 213 | */ 214 | protected static function updateNodePackages(callable $callback, $dev = true) 215 | { 216 | if (!file_exists(base_path('package.json'))) { 217 | return; 218 | } 219 | 220 | $configurationKey = $dev ? 'devDependencies' : 'dependencies'; 221 | 222 | $packages = json_decode(file_get_contents(base_path('package.json')), true); 223 | 224 | $packages[$configurationKey] = $callback( 225 | array_key_exists($configurationKey, $packages) ? $packages[$configurationKey] : [], 226 | $configurationKey 227 | ); 228 | 229 | ksort($packages[$configurationKey]); 230 | 231 | file_put_contents( 232 | base_path('package.json'), 233 | json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL 234 | ); 235 | } 236 | 237 | 238 | /** 239 | * Add building scripts to the "package.json" file 240 | * 241 | * @return void 242 | */ 243 | protected static function updateNodeScripts() 244 | { 245 | if (!file_exists(base_path('package.json'))) { 246 | return; 247 | } 248 | 249 | $packages = json_decode(file_get_contents(base_path('package.json')), true); 250 | 251 | $packages['scripts'] = [ 252 | 'dev' => 'vite', 253 | 'build' => 'vite build', 254 | 'serve' => 'vite preview', 255 | ]; 256 | 257 | file_put_contents( 258 | base_path('package.json'), 259 | json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL 260 | ); 261 | } 262 | 263 | /** 264 | * Delete the "node_modules" directory and remove the associated lock files. 265 | * 266 | * @return void 267 | */ 268 | protected static function flushNodeModules() 269 | { 270 | tap(new Filesystem, function ($files) { 271 | $files->deleteDirectory(base_path('node_modules')); 272 | 273 | $files->delete(base_path('yarn.lock')); 274 | $files->delete(base_path('package-lock.json')); 275 | }); 276 | } 277 | 278 | /** 279 | * Replace a given string within a given file. 280 | * 281 | * @param string $search 282 | * @param string $replace 283 | * @param string $path 284 | * @return void 285 | */ 286 | protected function replaceInFile($search, $replace, $path) 287 | { 288 | file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/SugarServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 28 | return; 29 | } 30 | 31 | $this->commands([ 32 | Console\InstallCommand::class, 33 | ]); 34 | } 35 | 36 | /** 37 | * Get the services provided by the provider. 38 | * 39 | * @return array 40 | */ 41 | public function provides() 42 | { 43 | return [Console\InstallCommand::class]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | Route::has('password.request'), 24 | 'status' => session('status'), 25 | ]); 26 | } 27 | 28 | /** 29 | * Handle an incoming authentication request. 30 | * 31 | * @param \App\Http\Requests\Auth\LoginRequest $request 32 | * @return \Illuminate\Http\RedirectResponse 33 | */ 34 | public function store(LoginRequest $request) 35 | { 36 | $request->authenticate(); 37 | 38 | $request->session()->regenerate(); 39 | 40 | return redirect()->intended(RouteServiceProvider::HOME); 41 | } 42 | 43 | /** 44 | * Destroy an authenticated session. 45 | * 46 | * @param \Illuminate\Http\Request $request 47 | * @return \Illuminate\Http\RedirectResponse 48 | */ 49 | public function destroy(Request $request) 50 | { 51 | Auth::guard('web')->logout(); 52 | 53 | $request->session()->invalidate(); 54 | 55 | $request->session()->regenerateToken(); 56 | 57 | return redirect('/'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 33 | 'email' => $request->user()->email, 34 | 'password' => $request->password, 35 | ])) { 36 | throw ValidationException::withMessages([ 37 | 'password' => __('auth.password'), 38 | ]); 39 | } 40 | 41 | $request->session()->put('auth.password_confirmed_at', time()); 42 | 43 | return redirect()->intended(RouteServiceProvider::HOME); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 20 | return redirect()->intended(RouteServiceProvider::HOME); 21 | } 22 | 23 | $request->user()->sendEmailVerificationNotification(); 24 | 25 | return back()->with('status', 'verification-link-sent'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 21 | ? redirect()->intended(RouteServiceProvider::HOME) 22 | : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request->email, 27 | 'token' => $request->route('token'), 28 | ]); 29 | } 30 | 31 | /** 32 | * Handle an incoming new password request. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @return \Illuminate\Http\RedirectResponse 36 | * 37 | * @throws \Illuminate\Validation\ValidationException 38 | */ 39 | public function store(Request $request) 40 | { 41 | $request->validate([ 42 | 'token' => 'required', 43 | 'email' => 'required|email', 44 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 45 | ]); 46 | 47 | // Here we will attempt to reset the user's password. If it is successful we 48 | // will update the password on an actual user model and persist it to the 49 | // database. Otherwise we will parse the error and return the response. 50 | $status = Password::reset( 51 | $request->only('email', 'password', 'password_confirmation', 'token'), 52 | function ($user) use ($request) { 53 | $user->forceFill([ 54 | 'password' => Hash::make($request->password), 55 | 'remember_token' => Str::random(60), 56 | ])->save(); 57 | 58 | event(new PasswordReset($user)); 59 | } 60 | ); 61 | 62 | // If the password was successfully reset, we will redirect the user back to 63 | // the application's home authenticated view. If there is an error we can 64 | // redirect them back to where they came from with their error message. 65 | if ($status == Password::PASSWORD_RESET) { 66 | return redirect()->route('login')->with('status', __($status)); 67 | } 68 | 69 | throw ValidationException::withMessages([ 70 | 'email' => [trans($status)], 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | session('status'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Handle an incoming password reset link request. 27 | * 28 | * @param \Illuminate\Http\Request $request 29 | * @return \Illuminate\Http\RedirectResponse 30 | * 31 | * @throws \Illuminate\Validation\ValidationException 32 | */ 33 | public function store(Request $request) 34 | { 35 | $request->validate([ 36 | 'email' => 'required|email', 37 | ]); 38 | 39 | // We will send the password reset link to this user. Once we have attempted 40 | // to send the link, we will examine the response then see the message we 41 | // need to show to the user. Finally, we'll send out a proper response. 42 | $status = Password::sendResetLink( 43 | $request->only('email') 44 | ); 45 | 46 | if ($status == Password::RESET_LINK_SENT) { 47 | return back()->with('status', __($status)); 48 | } 49 | 50 | throw ValidationException::withMessages([ 51 | 'email' => [trans($status)], 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 38 | 'name' => 'required|string|max:255', 39 | 'email' => 'required|string|email|max:255|unique:users', 40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 41 | ]); 42 | 43 | $user = User::create([ 44 | 'name' => $request->name, 45 | 'email' => $request->email, 46 | 'password' => Hash::make($request->password), 47 | ]); 48 | 49 | event(new Registered($user)); 50 | 51 | Auth::login($user); 52 | 53 | return redirect(RouteServiceProvider::HOME); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /stubs/app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 21 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 22 | } 23 | 24 | if ($request->user()->markEmailAsVerified()) { 25 | event(new Verified($request->user())); 26 | } 27 | 28 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'user' => $request->user(), 39 | ], 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /stubs/app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email'], 33 | 'password' => ['required', 'string'], 34 | ]; 35 | } 36 | 37 | /** 38 | * Attempt to authenticate the request's credentials. 39 | * 40 | * @return void 41 | * 42 | * @throws \Illuminate\Validation\ValidationException 43 | */ 44 | public function authenticate() 45 | { 46 | $this->ensureIsNotRateLimited(); 47 | 48 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 49 | RateLimiter::hit($this->throttleKey()); 50 | 51 | throw ValidationException::withMessages([ 52 | 'email' => __('auth.failed'), 53 | ]); 54 | } 55 | 56 | RateLimiter::clear($this->throttleKey()); 57 | } 58 | 59 | /** 60 | * Ensure the login request is not rate limited. 61 | * 62 | * @return void 63 | * 64 | * @throws \Illuminate\Validation\ValidationException 65 | */ 66 | public function ensureIsNotRateLimited() 67 | { 68 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 69 | return; 70 | } 71 | 72 | event(new Lockout($this)); 73 | 74 | $seconds = RateLimiter::availableIn($this->throttleKey()); 75 | 76 | throw ValidationException::withMessages([ 77 | 'email' => trans('auth.throttle', [ 78 | 'seconds' => $seconds, 79 | 'minutes' => ceil($seconds / 60), 80 | ]), 81 | ]); 82 | } 83 | 84 | /** 85 | * Get the rate limiting throttle key for the request. 86 | * 87 | * @return string 88 | */ 89 | public function throttleKey() 90 | { 91 | return Str::lower($this->input('email')).'|'.$this->ip(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /stubs/config/vite.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'resources/js', 14 | 'resources/scripts', 15 | ], 16 | 'ignore_patterns' => ['/\\.d\\.ts$/'], 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Aliases 21 | |-------------------------------------------------------------------------- 22 | | These aliases will be added to the Vite configuration and used 23 | | to generate a proper tsconfig.json file. 24 | */ 25 | 'aliases' => [ 26 | '@' => 'resources/js', 27 | ], 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Static assets path 32 | |-------------------------------------------------------------------------- 33 | | This option defines the directory that Vite considers as the 34 | | public directory. Its content will be copied to the build directory 35 | | at build-time. 36 | | https://vitejs.dev/config/#publicdir 37 | */ 38 | 'public_directory' => resource_path('static'), 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Ping timeout 43 | |-------------------------------------------------------------------------- 44 | | The maximum duration, in seconds, that the ping to the development 45 | | server should take while trying to determine whether to use the 46 | | manifest or the server in a local environment. Using false will disable 47 | | the feature. 48 | | https://laravel-vite.innocenzi.dev/guide/configuration.html#ping-timeout 49 | */ 50 | 'ping_timeout' => .1, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Build path 55 | |-------------------------------------------------------------------------- 56 | | The directory, relative to /public, in which Vite will build 57 | | the production files. This should match "build.outDir" in the Vite 58 | | configuration file. 59 | */ 60 | 'build_path' => 'build', 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Development URL 65 | |-------------------------------------------------------------------------- 66 | | The URL at which the Vite development server runs. 67 | | This is used to generate the script tags when developing. 68 | */ 69 | 'dev_url' => 'http://localhost:3000', 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Inject asset-fixing plugin 74 | |-------------------------------------------------------------------------- 75 | | Currently, Vite does not support loading assets from an URL other than 76 | | the development server's URL. If this option is enabled, a plugin fixing 77 | | this issue will be injected. 78 | | See: https://github.com/innocenzi/laravel-vite/issues/31 79 | */ 80 | 'asset_plugin' => [ 81 | 'find_regex' => '/\/resources\/(.*)\.(svg|jp?g|png|webp)/', 82 | 'replace_with' => '/resources/$1.$2', 83 | ], 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Commands 88 | |-------------------------------------------------------------------------- 89 | | Defines the list of artisan commands that will be executed when 90 | | the development server starts. 91 | */ 92 | 'commands' => [ 93 | 'vite:aliases', 94 | 'typescript:generate' 95 | ], 96 | ]; 97 | -------------------------------------------------------------------------------- /stubs/resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze.ts: -------------------------------------------------------------------------------- 1 | export { default as ApplicationLogo } from "./Breeze/ApplicationLogo.vue" 2 | export { default as Button } from "./Breeze/Button.vue" 3 | export { default as Checkbox } from "./Breeze/Checkbox.vue" 4 | export { default as Dropdown } from "./Breeze/Dropdown.vue" 5 | export { default as DropdownLink } from "./Breeze/DropdownLink.vue" 6 | export { default as Input } from "./Breeze/Input.vue" 7 | export { default as InputError } from "./Breeze/InputError.vue" 8 | export { default as Label } from "./Breeze/Label.vue" 9 | export { default as NavLink } from "./Breeze/NavLink.vue" 10 | export { default as ResponsiveNavLink } from "./Breeze/ResponsiveNavLink.vue" 11 | export { default as ValidationErrors } from "./Breeze/ValidationErrors.vue" 12 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/ApplicationLogo.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/Button.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/DropdownLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/Input.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/InputError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/Label.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/NavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/ResponsiveNavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /stubs/resources/js/Components/Breeze/ValidationErrors.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /stubs/resources/js/Hooks/useRoute.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "ziggy-js" 2 | 3 | export default () => { 4 | return ( 5 | name?: string, 6 | params?: Array | object, 7 | absolute?: boolean 8 | ): Router & string => { 9 | return window.route(name, params, absolute) 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /stubs/resources/js/Hooks/useUser.ts: -------------------------------------------------------------------------------- 1 | import { usePage } from "@inertiajs/inertia-vue3" 2 | import { computed } from "@vue/reactivity" 3 | 4 | export default () => { 5 | return computed(() => usePage().props.value.auth.user) 6 | } 7 | -------------------------------------------------------------------------------- /stubs/resources/js/Layouts/Authenticated.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /stubs/resources/js/Layouts/Guest.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/ConfirmPassword.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/Login.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 83 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/Register.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 81 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Auth/VerifyEmail.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /stubs/resources/js/Pages/Welcome.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 251 | 252 | -------------------------------------------------------------------------------- /stubs/resources/js/app.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "vue" 2 | import { createInertiaApp } from "@inertiajs/inertia-vue3" 3 | import { InertiaProgress } from "@inertiajs/progress" 4 | import "../css/app.css" 5 | 6 | const appName = window.document.getElementsByTagName("title")[0]?.innerText || "Laravel" 7 | const pages = import.meta.glob('./Pages/**/*.vue') 8 | 9 | createInertiaApp({ 10 | title: (title) => `${title} - ${appName}`, 11 | resolve: async (name) => { 12 | if (import.meta.env.DEV) { 13 | return (await import(`./Pages/${name}.vue`)).default 14 | } else { 15 | const importPage = pages[`./Pages/${name}.vue`] 16 | return importPage().then(module => module.default) 17 | } 18 | }, 19 | // @ts-ignore 20 | setup({ el, app, props, plugin }) { 21 | return createApp({ render: () => h(app, props) }) 22 | .use(plugin) 23 | .mount(el) 24 | }, 25 | }) 26 | 27 | InertiaProgress.init({ color: "#4B5563" }) 28 | -------------------------------------------------------------------------------- /stubs/resources/js/global.d.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | 3 | declare global { 4 | interface Window { 5 | route: Function 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stubs/resources/js/models.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace App.Models { 2 | export interface User { 3 | id: number; 4 | name: string; 5 | email: string; 6 | email_verified_at: string | null; 7 | password: string; 8 | remember_token: string | null; 9 | created_at: string | null; 10 | updated_at: string | null; 11 | } 12 | } -------------------------------------------------------------------------------- /stubs/resources/js/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // This is required for Visual Studio Code to recognize 2 | // imported .vue files 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } -------------------------------------------------------------------------------- /stubs/resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'Laravel') }} 10 | 11 | 12 | 13 | 14 | 15 | @routes 16 | @vite 17 | 18 | 19 | 20 | @inertia 21 | 22 | 23 | -------------------------------------------------------------------------------- /stubs/routes/auth.php: -------------------------------------------------------------------------------- 1 | middleware('guest') 15 | ->name('register'); 16 | 17 | Route::post('/register', [RegisteredUserController::class, 'store']) 18 | ->middleware('guest'); 19 | 20 | Route::get('/login', [AuthenticatedSessionController::class, 'create']) 21 | ->middleware('guest') 22 | ->name('login'); 23 | 24 | Route::post('/login', [AuthenticatedSessionController::class, 'store']) 25 | ->middleware('guest'); 26 | 27 | Route::get('/forgot-password', [PasswordResetLinkController::class, 'create']) 28 | ->middleware('guest') 29 | ->name('password.request'); 30 | 31 | Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) 32 | ->middleware('guest') 33 | ->name('password.email'); 34 | 35 | Route::get('/reset-password/{token}', [NewPasswordController::class, 'create']) 36 | ->middleware('guest') 37 | ->name('password.reset'); 38 | 39 | Route::post('/reset-password', [NewPasswordController::class, 'store']) 40 | ->middleware('guest') 41 | ->name('password.update'); 42 | 43 | Route::get('/verify-email', [EmailVerificationPromptController::class, '__invoke']) 44 | ->middleware('auth') 45 | ->name('verification.notice'); 46 | 47 | Route::get('/verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) 48 | ->middleware(['auth', 'signed', 'throttle:6,1']) 49 | ->name('verification.verify'); 50 | 51 | Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 52 | ->middleware(['auth', 'throttle:6,1']) 53 | ->name('verification.send'); 54 | 55 | Route::get('/confirm-password', [ConfirmablePasswordController::class, 'show']) 56 | ->middleware('auth') 57 | ->name('password.confirm'); 58 | 59 | Route::post('/confirm-password', [ConfirmablePasswordController::class, 'store']) 60 | ->middleware('auth'); 61 | 62 | Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) 63 | ->middleware('auth') 64 | ->name('logout'); 65 | -------------------------------------------------------------------------------- /stubs/routes/web.php: -------------------------------------------------------------------------------- 1 | Route::has('login'), 21 | 'canRegister' => Route::has('register'), 22 | 'laravelVersion' => Application::VERSION, 23 | 'phpVersion' => PHP_VERSION, 24 | ]); 25 | }); 26 | 27 | Route::get('/dashboard', function () { 28 | return Inertia::render('Dashboard'); 29 | })->middleware(['auth', 'verified'])->name('dashboard'); 30 | 31 | require __DIR__.'/auth.php'; 32 | -------------------------------------------------------------------------------- /stubs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | mode: 'jit', 5 | 6 | purge: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | './resources/js/**/*.vue', 11 | ], 12 | 13 | theme: { 14 | extend: { 15 | fontFamily: { 16 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 17 | }, 18 | }, 19 | }, 20 | 21 | variants: { 22 | extend: { 23 | opacity: ['disabled'], 24 | }, 25 | }, 26 | 27 | plugins: [require('@tailwindcss/forms')], 28 | } 29 | -------------------------------------------------------------------------------- /stubs/tests/Feature/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 16 | }); 17 | 18 | test('users can authenticate using the login screen', function () { 19 | $user = User::factory()->create(); 20 | 21 | post('/login', [ 22 | 'email' => $user->email, 23 | 'password' => 'password', 24 | ]) 25 | ->assertRedirect(RouteServiceProvider::HOME); 26 | 27 | assertAuthenticated(); 28 | }); 29 | 30 | test('users can not authenticate with invalid password', function () { 31 | $user = User::factory()->create(); 32 | 33 | post('/login', [ 34 | 'email' => $user->email, 35 | 'password' => 'wrong-password', 36 | ]) 37 | ->assertInvalid(); 38 | 39 | assertGuest(); 40 | }); 41 | -------------------------------------------------------------------------------- /stubs/tests/Feature/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 16 | 'email_verified_at' => null, 17 | ]); 18 | 19 | actingAs($user) 20 | ->get('/verify-email') 21 | ->assertStatus(200); 22 | }); 23 | 24 | test('email can be verified', function () { 25 | /** @var \App\Models\User */ 26 | $user = User::factory()->create([ 27 | 'email_verified_at' => null, 28 | ]); 29 | 30 | Event::fake(); 31 | 32 | $verificationUrl = URL::temporarySignedRoute( 33 | 'verification.verify', 34 | now()->addMinutes(60), 35 | ['id' => $user->id, 'hash' => sha1($user->email)] 36 | ); 37 | 38 | actingAs($user) 39 | ->get($verificationUrl) 40 | ->assertRedirect(RouteServiceProvider::HOME . '?verified=1'); 41 | 42 | Event::assertDispatched(Verified::class); 43 | assertTrue($user->fresh()->hasVerifiedEmail()); 44 | }); 45 | 46 | test('email is not verified with invalid hash', function () { 47 | /** @var \App\Models\User */ 48 | $user = User::factory()->create([ 49 | 'email_verified_at' => null, 50 | ]); 51 | 52 | $verificationUrl = URL::temporarySignedRoute( 53 | 'verification.verify', 54 | now()->addMinutes(60), 55 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 56 | ); 57 | 58 | actingAs($user) 59 | ->get($verificationUrl); 60 | 61 | assertFalse($user->fresh()->hasVerifiedEmail()); 62 | }); 63 | -------------------------------------------------------------------------------- /stubs/tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /stubs/tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | actingAs($user) 13 | ->get('/confirm-password') 14 | ->assertStatus(200); 15 | }); 16 | 17 | test('password can be confirmed', function () { 18 | /** @var \App\Models\User */ 19 | $user = User::factory()->create(); 20 | 21 | actingAs($user) 22 | ->post('/confirm-password', [ 23 | 'password' => 'password', 24 | ]) 25 | ->assertRedirect(RouteServiceProvider::HOME) 26 | ->assertSessionHasNoErrors(); 27 | }); 28 | 29 | test('password is not confirmed with invalid password', function () { 30 | /** @var \App\Models\User */ 31 | $user = User::factory()->create(); 32 | 33 | actingAs($user) 34 | ->post('/confirm-password', [ 35 | 'password' => 'wrong-password', 36 | ]) 37 | ->assertSessionHasErrors(); 38 | }); 39 | -------------------------------------------------------------------------------- /stubs/tests/Feature/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 13 | }); 14 | 15 | test('reset_password_link_can_be_requested', function () { 16 | Notification::fake(); 17 | 18 | $user = User::factory()->create(); 19 | 20 | post('/forgot-password', ['email' => $user->email]); 21 | 22 | Notification::assertSentTo($user, ResetPassword::class); 23 | }); 24 | 25 | test('reset_password_screen_can_be_rendered', function () { 26 | Notification::fake(); 27 | 28 | $user = User::factory()->create(); 29 | 30 | post('/forgot-password', ['email' => $user->email]); 31 | 32 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 33 | get('/reset-password/' . $notification->token) 34 | ->assertStatus(200); 35 | 36 | return true; 37 | }); 38 | }); 39 | 40 | test('password_can_be_reset_with_valid_token', function () { 41 | Notification::fake(); 42 | 43 | $user = User::factory()->create(); 44 | 45 | $this->post('/forgot-password', ['email' => $user->email]); 46 | 47 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 48 | post('/reset-password', [ 49 | 'token' => $notification->token, 50 | 'email' => $user->email, 51 | 'password' => 'password', 52 | 'password_confirmation' => 'password', 53 | ]) 54 | ->assertSessionHasNoErrors(); 55 | 56 | return true; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /stubs/tests/Feature/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 12 | }); 13 | 14 | test('new users can register', function () { 15 | post('/register', [ 16 | 'name' => 'Test User', 17 | 'email' => 'test@example.com', 18 | 'password' => 'password', 19 | 'password_confirmation' => 'password', 20 | ]) 21 | ->assertValid() 22 | ->assertRedirect(RouteServiceProvider::HOME); 23 | 24 | assertAuthenticated(); 25 | }); 26 | -------------------------------------------------------------------------------- /stubs/tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /stubs/tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 |