Learn Golang by building a fintech banking app - Lesson2: Login and REST API
Duomly
Posted on May 22, 2020
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.
Thanks for reading,
Radek from Duomly
Posted on May 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.