├── .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 | [](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 | [](https://github.styleci.io/repos/85807594)
5 | [](https://packagist.org/packages/laravel-enso/core)
6 | [](https://packagist.org/packages/laravel-enso/core)
7 | [](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 |
--------------------------------------------------------------------------------
/resources/images/corners/bottom-center.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/corners/bottom-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/corners/bottom-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/corners/top-center.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/corners/top-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/corners/top-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
70 |
--------------------------------------------------------------------------------
/resources/images/emails/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/images/emails/instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/resources/images/emails/tiktok.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/resources/images/emails/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------