Series List
Adding a public and private API (Part 1, Setup) - You are here
Adding a public and private API (Part 2, Implementation)
Deploy via S3
Repository
We left off in the last lesson with basic authentication working within our Nuxt + Amplify application. Our users could login, register, confirm their email, and logout. Today we'll be expanding our application by adding a private and public access GraphQL API using the Amplify CLI.
The Amplify documentation covers this topic pretty well, so be sure to at least glance over the documentation incase you should run into any issues.
Adding & Defining Our API
So, just like we did with authentication, we need to use the Amplify CLI to add an api to our project. To get started let's run $ amplify add api
in our terminal. Once we do, the Amplify CLI will walk us through the following configuration process and allow us to define our GraphQL schema.
Please select from one of the below mentioned services: GraphQL
Provide API name: amplifynuxt
Choose the default authorization type for the API: Amazon Cognito User Pool
Using User Pools will give us the ability to use the@auth
directive to define access controls on our endpoints for authenticated users.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
Cognito User Pool can't define access controls for unauthenticated users, so in order to allow public read access to our API, we need this second authorization type of API key.Enter a description for the API key: public access
After how many days from now the API key should expire (1-365): 365
After 365 days we'll need to regenerate a new API key for our application to maintain read access.Configure conflict decection?: No
Do you have an annotated GraphQL schema?: No
Do you want a guided schema creation?: Yes
What best describes your project?: One-to-many relationship
Do you want to edit the schema now?: Yes
Example Amplify Schema
Amplify will now pop the example GraphQL schema open in your default text editor. If the file opened is blank or Amplify doesn't open the file for you it can be found at:
amplify/backend/api/[api name]/schema.graphql
Once you have the example GraphQL schema open you should see something like this:
type Blog @model {
id: ID!
name: String!
posts: [Post] @connection(name: "BlogPosts")
}
type Post @model {
id: ID!
title: String!
blog: Blog @connection(name: "BlogPosts")
comments: [Comment] @connection(name: "PostComments")
}
type Comment @model {
id: ID!
content: String
post: Post @connection(name: "PostComments")
}
As you can see we have blog, post, and comment types. Each of these contain a model directive (@model
). This model directive states that each one of these types will a table within our database.
Inside each type we then have a list of fields along with their types. So, for example, the first field in our Blog model is an id of type ID. The exclamation point trailing the type denotes the field as required and not nullable.
We also have relationships defined via the connection directive (@connection
). Each connection directive contains a name argument. It's important to note here that each end of the relationship receives the same name on it's connection directive. So posts on type Blog has the same connection name as blog on type Post.
Defining Our Schema
Okay, so now that we've briefly covered what's going on with the example schema let's begin defining our own. Let's start by emptying this file out.
Our schema will contain two different models, User and Post. Instead of focusing on scale, we'll mostly be focusing on access controls. Before we begin with access controls, though, we'll begin by getting the models and fields defined.
Let's start by defining our User model.
type User @model {
id: ID!
email: String!
createdAt: String!
name: String
biography: String
website: String
posts: [Post] @connection(name: "UserPosts")
}
So far nothing here should look too different from the Amplify example. One oddity that might've entered your head is the String
type on our createdAt
field. GraphQL doesn't come with a native type for dates or datetimes, so we'll be using a string to hold a timestamp value.
Let's move forward with our Post model.
type Post @model {
id: ID!
title: String!
summary: String!
body: String!
createdAt: String!
authorId: String!
author: User! @connection(name: "UserPosts")
}
Now, since we're storing our user's id as authorId
and not userId
, we need a way to tell Amplify how to pull the user data for our author field. To do this we'll extend our connection directive with a second argument of keyField
. Amplify will then pull the user data for our author field using the id stored in the field we define as the keyField
value.
So, in our case, we'll want to define keyField
as authorId
.
author: User! @connection(name: "UserPosts", keyField: "authorId")
Now, it's important to note that the keyField
needs to be defined on both ends of the relationship. So, we also need to do the same for our connection directive on our User model.
posts: [Post] @connection(name: "UserPosts")
Defining Our Access Controls
Now that we have the base schema defined, we can move into defining our access controls. To do this we'll be using the auth directive (@auth
). The auth directive accepts an array of rules, each rule being an object. So, to start out, let's define the auth directive on our User model.
type User
@model
@auth(
rules: []
) {
id: ID!
email: String!
createdAt: String!
name: String
biography: String
website: String
posts: [Post] @connection(name: "UserPosts", keyField: "authorId")
}
Owner Access
Next let's start by adding a rule to allow owners to update and delete records that they own. We can also extend this rule to allow creating. This will result in allowing authenticated users as a whole to create since you can't own a record before it's created.
@auth(
rules: [
{ allow: owner, operations: [create, update, delete] }
]
)
Now, by default, Amplify will add an owner field to our model if we don't explicitly define an owner field or a way to track owners. However, for our User model, we'll be using the same id value as the currently authenticated user at the time the record is created. This is how we'll bind our Cognito user to our database user, they'll share ids. Thanks to this, our owner can be defined as the record's id value.
To do this, we can use the ownerField
property to define which field in our table to pull the owner's id off of, which is id in our case. So, let's add this to our rule.
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
]
)
Admin Access
Next up, let's give admins the ability to do whatever they want. We'll be doing this using groups, which you can create and assign groups to users within AppSync.
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
{ allow: groups, groups: ["admin"], operations: [create, update, delete] }
]
)
Now, any user we assign the group "admin" to within AppSync will be able to create, update, and delete any user record within our User model.
Authenticated User Read Access
If a user doesn't match either of the two criteria thus far, they currently wouldn't be able to do anything with our API. Therefore, we need to continue by allowing authenticated user's the ability to read any record within our User model.
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
{ allow: groups, groups: ["admin"], operations: [create, update, delete]
{ allow: private, operations: [read] }
]
)
Private here refers to authenticated users. So, we're stating any authenticated user can read from the User model.
Unauthenticated User Read Access
Since we don't want to require our users to login in order to be able to view our content, we'll also want to allow unauthenticated users to have read access.
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update, delete] }
{ allow: groups, groups: ["admin"], operations: [create, update, delete] }
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
)
Similar to how private is our authenticated users, public is our unauthenticated users.
Authorization Types
Whenever it comes time to specify which authorization type our API call should use our owner, groups, and private rules all deal with authenticated users. So, these are all okay to use the Cognito User Pool authorization type.
However, since Cognito User Pool only handles authenticated user access controls, our public access will need to use the secondary API key authorization type we set up.
Applying Rules To Post
That's it for our User model. We're now ready to move onto our Post model access controls and, for the most part, this will be the same.
type Post
@model
@auth(
rules: [
{ allow: owner, ownerField: "authorId", operations: [create, update, delete] }
{ allow: groups, groups: ["admin"], operations: [create, update, delete] }
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
) {
id: ID!
title: String!
summary: String!
body: String!
createdAt: String!
authorId: String!
author: User! @connection(name: "UserPosts")
}
The only difference with our Post model authorization rules is our ownerField
. Since the post id isn't going to be our owner's id, it'll be a uniquely generated id, we can't use that as the ownerField
. However, we are explicitly defining our authorId
which, in this case, will be the same as our owner's user id. Therefore, we can use our authorId
as the ownerField
.
Schema Review
Awesome, we're now done defining our schema and our API access controls. Let's take a quick moment here to confirm what I have and what you have match so we're on the same page.
type User
@model
@auth(
rules: []
) {
id: ID!
email: String!
createdAt: String!
name: String
biography: String
website: String
posts: [Post] @connection(name: "UserPosts", keyField: "authorId")
}
type Post
@model
@auth(
rules: [
{ allow: owner, ownerField: "authorId", operations: [create, update, delete] }
{ allow: groups, groups: ["admin"], operations: [create, update, delete] }
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
) {
id: ID!
title: String!
summary: String!
body: String!
createdAt: String!
authorId: String!
author: User! @connection(name: "UserPosts")
}
If it's a match, give the file a save and return back to your terminal.
Finishing Our API Setup
Now that we're done defining our schema we can return back to our terminal and hit "enter." Amplify will then validate our schema, and if all is okay we should be finished with the add API flow.
Next, let's run $ amplify push
to push our API configuration and schema up. Amplify will then use our configuration and schema to create the necessary resources and generate our API.
When you run amplify push, it should show you a preview of what changed, showing that we've created an API. Go ahead and hit "enter" to continue.
It will then ask us a couple of questions about generating some pre-defined queries, mutations, and subscriptions.
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: 2
Amplify will then spend a few minutes creating all the necessary resources for our API. You can take a quick look at the newly generated queries, mutations, and subscriptions that are within your project at /src/graphql
. We'll use these in the next lesson to communicate with our backend. It's also worth noting that we don't need to use these queries, mutations, or subscriptions. We could create our own if we wish and use them the exact same way we'll be using these in the next lesson.
Wrapping Up
So we've now successfully created our GraphQL API with both public and private access where only owners and admins will be able to update and delete records. In the next lesson, we'll put our API to work and create some Vuex modules to handle the communication.
Join The Discussion! (1 Comments)
Please sign in or sign up for free to join in on the dicussion.
vasyl-horban
I don`t really understand how this User model conects to Cognito user pool. In my current project I have to show specific data ( models ) to specific user ( cognito user ). But user don`t create this data and can`t update or delete. How to achive this?
Please sign in or sign up for free to reply