Learn Golang by building a fintech banking app - Lesson4: User authentication and Bank transfers PART 1

Duomly - Jun 9 '20 - - Dev Community

This article was originally published at: https://www.blog.duomly.com/golang-course-with-building-a-fintech-banking-app-lesson-4-user-authentication-and-bank-transactions-part-1


Intro

In Lesson 4 of the Golang course, we will talk about user authentication and banking transactions.

In the previous episodes of the course, we learned how to do migrations:

Golang course with building a fintech banking app - Lesson 1: Start the project

We learned how to do user login:

Golang course with building a fintech banking app - Lesson 2: Login and REST API

And, we learned how to do user registration:

Golang course with building a fintech banking app - Lesson 3: User registration

As well you need to remember about Angular 9 course created by my friend Anna:

Angular Course with building a banking application with Tailwind CSS - Lesson 1: Start the project

Today, we can focus on something crazy interesting, that is one of the main functionality in our project.

Of course, I mean bank transfers that I've told you about in the previous episode.

But not only!

In this lesson of the Golang course, we will learn how to get users and refactor a bit of the code.

I will teach you how yo use user authentication with JWT token as well.

Let's start!

If you prefer video, here is the youtube version:

Create readBody in api.go

In the first step of this Golang course lesson, we need to refactor our api.go file a bit.

Most of the API calls have a very similar structure, so we do not need to duplicate the same code all the time.

We can refactor the logic that we use to reading the body.

Let's create the readBody function in api.go, and move there the logic from the login and register functions.

func readBody(r *http.Request) []byte {
    body, err := ioutil.ReadAll(r.Body)
    helpers.HandleErr(err)

    return body
}
Enter fullscreen mode Exit fullscreen mode

Create apiResponse in api.go

The next logic that we often use in the API calls is logically related to the API response.

We can do similar action, let's create the function named apiResponse.

Next, move all the functions related to call response into and change the name of the objects from "login", "register" to "call".

func apiResponse(call map[string]interface{}, w http.ResponseWriter) {
    if call["message"] == "all is fine" {
        resp := call
        json.NewEncoder(w).Encode(resp)
        // Handle error in else
    } else {
        resp := interfaces.ErrResponse{Message: "Wrong username or password"}
        json.NewEncoder(w).Encode(resp)
    }
}
Enter fullscreen mode Exit fullscreen mode

Refactor login in api.go to use readBody and apiResponse

Fine, our cleaner code is ready.

Now, we need to clean the login function and replace all of the code by our functions.

Replace the logic related to the reading body and the logic related to the response.

func login(w http.ResponseWriter, r *http.Request) {
    // Refactor login to use readBody
    body := readBody(r)

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

    login := users.Login(formattedBody.Username, formattedBody.Password)
    // Refactor login to use apiResponse function
    apiResponse(login, w)
}
Enter fullscreen mode Exit fullscreen mode

Refactor register in api.go to use readBody and apiResponse

The function "register" should be cleaned in the same way that we did it with the "login" one.

Just replace the reading body and the response.

func register(w http.ResponseWriter, r *http.Request) {
    body := readBody(r)

    var formattedBody Register
    err := json.Unmarshal(body, &formattedBody)
    helpers.HandleErr(err)

    register := users.Register(formattedBody.Username, formattedBody.Email, formattedBody.Password)
    // Refactor register to use apiResponse function
    apiResponse(register, w)
}
Enter fullscreen mode Exit fullscreen mode

Create PanicHandler middleware to handle internal errors

In the next step, we should focus on handling our "panic" logs.

These are not very good because if something happens, the app starts panicking.

It would be great to let users know something is wrong and its internal error in most cases.

We will not handle all of the errors separately yet, but we will handle the internal ones.

To do that, we need to go into the helpers.go and create some middleware named "PanicHandler".

The middleware will intercept our HTTP calls.

Next, it will recover from the panic. It will return a response to the user with the message "Internal server error".

func PanicHandler(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            error := recover()
            if error != nil {
                log.Println(error)

                resp := interfaces.ErrResponse{Message: "Internal server error"}
                json.NewEncoder(w).Encode(resp)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

Add PanicHandler in api.go startApi function

When our code is ready, we should go into the api.go and add it in the "startApi" function after the router's declaration.

func StartApi() {
    router := mux.NewRouter()
    // Add panic handler middleware
    router.Use(helpers.PanicHandler)
    router.HandleFunc("/login", login).Methods("POST")
    router.HandleFunc("/register", register).Methods("POST")
    fmt.Println("App is working on port :8888")
    log.Fatal(http.ListenAndServe(":8888", router))
}
Enter fullscreen mode Exit fullscreen mode

Move ErrResponse to interfaces

In this step, we need to move the "ErrResponse" interface from the "api.go".

Le' ts move it into the interfaces.go file.

Create GetUser function in users.go

Great!

Refactoring is almost finished, now we can go into the users-related stuff.

The first step should be to go into the users.go and create the function named "GetUser".

That function should take two params, both as strings, the first one should be "id", and the second one should be "jwt".

GetUser function should return „map[string]interface{}.

func GetUser(id string, jwt string) map[string]interface{} {

}
Enter fullscreen mode Exit fullscreen mode

Validate jwt token

If we would like to do any activity related to the user, the application needs to know if we are the owner of the account.

We can verify that by validating the JWT token.

To create JWT validation, we need to go into the helpers.go and create a function named "ValidateToken".

The function should take id, and jwtToken as strings, and return a bool.

Inside the body, we need to remove "Bearer" from our token and verify the JWT token.

Next, we should verify if the id from the authenticated token is the same as an id that we sent to the API.

func ValidateToken(id string, jwtToken string) bool {
    cleanJWT := strings.Replace(jwtToken, "Bearer ", "", -1)
    tokenData := jwt.MapClaims{}
    token, err := jwt.ParseWithClaims(cleanJWT, tokenData, func(token *jwt.Token) (interface{}, error) {
            return []byte("TokenPassword"), nil
    })
    HandleErr(err)
    var userId, _ = strconv.ParseFloat(id, 8)
    if token.Valid && tokenData["user_id"] == userId {
        return true
    } else {
        return false
    }
}
Enter fullscreen mode Exit fullscreen mode

Add withToken feature to prepareResponse

Now, we can come back to the users.go, and refactor "prepareResponse".

We need to add a little change, that will be the third param named "withToken" as a bool.

And, we need to create token only if the withToken will be true.

func prepareResponse(user *interfaces.User, accounts []interfaces.ResponseAccount, withToken bool) map[string]interface{} {
    responseUser := &interfaces.ResponseUser{
        ID: user.ID,
        Username: user.Username,
        Email: user.Email,
        Accounts: accounts,
    }
    var response = map[string]interface{}{"message": "all is fine"}
    // Add withToken feature to prepare response
    if withToken {
        var token = prepareToken(user);
        response["jwt"] = token
    }
    response["data"] = responseUser
    return response
}
Enter fullscreen mode Exit fullscreen mode

Change prepare response in login and register in users.go

Don't forget to add the change into the login, and register functions inside the users.go.

Just add "true" in both of cases into the call to the "prepareResponse", as a third param.

func Login(username string, pass string) map[string]interface{} {
    // Add validation to login
    valid := helpers.Validation(
        []interfaces.Validation{
            {Value: username, Valid: "username"},
            {Value: pass, Valid: "password"},
        })
    if valid {
        // Connect DB
        db := helpers.ConnectDB()
        user := &interfaces.User{}
        if db.Where("username = ? ", username).First(&user).RecordNotFound() {
            return map[string]interface{}{"message": "User not found"}
        }
        // Verify 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
        accounts := []interfaces.ResponseAccount{}
        db.Table("accounts").Select("id, name, balance").Where("user_id = ? ", user.ID).Scan(&accounts)

        defer db.Close()

        var response = prepareResponse(user, accounts, true);

        return response
    } else {
        return map[string]interface{}{"message": "not valid values"}
    }
}

// Create registration function
func Register(username string, email string, pass string) map[string]interface{} {
    // Add validation to registration
    valid := helpers.Validation(
        []interfaces.Validation{
            {Value: username, Valid: "username"},
            {Value: email, Valid: "email"},
            {Value: pass, Valid: "password"},
        })
    if valid {
        // Create registration logic
        // Connect DB
        db := helpers.ConnectDB()
        generatedPassword := helpers.HashAndSalt([]byte(pass))
        user := &interfaces.User{Username: username, Email: email, Password: generatedPassword}
        db.Create(&user)

        account := &interfaces.Account{Type: "Daily Account", Name: string(username + "'s" + " account"), Balance: 0, UserID: user.ID}
        db.Create(&account)

        defer db.Close()
        accounts := []interfaces.ResponseAccount{}
        respAccount := interfaces.ResponseAccount{ID: account.ID, Name: account.Name, Balance: int(account.Balance)}
        accounts = append(accounts, respAccount)
        var response = prepareResponse(user, accounts, true)

        return response
    } else {
        return map[string]interface{}{"message": "not valid values"}
    }

}
Enter fullscreen mode Exit fullscreen mode

Add token validation to getUser

In this step, we are ready to come back to getUser.

The first what we will do here is to add the token validation.

func GetUser(id string, jwt string) map[string]interface{} {
    isValid := helpers.ValidateToken(id, jwt)
    if isValid {

    } else {
        return map[string]interface{}{"message": "Not valid token"}
     }
}
Enter fullscreen mode Exit fullscreen mode

Find and return user

If the token is validated, we can start the logic.

If not, we should return the response with the information about that fact.

As a logic, we actually should add almost the same logic as for the function "Login".

We need to find user. In this case, by ID, next find accounts for that user, and that's it.

Just in the "prepareResponse" call, the last param will be "false".

func GetUser(id string, jwt string) map[string]interface{} {
    isValid := helpers.ValidateToken(id, jwt)
    // Find and return user
    if isValid {
        db := helpers.ConnectDB()
        user := &interfaces.User{}
        if db.Where("id = ? ", id).First(&user).RecordNotFound() {
            return map[string]interface{}{"message": "User not found"}
        }
        accounts := []interfaces.ResponseAccount{}
        db.Table("accounts").Select("id, name, balance").Where("user_id = ? ", user.ID).Scan(&accounts)

        defer db.Close()

        var response = prepareResponse(user, accounts, false);
        return response
    } else {
        return map[string]interface{}{"message": "Not valid token"}
    }
}
Enter fullscreen mode Exit fullscreen mode

Create getUser function in api.go

We are done with users.go, let's move back into the api.go.

As the first step in that file, we need to create a function named "getUser", and as a standard API call, we take response and request as params.

Inside the function, we will use mux vars to take the user's ID that we need to pull from the DB.

The next important thing is to take authorization from our headers and pass it to the GetUser function.

func getUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userId := vars["id"]
    auth := r.Header.Get("Authorization")

    user := users.GetUser(userId, auth)
    apiResponse(user, w)
}
Enter fullscreen mode Exit fullscreen mode

Add router.Handle func for getting the user in api.go

Now we can add the next API endpoint into the routing.

Add that inside the "StartApi", and remember to set up the Methods, as "GET".

 

func StartApi() {
    router := mux.NewRouter()
    router.Use(helpers.PanicHandler)
    router.HandleFunc("/login", login).Methods("POST")
    router.HandleFunc("/register", register).Methods("POST")
    router.HandleFunc("/user/{id}", getUser).Methods("GET")
    fmt.Println("App is working on port :8888")
    log.Fatal(http.ListenAndServe(":8888", router))
}
Enter fullscreen mode Exit fullscreen mode

Create directory useraccounts

We are done with getting users, and we move into the bank accounts.

As the first step, we should create a directory named "useraccounts".

Create that directory inside the root of the project.

Create file useraccounts.go and package useraccounts

Next, we need to create a file named "useraccounts.go" inside that directory.

Inside the file, we need to declare a package named "useraccounts".

package useraccounts
Enter fullscreen mode Exit fullscreen mode

Create function updateAccount

The last step inside today's lesson of the Golang course is to create the function that gives the possibility of updating the user's bank account.

Let's go into the useraccounts.go, and create the function named "updateAccount".

Inside the function, we need to connect DB, update the account, and close DB.

Don't worry about connecting/disconnecting DB so often. We will work on connection pools in the next episodes.

func updateAccount(id uint, amount int) {
    db := helpers.ConnectDB()
    db.Model(&interfaces.Account{}).Where("id = ?", id).Update("balance", amount)
    defer db.Close()
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Congratulations, your project is going much more functional now!

I'm very, you know how to use user authentication with JWT token, and started building bank transfers feature.

Here is the code repository for the current lesson: https://github.com/Duomly/go-bank-backend/tree/Golang-course-Lesson-4

In the next episode, we will continue working on the bank transfers and will focus on the REST API for that feature.

I cannot wait until I teach you all of these things, and you will do the first bank transfer!

Stay updated, because the next episodes will be the most crucial for that Golang course, and we will build the most important features.

Programming courses online

Thanks for reading,

Radek from Duomly

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .