Amplifyでズボラ開発♪オフィス受付アプリの簡単レシピ

こんにちは!三度の飯より飯が好き。どうも僕です🍚🍙🌾

最近キラメックスでは大半のメンバーがリモートワークになり、オフィスへの来訪者も減っていることから、これまで使っていた有料の来客受付用のWebアプリを解約することになりました。

そこで、それに変わるものを作ろう!ということで、AWS Amplifyを使用して作ってみました!!

目次

受付アプリの要件

時代はMVP(Minimum Viable Product)開発!!ということで、受付ボタンを押すとSlackで通知されるというシンプルな構成です。

  • オフィスの受付にはiPadを設置しSafariで来客受付用のWebアプリケーションを開いておく
  • 受付画面には「受付」ボタンを表示
  • 「受付」ボタンをタップ(クリック)するとSlackに通知が飛び来客があることを社内に知らせてくれる

完成イメージ f:id:mtr-krmx:20210702155351p:plain


Slackの通知イメージ f:id:mtr-krmx:20210707105404p:plain

環境

$ node -v
v15.14.0

$ yarn -v
1.22.10

$ vue -V
@vue/cli 4.5.13

$ amplify -v
5.1.0

Vue.jsは2系で構築していきます。

プロジェクトの作成、セットアップ

$ vue create reception-demo

# 設定は以下の通りです。
# RouterとVuexを使いますので、チェックを入れてください
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Vuex, 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: Basic
? 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? (y/N) No

$ cd reception-demo

$ yarn serve --open
#(ctrl+cで停止)

ブラウザが立ち上がり、この画面が表示されればOKです! f:id:mtr-krmx:20210707135159p:plain

Amplifyの初期化

$ amplify init

? Enter a name for the project receptiondemo
? Initialize the project with the above configuration? No
? 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
? Select the authentication method you want to use:⇛皆さんの環境に合わせて設定

ライブラリを追加します。

$ yarn add aws-amplify aws-amplify-vue

今回は、デザインのフレームワークにVuetifyを使うので以下のコマンドで追加します。

$ vue add vuetify

? Still proceed? Yes
? Choose a preset: Default (recommended)

src/main.jsの編集

src/main.jsを以下の通りに編集します。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Amplify, * as AmplifyModules from "aws-amplify";
import { AmplifyPlugin } from "aws-amplify-vue";
import aws_exports from "./aws-exports";
import { components } from "aws-amplify-vue";
import vuetify from './plugins/vuetify'

Vue.config.productionTip = false;

Amplify.configure(aws_exports);

Vue.use(AmplifyPlugin, AmplifyModules);

new Vue({
  router,
  store,

  components: {
    App,
    ...components,
  },

  vuetify,
  render: h => h(App)
}).$mount('#app')

この時点で、画面を確認すると、Vuetifyが適応されていますね!
余談ですがVuetifyはセンスがない僕でもイケイケなWebアプリケーションが簡単に作れるのでおすすめです。これで僕も渋谷系エンジニアですね🥳

$ yarn serve --open

f:id:mtr-krmx:20210706175449p:plain

Amplify:認証機能の追加

以下のコマンドで、認証(Amazon Cognito)機能を追加します。

$ amplify add auth

 Do you want to use the default authentication and security configuration? Default configuration
 How do you want users to be able to sign in? Email
 Do you want to configure advanced settings? No, I am done.

pushすることで、AWS上にCognitoのリソースが作成されます。

$ amplify push

Amazon Cognitoでユーザーアカウントを作成

AWSのマネージメントコンソールに入り、Cognito>ユーザープール>receptiondemo{suffix} を開きます。

「ユーザーとグループ」を開き、ユーザーを新規作成します。 ユーザー名、メールアドレスはご自身のメールアドレス等を設定してください。 f:id:mtr-krmx:20210706105231p:plain

ログイン画面の追加

src/components/SignIn.vueの作成

src/components/SignIn.vueを作成して、以下の通りに編集します。これがログイン画面です。

$ touch src/components/SignIn.vue
<template>
  <v-row justify="center" align-content="center">
    <div class="signin">
      <div v-if="!signedIn">
        <amplify-authenticator
          v-bind:authConfig="authConfig"
        ></amplify-authenticator>
      </div>
      <div v-if="signedIn">
        <amplify-sign-out></amplify-sign-out>
      </div>
    </div>
  </v-row>
</template>
<script>
import { AmplifyEventBus } from "aws-amplify-vue";
import { Auth } from "aws-amplify";
export default {
  name: "SignIn",
  data() {
    return {
      signedIn: false,
      authConfig: {
        signInConfig: {
          isSignUpDisplayed: false,
        },
      },
    };
  },
  async beforeCreate() {
    try {
      await Auth.currentAuthenticatedUser();
      this.signedIn = true;
    } catch (err) {
      this.signedIn = false;
    }
    AmplifyEventBus.$on("authState", (info) => {
      if (info === "signedIn") {
        this.signedIn = true;
      } else {
        this.signedIn = false;
      }
    });
  },
};
</script>

src/router/index.jsの編集

src/router/index.jsを以下の通りに編集します。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import SignIn from '@/components/SignIn.vue'
import store from '@/store/index.js'

import { AmplifyEventBus } from 'aws-amplify-vue'
import * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin } from 'aws-amplify-vue'

Vue.use(VueRouter)
Vue.use(AmplifyPlugin, AmplifyModules)


let user

getUser().then((user) => {
  if (user) {
    router.push({path: '/'})
  }
})

function getUser() {
  return Vue.prototype.$Amplify.Auth.currentAuthenticatedUser().then((data) => {
    if (data && data.signInUserSession) {
      store.commit('setUser', data)
      return data;
    }
  }).catch(() => {
    store.commit('setUser', null)
    return null
  })
}

AmplifyEventBus.$on('authState', async (state) => {
  if (state === 'signedOut'){
    user = null
    store.commit('setUser', null)
    router.push({path: '/signin'}, () => {})
  } else if (state === 'signedIn') {
    user = await getUser();
    router.push({path: '/'})
  }
})

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { requireAuth: true }
  },
  {
    path: '/signin',
    name: 'signin',
    component: SignIn
  }
]

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

router.beforeResolve(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) {
    user = await getUser()
    if (!user) {
      return next({
        path: '/signin'
      })
    }
    return next()
  }
  return next()
})

export default router

src/store/index.jsの編集

src/store/index.jsを以下の通りに編集します。

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
    },
  },
  actions: {},
  modules: {},
});

ここまで来たら、もう一度画面を開いてみましょう。
今度は、ログイン画面が表示されます。
Cogitoで作成したユーザーのメールアドレスと仮パスワードでログインします。
新しいパスワードの入力を求められるので、新しいパスワードを設定するとログインすることができます!
f:id:mtr-krmx:20210706192451p:plain

Amplify:APIとFunctionの追加

以下のコマンドでGraphQLのAPIを追加します。

$ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: receptiondemo
? 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)
? Do you want to edit the schema now? No

続いて、LambdaのFunctionも追加します。

$ amplify add function

? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: receptiondemo
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? No

amplify/backend/api/receptiondemo/schema.graphqlの編集

APIの追加で自動生成されたamplify/backend/api/receptiondemo/schema.graphqlを以下の通りに編集します。
これでreceptionというクエリを呼び出すとGraphQL経由でLambdaの関数が実行されます。
nameの値はamplify add functionの際につけたFunctionの名前の語末に-${env}を付与したものです。

type Query {
  reception: String @function(name: "receptiondemo-${env}")
}

amplify/backend/function/receptiondemo/src/index.jsの編集

続いてLambdaの関数を実装しましょう!Slackに投稿する処理です。
自動生成されたamplify/backend/function/receptiondemo/src/index.jsを以下の通りに編集します。

const https = require ('https');
const slack_url = new URL(process.env.SLACK_URL);

exports.handler = function (event, context) {
    var slack_req_opts = {}
    slack_req_opts.protocol = slack_url.protocol
    slack_req_opts.host = slack_url.host
    slack_req_opts.path = slack_url.pathname
    slack_req_opts.method = 'POST';
    slack_req_opts.headers = {'Content-Type': 'application/json'};

    var req = https.request(slack_req_opts, function (res) {
        if (res.statusCode === 200) {
            context.succeed('post slack success');
        } else {
            context.fail('fail status code: ' + res.statusCode);
        }
    });
    req.on('error', function (e) {
        console.log('error: ' + e.message);
        context.fail(e.message);
    });

    // Slackに投稿される内容。
    // channel等、省略した場合はSlackのIncoming WebHook作成時に設定した値が使われる。
    var str = "<!channel> 受付に来訪者がお見えです。対応お願いします!"; // 投稿される文章。
    var channel = "reception"; // 投稿先のチャンネル名。省略可。
    var username = '受付'; // 投稿のユーザー名。省略可。
    var icon = ':watermelon:' // 投稿のアイコン。省略可。
    req.write(JSON.stringify({text: str, channel:channel, username:username,icon_emoji:icon}));

    req.end();
};

ここで一度、pushします。AWS上にLambdaの関数やAppSyncのリソースが作成されます。

$ amplify push 

? 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

Lambdaの環境変数にSlackのIncoming WebHookのURLを設定

SlackのIncoming WebHookを作成し発行されたURLをLambdaの環境変数に設定します。
Slack Incoming WebHookの発行手順については本記事では省略します。

AWSのマネージメントコンソールを開きLambda関数のreceptiondemo-devを開きます。
設定>環境変数 からSLACK_URLの値にIncoming WebHookのURL(例https://hooks.slack.com/services/●●●/▲▲▲/★★★)を設定します。
f:id:mtr-krmx:20210706221531p:plain

メインの画面の実装

自分はCongitoもGraphQLもこれまで全く触ったことがなかったのですがAmplify大先生のおかげで簡単にバックエンドが構築できました。
一生ついていきますAmplify先生🙇‍♂️
ここからは皆様のお待ちかね、フロントエンド側の実装です。受付のメイン画面を実装していきます。

メインのロゴ

src/assetsの下にロゴの画像ファイルを置きます。
本記事では会社のロゴ画像をlogo.pngという名前で置きました。

src/App.vueの編集

src/App.vueを以下の通りに編集します。
3行目のcolorはヘッダーの色です。お好みで変えてください。

<template>
  <v-app>
    <v-app-bar app color="#50A602" dark> </v-app-bar>
    <v-main>
      <router-view />
    </v-main>
  </v-app>
</template>
<script>
export default {
  name: "App",

  data: () => ({
    //
  }),
};
</script>

src/components/HelloWorld.vueの編集

最後はsrc/components/HelloWorld.vueです。以下の通りに編集します。
6行目はassetsの下に置いた画像のファイルを指定してください。
「受付」ボタンを押すと、GraphQLのreceptionクエリが呼び出されます。

<template>
  <v-container>
    <v-row class="text-center">
      <v-col cols="12">
        <v-img
          :src="require('../assets/logo.png')"
          class="my-3"
          contain
          height="200"
        />
      </v-col>
      <v-col class="mb-4">
        <v-btn
          class="mt-2"
          color="primary"
          elevation="24"
          rounded
          x-large
          width="300"
          height="100"
          @click="reception"
          ><h3>
            受付
          </h3></v-btn
        >
      </v-col>
      <v-col cols="12">
        御用の方は、こちらを一度タップしてお待ち下さい
      </v-col>
      <v-dialog v-model="dialog" max-width="290" class="text--white">
        <v-card
          class="mx-auto text--white"
          color="#1F7087"
          dark
          max-width="400"
        >
          <v-card-title />
          <v-card-text
            class="headline font-weight-bold text--white"
            color="white"
          >
            少々お待ち下さい。<br />
            担当の者が参ります。
          </v-card-text>
          <v-card-text class="text--white">
            <div>(このダイアログは自動で閉じます)</div>
          </v-card-text>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn
              class="ml-2 mt-5"
              outlined
              rounded
              small
              @click="dialog = false"
            >
              CLOSE
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </v-row>
  </v-container>
</template>
<script>
import { API, graphqlOperation } from "aws-amplify";
import { reception } from "../graphql/queries";

export default {
  data: function() {
    return {
      res: "",
      dialog: false,
    };
  },
  methods: {
    async reception() {
      this.dialog = true;
      const result = await API.graphql(graphqlOperation(reception));
      this.res = result.data.reception;
      setTimeout(() => {
        this.dialog = false;
      }, 3500);
    },
  },
};
</script>

これで完成です!
動作確認してみましょう!

$ yarn serve --open

受付ボタンを押すとSlackに通知され、来訪があることを知らせてくれます!!!
f:id:mtr-krmx:20210707105404p:plain

ホスティング

最後に、完成したサイトをホスティングします。

$ amplify add hosting

? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Manual deployment
$ amplify publish

? Are you sure you want to continue? Yes
・・・・
✨  Done in 47.47s.
✔ Zipping artifacts completed.
✔ Deployment complete!
https://dev.●●●.amplifyapp.com

↑最後に出力されるURLにアクセスしてみてください。
ログイン画面が表示され、前の手順で作成したユーザーでログインすると受付画面が表示されます。
あとは、このURLをタブレットのブラウザで開きオフィスの受付に設置すれば見事に受付対応してくれます。
( `・ω・´)🎉🎉🎉

Amplifyプロジェクトの削除

削除するときも簡単です、Amplifyならね😏
以下のコマンドを実行すると今回作成されたAWS上のリソースが削除されます。

$ amplify delete

あとは reception-demoディレクトリごと削除すればファイルも綺麗さっぱりです。

終わりに

いかがだったでしょうか!?
Amplifyを使うと、簡単にバックエンド・フロントエンド一括してアプリケーションが構築できるのでお手軽で高速に開発することができますね。

今後は、以下についても機能を拡充させて行きたいです。

  • エラーハンドリング処理の実装
  • Lambdaの環境変数をAmplify&Secrets Managerで管理
  • Slackの投稿に「受付対応します」ボタンを設置して社内の誰が対応するかわかりやすくする

それではまたお会いしましょう🙂