├── .github └── issue_template.md ├── .gitignore ├── LICENSE ├── README.md ├── codesize.xml ├── composer.json ├── config ├── auth.php ├── config.php ├── inspiring.php ├── state.php └── themes.php ├── database ├── migrations │ ├── 2017_01_01_001000_create_jobs_table.php │ ├── 2017_01_01_002000_create_failed_jobs_table.php │ ├── 2017_01_01_002500_create_job_batches_table.php │ ├── 2017_01_01_114000_create_logins_table.php │ ├── 2017_01_01_115000_create_preferences_table.php │ ├── 2017_01_01_116000_create_structure_for_home.php │ ├── 2017_01_01_118000_create_structure_for_preferences.php │ ├── 2017_01_01_121000_create_structure_for_administration.php │ ├── 2017_01_01_122000_create_structure_for_system.php │ └── 2017_01_01_123000_create_structure_for_integrations.php └── seeders │ └── DatabaseSeeder.php ├── resources ├── images │ ├── bulma-logo.png │ ├── bulma.svg │ ├── corners │ │ ├── bottom-center.svg │ │ ├── bottom-left.svg │ │ ├── bottom-right.svg │ │ ├── top-center.svg │ │ ├── top-left.svg │ │ └── top-right.svg │ ├── earthlink.svg │ ├── emails │ │ ├── facebook.svg │ │ ├── instagram.svg │ │ ├── tiktok.svg │ │ └── twitter.svg │ ├── enso-favicon.png │ ├── enso.svg │ ├── laravel-badge.png │ ├── logo.svg │ ├── made-with-bulma.png │ └── vue-badge.png ├── preferences.json └── views │ ├── emails │ └── reset.blade.php │ └── mail │ └── html │ ├── footer.blade.php │ └── themes │ └── enso.css ├── routes ├── api.php └── app │ ├── auth.php │ ├── core.php │ └── core │ └── preferences.php ├── src ├── AppServiceProvider.php ├── BroadcastServiceProvider.php ├── Commands │ ├── AnnounceAppUpdate.php │ ├── ClearPreferences.php │ ├── ResetStorage.php │ ├── UpdateGlobalPreferences.php │ └── Version.php ├── Contracts │ └── ProvidesState.php ├── EventServiceProvider.php ├── Events │ ├── AppUpdate.php │ └── Login.php ├── Exceptions │ ├── Authentication.php │ └── UserConflict.php ├── Facades │ └── Websockets.php ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── ForgotPasswordController.php │ │ │ ├── LoginController.php │ │ │ └── ResetPasswordController.php │ │ ├── Guest.php │ │ ├── Preferences │ │ │ ├── Reset.php │ │ │ └── Store.php │ │ └── Spa.php │ ├── Middleware │ │ ├── AuthorizationCookie.php │ │ ├── EnsureFrontendRequestsAreStateful.php │ │ ├── VerifyActiveState.php │ │ └── XssSanitizer.php │ ├── Requests │ │ └── ValidatePassword.php │ └── Responses │ │ └── GuestState.php ├── Listeners │ ├── LoginListener.php │ └── PasswordResetListener.php ├── MiddlewareServiceProvider.php ├── Models │ ├── Login.php │ └── Preference.php ├── Notifications │ ├── PasswordExpiresSoon.php │ └── ResetPassword.php ├── PasswordServiceProvider.php ├── Rules │ └── DistinctPassword.php ├── Services │ ├── DefaultPreferences.php │ ├── Inspiring.php │ ├── State │ │ ├── Builder.php │ │ └── Source.php │ ├── Version.php │ └── Websockets.php ├── State │ ├── Meta.php │ ├── Preferences.php │ ├── Themes.php │ └── Websockets.php ├── Traits │ ├── HasPassword.php │ ├── Login.php │ └── Logout.php └── WebsocketServiceProvider.php ├── stubs ├── development-index.stub └── production-index.blade.stub └── tests └── features └── LoginTest.php /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | This is a **bug | feature request**. 3 | 4 | 5 | ### Prerequisites 6 | * [ ] Are you running the latest version? 7 | * [ ] Are you reporting to the correct repository? 8 | * [ ] Did you check the documentation? 9 | * [ ] Did you perform a cursory search? 10 | 11 | ### Description 12 | 13 | 14 | ### Steps to Reproduce 15 | 20 | 21 | ### Expected behavior 22 | 23 | 24 | ### Actual behavior 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 laravel-enso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/cd39c3fa899a4975815209209f5c6fb9)](https://www.codacy.com/gh/laravel-enso/core?utm_source=github.com&utm_medium=referral&utm_content=laravel-enso/core&utm_campaign=Badge_Grade) 4 | [![StyleCI](https://github.styleci.io/repos/85807594/shield?branch=master)](https://github.styleci.io/repos/85807594) 5 | [![License](https://poser.pugx.org/laravel-enso/core/license)](https://packagist.org/packages/laravel-enso/core) 6 | [![Total Downloads](https://poser.pugx.org/laravel-enso/core/downloads)](https://packagist.org/packages/laravel-enso/core) 7 | [![Latest Stable Version](https://poser.pugx.org/laravel-enso/core/version)](https://packagist.org/packages/laravel-enso/core) 8 | 9 | Main requirement & dependency aggregator for [Laravel Enso](https://github.com/laravel-enso/Enso). 10 | 11 | This package works exclusively within the [Enso](https://github.com/laravel-enso/Enso) ecosystem. 12 | 13 | The front end assets that utilize this api are present in the [ui](https://github.com/enso-ui/ui) package. 14 | 15 | For live examples and demos, you may visit [laravel-enso.com](https://www.laravel-enso.com) 16 | 17 | ### Installation, Configuration & Usage 18 | 19 | Be sure to check out the full documentation for this package available at [docs.laravel-enso.com](https://docs.laravel-enso.com/backend/core.html) 20 | 21 | ### Contributions 22 | 23 | are welcome. Pull requests are great, but issues are good too. 24 | 25 | ### License 26 | 27 | This package is released under the MIT license. 28 | -------------------------------------------------------------------------------- /codesize.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | custom rules 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-enso/core", 3 | "description": "Main requirement & dependency aggregator for Laravel Enso", 4 | "keywords": [ 5 | "laravel-enso", 6 | "enso-core", 7 | "laravel-boilerplate", 8 | "enso-aggregator", 9 | "vue-bulma", 10 | "vue-spa" 11 | ], 12 | "homepage": "https://github.com/laravel-enso/Core", 13 | "type": "library", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Adrian Ocneanu", 18 | "email": "aocneanu@gmail.com", 19 | "homepage": "https://laravel-enso.com", 20 | "role": "Developer" 21 | }, 22 | { 23 | "name": "Mihai Ocneanu", 24 | "email": "mihai.ocneanu@gmail.com", 25 | "homepage": "https://laravel-enso.com", 26 | "role": "Developer" 27 | }, 28 | { 29 | "name": "Ionut Pirvulescu", 30 | "email": "ionut.pirvulescu1@gmail.com", 31 | "homepage": "https://laravel-enso.com", 32 | "role": "Developer" 33 | } 34 | ], 35 | "require": { 36 | "php": "^8.2", 37 | "jenssegers/agent": "^2.6", 38 | "laravel/framework": "^11.0", 39 | "laravel/sanctum": "^4.0", 40 | "laravel-enso/action-logger": "^3.1", 41 | "laravel-enso/avatars": "^4.0", 42 | "laravel-enso/charts": "^4.0", 43 | "laravel-enso/companies": "^4.0", 44 | "laravel-enso/data-export": "^3.0", 45 | "laravel-enso/dynamic-methods": "^3.0", 46 | "laravel-enso/files": "^5.0", 47 | "laravel-enso/forms": "^4.0", 48 | "laravel-enso/helpers": "^3.0", 49 | "laravel-enso/history-tracker": "^2.0", 50 | "laravel-enso/impersonate": "^3.2", 51 | "laravel-enso/io": "^2.3", 52 | "laravel-enso/localisation": "^5.0", 53 | "laravel-enso/logs": "^4.0", 54 | "laravel-enso/migrator": "^2.0", 55 | "laravel-enso/menus": "^5.0", 56 | "laravel-enso/notifications": "^4.1", 57 | "laravel-enso/people": "^4.0", 58 | "laravel-enso/permissions": "^5.0", 59 | "laravel-enso/rememberable": "^3.0", 60 | "laravel-enso/roles": "^5.0", 61 | "laravel-enso/searchable": "^2.2", 62 | "laravel-enso/sentry": "^1.0", 63 | "laravel-enso/select": "^4.0", 64 | "laravel-enso/track-who": "^2.0", 65 | "laravel-enso/tables": "^4.0", 66 | "laravel-enso/upgrade": "^2.5", 67 | "laravel-enso/users": "^2.0", 68 | "voku/anti-xss": "^4.1" 69 | }, 70 | "autoload": { 71 | "psr-4": { 72 | "LaravelEnso\\Core\\": "src/", 73 | "LaravelEnso\\Core\\Database\\Factories\\": "database/factories/", 74 | "LaravelEnso\\Core\\Database\\Seeders\\": "database/seeders/" 75 | } 76 | }, 77 | "extra": { 78 | "laravel": { 79 | "providers": [ 80 | "LaravelEnso\\Core\\AppServiceProvider", 81 | "LaravelEnso\\Core\\BroadcastServiceProvider", 82 | "LaravelEnso\\Core\\EventServiceProvider", 83 | "LaravelEnso\\Core\\PasswordServiceProvider", 84 | "LaravelEnso\\Core\\MiddlewareServiceProvider", 85 | "LaravelEnso\\Core\\WebsocketServiceProvider" 86 | ] 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | env('LOGIN_ATTEMPTS_PER_MINUTE', 5), 5 | 'password' => [ 6 | 'lifetime' => env('PASSWORD_LIFETIME', 0), 7 | 'minLength' => env('PASSWORD_MIN_LENGTH', 6), 8 | 'mixedCase' => (bool) env('PASSWORD_MIXED_CASE', 0), 9 | 'numeric' => (bool) env('PASSWORD_NUMERIC', 0), 10 | 'special' => (bool) env('PASSWORD_SPECIAL', 0), 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | '4.8.0', 5 | 'ownerCompanyId' => env('OWNER_COMPANY_ID', 1), 6 | 'showQuote' => env('SHOW_QUOTE', true), 7 | 'defaultRole' => 'admin', 8 | 'dateFormat' => 'd-m-Y', 9 | 'dateTimeFormat' => 'd-m-Y H:i:s', 10 | 'facebook' => 'https://facebook.com', 11 | 'instagram' => 'https://www.instagram.com', 12 | 'twitter' => 'https://twitter.com', 13 | 'tiktok' => 'https://tiktok.com', 14 | 'youtube' => 'https://youtube.com', 15 | 'extendedDocumentTitle' => env('EXTENDED_DOCUMENT_TITLE', true), 16 | ]; 17 | -------------------------------------------------------------------------------- /config/inspiring.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'There is only one boss... the customer - Sam Walton @ Walmart', 6 | 'Get Shit Done - Aaron Levie @ Box', 7 | 'Less Meetings. More Doing - Jason Goldberg @ Fab.com', 8 | "Don't Compromise - Steve Jobs @ Apple", 9 | 'Whatever the problem be part of the solution - Tina Fey', 10 | "If a user is having a problem, it's our problem - Steve Jobs @ Apple", 11 | 'Complaining is not a strategy - Jeff Bezons @ Amazon', 12 | "Optimism, pessimism, F... that! We're going to make it happen - Elon Musk @ Tesla", 13 | 'Think like a customer - Paul Gillin', 14 | 'Always deliver more than expected - Larry Page @ Google', 15 | 'Done is better than perfect - Sheryl Sandberg @ Facebook', 16 | 'Make it work then make it better', 17 | 'Be more curious - Tanner Christensen @ Facebook', 18 | 'Start where you are, use what you have, do what you can', 19 | 'Ideas are worthless until you get them out of your head to see what they can do - Tanner Christensen @ Facebook', 20 | 'Set goals. Reach. Repeat', 21 | 'Quality is the best business plan - John Lasseter @ Pixar', 22 | 'Never Never Never Give Up - Winston Churchill', 23 | 'Life is short. Do stuff that matters - Siqi Chen @ Hey', 24 | 'Experiment. Fail. Learn. Repeat.', 25 | "We have a strategic plan. It's called Doing Things - Herb Kelleher @ Southwest Arilines", 26 | 'Vision without execution is hallucination', 27 | "It's simple until you make it complicated - Jason Fried @ Twitter", 28 | "There's nothing wrong with being small. You can do big things with a small team - Jason Fried @ 37signals", 29 | "There are seven days in a week. Someday isn't one of them", 30 | 'Wake up with determination. Go to bed with satisfaction', 31 | "Be so good they can't ignore you - Steve Martin", 32 | 'The worst decision is indecision - Ryan Harwood @ PureWow', 33 | "Don't guess. Measure - Slava Akhmechet @ RethinkDB", 34 | 'Try again. Fail again. Fail better - Samuel Beckett', 35 | ], 36 | ]; 37 | -------------------------------------------------------------------------------- /config/state.php: -------------------------------------------------------------------------------- 1 | ['laravel-enso'], 5 | ]; 6 | -------------------------------------------------------------------------------- /config/themes.php: -------------------------------------------------------------------------------- 1 | 'light', 5 | 'dark' => 'dark', 6 | 'light-rtl' => 'light-rtl', 7 | 'dark-rtl' => 'dark-rtl', 8 | ]; 9 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_001000_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 12 | 13 | $table->string('queue'); 14 | $table->longText('payload'); 15 | $table->tinyInteger('attempts')->unsigned(); 16 | 17 | $table->integer('reserved_at')->unsigned()->nullable(); 18 | $table->integer('available_at')->unsigned(); 19 | $table->integer('created_at')->unsigned(); 20 | 21 | $table->index(['queue', 'reserved_at']); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('jobs'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_002000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 12 | $table->string('uuid')->unique(); 13 | $table->text('connection'); 14 | $table->text('queue'); 15 | $table->longText('payload'); 16 | $table->longText('exception'); 17 | 18 | $table->timestamp('failed_at')->useCurrent(); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('failed_jobs'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_002500_create_job_batches_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 12 | $table->string('name'); 13 | $table->integer('total_jobs'); 14 | $table->integer('pending_jobs'); 15 | $table->integer('failed_jobs'); 16 | $table->text('failed_job_ids'); 17 | $table->text('options')->nullable(); 18 | $table->integer('cancelled_at')->nullable(); 19 | $table->integer('created_at'); 20 | $table->integer('finished_at')->nullable(); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists('job_batches'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_114000_create_logins_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | 13 | $table->integer('user_id')->unsigned()->index(); 14 | $table->foreign('user_id')->references('id')->on('users'); 15 | 16 | $table->string('ip'); 17 | $table->text('user_agent'); 18 | 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('logins'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_115000_create_preferences_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 12 | 13 | $table->integer('user_id')->unsigned()->index()->unique(); 14 | $table->foreign('user_id')->references('id')->on('users') 15 | ->onUpdate('cascade')->onDelete('cascade'); 16 | 17 | $table->json('value'); 18 | 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('preferences'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_116000_create_structure_for_home.php: -------------------------------------------------------------------------------- 1 | 'core.home.index', 'description' => 'App State Builder', 'is_default' => true], 8 | ]; 9 | }; 10 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_118000_create_structure_for_preferences.php: -------------------------------------------------------------------------------- 1 | 'core.preferences.store', 'description' => "Set user's preferences", 'is_default' => true], 8 | ['name' => 'core.preferences.reset', 'description' => 'Reset preferences to default', 'is_default' => true], 9 | ]; 10 | }; 11 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_121000_create_structure_for_administration.php: -------------------------------------------------------------------------------- 1 | 'Administration', 'icon' => 'cogs', 'route' => null, 'order_index' => 500, 'has_children' => true, 8 | ]; 9 | }; 10 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_122000_create_structure_for_system.php: -------------------------------------------------------------------------------- 1 | 'System', 'icon' => 'sliders-h', 'route' => null, 'order_index' => 600, 'has_children' => true, 8 | ]; 9 | }; 10 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_123000_create_structure_for_integrations.php: -------------------------------------------------------------------------------- 1 | 'Integrations', 'icon' => 'fas puzzle-piece', 'route' => null, 'order_index' => 700, 'has_children' => true, 8 | ]; 9 | }; 10 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 17 | RoleSeeder::class, 18 | UserGroupSeeder::class, 19 | UserSeeder::class, 20 | LanguageSeeder::class, 21 | CountrySeeder::class, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/images/bulma-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-enso/core/bb78e83853b5eb6c5137a8e3ec7f41971acac8d4/resources/images/bulma-logo.png -------------------------------------------------------------------------------- /resources/images/bulma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/images/corners/bottom-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/corners/bottom-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/corners/bottom-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/corners/top-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/corners/top-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/corners/top-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/images/emails/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Facebook 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/images/emails/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Instagram 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/images/emails/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/images/emails/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twitter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/images/enso-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-enso/core/bb78e83853b5eb6c5137a8e3ec7f41971acac8d4/resources/images/enso-favicon.png -------------------------------------------------------------------------------- /resources/images/enso.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/images/laravel-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-enso/core/bb78e83853b5eb6c5137a8e3ec7f41971acac8d4/resources/images/laravel-badge.png -------------------------------------------------------------------------------- /resources/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/images/made-with-bulma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-enso/core/bb78e83853b5eb6c5137a8e3ec7f41971acac8d4/resources/images/made-with-bulma.png -------------------------------------------------------------------------------- /resources/images/vue-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-enso/core/bb78e83853b5eb6c5137a8e3ec7f41971acac8d4/resources/images/vue-badge.png -------------------------------------------------------------------------------- /resources/preferences.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "lang": "en", 4 | "dtStateSave": true, 5 | "expandedSidebar": true, 6 | "bookmarks": true, 7 | "theme": "light", 8 | "toastrPosition": "bottom-right" 9 | }, 10 | "local": {} 11 | } -------------------------------------------------------------------------------- /resources/views/emails/reset.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{ __('Hi :name', ['name' => $name]) }}, 3 | 4 | {{ __("You just asked for a password reset") }}. 5 | {{ __('To complete the process click the button below.') }} 6 | 7 | @component('mail::button', ['url' => $url, 'color' => 'red']) 8 | {{ __('Set your new password') }} 9 | @endcomponent 10 | 11 | @lang('Regards'), 12 |
13 | {{ config('app.name') }} 14 | @endcomponent 15 | -------------------------------------------------------------------------------- /resources/views/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /resources/views/mail/html/themes/enso.css: -------------------------------------------------------------------------------- 1 | /* Base */ 2 | 3 | body, body *:not(html):not(style):not(br):not(tr):not(code) { 4 | font-family: Avenir, Helvetica, sans-serif; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | background-color: #fefffe; 10 | color: #3c4751; 11 | height: 100%; 12 | hyphens: auto; 13 | line-height: 1.4; 14 | margin: 0; 15 | -moz-hyphens: auto; 16 | -ms-word-break: break-all; 17 | width: 100% !important; 18 | -webkit-hyphens: auto; 19 | -webkit-text-size-adjust: none; 20 | word-break: break-all; 21 | word-break: break-word; 22 | } 23 | 24 | p, 25 | ul, 26 | ol, 27 | blockquote { 28 | line-height: 1.4; 29 | text-align: left; 30 | } 31 | 32 | a { 33 | color: #047bd5; 34 | } 35 | 36 | a img { 37 | border: none; 38 | } 39 | 40 | /* Typography */ 41 | 42 | h1 { 43 | color: #161d2d; 44 | font-size: 19px; 45 | font-weight: bold; 46 | margin-top: 0; 47 | text-align: left; 48 | } 49 | 50 | h2 { 51 | color: #161d2d; 52 | font-size: 16px; 53 | font-weight: bold; 54 | margin-top: 0; 55 | text-align: left; 56 | } 57 | 58 | h3 { 59 | color: #161d2d; 60 | font-size: 14px; 61 | font-weight: bold; 62 | margin-top: 0; 63 | text-align: left; 64 | } 65 | 66 | p { 67 | color: #3c4751; 68 | font-size: 16px; 69 | line-height: 1.5em; 70 | margin-top: 0; 71 | text-align: left; 72 | } 73 | 74 | p.sub { 75 | font-size: 12px; 76 | } 77 | 78 | img { 79 | max-width: 100%; 80 | } 81 | 82 | /* Layout */ 83 | 84 | .wrapper { 85 | background-color: #fefffe; 86 | margin: 0; 87 | padding: 0; 88 | width: 100%; 89 | -premailer-cellpadding: 0; 90 | -premailer-cellspacing: 0; 91 | -premailer-width: 100%; 92 | } 93 | 94 | .content { 95 | margin: 0; 96 | padding: 0; 97 | width: 100%; 98 | -premailer-cellpadding: 0; 99 | -premailer-cellspacing: 0; 100 | -premailer-width: 100%; 101 | } 102 | 103 | /* Header */ 104 | 105 | .header { 106 | padding: 25px 0; 107 | text-align: center; 108 | } 109 | 110 | .header a { 111 | color: #161d2d; 112 | font-size: 19px; 113 | font-weight: bold; 114 | text-decoration: none; 115 | text-shadow: 0 1px 0 #fefffe; 116 | } 117 | 118 | /* Body */ 119 | 120 | .body { 121 | background-color: #fefffe; 122 | border-bottom: 1px solid #cfcfcf; 123 | border-top: 1px solid #cfcfcf; 124 | margin: 0; 125 | padding: 0; 126 | width: 100%; 127 | -premailer-cellpadding: 0; 128 | -premailer-cellspacing: 0; 129 | -premailer-width: 100%; 130 | } 131 | 132 | .inner-body { 133 | background-color: #fefffe; 134 | margin: 0 auto; 135 | padding: 0; 136 | width: 570px; 137 | -premailer-cellpadding: 0; 138 | -premailer-cellspacing: 0; 139 | -premailer-width: 570px; 140 | } 141 | 142 | /* Subcopy */ 143 | 144 | .subcopy { 145 | border-top: 1px solid #e2e2e2; 146 | margin-top: 25px; 147 | padding-top: 25px; 148 | } 149 | 150 | .subcopy p { 151 | font-size: 12px; 152 | } 153 | 154 | /* Footer */ 155 | 156 | .footer { 157 | margin: 0 auto; 158 | padding: 0; 159 | text-align: center; 160 | width: 570px; 161 | -premailer-cellpadding: 0; 162 | -premailer-cellspacing: 0; 163 | -premailer-width: 570px; 164 | } 165 | 166 | .footer p { 167 | color: #788595; 168 | font-size: 12px; 169 | text-align: center; 170 | } 171 | 172 | /* Tables */ 173 | 174 | .table table { 175 | margin: 30px auto; 176 | width: 100%; 177 | -premailer-cellpadding: 0; 178 | -premailer-cellspacing: 0; 179 | -premailer-width: 100%; 180 | } 181 | 182 | .nowrap { 183 | white-space: nowrap; 184 | } 185 | 186 | .text-center { 187 | text-align: center; 188 | } 189 | 190 | .text-right { 191 | text-align: right; 192 | } 193 | 194 | .table th { 195 | color: #3c4751; 196 | border-bottom: 1px solid #edeff2; 197 | padding-bottom: 8px; 198 | } 199 | 200 | .table td { 201 | color: #3c4751; 202 | font-size: 15px; 203 | line-height: 18px; 204 | padding: 10px 2px; 205 | } 206 | 207 | .content-cell { 208 | padding: 35px; 209 | } 210 | 211 | /* Buttons */ 212 | 213 | .action { 214 | margin: 30px auto; 215 | padding: 0; 216 | text-align: center; 217 | width: 100%; 218 | -premailer-cellpadding: 0; 219 | -premailer-cellspacing: 0; 220 | -premailer-width: 100%; 221 | } 222 | 223 | .button { 224 | border-radius: 3px; 225 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); 226 | color: #fefffe; 227 | display: inline-block; 228 | text-decoration: none; 229 | -webkit-text-size-adjust: none; 230 | } 231 | 232 | .button-blue, .button-primary { 233 | background-color: #161d2d; 234 | border-top: 10px solid #161d2d; 235 | border-right: 18px solid #161d2d; 236 | border-bottom: 10px solid #161d2d; 237 | border-left: 18px solid #161d2d; 238 | } 239 | 240 | .button-green, .button-success { 241 | background-color: #47ba77; 242 | border-top: 10px solid #47ba77; 243 | border-right: 18px solid #47ba77; 244 | border-bottom: 10px solid #47ba77; 245 | border-left: 18px solid #47ba77; 246 | } 247 | 248 | .button-red, .button-error { 249 | background-color: #e44024; 250 | border-top: 10px solid #e44024; 251 | border-right: 18px solid #e44024; 252 | border-bottom: 10px solid #e44024; 253 | border-left: 18px solid #e44024; 254 | } 255 | 256 | /* Panels */ 257 | 258 | .panel { 259 | margin: 0 0 21px; 260 | } 261 | 262 | .panel-content { 263 | background-color: #e5ecff; 264 | padding: 16px; 265 | } 266 | 267 | .panel-item { 268 | padding: 0; 269 | } 270 | 271 | .panel-item p:last-of-type { 272 | margin-bottom: 0; 273 | padding-bottom: 0; 274 | } 275 | 276 | /* Promotions */ 277 | 278 | .promotion { 279 | background-color: #FFFFFF; 280 | border: 2px dashed #9BA2AB; 281 | margin: 0; 282 | margin-bottom: 25px; 283 | margin-top: 25px; 284 | padding: 24px; 285 | width: 100%; 286 | -premailer-cellpadding: 0; 287 | -premailer-cellspacing: 0; 288 | -premailer-width: 100%; 289 | } 290 | 291 | .promotion h1 { 292 | text-align: center; 293 | } 294 | 295 | .promotion p { 296 | font-size: 15px; 297 | text-align: center; 298 | } 299 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 8 | Route::get('/meta', Guest::class)->name('meta'); 9 | 10 | require __DIR__.'/app/auth.php'; 11 | 12 | Route::middleware(['api', 'auth', 'core']) 13 | ->group(fn () => require __DIR__.'/app/core.php'); 14 | }); 15 | -------------------------------------------------------------------------------- /routes/app/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 10 | Route::middleware('guest')->group(function () { 11 | Route::post('login', [LoginController::class, 'login']) 12 | ->name('login'); 13 | Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail']) 14 | ->name('password.email'); 15 | Route::post('password/reset', [ResetPasswordController::class, 'attemptReset']) 16 | ->name('password.reset'); 17 | }); 18 | 19 | Route::middleware('auth')->group(function () { 20 | Route::post('logout', [LoginController::class, 'logout'])->name('logout'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /routes/app/core.php: -------------------------------------------------------------------------------- 1 | as('core.') 8 | ->group(function () { 9 | Route::get('home', Spa::class)->name('home.index'); 10 | 11 | require __DIR__.'/core/preferences.php'; 12 | }); 13 | -------------------------------------------------------------------------------- /routes/app/core/preferences.php: -------------------------------------------------------------------------------- 1 | as('preferences.') 9 | ->group(function () { 10 | Route::patch('store/{route?}', Store::class)->name('store'); 11 | Route::post('reset/{route?}', Reset::class)->name('reset'); 12 | }); 13 | -------------------------------------------------------------------------------- /src/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | Websockets::class, 22 | ]; 23 | 24 | public function boot() 25 | { 26 | JsonResource::withoutWrapping(); 27 | 28 | $this->loadDependencies() 29 | ->publishDependencies() 30 | ->publishResources() 31 | ->setFactoryResolver() 32 | ->commands( 33 | AnnounceAppUpdate::class, 34 | ClearPreferences::class, 35 | ResetStorage::class, 36 | UpdateGlobalPreferences::class, 37 | Version::class, 38 | ); 39 | } 40 | 41 | private function loadDependencies() 42 | { 43 | $this->mergeConfigFrom(__DIR__.'/../config/inspiring.php', 'enso.inspiring'); 44 | 45 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'enso.config'); 46 | 47 | $this->mergeConfigFrom(__DIR__.'/../config/auth.php', 'enso.auth'); 48 | 49 | $this->mergeConfigFrom(__DIR__.'/../config/state.php', 'enso.state'); 50 | 51 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 52 | 53 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 54 | 55 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-enso/core'); 56 | 57 | return $this; 58 | } 59 | 60 | private function publishDependencies() 61 | { 62 | $this->publishes([ 63 | __DIR__.'/../config' => config_path('enso'), 64 | ], ['core-config', 'enso-config']); 65 | 66 | $this->publishes([ 67 | __DIR__.'/../resources/preferences.json' => resource_path('preferences.json'), 68 | ], ['core-preferences', 'enso-preferences']); 69 | 70 | $this->publishes([ 71 | __DIR__.'/../database/seeders' => database_path('seeders'), 72 | ], ['core-seeders', 'enso-seeders']); 73 | 74 | return $this; 75 | } 76 | 77 | private function publishResources() 78 | { 79 | $this->publishes([ 80 | __DIR__.'/../resources/images' => resource_path('images'), 81 | ], ['core-assets', 'enso-assets']); 82 | 83 | $this->publishes([ 84 | __DIR__.'/../resources/views/mail' => resource_path('views/vendor/mail'), 85 | ], ['core-email', 'enso-email']); 86 | 87 | return $this; 88 | } 89 | 90 | private function setFactoryResolver() 91 | { 92 | Factory::guessFactoryNamesUsing(new FactoryResolver()); 93 | 94 | if (!class_exists('\Faker\Generator')) { 95 | App::bind(\Faker\Generator::class, Dummy::class); 96 | } 97 | 98 | return $this; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | Auth::check()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Commands/AnnounceAppUpdate.php: -------------------------------------------------------------------------------- 1 | info('Users will be notified.'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/ClearPreferences.php: -------------------------------------------------------------------------------- 1 | info('Preferences were successfully cleared.'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/ResetStorage.php: -------------------------------------------------------------------------------- 1 | option('include'))->explode(','); 22 | 23 | Collection::wrap(self::Folders)->concat($include)->filter() 24 | ->each(fn ($directory) => $this->reset($directory)); 25 | 26 | if (in_array(self::TestingFolder, Storage::directories())) { 27 | Storage::deleteDirectory(self::TestingFolder); 28 | } 29 | } 30 | 31 | private function reset($directory): void 32 | { 33 | if (Storage::has($directory)) { 34 | Collection::wrap(Storage::files($directory)) 35 | ->each(fn ($file) => Storage::delete($file)); 36 | 37 | return; 38 | } 39 | 40 | Storage::makeDirectory($directory); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/UpdateGlobalPreferences.php: -------------------------------------------------------------------------------- 1 | 'core-preferences', 24 | '--force' => true, 25 | ]); 26 | 27 | DB::transaction(function () { 28 | Preference::each(fn ($preference) => $this->update($preference)); 29 | }); 30 | 31 | $this->info('Preferences were successfully updated.'); 32 | } 33 | 34 | private function update($preference) 35 | { 36 | $meta = $preference->value; 37 | 38 | $this->diff($meta) 39 | ->each(fn ($key) => $meta->global->{$key} = $this->default()->global->{$key}); 40 | 41 | $preference->update(['value' => $meta]); 42 | } 43 | 44 | private function diff($meta) 45 | { 46 | return Collection::wrap($this->default()->global)->keys() 47 | ->diff(Collection::wrap($meta->global)->keys()); 48 | } 49 | 50 | private function default() 51 | { 52 | return $this->default ??= DefaultPreferences::data(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Commands/Version.php: -------------------------------------------------------------------------------- 1 | info("Current version is {$version->current()}"); 18 | 19 | if ($version->isOutdated()) { 20 | $this->warn("Latest version is {$version->latest()}"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Contracts/ProvidesState.php: -------------------------------------------------------------------------------- 1 | [LoginListener::class], 15 | PasswordReset::class => [PasswordResetListener::class], 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Events/AppUpdate.php: -------------------------------------------------------------------------------- 1 | queue = 'notifications'; 24 | 25 | $this->message = 'The application was updated, please refresh your page to load the latest application version'; 26 | } 27 | 28 | public function broadcastOn() 29 | { 30 | return new PrivateChannel('app-updates'); 31 | } 32 | 33 | public function broadcastAs() 34 | { 35 | return 'app-update'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Events/Login.php: -------------------------------------------------------------------------------- 1 | user; 24 | } 25 | 26 | public function ip(): string 27 | { 28 | return $this->ip; 29 | } 30 | 31 | public function userAgent(): string 32 | { 33 | return $this->userAgent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/Authentication.php: -------------------------------------------------------------------------------- 1 | trans($response)]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | maxAttempts = Config::get('enso.auth.maxLoginAttempts'); 30 | } 31 | 32 | protected function attemptLogin(Request $request) 33 | { 34 | $this->user = $this->loggableUser($request); 35 | 36 | if (!$this->user) { 37 | return false; 38 | } 39 | 40 | if ($request->attributes->get('sanctum')) { 41 | Auth::guard('web')->login($this->user, $request->input('remember')); 42 | } 43 | 44 | Event::dispatch($this->user, $request->ip(), $request->header('User-Agent')); 45 | 46 | return true; 47 | } 48 | 49 | protected function sendLoginResponse(Request $request) 50 | { 51 | $this->clearLoginAttempts($request); 52 | 53 | if ($request->attributes->get('sanctum')) { 54 | $request->session()->regenerate(); 55 | 56 | return [ 57 | 'auth' => Auth::check(), 58 | 'csrfToken' => csrf_token(), 59 | ]; 60 | } 61 | 62 | $token = $this->user->createToken($request->get('device_name')); 63 | 64 | return response()->json(['token' => $token->plainTextToken]) 65 | ->cookie('webview', true) 66 | ->cookie('Authorization', $token->plainTextToken); 67 | } 68 | 69 | protected function validateLogin(Request $request) 70 | { 71 | $attributes = [ 72 | $this->username() => 'required|string', 73 | 'password' => 'required|string', 74 | ]; 75 | 76 | if (!$request->attributes->get('sanctum')) { 77 | $attributes['device_name'] = 'required|string'; 78 | } 79 | 80 | $request->validate($attributes); 81 | } 82 | 83 | private function loggableUser(Request $request) 84 | { 85 | $user = User::whereEmail($request->input('email'))->first(); 86 | 87 | if (!$user?->currentPasswordIs($request->input('password'))) { 88 | return; 89 | } 90 | 91 | if ($user->passwordExpired()) { 92 | throw ValidationException::withMessages([ 93 | 'email' => 'Password expired. Please set a new one.', 94 | ]); 95 | } 96 | 97 | if ($user->isInactive()) { 98 | throw ValidationException::withMessages([ 99 | 'email' => 'Account disabled. Please contact the administrator.', 100 | ]); 101 | } 102 | 103 | return $user; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | reset($request); 25 | } 26 | 27 | protected function sendResetResponse(Request $request, $response) 28 | { 29 | return ['status' => trans($response)]; 30 | } 31 | 32 | protected function resetPassword($user, $password) 33 | { 34 | Auth::setUser($user); 35 | 36 | $this->setUserPassword($user, $password); 37 | 38 | $user->setRememberToken(Str::random(60)); 39 | 40 | $user->save(); 41 | 42 | event(new PasswordReset($user)); 43 | } 44 | 45 | protected function sendResetFailedResponse(Request $request, $response) 46 | { 47 | throw new UnprocessableEntityHttpException(trans($response)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Controllers/Guest.php: -------------------------------------------------------------------------------- 1 | user()->resetPreferences(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Controllers/Preferences/Store.php: -------------------------------------------------------------------------------- 1 | has('global')) { 14 | Auth::user()->storeGlobalPreferences($request->get('global')); 15 | 16 | return; 17 | } 18 | 19 | Auth::user()->storeLocalPreferences( 20 | $request->get('route'), 21 | $request->get('value') 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Controllers/Spa.php: -------------------------------------------------------------------------------- 1 | handle(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Middleware/AuthorizationCookie.php: -------------------------------------------------------------------------------- 1 | bearerToken() && $request->cookie('Authorization')) { 10 | $request->headers->set( 11 | 'Authorization', 12 | "Bearer {$request->cookie('Authorization')}" 13 | ); 14 | } 15 | 16 | return $next($request); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Http/Middleware/EnsureFrontendRequestsAreStateful.php: -------------------------------------------------------------------------------- 1 | header('webview') 12 | && !$request->cookie('webview') 13 | && parent::fromFrontend($request); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Http/Middleware/VerifyActiveState.php: -------------------------------------------------------------------------------- 1 | user()->isInactive()) { 16 | $this->logout($request); 17 | 18 | throw Authentication::disabledAccount(); 19 | } 20 | 21 | return $next($request); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Middleware/XssSanitizer.php: -------------------------------------------------------------------------------- 1 | antiXss = $antiXss; 14 | } 15 | 16 | public function handle($request, $next, ...$fields) 17 | { 18 | $request->merge($this->clean($request->all($fields))); 19 | 20 | return $next($request); 21 | } 22 | 23 | private function clean($input) 24 | { 25 | return $this->antiXss->removeEvilAttributes(['style']) 26 | ->xss_clean($input); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Requests/ValidatePassword.php: -------------------------------------------------------------------------------- 1 | 'exists:users,email', 21 | 'password' => [ 22 | 'nullable', 23 | 'confirmed', 24 | Password::defaults(), 25 | $this->distinctPassword(), 26 | ], 27 | ]; 28 | } 29 | 30 | protected function currentUser() 31 | { 32 | return $this->route('user') 33 | ?? User::whereEmail($this->get('email'))->first(); 34 | } 35 | 36 | private function distinctPassword(): ?DistinctPassword 37 | { 38 | $user = $this->currentUser(); 39 | 40 | return $user 41 | ? new DistinctPassword($this->currentUser()) 42 | : null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Http/Responses/GuestState.php: -------------------------------------------------------------------------------- 1 | has('locale')) { 15 | App::setLocale($request->get('locale')); 16 | } 17 | 18 | return [ 19 | 'meta' => $this->meta(), 20 | 'i18n' => $this->i18n(), 21 | 'routes' => $this->routes(), 22 | ]; 23 | } 24 | 25 | protected function meta(): array 26 | { 27 | return [ 28 | 'appName' => config('app.name'), 29 | 'appUrl' => url('/').'/', 30 | 'extendedDocumentTitle' => config('enso.config.extendedDocumentTitle'), 31 | 'showQuote' => config('enso.config.showQuote'), 32 | ]; 33 | } 34 | 35 | protected function i18n(): array 36 | { 37 | return [ 38 | App::getLocale() => [ 39 | 'Email' => __('Email'), 40 | 'Mobile' => __('Mobile'), 41 | 'Password' => __('Password'), 42 | 'Remember me' => __('Remember me'), 43 | 'Forgot password' => __('Forgot password'), 44 | 'Login' => __('Login'), 45 | 'Send a reset password link' => __('Send a reset password link'), 46 | 'Repeat Password' => __('Repeat Password'), 47 | 'Success' => __('Success'), 48 | 'Error' => __('Error'), 49 | ], 50 | ]; 51 | } 52 | 53 | protected function routes(): Collection 54 | { 55 | $authRoutes = new Collection(['login', 'password.email', 'password.reset']); 56 | 57 | return Collection::wrap(Route::getRoutes()->getRoutesByName()) 58 | ->filter(fn ($route, $name) => $authRoutes->contains($name)) 59 | ->map(fn ($route) => (new Collection($route)) 60 | ->only(['uri', 'methods']) 61 | ->put('domain', $route->domain())); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Listeners/LoginListener.php: -------------------------------------------------------------------------------- 1 | $event->user()->id, 15 | 'ip' => $event->ip(), 16 | 'user_agent' => $event->userAgent(), 17 | ]); 18 | 19 | if ($event->user()->needsPasswordChange()) { 20 | $event->user()->notify((new PasswordExpiresSoon()) 21 | ->onQueue('notifications')); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/PasswordResetListener.php: -------------------------------------------------------------------------------- 1 | 0) { 12 | $event->user->password_updated_at = Carbon::now(); 13 | $event->user->save(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MiddlewareServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['router'] 19 | ->aliasMiddleware('verify-active-state', VerifyActiveState::class); 20 | 21 | $this->app['router'] 22 | ->aliasMiddleware('xss-sanitizer', XssSanitizer::class); 23 | 24 | $this->app['router'] 25 | ->aliasMiddleware('ensure-frontent-requests-are-stateful', Stateful::class); 26 | 27 | $this->app['router']->middlewareGroup('core', [ 28 | VerifyActiveState::class, 29 | ActionLogger::class, 30 | Impersonate::class, 31 | VerifyRouteAccess::class, 32 | SetLanguage::class, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/Login.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Models/Preference.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 20 | } 21 | 22 | protected function casts(): array 23 | { 24 | return ['value' => 'object']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Notifications/PasswordExpiresSoon.php: -------------------------------------------------------------------------------- 1 | __('Your password will expire soon').'. '.$this->body($notifiable), 23 | 'path' => '#', 24 | 'icon' => 'cogs', 25 | ]; 26 | } 27 | 28 | public function toBroadcast($notifiable) 29 | { 30 | return (new BroadcastMessage([ 31 | 'level' => 'warning', 32 | 'title' => __('Your password will expire soon'), 33 | 'body' => $this->body($notifiable), 34 | ]))->onQueue($this->queue); 35 | } 36 | 37 | private function body($notifiable) 38 | { 39 | $daysLeft = $notifiable->passwordExpiresIn(); 40 | 41 | if ($daysLeft > 1) { 42 | return __("You've got :days days left to change it", [ 43 | 'days' => $daysLeft, 44 | ]); 45 | } 46 | 47 | if ($daysLeft === 1) { 48 | return __("You've got until tomorrow to change it"); 49 | } 50 | 51 | return __('You must change it today'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Notifications/ResetPassword.php: -------------------------------------------------------------------------------- 1 | token = $token; 20 | } 21 | 22 | public function via() 23 | { 24 | return ['mail']; 25 | } 26 | 27 | public function toMail($notifiable) 28 | { 29 | $appName = Config::get('app.name'); 30 | 31 | return (new MailMessage()) 32 | ->subject("[ {$appName} ] {$this->title()}") 33 | ->markdown('laravel-enso/core::emails.reset', [ 34 | 'name' => $notifiable->person->name, 35 | 'url' => url("password/reset/{$this->token}"), 36 | ]); 37 | } 38 | 39 | private function title(): string 40 | { 41 | return __('Reset password request'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/PasswordServiceProvider.php: -------------------------------------------------------------------------------- 1 | Password::min($password['minLength']) 16 | ->when($password['numeric'], fn ($pass) => $pass->numeric()) 17 | ->when($password['special'], fn ($pass) => $pass->symbols()) 18 | ->when($password['mixedCase'], fn ($pass) => $pass->mixedCase())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Rules/DistinctPassword.php: -------------------------------------------------------------------------------- 1 | user = $user; 15 | } 16 | 17 | public function passes($attribute, $value) 18 | { 19 | return !$value || !$this->user->currentPasswordIs($value); 20 | } 21 | 22 | public function message() 23 | { 24 | return __('You cannot use the existing password'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/DefaultPreferences.php: -------------------------------------------------------------------------------- 1 | object(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Services/Inspiring.php: -------------------------------------------------------------------------------- 1 | random(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Services/State/Builder.php: -------------------------------------------------------------------------------- 1 | sources() 15 | ->map(fn ($source) => $this->state($source)) 16 | ->filter->isNotEmpty() 17 | ->collapse() 18 | ->toArray(); 19 | } 20 | 21 | private function sources(): Collection 22 | { 23 | return Collection::wrap(Config::get('enso.state.vendors')) 24 | ->map(fn ($vendor) => base_path('vendor'.DIRECTORY_SEPARATOR.$vendor)) 25 | ->map(fn ($vendor) => File::directories($vendor)) 26 | ->flatten() 27 | ->push(base_path()) 28 | ->map(fn ($path) => new Source($path)); 29 | } 30 | 31 | private function state(Source $source): Collection 32 | { 33 | return $source->providers() 34 | ->map(fn ($provider) => App::make($provider)) 35 | ->map(fn ($provider) => [ 36 | 'mutation' => $provider->mutation(), 37 | 'state' => $provider->state(), 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/State/Source.php: -------------------------------------------------------------------------------- 1 | folder()) 25 | ? Collection::wrap(File::allFiles($this->folder())) 26 | ->map(fn (SplFileInfo $file) => $this->class($file)) 27 | ->filter(fn ($class) => $this->qualifies($class)) 28 | : new Collection(); 29 | } 30 | 31 | private function qualifies(string $class): bool 32 | { 33 | return (new ReflectionClass($class)) 34 | ->implementsInterface(ProvidesState::class); 35 | } 36 | 37 | private function class(SplFileInfo $file): string 38 | { 39 | return Collection::wrap([ 40 | rtrim($this->psr4Namespace(), '\\'), 41 | self::Folder, 42 | $file->getRelativePath(self::Folder), 43 | $file->getFilenameWithoutExtension(), 44 | ])->filter()->implode('\\'); 45 | } 46 | 47 | private function folder(): string 48 | { 49 | return Collection::wrap([ 50 | $this->sourceFolder, 51 | rtrim($this->psr4Folder(), DIRECTORY_SEPARATOR), 52 | self::Folder, 53 | ])->implode(DIRECTORY_SEPARATOR); 54 | } 55 | 56 | private function psr4Folder(): string 57 | { 58 | return $this->composer()['autoload']['psr-4'][$this->psr4Namespace()]; 59 | } 60 | 61 | private function psr4Namespace(): string 62 | { 63 | return key($this->composer()['autoload']['psr-4']); 64 | } 65 | 66 | private function composer(): array 67 | { 68 | return $this->composer 69 | ??= (new JsonReader($this->composerPath()))->array(); 70 | } 71 | 72 | private function composerPath(): string 73 | { 74 | return $this->sourceFolder.DIRECTORY_SEPARATOR.'composer.json'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Services/Version.php: -------------------------------------------------------------------------------- 1 | release 17 | ??= Http::get(self::Endpoint)->throw()->json('tag_name'); 18 | } 19 | 20 | public function current(): string 21 | { 22 | return Config::get('enso.config.version'); 23 | } 24 | 25 | public function isOutdated(): bool 26 | { 27 | return $this->current() !== $this->latest(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Websockets.php: -------------------------------------------------------------------------------- 1 | channels = new Collection(); 15 | } 16 | 17 | public function register($channels): void 18 | { 19 | Collection::wrap($channels) 20 | ->each(fn ($channel, $key) => $this->channels 21 | ->put($key, $channel)); 22 | } 23 | 24 | public function remove($aliases): void 25 | { 26 | Collection::wrap($aliases) 27 | ->each(fn ($alias) => $this->channels->forget($alias)); 28 | } 29 | 30 | public function all() 31 | { 32 | return $this->channels->map(fn ($channel) => is_string($channel) 33 | ? $channel 34 | : $channel->call($this, Auth::user())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/State/Meta.php: -------------------------------------------------------------------------------- 1 | Config::get('app.name'), 21 | 'appUrl' => url('/').'/', 22 | 'csrfToken' => csrf_token(), 23 | 'dateFormat' => Config::get('enso.config.dateFormat'), 24 | 'dateTimeFormat' => Config::get('enso.config.dateFormat').' H:i:s', 25 | 'env' => App::environment(), 26 | 'extendedDocumentTitle' => Config::get('enso.config.extendedDocumentTitle'), 27 | 'quote' => Inspiring::quote(), 28 | 'sentryDsn' => Config::get('sentry.dsn'), 29 | 'version' => Config::get('enso.config.version'), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/State/Preferences.php: -------------------------------------------------------------------------------- 1 | preferences(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/State/Themes.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'key' => Config::get('broadcasting.connections.pusher.key'), 21 | 'options' => Config::get('broadcasting.connections.pusher.options'), 22 | ], 23 | 'authEndpoint' => '/api/broadcasting/auth', 24 | 'channels' => Facade::all(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/HasPassword.php: -------------------------------------------------------------------------------- 1 | password && Hash::check($password, $this->password); 14 | } 15 | 16 | public function passwordExpired() 17 | { 18 | $lifetime = (int) config('enso.auth.password.lifetime'); 19 | 20 | $updatedAt = $this->password_updated_at; 21 | 22 | return $lifetime > 0 && ($updatedAt === null 23 | || (int) Carbon::now()->diffInDays($updatedAt, true) > $lifetime); 24 | } 25 | 26 | public function needsPasswordChange() 27 | { 28 | return (int) config('enso.auth.password.lifetime') > 0 29 | && $this->password_updated_at !== null 30 | && $this->passwordExpiresIn() <= 3; 31 | } 32 | 33 | public function passwordExpiresIn() 34 | { 35 | return (int) $this->password_updated_at 36 | ->addDays((int) config('enso.auth.password.lifetime')) 37 | ->diffInDays(Carbon::now(), true); 38 | } 39 | 40 | public function sendResetPasswordEmail() 41 | { 42 | $this->sendPasswordResetNotification( 43 | app('auth.password.broker') 44 | ->createToken($this) 45 | ); 46 | } 47 | 48 | public function sendPasswordResetNotification($token) 49 | { 50 | $this->notify((new ResetPassword($token)) 51 | ->onQueue('notifications')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Traits/Login.php: -------------------------------------------------------------------------------- 1 | validateLogin($request); 12 | 13 | if ( 14 | method_exists($this, 'hasTooManyLoginAttempts') && 15 | $this->hasTooManyLoginAttempts($request) 16 | ) { 17 | $this->fireLockoutEvent($request); 18 | 19 | return $this->sendLockoutResponse($request); 20 | } 21 | 22 | if ($this->attemptLogin($request)) { 23 | if ($request->attributes->get('sanctum')) { 24 | $request->session()->put('auth.password_confirmed_at', time()); 25 | } 26 | 27 | return $this->sendLoginResponse($request); 28 | } 29 | 30 | $this->incrementLoginAttempts($request); 31 | 32 | return $this->sendFailedLoginResponse($request); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Traits/Logout.php: -------------------------------------------------------------------------------- 1 | attributes->get('sanctum')) { 13 | Auth::guard('web')->logout(); 14 | $request->session()->invalidate(); 15 | } else { 16 | $request->user()->currentAccessToken()->delete(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WebsocketServiceProvider.php: -------------------------------------------------------------------------------- 1 | coreChannels() 20 | : Websockets::register($this->register); 21 | } 22 | 23 | private function coreChannels() 24 | { 25 | Websockets::register([ 26 | 'appUpdates' => 'app-updates', 27 | 'private' => $this->private(), 28 | ]); 29 | } 30 | 31 | private function private(): Closure 32 | { 33 | $segments = explode('\\', Config::get('auth.providers.users.model')); 34 | 35 | return fn (User $user) => Collection::wrap([...$segments, $user->id])->implode('.'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stubs/development-index.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Enso 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /stubs/production-index.blade.stub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Enso 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/features/LoginTest.php: -------------------------------------------------------------------------------- 1 | seed(); 31 | 32 | $this->testModel = $this->user(); 33 | 34 | $this->spaGuard = Arr::wrap(Config::get('sanctum.guard', 'web'))[0]; 35 | 36 | Config::set('sanctum.stateful', [self::SpaUrl]); 37 | } 38 | 39 | /** @test */ 40 | public function can_login_from_spa() 41 | { 42 | $response = $this->loginSpa(); 43 | 44 | $response->assertJson(['auth' => true]); 45 | 46 | $this->assertAuthenticatedAs($this->testModel, $this->spaGuard); 47 | } 48 | 49 | /** @test */ 50 | public function can_login_from_api() 51 | { 52 | $response = $this->loginApi(); 53 | 54 | $this->assertTokenAuthenticate($response->json('token')); 55 | } 56 | 57 | /** @test */ 58 | public function can_login_from_webview() 59 | { 60 | $response = $this->disableCookieEncryption() 61 | ->withCookie('webview', true) 62 | ->loginApi(null, self::SpaUrl); 63 | 64 | $this->assertTokenAuthenticate($response->json('token')); 65 | } 66 | 67 | /** @test */ 68 | public function can_authenticate_token_api() 69 | { 70 | $response = $this->loginApi(); 71 | 72 | $this->get(route('core.home.index'), [ 73 | 'Authorization' => 'Bearer '.$response->json('token'), 74 | ])->assertOk(); 75 | } 76 | 77 | /** @test */ 78 | public function can_authenticate_cookie_api() 79 | { 80 | $response = $this->loginApi(); 81 | 82 | $this->disableCookieEncryption() 83 | ->withCookie('Authorization', $response->json('token')) 84 | ->get(route('core.home.index')) 85 | ->assertOk(); 86 | } 87 | 88 | /** @test */ 89 | public function can_logout_from_spa() 90 | { 91 | $this->loginSpa(); 92 | 93 | $this->post(route('logout'), [], [ 94 | 'referer' => self::SpaUrl, 95 | ]); 96 | 97 | $this->assertFalse($this->isAuthenticated($this->spaGuard)); 98 | } 99 | 100 | /** @test */ 101 | public function can_logout_from_api() 102 | { 103 | $response = $this->loginApi(); 104 | 105 | $this->post(route('logout'), [], [ 106 | 'Authorization' => 'Bearer '.$response->json('token'), 107 | ]); 108 | 109 | $this->assertFalse($this->isAuthenticated('sanctum')); 110 | 111 | $this->assertTrue($this->testModel->tokens->isEmpty()); 112 | } 113 | 114 | /** @test */ 115 | public function cannot_login_from_api() 116 | { 117 | $this->loginApi(self::WrongPassword); 118 | 119 | $this->assertFalse($this->isAuthenticated('sanctum')); 120 | } 121 | 122 | /** @test */ 123 | public function cannot_login_from_spa() 124 | { 125 | $this->loginSpa(self::WrongPassword); 126 | 127 | $this->assertFalse($this->isAuthenticated()); 128 | } 129 | 130 | private function loginApi($password = null, $referer = null): TestResponse 131 | { 132 | return $this->post(route('login'), [ 133 | 'email' => $this->testModel->email, 134 | 'password' => $password ?? self::Password, 135 | 'device_name' => 'mobile', 136 | ], [ 137 | 'referer' => $referer, 138 | ]); 139 | } 140 | 141 | private function loginSpa($password = null): TestResponse 142 | { 143 | return $this->post(route('login'), [ 144 | 'email' => $this->testModel->email, 145 | 'password' => $password ?? self::Password, 146 | ], ['referer' => self::SpaUrl]); 147 | } 148 | 149 | private function user(): User 150 | { 151 | $user = User::first(); 152 | $user->password = Hash::make(self::Password); 153 | $user->is_active = true; 154 | 155 | return tap($user)->save(); 156 | } 157 | 158 | protected function assertTokenAuthenticate($token): void 159 | { 160 | $token = PersonalAccessToken::findToken($token); 161 | $this->assertTrue($token->tokenable->is($this->testModel)); 162 | } 163 | } 164 | --------------------------------------------------------------------------------