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)

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 ( ) { clientID = os . Getenv ( "CLIENT_ID" ) r := mux . NewRouter ( ) r . HandleFunc ( "/api/signin" , signInHandler ) . Methods ( "POST" ) http . Handle ( "/" , cors . AllowAll ( ) . Handler ( r ) ) }

Imports are skipped for brevity. Use goimports tool to add them. Code editors, such as Visual Studio Code have plugins for it.

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 ) 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 } 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 } 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 ) sessionToken := r . Header . Get ( "Authorization" ) if len ( sessionToken ) == 0 { responseError ( w , "Invalid session token" , http . StatusUnauthorized ) return } sessionItem , err := memcache . Get ( ctx , "session:" + sessionToken ) if err != nil { log . Errorf ( ctx , "%v" , err ) responseError ( w , "Could not authenticate" , http . StatusUnauthorized ) return } 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 ( ) { clientID = os . Getenv ( "CLIENT_ID" ) r := mux . NewRouter ( ) r . HandleFunc ( "/api/signin" , signInHandler ) . Methods ( "POST" ) r . HandleFunc ( "/api/hello" , authenticate ( helloHandler ) ) . Methods ( "GET" ) 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 ) { 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 ( ) 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 := datastore . NewQuery ( "Todo" ) . Filter ( "UserID =" , userID ) . Order ( "-CreatedAt" ) if keys , err := query . GetAll ( ctx , & todos ) ; err != nil { log . Errorf ( ctx , "%v" , err ) responseError ( w , "Could not read todos" , http . StatusInternalServerError ) } else { if len ( todos ) == 0 { responseJSON ( w , [ ] Todo { } ) return } 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]'

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 ) { id := mux . Vars ( r ) [ "id" ] todoID , err := strconv . ParseInt ( id , 10 , 64 ) if err != nil { responseError ( w , "Invalid todo ID" , http . StatusBadRequest ) return } 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 } 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 } 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 { key := datastore . NewKey ( ctx , "Todo" , "" , id , nil ) if err := datastore . Get ( ctx , key , todo ) ; err != nil { return err } 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 ) { id := mux . Vars ( r ) [ "id" ] todoID , err := strconv . ParseInt ( id , 10 , 64 ) if err != nil { responseError ( w , "Invalid todo ID" , http . StatusBadRequest ) return } 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 } 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.