AWS Amplify + AWS AppSync + Vue.js (Vue CLI) を使ったお知らせ機能の実装
前回からの続き。
前回でシステムへのログインができるようになったので、ログインしている場合としていない場合で機能を分ける実装をしてみます。
例えば、「システムからのお知らせ」のように、ログインしていなくても見ることができる、登録は特定のユーザーのみ、というような機能ですね。
目次
手順
認証機能を使ったAPI機能の追加
AppSync を使った GraphQL のAPI機能を、 Amplify では構築できます。
これを使うことで、認証機能(ログイン済か)によってAPIでのデータアクセスを制御することができます。
AppSync を使う時の認証方法と、制限の組み合わせはこちらのようになります。
今回は GCP の OAuth 2.0 を使っているので、該当するのは “oidc” となります。
oidc は owner のみなので、ログインしたユーザーがお知らせを追加・編集するときには良いですね。
一方で、ログインしていないユーザーも情報を読むことができるようにしないといけないので、 apiKey と併用してデータアクセスできるようにしていきます。
では、実際に追加していきます。
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: sampleauth
? Choose the default authorization type for the API OpenID Connect
? Enter a name for the OpenID Connect provider: Cognito
? Enter the OpenID Connect provider domain (Issuer URL): https://cognito-idp.[YOUR_REGION].amazonaws.com/[YOUR_POOLID]/
? Enter the Client Id from your OpenID Client Connect application (optional):
? Enter the number of milliseconds a token is valid after being issued to a user: 0
? Enter the number of milliseconds a token is valid after being authenticated: 0
? Do you want to configure advanced settings for the GraphQL API Yes, I want to make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API API key
API key configuration
? Enter a description for the API key:
? After how many days from now the API key should expire (1-365): 365
? Configure conflict detection? No
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No
The following types do not have '@auth' enabled. Consider using @auth with @model
- Todo
Learn more about @auth here: https://aws-amplify.github.io/docs/cli-toolchain/graphql#auth
GraphQL schema compiled successfully.
Edit your schema at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/schema.graphql or place .graphql files in a directory at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/schema
Successfully added resource sampleauth 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
$
amplify add api
で API 機能を追加します。
色々と聞かれますが、ポイントだけ。
4行目、 “OpenID Connect” を選択。
6行目、 “Issuer URL” は下記URLを参照しつつ入力してください。
基本的には https://cognito-idp.{region}.amazonaws.com/{userPoolId}
となります。region
は ap-northeast-1
などの Cognito (Amplify) を使っているリージョンを、 userPoolId
は利用する Cognito の “プール ID” を指定します。(AWS Console上で確認しましょう)
12行目、追加の設定を聞かれるので “API key” を追加します。(スペースで選択)
18行目、サンプルの Schema を追加するか聞かれるので “Yes” を答えておきます。
ここまでできたら、いったん “push” して AWS 上に環境を構築しておきましょう。
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------------ | --------- | ----------------- |
| Api | sampleauth | Create | awscloudformation |
| Auth | sampleauthXXXXXXXX | No Change | awscloudformation |
? Are you sure you want to continue? Yes
The following types do not have '@auth' enabled. Consider using @auth with @model
- Todo
Learn more about @auth here: https://aws-amplify.github.io/docs/cli-toolchain/graphql#auth
GraphQL schema compiled successfully.
Edit your schema at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/schema.graphql or place .graphql files in a directory at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/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
GraphQL API KEY: da2-XXXXXXXXXXXXXXXXXXXXXXXXXX
$
push できたので、ここまでの作業に問題がないことが確認できます。
では、今回実装したい「お知らせ」機能に関する作業を進めていきましょう。
「お知らせ」のスキーマ作成
まず、お知らせ情報は「お知らせのタイトル (title)」と「お知らせ内容 (description)」を持つようにします。
ログインしたユーザーは自分の作成したものについて、「作成」「更新」「削除」「読み込み」、すべてをできるようにします。(今回はすべて明示的に記載しています)
また、未ログイン時には「読み込み」のみできるようにします。
これらの条件を踏まえると、下記のようなスキーマとなります。この内容で amplify/backend/api/sampleauth/schema.graphql
を書き換えてください。
type Information
@model
@auth(
rules: [
{ allow: owner, provider: oidc, operations: [create, update, delete, read] }
{ allow: public, provider: apiKey, operations: [read] }
]
) {
id: ID!
title: String!
description: String
updatedAt: AWSDateTime
}
id という項目は、項目がユニークであることを表すものとなります。これは、 AppSync が作成時に自動で UUID を生成して格納してくれます。
作成・更新日として updatedAt という項目もありますが、この内容も AppSync が自動で更新してくれます。
保存できたら、再度 push しましょう。
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------------ | --------- | ----------------- |
| Api | sampleauth | Update | awscloudformation |
| Auth | sampleauthXXXXXXXX | No Change | awscloudformation |
? Are you sure you want to continue? Yes
GraphQL schema compiled successfully.
Edit your schema at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/schema.graphql or place .graphql files in a directory at /Users/[YOUR_HOME]/sample-auth/amplify/backend/api/sampleauth/schema
? Do you want to update code for your updated GraphQL API Yes
? Do you want to generate GraphQL statements (queries, mutations and subscription) based on your schema types?
This will overwrite your current graphql queries, mutations and subscriptions Yes
⠸ 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
GraphQL API KEY: da2-XXXXXXXXXXXXXXXXXXXXXXXXXX
$
ここで、少し調整が必要になります。
ブラウザで AWS Console を開き、今回 push している AppSync のページを開いてください。(ここでは “sampleauth-dev” というような名前になっています)
開いたら、左側のメニューの「設定」をクリックし、「デフォルトの認証モード」を表示します。
ここの “OpenID Connect プロバイダードメイン (発行者 URL)” を見てください。
ここには、先ほど CLI で設定した “Issuer URL” が設定されているはずです。 CLI で設定した時は最後に “/” を付けないと設定を行なえなかったのですが、このままだとAPIの利用で必ず失敗してしまいます。
下記のように、 AWS Console 上から 最後の “/” を削除して保存を行なっておいてください。
「お知らせ」機能の実装
次に、 Vue.js でお知らせ機能を実装しましょう。
(Vue.js および HTML/CSS 周りの詳細の解説は割愛します。)
まずは、お知らせ一覧のページです。
<template>
<div>
<h2>Informations</h2>
<p>
<a href="/information/new">Add</a>
</p>
<div class="informations-list">
<ul>
<li v-for="(information, index) in informations" :key="index">
{{ information.title }}: {{ information.description }}
</li>
</ul>
</div>
</div>
</template>
<script>
import { API } from 'aws-amplify'
import { listInformations } from '@/graphql/queries'
export default {
mounted: async function() {
const informations = await API.graphql({
query: listInformations,
authMode: 'API_KEY',
}).catch(err => console.error('listInformations', err))
console.log('List mounted', informations)
this.informations = informations.data.listInformations.items
},
data: function() {
return {
informations: [],
}
},
}
</script>
<style scoped>
.informations-list {
display: inline-block;
text-align: left;
}
</style>
24〜26行目で、 “apiKey” を使ってお知らせ一覧を取得しています。
デフォルトの認証モード “以外” を使う場合、このように明示的に認証モードを指定する必要があります。
次は、お知らせを追加・編集するページです。
<template>
<div>
<h2>Add Information</h2>
<div class="infromation-item">
<label for="title">Title</label>
<input type="text" name="title" id="title" v-model="title" />
</div>
<div class="infromation-item">
<label for="description">Description</label>
<input
type="text"
name="description"
id="description"
v-model="description"
/>
</div>
<div class="btn">
<button @click="save">Save</button>
</div>
</div>
</template>
<script>
import { API, graphqlOperation } from 'aws-amplify'
import { getInformation } from '@/graphql/queries'
import { createInformation, updateInformation } from '@/graphql/mutations'
export default {
props: {
id: String,
},
mounted: async function() {
if (this.id && this.id !== 'new') {
const information = await API.graphql(
graphqlOperation(getInformation, { id: this.id }),
).catch(err => console.error('getInformation', err))
console.log('Information get', getInformation)
this.information = information.data.getInformation
}
if ('title' in this.information) {
this.title = this.information.title
}
if ('description' in this.information) {
this.description = this.information.description
}
},
data: function() {
return {
information: {},
title: '',
description: '',
}
},
methods: {
save: async function() {
const saveInformation = {}
if ('id' in this.information && this.information.id !== 'new') {
saveInformation.id = this.information.id
}
if (this.title && this.title.length > 0) {
saveInformation.title = this.title
}
if (this.description && this.description.length > 0) {
saveInformation.description = this.description
}
console.log(saveInformation)
if ('id' in saveInformation) {
// update
const savedInformation = await API.graphql(
graphqlOperation(updateInformation, { input: saveInformation }),
).catch(err => console.error('updateSurvey', err))
console.log('updateInformation', savedInformation)
} else {
// create
const savedInformation = await API.graphql(
graphqlOperation(createInformation, { input: saveInformation }),
).catch(err => console.error('savedSurvey', err))
console.log('createInformation', savedInformation)
this.information.id = savedInformation.data.createInformation.id
}
},
},
}
</script>
<style scoped>
.infromation-item {
display: flex;
align-items: center;
}
.infromation-item > label {
flex-basis: 10rem;
text-align: right;
margin-right: 0.5rem;
}
.infromation-item > input {
flex-grow: 1;
margin-right: 10rem;
}
.btn {
text-align: center;
}
</style>
32行目からの mounted
で、このページを表示した場合の処理を行なっています。
id
が “new” (URLが http://localhost:8080/information/new
のようになっている)の場合は新規作成、そうでない場合は 34~36行目のように指定されたお知らせを取得するようにしています。
56行目からの save
で、画面上の “Save”ボタンをクリックした時の処理を行なっています。
ここも新規作成と更新の場合わけを行なっています。基本は id
が “new” かどうかとしています。
更新であれば 73〜75行目で更新処理を、新規作成であれば 79〜81行目で追加処理を行なっています。
新規作成後の 83行目ですが、追加後に実際に追加した時の情報を savedInformation
で受け取っているので、ここの id
を利用することで、以降は更新の方を呼ばれるようにしています。
最後に、ルーティングを見てみます。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import { Auth, Cache } from 'aws-amplify'
import store from '../store'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/signin',
name: 'Signin',
component: () =>
import(/* webpackChunkName: "signin" */ '../views/Signin.vue'),
meta: { isPublic: true },
},
{
path: '/signout',
name: 'Signout',
component: () =>
import(/* webpackChunkName: "signin" */ '../views/Signout.vue'),
},
{
path: '/authed',
name: 'Authed',
component: () =>
import(/* webpackChunkName: "signin" */ '../views/Authed.vue'),
meta: { isPublic: true },
},
{
path: '/information',
name: 'List',
component: () =>
import(/* webpackChunkName: "information" */ '../views/List.vue'),
meta: { isPublic: true },
},
{
path: '/information/:id',
name: 'Information',
props: true,
component: () =>
import(/* webpackChunkName: "information" */ '../views/Information.vue'),
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
})
router.beforeEach(async (to, from, next) => {
const user = await Auth.currentUserInfo()
store.commit('setUser', user)
if (to.matched.some(record => !record.meta.isPublic) && user === null) {
next({ path: '/signin' })
} else {
if (user !== null) {
const currentSession = await Auth.currentSession()
updateJwtToken(currentSession)
}
next()
}
})
export default router
async function updateJwtToken(currentSession) {
console.log('currentSession', currentSession)
const token = currentSession.getIdToken().getJwtToken()
const info = {}
info.token = token
await Cache.setItem('federatedInfo', info)
}
35~48行目で、新しくルートを追加しています。
43行目で path
に :id
を設定しているので、 お知らせを追加・編集するページで id
として利用することができています。
また、 AppSync を oidc で利用する場合 Sign In 時に取得したトークンを利用する必要があります。
この辺りを (2020年4月29日現在) ライブラリがうまく扱えていないようなので、73~80行目のようにして手動で設定しています。
画面で確認
では、実際に画面をみていきましょう。
まず、 Sign In ページ。
“Google” ボタンクリック後に Sign In 完了すると、 “Home” へ自動遷移。
ナビゲーションの “Informations” をクリックしてお知らせページへ遷移。
現時点では何も表示されていません。お知らせを追加するために “Add” リンクをクリック。
“Title” と “Description” に文字を入力して “Save” ボタンをクリック。
(実装していないので)画面上に変化はありませんが、再度ナビゲーションの “Informations” をクリック。
真ん中に、先ほど追加したお知らせが表示されました!
では、このお知らせが誰でもみれるかも確認しましょう。
ナビゲーションの “Sign Out” をクリック。
“Sign Out” ボタンをクリックしてサインアウト実行。完了後に、もう一度ナビゲーションの “Informations” をクリック。
サインアウトした状態でも、お知らせを表示することができました!
コードは下記の GitHub で確認できます。
https://github.com/tacck/sample-auth-vue-amplify-gcp-oauth/tree/api
まとめ
Amplify から API (AppSync) の認証方法を複数使うことで、データ操作の権限をバックエンドレベルで調整することができました。
ただし、2点ほど調整すべき点もありました。
- AppSync に登録する “OpenID Connect プロバイダードメイン (発行者 URL)” の最後の “/” を、 AWS Console 上で削除する必要がある。
- Vue.js から AppSync へアクセスする時に必要なトークンを、手動で所定の一にセットする必要がある。
この2点さえ気を付けておけば、 Amplify で oidc を使ったデータ操作の基本は問題なくできると思います。