TodoList App – Part 2

Reading Time: 4 minutes
 

In the previous article I showed you how to create an empty Go Rest API. In this article I’ll build upon that and add a crude login and registration capability. I’ll also refine the login as we move along in this tutorial series and add more resilience and options for login. The code is available at this repository. Use tag login_and_register_v1 to follow along with this post.

Since we’re going to use a persistence storage, a mongoDB instance, please make sure you’ve created one either locally or a cloud hosted instance. For this application I’ll be using https://www.mongodb.com cloud hosted free tier instance. Do checkout this post on how to configure mongoDB instance so you can use it from your application.

The Model

I’ll first explain what we’re using as our user model. The structure is very simple, but the only thing that’s tricky is the Meta information. Since we don’t want to introduce a new field every time we decide to add some specific information to user, I added a generic Meta field. We can add any key-value pairs here with key being a string and the value any type.

For now we’re not storing things like User’s name, phone etc. We’ll use all these things when we integrate this with our Android app

type User struct {
	//This is a unique field.
	ID         string                 `json:"id"`
	Meta       map[string]interface{} `json:"extra",omitempty  bson:"extra",omitempty`
	SignInType LoginType              `json:"-" bson:"type"`
	Password   string                 `json:"pass"`
} 

Using Mongo DB

If you have no idea how to use MongoDB no worries. Though I’m not going to teach you MongoDB but you can access an extremely helpful collection of examples here. There are numerous examples of integrating with different languages.

MongoDB stores data as collections. Each collection is stored as a JSON object albeit in a binary format. This binary format is bson and stands for Binary JSON. MongoDB Go driver provides some datatypes to create a query. These are array(bson.A), map (bson.M) and document (bson.D).

  • We’ll use bson.M for when things are simple. For example finding out if a user exists with a specified id and password. Places where we can map a key to a value directly is where bson.M can be used.
  • Sometimes we need to group a collection of conditions for those cases we’ll use bson.D and bson.A. Almost always these two would be used together to create complex queries. For example removing all stale users, those who’ve not verified their registration in the stipulated time.

NOTE: All fields are stored in lower case by default in the mongoDB collection unless specified otherwise with the bson tag in the structure. Since the mongo driver uses reflection to figure out the fields all fields that need to be store must be exported (UpperCase name).

For our server side code I’ll be mostly using the bson.M and wherever things need to be complex I’ll use bson.D

Errors when storing data in mongoDb

If you’re using the MongoDB Atlas cloud hosted instance make sure you’ve

  • Whitelisted access from Any IP address.  If your application is hosted on a fixed IP address you can provide that here.
  • The database user has the correct permissions, basically need allReadWriteeither to a specific or all databases. Need not be a atlas user if you’re using MongoDB Atlas cloud hosted instance.
  • Use a specific replica instead of the one which offers high availability. You can skip this if your application is able to lookup from the HA URL. If you keep getting things like lookup failure then please see Connecting to MongoDB Atlas Cluster on how to fix this.

Registration / Login code

The code is pretty straight forward. There’s no fancy stuff going on at this stage but you can see that it needs a lot more. For example,

  1. Using https instead of http scheme is much desired since we’re sending password in plain.
  2. Use of a specific Content-Type is desirable.
  3. We need to avoid access to database as much as possible post login. We’ll add jwt tokens later on to this.

The functions listed on the right use the connection to the MongoDB Server to check for existence , insert or update a User record.

As you can see I’ve used bson.M to create a query which finds out the exact User in the database which needs to be updated or fetched.

To check what fields and methods are available with collection please checkout the documentation.

//See if we can get a login Id and password
//to match anything in the database.
func GetUser(dbClient *mongo.Client, id, password string) *User {
	u := &User{}
	//Create a mongo query to find a user with
	//matching id and password.
	//For complex queries we'll use bson.D but since
	//this is simple we use bson.M (map)
	context := utils.GetContext()
	query := bson.M{
		"id":       id,
		"password": password,
	}
	collection := database.GetUserCollection(dbClient)
	err := collection.FindOne(context, query).Decode(u)
	if err == nil {
		return u
	}
	return nil
}

func AddUser(dbClient *mongo.Client, user *User) bool {
	context := utils.GetContext()
	if GetUser(dbClient, user.ID, user.Password) != nil {
		return false
	}
	collection := database.GetUserCollection(dbClient)
	res, err := collection.InsertOne(context, *user)
	if err != nil {
		log.Printf("Error adding user %v: %s", *user, err)
		return false
	}
	log.Printf("Added user %v to users collection with object id = %v\n", *user, res.InsertedID)
	return true
}

func (u *User) Update(dbClient *mongo.Client) {
	context := utils.GetContext()
	query := bson.M{
		"id":       u.ID,
		"password": u.Password,
	}
	collection := database.GetUserCollection(dbClient)
	result, err := collection.ReplaceOne(context, query, *u)
	if err == nil {
		log.Printf("Updated %d user(s)\n", result.MatchedCount)
	}
}
 

Where’s the connection URI?

If you see carefully there’s no database connection URI stored anywhere in the code. This is where the environment variables comes in handy. 

If you see how we get the connection URI, it’s from an environment variable. Most cloud based hosting solutions provide this . Since I’m using Heroku for deployment, it allows to create application specific config variables. These config variables are available to the application as environment variables.

We already used one such variable before , PORT which is where our server needs to listen for incoming requests.

func Register(w http.ResponseWriter, r *http.Request) {
	bytes, err := ioutil.ReadAll(r.Body)
	if err != nil {
		GenericInternalServerHeader(&w, r)
		return
	}
	user := &model.User{}
	err = json.Unmarshal(bytes, user)
	if err != nil {
		GenericWriteHeader(&w, r, http.StatusBadRequest)
		return
	}
	user.SignInType = model.WebLogin
	connection, err := database.GetMongoConnection(os.Getenv(utils.MongoDBConnectionString))
	if err != nil {
		GenericInternalServerHeader(&w, r)
		return
	}
	defer database.ReleaseMongoConnection(connection)
	if model.AddUser(connection, user) {
		GenericWriteHeader(&w, r, http.StatusOK)
		return
	}
	GenericInternalServerHeader(&w, r)
} 

Testing the changes

To test the changes you can use a curl along with a text file containing the json data. For example you can use something like

curl -d "$(cat user_login.data)" -vvv <your_app_url> 

The text file containing the data can then be created as,

{
    "id" :"myid",
    "pass": "mypass",
    "meta" : {
        "key1" : "value1"
    }
} 

Leave a Reply