How to make own Image Post Service by AWS Amplify + Vue.js
Tags: aws, amplify, vue, graphql
Published: 2021-03-31 14:43:49

Sometimes, we want to make "Image Post Service" for ourself or our friends.

This article introduces to you how to make "Private Image Post Service" by AWS Amplify and Vue.js.

**TOC**

| Section | Title |
|:---:| --- |
| 0.  | [Why AWS Amplify](#section-0) |
| 1.  | [Setup](#section-1) |
| 2.  | [Add Auth](#section-2) |
| 3.  | [Add API](#section-3) |
| 4.  | [Add Storage](#section-4) |
| 5.  | [Implement](#section-5) |
| 6.  | [Check](#section-6) |

<a name="section-0"></a>
## 0. Why AWS Amplify

AWS Amplify is so useful service and tool.

"Private Image Post Service" is required below functions.

* Authentication
* Item management
* Storage

AWS Amplify meets these requirements.
And it is "Serverless" framework, so we don't need to manage the instances.

<a name="section-1"></a>
## 1. Setup

* Vue CLI (+ Vuetify)
* Amplify CLI

### 1.1 Vue CLI

Install "Vue CLI".

[Vue CLI](https://cli.vuejs.org/guide/prototyping.html)

``` bash
$ vue -V
@vue/cli 4.5.12
$
```

### 1.2 Amplify CLI

Install "Amplify CLI".

[AWS Amplify](https://docs.amplify.aws/start/getting-started/installation/q/integration/vue)

``` bash
$ amplify -v
Scanning for plugins...
Plugin scan successful
4.46.1
$
```

### 1.3 Create Vue project

{% details vue create amplify-image-upload %}
``` bash
$ vue create amplify-image-upload


Vue CLI v4.5.12
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

(snip)

🎉  Successfully created project amplify-image-upload.
👉  Get started with the following commands:

 $ cd amplify-image-upload
 $ yarn serve

$ cd amplify-image-upload
$ vue add vuetify

(snip)

? Choose a preset: Default (recommended)

(snip)

✔  Successfully invoked generator for plugin: vue-cli-plugin-vuetify
 vuetify  Discord community: https://community.vuetifyjs.com
 vuetify  Github: https://github.com/vuetifyjs/vuetify
 vuetify  Support Vuetify: https://github.com/sponsors/johnleider

$
```
{% enddetails %}

### 1.4 Setup Amplify project

{% details amplify init %}
``` bash
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project amplifyimageupload
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  yarn build
? Start Command: yarn serve
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use default
Adding backend environment dev to AWS Amplify Console app: XXXXXXXXXXXXXX
⠇ Initializing project in the cloud...

(snip)


✔ Initialized provider successfully.
Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!

Some next steps:
"amplify status" will show you what you've added already and if it's locally configured or deployed
"amplify add <category>" will allow you to add features like user login or a backend API
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify console" to open the Amplify Console and view your project status
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything

$
```
{% enddetails %}

Add UI Components.

``` bash
$ yarn add aws-amplify @aws-amplify/ui-vue
```

And, edit `src/main.js` to include AWS Amplify.

[src/main.js](https://github.com/tacck/amplify-image-upload/blob/main/src/main.js)

{% details src/main.js %}
``` JavaScript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
import '@aws-amplify/ui-vue'
import Amplify from 'aws-amplify'
import awsconfig from './aws-exports'

Amplify.configure(awsconfig)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  vuetify,
  render: (h) => h(App),
}).$mount('#app')
```
{% enddetails %}

<a name="section-2"></a>
## 2. Add Auth

Add Authentication.

{% details amplify add auth %}
``` bash
$ amplify add auth
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource amplifyimageuploadXXXXXXXX locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

$
```
{% enddetails %}

Push project.

{% details amplify push %}
``` bash
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name              | Operation | Provider plugin   |
| -------- | -------------------------- | --------- | ----------------- |
| Auth     | amplifyimageuploadXXXXXXXX | Create    | awscloudformation |
? Are you sure you want to continue? Yes
⠙ Updating resources in the cloud. This may take a few minutes...

(snip)

✔ All resources are updated in the cloud


$
```
{% enddetails %}

<a name="section-3"></a>
## 3. Add API

Add API(GraphQL) for Item management.

{% details amplify add api %}
``` bash
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: amplifyimageupload
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

The following types do not have '@auth' enabled. Consider using @auth with @model
	 - Todo
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/auth


GraphQL schema compiled successfully.

Edit your schema at /[YOUR_DIRECTORY]/amplify-image-upload/amplify/backend/api/amplifyimageupload/schema.graphql or place .graphql files in a directory at /[YOUR_DIRECTORY]/amplify-image-upload/amplify/backend/api/amplifyimageupload/schema
? Do you want to edit the schema now? No
Successfully added resource amplifyimageupload locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

$
```
{% enddetails %}

Edit Schema: `amplify/backend/api/amplifyimageupload/schema.graphql`

[amplify/backend/api/amplifyimageupload/schema.graphql](https://github.com/tacck/amplify-image-upload/blob/main/amplify/backend/api/amplifyimageupload/schema.graphql)

``` graphql
type Item @model @auth(rules: [{ allow: owner, provider: userPools }]) {
  id: ID!
  name: String!
  createdAt: AWSDateTime
}
```

Push project.

{% details amplify push %}
``` bash
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name              | Operation | Provider plugin   |
| -------- | -------------------------- | --------- | ----------------- |
| Api      | amplifyimageupload         | Create    | awscloudformation |
| Auth     | amplifyimageuploadXXXXXXXX | No Change | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

Edit your schema at /[YOUR_DIRECTORY]/amplify-image-upload/amplify/backend/api/amplifyimageupload/schema.graphql or place .graphql files in a directory at /[YOUR_DIRECTORY]/amplify-image-upload/amplify/backend/api/amplifyimageupload/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠸ Updating resources in the cloud. This may take a few minutes...

(snip)

✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud

GraphQL endpoint: https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.ap-northeast-1.amazonaws.com/graphql


$
```
{% enddetails %}

<a name="section-4"></a>
## 4. Add Storage

Add Storage.

{% details amplify add storage %}
``` bash
$ amplify add storage
? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: XXXXXXXXXX
? Please provide bucket name: amplifyimageuploadXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users? create/update, read, delete
? Do you want to add a Lambda Trigger for your S3 Bucket? No
Successfully added resource XXXXXXXXXX locally

If a user is part of a user pool group, run "amplify update storage" to enable IAM group policies for CRUD operations
Some next steps:
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

$
```
{% enddetails %}

And, push project.

{% details amplify push %}
``` bash
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name              | Operation | Provider plugin   |
| -------- | -------------------------- | --------- | ----------------- |
| Storage  | XXXXXXXXXX                 | Create    | awscloudformation |
| Auth     | amplifyimageuploadXXXXXXXX | No Change | awscloudformation |
| Api      | amplifyimageupload         | No Change | awscloudformation |
? Are you sure you want to continue? Yes
⠸ Updating resources in the cloud. This may take a few minutes...

(snip)

✔ All resources are updated in the cloud


$
```
{% enddetails %}

<a name="section-5"></a>
## 5. Implement

Let's implementation for Image Post Service.

> See details.
> https://github.com/tacck/amplify-image-upload

### 5.1 Files

[src/router/index.js](https://github.com/tacck/amplify-image-upload/blob/main/src/router/index.js)

{% details src/router/index.js %}
``` JavaScript
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Items',
    component: () =>
      import(/* webpackChunkName: "public" */ '../views/Items.vue'),
  },
  {
    path: '/signin',
    name: 'SignIn',
    component: () => import(/* webpackChunkName: "public" */ '../views/SignIn'),
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
})

export default router
```
{% enddetails %}

[src/App.vue](https://github.com/tacck/amplify-image-upload/blob/main/src/App.vue)

{% details src/App.vue %}
``` vue
<template>
  <v-app>
    <v-app-bar app color="primary" dark>
      <div class="d-flex align-center">
        <v-img
          alt="Vuetify Logo"
          class="shrink mr-2"
          contain
          src="https://cdn.vuetifyjs.com/images/logos/vuetify-logo-dark.png"
          transition="scale-transition"
          width="40"
        />

        <v-img
          alt="Vuetify Name"
          class="shrink mt-1 hidden-sm-and-down"
          contain
          min-width="100"
          src="https://cdn.vuetifyjs.com/images/logos/vuetify-name-dark.png"
          width="100"
        />
      </div>

      <v-spacer></v-spacer>

      <v-menu offset-y>
        <template v-slot:activator="{ on, attrs }">
          <v-btn color="white--text" dark text v-bind="attrs" v-on="on">
            Menu
          </v-btn>
        </template>
        <v-list>
          <v-list-item
            v-for="(item, index) in items"
            :key="index"
            :to="item.to"
          >
            <v-list-item-title>{{ item.title }}</v-list-item-title>
          </v-list-item>
        </v-list>
      </v-menu>
    </v-app-bar>

    <v-main>
      <router-view />
    </v-main>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data: function () {
    return {
      items: [
        {
          title: 'Top',
          to: '/',
        },
        {
          title: 'Sign In',
          to: '/signin',
        },
      ],
    }
  },
}
</script>
```
{% enddetails %}

[src/views/SignIn.vue](https://github.com/tacck/amplify-image-upload/blob/main/src/views/SignIn.vue)

Write the Authentication logic as follows.

> See detail:
> https://docs.amplify.aws/ui/auth/authenticator/q/framework/vue

{% details src/views/SignIn.vue %}
``` vue
<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <amplify-authenticator>
          <div v-if="authState === 'signedin' && user">
            <div>Hello, {{ user.username }}</div>
          </div>
          <amplify-sign-up
            slot="sign-up"
            :form-fields.prop="signUpFormFields"
          ></amplify-sign-up>
          <amplify-sign-out></amplify-sign-out>
        </amplify-authenticator>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { onAuthUIStateChange } from '@aws-amplify/ui-components'

export default {
  name: 'AuthStateApp',
  created() {
    this.unsubscribeAuth = onAuthUIStateChange((authState, authData) => {
      this.authState = authState
      this.user = authData
    })
  },
  data() {
    return {
      user: undefined,
      authState: undefined,
      unsubscribeAuth: undefined,
      signUpFormFields: [
        {
          type: 'username',
          required: true,
        },
        {
          type: 'email',
          required: true,
        },
        {
          type: 'password',
          required: true,
        },
      ],
    }
  },
  beforeDestroy() {
    this.unsubscribeAuth()
  },
}
</script>

<style></style>
```
{% enddetails %}

[src/views/Items.vue](https://github.com/tacck/amplify-image-upload/blob/main/src/views/Items.vue)

{% details src/views/Items.vue %}
``` vue
<template>
  <v-container>
    <v-row>
      <v-col cols="12">
        <v-card
          ><v-card-title>Upload</v-card-title>
          <v-file-input
            accept="image/*"
            append-outer-icon="mdi-send"
            @click:append-outer="upload"
            v-model="uploadFile"
          ></v-file-input>
        </v-card>
      </v-col>
    </v-row>
    <v-row>
      <v-col cols="6" v-for="(item, index) in items" :key="index">
        <v-dialog width="unset">
          <template v-slot:activator="{ on, attrs }">
            <v-card>
              <v-img
                contain
                aspect-ratio="1.7"
                :src="item.url"
                max-height="400"
                v-bind="attrs"
                v-on="on"
              ></v-img>
              <v-card-text>{{ item.name }}</v-card-text>
            </v-card>
          </template>
          <template v-slot:default="dialog">
            <v-card>
              <amplify-s3-image
                width="100%"
                class="d-flex justify-center"
                :img-key="item.id"
                level="private"
                @click="dialog.value = false"
              />
            </v-card>
          </template>
        </v-dialog>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import { API, graphqlOperation, Storage } from 'aws-amplify'
import { listItems } from '@/graphql/queries'
import { createItem } from '@/graphql/mutations'

export default {
  data: function () {
    return {
      uploadFile: null,
      file: null,
      items: [],
    }
  },
  created: async function () {
    await this.listItems()
  },
  methods: {
    upload: async function () {
      // create new graphql record
      const createdItem = await API.graphql(
        graphqlOperation(createItem, {
          input: {
            name: this.uploadFile.name,
          },
        }),
      )

      // upload to s3
      try {
        await Storage.put(createdItem.data.createItem.id, this.uploadFile, {
          level: 'private',
          contentType: this.uploadFile.type,
        })
        await this.listItems()
      } catch (err) {
        console.error('Error uploading file: ', err)
      }

      this.uploadFile = null
    },
    listItems: async function () {
      const listedItems = await API.graphql(graphqlOperation(listItems))
      this.items = await this.urlAdd(listedItems.data.listItems.items)
    },
    urlAdd: async function (items) {
      const result = await Promise.all(
        items.map(async (item) => {
          item.url = await Storage.get(item.id, {
            level: 'private',
          })
          return item
        }),
      )

      return result
    },
  },
}
</script>

<style scoped>
amplify-s3-image {
  --width: 90vw;
}
</style>
```
{% enddetails %}

### 5.2 Point

Main logic is in "Item.vue".

For management the items, we create a record:

``` JavaScript
      const createdItem = await API.graphql(
        graphqlOperation(createItem, {
          input: {
            name: this.uploadFile.name,
          },
        }),
      )
```

When we upload to Storage, write this:

``` JavaScript
        await Storage.put(createdItem.data.createItem.id, this.uploadFile, {
          level: 'private',
          contentType: this.uploadFile.type,
        })
```

`level: 'private'` means "when you upload the image, only you can see it".

There are 2 ways to show the image from Storage.

"Get by Library" is here:

``` JavaScript
          item.url = await Storage.get(item.id, {
            level: 'private',
          })
```

"Get by Library" is get authenticated-url from Storage.
This is simple way. But we should resolve about "async/await".

I think, it's little bit complex.
But, this way makes the UI more expressive.

Another ("Get by UI Component") is here:

``` vue
              <amplify-s3-image
                width="100%"
                class="d-flex justify-center"
                :img-key="item.id"
                level="private"
                @click="dialog.value = false"
              />
```

"Get by UI Component" is more simple.
Just use `<amplify-s3-image>` tag.
But, this way becomes less expressive power of the UI.

<a name="section-6"></a>
## 6. Check

Please watch this demonstration movie.

{% youtube LKeCz_i-57M %}

That's it!

I hope your system is working well.