14 |
15 | #### This is a groundwork for a large modular SPA, that utilises Laravel, Vue, ElementPlus.
16 | #### CRUD generator is integrated in project creates standalone modules on the frontend and backend.
17 |
18 |
19 |
20 |
21 |
22 | The main goals of the project are:
23 | - to avoid high cohesion between modules
24 | - to form the basis for writing clean code
25 | - to be easy to expand
26 | - to avoid code duplication
27 | - to reduce the start time of the project
28 | - to reduce the time of project support and code navigation
29 | - to be understandable for an inexperienced programmer
30 |
31 | ## Extensions
32 |
33 | - Back-End: [Laravel 11](https://laravel.com/)
34 | - Front-End: [Vue3 Composition Api](https://vuejs.org) + [VueRouter](https://router.vuejs.org) + [Pinia](https://pinia.vuejs.org) + [VueI18n](https://kazupon.github.io/vue-i18n/)
35 | - Login using [Vue-Auth](https://websanova.com/docs/vue-auth/home), [Axios](https://github.com/mzabriskie/axios) and [Sanctum](https://laravel.com/docs/8.x/sanctum).
36 | - The api routes, are separate for each module, in **Modules/{ModuleName}/routes_api.php**
37 | - [ElementPlus](https://element-plus.org/) UI Kit
38 | - [Lodash](https://lodash.com) js utilities
39 | - [Day.js](https://dayjs.com) time manipulations
40 | - [FontAwesome 6](http://fontawesome.io/icons/) icons
41 |
42 | ## Install
43 | - `git clone https://github.com/Yurich84/laravel-vue3-spa.git`
44 | - `cd /laravel-vue3-spa`
45 | - `composer install`
46 | - `cp .env.example .env` - copy .env file
47 | - set your DB credentials in `.env`
48 | - `php artisan key:generate`
49 | - `php artisan migrate`
50 | - `yarn install`
51 |
52 | ## Testing
53 |
54 | ### Unit Testing
55 | `php artisan test`
56 |
57 | ## Usage
58 | - `npm run dev` for hot reloading
59 | - `php artisan serve` and go [localhost:8000](http://localhost:8000)
60 | - Create new user and login.
61 |
62 | ### Creating module
63 | You can easily create module with CRUD functionality.
64 |
65 | `php artisan make:module {ModuleName}`
66 |
67 | This will create:
68 |
69 | - **migration** `database/migrations/000_00_00_000000_create_{ModuleName}_table.php`
70 |
71 | - **model** `app/Models/{ModuleName}.php`
72 |
73 | - **factory** `database/factories/{ModuleName}Factory.php`
74 |
75 | - **tests** `tests/Feature/{ModuleName}Test.php`
76 |
77 | - **backend module** `app/Modules/{ModuleName}/`
78 | ```
79 | {ModuleName}/
80 | │
81 | ├── routes_api.php
82 | │
83 | ├── Controllers/
84 | │ └── {ModuleName}Controller.php
85 | │
86 | ├── Requests/
87 | │ └── {ModuleName}Request.php
88 | │
89 | └── Resources/
90 | └── {ModuleName}Resource.php
91 | ```
92 |
93 | - **frontend module** `resources/js/modules/{moduleName}/`
94 | ```
95 | {moduleName}/
96 | │
97 | ├── routes.js
98 | │
99 | ├── {moduleName}Api.js
100 | │
101 | ├── {moduleName}Store.js
102 | │
103 | ├── components/
104 | ├── {ModuleName}List.vue
105 | ├── {ModuleName}View.vue
106 | └── {ModuleName}Form.vue
107 | ```
108 |
109 |
110 | > After creating module, you can edit model and migration by adding fields you need.
111 | > Also you can add this fields into view.
112 | > Don't forget run php artisan migrate.
113 |
114 | Every module loads dynamically.
115 |
116 | ## [Video](https://www.youtube.com/watch?v=0qKNlrmhgNg)
117 |
--------------------------------------------------------------------------------
/app/Console/Commands/MakeBackEndModule.php:
--------------------------------------------------------------------------------
1 | output = new ConsoleOutput;
19 | $this->components = new Factory($this->output);
20 | }
21 |
22 | /**
23 | * @var string
24 | */
25 | private $module_path;
26 |
27 | /**
28 | * @param $module
29 | *
30 | * @throws FileNotFoundException
31 | */
32 | protected function create($module)
33 | {
34 | $this->files = new Filesystem;
35 | $this->module = $module;
36 | $this->module_path = app_path('Modules/'.$this->module);
37 |
38 | $this->createController();
39 | $this->createRoutes();
40 | $this->createRequest();
41 | $this->createResource();
42 | }
43 |
44 | /**
45 | * Create a controller for the module.
46 | *
47 | * @return void
48 | *
49 | * @throws FileNotFoundException
50 | */
51 | private function createController()
52 | {
53 | $path = $this->module_path."/Controllers/{$this->module}Controller.php";
54 |
55 | if ($this->alreadyExists($path)) {
56 | $this->components->error('Controller already exists!');
57 | } else {
58 | $stub = $this->files->get(base_path('stubs/backEnd/controller.api.stub'));
59 |
60 | $this->createFileWithStub($stub, $path);
61 |
62 | $this->components->info('Controller created successfully.');
63 | }
64 | }
65 |
66 | /**
67 | * Create a Routes for the module.
68 | *
69 | * @throws FileNotFoundException
70 | */
71 | private function createRoutes()
72 | {
73 | $path = $this->module_path.'/routes_api.php';
74 |
75 | if ($this->alreadyExists($path)) {
76 | $this->components->error('Routes already exists!');
77 | } else {
78 | $stub = $this->files->get(base_path('stubs/backEnd/routes.api.stub'));
79 |
80 | $this->createFileWithStub($stub, $path);
81 |
82 | $this->components->info('Routes created successfully.');
83 | }
84 | }
85 |
86 | /**
87 | * Create a Request for the module.
88 | *
89 | * @throws FileNotFoundException
90 | */
91 | private function createRequest()
92 | {
93 | $path = $this->module_path."/Requests/{$this->module}Request.php";
94 |
95 | if ($this->alreadyExists($path)) {
96 | $this->components->error('Request already exists!');
97 | } else {
98 | $stub = $this->files->get(base_path('stubs/backEnd/request.stub'));
99 |
100 | $this->createFileWithStub($stub, $path);
101 |
102 | $this->components->info('Request created successfully.');
103 | }
104 | }
105 |
106 | /**
107 | * Create a Resource for the module.
108 | *
109 | * @throws FileNotFoundException
110 | */
111 | private function createResource()
112 | {
113 | $path = $this->module_path."/Resources/{$this->module}Resource.php";
114 |
115 | if ($this->alreadyExists($path)) {
116 | $this->components->error('Resource already exists!');
117 | } else {
118 | $stub = $this->files->get(base_path('stubs/backEnd/resource.stub'));
119 |
120 | $this->createFileWithStub($stub, $path);
121 |
122 | $this->components->info('Resource created successfully.');
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/app/Console/Commands/MakeFrontEndModule.php:
--------------------------------------------------------------------------------
1 | output = new ConsoleOutput;
19 | $this->components = new Factory($this->output);
20 | }
21 |
22 | /**
23 | * @var string
24 | */
25 | private $module_path;
26 |
27 | /**
28 | * @param $module
29 | *
30 | * @throws FileNotFoundException
31 | */
32 | protected function create($module)
33 | {
34 | $this->files = new Filesystem;
35 | $this->module = $module;
36 | $this->module_path = base_path('resources/js/modules/'.lcfirst($this->module));
37 |
38 | $this->createVueList();
39 | $this->createVueView();
40 | $this->createVueForm();
41 |
42 | $this->createStore();
43 |
44 | $this->createRoutes();
45 | $this->createApi();
46 | }
47 |
48 | /**
49 | * Create a Vue component file for the module.
50 | *
51 | * @return void
52 | *
53 | * @throws FileNotFoundException
54 | */
55 | private function createVueList()
56 | {
57 | $path = $this->module_path."/components/{$this->module}List.vue";
58 |
59 | if ($this->alreadyExists($path)) {
60 | $this->components->error('VueList Component already exists!');
61 | } else {
62 | $stub = $this->files->get(base_path('stubs/frontEnd/vue.list.stub'));
63 |
64 | $this->createFileWithStub($stub, $path);
65 |
66 | $this->components->info('VueList Component created successfully.');
67 | }
68 | }
69 |
70 | /**
71 | * Create a Vue component file for the module.
72 | *
73 | * @return void
74 | *
75 | * @throws FileNotFoundException
76 | */
77 | private function createVueView()
78 | {
79 | $path = $this->module_path."/components/{$this->module}View.vue";
80 |
81 | if ($this->alreadyExists($path)) {
82 | $this->components->error('VueView Component already exists!');
83 | } else {
84 | $stub = $this->files->get(base_path('stubs/frontEnd/vue.view.stub'));
85 |
86 | $this->createFileWithStub($stub, $path);
87 |
88 | $this->components->info('VueView Component created successfully.');
89 | }
90 | }
91 |
92 | /**
93 | * Create a Vue component file for the module.
94 | *
95 | * @return void
96 | *
97 | * @throws FileNotFoundException
98 | */
99 | private function createVueForm()
100 | {
101 | $path = $this->module_path."/components/{$this->module}Form.vue";
102 |
103 | if ($this->alreadyExists($path)) {
104 | $this->components->error('VueForm Component already exists!');
105 | } else {
106 | $stub = $this->files->get(base_path('stubs/frontEnd/vue.form.stub'));
107 |
108 | $this->createFileWithStub($stub, $path);
109 |
110 | $this->components->info('VueForm Component created successfully.');
111 | }
112 | }
113 |
114 | /**
115 | * Create a Vue component file for the module.
116 | *
117 | * @return void
118 | *
119 | * @throws FileNotFoundException
120 | */
121 | private function createStore()
122 | {
123 | $moduleLC = lcfirst($this->module);
124 | $path = $this->module_path."/{$moduleLC}Store.js";
125 |
126 | if ($this->alreadyExists($path)) {
127 | $this->components->error('Store already exists!');
128 | } else {
129 | $stub = $this->files->get(base_path('stubs/frontEnd/store.stub'));
130 |
131 | $this->createFileWithStub($stub, $path);
132 |
133 | $this->components->info('Store created successfully.');
134 | }
135 | }
136 |
137 | /**
138 | * Create a Vue component file for the module.
139 | *
140 | * @return void
141 | *
142 | * @throws FileNotFoundException
143 | */
144 | private function createRoutes()
145 | {
146 | $path = $this->module_path.'/routes.js';
147 |
148 | if ($this->alreadyExists($path)) {
149 | $this->components->error('Vue Routes already exists!');
150 | } else {
151 | $stub = $this->files->get(base_path('stubs/frontEnd/routes.stub'));
152 |
153 | $this->createFileWithStub($stub, $path);
154 |
155 | $this->components->info('Vue Routes created successfully.');
156 | }
157 | }
158 |
159 | /**
160 | * Create a Vue component file for the module.
161 | *
162 | * @return void
163 | *
164 | * @throws FileNotFoundException
165 | */
166 | private function createApi()
167 | {
168 | $moduleLC = lcfirst($this->module);
169 | $path = $this->module_path."/{$moduleLC}Api.js";
170 |
171 | if ($this->alreadyExists($path)) {
172 | $this->components->error('Api file already exists!');
173 | } else {
174 | $stub = $this->files->get(base_path('stubs/frontEnd/api.stub'));
175 |
176 | $this->createFileWithStub($stub, $path);
177 |
178 | $this->components->info('Api file created successfully.');
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/app/Console/Commands/MakeModuleCommand.php:
--------------------------------------------------------------------------------
1 | files = $files;
60 |
61 | $this->module = Str::of(class_basename($this->argument('name')))->studly()->singular();
62 |
63 | $this->createModel();
64 |
65 | $this->createMigration();
66 |
67 | $backEndModule->create($this->module);
68 |
69 | $frontEndModule->create($this->module);
70 |
71 | $this->createFactory();
72 |
73 | $this->createTest();
74 | }
75 |
76 | /**
77 | * Create a model file for the module.
78 | *
79 | * @return void
80 | */
81 | protected function createModel()
82 | {
83 | $this->call('make:model', [
84 | 'name' => $this->module,
85 | ]);
86 | }
87 |
88 | /**
89 | * Create a migration file for the module.
90 | *
91 | * @return void
92 | */
93 | protected function createMigration()
94 | {
95 | $table = $this->module->plural()->snake();
96 |
97 | try {
98 | $this->call('make:migration', [
99 | 'name' => "create_{$table}_table",
100 | '--create' => $table,
101 | ]);
102 | } catch (Exception $e) {
103 | $this->error($e->getMessage());
104 | }
105 | }
106 |
107 | /**
108 | * Create a factory file for the module.
109 | *
110 | * @return void
111 | */
112 | protected function createFactory()
113 | {
114 | $this->call('make:factory', [
115 | 'name' => $this->module.'Factory',
116 | '--model' => "$this->module",
117 | ]);
118 | }
119 |
120 | /**
121 | * Create a test file for the module.
122 | *
123 | * @return void
124 | *
125 | * @throws FileNotFoundException
126 | */
127 | protected function createTest()
128 | {
129 | $path = base_path('tests/Feature/'.$this->module.'Test.php');
130 |
131 | if ($this->alreadyExists($path)) {
132 | $this->error('Test file already exists!');
133 | } else {
134 | $stub = (new Filesystem)->get(base_path('stubs/test.stub'));
135 |
136 | $this->createFileWithStub($stub, $path);
137 |
138 | $this->components->info('Tests created successfully.');
139 | }
140 | }
141 |
142 | /**
143 | * Determine if the class already exists.
144 | *
145 | * @param string $path
146 | * @return bool
147 | */
148 | protected function alreadyExists($path)
149 | {
150 | return $this->files->exists($path);
151 | }
152 |
153 | /**
154 | * Build the directory for the class if necessary.
155 | *
156 | * @param string $path
157 | * @return string
158 | */
159 | protected function makeDirectory($path)
160 | {
161 | if (! $this->files->isDirectory(dirname($path))) {
162 | $this->files->makeDirectory(dirname($path), 0777, true, true);
163 | }
164 |
165 | return $path;
166 | }
167 |
168 | /**
169 | * @param $stub
170 | * @param $path
171 | * @return void
172 | */
173 | protected function createFileWithStub($stub, $path)
174 | {
175 | $this->makeDirectory($path);
176 |
177 | $content = str_replace([
178 | 'DummyRootNamespace',
179 | 'DummySingular',
180 | 'DummyPlural',
181 | 'DUMMY_VARIABLE_SINGULAR',
182 | 'DUMMY_VARIABLE_PLURAL',
183 | 'dummyVariableSingular',
184 | 'dummyVariablePlural',
185 | 'dummy-plural',
186 | ], [
187 | App::getNamespace(),
188 | $this->module,
189 | $this->module->pluralStudly(),
190 | $this->module->snake()->upper(),
191 | $this->module->plural()->snake()->upper(),
192 | lcfirst($this->module),
193 | lcfirst($this->module->pluralStudly()),
194 | lcfirst($this->module->plural()->snake('-')),
195 | ],
196 | $stub
197 | );
198 |
199 | $this->files->put($path, $content);
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('inspire')
28 | // ->hourly();
29 | }
30 |
31 | /**
32 | * Register the commands for the application.
33 | *
34 | * @return void
35 | */
36 | protected function commands()
37 | {
38 | $this->load(__DIR__.'/Commands');
39 |
40 | require base_path('routes/console.php');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Contracts/RepositoryInterface.php:
--------------------------------------------------------------------------------
1 | [__('You must :linkOpen verify :linkClose your email first.', [
18 | 'linkOpen' => '',
19 | 'linkClose' => '',
20 | ])],
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EnsureEmailIsVerified.php:
--------------------------------------------------------------------------------
1 | user() ||
20 | ($request->user() instanceof MustVerifyEmail &&
21 | ! $request->user()->hasVerifiedEmail())) {
22 | return response()->json(['message' => 'Your email address is not verified.'], 409);
23 | }
24 |
25 | return $next($request);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Models/PersonalAccessToken.php:
--------------------------------------------------------------------------------
1 | 'datetime',
51 | ];
52 |
53 | /**
54 | * The accessors to append to the model's array form.
55 | *
56 | * @var array
57 | */
58 | protected $appends = [
59 | 'avatar',
60 | ];
61 |
62 | /**
63 | * Get the profile photo URL attribute.
64 | *
65 | * @return string
66 | */
67 | public function getAvatarAttribute()
68 | {
69 | return 'https://www.gravatar.com/avatar/'.md5(strtolower($this->email)).'.jpg?s=200&d=mm';
70 | }
71 |
72 | /**
73 | * Send the password reset notification.
74 | *
75 | * @param string $token
76 | * @return void
77 | */
78 | public function sendPasswordResetNotification($token)
79 | {
80 | $this->notify(new ResetPassword($token));
81 | }
82 |
83 | /**
84 | * Send the email verification notification.
85 | *
86 | * @return void
87 | */
88 | public function sendEmailVerificationNotification()
89 | {
90 | $this->notify(new VerifyEmail);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/AuthenticatedSessionController.php:
--------------------------------------------------------------------------------
1 | authenticate();
19 |
20 | $request->session()->regenerate();
21 |
22 | return response()->noContent();
23 | }
24 |
25 | /**
26 | * Destroy an authenticated session.
27 | */
28 | public function destroy(Request $request): Response
29 | {
30 | Auth::guard('web')->logout();
31 |
32 | $request->session()->invalidate();
33 |
34 | $request->session()->regenerateToken();
35 |
36 | return response()->noContent();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/AuthenticatedTokenController.php:
--------------------------------------------------------------------------------
1 | authenticate();
20 |
21 | $token = $user->createToken($request->device_name)->plainTextToken;
22 |
23 | return response()->json([
24 | 'token' => $token,
25 | ])->header('Authorization', $token);
26 | }
27 |
28 | /**
29 | * Destroy an authenticated session.
30 | */
31 | public function destroy(Request $request): Response
32 | {
33 | $request->user()->currentAccessToken()->delete();
34 | Auth::guard('api')->forgetUser();
35 | app()->get('auth')->forgetGuards();
36 |
37 | return response()->noContent();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/CurrentUserController.php:
--------------------------------------------------------------------------------
1 | json([
13 | 'data' => auth()->user(),
14 | ]);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/NewPasswordController.php:
--------------------------------------------------------------------------------
1 | validate([
25 | 'token' => ['required'],
26 | 'email' => ['required', 'email'],
27 | 'password' => ['required', 'confirmed', Rules\Password::defaults()],
28 | ]);
29 |
30 | // Here we will attempt to reset the user's password. If it is successful we
31 | // will update the password on an actual user model and persist it to the
32 | // database. Otherwise we will parse the error and return the response.
33 | $status = Password::reset(
34 | $request->only('email', 'password', 'password_confirmation', 'token'),
35 | function ($user) use ($request) {
36 | $user->forceFill([
37 | 'password' => Hash::make($request->password),
38 | 'remember_token' => Str::random(60),
39 | ])->save();
40 |
41 | event(new PasswordReset($user));
42 | }
43 | );
44 |
45 | if ($status != Password::PASSWORD_RESET) {
46 | throw ValidationException::withMessages([
47 | 'email' => [__($status)],
48 | ]);
49 | }
50 |
51 | return response()->json(['status' => __($status)]);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/PasswordResetLinkController.php:
--------------------------------------------------------------------------------
1 | validate([
21 | 'email' => ['required', 'email'],
22 | ]);
23 |
24 | // We will send the password reset link to this user. Once we have attempted
25 | // to send the link, we will examine the response then see the message we
26 | // need to show to the user. Finally, we'll send out a proper response.
27 | $status = Password::sendResetLink(
28 | $request->only('email')
29 | );
30 |
31 | if ($status != Password::RESET_LINK_SENT) {
32 | throw ValidationException::withMessages([
33 | 'email' => [__($status)],
34 | ]);
35 | }
36 |
37 | return response()->json(['status' => __($status)]);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/RegisteredUserController.php:
--------------------------------------------------------------------------------
1 | validate([
23 | 'name' => ['required', 'string', 'max:255'],
24 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
25 | 'password' => ['required', 'confirmed', Rules\Password::defaults()],
26 | ]);
27 |
28 | $user = User::create([
29 | 'name' => $request->name,
30 | 'email' => $request->email,
31 | 'password' => Hash::make($request->password),
32 | ]);
33 |
34 | event(new Registered($user));
35 |
36 | return response()->json($user);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Controllers/VerifyEmailController.php:
--------------------------------------------------------------------------------
1 | json([
26 | 'status' => trans('verification.invalid'),
27 | ], 400);
28 | }
29 |
30 | if ($user->hasVerifiedEmail()) {
31 | return response()->json([
32 | 'status' => trans('verification.already_verified'),
33 | ], 400);
34 | }
35 |
36 | $user->markEmailAsVerified();
37 |
38 | event(new Verified($user));
39 |
40 | return response()->json([
41 | 'status' => trans('verification.verified'),
42 | ]);
43 | }
44 |
45 | /**
46 | * Resend the email verification notification.
47 | *
48 | * @param Request $request
49 | * @return JsonResponse
50 | *
51 | * @throws ValidationException
52 | */
53 | public function resend(Request $request)
54 | {
55 | $request->validate(['email' => 'required|email']);
56 |
57 | /** @var User $user */
58 | $user = User::where('email', $request->email)->first();
59 |
60 | if (is_null($user)) {
61 | throw ValidationException::withMessages([
62 | 'email' => [trans('verification.user')],
63 | ]);
64 | }
65 |
66 | if ($user->hasVerifiedEmail()) {
67 | throw ValidationException::withMessages([
68 | 'email' => [trans('verification.already_verified')],
69 | ]);
70 | }
71 |
72 | $user->sendEmailVerificationNotification();
73 |
74 | return response()->json(['status' => trans('verification.sent')]);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Requests/LoginRequest.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function rules(): array
31 | {
32 | return [
33 | 'email' => ['required', 'string', 'email'],
34 | 'password' => ['required', 'string'],
35 | 'device_name' => ['required', 'string'],
36 | ];
37 | }
38 |
39 | /**
40 | * Attempt to authenticate the request's credentials.
41 | *
42 | * @throws ValidationException
43 | */
44 | public function authenticate(): User
45 | {
46 | $this->ensureIsNotRateLimited();
47 |
48 | $user = User::where('email', strtolower($this->email))->first();
49 |
50 | if (! $user || ! Hash::check($this->password, $user->password)) {
51 | throw ValidationException::withMessages([
52 | 'email' => __('auth.failed'),
53 | ]);
54 | }
55 |
56 | if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) {
57 | throw VerifyEmailException::forUser($user);
58 | }
59 |
60 | RateLimiter::clear($this->throttleKey());
61 |
62 | return $user;
63 | }
64 |
65 | /**
66 | * Ensure the login request is not rate limited.
67 | *
68 | * @throws ValidationException
69 | */
70 | public function ensureIsNotRateLimited(): void
71 | {
72 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
73 | return;
74 | }
75 |
76 | event(new Lockout($this));
77 |
78 | $seconds = RateLimiter::availableIn($this->throttleKey());
79 |
80 | throw ValidationException::withMessages([
81 | 'email' => trans('auth.throttle', [
82 | 'seconds' => $seconds,
83 | 'minutes' => ceil($seconds / 60),
84 | ]),
85 | ]);
86 | }
87 |
88 | /**
89 | * Get the rate limiting throttle key for the request.
90 | */
91 | public function throttleKey(): string
92 | {
93 | return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/Modules/Auth/Requests/LoginSessionRequest.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | public function rules(): array
28 | {
29 | return [
30 | 'email' => ['required', 'string', 'email'],
31 | 'password' => ['required', 'string'],
32 | 'device_name' => ['required', 'string'],
33 | ];
34 | }
35 |
36 | /**
37 | * Attempt to authenticate the request's credentials.
38 | *
39 | * @throws \Illuminate\Validation\ValidationException
40 | */
41 | public function authenticate(): void
42 | {
43 | $this->ensureIsNotRateLimited();
44 |
45 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
46 | RateLimiter::hit($this->throttleKey());
47 |
48 | throw ValidationException::withMessages([
49 | 'email' => __('auth.failed'),
50 | ]);
51 | }
52 |
53 | RateLimiter::clear($this->throttleKey());
54 | }
55 |
56 | /**
57 | * Ensure the login request is not rate limited.
58 | *
59 | * @throws \Illuminate\Validation\ValidationException
60 | */
61 | public function ensureIsNotRateLimited(): void
62 | {
63 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
64 | return;
65 | }
66 |
67 | event(new Lockout($this));
68 |
69 | $seconds = RateLimiter::availableIn($this->throttleKey());
70 |
71 | throw ValidationException::withMessages([
72 | 'email' => trans('auth.throttle', [
73 | 'seconds' => $seconds,
74 | 'minutes' => ceil($seconds / 60),
75 | ]),
76 | ]);
77 | }
78 |
79 | /**
80 | * Get the rate limiting throttle key for the request.
81 | */
82 | public function throttleKey(): string
83 | {
84 | return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/Modules/Auth/routes_api.php:
--------------------------------------------------------------------------------
1 | group(function () {
12 | Route::withoutMiddleware('auth:sanctum')->group(function () {
13 |
14 | Route::post('login', [AuthenticatedTokenController::class, 'store'])->name('login');
15 | Route::post('register', [RegisteredUserController::class, 'store'])->name('register');
16 |
17 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])->name('forgot-password');
18 | Route::post('reset-password', [NewPasswordController::class, 'store'])->name('reset-password');
19 |
20 | });
21 |
22 | Route::post('email/verify/{user}', [VerifyEmailController::class, 'verify'])
23 | ->middleware(['throttle:6,1'])
24 | ->name('verification.verify');
25 |
26 | Route::post('email/resend', [VerifyEmailController::class, 'resend'])
27 | ->middleware(['throttle:6,1'])
28 | ->name('verification.resend');
29 |
30 | Route::post('logout', [AuthenticatedTokenController::class, 'destroy'])->name('logout');
31 | Route::post('me', CurrentUserController::class)->name('me');
32 | });
33 |
--------------------------------------------------------------------------------
/app/Modules/Core/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | user();
23 |
24 | $user->fill($profileRequest->validated())->save();
25 |
26 | return response()->json([
27 | 'type' => self::RESPONSE_TYPE_SUCCESS,
28 | 'message' => 'Successfully updated',
29 | ]);
30 | }
31 |
32 | /**
33 | * Update the user's profile information.
34 | *
35 | * @param ChangePasswordRequest $request
36 | * @return JsonResponse
37 | */
38 | public function changePassword(ChangePasswordRequest $request)
39 | {
40 | /** @var User $user */
41 | $user = auth()->user();
42 |
43 | $user->password = bcrypt($request->password);
44 | $user->save();
45 |
46 | return response()->json([
47 | 'type' => self::RESPONSE_TYPE_SUCCESS,
48 | 'message' => 'Successfully updated',
49 | ]);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Modules/Setting/Requests/ChangePasswordRequest.php:
--------------------------------------------------------------------------------
1 | [
31 | 'string',
32 | 'required',
33 | function ($attribute, $value, $fail) {
34 | if ($value && ! Hash::check($value, auth()->user()->password)) {
35 | $fail(__('passwords.password_not_matched', ['attribute' => $attribute]));
36 | }
37 | },
38 | ],
39 | 'password' => [
40 | 'string',
41 | 'required',
42 | function ($attribute, $value, $fail) {
43 | if ($value && Hash::check($value, auth()->user()->password)) {
44 | $fail(__('passwords.password_matches_old', ['attribute' => $attribute]));
45 | }
46 | },
47 | Password::min(8),
48 | // ->mixedCase()
49 | // ->letters()
50 | // ->numbers()
51 | // ->symbols()
52 | // ->uncompromised(),
53 | ],
54 | 'password_confirmation' => [
55 | 'string',
56 | 'required',
57 | function ($attribute, $value, $fail) {
58 | if ($value != $this->password) {
59 | $fail(__('passwords.password_not_matched', ['attribute' => $attribute]));
60 | }
61 | },
62 | ],
63 | ];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/Modules/Setting/Requests/ProfileRequest.php:
--------------------------------------------------------------------------------
1 | 'required|string',
29 | User::COLUMN_EMAIL => 'required|email|unique:users,email,'.auth()->id(),
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Modules/Setting/routes_api.php:
--------------------------------------------------------------------------------
1 | group(function () {
6 | Route::patch('profile', 'ProfileController@update')->name('profile.update');
7 | Route::patch('change-password', 'ProfileController@changePassword')->name('profile.changePassword');
8 | });
9 |
--------------------------------------------------------------------------------
/app/Notifications/ResetPassword.php:
--------------------------------------------------------------------------------
1 | line('You are receiving this email because we received a password reset request for your account.')
20 | ->action('Reset Password', url(config('app.url').'/password/reset/'.$this->token).'?email='.urlencode($notifiable->email))
21 | ->line('If you did not request a password reset, no further action is required.');
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Notifications/VerifyEmail.php:
--------------------------------------------------------------------------------
1 | addMinutes(60), ['user' => $notifiable->id]
21 | );
22 |
23 | return str_replace('/api/v1/auth', '', $url);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | getEmailForPasswordReset()}";
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | 'App\Policies\ModelPolicy',
16 | ];
17 |
18 | /**
19 | * Register any authentication / authorization services.
20 | *
21 | * @return void
22 | */
23 | public function boot()
24 | {
25 | $this->registerPolicies();
26 |
27 | //
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | ['auth:sanctum']]);
18 |
19 | require base_path('routes/channels.php');
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | [
19 | SendEmailVerificationNotification::class,
20 | ],
21 | ];
22 |
23 | /**
24 | * Register any events for your application.
25 | *
26 | * @return void
27 | */
28 | public function boot()
29 | {
30 | parent::boot();
31 |
32 | //
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Repositories/AbstractRepository.php:
--------------------------------------------------------------------------------
1 | class) {
26 | $this->model = new $this->class;
27 | }
28 | }
29 |
30 | /**
31 | * @param int $id
32 | * @return Model|null
33 | */
34 | public function get(int $id): ?Model
35 | {
36 | return $this->model::find($id);
37 | }
38 |
39 | /**
40 | * @param array $data
41 | * @return Model|null
42 | */
43 | public function create(array $data): ?Model
44 | {
45 | return $this->model::create($data);
46 | }
47 |
48 | /**
49 | * @param array $data
50 | * @param Model $model
51 | * @return Model
52 | */
53 | public function update(array $data, Model $model): Model
54 | {
55 | $model->fill($data)->save();
56 |
57 | return $model;
58 | }
59 |
60 | /**
61 | * @param $id
62 | * @return bool
63 | *
64 | * @throws \Exception
65 | */
66 | public function delete(int $id): bool
67 | {
68 | return $this->model::destroy($id);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/Repositories/UserRepository.php:
--------------------------------------------------------------------------------
1 | handleCommand(new ArgvInput);
14 |
15 | exit($status);
16 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | withRouting(
13 | web: __DIR__.'/../routes/web.php',
14 | api: __DIR__.'/../routes/api.php',
15 | commands: __DIR__.'/../routes/console.php',
16 | health: '/up',
17 | )
18 | ->withMiddleware(function (Middleware $middleware) {
19 | $middleware->api(prepend: [
20 | EnsureFrontendRequestsAreStateful::class,
21 | // \Illuminate\Session\Middleware\StartSession::class,
22 | ]);
23 |
24 | $middleware->alias([
25 | 'verified' => EnsureEmailIsVerified::class,
26 | ]);
27 | })
28 | ->withExceptions(function (Exceptions $exceptions) {
29 | $exceptions->render(function (NotFoundHttpException $e, Request $request) {
30 | if ($request->is('api/*')) {
31 | return response()->json([
32 | 'message' => 'Record not found.',
33 | ], 404);
34 | }
35 | });
36 | })->create();
37 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/bootstrap/providers.php:
--------------------------------------------------------------------------------
1 | env('APP_NAME', 'Laravel Vue SPA Skeleton'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Application Environment
21 | |--------------------------------------------------------------------------
22 | |
23 | | This value determines the "environment" your application is currently
24 | | running in. This may determine how you prefer to configure various
25 | | services the application utilizes. Set this in your ".env" file.
26 | |
27 | */
28 |
29 | 'env' => env('APP_ENV', 'production'),
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Application Debug Mode
34 | |--------------------------------------------------------------------------
35 | |
36 | | When your application is in debug mode, detailed error messages with
37 | | stack traces will be shown on every error that occurs within your
38 | | application. If disabled, a simple generic error page is shown.
39 | |
40 | */
41 |
42 | 'debug' => env('APP_DEBUG', false),
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Application URL
47 | |--------------------------------------------------------------------------
48 | |
49 | | This URL is used by the console to properly generate URLs when using
50 | | the Artisan command line tool. You should set this to the root of
51 | | your application so that it is used when running Artisan tasks.
52 | |
53 | */
54 |
55 | 'url' => env('APP_URL', 'http://localhost'),
56 |
57 | 'api_url' => env('MIX_API_ENDPOINT', 'http://localhost/api'),
58 |
59 | 'asset_url' => env('ASSET_URL', null),
60 |
61 | /*
62 | |--------------------------------------------------------------------------
63 | | Application Timezone
64 | |--------------------------------------------------------------------------
65 | |
66 | | Here you may specify the default timezone for your application, which
67 | | will be used by the PHP date and date-time functions. We have gone
68 | | ahead and set this to a sensible default for you out of the box.
69 | |
70 | */
71 |
72 | 'timezone' => 'UTC',
73 |
74 | /*
75 | |--------------------------------------------------------------------------
76 | | Application Locale Configuration
77 | |--------------------------------------------------------------------------
78 | |
79 | | The application locale determines the default locale that will be used
80 | | by the translation service provider. You are free to set this value
81 | | to any of the locales which will be supported by the application.
82 | |
83 | */
84 |
85 | 'locale' => 'en',
86 |
87 | /*
88 | |--------------------------------------------------------------------------
89 | | Application Fallback Locale
90 | |--------------------------------------------------------------------------
91 | |
92 | | The fallback locale determines the locale to use when the current one
93 | | is not available. You may change the value to correspond to any of
94 | | the language folders that are provided through your application.
95 | |
96 | */
97 |
98 | 'fallback_locale' => 'en',
99 |
100 | /*
101 | |--------------------------------------------------------------------------
102 | | Faker Locale
103 | |--------------------------------------------------------------------------
104 | |
105 | | This locale will be used by the Faker PHP library when generating fake
106 | | data for your database seeds. For example, this will be used to get
107 | | localized telephone numbers, street address information and more.
108 | |
109 | */
110 |
111 | 'faker_locale' => 'en_US',
112 |
113 | /*
114 | |--------------------------------------------------------------------------
115 | | Encryption Key
116 | |--------------------------------------------------------------------------
117 | |
118 | | This key is used by the Illuminate encrypter service and should be set
119 | | to a random, 32 character string, otherwise these encrypted strings
120 | | will not be safe. Please do this before deploying an application!
121 | |
122 | */
123 |
124 | 'key' => env('APP_KEY'),
125 |
126 | 'cipher' => 'AES-256-CBC',
127 |
128 | /*
129 | |--------------------------------------------------------------------------
130 | | Autoloaded Service Providers
131 | |--------------------------------------------------------------------------
132 | |
133 | | The service providers listed here will be automatically loaded on the
134 | | request to your application. Feel free to add your own services to
135 | | this array to grant expanded functionality to your applications.
136 | |
137 | */
138 | ];
139 |
--------------------------------------------------------------------------------
/config/auth.php:
--------------------------------------------------------------------------------
1 | [
17 | 'guard' => 'web',
18 | 'passwords' => 'users',
19 | ],
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Authentication Guards
24 | |--------------------------------------------------------------------------
25 | |
26 | | Next, you may define every authentication guard for your application.
27 | | Of course, a great default configuration has been defined for you
28 | | here which uses session storage and the Eloquent user provider.
29 | |
30 | | All authentication drivers have a user provider. This defines how the
31 | | users are actually retrieved out of your database or other storage
32 | | mechanisms used by this application to persist your user's data.
33 | |
34 | | Supported: "session", "token"
35 | |
36 | */
37 |
38 | 'guards' => [
39 | 'web' => [
40 | 'driver' => 'session',
41 | 'provider' => 'users',
42 | ],
43 |
44 | 'api' => [
45 | 'driver' => 'sanctum',
46 | 'provider' => 'users',
47 | 'hash' => false,
48 | ],
49 | ],
50 |
51 | /*
52 | |--------------------------------------------------------------------------
53 | | User Providers
54 | |--------------------------------------------------------------------------
55 | |
56 | | All authentication drivers have a user provider. This defines how the
57 | | users are actually retrieved out of your database or other storage
58 | | mechanisms used by this application to persist your user's data.
59 | |
60 | | If you have multiple user tables or models you may configure multiple
61 | | sources which represent each model / table. These sources may then
62 | | be assigned to any extra authentication guards you have defined.
63 | |
64 | | Supported: "database", "eloquent"
65 | |
66 | */
67 |
68 | 'providers' => [
69 | 'users' => [
70 | 'driver' => 'eloquent',
71 | 'model' => App\Models\User::class,
72 | ],
73 |
74 | // 'users' => [
75 | // 'driver' => 'database',
76 | // 'table' => 'users',
77 | // ],
78 | ],
79 |
80 | /*
81 | |--------------------------------------------------------------------------
82 | | Resetting Passwords
83 | |--------------------------------------------------------------------------
84 | |
85 | | You may specify multiple password reset configurations if you have more
86 | | than one user table or model in the application and you want to have
87 | | separate password reset settings based on the specific user types.
88 | |
89 | | The expire time is the number of minutes that the reset token should be
90 | | considered valid. This security feature keeps tokens short-lived so
91 | | they have less time to be guessed. You may change this as needed.
92 | |
93 | */
94 |
95 | 'passwords' => [
96 | 'users' => [
97 | 'provider' => 'users',
98 | 'table' => 'password_resets',
99 | 'expire' => 60,
100 | 'throttle' => 60,
101 | ],
102 | ],
103 |
104 | /*
105 | |--------------------------------------------------------------------------
106 | | Password Confirmation Timeout
107 | |--------------------------------------------------------------------------
108 | |
109 | | Here you may define the amount of seconds before a password confirmation
110 | | times out and the user is prompted to re-enter their password via the
111 | | confirmation screen. By default, the timeout lasts for three hours.
112 | |
113 | */
114 |
115 | 'password_timeout' => 10800,
116 |
117 | ];
118 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_DRIVER', 'null'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Broadcast Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the broadcast connections that will be used
26 | | to broadcast events to other systems or over websockets. Samples of
27 | | each available type of connection are provided inside this array.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'pusher' => [
34 | 'driver' => 'pusher',
35 | 'key' => env('PUSHER_APP_KEY'),
36 | 'secret' => env('PUSHER_APP_SECRET'),
37 | 'app_id' => env('PUSHER_APP_ID'),
38 | 'options' => [
39 | 'cluster' => env('PUSHER_APP_CLUSTER'),
40 | 'useTLS' => true,
41 | ],
42 | ],
43 |
44 | 'redis' => [
45 | 'driver' => 'redis',
46 | 'connection' => 'default',
47 | ],
48 |
49 | 'log' => [
50 | 'driver' => 'log',
51 | ],
52 |
53 | 'null' => [
54 | 'driver' => 'null',
55 | ],
56 |
57 | ],
58 |
59 | ];
60 |
--------------------------------------------------------------------------------
/config/cache.php:
--------------------------------------------------------------------------------
1 | env('CACHE_DRIVER', 'file'),
22 |
23 | /*
24 | |--------------------------------------------------------------------------
25 | | Cache Stores
26 | |--------------------------------------------------------------------------
27 | |
28 | | Here you may define all of the cache "stores" for your application as
29 | | well as their drivers. You may even define multiple stores for the
30 | | same cache driver to group types of items stored in your caches.
31 | |
32 | */
33 |
34 | 'stores' => [
35 |
36 | 'apc' => [
37 | 'driver' => 'apc',
38 | ],
39 |
40 | 'array' => [
41 | 'driver' => 'array',
42 | ],
43 |
44 | 'database' => [
45 | 'driver' => 'database',
46 | 'table' => 'cache',
47 | 'connection' => null,
48 | ],
49 |
50 | 'file' => [
51 | 'driver' => 'file',
52 | 'path' => storage_path('framework/cache/data'),
53 | ],
54 |
55 | 'memcached' => [
56 | 'driver' => 'memcached',
57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
58 | 'sasl' => [
59 | env('MEMCACHED_USERNAME'),
60 | env('MEMCACHED_PASSWORD'),
61 | ],
62 | 'options' => [
63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000,
64 | ],
65 | 'servers' => [
66 | [
67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
68 | 'port' => env('MEMCACHED_PORT', 11211),
69 | 'weight' => 100,
70 | ],
71 | ],
72 | ],
73 |
74 | 'redis' => [
75 | 'driver' => 'redis',
76 | 'connection' => 'cache',
77 | ],
78 |
79 | 'dynamodb' => [
80 | 'driver' => 'dynamodb',
81 | 'key' => env('AWS_ACCESS_KEY_ID'),
82 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
83 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
84 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
85 | 'endpoint' => env('DYNAMODB_ENDPOINT'),
86 | ],
87 |
88 | ],
89 |
90 | /*
91 | |--------------------------------------------------------------------------
92 | | Cache Key Prefix
93 | |--------------------------------------------------------------------------
94 | |
95 | | When utilizing a RAM based store such as APC or Memcached, there might
96 | | be other applications utilizing the same cache. So, we'll specify a
97 | | value to get prefixed to all our keys so we can avoid collisions.
98 | |
99 | */
100 |
101 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),
102 |
103 | ];
104 |
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['*'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => true,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/config/filesystems.php:
--------------------------------------------------------------------------------
1 | env('FILESYSTEM_DRIVER', 'local'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Default Cloud Filesystem Disk
21 | |--------------------------------------------------------------------------
22 | |
23 | | Many applications store files both locally and in the cloud. For this
24 | | reason, you may specify a default "cloud" driver here. This driver
25 | | will be bound as the Cloud disk implementation in the container.
26 | |
27 | */
28 |
29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'),
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Filesystem Disks
34 | |--------------------------------------------------------------------------
35 | |
36 | | Here you may configure as many filesystem "disks" as you wish, and you
37 | | may even configure multiple disks of the same driver. Defaults have
38 | | been setup for each driver as an example of the required options.
39 | |
40 | | Supported Drivers: "local", "ftp", "sftp", "s3"
41 | |
42 | */
43 |
44 | 'disks' => [
45 |
46 | 'local' => [
47 | 'driver' => 'local',
48 | 'root' => storage_path('app'),
49 | ],
50 |
51 | 'public' => [
52 | 'driver' => 'local',
53 | 'root' => storage_path('app/public'),
54 | 'url' => env('APP_URL').'/storage',
55 | 'visibility' => 'public',
56 | ],
57 |
58 | 's3' => [
59 | 'driver' => 's3',
60 | 'key' => env('AWS_ACCESS_KEY_ID'),
61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
62 | 'region' => env('AWS_DEFAULT_REGION'),
63 | 'bucket' => env('AWS_BUCKET'),
64 | 'url' => env('AWS_URL'),
65 | ],
66 |
67 | ],
68 |
69 | ];
70 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Bcrypt Options
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may specify the configuration options that should be used when
26 | | passwords are hashed using the Bcrypt algorithm. This will allow you
27 | | to control the amount of time it takes to hash the given password.
28 | |
29 | */
30 |
31 | 'bcrypt' => [
32 | 'rounds' => env('BCRYPT_ROUNDS', 10),
33 | ],
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Argon Options
38 | |--------------------------------------------------------------------------
39 | |
40 | | Here you may specify the configuration options that should be used when
41 | | passwords are hashed using the Argon algorithm. These will allow you
42 | | to control the amount of time it takes to hash the given password.
43 | |
44 | */
45 |
46 | 'argon' => [
47 | 'memory' => 1024,
48 | 'threads' => 2,
49 | 'time' => 2,
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/config/logging.php:
--------------------------------------------------------------------------------
1 | env('LOG_CHANNEL', 'stack'),
21 |
22 | /*
23 | |--------------------------------------------------------------------------
24 | | Log Channels
25 | |--------------------------------------------------------------------------
26 | |
27 | | Here you may configure the log channels for your application. Out of
28 | | the box, Laravel uses the Monolog PHP logging library. This gives
29 | | you a variety of powerful log handlers / formatters to utilize.
30 | |
31 | | Available Drivers: "single", "daily", "slack", "syslog",
32 | | "errorlog", "monolog",
33 | | "custom", "stack"
34 | |
35 | */
36 |
37 | 'channels' => [
38 | 'stack' => [
39 | 'driver' => 'stack',
40 | 'channels' => ['single'],
41 | 'ignore_exceptions' => false,
42 | ],
43 |
44 | 'single' => [
45 | 'driver' => 'single',
46 | 'path' => storage_path('logs/laravel.log'),
47 | 'level' => 'debug',
48 | ],
49 |
50 | 'daily' => [
51 | 'driver' => 'daily',
52 | 'path' => storage_path('logs/laravel.log'),
53 | 'level' => 'debug',
54 | 'days' => 14,
55 | ],
56 |
57 | 'slack' => [
58 | 'driver' => 'slack',
59 | 'url' => env('LOG_SLACK_WEBHOOK_URL'),
60 | 'username' => 'Laravel Log',
61 | 'emoji' => ':boom:',
62 | 'level' => 'critical',
63 | ],
64 |
65 | 'papertrail' => [
66 | 'driver' => 'monolog',
67 | 'level' => 'debug',
68 | 'handler' => SyslogUdpHandler::class,
69 | 'handler_with' => [
70 | 'host' => env('PAPERTRAIL_URL'),
71 | 'port' => env('PAPERTRAIL_PORT'),
72 | ],
73 | ],
74 |
75 | 'stderr' => [
76 | 'driver' => 'monolog',
77 | 'handler' => StreamHandler::class,
78 | 'formatter' => env('LOG_STDERR_FORMATTER'),
79 | 'with' => [
80 | 'stream' => 'php://stderr',
81 | ],
82 | ],
83 |
84 | 'syslog' => [
85 | 'driver' => 'syslog',
86 | 'level' => 'debug',
87 | ],
88 |
89 | 'errorlog' => [
90 | 'driver' => 'errorlog',
91 | 'level' => 'debug',
92 | ],
93 |
94 | 'null' => [
95 | 'driver' => 'monolog',
96 | 'handler' => NullHandler::class,
97 | ],
98 |
99 | 'emergency' => [
100 | 'path' => storage_path('logs/laravel.log'),
101 | ],
102 | ],
103 |
104 | ];
105 |
--------------------------------------------------------------------------------
/config/mail.php:
--------------------------------------------------------------------------------
1 | env('MAIL_DRIVER', 'smtp'),
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | SMTP Host Address
24 | |--------------------------------------------------------------------------
25 | |
26 | | Here you may provide the host address of the SMTP server used by your
27 | | applications. A default option is provided that is compatible with
28 | | the Mailgun mail service which will provide reliable deliveries.
29 | |
30 | */
31 |
32 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | SMTP Host Port
37 | |--------------------------------------------------------------------------
38 | |
39 | | This is the SMTP port used by your application to deliver e-mails to
40 | | users of the application. Like the host we have set this value to
41 | | stay compatible with the Mailgun e-mail application by default.
42 | |
43 | */
44 |
45 | 'port' => env('MAIL_PORT', 587),
46 |
47 | /*
48 | |--------------------------------------------------------------------------
49 | | Global "From" Address
50 | |--------------------------------------------------------------------------
51 | |
52 | | You may wish for all e-mails sent by your application to be sent from
53 | | the same address. Here, you may specify a name and address that is
54 | | used globally for all e-mails that are sent by your application.
55 | |
56 | */
57 |
58 | 'from' => [
59 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
60 | 'name' => env('MAIL_FROM_NAME', 'Example'),
61 | ],
62 |
63 | /*
64 | |--------------------------------------------------------------------------
65 | | E-Mail Encryption Protocol
66 | |--------------------------------------------------------------------------
67 | |
68 | | Here you may specify the encryption protocol that should be used when
69 | | the application send e-mail messages. A sensible default using the
70 | | transport layer security protocol should provide great security.
71 | |
72 | */
73 |
74 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'),
75 |
76 | /*
77 | |--------------------------------------------------------------------------
78 | | SMTP Server Username
79 | |--------------------------------------------------------------------------
80 | |
81 | | If your SMTP server requires a username for authentication, you should
82 | | set it here. This will get used to authenticate with your server on
83 | | connection. You may also set the "password" value below this one.
84 | |
85 | */
86 |
87 | 'username' => env('MAIL_USERNAME'),
88 |
89 | 'password' => env('MAIL_PASSWORD'),
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Sendmail System Path
94 | |--------------------------------------------------------------------------
95 | |
96 | | When using the "sendmail" driver to send e-mails, we will need to know
97 | | the path to where Sendmail lives on this server. A default path has
98 | | been provided here, which will work well on most of your systems.
99 | |
100 | */
101 |
102 | 'sendmail' => '/usr/sbin/sendmail -bs',
103 |
104 | /*
105 | |--------------------------------------------------------------------------
106 | | Markdown Mail Settings
107 | |--------------------------------------------------------------------------
108 | |
109 | | If you are using Markdown based email rendering, you may configure your
110 | | theme and component paths here, allowing you to customize the design
111 | | of the emails. Or, you may simply stick with the Laravel defaults!
112 | |
113 | */
114 |
115 | 'markdown' => [
116 | 'theme' => 'default',
117 |
118 | 'paths' => [
119 | resource_path('views/vendor/mail'),
120 | ],
121 | ],
122 |
123 | /*
124 | |--------------------------------------------------------------------------
125 | | Log Channel
126 | |--------------------------------------------------------------------------
127 | |
128 | | If you are using the "log" driver, you may specify the logging channel
129 | | if you prefer to keep mail messages separate from other log entries
130 | | for simpler reading. Otherwise, the default channel will be used.
131 | |
132 | */
133 |
134 | 'log_channel' => env('MAIL_LOG_CHANNEL'),
135 |
136 | ];
137 |
--------------------------------------------------------------------------------
/config/queue.php:
--------------------------------------------------------------------------------
1 | env('QUEUE_CONNECTION', 'sync'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Queue Connections
21 | |--------------------------------------------------------------------------
22 | |
23 | | Here you may configure the connection information for each server that
24 | | is used by your application. A default configuration has been added
25 | | for each back-end shipped with Laravel. You are free to add more.
26 | |
27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'sync' => [
34 | 'driver' => 'sync',
35 | ],
36 |
37 | 'database' => [
38 | 'driver' => 'database',
39 | 'table' => 'jobs',
40 | 'queue' => 'default',
41 | 'retry_after' => 90,
42 | ],
43 |
44 | 'beanstalkd' => [
45 | 'driver' => 'beanstalkd',
46 | 'host' => 'localhost',
47 | 'queue' => 'default',
48 | 'retry_after' => 90,
49 | 'block_for' => 0,
50 | ],
51 |
52 | 'sqs' => [
53 | 'driver' => 'sqs',
54 | 'key' => env('AWS_ACCESS_KEY_ID'),
55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'),
58 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
59 | ],
60 |
61 | 'redis' => [
62 | 'driver' => 'redis',
63 | 'connection' => 'default',
64 | 'queue' => env('REDIS_QUEUE', 'default'),
65 | 'retry_after' => 90,
66 | 'block_for' => null,
67 | ],
68 |
69 | ],
70 |
71 | /*
72 | |--------------------------------------------------------------------------
73 | | Failed Queue Jobs
74 | |--------------------------------------------------------------------------
75 | |
76 | | These options configure the behavior of failed queue job logging so you
77 | | can control which database and table are used to store the jobs that
78 | | have failed. You may change them to any database / table you wish.
79 | |
80 | */
81 |
82 | 'failed' => [
83 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
84 | 'database' => env('DB_CONNECTION', 'mysql'),
85 | 'table' => 'failed_jobs',
86 | ],
87 |
88 | ];
89 |
--------------------------------------------------------------------------------
/config/sanctum.php:
--------------------------------------------------------------------------------
1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
19 | '%s%s%s',
20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
21 | Sanctum::currentApplicationUrlWithPort(),
22 | env('FRONTEND_URL') ? ','.parse_url(env('FRONTEND_URL'), PHP_URL_HOST) : ''
23 | ))),
24 |
25 | /*
26 | |--------------------------------------------------------------------------
27 | | Sanctum Guards
28 | |--------------------------------------------------------------------------
29 | |
30 | | This array contains the authentication guards that will be checked when
31 | | Sanctum is trying to authenticate a request. If none of these guards
32 | | are able to authenticate the request, Sanctum will use the bearer
33 | | token that's present on an incoming request for authentication.
34 | |
35 | */
36 |
37 | 'guard' => ['web'],
38 |
39 | /*
40 | |--------------------------------------------------------------------------
41 | | Expiration Minutes
42 | |--------------------------------------------------------------------------
43 | |
44 | | This value controls the number of minutes until an issued token will be
45 | | considered expired. This will override any values set in the token's
46 | | "expires_at" attribute, but first-party sessions are not affected.
47 | |
48 | */
49 |
50 | 'expiration' => null,
51 |
52 | /*
53 | |--------------------------------------------------------------------------
54 | | Token Prefix
55 | |--------------------------------------------------------------------------
56 | |
57 | | Sanctum can prefix new tokens in order to take advantage of numerous
58 | | security scanning initiatives maintained by open source platforms
59 | | that notify developers if they commit tokens into repositories.
60 | |
61 | | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
62 | |
63 | */
64 |
65 | 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
66 |
67 | /*
68 | |--------------------------------------------------------------------------
69 | | Sanctum Middleware
70 | |--------------------------------------------------------------------------
71 | |
72 | | When authenticating your first-party SPA with Sanctum you may need to
73 | | customize some of the middleware Sanctum uses while processing the
74 | | request. You may change the middleware listed below as required.
75 | |
76 | */
77 |
78 | 'middleware' => [
79 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
80 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
81 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
82 | ],
83 |
84 | ];
85 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'domain' => env('MAILGUN_DOMAIN'),
19 | 'secret' => env('MAILGUN_SECRET'),
20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
21 | ],
22 |
23 | 'postmark' => [
24 | 'token' => env('POSTMARK_TOKEN'),
25 | ],
26 |
27 | 'ses' => [
28 | 'key' => env('AWS_ACCESS_KEY_ID'),
29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
31 | ],
32 |
33 | ];
34 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | [
17 | resource_path('views'),
18 | ],
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Compiled View Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option determines where all the compiled Blade templates will be
26 | | stored for your application. Typically, this is within the storage
27 | | directory. However, as usual, you are free to change this value.
28 | |
29 | */
30 |
31 | 'compiled' => env(
32 | 'VIEW_COMPILED_PATH',
33 | realpath(storage_path('framework/views'))
34 | ),
35 |
36 | ];
37 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 | *.sqlite-journal
3 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->firstName.' '.$this->faker->lastName,
28 | User::COLUMN_EMAIL => $this->faker->unique()->safeEmail,
29 | User::COLUMN_EMAIL_VERIFIED_AT => now(),
30 | User::COLUMN_PASSWORD => Hash::make('password'),
31 | User::COLUMN_REMEMBER_TOKEN => Str::random(10),
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
16 | $table->string('name');
17 | $table->string('email')->unique();
18 | $table->timestamp('email_verified_at')->nullable();
19 | $table->string('password');
20 | $table->rememberToken();
21 | $table->timestamps();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('users');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_100000_create_password_resets_table.php:
--------------------------------------------------------------------------------
1 | string('email')->index();
16 | $table->string('token');
17 | $table->timestamp('created_at')->nullable();
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | */
24 | public function down()
25 | {
26 | Schema::dropIfExists('password_resets');
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/2019_08_19_000000_create_failed_jobs_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
16 | $table->text('connection');
17 | $table->text('queue');
18 | $table->longText('payload');
19 | $table->longText('exception');
20 | $table->timestamp('failed_at')->useCurrent();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down()
28 | {
29 | Schema::dropIfExists('failed_jobs');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->morphs('tokenable');
17 | $table->string('name');
18 | $table->string('token', 64)->unique();
19 | $table->text('abilities')->nullable();
20 | $table->timestamp('last_used_at')->nullable();
21 | $table->timestamp('expires_at')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('personal_access_tokens');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call(UsersTableSeeder::class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/database/seeders/UsersTableSeeder.php:
--------------------------------------------------------------------------------
1 | state([User::COLUMN_EMAIL => 'user@app.com'])->create();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "larave-vue3-spa-skeleton",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "lint": "./node_modules/.bin/eslint -c .eslintrc.js --ignore-path .gitignore resources/js/** --ext .js,.vue --max-warnings=0",
8 | "lint-fix": "./node_modules/.bin/eslint -c .eslintrc.js --ignore-path .gitignore resources/js/** --ext .js,.vue --fix"
9 | },
10 | "lint-staged": {
11 | "*.{js,vue}": [
12 | "./node_modules/.bin/eslint -c .eslintrc.js --fix --max-warnings=0"
13 | ],
14 | "*.php": [
15 | "./vendor/bin/pint --dirty"
16 | ]
17 | },
18 | "husky": {
19 | "hooks": {
20 | "pre-commit": "lint-staged"
21 | }
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.20.12",
25 | "@babel/eslint-parser": "^7.19.1",
26 | "eslint": "^7.20.0",
27 | "eslint-plugin-vue": "^7.6.0",
28 | "husky": "4",
29 | "lint-staged": "11.1.2",
30 | "sass": "^1.57.1"
31 | },
32 | "dependencies": {
33 | "@element-plus/icons-vue": "^2.0.10",
34 | "@fortawesome/fontawesome-free": "^6.2.1",
35 | "@vitejs/plugin-vue": "^4.0.0",
36 | "@vue/compiler-sfc": "^3.2.29",
37 | "@websanova/vue-auth": "^4.2.0",
38 | "axios": "^1.1.2",
39 | "dayjs": "^1.10.7",
40 | "element-plus": "^2.2.27",
41 | "laravel-vite-plugin": "^0.7.2",
42 | "lodash": "^4.17.19",
43 | "pinia": "^2.0.28",
44 | "postcss": "^8.1.14",
45 | "resolve-url-loader": "4.0.0",
46 | "vite": "^4.0.0",
47 | "vue": "3.2",
48 | "vue-axios": "^3.4.0",
49 | "vue-i18n": "^9.2.2",
50 | "vue-router": "^4.0.12"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/phpstorm.config.js:
--------------------------------------------------------------------------------
1 | System.config({
2 | "paths": {
3 | "@/*": "./resources/js/*",
4 | }
5 | });
6 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/Unit
6 |
7 |
8 | ./tests/Feature
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ./app
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel",
3 | "rules": {
4 | "class_attributes_separation": false,
5 | "no_superfluous_phpdoc_tags": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Redirect Trailing Slashes If Not A Folder...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_URI} (.+)/$
15 | RewriteRule ^ %1 [L,R=301]
16 |
17 | # Handle Front Controller...
18 | RewriteCond %{REQUEST_FILENAME} !-d
19 | RewriteCond %{REQUEST_FILENAME} !-f
20 | RewriteRule ^ index.php [L]
21 |
22 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yurich84/laravel-vue3-spa/53e1a7495f12ddc389fd2b9865da830faaa2997c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | handleRequest(Request::capture());
18 |
--------------------------------------------------------------------------------
/public/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yurich84/laravel-vue3-spa/53e1a7495f12ddc389fd2b9865da830faaa2997c/public/preview.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { createPinia } from 'pinia'
3 | import App from './base/App.vue'
4 | import ElementPlus from 'element-plus'
5 | import i18n from './plugins/i18n'
6 | import $dayjs from './plugins/day'
7 | import * as _ from 'lodash'
8 | import $filters from './includes/filters'
9 | import $bus from './includes/Event'
10 | import router from './plugins/router'
11 | import auth from './plugins/auth'
12 | import './plugins/day'
13 | import VueAxios from 'vue-axios'
14 | import axios from './plugins/axios-interceptor'
15 | import * as ElementPlusIconsVue from '@element-plus/icons-vue'
16 |
17 | const app = createApp(App)
18 |
19 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
20 | app.component(key, component)
21 | }
22 |
23 | window._ = _
24 |
25 | app.use(createPinia())
26 | app.use(router)
27 | app.use(VueAxios, axios)
28 | app.use(auth)
29 | app.use(i18n)
30 | app.use(ElementPlus, {i18n: (key, value) => i18n.t(key, value)})
31 |
32 | app.config.globalProperties.$config = window.config
33 | app.config.globalProperties.$filters = $filters
34 | app.config.globalProperties.$dayjs = $dayjs
35 | app.config.globalProperties.$bus = $bus
36 |
37 | app.mount('#app')
38 |
39 | export default app
40 |
--------------------------------------------------------------------------------
/resources/js/base/App.vue:
--------------------------------------------------------------------------------
1 |
2 |