├── .babelrc ├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky └── commit-msg ├── .prettierignore ├── .prettierrc.json ├── README.md ├── app ├── Console │ └── Kernel.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ └── Controller.php │ ├── Kernel.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── HandleInertiaRequests.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustHosts.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php │ └── Requests │ │ └── Auth │ │ └── LoginRequest.php ├── Models │ └── User.php └── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ ├── HorizonServiceProvider.php │ └── RouteServiceProvider.php ├── artisan ├── babel.config.js ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── horizon.php ├── logging.php ├── mail.php ├── queue.php ├── sanctum.php ├── services.php ├── session.php ├── view.php └── websockets.php ├── coverage ├── clover.xml ├── coverage-final.json ├── lcov-report │ ├── base.css │ ├── block-navigation.js │ ├── favicon.png │ ├── index.html │ ├── prettify.css │ ├── prettify.js │ ├── sort-arrow-sprite.png │ └── sorter.js └── lcov.info ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0000_00_00_000000_create_websockets_statistics_entries_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ └── 2019_12_14_000001_create_personal_access_tokens_table.php └── seeders │ └── DatabaseSeeder.php ├── docker-compose.yml ├── docker ├── Dockerfile └── site.conf ├── jsconfig.json ├── lang └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── index.php ├── robots.txt └── vendor │ └── horizon │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── img │ ├── favicon.png │ ├── horizon.svg │ └── sprite.svg │ └── mix-manifest.json ├── resources ├── css │ └── app.css ├── js │ ├── Components │ │ ├── ApplicationLogo.tsx │ │ ├── Button.tsx │ │ ├── Checkbox.tsx │ │ ├── Dropdown.tsx │ │ ├── Input.tsx │ │ ├── InputError.tsx │ │ ├── Label.tsx │ │ ├── NavLink.tsx │ │ └── ResponsiveNavLink.tsx │ ├── Contexts │ │ └── Socket.tsx │ ├── Echo.tsx │ ├── Interfaces │ │ ├── AuthUserInterface.tsx │ │ └── UserInterface.tsx │ ├── Layouts │ │ ├── Authenticated.tsx │ │ └── Guest.tsx │ ├── Pages │ │ ├── Auth │ │ │ ├── ConfirmPassword.tsx │ │ │ ├── ForgotPassword.tsx │ │ │ ├── Login.tsx │ │ │ ├── Register.tsx │ │ │ ├── ResetPassword.tsx │ │ │ └── VerifyEmail.tsx │ │ ├── Dashboard.tsx │ │ └── Welcome.tsx │ ├── Tests │ │ └── Components │ │ │ ├── ApplicationLogo.test.tsx │ │ │ ├── Button.test.tsx │ │ │ ├── Checkbox.test.tsx │ │ │ ├── Dropdown.test.tsx │ │ │ ├── Input.test.tsx │ │ │ ├── InputError.test.tsx │ │ │ ├── Label.test.tsx │ │ │ ├── NavLink.test.tsx │ │ │ └── ResponsiveNavLink.test.tsx │ ├── app.tsx │ ├── bootstrap.js │ ├── env.d.ts │ └── setupTests.ts └── views │ ├── app.blade.php │ └── welcome.blade.php ├── routes ├── api.php ├── auth.php ├── channels.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── CreatesApplication.php ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ └── RegistrationTest.php │ └── ExampleTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php ├── tsconfig.json └── vite.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 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 = 2 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=pgsql 12 | DB_HOST=pgsql 13 | DB_PORT=5432 14 | DB_DATABASE=laravel 15 | DB_USERNAME=localuser 16 | DB_PASSWORD=supersecretpassword 17 | 18 | BROADCAST_DRIVER=pusher 19 | CACHE_DRIVER=redis 20 | FILESYSTEM_DISK=local 21 | QUEUE_CONNECTION=redis 22 | SESSION_DRIVER=redis 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=redis 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=mailhog 33 | MAIL_PORT=1025 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS="hello@example.com" 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID=123456 47 | PUSHER_APP_KEY=123456 48 | PUSHER_APP_SECRET=123456 49 | PUSHER_HOST=websockets 50 | PUSHER_PORT=6001 51 | PUSHER_SCHEME=https 52 | PUSHER_APP_CLUSTER=mt1 53 | 54 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 55 | VITE_PUSHER_HOST="${PUSHER_HOST}" 56 | VITE_PUSHER_PORT="${PUSHER_PORT}" 57 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 58 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 59 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | 'react-app', 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:sonarjs/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": [ 23 | "react", 24 | "@typescript-eslint", 25 | "import", 26 | "testing-library", 27 | "sonarjs", 28 | "prettier", 29 | ], 30 | "ignorePatterns": [ 31 | "resources/js/app.tsx" 32 | ], 33 | "rules": { 34 | "@typescript-eslint/explicit-module-boundary-types": "error", 35 | // turn of the base eslint rule, or it can return false positives 36 | "no-unused-vars": "off", 37 | "@typescript-eslint/no-unused-vars": [ 38 | "error", 39 | { 40 | args: "after-used", 41 | varsIgnorePattern: "^_", 42 | argsIgnorePattern: "^_", 43 | } 44 | ], 45 | "react/self-closing-comp": [ 46 | "error", 47 | { 48 | "component": true, 49 | "html": true 50 | } 51 | ], 52 | "array-callback-return": "error", 53 | "arrow-body-style": ["error", "as-needed"], 54 | "curly": "error", 55 | "dot-notation": "error", 56 | eqeqeq: "error", 57 | // we use default exports at the moment for pages so this is off for pages 58 | // in the case of Components we turn it on. See overrides 59 | "import/no-default-export": "off", 60 | "no-alert": "error", 61 | "no-console": "error", 62 | "no-continue": "error", 63 | "no-negated-condition": "error", 64 | "no-nested-ternary": "error", 65 | "prefer-arrow-callback": "error", 66 | "semi": "error", 67 | // prefer single quotes when able 68 | "@typescript-eslint/quotes": [ 69 | "error", 70 | "single" 71 | ] 72 | }, 73 | "settings": { 74 | "react": { 75 | "pragma": "React", // Pragma to use, default to "React" 76 | "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment" 77 | "version": "detect", // React version. "detect" automatically picks the version you have installed. 78 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. 79 | // It will default to "latest" and warn if missing, and to "detect" in the future 80 | "flowVersion": "0.53" // Flow version 81 | } 82 | }, 83 | "overrides": [ 84 | { 85 | "files": [ "resources/js/Components/**/*.tsx" ], 86 | "rules": { 87 | "import/no-default-export": "error", 88 | } 89 | }, 90 | { 91 | "files": [ '**/*.test.*' ], 92 | "extends": [ 'plugin:testing-library/react' ], 93 | "rules": { 94 | '@typescript-eslint/no-non-null-assertions': 'off', 95 | 'testing-library/prefer-explicit-assert': [ 96 | 'error', 97 | { assertion: 'toBeInTheDocument' }, 98 | ], 99 | 'testing-library/prefer-user-event': 'error' 100 | } 101 | } 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 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 | /node_modules 2 | /public/build 3 | /public/hot 4 | /public/storage 5 | /public/vendor/horizon/* 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .phpunit.result.cache 11 | Homestead.json 12 | Homestead.yaml 13 | auth.json 14 | npm-debug.log 15 | yarn-error.log 16 | /.idea 17 | /.vscode 18 | .php-cs-fixer.cache 19 | yarn.lock 20 | package-lock.json 21 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" || { 5 | npx_exit_code=$? 6 | 7 | echo 'For a guided commit message builder, use "npm run commit" instead of "git commit"' 8 | echo "" 9 | exit "$npx_exit_code" 10 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "bracketSameLine": true, 8 | "parser": "typescript", 9 | "semi": true, 10 | "requirePragma": true, 11 | "proseWrap": "preserve", 12 | "arrowParens": "always", 13 | "htmlWhitespaceSensitivity": "css", 14 | "quoteProps": "as-needed" 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Build Status 5 | License 6 |

7 | 8 | ## Laravel - React - Docker - Boilerplate 9 | 10 | This repo is built with the following: 11 | - PHP 8.2.3 12 | - Laravel 10 13 | - Laravel Websockets 14 | - Laravel Horizon 15 | - React 17 16 | - Vite 3 17 | - ESLint 8 18 | - TypeScript 4.7 19 | - Husky/Commit lint 20 | - Redis 7 21 | - Postgres 14.4 22 | - Nginx 1.23 23 | 24 | I put together this repo in an effort to have a solid starting place to begin a React/Docker/Laravel project from. 25 | While I understand monolithic repos may not be everyone's bread and butter, it makes sense for a good number of my projects. 26 | If you find any bugs or see anything that needs to be changed/updated feel free to put up a PR! 27 | 28 | A few things to note. 29 | 30 | - Websockets are ready to use out of the box for development. They are not configured for production however so you will 31 | need to handle the production config/setup based on your needs. If working with Cloudflare you will need to change the 32 | port that the sockets run on as well. 33 | - The `php artisan schedule:work`, `php artisan websocket:serve` and `php artisan horizon` commands all run from within 34 | their own containers. If you make changes that impacts these processes you will need to restart the containers using 35 | `docker-compose restart ` 36 | - I would discourage using the container based Postgres and Redis services in production. While its great for development, 37 | a managed service is going to take less resources from your all and serve you far better than these stock implementations 38 | would. 39 | 40 | ## Get started 41 | 42 | 1. Install [Docker Desktop](https://docs.docker.com/desktop/#download-and-install) 43 | 2. Clone this repo 44 | 1. `git clone git@github.com:lockhinator/laravel-react-docker-boilerplate.git` 45 | 3. CD into the directory 46 | 1. `cd laravel-react-docker-boilerplate` 47 | 4. Copy the `.env.example` to `.env` 48 | 1. `cp .env.example .env` 49 | 5. Update your `.env` to reflect the database you want to connect to. If using the default `docker-compose.yml` then you can copy the blow and paste it over the initial values. 50 | 1. ```dotenv 51 | DB_CONNECTION=pgsql 52 | DB_HOST=pgsql 53 | DB_PORT=5432 54 | DB_DATABASE=laravel 55 | DB_USERNAME=localuser 56 | DB_PASSWORD=supersecretpassword 57 | ``` 58 | 6. Run the following commands to run the repo in docker 59 | 1. `docker-compose build fpm node web` 60 | 2. `docker-compose run --rm fpm composer install` 61 | 3. `docker-compose run --rm fpm php artisan key:generate` 62 | 4. `docker-compose run --rm fpm php artisan migrate` 63 | 5. `docker-compose run --rm node yarn` 64 | 6. `docker-compose up -d` 65 | 7. Visit [http://localhost](http://localhost) and make sure the app is running 66 | 8. Start making building your app! 67 | 68 | ## Quick links 69 | With the addition of Laravel Horizon and Laravel Websockets there are a few new endpoints that you may want to access to 70 | observe/manage your application in your local development environment. These endpoints are: 71 | - [http://localhost/laravel-websockets](http://localhost/laravel-websockets) which allows you to view all websocket communication 72 | (broadcast events) going through your development environment 73 | - [http://localhost/horizon](http://localhost/horizon) which allows you to view the current queue within your development 74 | environment. 75 | 76 | ## Usage 77 | 78 | Linters and test suites are available for the boilerplate. You are free to change the configs how you want. 79 | 80 | ### Running tests/linters 81 | 82 | You can run tests/linters for both React and Laravel/PHP independently. These are the commands to do so: 83 | 1. React 84 | 1. Jest tests 85 | 1. `docker-compose run --rm node yarn test` 86 | 2. Prettier 87 | 1. `docker-compose run node yarn prettier:check` 88 | 2. `docker-compose run node yarn prettier:fix` 89 | 2. Laravel 90 | 1. `docker-compose run --rm fpm php artisan test --coverage --min=85` 91 | 3. PHP CS Fixer 92 | 1. `docker-compose run --rm fpm composer fix-cs-check` 93 | 2. `docker-compose run --rm fpm composer fix-cs` 94 | 95 | The Jest configuration is defined in the `package.json` file under the `jest` key. 96 | The reason for this is that using `jest.config.ts` results in coverage not correctly running and no files will be found. 97 | If you want to make changes to the Jest configuration then do it in the `package.json` or risk not having coverage run correctly. 98 | 99 | ## Committing changes 100 | 101 | In order to keep commits looking good this repository uses commitlint in conjunction with husky. 102 | 103 | Because of this the commit process ends up being: 104 | - Create your branch 105 | - Make changes to the code base 106 | - Add your changes via `git add` 107 | - Commit your changes using `yarn commit` 108 | - Fill out the interactive prompts 109 | - Push your changes to your branch 110 | - `git push` 111 | 112 | ## Todo 113 | - [ ] Fill out the jest tests more to cover the different default Laravel Auth pages 114 | 115 | ## License 116 | 117 | This boilerplate is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 118 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | $schedule->command('cache:prune-stale-tags')->hourly(); 20 | } 21 | 22 | /** 23 | * Register the commands for the application. 24 | * 25 | * @return void 26 | */ 27 | protected function commands() 28 | { 29 | $this->load(__DIR__.'/Commands'); 30 | 31 | require base_path('routes/console.php'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed to the session on validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->reportable(function (Throwable $e) { 47 | // 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Illuminate\Http\Middleware\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | \App\Http\Middleware\HandleInertiaRequests::class, 40 | ], 41 | 42 | 'api' => [ 43 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 44 | 'throttle:api', 45 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 46 | ], 47 | ]; 48 | 49 | /** 50 | * The application's route middleware. 51 | * 52 | * These middleware may be assigned to groups or used individually. 53 | * 54 | * @var array 55 | */ 56 | protected $routeMiddleware = [ 57 | 'auth' => \App\Http\Middleware\Authenticate::class, 58 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 59 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 60 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 61 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 62 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 63 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 64 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 65 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 66 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 67 | ]; 68 | } 69 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | [ 39 | 'user' => $request->user(), 40 | ], 41 | 'ziggy' => function () use ($request) { 42 | return array_merge((new Ziggy())->toArray(), [ 43 | 'location' => $request->url(), 44 | ]); 45 | }, 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /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' => trans('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::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected $fillable = [ 23 | 'name', 24 | 'email', 25 | 'password', 26 | ]; 27 | 28 | /** 29 | * The attributes that should be hidden for serialization. 30 | * 31 | * @var array 32 | */ 33 | protected $hidden = [ 34 | 'password', 35 | 'remember_token', 36 | ]; 37 | 38 | /** 39 | * The attributes that should be cast. 40 | * 41 | * @var array 42 | */ 43 | protected $casts = [ 44 | 'email_verified_at' => 'datetime', 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | // 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | 33 | /** 34 | * Determine if events and listeners should be automatically discovered. 35 | * 36 | * @return bool 37 | */ 38 | public function shouldDiscoverEvents() 39 | { 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | email, [ 38 | // 39 | ]); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('web') 37 | ->group(base_path('routes/web.php')); 38 | }); 39 | } 40 | 41 | /** 42 | * Configure the rate limiters for the application. 43 | * 44 | * @return void 45 | */ 46 | protected function configureRateLimiting() 47 | { 48 | RateLimiter::for('api', function (Request $request) { 49 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ], 11 | '@babel/preset-typescript', 12 | [ 13 | 'react-app', 14 | { 15 | runtime: 'automatic', 16 | absoluteRuntime: false 17 | } 18 | ] 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "MIT", 7 | "require": { 8 | "php": "^8.0.2", 9 | "beyondcode/laravel-websockets": "^1.13", 10 | "guzzlehttp/guzzle": "^7.2", 11 | "inertiajs/inertia-laravel": "^0.6.9", 12 | "laravel/framework": "^10.0", 13 | "laravel/horizon": "^5.10", 14 | "laravel/sanctum": "^3.2", 15 | "laravel/tinker": "^2.7", 16 | "tightenco/ziggy": "^1.0" 17 | }, 18 | "require-dev": { 19 | "fakerphp/faker": "^1.9.1", 20 | "laravel/breeze": "^1.11", 21 | "laravel/pint": "^1.0", 22 | "laravel/sail": "^1.0.1", 23 | "mockery/mockery": "^1.4.4", 24 | "nunomaduro/collision": "^7.0", 25 | "phpunit/phpunit": "^10.0", 26 | "spatie/laravel-ignition": "^2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "App\\": "app/", 31 | "Database\\Factories\\": "database/factories/", 32 | "Database\\Seeders\\": "database/seeders/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "post-autoload-dump": [ 42 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 43 | "@php artisan package:discover --ansi" 44 | ], 45 | "post-update-cmd": [ 46 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 47 | ], 48 | "post-root-package-install": [ 49 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 50 | ], 51 | "post-create-project-cmd": [ 52 | "@php artisan key:generate --ansi" 53 | ] 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "dont-discover": [] 58 | } 59 | }, 60 | "config": { 61 | "optimize-autoloader": true, 62 | "preferred-install": "dist", 63 | "sort-packages": true, 64 | "allow-plugins": { 65 | "pestphp/pest-plugin": true 66 | } 67 | }, 68 | "minimum-stability": "stable", 69 | "prefer-stable": true 70 | } 71 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication drivers have a user provider. This defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | mechanisms used by this application to persist your user's data. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | sources which represent each model / table. These sources may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => App\Models\User::class, 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | You may specify multiple password reset configurations if you have more 80 | | than one user table or model in the application and you want to have 81 | | separate password reset settings based on the specific user types. 82 | | 83 | | The expire time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | */ 88 | 89 | 'passwords' => [ 90 | 'users' => [ 91 | 'provider' => 'users', 92 | 'table' => 'password_resets', 93 | 'expire' => 60, 94 | 'throttle' => 60, 95 | ], 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Password Confirmation Timeout 101 | |-------------------------------------------------------------------------- 102 | | 103 | | Here you may define the amount of seconds before a password confirmation 104 | | times out and the user is prompted to re-enter their password via the 105 | | confirmation screen. By default, the timeout lasts for three hours. 106 | | 107 | */ 108 | 109 | 'password_timeout' => 10800, 110 | 111 | ]; 112 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 40 | 'port' => env('PUSHER_PORT', 443), 41 | 'scheme' => env('PUSHER_SCHEME', 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env('ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "apc", "array", "database", "file", 30 | | "memcached", "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | 'lock_connection' => null, 50 | ], 51 | 52 | 'file' => [ 53 | 'driver' => 'file', 54 | 'path' => storage_path('framework/cache/data'), 55 | ], 56 | 57 | 'memcached' => [ 58 | 'driver' => 'memcached', 59 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 60 | 'sasl' => [ 61 | env('MEMCACHED_USERNAME'), 62 | env('MEMCACHED_PASSWORD'), 63 | ], 64 | 'options' => [ 65 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 66 | ], 67 | 'servers' => [ 68 | [ 69 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 70 | 'port' => env('MEMCACHED_PORT', 11211), 71 | 'weight' => 100, 72 | ], 73 | ], 74 | ], 75 | 76 | 'redis' => [ 77 | 'driver' => 'redis', 78 | 'connection' => 'cache', 79 | 'lock_connection' => 'default', 80 | ], 81 | 82 | 'dynamodb' => [ 83 | 'driver' => 'dynamodb', 84 | 'key' => env('AWS_ACCESS_KEY_ID'), 85 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 86 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 87 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 88 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 89 | ], 90 | 91 | 'octane' => [ 92 | 'driver' => 'octane', 93 | ], 94 | 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Cache Key Prefix 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When utilizing the APC, database, memcached, Redis, or DynamoDB cache 103 | | stores there might be other applications using the same cache. For 104 | | that reason, you may prefix every cache key to avoid collisions. 105 | | 106 | */ 107 | 108 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'sqlite' => [ 39 | 'driver' => 'sqlite', 40 | 'url' => env('DATABASE_URL'), 41 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 42 | 'prefix' => '', 43 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 44 | ], 45 | 46 | 'mysql' => [ 47 | 'driver' => 'mysql', 48 | 'url' => env('DATABASE_URL'), 49 | 'host' => env('DB_HOST', '127.0.0.1'), 50 | 'port' => env('DB_PORT', '3306'), 51 | 'database' => env('DB_DATABASE', 'forge'), 52 | 'username' => env('DB_USERNAME', 'forge'), 53 | 'password' => env('DB_PASSWORD', ''), 54 | 'unix_socket' => env('DB_SOCKET', ''), 55 | 'charset' => 'utf8mb4', 56 | 'collation' => 'utf8mb4_unicode_ci', 57 | 'prefix' => '', 58 | 'prefix_indexes' => true, 59 | 'strict' => true, 60 | 'engine' => null, 61 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 62 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 63 | ]) : [], 64 | ], 65 | 66 | 'pgsql' => [ 67 | 'driver' => 'pgsql', 68 | 'url' => env('DATABASE_URL'), 69 | 'host' => env('DB_HOST', '127.0.0.1'), 70 | 'port' => env('DB_PORT', '5432'), 71 | 'database' => env('DB_DATABASE', 'forge'), 72 | 'username' => env('DB_USERNAME', 'forge'), 73 | 'password' => env('DB_PASSWORD', ''), 74 | 'charset' => 'utf8', 75 | 'prefix' => '', 76 | 'prefix_indexes' => true, 77 | 'search_path' => 'public', 78 | 'sslmode' => 'prefer', 79 | ], 80 | 81 | 'sqlsrv' => [ 82 | 'driver' => 'sqlsrv', 83 | 'url' => env('DATABASE_URL'), 84 | 'host' => env('DB_HOST', 'localhost'), 85 | 'port' => env('DB_PORT', '1433'), 86 | 'database' => env('DB_DATABASE', 'forge'), 87 | 'username' => env('DB_USERNAME', 'forge'), 88 | 'password' => env('DB_PASSWORD', ''), 89 | 'charset' => 'utf8', 90 | 'prefix' => '', 91 | 'prefix_indexes' => true, 92 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 93 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 94 | ], 95 | 96 | ], 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Migration Repository Table 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This table keeps track of all the migrations that have already run for 104 | | your application. Using this information, we can determine which of 105 | | the migrations on disk haven't actually been run in the database. 106 | | 107 | */ 108 | 109 | 'migrations' => 'migrations', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Redis Databases 114 | |-------------------------------------------------------------------------- 115 | | 116 | | Redis is an open source, fast, and advanced key-value store that also 117 | | provides a richer body of commands than a typical key-value system 118 | | such as APC or Memcached. Laravel makes it easy to dig right in. 119 | | 120 | */ 121 | 122 | 'redis' => [ 123 | 124 | 'client' => env('REDIS_CLIENT', 'phpredis'), 125 | 126 | 'options' => [ 127 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 128 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 129 | ], 130 | 131 | 'default' => [ 132 | 'url' => env('REDIS_URL'), 133 | 'host' => env('REDIS_HOST', '127.0.0.1'), 134 | 'username' => env('REDIS_USERNAME'), 135 | 'password' => env('REDIS_PASSWORD'), 136 | 'port' => env('REDIS_PORT', '6379'), 137 | 'database' => env('REDIS_DB', '0'), 138 | ], 139 | 140 | 'cache' => [ 141 | 'url' => env('REDIS_URL'), 142 | 'host' => env('REDIS_HOST', '127.0.0.1'), 143 | 'username' => env('REDIS_USERNAME'), 144 | 'password' => env('REDIS_PASSWORD'), 145 | 'port' => env('REDIS_PORT', '6379'), 146 | 'database' => env('REDIS_CACHE_DB', '1'), 147 | ], 148 | 149 | ], 150 | 151 | ]; 152 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path('app/public'), 42 | 'url' => env('APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env('AWS_ACCESS_KEY_ID'), 50 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env('AWS_DEFAULT_REGION'), 52 | 'bucket' => env('AWS_BUCKET'), 53 | 'url' => env('AWS_URL'), 54 | 'endpoint' => env('AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path('storage') => storage_path('app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Deprecations Log Channel 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This option controls the log channel that should be used to log warnings 28 | | regarding deprecated PHP and library features. This allows you to get 29 | | your application ready for upcoming major versions of dependencies. 30 | | 31 | */ 32 | 33 | 'deprecations' => [ 34 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 35 | 'trace' => false, 36 | ], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Log Channels 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Here you may configure the log channels for your application. Out of 44 | | the box, Laravel uses the Monolog PHP logging library. This gives 45 | | you a variety of powerful log handlers / formatters to utilize. 46 | | 47 | | Available Drivers: "single", "daily", "slack", "syslog", 48 | | "errorlog", "monolog", 49 | | "custom", "stack" 50 | | 51 | */ 52 | 53 | 'channels' => [ 54 | 'stack' => [ 55 | 'driver' => 'stack', 56 | 'channels' => ['single'], 57 | 'ignore_exceptions' => false, 58 | ], 59 | 60 | 'single' => [ 61 | 'driver' => 'single', 62 | 'path' => storage_path('logs/laravel.log'), 63 | 'level' => env('LOG_LEVEL', 'debug'), 64 | ], 65 | 66 | 'daily' => [ 67 | 'driver' => 'daily', 68 | 'path' => storage_path('logs/laravel.log'), 69 | 'level' => env('LOG_LEVEL', 'debug'), 70 | 'days' => 14, 71 | ], 72 | 73 | 'slack' => [ 74 | 'driver' => 'slack', 75 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 76 | 'username' => 'Laravel Log', 77 | 'emoji' => ':boom:', 78 | 'level' => env('LOG_LEVEL', 'critical'), 79 | ], 80 | 81 | 'papertrail' => [ 82 | 'driver' => 'monolog', 83 | 'level' => env('LOG_LEVEL', 'debug'), 84 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 85 | 'handler_with' => [ 86 | 'host' => env('PAPERTRAIL_URL'), 87 | 'port' => env('PAPERTRAIL_PORT'), 88 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), 89 | ], 90 | ], 91 | 92 | 'stderr' => [ 93 | 'driver' => 'monolog', 94 | 'level' => env('LOG_LEVEL', 'debug'), 95 | 'handler' => StreamHandler::class, 96 | 'formatter' => env('LOG_STDERR_FORMATTER'), 97 | 'with' => [ 98 | 'stream' => 'php://stderr', 99 | ], 100 | ], 101 | 102 | 'syslog' => [ 103 | 'driver' => 'syslog', 104 | 'level' => env('LOG_LEVEL', 'debug'), 105 | ], 106 | 107 | 'errorlog' => [ 108 | 'driver' => 'errorlog', 109 | 'level' => env('LOG_LEVEL', 'debug'), 110 | ], 111 | 112 | 'null' => [ 113 | 'driver' => 'monolog', 114 | 'handler' => NullHandler::class, 115 | ], 116 | 117 | 'emergency' => [ 118 | 'path' => storage_path('logs/laravel.log'), 119 | ], 120 | ], 121 | 122 | ]; 123 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'smtp'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Mailer Configurations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure all of the mailers used by your application plus 24 | | their respective settings. Several examples have been configured for 25 | | you and you are free to add your own as your application requires. 26 | | 27 | | Laravel supports a variety of mail "transport" drivers to be used while 28 | | sending an e-mail. You will specify which one you are using for your 29 | | mailers below. You are free to add additional mailers as required. 30 | | 31 | | Supported: "smtp", "sendmail", "mailgun", "ses", 32 | | "postmark", "log", "array", "failover" 33 | | 34 | */ 35 | 36 | 'mailers' => [ 37 | 'smtp' => [ 38 | 'transport' => 'smtp', 39 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 40 | 'port' => env('MAIL_PORT', 587), 41 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 42 | 'username' => env('MAIL_USERNAME'), 43 | 'password' => env('MAIL_PASSWORD'), 44 | 'timeout' => null, 45 | 'local_domain' => env('MAIL_EHLO_DOMAIN'), 46 | ], 47 | 48 | 'ses' => [ 49 | 'transport' => 'ses', 50 | ], 51 | 52 | 'mailgun' => [ 53 | 'transport' => 'mailgun', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | ], 59 | 60 | 'sendmail' => [ 61 | 'transport' => 'sendmail', 62 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 63 | ], 64 | 65 | 'log' => [ 66 | 'transport' => 'log', 67 | 'channel' => env('MAIL_LOG_CHANNEL'), 68 | ], 69 | 70 | 'array' => [ 71 | 'transport' => 'array', 72 | ], 73 | 74 | 'failover' => [ 75 | 'transport' => 'failover', 76 | 'mailers' => [ 77 | 'smtp', 78 | 'log', 79 | ], 80 | ], 81 | ], 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Global "From" Address 86 | |-------------------------------------------------------------------------- 87 | | 88 | | You may wish for all e-mails sent by your application to be sent from 89 | | the same address. Here, you may specify a name and address that is 90 | | used globally for all e-mails that are sent by your application. 91 | | 92 | */ 93 | 94 | 'from' => [ 95 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 96 | 'name' => env('MAIL_FROM_NAME', 'Example'), 97 | ], 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Markdown Mail Settings 102 | |-------------------------------------------------------------------------- 103 | | 104 | | If you are using Markdown based email rendering, you may configure your 105 | | theme and component paths here, allowing you to customize the design 106 | | of the emails. Or, you may simply stick with the Laravel defaults! 107 | | 108 | */ 109 | 110 | 'markdown' => [ 111 | 'theme' => 'default', 112 | 113 | 'paths' => [ 114 | resource_path('views/vendor/mail'), 115 | ], 116 | ], 117 | 118 | ]; 119 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | 'after_commit' => false, 43 | ], 44 | 45 | 'beanstalkd' => [ 46 | 'driver' => 'beanstalkd', 47 | 'host' => 'localhost', 48 | 'queue' => 'default', 49 | 'retry_after' => 90, 50 | 'block_for' => 0, 51 | 'after_commit' => false, 52 | ], 53 | 54 | 'sqs' => [ 55 | 'driver' => 'sqs', 56 | 'key' => env('AWS_ACCESS_KEY_ID'), 57 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 58 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 59 | 'queue' => env('SQS_QUEUE', 'default'), 60 | 'suffix' => env('SQS_SUFFIX'), 61 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 62 | 'after_commit' => false, 63 | ], 64 | 65 | 'redis' => [ 66 | 'driver' => 'redis', 67 | 'connection' => 'default', 68 | 'queue' => env('REDIS_QUEUE', 'default'), 69 | 'retry_after' => 90, 70 | 'block_for' => null, 71 | 'after_commit' => false, 72 | ], 73 | 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Failed Queue Jobs 79 | |-------------------------------------------------------------------------- 80 | | 81 | | These options configure the behavior of failed queue job logging so you 82 | | can control which database and table are used to store the jobs that 83 | | have failed. You may change them to any database / table you wish. 84 | | 85 | */ 86 | 87 | 'failed' => [ 88 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 89 | 'database' => env('DB_CONNECTION', 'mysql'), 90 | 'table' => 'failed_jobs', 91 | ], 92 | 93 | ]; 94 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. If this value is null, personal access tokens do 45 | | not expire. This won't tweak the lifetime of first-party sessions. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Sanctum Middleware 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When authenticating your first-party SPA with Sanctum you may need to 57 | | customize some of the middleware Sanctum uses while processing the 58 | | request. You may change the middleware listed below as required. 59 | | 60 | */ 61 | 62 | 'middleware' => [ 63 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 64 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /config/websockets.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'port' => env('LARAVEL_WEBSOCKETS_PORT', 6001), 12 | ], 13 | 14 | /* 15 | * This package comes with multi tenancy out of the box. Here you can 16 | * configure the different apps that can use the webSockets server. 17 | * 18 | * Optionally you specify capacity so you can limit the maximum 19 | * concurrent connections for a specific app. 20 | * 21 | * Optionally you can disable client events so clients cannot send 22 | * messages to each other via the webSockets. 23 | */ 24 | 'apps' => [ 25 | [ 26 | 'id' => env('PUSHER_APP_ID'), 27 | 'name' => env('APP_NAME'), 28 | 'key' => env('PUSHER_APP_KEY'), 29 | 'secret' => env('PUSHER_APP_SECRET'), 30 | 'path' => env('PUSHER_APP_PATH'), 31 | 'capacity' => null, 32 | 'enable_client_messages' => false, 33 | 'enable_statistics' => true, 34 | ], 35 | ], 36 | 37 | /* 38 | * This class is responsible for finding the apps. The default provider 39 | * will use the apps defined in this config file. 40 | * 41 | * You can create a custom provider by implementing the 42 | * `AppProvider` interface. 43 | */ 44 | 'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class, 45 | 46 | /* 47 | * This array contains the hosts of which you want to allow incoming requests. 48 | * Leave this empty if you want to accept requests from all hosts. 49 | */ 50 | 'allowed_origins' => [ 51 | // 52 | ], 53 | 54 | /* 55 | * The maximum request size in kilobytes that is allowed for an incoming WebSocket request. 56 | */ 57 | 'max_request_size_in_kb' => 250, 58 | 59 | /* 60 | * This path will be used to register the necessary routes for the package. 61 | */ 62 | 'path' => 'laravel-websockets', 63 | 64 | /* 65 | * Dashboard Routes Middleware 66 | * 67 | * These middleware will be assigned to every dashboard route, giving you 68 | * the chance to add your own middleware to this list or change any of 69 | * the existing middleware. Or, you can simply stick with this list. 70 | */ 71 | 'middleware' => [ 72 | 'web', 73 | Authorize::class, 74 | ], 75 | 76 | 'statistics' => [ 77 | /* 78 | * This model will be used to store the statistics of the WebSocketsServer. 79 | * The only requirement is that the model should extend 80 | * `WebSocketsStatisticsEntry` provided by this package. 81 | */ 82 | 'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class, 83 | 84 | /** 85 | * The Statistics Logger will, by default, handle the incoming statistics, store them 86 | * and then release them into the database on each interval defined below. 87 | */ 88 | 'logger' => BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger::class, 89 | 90 | /* 91 | * Here you can specify the interval in seconds at which statistics should be logged. 92 | */ 93 | 'interval_in_seconds' => 60, 94 | 95 | /* 96 | * When the clean-command is executed, all recorded statistics older than 97 | * the number of days specified here will be deleted. 98 | */ 99 | 'delete_statistics_older_than_days' => 60, 100 | 101 | /* 102 | * Use an DNS resolver to make the requests to the statistics logger 103 | * default is to resolve everything to 127.0.0.1. 104 | */ 105 | 'perform_dns_lookup' => false, 106 | ], 107 | 108 | /* 109 | * Define the optional SSL context for your WebSocket connections. 110 | * You can see all available options at: http://php.net/manual/en/context.ssl.php 111 | */ 112 | 'ssl' => [ 113 | /* 114 | * Path to local certificate file on filesystem. It must be a PEM encoded file which 115 | * contains your certificate and private key. It can optionally contain the 116 | * certificate chain of issuers. The private key also may be contained 117 | * in a separate file specified by local_pk. 118 | */ 119 | 'local_cert' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT', null), 120 | 121 | /* 122 | * Path to local private key file on filesystem in case of separate files for 123 | * certificate (local_cert) and private key. 124 | */ 125 | 'local_pk' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_PK', null), 126 | 127 | /* 128 | * Passphrase for your local_cert file. 129 | */ 130 | 'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null), 131 | ], 132 | 133 | /* 134 | * Channel Manager 135 | * This class handles how channel persistence is handled. 136 | * By default, persistence is stored in an array by the running webserver. 137 | * The only requirement is that the class should implement 138 | * `ChannelManager` interface provided by this package. 139 | */ 140 | 'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class, 141 | ]; 142 | -------------------------------------------------------------------------------- /coverage/clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /coverage/coverage-final.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* yellow */ 156 | .cbranch-no { background: yellow !important; color: #111; } 157 | /* dark red */ 158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 | .low .chart { border:1px solid #C21F39 } 160 | .highlighted, 161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 | background: #C21F39 !important; 163 | } 164 | /* medium red */ 165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 | /* light red */ 167 | .low, .cline-no { background:#FCE1E5 } 168 | /* light green */ 169 | .high, .cline-yes { background:rgb(230,245,208) } 170 | /* medium green */ 171 | .cstat-yes { background:rgb(161,215,106) } 172 | /* dark green */ 173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 | .high .chart { border:1px solid rgb(77,146,33) } 175 | /* dark yellow (gold) */ 176 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 177 | .medium .chart { border:1px solid #f9cd0b; } 178 | /* light yellow */ 179 | .medium { background: #fff4c2; } 180 | 181 | .cstat-skip { background: #ddd; color: #111; } 182 | .fstat-skip { background: #ddd; color: #111 !important; } 183 | .cbranch-skip { background: #ddd !important; color: #111; } 184 | 185 | span.cline-neutral { background: #eaeaea; } 186 | 187 | .coverage-summary td.empty { 188 | opacity: .5; 189 | padding-top: 4px; 190 | padding-bottom: 4px; 191 | line-height: 1; 192 | color: #888; 193 | } 194 | 195 | .cover-fill, .cover-empty { 196 | display:inline-block; 197 | height: 12px; 198 | } 199 | .chart { 200 | line-height: 0; 201 | } 202 | .cover-empty { 203 | background: white; 204 | } 205 | .cover-full { 206 | border-right: none !important; 207 | } 208 | pre.prettyprint { 209 | border: none !important; 210 | padding: 0 !important; 211 | margin: 0 !important; 212 | } 213 | .com { color: #999 !important; } 214 | .ignore-none { color: #999; font-weight: normal; } 215 | 216 | .wrapper { 217 | min-height: 100%; 218 | height: auto !important; 219 | height: 100%; 220 | margin: 0 auto -48px; 221 | } 222 | .footer, .push { 223 | height: 48px; 224 | } 225 | -------------------------------------------------------------------------------- /coverage/lcov-report/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | if ( 67 | document.getElementById('fileSearch') === document.activeElement && 68 | document.activeElement != null 69 | ) { 70 | // if we're currently focused on the search input, we don't want to navigate 71 | return; 72 | } 73 | 74 | switch (event.which) { 75 | case 78: // n 76 | case 74: // j 77 | goToNext(); 78 | break; 79 | case 66: // b 80 | case 75: // k 81 | case 80: // p 82 | goToPrevious(); 83 | break; 84 | } 85 | }; 86 | })(); 87 | window.addEventListener('keydown', jumpToCode); 88 | -------------------------------------------------------------------------------- /coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for All files 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |
22 |

All files

23 |
24 | 25 |
26 | Unknown% 27 | Statements 28 | 0/0 29 |
30 | 31 | 32 |
33 | Unknown% 34 | Branches 35 | 0/0 36 |
37 | 38 | 39 |
40 | Unknown% 41 | Functions 42 | 0/0 43 |
44 | 45 | 46 |
47 | Unknown% 48 | Lines 49 | 0/0 50 |
51 | 52 | 53 |
54 |

55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

57 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
FileStatementsBranchesFunctionsLines
83 |
84 |
85 |
86 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/coverage/lcov.info -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | * 32 | * @return static 33 | */ 34 | public function unverified() 35 | { 36 | return $this->state(function (array $attributes) { 37 | return [ 38 | 'email_verified_at' => null, 39 | ]; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('app_id'); 19 | $table->integer('peak_connection_count'); 20 | $table->integer('websocket_message_count'); 21 | $table->integer('api_message_count'); 22 | $table->nullableTimestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('websockets_statistics_entries'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->string('name'); 18 | $table->string('email')->unique(); 19 | $table->timestamp('email_verified_at')->nullable(); 20 | $table->string('password'); 21 | $table->rememberToken(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('users'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 17 | $table->string('token'); 18 | $table->timestamp('created_at')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('password_resets'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->string('uuid')->unique(); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->morphs('tokenable'); 18 | $table->string('name'); 19 | $table->string('token', 64)->unique(); 20 | $table->text('abilities')->nullable(); 21 | $table->timestamp('last_used_at')->nullable(); 22 | $table->timestamp('expires_at')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('personal_access_tokens'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | // \App\Models\User::factory()->create([ 20 | // 'name' => 'Test User', 21 | // 'email' => 'test@example.com', 22 | // ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | fpm: 5 | build: 6 | context: . 7 | dockerfile: docker/Dockerfile 8 | target: fpm-dev 9 | container_name: fpm 10 | restart: always 11 | tty: true 12 | volumes: 13 | - .:/var/www 14 | networks: 15 | - app-network 16 | 17 | worker: 18 | build: 19 | context: . 20 | dockerfile: docker/Dockerfile 21 | target: fpm-dev 22 | container_name: worker 23 | command: php artisan horizon 24 | restart: always 25 | tty: true 26 | volumes: 27 | - .:/var/www 28 | networks: 29 | - app-network 30 | 31 | schedule: 32 | build: 33 | context: . 34 | dockerfile: docker/Dockerfile 35 | target: fpm-dev 36 | container_name: schedule 37 | command: php artisan schedule:work 38 | restart: always 39 | tty: true 40 | volumes: 41 | - .:/var/www 42 | networks: 43 | - app-network 44 | 45 | web: 46 | build: 47 | context: . 48 | dockerfile: docker/Dockerfile 49 | target: nginx 50 | container_name: web 51 | restart: always 52 | tty: true 53 | ports: 54 | - "80:80" 55 | - "443:443" 56 | volumes: 57 | - .:/var/www 58 | networks: 59 | - app-network 60 | 61 | websockets: 62 | build: 63 | context: . 64 | dockerfile: docker/Dockerfile 65 | target: fpm-dev 66 | container_name: websockets 67 | command: php artisan websockets:serve 68 | restart: always 69 | tty: true 70 | ports: 71 | - "6001:6001" 72 | volumes: 73 | - .:/var/www 74 | networks: 75 | - app-network 76 | 77 | node: 78 | build: 79 | context: . 80 | dockerfile: docker/Dockerfile 81 | target: node 82 | container_name: node 83 | command: yarn dev 84 | restart: always 85 | tty: true 86 | extra_hosts: 87 | - 'host.docker.internal:host-gateway' 88 | ports: 89 | - "5173:5173" 90 | volumes: 91 | # the node container only needs access to these resources 92 | # this is also only in the event you are updating these resources 93 | - .env:/var/www/.env 94 | - ./public:/var/www/public 95 | - ./vite.config.js:/var/www/vite.config.js 96 | - ./resources:/var/www/resources 97 | - ./package.json:/var/www/package.json 98 | - node_modules:/var/www/node_modules 99 | networks: 100 | - app-network 101 | 102 | pgsql: 103 | image: postgres:14.4-alpine 104 | container_name: pgsql 105 | restart: always 106 | tty: true 107 | ports: 108 | - "5432:5432" 109 | environment: 110 | POSTGRES_USER: localuser 111 | POSTGRES_PASSWORD: supersecretpassword 112 | POSTGRES_DB: laravel 113 | volumes: 114 | - dbdata:/var/lib/postgresql/data 115 | networks: 116 | - app-network 117 | 118 | redis: 119 | image: redis:7.0.4 120 | container_name: redis 121 | restart: always 122 | tty: true 123 | ports: 124 | - "6379:6379" 125 | command: redis-server --save 20 1 --loglevel warning 126 | volumes: 127 | - cache:/data 128 | networks: 129 | - app-network 130 | 131 | networks: 132 | app-network: 133 | driver: bridge 134 | 135 | volumes: 136 | dbdata: 137 | driver: local 138 | cache: 139 | driver: local 140 | node_modules: 141 | driver: local 142 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ######################################################################################################################## 2 | # fpm-base - this can be used in prod builds 3 | ######################################################################################################################## 4 | FROM php:8.2-fpm-buster as fpm-base 5 | 6 | RUN apt-get update && apt-get install -y build-essential libpng-dev libodb-pgsql-dev libpq-dev libzip-dev \ 7 | libonig-dev libjpeg62-turbo-dev libfreetype6-dev locales zip jpegoptim optipng pngquant gifsicle vim unzip git curl 8 | RUN pecl install redis && docker-php-ext-enable redis 9 | RUN docker-php-ext-install pdo_pgsql mbstring zip exif pcntl 10 | RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \ 11 | docker-php-ext-install gd 12 | 13 | # libuv install for using event loop with stuff such as reactphp 14 | RUN apt-get install -y libuv1-dev 15 | RUN git clone https://github.com/bwoebi/php-uv.git && \ 16 | cd php-uv && \ 17 | phpize && \ 18 | ./configure && \ 19 | make && \ 20 | make install 21 | RUN echo 'extension=uv.so' >> /usr/local/etc/php/php.ini 22 | 23 | WORKDIR /var/www 24 | # Copy existing application directory contents 25 | COPY . /var/www 26 | RUN chown -R www-data:www-data /var/www 27 | # Set 775 Permissions 28 | RUN chmod -R 775 /var/www/storage 29 | 30 | # Install all production composer deps 31 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 32 | RUN composer install --no-dev 33 | 34 | 35 | # Generate the ziggy configuration 36 | RUN php artisan ziggy:generate 37 | 38 | ######################################################################################################################## 39 | # fpm-dev build 40 | ######################################################################################################################## 41 | FROM fpm-base as fpm-dev 42 | RUN pecl install xdebug && \ 43 | docker-php-ext-enable xdebug 44 | RUN echo "xdebug.mode=develop,debug,coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 45 | # install composer development dependancies 46 | RUN composer install --dev 47 | 48 | ######################################################################################################################## 49 | # node build 50 | ######################################################################################################################## 51 | FROM fpm-base as node 52 | # install nodejs 16 (only built for prod run) 53 | RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - 54 | RUN apt install -y nodejs 55 | RUN npm install --location=global npm@9.6.0 56 | RUN npm install --location=global -y yarn 57 | RUN yarn 58 | RUN yarn build 59 | 60 | ######################################################################################################################## 61 | # nginx build 62 | ######################################################################################################################## 63 | FROM nginx:1.23 as nginx 64 | COPY --from=fpm-base /var/www /var/www 65 | COPY docker/site.conf /etc/nginx/conf.d/default.conf 66 | 67 | -------------------------------------------------------------------------------- /docker/site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | index index.php index.html; 5 | error_log /dev/stdout info; 6 | access_log /dev/stdout; 7 | root /var/www/public; 8 | 9 | location ~ \.php$ { 10 | try_files $uri =404; 11 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 12 | fastcgi_pass fpm:9000; 13 | fastcgi_index index.php; 14 | include fastcgi_params; 15 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 16 | fastcgi_param PATH_INFO $fastcgi_path_info; 17 | } 18 | 19 | location / { 20 | try_files $uri $uri/ /index.php?$query_string; 21 | gzip_static on; 22 | } 23 | 24 | location ~ /\.ht { 25 | deny all; 26 | } 27 | 28 | location ~ /.well-known { 29 | allow all; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "public"] 9 | } 10 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "commit": "git-cz", 7 | "prettier": "prettier --config .prettierrc.json --ignore-path .prettierignore ./resources/js", 8 | "prettier:check": "npm run prettier -- --check", 9 | "prettier:fix": "npm run prettier -- --write", 10 | "lint": "eslint --ext .tsx resources/js", 11 | "lint:fix": "eslint --fix --ext .tsx resources/js", 12 | "test": "jest" 13 | }, 14 | "config": { 15 | "commitizen": { 16 | "path": "@commitlint/cz-commitlint" 17 | } 18 | }, 19 | "dependencies": { 20 | "laravel-echo": "^1.14.0", 21 | "pusher-js": "^7.4.0", 22 | "react-app-polyfill": "^3.0.0" 23 | }, 24 | "peerDependencies": { 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.18.10", 30 | "@babel/plugin-syntax-flow": "^7.18.6", 31 | "@babel/plugin-transform-react-jsx": "^7.18.10", 32 | "@babel/preset-env": "^7.18.10", 33 | "@babel/preset-react": "^7.18.6", 34 | "@babel/preset-typescript": "^7.18.6", 35 | "@commitlint/cli": "^17.0.3", 36 | "@commitlint/config-conventional": "^17.0.3", 37 | "@commitlint/cz-commitlint": "^17.0.3", 38 | "@headlessui/react": "^1.4.2", 39 | "@inertiajs/inertia": "^0.11.0", 40 | "@inertiajs/inertia-react": "^0.8.1", 41 | "@inertiajs/progress": "^0.2.6", 42 | "@tailwindcss/forms": "^0.5.2", 43 | "@testing-library/dom": "^8.17.1", 44 | "@testing-library/jest-dom": "^5.16.5", 45 | "@testing-library/react": "^12.1.5", 46 | "@testing-library/user-event": "^14.4.3", 47 | "@types/jest": "^29.4.0", 48 | "@types/node": "^18.7.6", 49 | "@types/react-dom": "^18.0.6", 50 | "@types/styled-components": "^5.1.26", 51 | "@types/testing-library__jest-dom": "^5.14.5", 52 | "@typescript-eslint/eslint-plugin": "^5.33.1", 53 | "@typescript-eslint/parser": "^5.33.1", 54 | "@vitejs/plugin-react": "^2.0.0", 55 | "autoprefixer": "^10.4.2", 56 | "axios": "^0.27", 57 | "babel-jest": "^29.4.3", 58 | "commitizen": "^4.0.3", 59 | "eslint": "^8.20.0", 60 | "eslint-config-prettier": "^8.5.0", 61 | "eslint-config-react-app": "^7.0.0", 62 | "eslint-import-resolver-alias": "^1.1.2", 63 | "eslint-plugin-import": "^2.26.0", 64 | "eslint-plugin-jest-dom": "^4.0.2", 65 | "eslint-plugin-prettier": "^4.2.1", 66 | "eslint-plugin-react": "^7.30.1", 67 | "eslint-plugin-sonarjs": "^0.15.0", 68 | "eslint-plugin-testing-library": "^5.6.0", 69 | "git-cz": "^4.9.0", 70 | "glob": "^8.0.3", 71 | "husky": "^8.0.1", 72 | "inquirer": "^8.0.0", 73 | "jest": "^29.4.3", 74 | "jest-environment-jsdom": "^29.4.3", 75 | "laravel-vite-plugin": "^0.5.0", 76 | "lodash": "^4.17.19", 77 | "postcss": "^8.4.6", 78 | "prettier": "^2.7.1", 79 | "react": "^17.0.2", 80 | "react-dom": "^17.0.2", 81 | "tailwind-styled-components": "^2.1.8", 82 | "tailwindcss": "^3.1.0", 83 | "ts-jest": "^29.0.5", 84 | "ts-loader": "^9.3.1", 85 | "ts-node": "^10.9.1", 86 | "typescript": "^4.7.4", 87 | "vite": "^3.0.0", 88 | "webpack": "^5.74.0" 89 | }, 90 | "jest": { 91 | "collectCoverage": true, 92 | "collectCoverageFrom": [ 93 | "resources/js/**/*.tsx", 94 | "resources/js/Components/*.tsx", 95 | "!**/node_modules/**", 96 | "!resources/js/Pages/**", 97 | "!resources/js/Layouts/**", 98 | "!resources/js/Interfaces/**", 99 | "!resources/js/app.tsx", 100 | "!resources/js/Echo.tsx" 101 | ], 102 | "coverageThreshold": { 103 | "global": { 104 | "statements": 85, 105 | "branches": 50, 106 | "functions": 85, 107 | "lines": 85 108 | } 109 | }, 110 | "setupFiles": [ 111 | "react-app-polyfill/jsdom" 112 | ], 113 | "setupFilesAfterEnv": [ 114 | "/resources/js/setupTests.ts" 115 | ], 116 | "testEnvironment": "jsdom" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | ./app/Http/Middleware/Authenticate.php 22 | ./app/Http/Middleware/RedirectIfAuthenticated.php 23 | ./app/Http/Middleware/TrustHosts.php 24 | ./app/Providers/BroadcastServiceProvider.php 25 | ./app/Providers/RouteServiceProvider.php 26 | ./app/Http/Controllers/Auth/AuthenticatedSessionController.php 27 | ./app/Http/Controllers/Auth/ConfirmablePasswordController.php 28 | ./app/Http/Controllers/Auth/EmailVerificationNotificationController.php 29 | ./app/Http/Controllers/Auth/EmailVerificationPromptController.php 30 | ./app/Http/Controllers/Auth/NewPasswordController.php 31 | ./app/Http/Controllers/Auth/PasswordResetLinkController.php 32 | ./app/Http/Controllers/Auth/VerifyEmailController.php 33 | ./app/Http/Requests/Auth/LoginRequest.php 34 | ./app/Providers/HorizonServiceProvider.php 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 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 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/horizon/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lockhinator/laravel-react-docker-boilerplate/30e3ecbb6f601196c6a5540af684cd922bff4926/public/vendor/horizon/img/favicon.png -------------------------------------------------------------------------------- /public/vendor/horizon/img/horizon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/vendor/horizon/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=6caa5316e06082f6a22b0687b88b3ab0", 3 | "/app-dark.css": "/app-dark.css?id=ff172044c4efc9f08f12c0eb824b0226", 4 | "/app.css": "/app.css?id=a38514598173eedd6b8575a77bc1ead4", 5 | "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f" 6 | } 7 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/Components/ApplicationLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface ApplicationLogoInterface { 4 | className?: string 5 | } 6 | 7 | export const ApplicationLogo = (props: ApplicationLogoInterface): JSX.Element => ( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /resources/js/Components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface ButtonInterface { 4 | type?: 'button' | 'submit' | 'reset' | undefined, 5 | label?: string, 6 | className?: string, 7 | processing?: boolean, 8 | onClickHandler?: (e) => void, 9 | children?: JSX.Element | string, 10 | } 11 | 12 | export const Button = ({ type = 'submit', className = '', label = 'button', ...props }: ButtonInterface): JSX.Element => ( 13 | 26 | ); 27 | -------------------------------------------------------------------------------- /resources/js/Components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface CheckboxInterface { 4 | name: string; 5 | value: string; 6 | disabled?: boolean; 7 | handleChange: (e) => void; 8 | } 9 | 10 | export const Checkbox = ({ disabled = true, ...props }: CheckboxInterface): JSX.Element => ( 11 | 20 | ); 21 | -------------------------------------------------------------------------------- /resources/js/Components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, Fragment } from 'react'; 2 | import { Link } from '@inertiajs/inertia-react'; 3 | import { Transition } from '@headlessui/react'; 4 | 5 | type DropdownContextType = { 6 | open: boolean; 7 | setOpen: (open: boolean) => void; 8 | toggleOpen: (open: boolean) => void; 9 | } 10 | 11 | let DropDownContext; 12 | 13 | interface DropdownCommonInterface { 14 | children?: JSX.Element | JSX.Element[] | string 15 | } 16 | 17 | const Dropdown = (props: DropdownCommonInterface): JSX.Element => { 18 | 19 | const [open, setOpen] = useState(false); 20 | 21 | const toggleOpen = () => { 22 | setOpen((previousState: boolean) => !previousState); 23 | }; 24 | 25 | DropDownContext = React.createContext({ 26 | open: open, 27 | setOpen: setOpen, 28 | toggleOpen: toggleOpen 29 | }); 30 | 31 | return ( 32 | 33 |
{props.children}
34 |
35 | ); 36 | }; 37 | 38 | const Trigger = (props: DropdownCommonInterface) => { 39 | const { open, setOpen, toggleOpen } = useContext(DropDownContext); 40 | 41 | return ( 42 | <> 43 |
toggleOpen(open)}>{props.children}
44 | 45 | {open &&
setOpen(false)}/>} 46 | 47 | ); 48 | }; 49 | 50 | interface ContentInterface extends DropdownCommonInterface { 51 | align?: string, 52 | width?: string, 53 | contentClasses?: string, 54 | } 55 | const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white', ...props }: ContentInterface) => { 56 | const { open, setOpen } = useContext(DropDownContext); 57 | 58 | let alignmentClasses = 'origin-top'; 59 | 60 | if (align === 'left') { 61 | alignmentClasses = 'origin-top-left left-0'; 62 | } else if (align === 'right') { 63 | alignmentClasses = 'origin-top-right right-0'; 64 | } 65 | 66 | let widthClasses = ''; 67 | 68 | if (width === '48') { 69 | widthClasses = 'w-48'; 70 | } 71 | 72 | return ( 73 | <> 74 | 84 |
setOpen(false)} 87 | > 88 |
{props.children}
89 |
90 |
91 | 92 | ); 93 | }; 94 | 95 | interface DropdownLinkInterface extends DropdownCommonInterface { 96 | href: string, 97 | method?: string, 98 | as?: string, 99 | } 100 | const DropdownLink = ({ href, method = 'post', as = 'a', ...props }: DropdownLinkInterface) => ( 101 | 107 | {props.children} 108 | 109 | ); 110 | 111 | Dropdown.Trigger = Trigger; 112 | Dropdown.Content = Content; 113 | Dropdown.Link = DropdownLink; 114 | 115 | export { Dropdown }; 116 | -------------------------------------------------------------------------------- /resources/js/Components/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, MutableRefObject } from 'react'; 2 | 3 | export interface InputInterface { 4 | type?: string 5 | name: string, 6 | value?: string, 7 | className: string, 8 | autoComplete?: string, 9 | required?: boolean, 10 | isFocused?: boolean, 11 | handleChange: (e) => void, 12 | } 13 | 14 | export const Input = ({type = 'text', required = false, autoComplete = '', isFocused = false, ...props}: InputInterface): JSX.Element => { 15 | const input = useRef() as MutableRefObject; 16 | 17 | useEffect(() => { 18 | if (isFocused) { 19 | input.current?.focus(); 20 | } 21 | }, [isFocused]); 22 | 23 | return ( 24 |
25 | 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /resources/js/Components/InputError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface InputErrorInterface { 4 | message: string, 5 | className?: string 6 | } 7 | 8 | export const InputError = ({ message, className = '' }: InputErrorInterface): JSX.Element => ( 9 | message ?

{message}

: <> 10 | ); 11 | -------------------------------------------------------------------------------- /resources/js/Components/Label.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface LabelInterface { 4 | forInput: string, 5 | value?: string, 6 | className?: string, 7 | children?: JSX.Element, 8 | } 9 | 10 | export const Label = ({ className = '', ...props}: LabelInterface): JSX.Element => ( 11 | 14 | ); 15 | -------------------------------------------------------------------------------- /resources/js/Components/NavLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from '@inertiajs/inertia-react'; 3 | 4 | export interface NavLinkInterface { 5 | href: string, 6 | active: boolean, 7 | children?: JSX.Element | string, 8 | } 9 | 10 | export const NavLink = ({ href, active, children }: NavLinkInterface): JSX.Element => ( 11 | 19 | {children} 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /resources/js/Components/ResponsiveNavLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from '@inertiajs/inertia-react'; 3 | 4 | export interface ResponsiveNavLinkInterface { 5 | method?: string, 6 | as?: string, 7 | href: string, 8 | active?: boolean, 9 | children?: JSX.Element | JSX.Element[] | string, 10 | } 11 | 12 | export const ResponsiveNavLink = ({ method = 'get', as = 'a', active = false, ...props }: ResponsiveNavLinkInterface): JSX.Element => ( 13 | 23 | {props.children} 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /resources/js/Contexts/Socket.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { echo } from '../Echo'; 3 | 4 | export const SocketContext = React.createContext(echo); 5 | -------------------------------------------------------------------------------- /resources/js/Echo.tsx: -------------------------------------------------------------------------------- 1 | import Echo from 'laravel-echo'; 2 | 3 | export const echo = new Echo({ 4 | broadcaster: 'pusher', 5 | key: import.meta.env.VITE_PUSHER_APP_KEY, 6 | wsHost: window.location.hostname, 7 | wsPort: import.meta.env.VITE_PUSHER_PORT, 8 | wssPort: import.meta.env.VITE_PUSHER_PORT, 9 | forceTLS: false, 10 | disableStats: true, 11 | enabledTransports: ['ws', 'wss'] 12 | }); 13 | -------------------------------------------------------------------------------- /resources/js/Interfaces/AuthUserInterface.tsx: -------------------------------------------------------------------------------- 1 | import { UserInterface } from './UserInterface'; 2 | 3 | export interface AuthUserInterface { 4 | user: UserInterface 5 | } 6 | -------------------------------------------------------------------------------- /resources/js/Interfaces/UserInterface.tsx: -------------------------------------------------------------------------------- 1 | export interface UserInterface { 2 | name: string, 3 | email: string, 4 | } 5 | -------------------------------------------------------------------------------- /resources/js/Layouts/Guest.tsx: -------------------------------------------------------------------------------- 1 | import React, { JSX } from 'react'; 2 | import { ApplicationLogo } from '../Components/ApplicationLogo'; 3 | import { Link } from '@inertiajs/inertia-react'; 4 | 5 | interface GuestInterface { 6 | children?: JSX.Element, 7 | } 8 | 9 | export default function Guest(props: GuestInterface): JSX.Element { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 |
19 | {props.children} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ConfirmPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useEffect } from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import Guest from '../../Layouts/Guest'; 4 | import { Input } from '../../Components/Input'; 5 | import { InputError } from '../../Components/InputError'; 6 | import { Label } from '../../Components/Label'; 7 | import { Head, useForm } from '@inertiajs/inertia-react'; 8 | import route from '../../../../vendor/tightenco/ziggy'; 9 | 10 | export default function ConfirmPassword(): JSX.Element { 11 | const { data, setData, post, processing, errors, reset } = useForm({ 12 | password: '', 13 | }); 14 | 15 | useEffect(() => ( 16 | reset('password') 17 | ), [reset]); 18 | 19 | const onHandleChange = (event) => { 20 | setData(event.target.name, event.target.value); 21 | }; 22 | 23 | const submit = (e: FormEvent) => { 24 | e.preventDefault(); 25 | 26 | post(route('password.confirm')); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 |
34 | This is a secure area of the application. Please confirm your password before continuing. 35 |
36 | 37 |
38 |
39 |
52 | 53 |
54 | 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import Guest from '../../Layouts/Guest'; 4 | import { Input } from '../../Components/Input'; 5 | import { InputError } from '../../Components/InputError'; 6 | import { Head, useForm } from '@inertiajs/inertia-react'; 7 | import route from '../../../../vendor/tightenco/ziggy'; 8 | 9 | interface ForgotPasswordInterface { 10 | status?: string 11 | } 12 | 13 | export default function ForgotPassword({ status }: ForgotPasswordInterface): JSX.Element { 14 | const { data, setData, post, processing, errors } = useForm({ 15 | email: '', 16 | }); 17 | 18 | const onHandleChange = (event) => { 19 | setData(event.target.name, event.target.value); 20 | }; 21 | 22 | const submit = (e) => { 23 | e.preventDefault(); 24 | 25 | post(route('password.email')); 26 | }; 27 | 28 | return ( 29 | 30 | 31 | 32 |
33 | Forgot your password? No problem. Just let us know your email address and we will email you a password 34 | reset link that will allow you to choose a new one. 35 |
36 | 37 | {status &&
{status}
} 38 | 39 |
40 | 48 | 49 | 50 | 51 |
52 | 55 |
56 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import { Checkbox } from '../../Components/Checkbox'; 4 | import Guest from '../../Layouts/Guest'; 5 | import { Input } from '../../Components/Input'; 6 | import { InputError } from '../../Components/InputError'; 7 | import { Label } from '../../Components/Label'; 8 | import { Head, Link, useForm } from '@inertiajs/inertia-react'; 9 | import route from '../../../../vendor/tightenco/ziggy'; 10 | 11 | interface LoginInterface { 12 | status?: string, 13 | canResetPassword?: boolean, 14 | } 15 | 16 | export default function Login({ status, canResetPassword }: LoginInterface): JSX.Element { 17 | 18 | const { data, setData, post, processing, errors, reset } = useForm({ 19 | email: '', 20 | password: '', 21 | remember: '', 22 | }); 23 | 24 | const onHandleChange = (event) => { 25 | setData(event.target.name, event.target.type === 'checkbox' ? event.target.checked : event.target.value); 26 | }; 27 | 28 | const submit = (e) => { 29 | e.preventDefault(); 30 | 31 | post(route('login'), { 32 | onSuccess: () => reset('password') 33 | }); 34 | }; 35 | 36 | return ( 37 | 38 | 39 | 40 | {status &&
{status}
} 41 | 42 |
43 |
44 |
58 | 59 |
60 |
73 | 74 |
75 | 80 |
81 | 82 |
83 | {canResetPassword && ( 84 | 88 | Forgot your password? 89 | 90 | )} 91 | 92 | 95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/Register.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import Guest from '../../Layouts/Guest'; 4 | import { Input } from '../../Components/Input'; 5 | import { InputError } from '../../Components/InputError'; 6 | import { Label } from '../../Components/Label'; 7 | import { Head, Link, useForm } from '@inertiajs/inertia-react'; 8 | import route from '../../../../vendor/tightenco/ziggy'; 9 | 10 | export default function Register(): JSX.Element { 11 | 12 | const formOptions = { 13 | name: '', 14 | email: '', 15 | password: '', 16 | password_confirmation: '', 17 | }; 18 | 19 | const { data, setData, post, processing, errors, reset } = useForm(formOptions); 20 | 21 | const onHandleChange = (event) => { 22 | setData(event.target.name, event.target.type === 'checkbox' ? event.target.checked : event.target.value); 23 | }; 24 | 25 | const submit = (e) => { 26 | e.preventDefault(); 27 | 28 | post(route('register'), { 29 | preserveScroll: true, 30 | onSuccess: () => reset('password', 'password_confirmation'), 31 | }); 32 | }; 33 | 34 | return ( 35 | 36 | 37 | 38 |
39 |
40 |
55 | 56 |
57 |
71 | 72 |
73 |
87 | 88 |
89 |
102 | 103 |
104 | 105 | Already registered? 106 | 107 | 108 | 111 |
112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import Guest from '../../Layouts/Guest'; 4 | import { Input } from '../../Components/Input'; 5 | import { InputError } from '../../Components/InputError'; 6 | import { Label } from '../../Components/Label'; 7 | import { Head, useForm } from '@inertiajs/inertia-react'; 8 | import route from '../../../../vendor/tightenco/ziggy'; 9 | 10 | interface ResetPasswordInterface { 11 | token: string, 12 | email: string, 13 | } 14 | 15 | export default function ResetPassword({ token, email }: ResetPasswordInterface): JSX.Element { 16 | const { data, setData, post, processing, errors, reset } = useForm({ 17 | token: token, 18 | email: email, 19 | password: '', 20 | password_confirmation: '', 21 | }); 22 | 23 | useEffect(() => ( 24 | reset('password', 'password_confirmation') 25 | ), [reset]); 26 | 27 | const onHandleChange = (event) => { 28 | setData(event.target.name, event.target.value); 29 | }; 30 | 31 | const submit = (e) => { 32 | e.preventDefault(); 33 | 34 | post(route('password.update')); 35 | }; 36 | 37 | return ( 38 | 39 | 40 | 41 |
42 |
43 |
56 | 57 |
58 |
72 | 73 |
74 |
87 | 88 |
89 | 92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/VerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../../Components/Button'; 3 | import Guest from '../../Layouts/Guest'; 4 | import { Head, Link, useForm } from '@inertiajs/inertia-react'; 5 | import route from '../../../../vendor/tightenco/ziggy'; 6 | 7 | interface VerifyEmailInterface { 8 | status?: string, 9 | } 10 | 11 | export default function VerifyEmail({ status }: VerifyEmailInterface): JSX.Element { 12 | const { post, processing } = useForm(); 13 | 14 | const submit = (e) => { 15 | e.preventDefault(); 16 | 17 | post(route('verification.send')); 18 | }; 19 | 20 | return ( 21 | 22 | 23 | 24 |
25 | Thanks for signing up! Before getting started, could you verify your email address by clicking on the 26 | link we just emailed to you? If you didn't receive the email, we will gladly send you another. 27 |
28 | 29 | {status === 'verification-link-sent' && ( 30 |
31 | A new verification link has been sent to the email address you provided during registration. 32 |
33 | )} 34 | 35 |
36 |
37 | 38 | 39 | 45 | Log Out 46 | 47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/Pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Authenticated from '../Layouts/Authenticated'; 3 | import { Head } from '@inertiajs/inertia-react'; 4 | import { AuthUserInterface } from "../Interfaces/AuthUserInterface"; 5 | 6 | interface DashboardInterface { 7 | auth: AuthUserInterface, 8 | errors?: string[], 9 | } 10 | 11 | export default function Dashboard(props: DashboardInterface): JSX.Element { 12 | return ( 13 | Dashboard} 18 | > 19 | 20 | 21 |
22 |
23 |
24 |
You're logged in!
25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/ApplicationLogo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { ApplicationLogo, ApplicationLogoInterface } from '../../Components/ApplicationLogo'; 4 | 5 | describe('ApplicationLogo', () => { 6 | const mockProps = ( 7 | overrideProps: Partial = {} 8 | ): ApplicationLogoInterface => ({ 9 | ...overrideProps 10 | }); 11 | 12 | it('renders ApplicationLogo', () => { 13 | render(); 14 | 15 | expect(screen.getByTestId('application-logo')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Button, ButtonInterface } from '../../Components/Button'; 4 | 5 | describe('Button', () => { 6 | 7 | const buttonLabel = 'test-button'; 8 | 9 | const mockProps = ( 10 | overrideProps: Partial = {} 11 | ): ButtonInterface => ({ 12 | type: 'button', 13 | label: buttonLabel, 14 | processing: false, 15 | ...overrideProps 16 | }); 17 | 18 | it('renders ApplicationLogo', () => { 19 | render(); 20 | 21 | expect(screen.getByTestId(buttonLabel)).toBeInTheDocument(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/Checkbox.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Checkbox, CheckboxInterface } from '../../Components/Checkbox'; 4 | import userEvent from '@testing-library/user-event'; 5 | 6 | describe('Checkbox', () => { 7 | 8 | const mockProps = ( 9 | overrideProps: Partial = {} 10 | ): CheckboxInterface => ({ 11 | name: 'my-checkbox', 12 | value: 'myvalue', 13 | handleChange: jest.fn(), 14 | disabled: false, 15 | ...overrideProps 16 | }); 17 | 18 | // prevent duplicating this a bunch of times 19 | // use this function to get the element we are testing 20 | const element = (): HTMLElement | SVGElement => screen.getByTestId('test-checkbox'); 21 | 22 | it('renders Checkbox', () => { 23 | render(); 24 | 25 | expect(element()).toBeInTheDocument(); 26 | }); 27 | 28 | it('triggers change handler when clicked', async () => { 29 | const mock = jest.fn(); 30 | const props = mockProps({ 31 | handleChange: mock, 32 | }); 33 | 34 | render(); 35 | 36 | expect(element()).toBeInTheDocument(); 37 | 38 | // test the click to check the box 39 | await userEvent.click(element()); 40 | expect(element()).toBeChecked(); 41 | expect(mock).toHaveBeenCalled(); 42 | 43 | // test the click to uncheck the box 44 | await userEvent.click(element()); 45 | expect(element()).not.toBeChecked(); 46 | expect(mock).toHaveBeenCalledTimes(2); 47 | }); 48 | 49 | it('does not trigger change handler when clicked and disabled', async () => { 50 | const mock = jest.fn(); 51 | const props = mockProps({ 52 | disabled: true, 53 | handleChange: mock, 54 | }); 55 | 56 | render(); 57 | 58 | expect(element()).toBeInTheDocument(); 59 | 60 | // test the click to check the box 61 | await userEvent.click(element()); 62 | expect(element()).not.toBeChecked(); 63 | expect(mock).not.toHaveBeenCalled(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/Dropdown.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Dropdown } from '../../Components/Dropdown'; 5 | 6 | describe('Dropdown', () => { 7 | 8 | const DropDownElement = (): JSX.Element => ( 9 | 10 | 11 | 12 | 26 | 27 | 28 | 29 | 30 | 31 | Log Out 32 | 33 | 34 | 35 | ); 36 | 37 | it('renders Dropdown', () => { 38 | render(); 39 | 40 | expect(screen.getByTestId('test-dropdown-trigger')).toBeInTheDocument(); 41 | expect(screen.queryAllByRole('button', { name: 'Log Out' })).toHaveLength(0); 42 | }); 43 | 44 | it('shows content on Dropdown click', async () => { 45 | render(); 46 | 47 | const trigger = screen.getByTestId('test-dropdown-trigger'); 48 | 49 | expect(trigger).toBeInTheDocument(); 50 | await userEvent.click(trigger); 51 | expect(screen.getByRole('button', { name: 'Log Out' })).toBeInTheDocument(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/Input.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Input, InputInterface } from '../../Components/Input'; 4 | import userEvent from '@testing-library/user-event'; 5 | 6 | describe('Input', () => { 7 | 8 | const mockProps = ( 9 | overrideProps: Partial = {} 10 | ): InputInterface => ({ 11 | type: 'text', 12 | name: 'my-input', 13 | className: '', 14 | autoComplete: '', 15 | required: false, 16 | isFocused: false, 17 | handleChange: jest.fn(), 18 | ...overrideProps 19 | }); 20 | 21 | it('renders Input', () => { 22 | render(); 23 | 24 | expect(screen.getByTestId('test-input')).toBeInTheDocument(); 25 | }); 26 | 27 | it('triggers onChange event and value is updated when entering text', async () => { 28 | const mock = jest.fn(); 29 | const props = mockProps({ 30 | handleChange: mock 31 | }); 32 | render(); 33 | 34 | const input = screen.getByTestId('test-input'); 35 | 36 | expect(input).toBeInTheDocument(); 37 | await userEvent.clear(input); 38 | await userEvent.type(input, 'Hello!'); 39 | expect(mock).toHaveBeenCalledTimes(6); 40 | expect(input).toHaveValue('Hello!'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/InputError.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { InputError, InputErrorInterface } from '../../Components/InputError'; 4 | 5 | describe('InputError', () => { 6 | 7 | const mockProps = ( 8 | overrideProps: Partial = {} 9 | ): InputErrorInterface => ({ 10 | message: 'My custom message', 11 | className: '', 12 | ...overrideProps 13 | }); 14 | 15 | it('renders InputError', () => { 16 | const props = mockProps(); 17 | render(); 18 | 19 | expect(screen.getByText(props.message)).toBeInTheDocument(); 20 | }); 21 | 22 | it('does not render InputError with empty message', () => { 23 | const props = mockProps({ 24 | message: '' 25 | }); 26 | render(); 27 | 28 | expect(screen.queryAllByTestId('test-input-error')).toHaveLength(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /resources/js/Tests/Components/Label.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Label, LabelInterface } from '../../Components/Label'; 4 | 5 | describe('Label', () => { 6 | 7 | const mockProps = ( 8 | overrideProps: Partial = {} 9 | ): LabelInterface => ({ 10 | forInput: 'someinput', 11 | value: 'My Value', 12 | ...overrideProps 13 | }); 14 | 15 | it('renders Label', () => { 16 | render(