Build a Todo List with Angular and Google App Engine - Part 1
August 30, 2017
The world needs more todo lists. Let us deliver another one.
This project was developed on Ubuntu 17.04 using the following technologies:
- Angular 4.3.6
- Angular CLI 1.3.2 (Installation instructions)
- Google Cloud SDK 169.0.0 (Installation instructions, also install
google-cloud-sdk-app-engine-go
component) - Angular Material 2 (2.0.0-beta.8)
A quick preview of finished product.
In the first part you'll create a back-end service using Google App Engine, and in the second part a front-end app using Angular.
Getting started
To authenticate users with Google, you need to create a Google API Console project and obtain your client ID. Under Authorized JavaScript origins enter all URIs you'll be using. That includes http://localhost:4200
for Angular development server and https://[PROJECT_ID].appspot.com
for hosting on Google App Engine.
Create a project directory for your back-end service containing app.yaml
file.
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app
env_variables:
CLIENT_ID: '[CLIENT_ID]'
Signing in users
To begin, you'll create an endpoint for signing in Google users. ID tokens will be validated using Google's tokeninfo
endpoint and subsequent requests will carry custom session token. This approach is only sufficient for development. For production, you'll want to use a JWT library and Google's public keys. See Authenticate with a backend server.
Install necessary Go packages.
go get github.com/rs/cors github.com/gorilla/mux
Create app.go
file.
package todo
var (
clientID string
)
func init() {
// Read configuration environment variables
clientID = os.Getenv("CLIENT_ID")
// Register routes
r := mux.NewRouter()
r.HandleFunc("/api/signin", signInHandler).
Methods("POST")
// Start HTTP server
http.Handle("/", cors.AllowAll().Handler(r))
}
Sign-in handler
Write a couple of utility functions inside utility.go
file for future use.
package todo
func responseError(w http.ResponseWriter, message string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": message})
}
func responseJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
func readJSON(rc io.ReadCloser, v interface{}) error {
defer rc.Close()
data, err := ioutil.ReadAll(rc)
if err != nil {
return err
}
err = json.Unmarshal(data, v)
if err != nil {
return err
}
return nil
}
Declare signInHandler
handler function inside signin_handler.go
file.
package todo
type SignInResponse struct {
UserID string `json:"userId"`
SessionToken string `json:"sessionToken"`
}
func signInHandler(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
// Verify ID token provided in header
token := r.Header.Get("Authorization")
userID, err := verifyToken(ctx, token)
if err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Invalid ID token", http.StatusBadRequest)
return
}
// Generate a new session token and store it in Memcache
sessionToken := generateSessionToken()
if err := memcache.Set(ctx, &memcache.Item{
Key: "session:" + sessionToken,
Value: []byte(userID),
Expiration: 10 * time.Hour,
}); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not start user session", http.StatusInternalServerError)
return
}
// Return session data
responseJSON(w, SignInResponse{userID, sessionToken})
}
The code above verifies ID token by calling verifyToken
function, which returns user's ID. If validation is successful, a new session token is generated and cached in Memcache for 1 hour.
Declare the verifyToken
function inside signin_handler.go
.
func verifyToken(ctx context.Context, token string) (string, error) {
client := urlfetch.Client(ctx)
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + token)
if err != nil {
return "", err
}
var bodyJSON map[string]interface{}
if err := readJSON(resp.Body, &bodyJSON); err != nil {
return "", err
}
if aud, ok := bodyJSON["aud"].(string); ok {
if clientID != aud {
return "", errors.New("Invalid client ID")
}
} else {
return "", errors.New("Invalid ID token")
}
if sub, ok := bodyJSON["sub"].(string); ok {
return sub, nil
}
return "", errors.New("Invalid ID token")
}
With the current version of Google App Engine you have to import context from golang.org/x/net/context
. Later it will work with just a standard context
.
Also declare the generateSessionToken
function.
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
func generateSessionToken() string {
const n = 64
data := make([]byte, n)
rand.Read(data)
token := make([]rune, n)
for i := range data {
token[i] = letters[int(data[i])%len(letters)]
}
return string(token)
}
This simply creates a string of 64 random characters.
Authentication middleware
To simplify authentication code, declare a new handler type, which extends http.HandlerFunc
. For this, create a new file auth.go
.
package todo
type AuthenticatedHandler func(context.Context, http.ResponseWriter, *http.Request, string)
AuthenticatedHandler
receives the context, user's ID as a string and parameters from the standard handler.
Now create a middleware between authenticated handler functions and the old http.HandlerFunc
.
func authenticate(handler AuthenticatedHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
// Get session token from header
sessionToken := r.Header.Get("Authorization")
if len(sessionToken) == 0 {
responseError(w, "Invalid session token", http.StatusUnauthorized)
return
}
// Fetch user's ID from Memcache
sessionItem, err := memcache.Get(ctx, "session:"+sessionToken)
if err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not authenticate", http.StatusUnauthorized)
return
}
// Call handler function
userID := string(sessionItem.Value)
handler(ctx, w, r, userID)
}
}
The code above checks Memcache for existing session and fetches the ID of user making the request. It then forwards data to the supplied handler function.
To see how it works, declare a dummy handler and update the init
function inside app.go
file.
func init() {
// Read configuration environment variables
clientID = os.Getenv("CLIENT_ID")
// Register routes
r := mux.NewRouter()
r.HandleFunc("/api/signin", signInHandler).
Methods("POST")
r.HandleFunc("/api/hello", authenticate(helloHandler)).
Methods("GET")
// Start HTTP server
http.Handle("/", cors.AllowAll().Handler(r))
}
func helloHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, userID string) {
responseJSON(w, "Hello, "+userID)
}
Test it using cURL. You can obtain the ID token using OAuth 2.0 Playground.
curl localhost:8080/api/signin -X POST -H 'Authorization:[ID_TOKEN]'
curl localhost:8080/api/hello -H 'Authorization:[SESSION_TOKEN]'
You should be getting your own Google ID in the response body.
Todo handlers
Users can create, read, update and delete their own todos. Each use case will be implemented in its own handler function.
Declare Todo
struct inside todo_handlers.go
file.
package todo
type Todo struct {
ID string `json:"id" datastore:"-"`
UserID string `json:"userId"`
Title string `json:"title"`
CreatedAt time.Time `json:"createdAt"`
}
The ID
field has datastore
tag set to "-"
, which tells Datastore to ignore this field when inserting. Each Datastore entity already has datastore.Key
associated with it, but auto generated ID will be of type int64
. Declared ID
field will be used for encoding in JSON, where integers can't be correctly expressed with double-precision floating-point numbers.
Create
Write createTodoHandler
handler function.
func createTodoHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, userID string) {
// Read todo from request body
var todo Todo
if err := readJSON(r.Body, &todo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not read todo", http.StatusBadRequest)
return
}
todo.UserID = userID
todo.CreatedAt = time.Now()
// Store todo
key := datastore.NewIncompleteKey(ctx, "Todo", nil)
if key, err := datastore.Put(ctx, key, &todo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not create todo", http.StatusInternalServerError)
} else {
todo.ID = strconv.FormatInt(key.IntID(), 10)
responseJSON(w, todo)
}
}
Update the init
function inside app.go
file to register createTodoHandler
handler.
r.HandleFunc("/api/todos", authenticate(createTodoHandler)).
Methods("POST")
See if it works.
curl localhost:8080/api/todos \
-H 'Authorization:[SESSION_TOKEN]' \
-d '{"title":"write more code"}'
Read
Declare listTodosHandler
handler function which reads todos made by the current user and orders them by creation time.
func listTodosHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, userID string) {
var todos []Todo
// Query todos by user's ID and order them by creation time
query := datastore.NewQuery("Todo").
Filter("UserID =", userID).
Order("-CreatedAt")
// Execute query
if keys, err := query.GetAll(ctx, &todos); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not read todos", http.StatusInternalServerError)
} else {
// Return empty array instead of 'null'
if len(todos) == 0 {
responseJSON(w, []Todo{})
return
}
// Set string IDs
for i := range todos {
todos[i].ID = strconv.FormatInt(keys[i].IntID(), 10)
}
responseJSON(w, todos)
}
}
Ordering by CreatedAt
field requires setting up an index. A new file is created automatically by development Google App Engine server, if not, you can do it manually by creating a index.yaml
file inside project directory with the following content.
indexes:
- kind: Todo
properties:
- name: UserID
- name: CreatedAt
direction: desc
Update the init
function.
r.HandleFunc("/api/todos", authenticate(listTodosHandler)).
Methods("GET")
Try it out.
curl localhost:8080/api/todos \
-H 'Authorization:[SESSION_TOKEN]'
Update
Updating is a bit more complex. You need to check if requested todo exists and belongs to the current user before updating its title. Todo's ID is passed in as a path variable and new title inside request body.
func updateTodoHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, userID string) {
// Parse ID
id := mux.Vars(r)["id"]
todoID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
responseError(w, "Invalid todo ID", http.StatusBadRequest)
return
}
// Get old todo
var todo Todo
if err := getOwningTodo(ctx, userID, todoID, &todo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not read old todo", http.StatusBadRequest)
return
}
// Read new todo from request body
var newTodo Todo
if err := readJSON(r.Body, &newTodo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not read request body", http.StatusBadRequest)
return
}
// Update todo
todo.Title = newTodo.Title
key := datastore.NewKey(ctx, "Todo", "", todoID, nil)
if _, err := datastore.Put(ctx, key, &todo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not update todo", http.StatusInternalServerError)
return
}
todo.ID = id
responseJSON(w, todo)
}
Also declare a utility function which reads a todo by ID and checks if it belongs to the current user.
func getOwningTodo(ctx context.Context, userID string, id int64, todo *Todo) error {
// Fetch todo
key := datastore.NewKey(ctx, "Todo", "", id, nil)
if err := datastore.Get(ctx, key, todo); err != nil {
return err
}
// Check if it belongs to the current user
if todo.UserID != userID {
return errors.New("Not own todo")
}
return nil
}
Update the init
function.
r.HandleFunc("/api/todos/{id}", authenticate(updateTodoHandler)).
Methods("POST")
Make sure it works.
curl localhost:8080/api/todos/[TODO_ID] -H 'Authorization:[SESSION_TOKEN]' \
-d '{"title":"new title"}'
Delete
Similarly as with updating, deleting requires checking validity before performing the deletion.
func deleteTodoHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, userID string) {
// Parse ID
id := mux.Vars(r)["id"]
todoID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
responseError(w, "Invalid todo ID", http.StatusBadRequest)
return
}
// Get todo to check if it can be deleted
var todo Todo
if err := getOwningTodo(ctx, userID, todoID, &todo); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not read todo", http.StatusInternalServerError)
return
}
// Delete todo
key := datastore.NewKey(ctx, "Todo", "", todoID, nil)
if err := datastore.Delete(ctx, key); err != nil {
log.Errorf(ctx, "%v", err)
responseError(w, "Could not delete todo", http.StatusInternalServerError)
return
}
todo.ID = id
responseJSON(w, todo)
}
Register it in the init
function.
r.HandleFunc("/api/todos/{id}", authenticate(deleteTodoHandler)).
Methods("DELETE")
Try deleting an existing todo.
curl localhost:8080/api/todos/[TODO_ID] -X DELETE \
-H 'Authorization:[SESSION_TOKEN]'
Wrapping up
That's it for the back-end. In the next part you'll create the front-end and deploy it to the Google App Engine.
Second part: Build a Todo List with Angular and Google App Engine - Part 2.
Source code for this tutorial is available on GitHub.