├── .github
└── FUNDING.yml
├── .gitignore
├── README.md
├── content
├── en
│ ├── api-resources-overview.md
│ ├── authentication
│ │ ├── laravel-authentication.md
│ │ ├── update-user.md
│ │ └── vue-authentication.md
│ ├── authorization
│ │ └── laravel-basic-authorization.md
│ ├── examples
│ │ └── pagination.md
│ ├── file-uploads
│ │ ├── single-file-upload-laravel.md
│ │ └── single-file-upload-vue.md
│ ├── handling-errors.md
│ ├── hosting.md
│ ├── index.md
│ ├── middleware
│ │ ├── middleware-admin.md
│ │ ├── middleware-auth.md
│ │ ├── middleware-guest.md
│ │ └── middleware-overview.md
│ └── setup
│ │ ├── demo.md
│ │ ├── laravel-setup.md
│ │ ├── telescope.md
│ │ ├── tooling.md
│ │ └── vue-setup.md
└── settings.json
├── netlify.toml
├── nuxt.config.js
├── package-lock.json
├── package.json
├── static
├── icon.png
├── logo-dark.svg
├── logo-light.svg
├── preview-dark.png
└── preview.png
└── utils
└── getRoutes.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [garethredfern]
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.iml
3 | .idea
4 | *.log*
5 | .nuxt
6 | .vscode
7 | .DS_Store
8 | coverage
9 | dist
10 | sw.*
11 | .env
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Vue SPA
2 |
3 | ## Setup
4 |
5 | Install dependencies:
6 |
7 | ```bash
8 | npm run install
9 | ```
10 |
11 | ## Development
12 |
13 | ```bash
14 | npm run dev
15 | ```
16 |
--------------------------------------------------------------------------------
/content/en/api-resources-overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "API Resources - Overview"
3 | description: "Laravel API resources provide a way to shape the response you send back when a call is made to your API. Let's see how they can be used to serve data to a Vue SPA."
4 | position: 13
5 | category: "API"
6 | menuTitle: "API Resources"
7 | ---
8 |
9 | Laravel API resources provide a way to shape the response you send back when a call is made to your API. They give you fine grain control over which attributes are returned and even allow you to include relationship data. You can think of API resources as the data layer between your SPA and API. The response that is sent back follows the [JSON API spec](https://jsonapi.org/) which is a convention that is used throughout the industry. Let’s take a look at how we can use them.
10 |
11 | ### Creating a Resource
12 |
13 | Laravel provides the handy artisan command (don’t forget we are using Sail, if you are not, then swap `sail` for `php`).
14 |
15 | ```bash
16 | sail artisan make:resource UserResource
17 | ```
18 |
19 | The created resource will be placed in the app/Http/Resources directory of your application. Within the return statement you can add the model fields that you would like to be converted to JSON. Let’s take a look at the [UserResource](https://github.com/garethredfern/laravel-api/blob/v1.1/app/Http/Resources/UserResource.php) class.
20 |
21 | ```php
22 | class UserResource extends JsonResource
23 | {
24 | public function toArray($request)
25 | {
26 | return [
27 | 'id' => $this->id,
28 | 'name' => $this->name,
29 | 'email' => $this->email,
30 | 'avatar' => $this->avatar,
31 | 'emailVerified' => $this->email_verified_at,
32 | ];
33 | }
34 | }
35 | ```
36 |
37 | The above will send back the user data in the following JSON format:
38 |
39 | ```json
40 | {
41 | "data": {
42 | "id": 1,
43 | "name": "Luke Skywalker",
44 | "email": "luke@jedi.com",
45 | "avatar": "https://imageurls.com/image",
46 | "emailVerified": null
47 | }
48 | }
49 | ```
50 |
51 | The Laravel automatically wraps the user data in a data object, which is what the JSON spec suggests. To return the above JSON response you need to new up a UserResource and pass a user model into it.
52 |
53 | ```php
54 | use App\Http\Resources\UserResource;
55 | use App\Models\User;
56 |
57 | Route::get('/user/{id}', function ($id) {
58 | return new UserResource(User::findOrFail($id));
59 | });
60 | ```
61 |
62 | ### Resource Collections
63 |
64 | If you are returning a collection of resources or a paginated response, you should use the `collection` method provided by your resource class when creating the resource instance in your route or controller:
65 |
66 | ```php
67 | use App\Http\Resources\UserResource;
68 | use App\Models\User;
69 |
70 | Route::get('/users', function () {
71 | return UserResource::collection(User::all());
72 | });
73 | ```
74 |
75 | ### Pagination
76 |
77 | Laravel provides a handy paginate helper which you can use to paginate a resource collection. You can specify the number of records you would like to display (2 in this example) for each page. Alternatively leave the paginate method empty, and it will default to 15.
78 |
79 | ```php
80 | use App\Http\Resources\UserResource;
81 | use App\Models\User;
82 |
83 | Route::get('/users', function () {
84 | return new UserResource(User::paginate(2));
85 | });
86 | ```
87 |
88 | The JSON output from the paginate method will look like this:
89 |
90 | ```json
91 | {
92 | "data": [
93 | {
94 | "id": 1,
95 | "name": "Luke Skywalker",
96 | "email": "luke@jedi.com"
97 | },
98 | {
99 | "id": 2,
100 | "name": "Ben Kenobi",
101 | "email": "ben@jedi.com"
102 | }
103 | ],
104 | "links": {
105 | "first": "http://example.com/pagination?page=1",
106 | "last": "http://example.com/pagination?page=1",
107 | "prev": null,
108 | "next": null
109 | },
110 | "meta": {
111 | "current_page": 1,
112 | "from": 1,
113 | "last_page": 1,
114 | "path": "http://example.com/pagination",
115 | "per_page": 15,
116 | "to": 10,
117 | "total": 10
118 | }
119 | }
120 | ```
121 |
122 | As you can see the JSON output has a `links` object for the first, last, previous and next pages so that you can fetch page data. The `meta` object can be used for displaying useful link information in your pagination navigation.
123 |
124 | The above examples have been taken from the Laravel documentation to get you started but there are a lot more features you can use. Take a look at the [full documentation](https://laravel.com/docs/8.x/eloquent-resources) for further reading.
125 |
--------------------------------------------------------------------------------
/content/en/authentication/laravel-authentication.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Authentication - Laravel API"
3 | description: "How to set up full authentication using Laravel Sanctum & Fortify in a Vue SPA. Laravel API documentation."
4 | category: Authentication
5 | position: 7
6 | menuTitle: "Laravel Authentication"
7 | ---
8 |
9 | ### Setting Up CORS
10 |
11 | If you don’t get CORS set up correctly, it can be the cause (pardon the pun) of great frustration. The first thing to remember is that your SPA and API need to be running on the same top-level domain. However, they may be placed on different subdomains. Running locally (using Sail) the API will run on `http://localhost` and the SPA using the Vue CLI will normally run on `http://localhost:8080` (the port may vary but that is OK).
12 |
13 | With this in place we just need to add the routes which will be allowed via CORS. Most of the API endpoints will be via `api/*` but Fortify has a number of endpoints you need to add along with the fetching of `'sanctum/csrf-cookie'` add the following in your config/cors.php file:
14 |
15 | ```php
16 | 'paths' => [
17 | 'api/*',
18 | 'login',
19 | 'logout',
20 | 'register',
21 | 'user/password',
22 | 'forgot-password',
23 | 'reset-password',
24 | 'sanctum/csrf-cookie',
25 | 'user/profile-information',
26 | 'email/verification-notification',
27 | ],
28 | ```
29 |
30 | While you are in the config/cors.php file set the following:
31 |
32 | ```php
33 | 'supports_credentials' => true,
34 | ```
35 |
36 | The above ensures you have the `Access-Control-Allow-Credentials` header with a value of `True` set. You can read more about this in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials). We will be passing this header via the SPA but [more on that when we move to set it up](/setup/vue-setup).
37 |
38 | ### Setting Up Fortify
39 |
40 | Fortify also has a config file (config/fortify.php) which will need some changes. First set the `home` variable to point at the SPA URL, this can be done via the .env variable. This is where the API redirects to during authentication or password reset when the operations are successful and the user is authenticated.
41 |
42 | ```php
43 | 'home' => env('SPA_URL') . '/dashboard',
44 | ```
45 |
46 | Next switch off using any Laravel views for the authentication features, the SPA is handling all of this.
47 |
48 | ```php
49 | 'views' => false,
50 | ```
51 |
52 | Finally, turn on the authentication features you would like to use:
53 |
54 | ```php
55 | 'features' => [
56 | Features::registration(),
57 | Features::resetPasswords(),
58 | Features::emailVerification(),
59 | Features::updateProfileInformation(),
60 | Features::updatePasswords(),
61 | ],
62 | ```
63 |
64 | ### Redirecting If Authenticated
65 |
66 | Laravel provides a `RedirectIfAuthenticated` middleware which out of the box will try and redirect you to the home view if you are already authenticated. For the SPA to work you can add the following which will simply send back a 200 success message in a JSON response. We will then handle redirecting to the home page of the SPA using VueJS routing.
67 |
68 | ```php
69 | foreach ($guards as $guard) {
70 | if (Auth::guard($guard)->check()) {
71 | if ($request->expectsJson()) {
72 | return response()->json(['error' => 'Already authenticated.'], 200);
73 | }
74 | return redirect(RouteServiceProvider::HOME);
75 | }
76 | }
77 | ```
78 |
79 | ### Email Verification
80 |
81 | Laravel can handle email verification as it normally would but with one small adjustment to the `Authenticate` middleware. First. Let’s make sure your `App\Models\User` implements the `MustVerifyEmail` contract:
82 |
83 | ```php
84 | class User extends Authenticatable implements MustVerifyEmail
85 | {
86 | use Notifiable;
87 |
88 | //...
89 | }
90 | ```
91 |
92 | In the `Authenticate` Middleware change the `redirectTo` method to redirect to the SPA URL rather than a Laravel view:
93 |
94 | ```php
95 | protected function redirectTo($request)
96 | {
97 | if (! $request->expectsJson()) {
98 | return url(env('SPA_URL') . '/login');
99 | }
100 | }
101 | ```
102 |
103 | With this is in place Laravel will now send out the verification email and when a user clicks on the verification link it will do the necessary security checks and redirect back to your SPA’s URL.
104 |
105 | ### Reset Password
106 |
107 | Setting up the reset password functionality in the API is as simple as following the [official docs](https://laravel.com/docs/8.x/passwords#reset-link-customization). For reference here is what you need to do.
108 |
109 | Add the following at the top of `App\Providers\AuthServiceProvider`
110 |
111 | ```php
112 | use Illuminate\Auth\Notifications\ResetPassword;
113 | ```
114 |
115 | Add the following in the `AuthServiceProvider` boot method, this will create the URL which is used in the SPA with a generated token:
116 |
117 | ```php
118 | ResetPassword::createUrlUsing(function ($user, string $token) {
119 | return env('SPA_URL') . '/reset-password?token=' . $token;
120 | });
121 | ```
122 |
123 | To make this all work we will need to have a reset-password view in the SPA which handles the token and passes back the users new password. This is explained, with a link to the component, on the [Vue authentication](/authentication/vue-authentication#page-templates-views-and-component-overview) page under the Reset Password View heading.
124 |
125 | ### API Routes
126 |
127 | Once you have all the authentication in place, any protected routes will need to use the `auth:sanctum` middleware guard. This will ensure that the user has been authenticated before they can view the requested data from the API. Here is a simple example of what those endpoints would look like.
128 |
129 | ```php
130 | use App\Models\User;
131 |
132 | Route::middleware(['auth:sanctum'])->group(function () {
133 | //...
134 | Route::get('/users/{id}', function ($id) {
135 | return User::findOrFail($id);
136 | });
137 | });
138 | ```
139 |
--------------------------------------------------------------------------------
/content/en/authentication/update-user.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Update the Authenticated User Details"
3 | description: "How to update a users details in a Vue Spa with a Laravel API using Fortify."
4 | category: Authentication
5 | position: 9
6 | menuTitle: "Update User"
7 | ---
8 |
9 | Using the Laravel Fortify action `UpdateUserProfileInformation` we can allow a user to update their details. This requires us to send a `PUT` request to the `/user/profile-information` endpoint.
10 |
11 | ### Setting Up Laravel
12 |
13 | There isn’t much set up required on the Laravel side, just make sure that the `'user/profile-information'` endpoint is added in the paths array of the config/cors.php file.
14 |
15 | ```js
16 | 'paths' => [
17 | 'user/profile-information',
18 | //...
19 | ],
20 | ```
21 |
22 | With the above in place we should be able to send a request with the updated user details and Laravel will validate and update them.
23 |
24 | ### Setting Up the Auth Service Endpoint
25 |
26 | In the services/AuthService.js file add the following endpoint:
27 |
28 | ```js
29 | async updateUser(payload) {
30 | await authClient.put("/user/profile-information", payload);
31 | },
32 | ```
33 |
34 | The above will be used to send the user details to the Laravel API.
35 |
36 | ### The AuthUserForm Component
37 |
38 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/v1.1.2/src/components/AuthUserForm.vue)
39 |
40 | Now let’s focus on the form for updating a users details, starting with the template. We have two input fields, one for the name and one for the email. The input field is actually made from a `BaseInput` component. This component doesn’t do anything different to a normal HTML input field, but it provides a way to keep all the styling consistent and in one place.
41 |
42 | ```js
43 |
44 |
64 |
65 | ```
66 |
67 | When the form is submitted, it fires the `updateUser` method. Let’s take a look at the code required to send the user details through to the Laravel API.
68 |
69 | ```js
70 | export default {
71 | //...
72 | data() {
73 | return {
74 | name: null,
75 | email: null,
76 | error: null,
77 | message: null,
78 | };
79 | },
80 | computed: {
81 | ...mapGetters("auth", ["authUser"]),
82 | },
83 | methods: {
84 | updateUser() {
85 | this.error = null;
86 | this.message = null;
87 | const payload = {
88 | name: this.name,
89 | email: this.email,
90 | };
91 | AuthService.updateUser(payload)
92 | .then(() => this.$store.dispatch("auth/getAuthUser"))
93 | .then(() => (this.message = "User updated."))
94 | .catch((error) => (this.error = getError(error)));
95 | },
96 | },
97 | mounted() {
98 | this.name = this.authUser.name;
99 | this.email = this.authUser.email;
100 | },
101 | };
102 | ```
103 |
104 | The `AuthUserForm` component fetches the authenticated user’s details when it is mounted to the DOM. These details come from the Vuex auth store using a getter `authUser` and populate the `name` and `email` data object. With these data properties populated the user can change their details locally in the form and submit them using the `updateUser` method.
105 |
106 | The updateUser method will send the details as a payload through the `AuthService.updateUser` method to the Laravel API. If there are any errors they are sent back and the error data property is populated. Errors and message are displayed via the `FlashMessage` component. If the changes were successful then the user is fetched using an action `auth/getAuthUser`. We do this to enable the new user details to be updated in the Vuex store, across the application these changes will be visible. Finally, a success message is shown to the user.
107 |
--------------------------------------------------------------------------------
/content/en/authentication/vue-authentication.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Authentication - Vue SPA"
3 | description: "How to set up full authentication using Laravel Sanctum and Fortify in a Vue SPA. Vue SPA documentation."
4 | category: Authentication
5 | position: 8
6 | menuTitle: "Vue Authentication"
7 | ---
8 |
9 | ### Auth Endpoints and CORS
10 |
11 | First set up the [Auth Services File](https://github.com/garethredfern/laravel-vue/blob/main/src/services/AuthService.js) to keep all the API Auth endpoints in one place. The methods in this file interact with the Fortify endpoints we have [previously set up](/authentication/laravel-authentication#setting-up-fortify). At the top of the file Axios is imported to handle the data fetching from our API.
12 |
13 | An important note is that you must set the following in the axios create method:
14 |
15 | ```js
16 | withCredentials: true;
17 | ```
18 |
19 | A XMLHttpRequest from a different domain cannot set cookie values for its domain unless `withCredentials` is set to `true` before making the request.
20 |
21 |
22 | It’s important to highlight that this requires the SPA and API to share the same top-level domain. However, they may be placed on different subdomains.
23 |
24 |
25 | ### Sessions, Cookies and CSRF
26 |
27 | To authenticate your SPA, the login page should first make a request to the `/sanctum/csrf-cookie` endpoint to initialise CSRF protection for the application:
28 |
29 | ```js
30 | await authClient.get("/sanctum/csrf-cookie");
31 | ```
32 |
33 | This also applies to any other Fortify actions which require CSRF protection. Note the other routes in the services/AuthService.js file that also include a get request for the CSRF cookie; forgotPassword, resetPassword etc.
34 |
35 | If a login request is successful, the user is authenticated and subsequent requests to the SPA will automatically be authenticated via the session cookie that the Laravel application issues. In addition, since we already made a request to the /sanctum/csrf-cookie route, subsequent requests should automatically receive CSRF protection because Axios automatically sends the XSRF-TOKEN cookie in the X-XSRF-TOKEN header.
36 |
37 | ### Protecting Routes and Maintaining State
38 |
39 | The method for protecting your application routes is fairly simple. In the [router](https://github.com/garethredfern/laravel-vue/blob/v1.2.7/src/router/index.js) file there is a meta field `requiresAuth` it's a boolean held against every route you want to protect. Using the Vue router `beforeEach` method check if a route has a `requiresAuth` boolean of `true` and there is an authenticated user held in [Auth Vuex Store](https://github.com/garethredfern/laravel-vue/blob/v1.2.7/src/store/modules/Auth.js):
40 |
41 | ```js
42 | router.beforeEach((to, from, next) => {
43 | const authUser = store.getters["auth/authUser"];
44 | const reqAuth = to.matched.some((record) => record.meta.requiresAuth);
45 | const loginQuery = { path: "/login", query: { redirect: to.fullPath } };
46 |
47 | if (reqAuth && !authUser) {
48 | store.dispatch("auth/getAuthUser").then(() => {
49 | if (!store.getters["auth/authUser"]) next(loginQuery);
50 | else next();
51 | });
52 | } else {
53 | next(); // make sure to always call next()!
54 | }
55 | });
56 | ```
57 |
58 | A few scenarios need to be handled here:
59 |
60 | 1. If there is an authenticated user in the Vuex state, the route allows the page to load.
61 | 2. If there is no authenticated user in state then make a call to the Laravel API to check if there is an authenticated user which ties in with the session. Assuming there is, the Vuex store will be populated with the user details. The router allows the page to load.
62 | 3. Finally, if there is no valid session then redirect to the login page.
63 |
64 | Refreshing the browser will send a GET request to the API for the authenticated user, store the details in Vuex state. Navigating around the application will use the auth Vuex state to minimise API requests, keeping things snappy. This also helps with security. Any time data is fetched from the API, Laravel checks the session. If the session becomes invalid a 401 or 419 response is sent to the SPA. Handled via an Axios interceptor, logging the user out.
65 |
66 | ```js
67 | authClient.interceptors.response.use(
68 | (response) => {
69 | return response;
70 | },
71 | function(error) {
72 | if (error.response.status === 401 || error.response.status === 419) {
73 | store.dispatch("auth/logout");
74 | }
75 | return Promise.reject(error.response);
76 | }
77 | );
78 | ```
79 |
80 | ### Page Templates (Views) and Component Overview
81 |
82 | Here is a breakdown of each of the Vue components and views that are used for handling user authentication, password resets and email verification.
83 |
84 | #### Registration Component
85 |
86 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/RegisterForm.vue)
87 |
88 | The registration component allows users to sign up for an account if they don’t have one. It works with the Fortify /register endpoint. It only works when a user is not logged in, you can’t use it for adding users if you are logged in. To add users through an admin screen we would need to create another API endpoint and alter this component to post to that too. For now, it’s kept simply to register new users. Once a user is registered successfully they are automatically logged in and redirected to the dashboard.
89 |
90 | #### Login Component
91 |
92 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/LoginForm.vue)
93 |
94 | The login form works with the Fortify /login endpoint. Notice that all the endpoints are kept in the AuthService file which is imported into each view/component. Once a user logs in successfully, they are redirected to the dashboard.
95 |
96 | #### Logout Component
97 |
98 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/Logout.vue)
99 |
100 | A simple component which works with the Fortify /logout endpoint. When a user is logged out, the `auth/logout` action is dispatched clearing the user from the Vuex state and redirects to the login view.
101 |
102 | #### Dashboard View (Protected Route)
103 |
104 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/views/Dashboard.vue)
105 |
106 | This component requires authentication before it can be viewed, it displays the user messages component. A dashboard could display much more but the takeaway here is that it is protected. A user must be logged in to see it.
107 |
108 | #### Forgot Password View
109 |
110 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/views/ForgotPassword.vue)
111 |
112 | The forgot password view can be accessed if a user is not logged in and needs to reset their password. It works with the Fortify /forgot-password endpoint. Once the form is submitted Laravel will check the email is valid and send out a reset password email. The link in this email will have a token and the URL will point to the reset password view in the SPA.
113 |
114 | #### Reset Password View
115 |
116 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/views/ResetPassword.vue)
117 |
118 | The reset password view displays a form where a user can change their password. Importantly it will also have access to the token provided by Laravel. It works with the Fortify /reset-password endpoint. When the form is submitted the users email and token are checked by Laravel. If everything was successful, a message is displayed and the user can log in.
119 |
120 | #### Update Password Component
121 |
122 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/UpdatePassword.vue)
123 |
124 | This form allows a logged-in user to update their password. It works with the Fortify /user/password endpoint.
125 |
126 | #### Email Verification
127 |
128 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/VerifyEmail.vue)
129 |
130 | Laravel provides the ability for a user to verify their email as an added layer of security. This component works with the /email/verification-notification endpoint. To get the email notification working, there is some set up required within the Laravel API. More detail in these [instructions](/authentication/laravel-authentication#email-verification).
131 |
132 | With this in place, the SPA will check a user is verified using the details in the auth Vuex store. If they are not, a button is displayed, when clicked the verification email will be sent by Laravel. The email will have a link to verify and return the user back to the SPA dashboard.
133 |
134 | #### Flash Message Component
135 |
136 | [View file on GitHub](https://github.com/garethredfern/laravel-vue/blob/main/src/components/FlashMessage.vue)
137 |
138 | While the user is interacting with the API via the SPA we need to give them success and error messages. The Laravel API will be handling a lot of these messages, but we can also use catch try/catch blocks to display messages within the SPA. To keep things all in one place there is a `FlashMessage` component which takes a message and error prop.
139 |
--------------------------------------------------------------------------------
/content/en/authorization/laravel-basic-authorization.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Basic Authorization"
3 | description: "How to set up basic authorization in a Laravel API, adding an is_admin field to a user model."
4 | category: Authorization
5 | position: 10
6 | menuTitle: "Basic Authorization"
7 | ---
8 |
9 | ## Basic Authorization - Laravel API
10 |
11 | While authentication determines if a user has access to your application, authorization determines what they can see when they are logged in. Authorization can be as simple or as complex as you need it to be. Often you will have user roles with permissions assigned to them. A role or maybe multiple roles are then assigned to a user.
12 |
13 | ### Add a Column to the User Table
14 |
15 | To keep things nice and simple let’s start with assigning a user as an admin. Admins will be able to see extra content and perform additional tasks in the application. To set up this functionality we will add a boolean column to the users table of `is_admin`. Add the following to your user migration file in the Laravel API:
16 |
17 | ```php
18 | public function up()
19 | {
20 | Schema::create('users', function (Blueprint $table) {
21 | //...
22 | $table->boolean('is_admin')->default(false);
23 | });
24 | }
25 | ```
26 |
27 | Run the migrations (don’t forget we are using Sail, if you are not, then swap `sail` for `php`).
28 |
29 | ```bash
30 | sail artisan migrate:fresh --seed
31 | ```
32 |
33 | ### Create a Helper Method on the User Model
34 |
35 | With the new `is_admin` column in place on the users table it’s time to update the `User` model. First cast the `is_admin` column to a boolean:
36 |
37 | ```php
38 | protected $casts = [
39 | //...
40 | 'is_admin' => 'boolean',
41 | ];
42 | ```
43 |
44 | Now add the following helper method to the `User` model:
45 |
46 | ```php
47 | public function isAdmin(): bool
48 | {
49 | return $this->is_admin;
50 | }
51 | ```
52 |
53 | This method will allow you to write very readable code when checking if a user is an admin for example:
54 |
55 | ```php
56 | Auth::user()->isAdmin();
57 | ```
58 |
59 | ### Protecting a List of Users
60 |
61 | Let’s put our new `isAdmin` method to good use. In our demo application we may want only admins to be able to see other users in the application. In the `UserController` `index` method we paginate a list of all the users. We protect this by checking if the authenticated user is an admin.
62 |
63 | ```php
64 | public function index()
65 | {
66 | if (Auth::user()->isAdmin()) {
67 | return UserResource::collection(User::paginate());
68 | }
69 | return response()->json(["message" => "Forbidden"], 403);
70 | }
71 | ```
72 |
73 | With the above in place, when the API route GET method of /users is hit. Laravel will check a user is authenticated (logged in), then check if they are an admin. If the user is an admin, the paginated list of users is returned. If they are not an admin, a 403 HTTP status is returned (forbidden).
74 |
75 | ### Restricting Content and Functionality in Vue
76 |
77 | In the SPA we already fetch the authenticated user’s details when they log in. Now we can send back whether the user is an admin by adding the field to the UserResource.
78 |
79 | ```php
80 | public function toArray($request)
81 | {
82 | return [
83 | //...
84 | 'isAdmin' => $this->isAdmin(),
85 | ];
86 | }
87 | ```
88 |
89 | With the above code in place any time you want to check if a user is an admin in Vue you can access it from the `auth` Vuex store. Let’s add a getter to check for the isAdmin property. In your auth Vuex store add the following to your getters:
90 |
91 | ```js
92 | export const getters = {
93 | //...
94 | isAdmin: (state) => {
95 | return state.user ? state.user.isAdmin : false;
96 | },
97 | };
98 | ```
99 |
100 | Now you can check in any component or page view if a user is an admin by using the getter:
101 |
102 | ```js
103 | import { mapGetters } from "vuex";
104 |
105 | export default {
106 | //...
107 | computed: {
108 | ...mapGetters("auth", ["isAdmin"]),
109 | },
110 | };
111 | ```
112 |
113 | ### Protecting Routes based on isAdmin
114 |
115 | You can protect a route in Vue by adding a [navigation guard](https://router.vuejs.org/guide/advanced/navigation-guards.html). We have already seen this in action when we check for an authenticated user before entering the application. To protect a single route you can add the`beforeEnter` check to a route you want to protect in your src/router.index.js file.
116 |
117 | ```js
118 | const routes = [
119 | //...
120 | {
121 | path: "/users",
122 | name: "users",
123 | meta: { requiresAuth: true },
124 | component: () => import("../views/Users"),
125 | beforeEnter: (to, from, next) => {
126 | if (store.getters["auth/isAdmin"]) next();
127 | else next(false);
128 | }
129 | }
130 | });
131 | ```
132 |
133 | This will stop the route being accessed by anyone who doesn’t have the `isAuth` property set to `true` on their profile.
134 |
135 | ### Summary
136 |
137 | This page provides a basic example of setting up authorization. With this alone, you can get quite far in a simple application. Often you will only have users who log in and see their data, while an admin might need to log in and see multiple users. For more complex applications you will probably need to set up roles with permissions.
138 |
--------------------------------------------------------------------------------
/content/en/examples/pagination.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Pagination Example"
3 | description: "Here is an example on building a Vue pagination component with Vuex, that fetches data from a Laravel API."
4 | position: 20
5 | category: "Examples"
6 | menuTitle: "Pagination"
7 | ---
8 |
9 | In the [API Resources documentation](/api-resources-overview#pagination) we set up a `UserResource` using the paginate method to return a JSON structure which looks like this:
10 |
11 | ```js
12 | {
13 | "data": [
14 | {
15 | "id": 1,
16 | "name": "Luke Skywalker",
17 | "email": "luke@jedi.com"
18 | },
19 | {
20 | "id": 2,
21 | "name": "Ben Kenobi",
22 | "email": "ben@jedi.com"
23 | }
24 | ],
25 | "links": {
26 | "first": "http://example.com/pagination?page=1",
27 | "last": "http://example.com/pagination?page=1",
28 | "prev": null,
29 | "next": null
30 | },
31 | "meta": {
32 | "current_page": 1,
33 | "from": 1,
34 | "last_page": 1,
35 | "path": "http://example.com/pagination",
36 | "per_page": 15,
37 | "to": 10,
38 | "total": 10
39 | }
40 | }
41 | ```
42 |
43 | To make use of this data within Vue there are few stages we need to work through.
44 |
45 | ### Add a Users Route
46 |
47 | Create a `Users` component in /src/components/views, for now, you can leave it blank. In the /src/router/index.js file add the following route:
48 |
49 | ```js
50 | const routes = [
51 | //...
52 | {
53 | path: "/users",
54 | name: "users",
55 | meta: { requiresAuth: true },
56 | component: () => import("../views/Users"),
57 | beforeEnter: (to, from, next) => {
58 | if (store.getters["auth/isAdmin"]) next();
59 | else next(false);
60 | }
61 | }
62 | });
63 | ```
64 |
65 | For a full explanation on what this route is doing make sure to check out the [Protecting Routes](/authorization/laravel-basic-authorization#protecting-routes-based-on-isadmin) guide.
66 |
67 | ### User Service
68 |
69 | Set up a /src/services/UserService.js file that will manage the endpoints used to fetch user data. Add the following endpoints:
70 |
71 | ```js
72 | import * as API from "@/services/API";
73 |
74 | export default {
75 | getUsers(page) {
76 | return API.apiClient.get(`/users/?page=${page}`);
77 | },
78 | paginateUsers(link) {
79 | return API.apiClient.get(link);
80 | },
81 | };
82 | ```
83 |
84 | ### User Vuex Store
85 |
86 | Once the API has sent back the request, we will add it to a `User` Vuex store. Create the following file /src/store/User.js adding the following:
87 |
88 | ```js
89 | import { getError } from "@/utils/helpers";
90 | import UserService from "@/services/UserService";
91 |
92 | export const namespaced = true;
93 |
94 | export const state = {};
95 |
96 | export const mutations = {};
97 |
98 | export const actions = {};
99 |
100 | export const getters = {};
101 | ```
102 |
103 | In the state object add a users array property, along with the other state properties we will need:
104 |
105 | ```js
106 | export const state = {
107 | users: [],
108 | meta: null,
109 | links: null,
110 | loading: false,
111 | error: null,
112 | };
113 | ```
114 |
115 | Next lets set up the mutations which will update the state:
116 |
117 | ```js
118 | export const mutations = {
119 | SET_USERS(state, users) {
120 | state.users = users;
121 | },
122 | SET_META(state, meta) {
123 | state.meta = meta;
124 | },
125 | SET_LINKS(state, links) {
126 | state.links = links;
127 | },
128 | SET_LOADING(state, loading) {
129 | state.loading = loading;
130 | },
131 | SET_ERROR(state, error) {
132 | state.error = error;
133 | },
134 | };
135 | ```
136 |
137 | Set up the action that will be called to initially fetch the first page of users, and pass them to the mutation:
138 |
139 | ```js
140 | export const actions = {
141 | getUsers({ commit }, page) {
142 | commit("SET_LOADING", true);
143 | UserService.getUsers(page)
144 | .then((response) => {
145 | setPaginatedUsers(commit, response);
146 | })
147 | .catch((error) => {
148 | commit("SET_LOADING", false);
149 | commit("SET_ERROR", getError(error));
150 | });
151 | },
152 | };
153 | ```
154 |
155 | Because the `getUsers` action and proceeding `paginateUsers` action both have similar functionality we can pull the logic out into a single `setPaginatedUsers` method. Place this at the top of the /src/store/User.js file.
156 |
157 | ```js
158 | function setPaginatedUsers(commit, response) {
159 | commit("SET_USERS", response.data.data);
160 | commit("SET_META", response.data.meta);
161 | commit("SET_LINKS", response.data.links);
162 | commit("SET_LOADING", false);
163 | }
164 | ```
165 |
166 | Set up another action that will fetch the paginated users based on the link which is passed in:
167 |
168 | ```js
169 | export const actions = {
170 | //...
171 | paginateUsers({ commit }, link) {
172 | commit("SET_LOADING", true);
173 | UserService.paginateUsers(link)
174 | .then((response) => {
175 | setPaginatedUsers(commit, response);
176 | })
177 | .catch((error) => {
178 | commit("SET_LOADING", false);
179 | commit("SET_ERROR", getError(error));
180 | });
181 | },
182 | };
183 | ```
184 |
185 | Finally, let’s add the getters so that we can have access to the state:
186 |
187 | ```js
188 | export const getters = {
189 | users: (state) => {
190 | return state.users;
191 | },
192 | meta: (state) => {
193 | return state.meta;
194 | },
195 | links: (state) => {
196 | return state.links;
197 | },
198 | loading: (state) => {
199 | return state.loading;
200 | },
201 | error: (state) => {
202 | return state.error;
203 | },
204 | };
205 | ```
206 |
207 | You should now have a Vuex store for managing the user state. Take a look at the complete file in the demo application’s [GitHub repo](https://github.com/garethredfern/laravel-vue/blob/v1.3.4/src/store/modules/User.js). Don’t forget to include your new `User` store in the /src/store/index.js file:
208 |
209 | ```js
210 | import * as user from "@/store/modules/User";
211 |
212 | export default new Vuex.Store({
213 | modules: {
214 | //...
215 | user,
216 | },
217 | });
218 | ```
219 |
220 | ### Users View
221 |
222 | The `users` view will display a list of the first page of users when they load. To fetch the users before the page loads we will use the `beforeRouteEnter` Vue hook:
223 |
224 | ```js
225 | beforeRouteEnter(to, from, next) {
226 | const currentPage = parseInt(to.query.page) || 1;
227 | store.dispatch("user/getUsers", currentPage).then(() => {
228 | to.params.page = currentPage;
229 | next();
230 | });
231 | }
232 | ```
233 |
234 | Inside the `beforeRouteEnter` hook we get the current page number from the query string, or set it to 1 if it doesn’t exist. Then we dispatch the `getUsers` action we created before. The current page number is passed into the `getUsers` action.
235 |
236 | With this in place, when the view loads the first page of users will be added into the Vuex store, and they can then be looped though within the Users template.
237 |
238 | Take a look at the complete template code [on GitHub](https://github.com/garethredfern/laravel-vue/blob/v1.3.7/src/views/Users.vue).
239 |
240 | ### Paginate Component
241 |
242 | The final part of the paginated `Users` template is to create the navigation to paginate between pages of users. To achieve this, we will create a `BasePagination` component. This component will be re-usable and not specific to just paginating users. It can be used to paginate any paginated list we get back from the Laravel API.
243 |
244 | The complete `BasePagination` component can be seen [on GitHub](https://github.com/garethredfern/laravel-vue/blob/v1.3.7/src/components/BasePagination.vue).
245 | First, pass in the props:
246 |
247 | - `action`: is the Vuex action we want to call (`user/paginateUsers` in this example).
248 | - `path`: is the route path for the view (/users in this example).
249 | - `meta`: the API meta data passed from the `UserResource` paginate method.
250 | - `links`: the API links data passed from the `UserResource` paginate method.
251 |
252 | At the top of the template we display the page we are on and the number of pages:
253 |
254 | ```js
255 | Page {{ meta.current_page }} of {{ meta.last_page }}
256 | ```
257 |
258 | Each pagination button is then displayed:
259 |
260 | ```js
261 |
269 | ```
270 |
271 | There are four buttons in total (first, previous, next, last), they all follow the same format except for the method they call and the conditional for displaying the button. The method for the `firstPage` button looks like this:
272 |
273 | ```js
274 | methods: {
275 | firstPage() {
276 | this.$store.dispatch(this.action, this.links.first).then(() => {
277 | if (this.path) {
278 | this.$router.push({
279 | path: this.path,
280 | query: { page: 1 },
281 | });
282 | }
283 | });
284 | },
285 | //...
286 | }
287 | ```
288 |
289 | When the user clicks on the first page button the action prop is called (user/paginateUsers) the action is passed the link that the Laravel API provides for the first page using `this.links.first`. If a `path` prop has been passed in to the `BasePagination` component then the router updates the page with a query parameter `?page=1`. If no `path` prop is passed in then the next page is still fetched but the router does not update the URL. This maybe desired in some situations.
290 |
291 | A similar process is followed for the other pagination links. Take a look at the full component [on GitHub](https://github.com/garethredfern/laravel-vue/blob/v1.3.7/src/components/BasePagination.vue).
292 |
293 | ### Conclusion
294 |
295 | With the above examples complete you should have pagination set up for the users who are registered. Don’t forget the /users route is protected using the flag `isAdmin`. Have a look at the page on [Authorization](https://laravelvuespa.com/authorization/laravel-basic-authorization) for more details. To see the list of users, make sure you have set yourself to admin in the Laravel API database. You can reuse the pagination component anywhere you need to paginate lists of data from the API.
296 |
--------------------------------------------------------------------------------
/content/en/file-uploads/single-file-upload-laravel.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "File Uploads Laravel"
3 | description: "Set up the ability to upload a users avatar to Digital Ocean Spaces, using the Flysystem in Laravel."
4 | position: 11
5 | category: "File Uploads"
6 | menuTitle: "File Uploads Laravel"
7 | ---
8 |
9 | File uploads in Laravel are processed using the Flysystem package. To get started, add using composer, at **version 1 for Laravel 8.0** (version 2 will be available for Laravel 9).
10 |
11 | ```bash
12 | sail composer require league/flysystem-aws-s3-v3 ^1.0
13 | ```
14 |
15 | ### Digital Ocean Spaces
16 |
17 | The example project will use [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/) to upload the user’s avatar. You can set up an account for free to test this out. There are a few credentials required that you add in your .env file.
18 |
19 | 1. You will need your key & add to DO_SPACES_KEY
20 | 2. You will need your secret & add to DO_SPACES_SECRET
21 | 3. The endpoint will look similar to this [https://fra1.digitaloceanspaces.com](https://fra1.digitaloceanspaces.com) depending on the region you have chosen (DO_SPACES_ENDPOINT).
22 | 4. The region in this example is `fra1` (DO_SPACES_REGION).
23 | 5. Finally, the bucket is the unique name you create when you set up the space (DO_SPACES_BUCKET).
24 |
25 | Laravel doesn’t come with Digital Ocean configuration out of the box, you can add it into your config/filesystem.php file:
26 |
27 | ```php
28 | 'disks' => [
29 | //...
30 | 'spaces' => [
31 | 'driver' => 's3',
32 | 'key' => env('DO_SPACES_KEY'),
33 | 'secret' => env('DO_SPACES_SECRET'),
34 | 'endpoint' => env('DO_SPACES_ENDPOINT'),
35 | 'region' => env('DO_SPACES_REGION'),
36 | 'bucket' => env('DO_SPACES_BUCKET'),
37 | ]
38 | ]
39 | ```
40 |
41 | ### Database Set up
42 |
43 | We need to add a column to the users table for the avatar. In the users migration file, within the `up` method add the following:
44 |
45 | ```php
46 | Schema::create('users', function (Blueprint $table) {
47 | //...
48 | $table->string('avatar')->nullable();
49 | });
50 | ```
51 |
52 | Set the column to fillable in the User model:
53 |
54 | ```php
55 | protected $fillable = [
56 | 'name',
57 | 'email',
58 | 'avatar',
59 | 'password',
60 | ];
61 | ```
62 |
63 | Run the migrations (don’t forget we are using Sail, if you are not, then swap `sail` for `php`).
64 |
65 | ```php
66 | sail artisan migrate:fresh --seed
67 | ```
68 |
69 | ### AvatarController Controller
70 |
71 | Add an AvatarController controller using the Artisan command:
72 |
73 | ```php
74 | sail artisan make:controller AvatarController
75 | ```
76 |
77 | Add a `store` method to the AvatarController. This method first gets the authenticated user and creates a file path using the `Storage` helper.
78 |
79 | The first parameter to the `Storage` method creates a string path where you want to save the file. Digital Ocean will build a folder structure from this `avatars/user-1` as an example. Next the file is retrieved from the request, and we set the url to be `public` so that it can be viewed in our application. Finally, the full URL to the avatar is saved against the user in the avatar column. Note how we use the `DO_SPACES_PUBLIC` environment variable with the file path. Make sure to have a trailing slash at the end of your URL:
80 |
81 | ```bash
82 | DO_SPACES_PUBLIC=https://laravel-api.fra1.digitaloceanspaces.com/
83 | ```
84 |
85 | To return the user’s data back in a formatted json response we create a `UserResource`, you can review how resources work [here](/api-resources-overview).
86 |
87 | ```php
88 | namespace App\Http\Controllers;
89 |
90 | use Exception;
91 | use Illuminate\Http\Request;
92 | use App\Http\Resources\UserResource;
93 | use Illuminate\Support\Facades\Auth;
94 | use Illuminate\Support\Facades\Storage;
95 |
96 | class AvatarController extends Controller
97 | {
98 | public function store(Request $request)
99 | {
100 | try {
101 | $user = Auth::user();
102 | $filePath = Storage::disk('spaces')
103 | ->putFile('avatars/user-'.$user->id, $request->file, 'public');
104 | $user->avatar = env('DO_SPACES_PUBLIC').$filePath;
105 | $user->save();
106 | } catch (Exception $exception) {
107 | return response()->json(['message' => $exception->getMessage()], 409);
108 | }
109 | return new UserResource($user);
110 | }
111 | }
112 | ```
113 |
114 | ### UserResource
115 |
116 | Add an avatar field to the UserResource:
117 |
118 | ```php
119 | namespace App\Http\Resources;
120 |
121 | use Illuminate\Http\Resources\Json\JsonResource;
122 |
123 | class UserResource extends JsonResource
124 | {
125 | public function toArray($request)
126 | {
127 | return [
128 | //...
129 | 'avatar' => $this->avatar,
130 | ];
131 | }
132 | }
133 | ```
134 |
135 | ### API Endpoint
136 |
137 | Add the avatar upload endpoint within the sanctum middleware group of your routes/api.php file:
138 |
139 | ```php
140 | use App\Http\Controllers\AvatarController;
141 |
142 | Route::middleware(['auth:sanctum'])->group(function () {
143 | //...
144 | Route::post('/users/auth/avatar', [AvatarController::class, 'store']);
145 | });
146 | ```
147 |
148 | With all this set up complete you should be able to [upload an image from your Vue SPA](/file-uploads/single-file-upload-vue) and see it in your Digital Ocean admin area.
149 |
--------------------------------------------------------------------------------
/content/en/file-uploads/single-file-upload-vue.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "File Uploads Vue"
3 | description: "Using vue as an SPA. Set up the ability to upload a users avatar to Digital Ocean Spaces, using the Flysystem in Laravel."
4 | position: 12
5 | category: "File Uploads"
6 | menuTitle: "File Uploads Vue"
7 | ---
8 |
9 | To upload a single file in our application we will create a generic `FileUpload` component that can be reused. The component will accept props for the file types and API endpoint that the file will be uploaded to. In this example we will use the `FileUpload` component to upload a user's avatar to [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/) using the Laravel API.
10 |
11 | ### FileService
12 |
13 | Create a FileService.js file in the services folder and add the following uploadFile method:
14 |
15 | ```js
16 | import * as API from "@/services/API";
17 |
18 | export default {
19 | async uploadFile(payload) {
20 | await API.apiClient.post(payload.endpoint, payload.file);
21 | },
22 | };
23 | ```
24 |
25 | Create a FileUpload component that can be used to upload any single file. The full component can be found in the [GitHub repo](https://github.com/garethredfern/laravel-vue/blob/v1.2.1/src/components/FileUpload.vue), let’s break it down.
26 |
27 | ### File Input
28 |
29 | The template will have an input field which has a type of `file` and the `accept` attribute value is a string that defines the file types, see further details [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept). The `fileTypes` are passed into the component as a prop.
30 |
31 | ```js
32 |
33 | ```
34 |
35 | ### Data Object
36 |
37 | The data object just has three properties. The `file` property will hold the file object when it’s selected. The `message` and `error` properties hold the success and error messages that get displayed.
38 |
39 | ```js
40 | data() {
41 | return {
42 | file: null,
43 | message: null,
44 | error: null,
45 | };
46 | }
47 | ```
48 |
49 | ### Methods
50 |
51 | The clearMessage method is simply used to clear any messages or errors each time a new file is uploaded.
52 |
53 | ```js
54 | clearMessage() {
55 | this.error = null;
56 | this.message = null;
57 | }
58 | ```
59 |
60 | The fileChange method runs every time a file is selected, and it sets the `file` data property to the `event.target.files[0]` which will be a [File object](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#getting_information_on_selected_files).
61 |
62 | ```js
63 | fileChange(event) {
64 | this.clearMessage();
65 | this.file = event.target.files[0];
66 | }
67 | ```
68 |
69 | The `uploadFile` method will create a payload to pass through to the `FileService`. To send the file through in the correct format we need to new up an instance of the `FormData` object. The `FormData` object has an `append` method where the file is passed in. See more information how this works over on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData). The payload also has an `endpoint` property which is used to accept the API endpoint that the file will be sent to. We do this so that the `FileUpload` component can be reusable for uploading different files to different API endpoints.
70 |
71 | ```js
72 | uploadFile() {
73 | const payload = {};
74 | const formData = new FormData();
75 | formData.append("file", this.file);
76 | payload.file = formData;
77 | payload.endpoint = this.endpoint;
78 | this.clearMessage();
79 | FileService.uploadFile(payload)
80 | .then(() => {
81 | this.message = "File uploaded.";
82 | this.$emit("fileUploaded");
83 | })
84 | .catch((error) => (this.error = getError(error)));
85 | }
86 | ```
87 |
88 | Notice in the `then` method on the `FileService.uploadFile` we emit an event called `fileUploaded`, this enables us to listen for when the file has been uploaded in the parent component. The listener provides other functionality, we will use it to update the users profile page without them needing to refresh the page.
89 |
90 | With the `FileUpload` component built all that's left to do is add it into the [User view](https://github.com/garethredfern/laravel-vue/blob/v1.2.5/src/views/User.vue) we previously created.
91 |
92 | ```js
93 |
94 | //...
95 |
102 |
103 |
117 | ```
118 |
119 | With this in place and the [Laravel API functionality](/file-uploads/single-file-upload-laravel) built, you should be able to login to your application and upload a file to your Digital Ocean Spaces account.
120 |
--------------------------------------------------------------------------------
/content/en/handling-errors.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Handling Errors"
3 | description: "How to safely handle errors in a Vue SPA using Laravel as an API. Laravel API Errors and Exceptions: How to Return Responses."
4 | position: 14
5 | category: "Errors"
6 | menuTitle: "Handling Errors"
7 | ---
8 |
9 | ### Handling Errors
10 |
11 | As users interact with your app, there will be various errors generated which need to be displayed to the user and logged for developers. These errors can come in a few different formats depending on if they are data validation errors or server generated errors. If the server is down or there is a problem with the code. Let’s have a look at the errors which may occur and how to handle them.
12 |
13 | All errors will need to be “caught” in the SPA so that the error can be handled. Lots of examples are throughout the demo app but here is a reminder when working with promises of one way to handle it:
14 |
15 | ```js
16 | AuthService.updatePassword(payload)
17 | .then(() => (this.message = "Password updated."))
18 | .catch((error) => (this.error = getError(error)));
19 | ```
20 |
21 | The above code returns a promise via the `AuthService` if that promise has an error attached to it the catch statement passes the error to a helper function `getError`. We will take a look at the `getError` helper method in a minute, but it’s helpful to understand the properties that can be on the error object. The first one is the response property.
22 |
23 | ```js
24 | error.response;
25 | ```
26 |
27 | The `response` property can be `undefined` if an API route that doesn’t exist is being hit or the API server is down. Generally, there will be a response retuned and on that, there are a few useful properties.
28 |
29 | ```js
30 | error.response.status;
31 | ```
32 |
33 | The `status` property will give the HTTP status code `200` etc.
34 |
35 | ```js
36 | error.response.headers;
37 | ```
38 |
39 | The `headers` property provides any headers sent back with the response.
40 |
41 | ```js
42 | error.response.data;
43 | ```
44 |
45 | Finally, the data property will provide details about the error usually with a message.
46 |
47 | When a user fills out a form in the SPA, there is the option to server side validate the data that is sent in the API. While SPA validation arguably gives a better user experience, it’s still important to handle validation errors server side. Laravel’s validation errors will be sent in the following format:
48 |
49 | ```js
50 | error.response.data.errors = {
51 | current_password: [
52 | "The current password field is required.",
53 | "The provided password does not match your current password.",
54 | ],
55 | password: ["The password field is required."],
56 | };
57 | ```
58 |
59 | Laravel might also send 404 or 500 error if the SPA code is trying to access a route that doesn’t exist or the API server is down. To handle all the possible errors let’s look at the `getError` helper method /utils/helpers.js in the Vue SPA.
60 |
61 | ```js
62 | export const getError = (error) => {
63 | const errorMessage = "API Error, please try again.";
64 |
65 | if (!error.response) {
66 | console.error(`API ${error.config.url} not found`);
67 | return errorMessage;
68 | }
69 | if (process.env.NODE_ENV === "development") {
70 | console.error(error.response.data);
71 | console.error(error.response.status);
72 | console.error(error.response.headers);
73 | }
74 | if (error.response.data && error.response.data.errors) {
75 | return error.response.data.errors;
76 | }
77 |
78 | return errorMessage;
79 | };
80 | ```
81 |
82 | The first condition checks for no response on the error object e.g. it’s `undefined` this usually indicates there is a 404 error. The API endpoint is logged to the console and the error message is returned.
83 |
84 | The second condition logs useful error date to the console if the App is in development mode.
85 |
86 | The last condition checks for errors attached to the data object on the response. These will be the Laravel validation errors.
87 |
88 | ### Displaying Errors
89 |
90 | To display any errors or messages there is a `FlashMessage` component. The component takes either a message or error as props. Let’s look at the methods and computed properties first:
91 |
92 | ```js
93 |
129 |
130 | ```
131 |
132 | The computed property `errorKeys` will check if there is an error and that the error is not a simple string. If it’s an object it returns the objects keys as an array. These keys will be looped over to get the title of each error, they are normally the Laravel API column names where the error occurred:
133 |
134 | - current_password
135 | - password
136 |
137 | The `errorKeys` are then looped over in the template and two things happen.
138 |
139 | 1. The Keys are passed into the `getErrors` method to display each of the errors against that key:
140 |
141 | ```js
142 | current_password: [
143 | "The current password field is required.",
144 | "The provided password does not match your current password."
145 | ],
146 | ```
147 |
148 | 2. The key is title cased using the filter `titleCase` to make it look like a heading and remove any underscores from the key name.
149 |
150 | The last method `getType` is used to safely get the type in JavaScript. It’s used for checking if the error is an object or string.
151 |
152 | ```js
153 | getType(obj) {
154 | return Object.prototype.toString
155 | .call(obj)
156 | .slice(8, -1)
157 | .toLowerCase();
158 | }
159 | ```
160 |
161 | Here is the template code for the `FlashMessage` component (css classes removed to simplify things). Depending on whether the error is a string or object, the errors are looped through and displayed under each error heading.
162 |
163 | ```js
164 |
165 |
166 |
167 | {{ message }}
168 |
169 |
173 | {{ error }}
174 |
175 |
179 |
180 | {{ key | titleCase }}
181 |
182 |
183 | {{ item }}
184 |
185 |
186 |
187 |
188 |
189 |
190 | ```
191 |
--------------------------------------------------------------------------------
/content/en/hosting.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Hosting & Going Live"
3 | description: "An overview of what is needed to publish your Vue Spa on Netlify and host the API via Laravel Forge."
4 | position: 15
5 | category: "Hosting"
6 | menuTitle: "Hosting Set up"
7 | ---
8 |
9 | Once you are ready to go live with your site, there are a few steps you will need to follow. Choosing the right host is important. Here we will look at hosting the Laravel API through [Laravel Forge](https://forge.laravel.com/) on [Digital Ocean](https://www.digitalocean.com/) and the Vue SPA on [Netlify](https://www.netlify.com/). The reason for choosing Forge and Netlify is because of how they offer one of the lowest barriers to entry for managing servers. This page doesn’t cover everything you need to do for setting up your hosting as it assumes basic knowledge of DNS and server admin.
10 |
11 | ### Forge
12 |
13 | Create a new server in the admin panel of Forge and make sure you have added your SSH key for your local machine. Once a server is provisioned, you will want to delete the default site and create a new site called `api.yourappname.com`. Hook it up to deploy from your GitHub account and make sure to select Let’s Encrypt to add the SSL certificate so that the domain runs on https.
14 |
15 | ### Configure Environment Variables
16 |
17 | In your Forge .env file (editable via the control panel) make sure to add/change the following variables. Obviously, change out the url to your own.
18 |
19 | **It’s critical you add these otherwise sessions and Sanctum will not work.** Also note the period `.yourappname.com` before the domain name, this allows for the API to run on a subdomain and still have sessions work.
20 |
21 | ```bash
22 | SANCTUM_STATEFUL_DOMAINS=yourappname.com
23 | SESSION_DOMAIN=.yourappname.com
24 | SPA_URL=https://yourappname.com
25 | ```
26 |
27 | While you are in the .env file add a mail provider, here is an example of setting up [Mailgun](https://www.mailgun.com/).
28 |
29 | ```bash
30 | MAIL_MAILER=smtp
31 | MAIL_HOST=smtp.eu.mailgun.org
32 | MAIL_PORT=587
33 | MAIL_USERNAME=addyourusername
34 | MAIL_PASSWORD=addyourpassword
35 | MAIL_ENCRYPTION=null
36 | MAIL_FROM_ADDRESS=info@yourappname.com
37 | MAIL_FROM_NAME="${APP_NAME}"
38 | ```
39 |
40 | ### View Database with Table Plus
41 |
42 | If you have your SSH key set up in Forge and added to your server you can connect to your database using a GUI such as Table Plus. Here are the details you need to add in the Table Plus config:
43 |
44 | 
45 |
46 | ### Netlify
47 |
48 | Netlify will be used to host the main SPA and control the DNS for both the API and SPA. It’s free to set up an account and host a starter project. Once you are logged in click on the “New site from Git” button and link up your repository from GitHub. Follow the 3 step on screen instructions and when you get to the basic build settings make sure you click on “show advanced”, add a new variable:
49 |
50 | 
51 |
52 | With the above in place you can go ahead and deploy your app. Once it’s deployed you can visit the app using the Netlify provided URL. The next thing to do will be to use Netlify for your DNS. Before you do, add a netlify.toml file in the root of your project.
53 |
54 | In the netlify.toml file add the following config. It will stop your SPA going to a Netlify 404 when the user refreshes their browser, read more in [the docs](https://docs.netlify.com/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps). Once you have done this, redeploy your app.
55 |
56 | ```bash
57 | [build]
58 | publish = "dist"
59 | command = "npm run build"
60 | [[redirects]]
61 | from = "/*"
62 | to = "/index.html"
63 | status = 200
64 | ```
65 |
66 | ### DNS Set up
67 |
68 | While your app can be viewed at yoursite.netlify.app, you need it to be routed to your domain. On the far right-hand side of the top menu click on site settings.
69 |
70 | 
71 |
72 | Then on the left-hand menu click on Domain management. On this page you will set up Netlify as your DNS provider and once your domain is pointing at Netlify you can add an SSL certificate using the Let’s Encrypt option.
73 |
74 | The final set up required is to add your subdomain for the API as a `A` record which points to the server you provisioned in Laravel Forge. Make sure to also add the details from your malign account so that mail can be sent. Here is the full list from the demo app for reference:
75 |
76 | 
77 |
78 | This should be everything set up and your app should now be live. The next thing to do is add a user via the register form and check that you receive a verification email. Once verified, you can log in and start testing.
79 |
--------------------------------------------------------------------------------
/content/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | description: "A step by step resource on how to build a Laravel API which has a Vue SPA to consume it's data."
4 | position: 1
5 | category: "Getting Started"
6 | fullscreen: true
7 | ---
8 |
9 |
10 |
11 |
12 | If you are wanting to go down the route of having a completely separate SPA that consumes a Laravel API then these docs should provide all the reference you need to get things set up.
13 |
14 | If you would like to hear an excellent explanation from Taylor on the how Sanctum and Fortify came about I highly recommend listening to his [podcast episode](https://blog.laravel.com/laravel-snippet-25-ecosystem-discussion-auth-recap-passport-sanctum).
15 |
16 | The example project files can be found on GitHub:
17 |
18 | - [Larvel API](https://github.com/garethredfern/laravel-api)
19 | - [Vue SPA](https://github.com/garethredfern/laravel-vue)
20 |
21 | ### Questions
22 |
23 | If you have any questions please feel free to [start a discussion](https://github.com/garethredfern/laravelvue-spa/discussions) over on GitHub.
24 |
25 | ### Further Learning
26 | [Codecourse](https://codecourse.com/) provides some excellent video tutorials on the subjects covered on this site and I highly recommend you take a look at the following:
27 |
28 | - [Laravel Sanctum Authentication with the Vue Composition API](https://codecourse.com/courses/laravel-sanctum-authentication-with-the-vue-composition-api)
29 | - [Vue Middleware Pipelines](https://codecourse.com/courses/create-vue-middleware-pipelines)
30 | - [Laravel Sanctum (Airlock) with Vue](https://codecourse.com/courses/laravel-airlock-with-vue)
31 | - [Laravel Sanctum (Airlock) with Postman](https://codecourse.com/courses/laravel-sanctum-airlock-with-postman)
32 |
33 | ### Help to Grow
34 |
35 | I want to keep growing this resource and provide a full set of documentation/videos on how to build Vue & Nuxt SPA's with a Laravel API. The plan is to release weekly updates until all areas of building an API and SPA are covered.
36 |
37 | **If you have found this project useful, please consider giving it a star on [GitHub](https://github.com/garethredfern/laravelvue-spa).**
38 |
39 |
40 |
--------------------------------------------------------------------------------
/content/en/middleware/middleware-admin.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Middleware Admin"
3 | description: "A Vue middleware to check if the authenticated user is an admin. If they are not then the route redirects to a 404 view."
4 | position: 19
5 | category: "Middleware"
6 | menuTitle: "Admin"
7 | ---
8 |
9 | A middleware to check if the authenticated user is an admin. If they are not then the route redirects to a 404 view.
10 |
11 | ```js
12 | export default function admin({ next, store }) {
13 | if (store.getters["auth/isAdmin"]) next();
14 | else next({ name: "notFound" });
15 | }
16 | ```
17 |
18 | To add this middleware to any route simply import it into your router/index.js file:
19 |
20 | ```js
21 | import admin from "@/middleware/admin";
22 | ```
23 |
24 | Finally add the admin method as a middleware router parameter on the meta property:
25 |
26 | ```js
27 | {
28 | path: "/users",
29 | name: "users",
30 | meta: { middleware: [auth, admin] },
31 | component: () =>
32 | import(/* webpackChunkName: "users" */ "../views/Users"),
33 | }
34 | ```
35 |
--------------------------------------------------------------------------------
/content/en/middleware/middleware-auth.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Middleware Auth"
3 | description: "A Vue middleware to check if a user is authenticated before displaying the protected route. If the authentication fails the user is redirected to the login page."
4 | position: 17
5 | category: "Middleware"
6 | menuTitle: "Auth"
7 | ---
8 |
9 | A middleware to check if a user is authenticated before displaying the protected route. If the authentication fails the user is redirected to the login page.
10 |
11 | ```js
12 | export default function auth({ to, next, store }) {
13 | const loginQuery = { path: "/login", query: { redirect: to.fullPath } };
14 |
15 | if (!store.getters["auth/authUser"]) {
16 | store.dispatch("auth/getAuthUser").then(() => {
17 | if (!store.getters["auth/authUser"]) next(loginQuery);
18 | else next();
19 | });
20 | } else {
21 | next();
22 | }
23 | }
24 | ```
25 |
26 | To add this middleware to any route simply import it into your router/index.js file:
27 |
28 | ```js
29 | import auth from "@/middleware/auth";
30 | ```
31 |
32 | Finally add the auth method as a middleware router parameter on the meta property:
33 |
34 | ```js
35 | {
36 | path: "/dashboard",
37 | name: "dashboard",
38 | meta: { middleware: [auth] },
39 | component: () =>
40 | import(/* webpackChunkName: "dashboard" */ "../views/Dashboard"),
41 | }
42 | ```
43 |
--------------------------------------------------------------------------------
/content/en/middleware/middleware-guest.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Middleware Guest"
3 | description: "A Vue middleware which checks if the current user is logged in and stops them seeing guest pages such as login."
4 | position: 18
5 | category: "Middleware"
6 | menuTitle: "Guest"
7 | ---
8 |
9 | A middleware which checks if the current user is logged in and stops them seeing guest pages such as login. If you are logged in then it makes no sense to be able to view the login view, the user gets redirected to the dashboard instead.
10 |
11 | ```js
12 | export default function guest({ next, store }) {
13 | const storageItem = window.localStorage.getItem("guest");
14 | if (storageItem === "isNotGuest" && !store.getters["auth/authUser"]) {
15 | store.dispatch("auth/getAuthUser").then(() => {
16 | if (store.getters["auth/authUser"]) {
17 | next({ name: "dashboard" });
18 | } else {
19 | store.dispatch("auth/setGuest", { value: "isGuest" });
20 | next();
21 | }
22 | });
23 | } else {
24 | next();
25 | }
26 | }
27 | ```
28 |
29 | To add this middleware to any route simply import it into your router/index.js file:
30 |
31 | ```js
32 | import guest from "@/middleware/guest";
33 | ```
34 |
35 | Finally add the guest method as a middleware router parameter on the meta property:
36 |
37 | ```js
38 | {
39 | path: "/login",
40 | name: "login",
41 | meta: { middleware: [guest] },
42 | component: () =>
43 | import(/* webpackChunkName: "login" */ "../views/Login"),
44 | }
45 | ```
46 |
--------------------------------------------------------------------------------
/content/en/middleware/middleware-overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Middleware Overview"
3 | description: "Adding middleware to a Vue Spa will keep code clean and provide a way to have multiple functions run before the route loads."
4 | position: 16
5 | category: "Middleware"
6 | menuTitle: "Overview"
7 | ---
8 |
9 | As your app grows, you will need to have a way to control what happens before a route is loaded. An example of this is covered in [adding authentication](/authentication/vue-authentication#protecting-routes-and-maintaining-state). Using the `beforeEach` router hook we check if a route `requiresAuth`, if it does then the authentication logic is run. This works well if you are only checking authentication, but what happens if you need to add additional checks for admin routes? A user is checked to see if they are authenticated, then if they are going to view the /users route they also have to be an admin. See the [basic authorization](/authorization/laravel-basic-authorization) example for a refresher on this functionality.
10 |
11 | ### Refactoring Authentication for Middleware
12 |
13 | To provide a solution for adding multiple checks on a route we can use the middleware design pattern. Making use of the `beforeEach` router hook we can chain multiple middleware functions together whilst keeping the router template code clean.
14 |
15 |
16 | Please note, this example is more advanced than the previous examples. There will be a number of changes made to the codebase. See the middleware release v1.5 on GitHub for more details.
17 |
18 |
19 | [Middleware release v1.5](https://github.com/garethredfern/laravel-vue/releases/tag/v1.5)
20 |
21 | Previously we set a `meta` attribute of `requiresAuth` on any route which needed authentication:
22 |
23 | ```js
24 | {
25 | path: "/dashboard",
26 | name: "dashboard",
27 | meta: { requiresAuth: true }
28 | //...
29 | }
30 | ```
31 |
32 | This can now be swapped out to pass an array of middleware functions that will be invoked before the route is entered:
33 |
34 | ```js
35 | {
36 | path: "/dashboard",
37 | name: "dashboard",
38 | meta: { middleware: [auth] }
39 | }
40 | ```
41 |
42 | The middleware functions will be kept together in a new folder src/middleware. Let’s have a look at the `auth` function. It should look familiar because most of the code is cut from the original `beforeEach` method we created.
43 |
44 | ```js
45 | export default function auth({ to, next, store }) {
46 | const loginQuery = { path: "/login", query: { redirect: to.fullPath } };
47 |
48 | if (!store.getters["auth/authUser"]) {
49 | store.dispatch("auth/getAuthUser").then(() => {
50 | if (!store.getters["auth/authUser"]) next(loginQuery);
51 | else next();
52 | });
53 | } else {
54 | next();
55 | }
56 | }
57 | ```
58 |
59 | See the [auth middleware page](/middleware/middleware-auth) for a detailed description of this method. For now, just focus on the pattern for a middleware function:
60 |
61 | ```js
62 | export default function auth({ to, next, store }) {}
63 | ```
64 |
65 | The function `auth` takes an object of parameters we require. This will usually be `to` and always be `next` which are passed in from the Vue router as `context`. Here we also require access to the Vuex store so that gets passed in too.
66 |
67 |
68 | Don’t forget to import any middleware into the router file at the top.
69 |
70 |
71 | Now let’s look at the new `beforeEach` router method and how we can then call the `auth` middleware method.
72 |
73 | ```js
74 | router.beforeEach((to, from, next) => {
75 | const middleware = to.meta.middleware;
76 | const context = { to, from, next, store };
77 |
78 | // Check if no middlware on route
79 | if (!middleware) {
80 | return next();
81 | }
82 |
83 | middleware[0]({ ...context });
84 | });
85 | ```
86 |
87 | Middleware and context get stored as variables:
88 |
89 | ```js
90 | const middleware = to.meta.middleware;
91 | ```
92 |
93 | The `context` object holds any properties required in the middleware being called:
94 |
95 | ```js
96 | const context = { to, from, next, store };
97 | ```
98 |
99 | A check is performed to see if there is no middleware on the route being requested. If there isn’t, return `next` which allows the route to load as normal.
100 |
101 | ```js
102 | if (!middleware) {
103 | return next();
104 | }
105 | ```
106 |
107 | Finally, call the first middleware function from the middleware array. Here we only have one `auth` to keep the example simple:
108 |
109 | ```js
110 | return middleware[0]({ ...context });
111 | ```
112 |
113 | ### The Middleware Pipeline
114 |
115 | So far, there has only been one middleware function called `auth`, let’s look at how we can call multiple middleware functions using a pipeline. On the /users route we need authentication and admin middleware:
116 |
117 | ```js
118 | {
119 | path: "/users",
120 | name: "users",
121 | meta: { middleware: [auth, admin] }
122 | }
123 | ```
124 |
125 | You can review the `admin` middleware function in its [separate middleware page](/middleware/middleware-admin). Let’s look at how we can call both `auth` and `admin` middleware functions from with the `berforeEach` hook.
126 |
127 | ```js
128 | router.beforeEach((to, from, next) => {
129 | const middleware = to.meta.middleware;
130 | const context = { to, from, next, store };
131 |
132 | if (!middleware) {
133 | return next();
134 | }
135 |
136 | middleware[0]({
137 | ...context,
138 | next: middlewarePipeline(context, middleware, 1),
139 | });
140 | });
141 | ```
142 |
143 | The only difference with the above `beforeEach` method and the previous one is this line:
144 |
145 | ```js
146 | next: middlewarePipeline(context, middleware, 1);
147 | ```
148 |
149 |
150 | Don’t forget to import the middlewarePipeline file at the top of the main router file.
151 |
152 |
153 | The `middlewarePipeline` method on the `next` property gets called recursively, passing in any context, middleware and the `index` for the next middleware array function to be called. Create a new file router/middlewarePipeline.js and add the following:
154 |
155 | ```js
156 | export default function middlewarePipeline(context, middleware, index) {
157 | const nextMiddleware = middleware[index];
158 |
159 | if (!nextMiddleware) {
160 | return context.next;
161 | }
162 |
163 | return () => {
164 | nextMiddleware({
165 | ...context,
166 | next: middlewarePipeline(context, middleware, index + 1),
167 | });
168 | };
169 | }
170 | ```
171 |
172 | Breaking the `middlewarePipeline` function down:
173 |
174 | 1. The `context`, `middleware` array and current array `index` get passed in.
175 | 2. A variable is created which saves the next middleware to run. If there are two items in the middleware array `[auth, admin]` and `auth` has just run `nextMiddleware` will hold `admin`.
176 | 3. If there are no more items in the middleware array the condition `if (!nextMiddleware)` checks for it and returns `next`, so that the route will still load.
177 | 4. If there is a middleware to run then `nextMiddleware` gets returned (and then called) passing in the `context` and recursively calling the `middlewarePipeline` function with the `index` being **incremented by 1** so that the next middleware can be run (if it exists).
178 |
179 | The middleware pipeline can take a bit of time to understand. Try to think of it as a helper method checking for any additional middleware to call pushing them through the pipeline until there are no more. Check out the full code by reviewing [Middleware release v1.5](https://github.com/garethredfern/laravel-vue/releases/tag/v1.5).
180 |
--------------------------------------------------------------------------------
/content/en/setup/demo.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Demo Application
3 | description: "Visit the demo Vue Spa and sign up for an account."
4 | position: 6
5 | category: "Getting Started"
6 | menuTitle: "Demo App"
7 | fullscreen: true
8 | ---
9 |
10 | To see what you will be building, you will need to install the Laravel API and the Vue Spa locally.
11 |
12 | The instructions to set the local projects up can be found here:
13 |
14 | - [laravel-setup](/setup/laravel-setup)
15 | - [vue-setup](/setup/vue-setup)
16 |
17 | Once you have everything installed locally you can create an account and login to see the demo.
18 |
--------------------------------------------------------------------------------
/content/en/setup/laravel-setup.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Setup Laravel API
3 | description: "How to set up Laravel Sanctum and Fortify for use as a headless API."
4 | position: 2
5 | category: "Getting Started"
6 | menuTitle: "Setup Laravel"
7 | ---
8 |
9 | First, set up the Laravel API as you normally would. Here we are using [Laravel Sail](https://laravel.com/docs/8.x/sail). If you choose to run Laravel via Sail, your API will be accessible via `http://localhost`.
10 |
11 | Make sure you change the following in your .env file:
12 |
13 | ```bash
14 | DB_HOST=127.0.0.1
15 | ```
16 |
17 | To this:
18 |
19 | ```bash
20 | DB_HOST=mysql
21 | ```
22 |
23 | Add a sender address in the `.env` so that email can be sent.
24 |
25 | ```bash
26 | MAIL_FROM_ADDRESS=test@test.com
27 | ```
28 |
29 | ### Running Artisan Commands
30 |
31 | You need to use the sail command to enable artisan to run within the Docker container.
32 |
33 | Example of running a migration:
34 |
35 | ```bash
36 | sail up -d
37 | sail artisan migrate
38 | ```
39 |
40 | ### Install Sanctum
41 |
42 | The full documentation can be found on the [Laravel website](https://laravel.com/docs/8.x/sanctum).
43 |
44 | ```bash
45 | composer require laravel/sanctum
46 |
47 | sail artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
48 | ```
49 |
50 | Sanctum needs some specific set up to enable it to work with a separate SPA. First lets add the following in your .env file:
51 |
52 | ```bash
53 | SANCTUM_STATEFUL_DOMAINS=localhost:8080
54 | SPA_URL=http://localhost:8080
55 | SESSION_DOMAIN=localhost
56 | ```
57 |
58 | The stateful domain tells Sanctum which domain you are using for the SPA. You can find the full notes and config for this in the config/sanctum.php file. As we are using cookies and sessions for authentication you need to add a session domain. This determines which domain the cookie is available to in your application. Full notes can be found in the config/session.php file and the [official documentation](https://laravel.com/docs/8.x/sanctum#spa-authentication).
59 |
60 | Add Sanctum's middleware to your api middleware group within your application's app/Http/Kernel.php file:
61 |
62 | ```php
63 | 'api' => [
64 | \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
65 | 'throttle:api',
66 | \Illuminate\Routing\Middleware\SubstituteBindings::class,
67 | ],
68 | ```
69 |
70 | ### Install Fortify
71 |
72 | The full documentation can be found on the [Laravel website](https://laravel.com/docs/8.x/fortify).
73 |
74 | ```bash
75 | composer require laravel/fortify
76 |
77 | sail artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
78 | ```
79 |
80 | Ensure the FortifyServiceProvider class is registered within the providers array of your application's config/app.php file.
81 |
82 | ```php
83 | /*
84 | * Application Service Providers...
85 | */
86 |
87 | App\Providers\FortifyServiceProvider::class,
88 | ```
89 |
90 | ### Database Seeding
91 |
92 | Set up a seed for adding a test user, in the DatabaseSeeder.php file add the following:
93 |
94 | ```php
95 | \App\Models\User::factory(1)->create(
96 | [
97 | 'name' => 'Luke Skywalker',
98 | 'email' => 'luke@jedi.com',
99 | 'email_verified_at' => null,
100 | ]
101 | );
102 | ```
103 |
104 | Run the migrations:
105 |
106 | ```bash
107 | sail artisan migrate --seed
108 | ```
109 |
--------------------------------------------------------------------------------
/content/en/setup/telescope.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test API Endpoints with Telescope"
3 | description: "Testing your API endpoints to see what is returned is an essential part of building a Laravel API, here's how to use Telescope."
4 | position: 5
5 | category: "Getting Started"
6 | menuTitle: "Telescope"
7 | ---
8 |
9 | Laravel has a first party package called [Telescope](https://laravel.com/docs/8.x/telescope). It provides insight into the requests coming into your application, exceptions, log entries, and a lot more. For any Laravel development you will find it an essential tool to have in your belt.
10 |
11 | ### Installing Telescope
12 |
13 | You can use Composer to install Telescope into your Laravel project. Don’t forget we are using Sail, if you are not, then swap `sail` for `php`.
14 |
15 | ```bash
16 | composer require laravel/telescope
17 | ```
18 |
19 | After installing Telescope, publish its assets using the telescope:install Artisan command. After installing Telescope, you should also run the migrate command in order to create the tables needed to store Telescope's data. Don’t forget we are using Sail, if you are not, then swap `sail` for `php`).
20 |
21 | ```bash
22 | sail artisan telescope:install
23 |
24 | sail artisan migrate
25 | ```
26 |
27 | Once you have installed Telescope visit the API url /telescope, if you have set your site up following the setup instructions using Sail then visit http://localhost/telescope in your browser. You will now be able to use it in parallel with [Insomnia or Postman](/setup/tooling). Every request that goes into your API will be recorded and you can see useful information to help with debugging. Here's and example of viewing a request and its associated response in the telescope control panel.
28 |
29 | 
30 |
31 | For further reading on what telescope can do take a look at the [documentation](https://laravel.com/docs/8.x/telescope).
32 |
--------------------------------------------------------------------------------
/content/en/setup/tooling.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Test API Endpoints with Insomnia or Postman"
3 | description: "Testing your API endpoints to see what is returned is an essential part of building a Laravel API, here's how to use Insomnia or Postman."
4 | position: 4
5 | category: "Getting Started"
6 | menuTitle: "Tooling"
7 | ---
8 |
9 | To test the API whilst building all its endpoints and data fetching functionality, you can use either [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/). Both tools allow you to interact with your API endpoints whilst saving the necessary authentication token.
10 |
11 | Both Insomnia and Postman should enable you to interact with Sanctum using cookies and sessions in the same way as the SPA does. It is much simpler to create a token endpoint which returns a Bearer token to use whilst interacting with the API locally. **Do not use this method authenticating your SPA** cookies and sessions are the preferred and more secure method in production. For testing locally a Bearer token is nice and simple and works well.
12 |
13 | ### Add HasApiTokens Trait to User Model
14 |
15 | To begin issuing tokens for users, your User model should use the `LaravelSanctumHasApiTokens` trait:
16 |
17 | ```php
18 | use Laravel\Sanctum\HasApiTokens;
19 |
20 | class User extends Authenticatable
21 | {
22 | use HasApiTokens, HasFactory, Notifiable;
23 | }
24 | ```
25 |
26 | ### Token Controller
27 |
28 | Create a `TokenController` either using the artisan command or by creating the file manually. The code to generate the token is taken directly from the [Sanctum documentation](https://laravel.com/docs/8.x/sanctum#issuing-mobile-api-tokens) accept returning a json response with the token set in a token variable.
29 |
30 | ```php
31 | use App\Models\User;
32 | use Illuminate\Http\Request;
33 | use Illuminate\Support\Facades\Hash;
34 | use Illuminate\Validation\ValidationException;
35 |
36 | class TokenController extends Controller
37 | {
38 | public function __invoke(Request $request)
39 | {
40 | $request->validate([
41 | 'email' => 'required|email',
42 | 'password' => 'required',
43 | 'device_name' => 'required',
44 | ]);
45 |
46 | $user = User::where('email', $request->email)->first();
47 |
48 | if (!$user || !Hash::check($request->password, $user->password)) {
49 | throw ValidationException::withMessages([
50 | 'email' => ['The provided credentials are incorrect.'],
51 | ]);
52 | }
53 |
54 | $token = $user->createToken($request->device_name)->plainTextToken;
55 |
56 | return response()->json(['token' => $token], 200);
57 | }
58 | }
59 | ```
60 |
61 | ### Token API Endpoint & Controller
62 |
63 | In your api routes file add the endpoint used to fetch a token:
64 |
65 | ```php
66 | use App\Http\Controllers\TokenController;
67 |
68 | Route::post('/sanctum/token', TokenController::class);
69 | ```
70 |
71 | ### Basic API Route & Controller
72 | Add the following to your api routes file:
73 | ```php
74 | use App\Http\Controllers\AuthController;
75 |
76 | Route::middleware(['auth:sanctum'])->group(function () {
77 | Route::get('/users/auth', AuthController::class);
78 | });
79 | ```
80 |
81 | Create an `AuthController` which will include the following code:
82 | ```php
83 | namespace App\Http\Controllers;
84 |
85 | use App\Http\Resources\UserResource;
86 | use Illuminate\Support\Facades\Auth;
87 |
88 | class AuthController extends Controller
89 | {
90 | public function __invoke()
91 | {
92 | return new UserResource(Auth::user());
93 | }
94 | }
95 | ```
96 | The AuthController uses a `UserResource` this is just a handy way to send json in the format you require, for more information review the [API resources](/api-resources-overview) section.
97 |
98 | To log in and receive a token you will need to send your login details and device name in a request using Insomnia or Postman:
99 |
100 | ```json
101 | {
102 | "email": "luke@jedi.com",
103 | "password": "password",
104 | "device_name": "insomnia"
105 | }
106 | ```
107 |
108 | Once you have successfully logged in, you will need to send the Bearer token with every request to the API. You can save the Bearer token in an environment variable for convenience. [Here’s how to do it with Insomnia](https://stackoverflow.com/questions/54925915/insomnia-using-oath2-0-how-do-i-pull-the-access-token-into-a-variable).
109 |
110 | ### Test Endpoints & Request Headers
111 |
112 | Make sure to send the `Bearer` token and `Accept: Application JSON` in the header for each request. Access the authenticated users details by sending a GET request:
113 |
114 | ```bash
115 | http://localhost/api/users/auth
116 | ```
117 |
118 | If everything has worked, you should receive a response like this:
119 |
120 | ```json
121 | {
122 | "id": 1,
123 | "name": "Luke Skywalker",
124 | "email": "luke@jedi.com",
125 | "email_verified_at": "2020-12-30T08:38:13.000000Z",
126 | "two_factor_secret": null,
127 | "two_factor_recovery_codes": null,
128 | "created_at": "2020-12-27T07:54:43.000000Z",
129 | "updated_at": "2021-01-07T07:10:42.000000Z"
130 | }
131 | ```
132 |
133 | Additional endpoints can be built then tested similarly.
134 |
135 | ### Useful Links
136 |
137 | - [Saving Environment Variables Insomnia](https://stackoverflow.com/questions/54925915/insomnia-using-oath2-0-how-do-i-pull-the-access-token-into-a-variable)
138 | - [Laravel Sanctum with Postman](https://blog.codecourse.com/laravel-sanctum-airlock-with-postman/)
139 |
--------------------------------------------------------------------------------
/content/en/setup/vue-setup.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Setup Vue SPA
3 | description: "How to set up a Vue SPA that uses Laravel as an API."
4 | position: 3
5 | category: "Getting Started"
6 | menuTitle: "Setup Vue"
7 | fullscreen: true
8 | ---
9 |
10 | It is assumed you have some experience working with VueJS, its router and state management package Vuex. You can install the Vue CLI using:
11 |
12 |
13 |
14 |
15 | ```bash
16 | npm install -g @vue/cli @vue/cli-service-global
17 | ```
18 |
19 |
20 |
21 |
22 | ```bash
23 | yarn global add @vue/cli @vue/cli-service-global
24 | ```
25 |
26 |
27 |
28 |
29 | Using the Vue CLI create a project, the example used is called `laravel-vue`:
30 |
31 | ```bash
32 | vue create laravel-vue
33 | ```
34 |
35 | When asked, install the following packages:
36 |
37 | - [Vue Router](https://router.vuejs.org/)
38 | - [Vuex](https://vuex.vuejs.org/)
39 | - [Axios](https://github.com/axios/axios)
40 |
41 | - [Tailwind CSS](https://tailwindcss.com/)
42 |
--------------------------------------------------------------------------------
/content/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Build a Laravel Vue Spa",
3 | "url": "https://laravelvuespa.com",
4 | "logo": {
5 | "light": "/logo-light.svg",
6 | "dark": "/logo-dark.svg"
7 | },
8 | "github": "garethredfern/laravelvue-spa",
9 | "twitter": "garethredfern"
10 | }
11 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "dist"
3 | command = "npm run generate"
4 | [[redirects]]
5 | from = "/setup/laravel"
6 | to = "/setup/laravel-setup"
7 | status = 301
8 | force = true
9 | [[redirects]]
10 | from = "/setup/vue"
11 | to = "/setup/vue-setup"
12 | status = 301
13 | force = true
14 | [[redirects]]
15 | from = "/authentication/laravel"
16 | to = "/authentication/laravel-auth"
17 | status = 301
18 | force = true
19 | [[redirects]]
20 | from = "/authentication/vue"
21 | to = "/authentication/vue-auth"
22 | status = 301
23 | force = true
24 | [[redirects]]
25 | from = "/authentication/laravel-auth"
26 | to = "/authentication/laravel-authentication"
27 | status = 301
28 | force = true
29 | [[redirects]]
30 | from = "/authentication/vue-auth"
31 | to = "/authentication/vue-authentication"
32 | status = 301
33 | force = true
34 |
--------------------------------------------------------------------------------
/nuxt.config.js:
--------------------------------------------------------------------------------
1 | import theme from "@nuxt/content-theme-docs";
2 | import getRoutes from "./utils/getRoutes";
3 |
4 | export default theme({
5 | docs: {
6 | primaryColor: "#d53f8c",
7 | },
8 | head: {
9 | htmlAttrs: {
10 | lang: "en-GB",
11 | },
12 | script: [
13 | {
14 | async: true,
15 | defer: true,
16 | "data-domain": "laravelvuespa.com",
17 | src: "https://plausible.io/js/plausible.js",
18 | },
19 | ],
20 | },
21 | buildModules: ["@nuxtjs/sitemap"],
22 | sitemap: {
23 | hostname: process.env.BASE_URL,
24 | routes() {
25 | return getRoutes();
26 | },
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravelvue-spa",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nuxt",
7 | "build": "nuxt build",
8 | "start": "nuxt start",
9 | "generate": "nuxt generate"
10 | },
11 | "dependencies": {
12 | "@nuxt/content-theme-docs": "^0.8.0",
13 | "@nuxtjs/sitemap": "^2.4.0",
14 | "nuxt": "^2.14.7"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/garethredfern/laravelvue-spa/baa148602c987e16202e03b2d5148e261dabe881/static/icon.png
--------------------------------------------------------------------------------
/static/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/static/logo-light.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/static/preview-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/garethredfern/laravelvue-spa/baa148602c987e16202e03b2d5148e261dabe881/static/preview-dark.png
--------------------------------------------------------------------------------
/static/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/garethredfern/laravelvue-spa/baa148602c987e16202e03b2d5148e261dabe881/static/preview.png
--------------------------------------------------------------------------------
/utils/getRoutes.js:
--------------------------------------------------------------------------------
1 | export default async () => {
2 | const { $content } = require("@nuxt/content");
3 | const authentication = await $content("en/authentication")
4 | .only(["path"])
5 | .fetch();
6 | const authorization = await $content("en/authorization")
7 | .only(["path"])
8 | .fetch();
9 | const fileUploads = await $content("en/file-uploads")
10 | .only(["path"])
11 | .fetch();
12 | const examples = await $content("en/examples")
13 | .only(["path"])
14 | .fetch();
15 | const setup = await $content("en/setup")
16 | .only(["path"])
17 | .fetch();
18 | const en = await $content("en")
19 | .only(["path"])
20 | .fetch();
21 |
22 | // Map and concatenate the routes and return the array.
23 | return []
24 | .concat(...authentication.map((x) => x.path.substring(3)))
25 | .concat(...authorization.map((x) => x.path.substring(3)))
26 | .concat(...fileUploads.map((x) => x.path.substring(3)))
27 | .concat(...examples.map((x) => x.path.substring(3)))
28 | .concat(...setup.map((x) => x.path.substring(3)))
29 | .concat(
30 | ...en.map((x) => x.path.substring(3)).filter((x) => x !== "/index")
31 | );
32 | };
33 |
--------------------------------------------------------------------------------