diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index f55d1fa8..5b1c7c38 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -20,4 +20,4 @@ jobs: - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 with: - commit_message: Fix styling + commit_message: "fix: apply code styling rules" diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index b184d0b2..28b0602f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [7.4, 8.0] + php: [8.0] laravel: [8.*] stability: [prefer-lowest, prefer-stable] include: diff --git a/.php_cs.dist.php b/.php_cs.dist.php index 3de28fd4..b524e240 100644 --- a/.php_cs.dist.php +++ b/.php_cs.dist.php @@ -3,6 +3,7 @@ $finder = Symfony\Component\Finder\Finder::create() ->in([ __DIR__ . '/src', + __DIR__ . '/stubs', __DIR__ . '/tests', ]) ->name('*.php') diff --git a/README.md b/README.md index 9ad68b71..5a6a1a2b 100644 --- a/README.md +++ b/README.md @@ -21,28 +21,38 @@ Hearth is a simple starter kit for the Laravel framework. It provides a few thin ## Installation -You can install the package via composer: +You may use Composer to install Hearth into your new Laravel project: ```bash composer require fluid-project/hearth ``` -After installing the Hearth package, you can use the `hearth:install` command to +_Note: attempting to install Hearth into an existing Laravel application will result in unexpected behaviour._ + +After installing the Hearth package, you can use the `hearth:install` Artisan command to install the Hearth scaffolding within your Laravel application: ```bash php artisan hearth:install ``` -After installing Hearth, you will need to install and build your NPM dependencies -and run your database migrations: +After installing Hearth, you will need to install and build your NPM dependencies, run your database migrations and link +public storage: ```bash npm install npm run dev php artisan migrate +php artisan storage:link ``` +### Emails + +In order to test emails (for example, using Mailhog with [Laravel Sail](https://laravel.com/docs/8.x/sail#previewing-emails)), +you must update your Laravel application's `.env` file's `MAIL_FROM_ADDRESS` environment variable with a +properly-formatted email address. For local development, this might be `noreply@hearth.test` (assuming your local + application is accessible at `http://hearth.test`). + ## Usage TODO. @@ -73,3 +83,13 @@ Please review [our security policy](../../security/policy) on how to report secu ## License The BSD 3-Clause License. Please see [License File](LICENSE.md) for more information. + +## Third Party Software in Hearth + +Hearth is based on other publicly available software, categorized by license: + +### MIT License + +- [Laravel Breeze](https://github.com/laravel/breeze) +- [Laravel Jetstream](https://github.com/laravel/jetstream) +- [Laravel Package Skeleton](https://github.com/spatie/package-skeleton-laravel) diff --git a/composer.json b/composer.json index 30ebd943..7e9de9c5 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,16 @@ } ], "require": { - "php": "^7.4|^8.0", + "php": "^8.0", "chinleung/laravel-locales": "^1.2", "chinleung/laravel-multilingual-routes": "^2.7", + "commerceguys/intl": "^1.1", "illuminate/contracts": "^8.37", "laravel/fortify": "^1.7", - "spatie/laravel-package-tools": "^1.4.3" + "spatie/laravel-flash": "^1.8", + "spatie/laravel-google-fonts": "^1.0", + "spatie/laravel-package-tools": "^1.9", + "spatie/laravel-sluggable": "^3.1" }, "require-dev": { "brianium/paratest": "^6.2", diff --git a/config/hearth.php b/config/hearth.php index 1e70ae14..d77e8fb4 100644 --- a/config/hearth.php +++ b/config/hearth.php @@ -1,5 +1,6 @@ faker->name; + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'email' => $this->faker->unique()->safeEmail, + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + 'locale' => 'en', + ]; + } +} diff --git a/database/migrations/create_hearth_table.php.stub b/database/migrations/create_hearth_table.php.stub deleted file mode 100644 index 7491b59f..00000000 --- a/database/migrations/create_hearth_table.php.stub +++ /dev/null @@ -1,19 +0,0 @@ -id(); - - // add fields - - $table->timestamps(); - }); - } -}; diff --git a/database/migrations/create_users_table.php.stub b/database/migrations/create_users_table.php.stub new file mode 100644 index 00000000..e6b41747 --- /dev/null +++ b/database/migrations/create_users_table.php.stub @@ -0,0 +1,38 @@ +id(); + $table->string('name'); + $table->string('slug'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->string('locale')->default('en'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('users'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ccdd25d6..da767ac6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,7 +19,7 @@ verbose="true" > - + tests diff --git a/resources/lang/en/alert.php b/resources/lang/en/alert.php new file mode 100644 index 00000000..92d263fc --- /dev/null +++ b/resources/lang/en/alert.php @@ -0,0 +1,8 @@ + 'Error', + 'warning' => 'Warning', + 'success' => 'Success', + 'notice' => 'Notice' +]; diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php new file mode 100644 index 00000000..813cebce --- /dev/null +++ b/resources/lang/en/auth.php @@ -0,0 +1,42 @@ + 'Sign in', + 'sign_out' => 'Sign out', + 'create_account' => 'Create an account', + 'label_password' => 'Password', + 'label_password_confirmation' => 'Confirm password', + 'existing_account_prompt' => 'Do you already have an account?', + 'create_your_account' => 'Create your account', + 'label_current_password' => 'Current password', + 'change_password' => 'Change password', + 'action_confirm' => 'Confirm', + 'password_change_succeeded' => 'Your password has been changed.', + 'label_remember_me' => 'Remember me', + 'error_intro' => 'Sorry, something went wrong.', + 'failed' => 'These credentials do not match our records.', + 'wrong_password' => 'The provided password is incorrect.', + 'throttle' => 'Too many log in attempts. Please try again in :seconds seconds.', + 'confirm_intro' => 'For your security, please confirm your password before continuing.', + 'forget_prompt' => 'Forgot your password?', + 'forgot_intro' => 'Forgot your password? Enter your email address and we will email you a password reset link that will let you choose a new one.', + 'forgot_submit' => 'Send password reset link', + 'reset_submit' => 'Reset password', + 'verification_required' => 'Email verification required', + 'verification_intro' => 'Please verify your email address by clicking on the link we emailed to you. If you didn’t receive the email, we will gladly send you another.', + 'verification_sent' => 'A new verification link has been sent to the email address you provided.', + 'resend_verification_email' => 'Resend verification email', + 'verification_succeeded' => 'Your email address has been verified.' +]; diff --git a/resources/lang/en/dashboard.php b/resources/lang/en/dashboard.php new file mode 100644 index 00000000..0c9545f5 --- /dev/null +++ b/resources/lang/en/dashboard.php @@ -0,0 +1,6 @@ + 'Dashboard', + 'welcome' => 'Welcome, :name!' +]; diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php new file mode 100644 index 00000000..6d75dd8b --- /dev/null +++ b/resources/lang/en/errors.php @@ -0,0 +1,31 @@ + 'Not authorized', + 'error_401_message' => 'You are not authorized to visit this page. You may need to log in.', + 'error_403_title' => 'Access forbidden', + 'error_403_message' => 'You do not have permission to visit this page.', + 'error_404_title' => 'Not found', + 'error_404_message' => 'The page you are trying to visit could not be found.', + 'error_419_title' => 'Page expired', + 'error_419_message' => 'The page has expired. Please try again.', + 'error_429_title' => 'Too many requests', + 'error_429_message' => 'You have sent too many requests to this page. Please wait a few moments and try again.', + 'error_500_title' => 'Server error', + 'error_500_message' => 'Sorry, it looks like something isn’t working properly on our end.', + 'error_503_title' => 'Service unavailable', + 'error_503_message' => 'Sorry, it looks like the website can’t respond to your request right now. Please wait a few moments and try again.', + 'return_home' => 'Return to home page', +]; diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php new file mode 100644 index 00000000..29592f83 --- /dev/null +++ b/resources/lang/en/forms.php @@ -0,0 +1,8 @@ + 'Save changes', + 'label_email' => 'Email address', + 'errors_found' => 'Errors found', + 'errors_found_message' => 'Sorry, some errors were found in your submission. Please correct these errors and try again.' +]; diff --git a/resources/lang/en/mail.php b/resources/lang/en/mail.php new file mode 100644 index 00000000..050d0d27 --- /dev/null +++ b/resources/lang/en/mail.php @@ -0,0 +1,8 @@ + 'Sorry!', + 'greeting' => 'Hello!', + 'salutation' => 'Regards', + 'link_guidance' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:' +]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php new file mode 100644 index 00000000..2345a56b --- /dev/null +++ b/resources/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => "We can't find a user with that email address.", + +]; diff --git a/resources/lang/en/routes.php b/resources/lang/en/routes.php new file mode 100644 index 00000000..ed44e4c7 --- /dev/null +++ b/resources/lang/en/routes.php @@ -0,0 +1,9 @@ + 'dashboard', + 'logout' => 'logout', + 'login' => 'login', + 'register' => 'register', + 'verification.verify' => '/verify-email/{id}/{hash}' +]; diff --git a/resources/lang/en/user.php b/resources/lang/en/user.php new file mode 100644 index 00000000..582612b9 --- /dev/null +++ b/resources/lang/en/user.php @@ -0,0 +1,13 @@ + 'Settings', + 'account' => 'Account', + 'label_name' => 'Full name', + 'label_locale' => 'Preferred language', + 'delete_account' => 'Delete account', + 'delete_account_intro' => 'Your account will be deleted and cannot be recovered. If you still want to delete your account, please enter your current password to proceed.', + 'action_delete_account' => 'Delete my account', + 'settings_update_succeeded' => 'Your settings have been saved.', + 'destroy_succeeded' => 'Your account has been deleted.', +]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php new file mode 100644 index 00000000..a664bdf1 --- /dev/null +++ b/resources/lang/en/validation.php @@ -0,0 +1,162 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'multiple_of' => 'The :attribute must be a multiple of :value.', + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'password' => [ + 'length-uppercase' => 'The :attribute must be at least :length characters and contain at least one uppercase character.', + 'length-numeric' => 'The :attribute must be at least :length characters and contain at least one number.', + 'length-specialcharacter' => 'The :attribute must be at least :length characters and contain at least one special character.', + 'length-uppercase-numeric' => 'The :attribute must be at least :length characters and contain at least one uppercase character and one number.', + 'length-uppercase-specialcharacter' => 'The :attribute must be at least :length characters and contain at least one uppercase character and one special character.', + 'length-uppercase-numeric-specialcharacter' => 'The :attribute must be at least :length characters and contain at least one uppercase character, one number, and one special character.', + 'length-numeric-specialcharacter' => 'The :attribute must be at least :length characters and contain at least one special character and one number.', + 'length' => 'The :attribute must be at least :length characters.' + ], + 'organization_user' => [ + 'not_last_admin' => 'There must be at least one administrator in this organization.' + ] + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/lang/en/welcome.php b/resources/lang/en/welcome.php new file mode 100644 index 00000000..75c08436 --- /dev/null +++ b/resources/lang/en/welcome.php @@ -0,0 +1,6 @@ + 'Welcome to Hearth!', + 'details' => 'Make yourself at home.', +]; diff --git a/resources/lang/fr/alert.php b/resources/lang/fr/alert.php new file mode 100644 index 00000000..0b67a5fe --- /dev/null +++ b/resources/lang/fr/alert.php @@ -0,0 +1,3 @@ + +

{{ $title }}

+ {{ $slot }} + diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php new file mode 100644 index 00000000..79f8bc6a --- /dev/null +++ b/resources/views/components/button.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/input.blade.php b/resources/views/components/input.blade.php new file mode 100644 index 00000000..008b41b0 --- /dev/null +++ b/resources/views/components/input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge([]) !!}> diff --git a/resources/views/components/label.blade.php b/resources/views/components/label.blade.php new file mode 100644 index 00000000..da88379e --- /dev/null +++ b/resources/views/components/label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/resources/views/components/language-switcher.blade.php b/resources/views/components/language-switcher.blade.php new file mode 100644 index 00000000..f4383aec --- /dev/null +++ b/resources/views/components/language-switcher.blade.php @@ -0,0 +1,17 @@ +
+ + + {{ $locales[locale()] }} + + + + @foreach ($locales as $key => $locale ) +

+ + {{ $locale }} + +

+ @endforeach +
+
+
diff --git a/resources/views/components/locale-select.blade.php b/resources/views/components/locale-select.blade.php new file mode 100644 index 00000000..bb125e2a --- /dev/null +++ b/resources/views/components/locale-select.blade.php @@ -0,0 +1,10 @@ + diff --git a/resources/views/components/select.blade.php b/resources/views/components/select.blade.php new file mode 100644 index 00000000..4e92247b --- /dev/null +++ b/resources/views/components/select.blade.php @@ -0,0 +1,10 @@ + diff --git a/src/Commands/HearthCommand.php b/src/Commands/HearthCommand.php index 3f195e0a..79b2b048 100644 --- a/src/Commands/HearthCommand.php +++ b/src/Commands/HearthCommand.php @@ -3,15 +3,273 @@ namespace Hearth\Commands; use Illuminate\Console\Command; +use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Str; class HearthCommand extends Command { - public $signature = 'hearth'; + public $signature = 'hearth:install'; - public $description = 'My command'; + public $description = 'Install Hearth.'; public function handle() { - $this->comment('All done'); + // Publish vendor files... + $this->callSilent('vendor:publish', ['--provider' => 'Laravel\Fortify\FortifyServiceProvider']); + $this->callSilent('vendor:publish', [ + '--provider' => 'ChinLeung\LaravelLocales\LaravelLocalesServiceProvider', + '--tag' => 'config', + ]); + $this->callSilent('vendor:publish', [ + '--provider' => 'Spatie\GoogleFonts\GoogleFontsServiceProvider', + '--tag' => 'google-fonts-config', + ]); + + // Install NPM packages... + $this->updateNodePackages(function ($packages) { + return [ + '@accessibility-in-action/looseleaf' => '^1.3', + 'alpinejs' => '^3.0', + 'modern-css-reset' => '^1.4', + ] + $packages; + }); + + $this->updateNodePackages(function ($packages) { + return [ + 'resolve-url-loader' => '^4.0.0', + 'sass' => '^1.35', + 'sass-loader' => '^12.1', + ] + $packages; + }, true); + + // Name... + $this->replaceInFile('APP_NAME=Laravel', 'APP_NAME=Hearth', base_path('.env')); + $this->replaceInFile('APP_NAME=Laravel', 'APP_NAME=Hearth', base_path('.env.example')); + + // AuthenticateSession Middleware... + $this->replaceInFile( + '// \Illuminate\Session\Middleware\AuthenticateSession::class,', + "\Illuminate\Session\Middleware\AuthenticateSession::class,", + app_path('Http/Kernel.php') + ); + + // DetectRequestLocale Middleware... + $this->installMiddlewareAfter('VerifyCsrfToken::class', '\ChinLeung\MultilingualRoutes\DetectRequestLocale::class'); + + // RedirectToPreferredLocale Middleware... + $this->replaceInFile( + "'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,", + "'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'localize' => \App\Http\Middleware\RedirectToPreferredLocale::class,", + app_path('Http/Kernel.php') + ); + + // FortifyServiceProvider... + $this->installServiceProviderAfter('RouteServiceProvider', 'FortifyServiceProvider'); + + // Ensure folders are in place... + (new Filesystem())->ensureDirectoryExists(app_path('Actions/Fortify')); + (new Filesystem())->ensureDirectoryExists(app_path('Http/Requests')); + (new Filesystem())->ensureDirectoryExists(app_path('Http/Requests/Auth')); + (new Filesystem())->ensureDirectoryExists(app_path('Http/Responses')); + (new Filesystem())->ensureDirectoryExists(app_path('Policies')); + (new Filesystem())->ensureDirectoryExists(app_path('Rules')); + (new Filesystem())->ensureDirectoryExists(app_path('View/Components')); + + // App stubs... + $app_stubs = [ + 'Actions/Fortify/CreateNewUser.php', + 'Actions/Fortify/PasswordValidationRules.php', + 'Actions/Fortify/UpdateUserPassword.php', + 'Actions/Fortify/UpdateUserProfileInformation.php', + 'Http/Controllers/UserController.php', + 'Http/Controllers/VerifyEmailController.php', + 'Http/Middleware/Authenticate.php', + 'Http/Middleware/RedirectIfAuthenticated.php', + 'Http/Middleware/RedirectToPreferredLocale.php', + 'Http/Requests/Auth/LoginRequest.php', + 'Http/Requests/DestroyUserRequest.php', + 'Http/Responses/LoginResponse.php', + 'Http/Responses/PasswordResetResponse.php', + 'Http/Responses/RegisterResponse.php', + 'Http/Responses/TwoFactorLoginResponse.php', + 'Models/User.php', + 'Policies/UserPolicy.php', + 'Providers/FortifyServiceProvider.php', + 'Rules/Password.php', + ]; + + foreach ($app_stubs as $path) { + copy(__DIR__ . "/../../stubs/app/{$path}", app_path($path)); + } + + // Config stubs... + $config_stubs = [ + 'fortify.php', + 'laravel-multilingual-routes.php', + 'locales.php', + ]; + + foreach ($config_stubs as $config) { + copy(__DIR__ . "/../../stubs/config/{$config}", base_path("config/{$config}")); + } + + // Route stubs... + $route_stubs = [ + 'fortify.php', + 'web.php', + ]; + + foreach ($route_stubs as $route) { + copy(__DIR__ . "/../../stubs/routes/{$route}", base_path("routes/{$route}")); + } + + // Factories... + $factories = [ + 'UserFactory.php', + ]; + + foreach ($factories as $factory) { + copy(__DIR__ . "/../../database/factories/{$factory}", base_path("database/factories/{$factory}")); + } + + // Views... + (new Filesystem())->copyDirectory(__DIR__.'/../../stubs/resources/views/', resource_path('views')); + + // View components... + $components = [ + 'AppLayout.php', + 'GuestLayout.php', + ]; + + foreach ($components as $component) { + copy(__DIR__ . "/../../stubs/app/View/Components/{$component}", app_path("View/Components/{$component}")); + } + + // Fonts... + $this->replaceInFile( + 'https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,700;1,400;1,700', + 'https://fonts.googleapis.com/css2?family=Comic+Neue:ital,wght@0,400;0,700;1,400;1,700&display=swap', + config_path('google-fonts.php') + ); + + // Mix configuration... + copy(__DIR__ . '/../../stubs/webpack.mix.js', base_path('webpack.mix.js')); + + // Assets... + (new Filesystem())->delete(resource_path('css/app.css')); + (new Filesystem())->copyDirectory(__DIR__.'/../../stubs/resources/css/', resource_path('css')); + (new Filesystem())->copyDirectory(__DIR__.'/../../stubs/resources/js/', resource_path('js')); + + + $this->line(''); + $this->info('Hearth scaffolding installed successfully.'); + $this->comment('Please execute "npm install && npm run dev" to build your assets.'); + } + + /** + * Install the service provider in the application configuration file. + * + * @param string $after + * @param string $name + * @return void + */ + protected function installServiceProviderAfter($after, $name) + { + if (! Str::contains($appConfig = file_get_contents(config_path('app.php')), 'App\\Providers\\'.$name.'::class')) { + file_put_contents(config_path('app.php'), str_replace( + 'App\\Providers\\'.$after.'::class,', + 'App\\Providers\\'.$after.'::class,'.PHP_EOL.' App\\Providers\\'.$name.'::class,', + $appConfig + )); + } + } + + /** + * Install the middleware to a group in the application Http Kernel. + * + * @param string $after + * @param string $name + * @param string $group + * @return void + */ + protected function installMiddlewareAfter($after, $name, $group = 'web') + { + $httpKernel = file_get_contents(app_path('Http/Kernel.php')); + + $middlewareGroups = Str::before(Str::after($httpKernel, '$middlewareGroups = ['), '];'); + $middlewareGroup = Str::before(Str::after($middlewareGroups, "'$group' => ["), '],'); + + if (! Str::contains($middlewareGroup, $name)) { + $modifiedMiddlewareGroup = str_replace( + $after.',', + $after.','.PHP_EOL.' '.$name.',', + $middlewareGroup, + ); + + file_put_contents(app_path('Http/Kernel.php'), str_replace( + $middlewareGroups, + str_replace($middlewareGroup, $modifiedMiddlewareGroup, $middlewareGroups), + $httpKernel + )); + } + } + + /** + * Update the "package.json" file. + * + * @param callable $callback + * @param bool $dev + * @return void + */ + protected static function updateNodePackages(callable $callback, $dev = true) + { + if (! file_exists(base_path('package.json'))) { + return; + } + + $configurationKey = $dev ? 'devDependencies' : 'dependencies'; + + $packages = json_decode(file_get_contents(base_path('package.json')), true); + + $packages[$configurationKey] = $callback( + array_key_exists($configurationKey, $packages) ? $packages[$configurationKey] : [], + $configurationKey + ); + + ksort($packages[$configurationKey]); + + file_put_contents( + base_path('package.json'), + json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL + ); + } + + /** + * Delete the "node_modules" directory and remove the associated lock files. + * + * @return void + */ + protected static function flushNodeModules() + { + tap(new Filesystem(), function ($files) { + $files->deleteDirectory(base_path('node_modules')); + + $files->delete(base_path('yarn.lock')); + $files->delete(base_path('package-lock.json')); + }); + } + + /** + * Replace a given string within a given file. + * + * @param string $search + * @param string $replace + * @param string $path + * @return void + */ + protected function replaceInFile($search, $replace, $path) + { + file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); } } diff --git a/src/HearthServiceProvider.php b/src/HearthServiceProvider.php index b81e0615..feed4c97 100644 --- a/src/HearthServiceProvider.php +++ b/src/HearthServiceProvider.php @@ -3,23 +3,101 @@ namespace Hearth; use Hearth\Commands\HearthCommand; +use Hearth\View\Components\Alert; +use Hearth\View\Components\LanguageSwitcher; +use Hearth\View\Components\LocaleSelect; +use Hearth\View\Components\Select; +use Illuminate\Support\Facades\Blade; +use Illuminate\View\Compilers\BladeCompiler; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; class HearthServiceProvider extends PackageServiceProvider { + /** + * Configure the PackageServiceProvider. + * + * @see https://github.com/spatie/laravel-package-tools + * + * @param \Spatie\LaravelPackageTools\Package $package + * + * @return void + */ public function configurePackage(Package $package): void { - /* - * This class is a Package Service Provider - * - * More info: https://github.com/spatie/laravel-package-tools - */ $package ->name('hearth') ->hasConfigFile() ->hasViews() - ->hasMigration('create_hearth_table') + ->hasTranslations() ->hasCommand(HearthCommand::class); } + + /** + * Custom logic which should be run at the start of the boot method of PackageServiceProvider + * + * @see https://github.com/spatie/laravel-package-tools#using-lifecycle-hooks + * + * @return void + */ + public function bootingPackage() + { + $this->configureComponents(); + } + + /** + * Custom logic which should be run at the end of the boot method of PackageServiceProvider + * + * @see https://github.com/spatie/laravel-package-tools#using-lifecycle-hooks + * + * @return void + */ + public function packageBooted() + { + if (! $this->app->runningInConsole()) { + return; + } + + $this->publishes([ + __DIR__ . '/../database/migrations/create_users_table.php.stub' => + database_path('migrations/2014_10_12_000000_create_users_table.php'), + ], 'hearth-migrations'); + } + + /** + * Configure the Hearth Blade components. + * + * @return void + */ + protected function configureComponents() + { + $this->callAfterResolving(BladeCompiler::class, function () { + $this->registerComponent('alert'); + $this->registerComponent('button'); + $this->registerComponent('input'); + $this->registerComponent('label'); + $this->registerComponent('language-switcher'); + $this->registerComponent('locale-select'); + $this->registerComponent('select'); + }); + + $this->loadViewComponentsAs('hearth', [ + Alert::class, + LanguageSwitcher::class, + LocaleSelect::class, + Select::class, + ]); + } + + /** + * Register the given component. + * + * @param string $component + * + * @return void + */ + protected function registerComponent(string $component) + { + Blade::component('hearth::components.' . $component, 'hearth-' . $component); + } } diff --git a/src/View/Components/Alert.php b/src/View/Components/Alert.php new file mode 100644 index 00000000..1d42b5fe --- /dev/null +++ b/src/View/Components/Alert.php @@ -0,0 +1,45 @@ +type = $type; + + $this->title = $title ? $title : __("hearth::alert." . $type); + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View + */ + public function render() + { + return View::make('hearth::components.alert'); + } +} diff --git a/src/View/Components/LanguageSwitcher.php b/src/View/Components/LanguageSwitcher.php new file mode 100644 index 00000000..44421cfc --- /dev/null +++ b/src/View/Components/LanguageSwitcher.php @@ -0,0 +1,56 @@ +locales = []; + + foreach ($locales as $locale) { + $this->locales[$locale] = $languages->get($locale, $locale)->getName(); + } + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|string + */ + public function render() + { + return View::make('hearth::components.language-switcher'); + } +} diff --git a/src/View/Components/LocaleSelect.php b/src/View/Components/LocaleSelect.php new file mode 100644 index 00000000..d2fafb26 --- /dev/null +++ b/src/View/Components/LocaleSelect.php @@ -0,0 +1,57 @@ +locales = []; + + foreach ($locales as $locale) { + $this->locales[$locale] = $languages->get($locale, $locale)->getName(); + } + + $this->selected = $selected; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View + */ + public function render() + { + return View::make('hearth::components.locale-select'); + } +} diff --git a/src/View/Components/Select.php b/src/View/Components/Select.php new file mode 100644 index 00000000..0c06e70f --- /dev/null +++ b/src/View/Components/Select.php @@ -0,0 +1,44 @@ +options = $options; + $this->selected = $selected; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View + */ + public function render() + { + return View::make('hearth::components.select'); + } +} diff --git a/stubs/app/Actions/Fortify/CreateNewUser.php b/stubs/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 00000000..13c82f33 --- /dev/null +++ b/stubs/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,50 @@ + ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => $this->passwordRules(), + ], + [ + + ] + )->validate(); + + Cookie::queue('locale', locale()); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + } +} diff --git a/stubs/app/Actions/Fortify/PasswordValidationRules.php b/stubs/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 00000000..6b907a66 --- /dev/null +++ b/stubs/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ + $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/stubs/app/Actions/Fortify/UpdateUserPassword.php b/stubs/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 00000000..4605ee2f --- /dev/null +++ b/stubs/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,38 @@ + ['required', 'string'], + 'password' => $this->passwordRules(), + ])->after(function ($validator) use ($user, $input) { + if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) { + $validator->errors()->add( + 'current_password', + __('hearth::auth.wrong_password') + ); + } + })->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php b/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 00000000..98610ee5 --- /dev/null +++ b/stubs/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,68 @@ + ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + 'locale' => ['required', Rule::in(config('locales.supported', ['en', 'fr']))], + ])->validateWithBag('updateProfileInformation'); + + if ( + $input['email'] !== $user->email && + $user instanceof MustVerifyEmail + ) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'locale' => $input['locale'], + ])->save(); + } + + Cookie::queue('locale', $input['locale']); + } + + /** + * Update the given verified user's profile information. + * + * @param mixed $user + * @param array $input + * @return void + */ + protected function updateVerifiedUser($user, array $input) + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + 'locale' => $input['locale'], + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/stubs/app/Http/Controllers/UserController.php b/stubs/app/Http/Controllers/UserController.php new file mode 100644 index 00000000..e523a316 --- /dev/null +++ b/stubs/app/Http/Controllers/UserController.php @@ -0,0 +1,61 @@ +middleware('localize')->only('edit'); + } + + /** + * Show the profile edit view for the logged-in user. + * + * @return \Illuminate\View\View + */ + public function edit() + { + return view('users.edit', [ + 'user' => Auth::user(), + ]); + } + + /** + * Show the account admin view for the logged-in user. + * + * @return \Illuminate\View\View + */ + public function admin() + { + return view('users.admin', ['user' => Auth::user()]); + } + + /** + * Destroy a given user. + * + * @param \App\Http\Requests\DestroyUserRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(DestroyUserRequest $request) + { + $user = $request->user(); + + Auth::guard('web')->logout(); + + $user->delete(); + + flash(__('hearth::user.destroy_succeeded'), 'success'); + + return redirect(localized_route('welcome')); + } +} diff --git a/stubs/app/Http/Controllers/VerifyEmailController.php b/stubs/app/Http/Controllers/VerifyEmailController.php new file mode 100644 index 00000000..dfedd373 --- /dev/null +++ b/stubs/app/Http/Controllers/VerifyEmailController.php @@ -0,0 +1,33 @@ + 1], $request->user()->locale); + + if ($request->user()->hasVerifiedEmail()) { + return redirect()->intended($dashboard); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + flash(__('hearth::auth.verification_succeeded'), 'success'); + + return redirect()->intended($dashboard); + } +} diff --git a/stubs/app/Http/Middleware/Authenticate.php b/stubs/app/Http/Middleware/Authenticate.php new file mode 100644 index 00000000..207c18fb --- /dev/null +++ b/stubs/app/Http/Middleware/Authenticate.php @@ -0,0 +1,23 @@ +expectsJson()) { + return localized_route('login'); + } + + return null; + } +} diff --git a/stubs/app/Http/Middleware/RedirectIfAuthenticated.php b/stubs/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 00000000..dcc6ca46 --- /dev/null +++ b/stubs/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,31 @@ +check()) { + return redirect(localized_route('dashboard')); + } + } + + return $next($request); + } +} diff --git a/stubs/app/Http/Middleware/RedirectToPreferredLocale.php b/stubs/app/Http/Middleware/RedirectToPreferredLocale.php new file mode 100644 index 00000000..9ef9a75a --- /dev/null +++ b/stubs/app/Http/Middleware/RedirectToPreferredLocale.php @@ -0,0 +1,31 @@ +cookie('locale'); + + if ( + $locale + && in_array($locale, locales()) + && $locale !== $request->segment(1) + ) { + return redirect(current_route($locale)); + } + + return $next($request); + } +} diff --git a/stubs/app/Http/Requests/Auth/LoginRequest.php b/stubs/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 00000000..77967e5f --- /dev/null +++ b/stubs/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,93 @@ + 'required|string|email', + 'password' => 'required|string', + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate() + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->filled('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('hearth::auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @return void + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited() + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + * + * @return string + */ + public function throttleKey() + { + return Str::lower($this->input('email')) . '|' . $this->ip(); + } +} diff --git a/stubs/app/Http/Requests/DestroyUserRequest.php b/stubs/app/Http/Requests/DestroyUserRequest.php new file mode 100644 index 00000000..2645de46 --- /dev/null +++ b/stubs/app/Http/Requests/DestroyUserRequest.php @@ -0,0 +1,51 @@ + 'required|string', + ]; + } + + /** + * Configure the validator instance. + * + * @param \Illuminate\Validation\Validator $validator + * @return void + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (! Hash::check($this->current_password, $this->user()->password)) { + $validator->errors()->add( + 'current_password', + __('hearth::auth.wrong_password') + ); + } + })->validateWithBag('destroyAccount'); + + return; + } +} diff --git a/stubs/app/Http/Responses/LoginResponse.php b/stubs/app/Http/Responses/LoginResponse.php new file mode 100644 index 00000000..7362d56c --- /dev/null +++ b/stubs/app/Http/Responses/LoginResponse.php @@ -0,0 +1,26 @@ +locale); + + if ($request->wantsJson()) { + return response()->json(['two_factor' => false]); + } + + return redirect()->intended($dashboard); + } +} diff --git a/stubs/app/Http/Responses/PasswordResetResponse.php b/stubs/app/Http/Responses/PasswordResetResponse.php new file mode 100644 index 00000000..43970927 --- /dev/null +++ b/stubs/app/Http/Responses/PasswordResetResponse.php @@ -0,0 +1,23 @@ +wantsJson()) { + return response()->json(['two_factor' => false]); + } + + return redirect()->intended(localized_route('login')); + } +} diff --git a/stubs/app/Http/Responses/RegisterResponse.php b/stubs/app/Http/Responses/RegisterResponse.php new file mode 100644 index 00000000..68f7be0f --- /dev/null +++ b/stubs/app/Http/Responses/RegisterResponse.php @@ -0,0 +1,26 @@ +locale); + + if ($request->wantsJson()) { + return response()->json(['two_factor' => false]); + } + + return redirect()->intended($dashboard); + } +} diff --git a/stubs/app/Http/Responses/TwoFactorLoginResponse.php b/stubs/app/Http/Responses/TwoFactorLoginResponse.php new file mode 100644 index 00000000..eb4c5c05 --- /dev/null +++ b/stubs/app/Http/Responses/TwoFactorLoginResponse.php @@ -0,0 +1,26 @@ +locale); + + if ($request->wantsJson()) { + return response('', 204); + } + + return redirect()->intended($dashboard); + } +} diff --git a/stubs/app/Models/User.php b/stubs/app/Models/User.php new file mode 100644 index 00000000..cfdf78ff --- /dev/null +++ b/stubs/app/Models/User.php @@ -0,0 +1,79 @@ + 'datetime', + ]; + + /** + * Get the options for generating the slug. + */ + public function getSlugOptions(): SlugOptions + { + return SlugOptions::create() + ->generateSlugsFrom('name') + ->saveSlugsTo('slug'); + } + + /** + * Get the route key for the model. + * + * @return string + */ + public function getRouteKeyName() + { + return 'slug'; + } + + /** + * Get the user's preferred locale. + * + * @return string + */ + public function preferredLocale() + { + return $this->locale; + } +} diff --git a/stubs/app/Policies/UserPolicy.php b/stubs/app/Policies/UserPolicy.php new file mode 100644 index 00000000..e769a810 --- /dev/null +++ b/stubs/app/Policies/UserPolicy.php @@ -0,0 +1,98 @@ +id === $model->id + ? Response::allow() + : Response::deny('You cannot edit this profile.'); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $user + * @param \App\Models\User $model + * @return mixed + */ + public function delete(User $user, User $model) + { + return $user->id === $model->id + ? Response::allow() + : Response::deny('You cannot delete this account.'); + } + + /** + * Determine whether the user can restore the model. + * + * @param \App\Models\User $user + * @param \App\Models\User $model + * @return mixed + */ + public function restore(User $user, User $model) + { + // + } + + /** + * Determine whether the user can permanently delete the model. + * + * @param \App\Models\User $user + * @param \App\Models\User $model + * @return mixed + */ + public function forceDelete(User $user, User $model) + { + // + } +} diff --git a/stubs/app/Providers/FortifyServiceProvider.php b/stubs/app/Providers/FortifyServiceProvider.php new file mode 100644 index 00000000..b66956e5 --- /dev/null +++ b/stubs/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,87 @@ +registerResponseBindings(); + + Fortify::ignoreRoutes(); + } + + /** + * Bootstrap any application services. + * + * @return void + */ + public function boot() + { + Fortify::createUsersUsing(CreateNewUser::class); + Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); + Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + + RateLimiter::for('login', function (Request $request) { + return Limit::perMinute(5)->by($request->email . $request->ip()); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + + Fortify::loginView(fn () => view('auth.login')); + Fortify::registerView(fn () => view('auth.register')); + Fortify::requestPasswordResetLinkView(fn () => view('auth.forgot-password')); + Fortify::resetPasswordView(fn () => view('auth.reset-password')); + Fortify::confirmPasswordView(fn () => view('auth.confirm-password')); + Fortify::verifyEmailView(fn () => view('auth.verify-email')); + } + + /** + * Register response bindings. + */ + protected function registerResponseBindings() + { + $this->app->singleton( + LoginResponseContract::class, + LoginResponse::class + ); + $this->app->singleton( + TwoFactorLoginResponseContract::class, + TwoFactorLoginResponse::class + ); + $this->app->singleton( + RegisterResponseContract::class, + RegisterResponse::class + ); + $this->app->singleton( + PasswordResetResponseContract::class, + PasswordResetResponse::class + ); + } +} diff --git a/stubs/app/Rules/Password.php b/stubs/app/Rules/Password.php new file mode 100644 index 00000000..5a8037ff --- /dev/null +++ b/stubs/app/Rules/Password.php @@ -0,0 +1,76 @@ +message) { + return $this->message; + } + + switch (true) { + case $this->requireUppercase + && ! $this->requireNumeric + && ! $this->requireSpecialCharacter: + return __('hearth::validation.custom.password.length-uppercase', [ + 'length' => $this->length, + ]); + + case $this->requireNumeric + && ! $this->requireUppercase + && ! $this->requireSpecialCharacter: + return __('hearth::validation.custom.password.length-numeric', [ + 'length' => $this->length, + ]); + + case $this->requireSpecialCharacter + && ! $this->requireUppercase + && ! $this->requireNumeric: + return __('hearth::validation.custom.password.length-specialcharacter', [ + 'length' => $this->length, + ]); + + case $this->requireUppercase + && $this->requireNumeric + && ! $this->requireSpecialCharacter: + return __('hearth::validation.custom.password.length-uppercase-numeric', [ + 'length' => $this->length, + ]); + + case $this->requireUppercase + && $this->requireSpecialCharacter + && ! $this->requireNumeric: + return __('hearth::validation.custom.password.length-uppercase-specialcharacter', [ + 'length' => $this->length, + ]); + + case $this->requireUppercase + && $this->requireNumeric + && $this->requireSpecialCharacter: + return __('hearth::validation.custom.password.length-uppercase-numeric-specialcharacter', [ + 'length' => $this->length, + ]); + + case $this->requireNumeric + && $this->requireSpecialCharacter + && ! $this->requireUppercase: + return __('hearth::validation.custom.password.length-numeric-specialcharacter', [ + 'length' => $this->length, + ]); + + default: + return __('hearth::validation.custom.password.length', [ + 'length' => $this->length, + ]); + } + } +} diff --git a/stubs/app/View/Components/AppLayout.php b/stubs/app/View/Components/AppLayout.php new file mode 100644 index 00000000..b45d3425 --- /dev/null +++ b/stubs/app/View/Components/AppLayout.php @@ -0,0 +1,18 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => RouteServiceProvider::HOME, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + // Features::twoFactorAuthentication([ + // 'confirmPassword' => true, + // ]), + ], + +]; diff --git a/stubs/config/laravel-multilingual-routes.php b/stubs/config/laravel-multilingual-routes.php new file mode 100644 index 00000000..67dc4c2b --- /dev/null +++ b/stubs/config/laravel-multilingual-routes.php @@ -0,0 +1,50 @@ + env('MULTILINGUAL_ROUTES_DEFAULT_LOCALE', config('app.fallback_locale')), + + /* + |-------------------------------------------------------------------------- + | Prefix Configuration + |-------------------------------------------------------------------------- + | + | The configuration option that defines if the routes of the default + | locale should be prefixed. + | + */ + + 'prefix_default' => env('MULTILINGUAL_ROUTES_PREFIX_DEFAULT', true), + + /* + |-------------------------------------------------------------------------- + | Home Prefix Configuration + |-------------------------------------------------------------------------- + | + | The configuration option defines if the home route of the default locale + | should be prefixed. + | + */ + + 'prefix_default_home' => env('MULTILINGUAL_ROUTES_PREFIX_DEFAULT_HOME', true), + + /* + |-------------------------------------------------------------------------- + | Name Prefix Configuration + |-------------------------------------------------------------------------- + | + | The configuration option that defines if the route name prefix should + | be before the locale. + | + */ + + 'name_prefix_before_locale' => env('MULTILINGUAL_ROUTES_NAME_PREFIX_BEFORE_LOCALE', false), +]; diff --git a/stubs/config/locales.php b/stubs/config/locales.php new file mode 100644 index 00000000..45cb52c3 --- /dev/null +++ b/stubs/config/locales.php @@ -0,0 +1,17 @@ + [ + 'en', + 'fr', + ], +]; diff --git a/stubs/resources/css/app.scss b/stubs/resources/css/app.scss new file mode 100644 index 00000000..64be63a7 --- /dev/null +++ b/stubs/resources/css/app.scss @@ -0,0 +1,132 @@ +@import "modern-css-reset"; +@import "@accessibility-in-action/looseleaf"; + +// Banner +@media (min-width: 768px) { + [role="banner"] .wrapper { + align-items: center; + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-block: var(--step-2); + } +} + +// Alert +.alert { + border: var(--ll-border-thin); + border-radius: var(--ll-border-radius); + padding: var(--step-0) var(--step-1); + + .title { + align-items: center; + display: flex; + flex-direction: row; + font-weight: 700; + } +} + +// Authentication Card +.auth-card { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 100vh; + + > * { + max-width: calc(65ch / 2); + width: 100%; + } +} + +.auth-card__logo { + text-align: center; +} + +// Brand +.brand { + font-size: var(--step-2); +} + +// Dropdown +.dropdown .dropdown__content { + background-color: var(--ll-background-color); + border: var(--ll-border-thin); + border-radius: var(--ll-border-radius); + padding: var(--step--5) var(--step--4); + text-align: start; +} + +@media (min-width: 768px) { + .dropdown { + position: relative; + + .dropdown__content { + height: auto; + inset-inline-end: 0; + position: absolute; + text-align: end; + white-space: nowrap; + width: auto; + } + } +} + +// Navigation +nav { + display: flex; + flex-direction: column; + margin-block-start: var(--step-2); +} + +nav ul { + margin: 0; + padding-inline-start: 0; +} + +nav ul > li { + padding-inline-start: 0; +} + +nav ul > li::before { + display: none; +} + +nav > * + * { + margin-block-start: var(--step--2); +} + +nav .link { + align-items: center; + display: flex; + flex-direction: row; +} + +nav .icon { + height: 1em; + margin-inline-end: 0.125em; + width: 1em; +} + +@media (min-width: 768px) { + nav { + flex-direction: row; + margin-block-start: 0; + margin-inline-start: auto; + } + + nav > * + * { + margin-block-start: 0; + margin-inline-start: var(--step--2); + } + + nav ul { + display: flex; + flex-direction: row; + } + + nav ul > li + li { + margin-inline-start: var(--step--2); + } +} diff --git a/stubs/resources/js/app.js b/stubs/resources/js/app.js new file mode 100644 index 00000000..73e4ba73 --- /dev/null +++ b/stubs/resources/js/app.js @@ -0,0 +1,7 @@ +require("./bootstrap"); + +import Alpine from 'alpinejs' + +window.Alpine = Alpine; + +Alpine.start(); diff --git a/stubs/resources/js/bootstrap.js b/stubs/resources/js/bootstrap.js new file mode 100644 index 00000000..90dfe56a --- /dev/null +++ b/stubs/resources/js/bootstrap.js @@ -0,0 +1,9 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + + window.axios = require("axios"); + + window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; diff --git a/stubs/resources/views/auth/confirm-password.blade.php b/stubs/resources/views/auth/confirm-password.blade.php new file mode 100644 index 00000000..9a488885 --- /dev/null +++ b/stubs/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,34 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + +
+ {{ __('hearth::auth.confirm_intro') }} +
+ + + + +
+ @csrf + + +
+ + + +
+ + + {{ __('hearth::auth.action_confirm') }} + +
+
+
diff --git a/stubs/resources/views/auth/forgot-password.blade.php b/stubs/resources/views/auth/forgot-password.blade.php new file mode 100644 index 00000000..798413fb --- /dev/null +++ b/stubs/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,34 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + +
+ {{ __('hearth::auth.forgot_intro') }} +
+ + + + + + + +
+ @csrf + + +
+ + + +
+ + + {{ __('hearth::auth.forgot_submit') }} + +
+
+
diff --git a/stubs/resources/views/auth/login.blade.php b/stubs/resources/views/auth/login.blade.php new file mode 100644 index 00000000..a1aa61c5 --- /dev/null +++ b/stubs/resources/views/auth/login.blade.php @@ -0,0 +1,56 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + + + + + + + +
+ @csrf + + +
+ + + +
+ + +
+ + + +
+ + +
+ + +
+ + @if (Route::has('en.password.request')) +

+ + {{ __('hearth::auth.forget_prompt') }} + +

+ @endif + + + {{ __('hearth::auth.sign_in') }} + +
+
+
diff --git a/stubs/resources/views/auth/register.blade.php b/stubs/resources/views/auth/register.blade.php new file mode 100644 index 00000000..72fc17b7 --- /dev/null +++ b/stubs/resources/views/auth/register.blade.php @@ -0,0 +1,61 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + + + + +
+ @csrf + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + + + + {{ __('hearth::auth.create_your_account') }} + + +
+
diff --git a/stubs/resources/views/auth/reset-password.blade.php b/stubs/resources/views/auth/reset-password.blade.php new file mode 100644 index 00000000..c5023960 --- /dev/null +++ b/stubs/resources/views/auth/reset-password.blade.php @@ -0,0 +1,46 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + + + + +
+ @csrf + + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + + {{ __('hearth::auth.reset_submit') }} + +
+
+
diff --git a/stubs/resources/views/auth/verify-email.blade.php b/stubs/resources/views/auth/verify-email.blade.php new file mode 100644 index 00000000..6134bbf5 --- /dev/null +++ b/stubs/resources/views/auth/verify-email.blade.php @@ -0,0 +1,39 @@ + + + + + {{ config('app.name', 'Accessibility in Action') }} + + + +
+ {{ __('hearth::auth.verification_intro') }} +
+ + @if (session('status') == 'verification-link-sent') + + {{ __('hearth::auth.verification_sent') }} + + @endif + +
+
+ @csrf + +
+ + {{ __('hearth::auth.resend_verification_email') }} + +
+
+ +
+ @csrf + + +
+
+
+
diff --git a/stubs/resources/views/components/auth-card.blade.php b/stubs/resources/views/components/auth-card.blade.php new file mode 100644 index 00000000..4a7c1973 --- /dev/null +++ b/stubs/resources/views/components/auth-card.blade.php @@ -0,0 +1,9 @@ +
+ + +
+ {{ $slot }} +
+
diff --git a/stubs/resources/views/components/auth-session-status.blade.php b/stubs/resources/views/components/auth-session-status.blade.php new file mode 100644 index 00000000..f59b73da --- /dev/null +++ b/stubs/resources/views/components/auth-session-status.blade.php @@ -0,0 +1,7 @@ +@props(['status']) + +@if ($status) +
merge([]) }}> + {{ $status }} +
+@endif diff --git a/stubs/resources/views/components/auth-validation-errors.blade.php b/stubs/resources/views/components/auth-validation-errors.blade.php new file mode 100644 index 00000000..07ecefde --- /dev/null +++ b/stubs/resources/views/components/auth-validation-errors.blade.php @@ -0,0 +1,13 @@ +@props(['errors']) + +@if ($errors->any()) +
merge(['class' => 'flow']) }}> +

{{ __('hearth::auth.error_intro') }}

+ + @foreach ($errors->all() as $error) + +

{{ $error }}

+
+ @endforeach +
+@endif diff --git a/stubs/resources/views/components/brand.blade.php b/stubs/resources/views/components/brand.blade.php new file mode 100644 index 00000000..eec5abba --- /dev/null +++ b/stubs/resources/views/components/brand.blade.php @@ -0,0 +1,4 @@ + +
+ {{ config('app.name', 'Hearth') }} +
diff --git a/stubs/resources/views/components/dropdown-link.blade.php b/stubs/resources/views/components/dropdown-link.blade.php new file mode 100644 index 00000000..9039a9c3 --- /dev/null +++ b/stubs/resources/views/components/dropdown-link.blade.php @@ -0,0 +1,9 @@ +@props(['active']) + +@php +$current = ($active ?? false) + ? ['aria-current' => 'page'] + : []; +@endphp + +merge($current) }}>{{ $slot }} diff --git a/stubs/resources/views/components/dropdown.blade.php b/stubs/resources/views/components/dropdown.blade.php new file mode 100644 index 00000000..f76e12a2 --- /dev/null +++ b/stubs/resources/views/components/dropdown.blade.php @@ -0,0 +1,10 @@ + diff --git a/stubs/resources/views/components/nav-link.blade.php b/stubs/resources/views/components/nav-link.blade.php new file mode 100644 index 00000000..46c1c93f --- /dev/null +++ b/stubs/resources/views/components/nav-link.blade.php @@ -0,0 +1,13 @@ +@props(['active']) + +@php +$current = ($active ?? false) + ? ['aria-current' => 'page'] + : []; +@endphp + +
  • + merge($current) }}> + {{ $slot }} + +
  • diff --git a/stubs/resources/views/components/navigation.blade.php b/stubs/resources/views/components/navigation.blade.php new file mode 100644 index 00000000..dd47ff90 --- /dev/null +++ b/stubs/resources/views/components/navigation.blade.php @@ -0,0 +1,59 @@ + + diff --git a/stubs/resources/views/components/validation-error.blade.php b/stubs/resources/views/components/validation-error.blade.php new file mode 100644 index 00000000..3411ec33 --- /dev/null +++ b/stubs/resources/views/components/validation-error.blade.php @@ -0,0 +1,3 @@ +

    + {{ $slot }} +

    diff --git a/stubs/resources/views/dashboard.blade.php b/stubs/resources/views/dashboard.blade.php new file mode 100644 index 00000000..e3ee256a --- /dev/null +++ b/stubs/resources/views/dashboard.blade.php @@ -0,0 +1,7 @@ + + +

    {{ __('hearth::dashboard.title') }}

    +
    + +

    {{ __('hearth::dashboard.welcome', ['name' => Auth::user()->name]) }}

    +
    diff --git a/stubs/resources/views/errors/401.blade.php b/stubs/resources/views/errors/401.blade.php new file mode 100644 index 00000000..7dad0512 --- /dev/null +++ b/stubs/resources/views/errors/401.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_401_title')) +@section('code', '401') +@section('message', __('hearth::errors.error_401_message')) diff --git a/stubs/resources/views/errors/403.blade.php b/stubs/resources/views/errors/403.blade.php new file mode 100644 index 00000000..53e2ab16 --- /dev/null +++ b/stubs/resources/views/errors/403.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_403_title')) +@section('code', '403') +@section('message', __($exception->getMessage() ?: 'errors.error_403_message')) diff --git a/stubs/resources/views/errors/404.blade.php b/stubs/resources/views/errors/404.blade.php new file mode 100644 index 00000000..3a24574a --- /dev/null +++ b/stubs/resources/views/errors/404.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_404_title')) +@section('code', '404') +@section('message', __('hearth::errors.error_404_message')) diff --git a/stubs/resources/views/errors/419.blade.php b/stubs/resources/views/errors/419.blade.php new file mode 100644 index 00000000..7fc5d51e --- /dev/null +++ b/stubs/resources/views/errors/419.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_419_title')) +@section('code', '419') +@section('message', __('hearth::errors.error_419_message')) diff --git a/stubs/resources/views/errors/429.blade.php b/stubs/resources/views/errors/429.blade.php new file mode 100644 index 00000000..3603e85d --- /dev/null +++ b/stubs/resources/views/errors/429.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_429_title')) +@section('code', '429') +@section('message', __('hearth::errors.error_429_message')) diff --git a/stubs/resources/views/errors/500.blade.php b/stubs/resources/views/errors/500.blade.php new file mode 100644 index 00000000..5c732fca --- /dev/null +++ b/stubs/resources/views/errors/500.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_500_title')) +@section('code', '500') +@section('message', __('hearth::errors.error_500_message')) diff --git a/stubs/resources/views/errors/503.blade.php b/stubs/resources/views/errors/503.blade.php new file mode 100644 index 00000000..9562cde0 --- /dev/null +++ b/stubs/resources/views/errors/503.blade.php @@ -0,0 +1,5 @@ +@extends('errors::errorpage') + +@section('title', __('hearth::errors.error_503_title')) +@section('code', '503') +@section('message', __('hearth::errors.error_503_message')) diff --git a/stubs/resources/views/errors/errorpage.blade.php b/stubs/resources/views/errors/errorpage.blade.php new file mode 100644 index 00000000..c343f561 --- /dev/null +++ b/stubs/resources/views/errors/errorpage.blade.php @@ -0,0 +1,8 @@ + + +

    @yield('code'): @yield('title')

    +
    + +

    @yield('message')

    +

    {{ __('hearth::errors.return_home') }}

    +
    diff --git a/stubs/resources/views/layouts/app.blade.php b/stubs/resources/views/layouts/app.blade.php new file mode 100644 index 00000000..0618ba93 --- /dev/null +++ b/stubs/resources/views/layouts/app.blade.php @@ -0,0 +1,27 @@ + + + + @include('partials.head') + + + @include('layouts.banner') + + +
    +
    + +
    + {{ $header }} +
    + + + @include('partials.flash-messages') + + +
    + {{ $slot }} +
    +
    +
    + + diff --git a/stubs/resources/views/layouts/banner.blade.php b/stubs/resources/views/layouts/banner.blade.php new file mode 100644 index 00000000..049d8865 --- /dev/null +++ b/stubs/resources/views/layouts/banner.blade.php @@ -0,0 +1,6 @@ +
    +
    + @include('components.brand') + @include('components.navigation') +
    +
    diff --git a/stubs/resources/views/layouts/guest.blade.php b/stubs/resources/views/layouts/guest.blade.php new file mode 100644 index 00000000..3d8cab7c --- /dev/null +++ b/stubs/resources/views/layouts/guest.blade.php @@ -0,0 +1,13 @@ + + + + @include('partials.head') + + +
    +
    + {{ $slot }} +
    +
    + + diff --git a/stubs/resources/views/partials/flash-messages.blade.php b/stubs/resources/views/partials/flash-messages.blade.php new file mode 100644 index 00000000..9839e9fb --- /dev/null +++ b/stubs/resources/views/partials/flash-messages.blade.php @@ -0,0 +1,35 @@ +@if (flash()->message) + +

    {{ flash()->message }} + +@endif + +@if(session('status') === 'verification-link-sent') + +

    {{ __('hearth::auth.verification_sent') }}

    +
    +@endif + +@if(session('status') === 'password-updated') + +

    {{ __('hearth::auth.password_change_succeeded') }}

    +
    +@endif + +@auth +@unless(Auth::user()->hasVerifiedEmail()) + +

    {{ __('hearth::auth.verification_intro') }}

    +
    + @csrf +
    + + {{ __('hearth::auth.resend_verification_email') }} + +
    +
    +
    +@endunless +@endauth + + diff --git a/stubs/resources/views/partials/head.blade.php b/stubs/resources/views/partials/head.blade.php new file mode 100644 index 00000000..0df22725 --- /dev/null +++ b/stubs/resources/views/partials/head.blade.php @@ -0,0 +1,26 @@ + + + + +{{ config('app.name', 'Hearth') }} + + + + + + + + + + + + + + +@googlefonts + + + + + + diff --git a/stubs/resources/views/partials/validation-errors.blade.php b/stubs/resources/views/partials/validation-errors.blade.php new file mode 100644 index 00000000..427988f5 --- /dev/null +++ b/stubs/resources/views/partials/validation-errors.blade.php @@ -0,0 +1,12 @@ +@if($errors->getBags()) + @foreach($errors->getBags() as $bag) + +

    {{ __('hearth::forms.errors_found_message') }}

    + +
    + @endforeach +@endif diff --git a/stubs/resources/views/users/admin.blade.php b/stubs/resources/views/users/admin.blade.php new file mode 100644 index 00000000..6b4a70fa --- /dev/null +++ b/stubs/resources/views/users/admin.blade.php @@ -0,0 +1,66 @@ + + +

    + {{ __('hearth::user.account') }} +

    +
    + + + @include('partials.validation-errors') + +

    {{ __('hearth::auth.change_password') }}

    + +
    + @csrf + @method('PUT') + +
    + + + @error('current_password', 'updatePassword') + {{ $message }} + @enderror +
    + +
    + + + @error('password', 'updatePassword') + {{ $message }} + @enderror +
    + +
    + + + @error('password_confirmation', 'updatePassword') + {{ $message }} + @enderror +
    + + + {{ __('hearth::auth.change_password') }} + +
    + +

    {{ __('hearth::user.delete_account') }}

    + +

    {{ __('hearth::user.delete_account_intro') }}

    + +
    + @csrf + @method('DELETE') + +
    + + + @error('current_password', 'destroyAccount') + {{ $message }} + @enderror +
    + + + {{ __('hearth::user.action_delete_account') }} + +
    +
    diff --git a/stubs/resources/views/users/edit.blade.php b/stubs/resources/views/users/edit.blade.php new file mode 100644 index 00000000..577281eb --- /dev/null +++ b/stubs/resources/views/users/edit.blade.php @@ -0,0 +1,39 @@ + + +

    + {{ __('hearth::user.settings') }} +

    +
    + + + @include('partials.validation-errors') + +
    + @csrf + @method('PUT') +
    + + + @error('name', 'updateProfileInformation') + {{ $message }} + @enderror +
    + +
    + + + @error('email', 'updateProfileInformation') + {{ $message }} + @enderror +
    + +
    + + +
    + + + {{ __('hearth::forms.save_changes') }} + +
    +
    diff --git a/stubs/resources/views/welcome.blade.php b/stubs/resources/views/welcome.blade.php new file mode 100644 index 00000000..581c224e --- /dev/null +++ b/stubs/resources/views/welcome.blade.php @@ -0,0 +1,8 @@ + + +

    {{ config('app.name', 'Hearth') }}

    +
    + +

    {{ __('hearth::welcome.intro') }}

    +

    {{ __('hearth::welcome.details') }}

    +
    diff --git a/stubs/routes/fortify.php b/stubs/routes/fortify.php new file mode 100644 index 00000000..34b917ba --- /dev/null +++ b/stubs/routes/fortify.php @@ -0,0 +1,100 @@ + config('fortify.middleware', ['web'])], function () { + if (Features::enabled(Features::registration())) { + Route::multilingual('/register', [RegisteredUserController::class, 'create']) + ->middleware('guest') + ->name('register'); + + Route::multilingual('/register', [RegisteredUserController::class, 'store']) + ->method('post') + ->middleware('guest'); + } + + Route::multilingual('/login', [AuthenticatedSessionController::class, 'create']) + ->middleware('guest') + ->name('login'); + + $limiter = config('fortify.limiters.login'); + + Route::multilingual('/login', [AuthenticatedSessionController::class, 'store']) + ->method('post') + ->middleware(array_filter([ + 'guest', + $limiter ? 'throttle:' . $limiter : null, + ])); + + Route::multilingual('/logout', [AuthenticatedSessionController::class, 'destroy']) + ->method('post') + ->middleware('auth') + ->name('logout'); + + if (Features::enabled(Features::resetPasswords())) { + Route::multilingual('/forgot-password', [PasswordResetLinkController::class, 'create']) + ->middleware('guest') + ->name('password.request'); + + + Route::get('/reset-password/{token}', [NewPasswordController::class, 'create']) + ->middleware('guest') + ->name('password.reset'); + + Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) + ->middleware('guest') + ->name('password.email'); + + Route::multilingual('/reset-password', [NewPasswordController::class, 'store']) + ->method('post') + ->middleware('guest') + ->name('password.update'); + } + + if (Features::enabled(Features::emailVerification())) { + Route::multilingual('/verify-email', [EmailVerificationPromptController::class, '__invoke']) + ->middleware('auth') + ->name('verification.notice'); + + Route::get('/verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) + ->middleware(['auth', 'signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware(['auth', 'throttle:6,1']) + ->name('verification.send'); + } + + if (Features::enabled(Features::updateProfileInformation())) { + Route::multilingual('/account/update', [ProfileInformationController::class, 'update']) + ->method('put') + ->middleware(['auth']) + ->name('user-profile-information.update'); + } + + if (Features::enabled(Features::updatePasswords())) { + Route::multilingual('/account/update-password', [PasswordController::class, 'update']) + ->method('put') + ->middleware(['auth']) + ->name('user-password.update'); + } + + Route::multilingual('/confirm-password', [ConfirmablePasswordController::class, 'show']) + ->middleware('auth') + ->name('password.confirm'); + + Route::multilingual('/confirm-password', [ConfirmablePasswordController::class, 'store']) + ->method('post') + ->middleware('auth'); +}); diff --git a/stubs/routes/web.php b/stubs/routes/web.php new file mode 100644 index 00000000..06f6ee5d --- /dev/null +++ b/stubs/routes/web.php @@ -0,0 +1,39 @@ +name('welcome'); + +Route::multilingual('/dashboard', function () { + return view('dashboard'); +})->middleware(['auth', 'verified:' . locale() . '.verification.notice'])->name('dashboard'); + +Route::multilingual('/account/edit', [UserController::class, 'edit']) + ->middleware(['auth']) + ->name('users.edit'); + +Route::multilingual('/account/admin', [UserController::class, 'admin']) + ->middleware(['auth']) + ->name('users.admin'); + +Route::multilingual('/account/delete', [UserController::class, 'destroy']) + ->method('delete') + ->middleware(['auth']) + ->name('users.destroy'); + +require __DIR__ . '/fortify.php'; diff --git a/stubs/webpack.mix.js b/stubs/webpack.mix.js new file mode 100644 index 00000000..dff610b1 --- /dev/null +++ b/stubs/webpack.mix.js @@ -0,0 +1,9 @@ +const mix = require("laravel-mix"); + +mix.js("resources/js/app.js", "public/js").extract(); + +mix.sass("resources/css/app.scss", "public/css/app.css"); + +if (mix.inProduction()) { + mix.version(); +} diff --git a/testbench.yaml b/testbench.yaml index 512e36a7..74eefc87 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -1,2 +1,2 @@ providers: - - FluidProject\Hearth\HearthServiceProvider + - Hearth\HearthServiceProvider diff --git a/tests/TestCase.php b/tests/TestCase.php index fa5ec028..5a14c5fc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,8 +2,11 @@ namespace Hearth\Tests; +use ChinLeung\LaravelLocales\LaravelLocalesServiceProvider; +use ChinLeung\MultilingualRoutes\MultilingualRoutesServiceProvider; use Hearth\HearthServiceProvider; use Illuminate\Database\Eloquent\Factories\Factory; +use Laravel\Fortify\FortifyServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; class TestCase extends Orchestra @@ -20,7 +23,10 @@ public function setUp(): void protected function getPackageProviders($app) { return [ + FortifyServiceProvider::class, HearthServiceProvider::class, + LaravelLocalesServiceProvider::class, + MultilingualRoutesServiceProvider::class, ]; } @@ -28,9 +34,7 @@ public function getEnvironmentSetUp($app) { config()->set('database.default', 'testing'); - /* - include_once __DIR__.'/../database/migrations/create_hearth_table.php.stub'; - (new \CreatePackageTable())->up(); - */ + include_once __DIR__.'/../database/migrations/create_users_table.php.stub'; + (new \CreateUsersTable())->up(); } }