Series List
Adding a public and private API (Part 2, Implementation) - You are here
Deploy via S3
Repository
Creating Our API Module
To get started let's create an API Vuex module. We'll use this module to do all the communication with our API. So, let's create this module at /store/api.js
within our project.
Within this store, we're going to need to import API from the aws-amplify package as well as our queries and mutations Amplify generated for us in the last lesson.
For now, let's also leave the state empty. We'll fill this in when we get to rigging up our posts.
import { API } from 'aws-amplify' import * as gqlQueries from '~/src/graphql/queries' import * as gqlMutations from '~/src/graphql/mutations' export const state = {}
Copied!
Let's add a single getter called authMode
. We'll use this to determine which authorization mode to use for each API call.
export const getters = { authMode: (state, getters, rootState) => rootState.auth.isAuthenticated ? 'AMAZON_COGNITO_USER_POOLS' : 'API_KEY' }
Copied!
We'll also only have a single mutation. This mutation will set whatever key-value pair is passed into it.
export const mutations = { set(state, { key, value }) { state[key] = value } }
Copied!
Lastly, we'll have three actions to start with. These will be helper actions to serve as a middleman to help keep things DRY, since repeating this code for each request would be a lot of repeating if our API were to grow.
export const actions = { /** * API Helpers * These api helpers help cut back on repetitive code we'd otherwise need in each action */ async get({ commit, getters }, { key, query, id }) { const { data } = await API.graphql({ query: gqlQueries[query], variables: { id }, authMode: getters.authMode }) const value = data[query] if (key) commit('set', { key, value }) return value }, async query({ commit, getters }, { key, query, filter }) { const { data } = await API.graphql({ query: gqlQueries[query], variables: { filter }, authMode: getters.authMode }) const value = data[query].items if (key) commit('set', { key, value }) return value }, async mutate({ commit, getters }, { key, mutation, input }) { const { data } = await API.graphql({ query: gqlMutations[mutation], variables: { input }, authMode: getters.authMode }) const value = data[mutation] if (key) commit('set', { key, value }) return value } }
Copied!
Each of our helper actions accepts a key and query or mutation. If a key is provided the action will set whatever the result of our query/mutation is in our state under that name. Query or mutation will be a string of the name of the query or mutation we want to run, for example "listPosts"
.
All of our results return back a data object, which we'll use object spreading to extract from our response. Inside our data object will be a key of the name of the query or mutation that was run. This is why we reference the query or mutation string off data to extract the actual value.
const value = data[query]
Copied!
Our get
action will be used to get a specific record from our database by an id.
Our query
action will be used to query lists of items from our database. We can optionally provide a filter to filter down our results. To get the value of our query results we need to nest one level deeper inside an items
key. For example, our value path will be data.listPosts.items
.
const data = { listPosts: { items: [] } }
Copied!
Our mutate
action will be used to create, update, and delete records in our database. The input
we pass into it will be the fields required for the model. You can find these fields within your API documentation in AppSync.
So, here's the full picture of what our API store module should currently look like.
import { API } from 'aws-amplify' import * as gqlQueries from '~/src/graphql/queries' import * as gqlMutations from '~/src/graphql/mutations' export const state = { } export const getters = { authMode: (state, getters, rootState) => rootState.auth.isAuthenticated ? 'AMAZON_COGNITO_USER_POOLS' : 'API_KEY' } export const mutations = { set(state, { key, value }) { state[key] = value } } export const actions = { /** * API Helpers * These api helpers help cut back on repetitive code we'd otherwise need in each action */ async get({ commit, getters }, { key, query, id }) { const { data } = await API.graphql({ query: gqlQueries[query], variables: { id }, authMode: getters.authMode }) const value = data[query] if (key) commit('set', { key, value }) return value }, async query({ commit, getters }, { key, query, input, filter }) { const { data } = await API.graphql({ query: gqlQueries[query], variables: { input, filter }, authMode: getters.authMode }) const value = data[query].items if (key) commit('set', { key, value }) return value }, async mutate({ commit, getters }, { key, mutation, input }) { const { data } = await API.graphql({ query: gqlMutations[mutation], variables: { input }, authMode: getters.authMode }) const value = data[mutation] if (key) commit('set', { key, value }) return value } }
Copied!
Creating Our User Profile Module
Next, let's create a store to manage our user's profile. Let's create a module called user at /store/user.js
. You could also use the auth store or name this store profile.
For this module, we won't need to import any dependencies. Our state and mutations will start off pretty typical. We'll have a user state item defaulted to null and a setUser
mutation to mutate our user state.
export const state = { user: null } export const mutations = { setUser(state, user) { state.user = user } }
Copied!
We'll be working with three actions; these will be called getUser, createUser, and findOrCreateUser.
export const actions = { async getUser({ commit, dispatch }, id) { const user = await dispatch( 'api/get', { query: 'getUser', id }, { root: true } ) commit('setUser', user) return user }, async createUser({ commit, dispatch }, input) { const user = await dispatch( 'api/mutate', { mutation: 'createUser', input }, { root: true } ) commit('setUser', user) return user }, async findOrCreateUser({ dispatch }, { attributes, username }) { let user = await dispatch('getUser', username) if (user) return user return dispatch('createUser', { id: username, email: attributes.email, createdAt: Date.now() }) } }
Copied!
Our getUser
action dispatches our to our API module calling api/get
to fetch a user profile by the id provided. Note our user profile id and authenticated user id will be one in the same. Also, note that the { root: true }
at the end of our dispatch tells Vuex to run this dispatch in the root context outside this module.
Our createUser
action dispatches to our API module calling api/mutate
to create a new user profile with the fields defined inside the input object.
Lastly, our findOrCreateUser
action will use our getUser
action to attempt to find a user profile. If it does that user profile will be returned, otherwise a new one will be created with the data passed in. The attributes and username parameters will be coming directly from our logged-in user data.
So, here's our completed user Vuex module.
export const state = { user: null } export const mutations = { setUser(state, user) { state.user = user } } export const actions = { async getUser({ commit, dispatch }, id) { const user = await dispatch( 'api/get', { query: 'getUser', id }, { root: true } ) commit('setUser', user) return user }, async createUser({ commit, dispatch }, input) { const user = await dispatch( 'api/mutate', { mutation: 'createUser', input }, { root: true } ) commit('setUser', user) return user }, async findOrCreateUser({ dispatch }, { attributes, username }) { let { getUser } = await dispatch('getUser', username) if (getUser) return getUser return dispatch('createUser', { id: username, email: attributes.email, createdAt: Date.now() }) } }
Copied!
Creating A User Profile On Login
Now we can use the findOrCreateUser
action during login to load our user's profile or create a profile if one can't be found.
To do this let's open our auth Vuex store at /store/auth.js
. Below our commit('set', user)
let's add a call do findOrCreateUser
, passing in the user object we get back from calling Auth.signIn
.
// log user in & get back JWT session async login({ commit, dispatch }, { email, password }) { const user = await Auth.signIn(email, password) commit('set', user) await dispatch('user/findOrCreateUser', user, { root: true }) return user },
Copied!
While we're here, we'll also want to get our user profile on load if there's an authenticated user. We can hook this check into our auth store's load action, again just below the commit('set', user)
.
// load the currently authenticated user if there is one async load({ commit, dispatch }) { try { const user = await Auth.currentAuthenticatedUser() commit('set', user) if (user) { // get and set user's profile await dispatch('user/getUser', user.username, { root: true }) } return user } catch (error) { commit('set', null) } },
Copied!
At this point, we can go ahead and test and make sure everything we have so far is working. We can register a new user, and upon registration, the user should have a profile created for them. We can log in to an older account and verify that a profile was also created for them. And, we can reload to verify it's loading okay if the user were to leave and come back.
One Last Auth Change
Before we move on from authentication changes, later on, we'll be using our user's id for privilege checking. Let's take a moment here to expand our $auth
helper class with an id-getter.
get id() { if (!this.user) return return this.user.username }
Copied!
Now that we have the authentication bits out of the way, we can move into setting up and handling our posts.
Adding Our Post API
To start, let's expand upon the API store we created earlier in this lesson. So open up /store/api.js
and let's add our post CRUD (create, read, update, delete) actions. We'll be able to make all of these actions short and sweet by using the API helpers we set up when we created this module.
First and foremost, though, let's add in our default state for our post items.
export const state = { posts: [], post: null }
Copied!
We'll use posts
to store the results of our list queries and post
to store the results of our get queries.
Next, let's move into our actions. We'll have five actions to communicate with our post endpoints.
listPosts
to get all our posts, you can add filtering to this if you'd like.getPost
to get a specific post.createPost
to make a new post.updatePost
to update a specific post's data.deletePost
to delete a specific post.
Each action will call one of our API helper actions. If a key is provided the value we get back from our API will be stored in our Vuex state under that key.
export const actions = { async listPosts({ dispatch }) { return dispatch('query', { key: 'posts', query: 'listPosts' }) }, async getPost({ dispatch }, id) { return dispatch('get', { key: 'post', query: 'getPost', id }) }, async createPost({ dispatch }, input) { return dispatch('mutate', { key: 'post', mutation: 'createPost', input }) }, async updatePost({ dispatch }, input) { return dispatch('mutate', { key: 'post', mutation: 'updatePost', input }) }, async deletePost({ dispatch }, id) { return dispatch('mutate', { mutation: 'deletePost', input: { id } }) }, // ... api helpers ... // }
Copied!
Creating A Post
Now that we have everything in place to communicate with our API, we can start working on our views. To start with let's add a view to create a post at /pages/posts/create.vue
<template> <div class="p-6"> <h1 class="text-3xl font-semibold mb-6">Create New Post</h1> <form @submit.prevent="create"> <label>Title</label> <input v-model="form.title" placeholder="Email" class="form-control" /> <label>Summary</label> <textarea v-model="form.summary" placeholder="Email" class="form-control"></textarea> <label>Body</label> <textarea v-model="form.body" placeholder="Email" class="form-control"></textarea> <button type="submit" class="button--green">Create</button> </form> </div> </template> <script> export default { data: () => ({ form: { title: '', summary: '', body: '' } }), methods: { async create() { try { const post = await this.$store.dispatch( 'api/createPost', this.getPayload() ) this.$router.push('/') } catch (error) { console.log({ error }) } }, getPayload() { return { ...this.form, authorId: this.$auth.user.username, createdAt: Date.now() } } } } </script>
Copied!
We can test this out and verify all the API calls are resolving okay.
Listing Our Posts
Now we'll need to list these out. We'll do this on our home page at /pages/index.vue
below all our authentication checking. While we're here we can also rig up the ability to delete a post.
<template> <!-- auth checks are up here --> <!-- Posts --> <div v-for="post in posts" :key="post.id" class="my-6"> <h4 class="text-lg font-semibold"> <nuxt-link :to="`/posts/${post.id}`">{{ post.title }}</nuxt-link> </h4> <p>{{ post.summary }}</p> <small>By: {{ post.author.email }}</small> <div v-if="post.author.id === $auth.id" class="flex justify-center text-xs"> <nuxt-link :to="`/posts/${post.id}/edit`" class="mr-3">Edit</nuxt-link> <button @click="deletePost(post.id)" class="text-red-500">Delete</button> </div> </div> </template> <script> export default { async asyncData({ store }) { return { posts: await store.dispatch('api/listPosts') } }, methods: { async deletePost(id) { // delete the post await this.$store.dispatch('api/deletePost', id) // refresh our post data this.posts = await this.$store.dispatch('api/listPosts') } } } </script>
Copied!
Now we can check and verify the posts we created will actually pull down from the API and list out. We can also verify we can delete successfully.
View A Post
Next up, let's create a page to view a specific post at /pages/posts/_id/index.vue
. Note that in Nuxt, a directory beginning with a _ is treated as a route parameter. Meaning _id
will be a post's actual id in the URL.
<template> <div class="flex flex-col p-12"> <nuxt-link to="/">< Back</nuxt-link> <h1 class="text-4xl mb-3">{{ post.title }}</h1> <p class="text-lg mb-3 pb-3 border-b border-grey-light">{{ post.summary}}</p> <p>{{ post.body }}</p> </div> </template> <script> export default { async asyncData({ store, params }) { return { post: await store.dispatch('api/getPost', params.id) } } } </script>
Copied!
Edit A Post
Then finally, we need to be able to edit our posts. You can alter the create page to allow this, but to keep things separate and easy to read through, I'll be creating a new page at /pages/posts/_id/edit.vue
<template> <div class="p-6"> <h1 class="text-3xl font-semibold mb-6">Update {{ post.title }}</h1> <form @submit.prevent="update"> <label>Title</label> <input v-model="form.title" placeholder="Email" class="form-control" /> <label>Summary</label> <textarea v-model="form.summary" placeholder="Email" class="form-control"></textarea> <label>Body</label> <textarea v-model="form.body" placeholder="Email" class="form-control"></textarea> <button type="submit" class="button--green">Update</button> </form> </div> </template> <script> export default { async asyncData({ store, params }) { const post = await store.dispatch('api/getPost', params.id) return { post, form: { title: post.title, summary: post.summary, body: post.body } } }, data: () => ({ form: { title: '', summary: '', body: '' } }), methods: { async update() { try { const post = await this.$store.dispatch( 'api/updatePost', this.getPayload() ) this.$router.push('/') } catch (error) { console.log({ error }) } }, getPayload() { return { ...this.form, id: this.post.id } } } } </script>
Copied!
Testing Access Controls
Finally, before we wrap up let's test our access controls and make sure they're all working. In order to fully do this we'll need to set up an admin group and assign at least one user to that group.
Let's head into the AWS Management Console and traverse to Cognito. Click "Manage User Pools" then click on your project name. Go to user's and group in the left-hand menu then click the Groups tab. Click "Create Group" and create a group with the name "admin" and click "Create group," everything else can remain empty.
To assign this group to a user, go back to the Users tab. Click on a user, click "Add to group," select your admin group, then click "Add to group."
Cases To Test
A user who's not an admin cannot edit or delete a post they don't own.
A user who is an admin can edit or delete any post.
A user who is authenticated can view a list of posts and a single post.
A user whose unauthenticated can view a list of posts and a single post.
Next Lesson
That does it for this lesson. We skipped over updating and deleting our user's profile, however, the process is nearly identical to what we did for our posts. So, it'll serve as something good for you to walk through on your own, and if you should need help I'll be here.
In the next lesson, we'll cover deploying.
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!