Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Developing a User Profile Store With Golang and a NoSQL Database

DZone's Guide to

Developing a User Profile Store With Golang and a NoSQL Database

User data models change frequently, so using a NoSQL database with a flexible storage model is often more effective than an RDBMS alternative.

· Database Zone ·
Free Resource

Running out of memory? Learn how Redis Enterprise enables large dataset analysis with the highest throughput and lowest latency while reducing costs over 75%! 

Remember the tutorial series I wrote in regards to creating a user profile store with Node.js and NoSQL? That tutorial covered a lot of ground, from creating a RESTful API with Node.js, handling user sessions to data modeling and storing data associated with users.

What if we wanted to take the same concepts and apply them with Golang instead of JavaScript with Node.js?

We're going to see how to develop a user profile store with Golang and Couchbase Server that acts as a modular replacement to the Node.js alternative.

Going forward, we're going to assume that you have Go installed and configured, as well as Couchbase Server. It is not important if you've seen the Node.js version of this tutorial because we're going to revisit everything.

In case you're unfamiliar with what a user profile store is or what it does, it is simply a solution for storing information about users and information associated with users. Take, for example, a blog. A blog might have numerous authors who are technically users. Each author will write content and that content will be associated to the particular user that wrote it. Each author will have their own method of signing into the blog as well.

Because user data models can change so frequently, using a NoSQL database with a flexible storage model is often more effective than an RDBMS alternative. More information on the data modeling aspect can be found in this article User Profile Store: Advanced Data Modeling, written by Kirk Kirkconnell.

Creating a New Golang Project

We're going to spend all of our time in a single Go file. Somewhere in your $GOPATH, create a file called main.go.

We're also going to need a few dependencies, for Couchbase as well as other packages. From the command line, execute the following:

go get github.com/couchbase/gocb
go get github.com/gorilla/context
go get github.com/gorilla/handlers
go get github.com/gorilla/mux
go get github.com/satori/go.uuid

The above dependencies will allow us to communicate with Couchbase Server, generate UUID values, and create a RESTful API with cross origin resource sharing (CORS) handling.

The next step is to throw down some boilerplate code for our project. Open the project's  main.go file and include the following:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"

	"github.com/couchbase/gocb"
	"github.com/gorilla/context"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	uuid "github.com/satori/go.uuid"
)

type Account struct {
	Type     string `json:"type,omitempty"`
	Pid      string `json:"pid,omitempty"`
	Email    string `json:"email,omitempty"`
	Password string `json:"password,omitempty"`
}

type Profile struct {
	Type      string `json:"type,omitempty"`
	Firstname string `json:"firstname,omitempty"`
	Lastname  string `json:"lastname,omitempty"`
}

type Session struct {
	Type string `json:"type,omitempty"`
	Pid  string `json:"pid,omitempty"`
}

type Blog struct {
	Type      string `json:"type,omitempty"`
	Pid       string `json:"pid,omitempty"`
	Title     string `json:"title,omitempty"`
	Content   string `json:"content,omitempty"`
	Timestamp int    `json:"timestamp,omitempty"`
}

var bucket *gocb.Bucket

func Validate(next http.HandlerFunc) http.HandlerFunc {}

func RegisterEndpoint(w http.ResponseWriter, req *http.Request) {}
func LoginEndpoint(w http.ResponseWriter, req *http.Request) {}
func AccountEndpoint(w http.ResponseWriter, req *http.Request) {}
func BlogsEndpoint(w http.ResponseWriter, req *http.Request) {}
func BlogEndpoint(w http.ResponseWriter, req *http.Request) {}

func main() {
	fmt.Println("Starting the Go server...")
	router := mux.NewRouter()
	cluster, _ := gocb.Connect("couchbase://localhost")
	bucket, _ = cluster.OpenBucket("default", "")
	router.HandleFunc("/account", RegisterEndpoint).Methods("POST")
	router.HandleFunc("/login", LoginEndpoint).Methods("POST")
	router.HandleFunc("/account", Validate(AccountEndpoint)).Methods("GET")
	router.HandleFunc("/blogs", Validate(BlogsEndpoint)).Methods("GET")
	router.HandleFunc("/blog", Validate(BlogEndpoint)).Methods("POST")
	log.Fatal(http.ListenAndServe(":3000", handlers.CORS(handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{"*"}))(router)))
}

In the above, you'll notice that we've created a few data structures to represent our data. We're going with the idea of a blogging platform.

The application will have five RESTful API endpoints, a validator method for our user sessions, and a global Couchbase variable that will allow us to access our open instance anywhere in the application.

router.HandleFunc("/account", Validate(AccountEndpoint)).Methods("GET")
router.HandleFunc("/blogs", Validate(BlogsEndpoint)).Methods("GET")
router.HandleFunc("/blog", Validate(BlogEndpoint)).Methods("POST")

Notice that the above three endpoints have the Validate function attached to them. This means that the user must have authenticated and be providing a valid session to progress. In this sense, the Validate function acts as a middleware.

Because we plan to query for data — more specifically, blog articles to a particular user — we need to have an index created. Using the web dashboard, Couchbase CLI, or a Go application, execute the following:

CREATE INDEX `blogbyuser` ON `default`(type, pid);

The above index will allow us to query by a type property as well as a pid property.

Now we can start filling in the holes for each of our API endpoints.

Allowing Users to Register New Information With the Profile Store

Since we have no users in the profile store as of right now, it would make sense to create an endpoint that supports the creation of new users.

It is good practice to never store username and password type credential information with actual user information. For this reason, creating a new user means creating a profile document as well as an account document. The account document will reference the profile document. Both will be modeled after our Go data structures that we saw in the boilerplate code.

In the main.go file, add the following:

func RegisterEndpoint(w http.ResponseWriter, req *http.Request) {
	var data map[string]interface{}
	_ = json.NewDecoder(req.Body).Decode(&data)
	id := uuid.NewV4().String()
	passwordHash, _ := bcrypt.GenerateFromPassword([]byte(data["password"].(string)), 10)
	account := Account{
		Type:     "account",
		Pid:      id,
		Email:    data["email"].(string),
		Password: string(passwordHash),
	}
	profile := Profile{
		Type:      "profile",
		Firstname: data["firstname"].(string),
		Lastname:  data["lastname"].(string),
	}
	_, err := bucket.Insert(id, profile, 0)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	_, err = bucket.Insert(data["email"].(string), account, 0)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	json.NewEncoder(w).Encode(account)
}

There are a few important things happening in the above endpoint code.

First, we are accepting JSON data that was sent with the POST body from the client request. We are generating a unique ID for the profile document and hashing the password for safe keeping with BCrypt.

When it comes to actually saving the data, the user profile will receive a unique ID while the account will receive an email address as the ID and a reference to the profile ID within the document.

By following this approach, the account document could easily be extended to other forms of credentials. For example, the account document could be rebranded as basicauth, and we could have Facebook, Twitter, etc., that reference the profile information.

Implementing a Session Token for Users

Signing into the application via our two documents is a little different. It is never a good idea to pass around the username and password more than is absolutely necessary.

For this reason, it is a good idea to use a session token that represents the user. This token can expire and holds no sensitive information.

Take the following sign-in code for the main.go file:

func LoginEndpoint(w http.ResponseWriter, req *http.Request) {
	var data Account
	var account Account
	_ = json.NewDecoder(req.Body).Decode(&data)
	_, err := bucket.Get(data.Email, &account)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(data.Password))
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	session := Session{
		Type: "session",
		Pid:  account.Pid,
	}
	var result map[string]interface{}
	result = make(map[string]interface{})
	result["sid"] = uuid.NewV4().String()
	_, err = bucket.Insert(result["sid"].(string), &session, 3600)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	json.NewEncoder(w).Encode(result)
}

When the email and password is passed to this endpoint, the account document is retrieved based on the email that was provided. The hashed password inside this document is then compared against the unhashed password.

If the credentials are valid, a session document is created. This session document has a unique key but references the key of the profile document. An expiration time is also added to the document. When the expiration time passes, the document will be automatically removed from Couchbase without any application or user intervention. This helps secure the account.

With the accounts functional, we need to worry about associating information with the users.

Managing User Information in the Profile Store via a Session Token

When a user tries to do something specific to his or herself, we need to validate that they are who they should be and that the information they are changing is applied to the correct person.

This is where the session token validation middleware comes into play.

func Validate(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		authorizationHeader := req.Header.Get("authorization")
		if authorizationHeader != "" {
			bearerToken := strings.Split(authorizationHeader, " ")
			if len(bearerToken) == 2 {
				var session Session
				_, err := bucket.Get(bearerToken[1], &session)
				if err != nil {
					w.WriteHeader(401)
					w.Write([]byte(err.Error()))
					return
				}
				context.Set(req, "pid", session.Pid)
				bucket.Touch(bearerToken[1], 0, 3600)
				next(w, req)
			}
		} else {
			w.WriteHeader(401)
			w.Write([]byte("An authorization header is required"))
			return
		}
	})
}

Every request to one of our three special endpoints will require an authorization header that contains a bearer token with the session ID. No bearer token means the request will fail. Incorrect or expired bearer tokens mean the request will fail.

Validation will exchange the session ID for a profile ID to be used in the next step of the request.

Starting simply, let's say we want to return the profile information for a particular user. Our endpoint might look like the following:

func AccountEndpoint(w http.ResponseWriter, req *http.Request) {
	pid := context.Get(req, "pid").(string)
	var profile Profile
	_, err := bucket.Get(pid, &profile)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	json.NewEncoder(w).Encode(profile)
}

The pid is passed from the validation middleware and a lookup is done with the profile ID.

Not so difficult so far, right?

Let's kick it up a notch and introduce some N1QL queries into our project. Let's say we want to get all blog posts for a particular user. This will make use of our index as well as the SQL-like queries.

func BlogsEndpoint(w http.ResponseWriter, req *http.Request) {
	var n1qlParams []interface{}
	n1qlParams = append(n1qlParams, context.Get(req, "pid").(string))
	query := gocb.NewN1qlQuery("SELECT `" + bucket.Name() + "`.* FROM `" + bucket.Name() + "` WHERE type = 'blog' AND pid = $1")
	query.Consistency(gocb.RequestPlus)
	rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	var row Blog
	var result []Blog
	for rows.Next(&row) {
		result = append(result, row)
		row = Blog{}
	}
	rows.Close()
	if result == nil {
		result = make([]Blog, 0)
	}
	json.NewEncoder(w).Encode(result)
}

In the above endpoint code, we take the pid from the validation middleware and add it as a parameter for our parameterized query.

We'll iterate through the QueryResults returned from the query and add them to a []Blog variable. If no results were found, we can just return an empty slice.

Doing direct lookups based on key will always be faster than N1QL queries, but N1QL queries are very useful when you need to query by property information.

The final endpoint isn't any different than we've already seen:

func BlogEndpoint(w http.ResponseWriter, req *http.Request) {
	var blog Blog
	_ = json.NewDecoder(req.Body).Decode(&blog)
	blog.Type = "blog"
	blog.Pid = context.Get(req, "pid").(string)
	blog.Timestamp = int(time.Now().Unix())
	_, err := bucket.Insert(uuid.NewV4().String(), blog, 0)
	if err != nil {
		w.WriteHeader(401)
		w.Write([]byte(err.Error()))
		return
	}
	json.NewEncoder(w).Encode(blog)
}

In the above code, we accept a POST body from the client as well as the pid from the validation middleware. That information is then saved into Couchbase.

Conclusion

You just saw how to create a basic user profile store, or in this case, a blogging platform, using the Go programming language and Couchbase Server. This is an alternative tutorial to a previous tutorial that I had written on the same subject, but with Node.js.

Want to take this tutorial to the next level? Check out how to create a web client front-end with Angular or a mobile client front-end with NativeScript.

For more information on using Couchbase with Golang, check out the Couchbase Developer Portal.

Running out of memory? Never run out of memory with Redis Enterprise databaseStart your free trial today.

Topics:
database ,nosql ,golang ,tutorial ,rdbms

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}