DEV Community

Cover image for Learn Golang by building a fintech banking app - Lesson2: Login and REST API
Duomly
Duomly

Posted on • Edited on • Originally published at blog.duomly.com

Learn Golang by building a fintech banking app - Lesson2: Login and REST API

This post was originally published at:
https://www.blog.duomly.com/golang-course-with-building-a-fintech-banking-app-lesson-2-login-and-rest-api


Intro

In the last episode of the Golang course, we did a project setup and the first database migration.
 
Here is the URL:
https://www.blog.duomly.com/golang-course-with-building-a-fintech-banking-app-lesson-1-start-the-project
 
In the last episode of the "Learn Angular 9 with Tailwind CSS by building a fintech banking app", my friend Anna created the project setup and the first Login UI.
 
You can use that to connect with our backend and be building the complete fintech app.

Here is the URL:
https://www.blog.duomly.com/angular-course-building-a-banking-application-with-tailwind-css-lesson-1-start-the-project/
 
In today's episode, you will learn the second part of how Golang course, and we will focus on user authentication.
 
I will teach you how to build login functionality and how to create the first endpoints of the REST API.
 
And we will do a bit refactoring of the old code to make it cleaner.
 
Let's start!
 
Btw, if you prefer video, here is the youtube version:


 

Refactor interfaces

The first step that we need to do is to refactor the interfaces that we created in the previous episode.
 
To do that, we need to create dir named "interfaces" and create the file called "interfaces.go" inside this directory.

 
 

package interfaces

import "github.com/jinzhu/gorm"

type User struct {
    gorm.Model
  Username string
    Email string
    Password string
}

type Account struct {
    gorm.Model
  Type string
    Name string
    Balance uint
    UserID uint
}

type ResponseAccount struct {
    ID uint
    Name string
    Balance int
}

type ResponseUser struct {
    ID uint
    Username string
    Email string
    Accounts []ResponseAccount
}

 
Next, we need to use the structs from the interfaces.go file instead of these from the migrations.go.
 
We need to change them in places where we relate to the "User" and "Account" structs.
 
Your migrations.go file should look like the example below.

 

package migrations

import (
    "duomly.com/go-bank-backend/helpers"
    "duomly.com/go-bank-backend/interfaces"
    _ "github.com/jinzhu/gorm/dialects/postgres"
)

func connectDB() *gorm.DB {
    db, err := gorm.Open("postgres", "host=127.0.0.1 port=5432 user=postgres dbname=bankapp password=postgres sslmode=disable")
    HandleErr(err)
    return db
}

func createAccounts() {
    db := connectDB()

    users := &[2]interfaces.User{
        {Username: "Martin", Email: "martin@martin.com"},
        {Username: "Michael", Email: "michael@michael.com"},
    }

    for i := 0; i < len(users); i++ {
        // Correct one way
        generatedPassword := helpers.HashAndSalt([]byte(users[i].Username))
        user := &interfaces.User{Username: users[i].Username, Email: users[i].Email, Password: generatedPassword}
        db.Create(&user)

        account := &interfaces.Account{Type: "Daily Account", Name: string(users[i].Username + "'s" + " account"), Balance: uint(10000 * int(i+1)), UserID: user.ID}
        db.Create(&account)
    }
    defer db.Close()
}


func Migrate() {
    User := &interfaces.User{}
    Account := &interfaces.Account{}
    db := connectDB()
    db.AutoMigrate(&User, &Account)
    defer db.Close()

    createAccounts()
}


 

Refactor ConnectDB to helpers

The next step will also be related to the migrations.go file, and to the helpers.go.
 
We need to move the "connectDB" function to the helpers.go.
Next, we need to export it by naming as „ConnectDB".

The last step will be to reuse it in migrations.go but in the imported form.
Your helpers.go should look like the example below:

package helpers

import (
    "github.com/jinzhu/gorm"
    _ "github.com/lib/pq"
    "golang.org/x/crypto/bcrypt"
)


func HandleErr(err error) {
    if err != nil {
        panic(err.Error())
    }
}

func HashAndSalt(pass []byte) string {
    hashed, err := bcrypt.GenerateFromPassword(pass, bcrypt.MinCost)
    HandleErr(err)

    return string(hashed)
}

func ConnectDB() *gorm.DB {
    db, err := gorm.Open("postgres", "host=127.0.0.1 port=5432 user=postgres dbname=bankapp password=postgres sslmode=disable")
    HandleErr(err)
    return db
}

 
Your migrations.go should look like the example below:

 

package migrations

import (
    "duomly.com/go-bank-backend/helpers"
    "duomly.com/go-bank-backend/interfaces"
    _ "github.com/jinzhu/gorm/dialects/postgres"
)

func createAccounts() {
    db := helpers.ConnectDB()

    users := &[2]interfaces.User{
        {Username: "Martin", Email: "martin@martin.com"},
        {Username: "Michael", Email: "michael@michael.com"},
    }

    for i := 0; i < len(users); i++ {
        // Correct one way
        generatedPassword := helpers.HashAndSalt([]byte(users[i].Username))
        user := &interfaces.User{Username: users[i].Username, Email: users[i].Email, Password: generatedPassword}
        db.Create(&user)

        account := &interfaces.Account{Type: "Daily Account", Name: string(users[i].Username + "'s" + " account"), Balance: uint(10000 * int(i+1)), UserID: user.ID}
        db.Create(&account)
    }
    defer db.Close()
}


func Migrate() {
    User := &interfaces.User{}
    Account := &interfaces.Account{}
    db := helpers.ConnectDB()
    db.AutoMigrate(&User, &Account)
    defer db.Close()

    createAccounts()
}

 

Create a login function

Now we can go into the login logic.
 
The first action that we should do is to create a directory named "users" and a file named "users.go".
 
In the "users.go" we should create a package named "users", and the empty function called "Login".
 
Function "login" should take "username" and "pass" as strings.
And we should be able to return a map with "any" type of keys.
 
You should import a few deps as well, but you can copy them from the example below.

 

package users

import (
    "time"

    "duomly.com/go-bank-backend/helpers"
    "duomly.com/go-bank-backend/interfaces"
    "github.com/dgrijalva/jwt-go"
    "golang.org/x/crypto/bcrypt"
)

func Login(username string, pass string) map[string]interface{} {
}

 
Connect DB in the login
The next important thing that we will do is to create the db connection and look for the user with a username from the function params.
 
We need to remember about the "if" statement with the RecordNotFound function that will notify us if the user has the status "not found".

 

db := helpers.ConnectDB()
user := &interfaces.User{}
if db.Where("username = ? ", username).First(&user).RecordNotFound() {
    return map[string]interface{}{"message": "User not found"}
}

 

Verify password

When we have a user, we should verify if the password that we sent is the correct one.
 
Inside the same function named "Login", we need to create simple password verification by using a method "bcrypt.CompareHashAndPassword”.
 
Next, we need to create if statement, where we will check if our password is not mismatched and if there is no error.
 
If yes, we should return the message about "Wrong password".

 

passErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))

if passErr == bcrypt.ErrMismatchedHashAndPassword && passErr != nil {
    return map[string]interface{}{"message": "Wrong password"}
}

 

Find accounts for the user

Now we have user, password, and all is validated, but our user is not complete yet.
 
We need a bank account for the user object.
 
To get that, we should define var accounts with a slice of type ResponseAccount.
We will assign data from the database inside this array.
 
Next, we need to ask the database to return fields "id", "name", and "balance" for the all records from the table "accounts" where "user_id" is equal to our user's id.

 

accounts := []interfaces.ResponseAccount{}
db.Table("accounts").Select("id, name, balance").Where("user_id = ? ", user.ID).Scan(&accounts)

Setup responseUser

In the next step, we should set up the struct of the responseUser, and assign values to the keys.

After responseUser struct we can close DB connection.

 

responseUser := &interfaces.ResponseUser{
    ID: user.ID,
    Username: user.Username,
    Email: user.Email,
    Accounts: accounts,
}

defer db.Close()

 

Sign token

One of the last and the most critical parts of the "Login" function is JWT token that we should set up, and sign.

We will use the JWT package for that.

Take a look at how it should be done in the example below.

 

tokenContent := jwt.MapClaims{
    "user_id": user.ID,
    "expiry": time.Now().Add(time.Minute * 60).Unix(),
}
jwtToken := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tokenContent)
token, err := jwtToken.SignedString([]byte("TokenPassword"))
helpers.HandleErr(err)

 

Prepare response

Finally! It's the last part of the "Login" function is a response that we will return into the API.

We should set up a message as "all is fine", our token pass to the "jwt" key, and responseUser into the "data" param.

 

var response = map[string]interface{}{"message": "all is fine"}
response["jwt"] = token
response["data"] = responseUser

return response

 

Create an API package

Now, we can go into the API.

The first step that we should complete is a new directory named "api", next create a file with the same name, and "go" extension.

Inside the file, we should declare a package named "api", and import a few dependencies.

 

package api

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "duomly.com/go-bank-backend/helpers"
    "duomly.com/go-bank-backend/users"

    "github.com/gorilla/mux"
)

Create structs in API

The second step is the struct declaration.

We should create two structs, the first one named "Login" with "Username" as a string, and "Password" as a string as well.

The last struct should be declared as "ErrResponse" with "Message" as a string.

 

type Login struct {
    Username string
    Password string
}


type ErrResponse struct {
    Message string
}

 

Create a login function in API

In this step, we will create the first function for our API.

It should be named "login", and take "w http.ResponseWriter", and "r *http.Request" as params.

 

func login(w http.ResponseWriter, r *http.Request) {

}

 

Read the body of the API call

The first logic inside the "login" function is a code that will read the body of our API request.

We should specify "body," and "err" variables and assign "ioutil.ReadAll" with request's body inside.

Next, we need to verify if all is fine by using "HandleErr" from the "helpers" package and passing "err" inside.

body, err := ioutil.ReadAll(r.Body)
helpers.HandleErr(err)

 

Handle login

The most important part of this function is the proper login.

We need to define a variable with a type "Login", and assign there Unmarshaled Request's body.

Next, we should pass the formatted username and formatted password into the "users.Login".

We should assign that logic to the variable named "login".

 

var formattedBody Login
err = json.Unmarshal(body, &formattedBody)
helpers.HandleErr(err)
login := users.Login(formattedBody.Username, formattedBody.Password)

 

Prepare a response for the API call

After calling the login, we should check if the login message is equal to the "all is fine".

If yes, we should declare a variable named "resp", and assign the login into it.

Next, we should use "json.NewEncoder" and encode our resp.

 

if login["message"] == "all is fine" {
    resp := login
    json.NewEncoder(w).Encode(resp)
} else {

}

 

Handle error in else

Now, when we handled the "if" statement, we should handle "error" as well.

To do that, we should create an "else" statement, and format a response inside it.

We need to return a "Message" with string "Wrong username or password", as a value.

This response should be encoded as same as the positive case.

 

if login["message"] == "all is fine" {
    resp := login
    json.NewEncoder(w).Encode(resp)
} else {
    resp := ErrResponse{Message: "Wrong username or password"}
    json.NewEncoder(w).Encode(resp)
}

 

Create startApi function

In the last step in the API logic, we should create a proper router, and handle our API endpoints.

We will use gorilla mux to create a router, and as a first one, we will define "/login" endpoints that will accept the "POST" method.

Next, we should set up an HTTP listener on the 8888 port.

 

func StartApi() {
    router := mux.NewRouter()
    router.HandleFunc("/login", login).Methods("POST")
    fmt.Println("App is working on port :8888")
    log.Fatal(http.ListenAndServe(":8888", router))

}

 

Implement API in the main function

API is ready!
To make it work, we should implement API inside the "main" function in the "main.go" file.

Check the example below.

 

package main

import "duomly.com/go-bank-backend/api"

func main() {
    // migrations.Migrate()
    api.StartApi()
}

 

Run API

Woohoo!
Now you can run your app and start testing it.

Open the terminal in the project's directory and type:

go run main.go

 

Conclusion

Congratulations, your project has the login and the first rest API.
 
You can start connecting it with front-end from the course:
 
Learn Angular 9 with Tailwind CSS by building fintech banking app
 
If you would like to compare the code with what I've done here is the URL: 
 
https://github.com/Duomly/go-bank-backend 
 
The branch for this lesson is named "Golang-course-Lesson-2". 
 
See you in the next lesson when we will build user registration.
 
Programming courses online 
 
Thanks for reading,
Radek from Duomly

Top comments (0)