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
}
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)
}
}
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)
}
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)
}
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)
})
}
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))
}
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{} {
}
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
}
}
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
}
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"}
}
}
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"}
}
}
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"}
}
}
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)
}
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))
}
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
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()
}
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.
Thanks for reading,
Radek from Duomly
Top comments (1)
Great resource!
A good pre read or follow: dev.to/dpkahuja/learn-and-build-we...