Building a Photo Uploader: A Practical Introduction to the JAMSTACK with Vue.js
Tags: jamstack, vue, auth0, cloudinary
Published: 2021-10-18 22:14:41

## Introduction

JAMSTACK is a JavaScript-powered stack that enables you to harness the powers of JavaScript, APIs, and markups. You can build with the JAMSTACK by using the various JavaScript frameworks and libraries and integrate with serverless functions or APIs. It is fast, lean and inexpensive.

In this tutorial, you will learn how to build JAMSTACK apps with a feature to upload images. You will use Cloudinary to power the upload functionality and [Auth0](https://auth0.com/) for authentication. This can come in handy when building photo albums, e-commerce applications, and business websites.

For a brief introduction to Vue.js, you can check out this article, [Getting Started with Vue JS: The Progressive JavaScript Framework](https://cloudinary.com/blog/getting_started_with_vue_js_the_progressive_javascript_framework).



## Prerequisites

1. A machine with [Node.js](https://nodejs.org/en/about/) and [npm](https://docs.npmjs.com/about-npm)) installed.

2. Basic familiarity with [JavaScript](https://www.w3schools.com/js/DEFAULT.asp), and [ES6](https://www.freecodecamp.org/news/write-less-do-more-with-javascript-es6-5fd4a8e50ee2/) and Vue.js).

3. A text editor. E.g. Sublime Text, Visual Studio Code, Atom, etc.

3. A Cloudinary account. You can [sign up for a Cloudinary account for free](https://cloudinary.com/users/register/free). 

4. An Auth0 account. You may create a free Auth0 account [here](https://auth0.com/signup).


## Install the Project Dependencies

You need the Node.js runtime and the npm to use [Vue.js](https://vuejs.org/). The Node package manager, [npm](https://www.npmjs.com/) will enable you to use Node.js on the command-line interface, CLI. If you do not have Node.js or npm installed, you can follow the instructions [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to install them on your machine.

Another tool that you need to follow in this tutorial is the [Vue CLI](https://cli.vuejs.org/) package. The `vue` library will provide the capabilities of the Vue.js framework while `vue-cli` will enable you to use certain terminal commands to interact with Vue.js and your project. 

You can go ahead to install Vue and the `vue-cli` tool if you have Node.js and npm on your machine already. Use the following terminal commands to install the two packages respectively.

```bash
npm install vue
npm install -g @vue/cli
```

In the next section, you will set up your Vue.js project.


## Create Vue Project

Use the following `vue-cli` command to set up a Vue.js project on your terminal. You may name the project `vue-photo-uploader`.

```bash
vue create vue-photo-uploader
```
Select Vue.js 2 version in the options prompted and allow Vue to scaffold the project folders and files for you. You should get a message that looks like the following in your terminal.

```bash

📄 Generating README.md...

🎉 Successfully created project vue-photo-uploader.
👉 Get started with the following commands:

```

Now, you need to set up the new project to handle file uploads. This is the purpose of the next section of the tutorial.

## Configue the Vue.js Project for File Uploads

To handle file uploads, you will need to create an interface for your users to be able to upload files. To do that, modify the `App.vue` file of your project as follows:

```vue
<template>
  <div id="app">
    <div>
      <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    </div>

    <div>
        <div>
            <button @click="openUploadModel">Add Photo</button>
        </div>
    </div>
  </div>
</template>
```

In the above code, you edited the message to be displayed by the `HelloWorld` component. You changed it to `learn how to upload photos with Cloudinary`. In addition, you created a button that calls the `openUploadModel` method. The method will open the Cloudinary upload modal when the button is clicked. Now, add the method inside the <script> part of the `App.vue` file as follows:

```js
<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
  methods: {
    openUploadModel() {
     }
}
</script>
```

The full code of the `App.vue` file becomes:

```html
<template>
  <div id="app">
    <div>
      <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    </div>

    <div>
        <div>
            <button @click="openUploadModel">Add Photo</button>
        </div>
    </div>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
  methods: {
    openUploadModal() {
     }
}
</script>
```


## Configure Cloudinary Upload Widget

**Login and obtain parameters**

Login to your Cloudinary account and go to your [console dashboard](https://cloudinary.com/console) and get your cloud name. Also, get your upload preset from your Cloudinary [acount settings](https://cloudinary.com/console/settings/upload). Copy the two values somewhere because you will need them in your Vue.js project.

**Configure Upload Preset settings**

In your account settings page, scroll down the [Upload Settings tab](https://cloudinary.com/console/settings/upload) to the Upload Preset section as shown below.

![Upload preset settings on Cloudinary dashboard](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957465326_4.+Scroll+down+to+Upload+Preset.png)


Activate the unsigned uploading feature by clicking the **Enable unsigned uploading** link. The link should change to **Signed** as shown below.


![Unsigned uploading feature enabled](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957494073_4b.+Enable+unSigned+uploading.png)

If you don't enable unsigned upload preset, you might get the following error.

```bash
Upload preset must be whitelisted for unsigned uploads
```

Next, navigate to the `index.html` page in the `public` folder of your project. Then, include the Cloudinary widget script inside the <body> tag.

```html
<body>
<script src="https://widget.cloudinary.com/v2.0/global/all.js" type="text/javascript"></script>
...
</body>
```


Next, modify the `openUploadModal` of the `App.vue` file as follows:

```js
  ...
    openUploadModal() {
      window.cloudinary.openUploadWidget(
        { cloud_name: '<CLOUD_NAME>',
          upload_preset: '<UPLOAD_PRESET>'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  
          }
        }).open();
    }
```

In the above code, you added the logic of the `@click` event of the button by which calls the `openUploadWidget` method that activates the Cloudinary file upload modal.

Replace `CLOUD_NAME` and `UPLOAD_PRESET` in the above code with the values you got from your Cloudinary account.


Next, run the project on your terminal:

```bash
npm run serve
```
Then, navigate to `127.0.0.1:8000` on your browser to add images to the application. You should get a page that looks like the following image.


![Photo album prototype live!](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957517736_1.+Ready-to-add-photo.png)


Click the `Add Photo` button and the upload modal will be displayed like the following.


![Cloudinary upload widget](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957529258_2.+Upload+Modal+Widget.png)


Now, you can upload your images.




## Deliver and View Images with Cloudinary

Cloudinary enables the delivery of images to the browser via its Vue.js SDK, `cloudinary-vue`. Install the library to get started:

```bash
npm install cloudinary-vue 
```

You need the following Cloudinary Vue.js components to achieve the goal of this section of the article.

`CldContext` - this tag helps you to set the parameters that all the child components share.
`CldImage` - the Cloudinary Image tag. It sources images from Cloudinary and makes them available on the browser.
`CldTransformation` - the tag that allows you to describe the transformations that are applied to the images delivered.

Next, import the components in the `<script>` section of the `App.vue` file. In addition, add the image `url` and `public_id` to the Data property of Vue.js. These two variables will be defined later in the article.

```js
<script>
...
import { CldContext, CldImage, CldTransformation } from 'cloudinary-vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    CldContext,
    CldImage,
    CldTransformation
  },
  data() {
    return {
      url: '',
      publicId: ''
    }
  },
...

</script>
```

Then, apply the imported components in the template code.

```vue
<template>
  <div id="app">
    ...

    <div>
    <cld-context cloudName="CLOUD_NAME">
      <div style="display: flex; justify-content: center;">
        <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>
         
      </div>
    </cld-context>
  </div>
  </div>
</template>

```

Don't forget to replace `CLOUD_NAME` in the code above with the real value gotten from your dashboard.

Whenever you upload an image, Cloudinary assigns a `public_id` to an image whenever you are uploading one.
Therefore, you can use the `public_id` returned in Cloudinary's JSON response when an image is being uploaded. 

So, modify the `openUploadModal()` method to capture the `public_id` of an uploaded image.

```js
    openUploadModal() {
      window.cloudinary.openUploadWidget(
        { cloud_name: 'CLOUD_NAME',
          upload_preset: 'UPLOAD_PRESET'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  

            //add the next 2 lines 
            this.url = result.info.url;
            this.publicId = result.info.public_id;
            }
        }).open();
    }
```
The full code of the `App.vue` file is the following:

```html
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    <div>
        <button @click="openUploadModel">Add Photo</button>
    </div>
  <div>
    <cld-context cloudName="diiayntjq">
      <div style="display: flex; justify-content: center;">
        <cld-image :publicId="publicId" width="250">
          <cld-transformation width="600" height="600" gravity="face" crop="thumb" />
         </cld-image>
         
      </div>
    </cld-context>
  </div>

  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import { CldContext, CldImage, CldTransformation } from 'cloudinary-vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    CldContext,
    CldImage,
    CldTransformation
  },
  data() {
    return {
      url: '',
      publicId: ''
    }
  },
  methods: {
    openUploadModel() {
      window.cloudinary.openUploadWidget(
        { cloud_name: 'diiayntjq',
          upload_preset: 'bi7uln2q'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  

            this.url = result.info.url;
            this.publicId = result.info.public_id;
          }
        }).open();
    }
  }    
}
</script>

```

## Authenticating with Auth0

Auth0 allows you to [authenticate](https://en.wikipedia.org/wiki/Authentication) and [authorize](https://en.wikipedia.org/wiki/Authorization) users. It integrates with several languages and frameworks for building applications. Furthermore, Auth0 proves a [Universal Login](https://auth0.com/docs/login/universal-login) page to help with authenticating users. The login page redirects users back to your application after they log in.

**Create an Auth0 Application**

To start the authentication setup, log in to your [Auth0 dashboard](https://manage.auth0.com/dashboard/) and follow the following steps to create an Auth0 Application.

1. Click **"Applications"** in the left sidebar menu. Supply these parameters:
 - **Name**: Vue.js Image Uploader
 -  **Application type** as Single Page Applications
2. Click **"Create"** button to complete the application creation
3. Click on the **"Settings"** tab of the newly created Application page. Set the following parameters:
 - **Allowed Callback URLs**: `http://localhost:8080`
 - **Allowed Logout URLs**: `http://localhost:8080`
 - **Allowed Web Origins**: `http://localhost:8080`
4. Scroll down the page and click on the **"Save Changes"** button to save the settings.

**Install Auth0** 
Install the Client SDK for Auth0 Single-Page Applications:

```bash
npm install @auth0/auth0-spa-js
```

Since you need to move from one page or link to another in the Vue.js application, you will need the [Vue router](https://router.vuejs.org/). Add it with the following command and choose **'yes'** when asked if you'd like to use it in history mode.

```bash
vue add router
```

Create a new folder inside the `src` folder and name it `auth`. Then, create a new file inside the `auth` folder and name it `index.js`. Add the following code inside the file.

```js
import Vue from "vue";
import createAuth0Client from "@auth0/auth0-spa-js";

/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

let instance;

/** Returns the current instance of the SDK */
export const getInstance = () => instance;

/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  redirectUri = window.location.origin,
  ...options
}) => {
  if (instance) return instance;

  // The 'instance' is simply a Vue object
  instance = new Vue({
    data() {
      return {
        loading: true,
        isAuthenticated: false,
        user: {},
        auth0Client: null,
        popupOpen: false,
        error: null
      };
    },
    methods: {
      /** Authenticates the user using a popup window */
      async loginWithPopup(options, config) {
        this.popupOpen = true;

        try {
          await this.auth0Client.loginWithPopup(options, config);
          this.user = await this.auth0Client.getUser();
          this.isAuthenticated = await this.auth0Client.isAuthenticated();
          this.error = null;
        } catch (e) {
          this.error = e;
          // eslint-disable-next-line
          console.error(e);
        } finally {
          this.popupOpen = false;
        }

        this.user = await this.auth0Client.getUser();
        this.isAuthenticated = true;
      },
      /** Handles the callback when logging in using a redirect */
      async handleRedirectCallback() {
        this.loading = true;
        try {
          await this.auth0Client.handleRedirectCallback();
          this.user = await this.auth0Client.getUser();
          this.isAuthenticated = true;
          this.error = null;
        } catch (e) {
          this.error = e;
        } finally {
          this.loading = false;
        }
      },
      /** Authenticates the user using the redirect method */
      loginWithRedirect(o) {
        return this.auth0Client.loginWithRedirect(o);
      },
      /** Returns all the claims present in the ID token */
      getIdTokenClaims(o) {
        return this.auth0Client.getIdTokenClaims(o);
      },
      /** Returns the access token. If the token is invalid or missing, a new one is retrieved */
      getTokenSilently(o) {
        return this.auth0Client.getTokenSilently(o);
      },
      /** Gets the access token using a popup window */

      getTokenWithPopup(o) {
        return this.auth0Client.getTokenWithPopup(o);
      },
      /** Logs the user out and removes their session on the authorization server */
      logout(o) {
        return this.auth0Client.logout(o);
      }
    },
    /** Use this lifecycle method to instantiate the SDK client */
    async created() {
      // Create a new instance of the SDK client using members of the given options object
      this.auth0Client = await createAuth0Client({
        ...options,
        client_id: options.clientId,
        redirect_uri: redirectUri
      });

      try {
        // If the user is returning to the app after authentication..
        if (
          window.location.search.includes("code=") &&
          window.location.search.includes("state=")
        ) {
          // handle the redirect and retrieve tokens
          const { appState } = await this.auth0Client.handleRedirectCallback();

          this.error = null;

          // Notify subscribers that the redirect callback has happened, passing the appState
          // (useful for retrieving any pre-authentication state)
          onRedirectCallback(appState);
        }
      } catch (e) {
        this.error = e;
      } finally {
        // Initialize our internal authentication state
        this.isAuthenticated = await this.auth0Client.isAuthenticated();
        this.user = await this.auth0Client.getUser();
        this.loading = false;
      }
    }
  });

  return instance;
};

// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
  install(Vue, options) {
    Vue.prototype.$auth = useAuth0(options);
  }
};
```

The options object in the above code is used to pass in the `domain` and `clientId` values. Hence, you have to create a JSON file called `auth_config.json` in the root folder of the Vue.js application and add the following values to it. Obtain the domain and clientId values from the **"Settings"** tab of the Auth0 Application page and put them in the JSON file like the following.


```bash
{
  "domain": "YOUR_DOMAIN",
  "clientId": "YOUR_CLIENT_ID"
}
```


Next, import the Auth0 plugin and Vue router in the `main.js` file as shown below.

```vue
...
import router from './router' // newly added line

// Import the Auth0 configuration
import { domain, clientId } from "../auth_config.json";

// Import the plugin here
import { Auth0Plugin } from "./auth";

// Install the authentication plugin here
Vue.use(Auth0Plugin, {
  domain,
  clientId,
  onRedirectCallback: appState => {
    router.push(
      appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname
    );
  }
});

Vue.config.productionTip = false;

new Vue({
  router, // newly added line
  render: h => h(App)
}).$mount("#app");
```

**Configure Log in and Log out follows**

Edit the App.vue file by adding two buttons for login and log out after the HelloWorld tag in the template code.

```vue
<template>
  <div id="app">
  ...

    <div v-if="!$auth.loading">
        <!-- show login when not authenticated -->
        <button v-if="!$auth.isAuthenticated" @click="login">Log in</button>
        <!-- show logout when authenticated -->
        <button v-if="$auth.isAuthenticated" @click="logout">Log out</button>
    </div>

  ...
  </div>
</template>
```

Next, add the login and log out methods in the list of methods inside the `<script>` section

```js
...
  methods: {
    // Log the user in
    login() {
      this.$auth.loginWithRedirect();
    },
    // Log the user out
    logout() {
      this.$auth.logout({
        returnTo: window.location.origin
      });
    },
    openUploadModel() {
      ...
```

Now, you have added the login and log-out buttons and can proceed to test the authentication flow. Go to 127.0.0.1:8000 on your web browser. You will see the login button present now as shown below.


![Login button is visible on the page now](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957574898_Screenshot+2021-09-29+at+23.45.16.png)


When you click the login button, you will be directed to the Auth0 Universal login Page where your users can sign up or log in after which they will be redirected to your application.


![Auth0 universal login page activated](https://paper-attachments.dropbox.com/s_AFF2FF088F20EBD06640F4218CCE5DBD4255763E00DF0ED2933F2E3D5F60B001_1632957589856_Screenshot+2021-09-29+at+23.45.43.png)


You can study the [The Complete Guide to Vue.js User Authentication with Auth0](https://auth0.com/blog/complete-guide-to-vue-user-authentication/) for a detailed explanation of the Auth0 implementation in Vue.js.



## Conclusion

With the knowledge gained, you can try implementing the [image and video upload](https://cloudinary.com/documentation/image_video_and_file_upload) in your next project. You may also develop a full-fledged user profile with Auth0. Much more, you can learn about the [JAMSTACK](https://jamstack.org/) and its ecosystem as you build more JAMSTACK projects.

Thanks for your attention.

Content created for the [Hackmamba](https://content.hackmamba.io/) Jamstack Content Hackathon using [Auth0](https://auth0.com/), [Cloudinary](https://cloudinary.com/) and [Vue.js](https://vuejs.org/).