The last thing we did in our account application was to create and test a Signup
handler. In this tutorial, we'll get work on the logic for signing up a user by adding service and repository layer method implementations.
Our goal over the next 3 tutorials will be to get the application working for signing up a user, including storing a newly created user in a database, and creating authorization tokens for a newly signed up, or a signed in user.
If at any point you are confused about file structure or code, go to Github repository and check out the branch for the previous lesson to be in sync with me!
If you prefer video, check out the video version below!
And for an overview of what the app we're building, see this.
Current Progress
The diagram below shows the methods in each application layer that we'll eventually need to implement, along with check marks next to those we've already completed. We started out by working on the me
handler and UserService.Get
method. We did this first just do get a feel for creating and unit testing handlers and services.
Now, though, we want to learn how to put these pieces together into a functioning web API application!
Looking at the code code inside of the Signup
handler, we see that we reached out to UserService.Signup
and TokenService.NewPairFromUser
. We'll be working on creating concrete implementations of these methods in the next two tutorials so that we can send HTTP requests to our application and create real users inside of a PostgresQL database!
// ...data binding and instantiation of User model...
err := h.UserService.Signup(c, u)
// ...error handling ...
// ...create token pair as strings
tokens, err := h.TokenService.NewPairFromUser(c, u, "")
In order to store users in an actual database, we'll need to implement a UserRepository
, along with its Create
method. This repository will accept a connection to a Postgres database. This Postgres database will be created inside of a docker container. I know it's been a while, but recall that we do have a docker-compose.yml
file at the root of the project for defining the containers required to run our application.
In the next tutorial we'll work on creating tokens in the NewPairFromUser
method. There's a little bit of context I'll need to provide on these token pairs, so it will require a second tutorial.
In the third tutorial, we're going to connect all of these pieces together by injecting a database connection into our UserRepository, injecting the UserRepository into the UserService, and then injecting the TokenService and UserService into the handler layer.
At that point, we'll finally be able to fire up our live reload environment and send requests to sign up users!
Are you intimidated yet? ... Or are you significantly more resilient than me?
After we have these pieces in place, we'll have implemented some of the most difficult parts of this project.
Add Create to UserRepository Interface
Let's add the expectation that our UserRepository
should Create
users inside of ~/model/interfaces.go
.
// UserRepository defines methods the service layer expects
// any repository it interacts with to implement
type UserRepository interface {
FindByID(ctx context.Context, uid uuid.UUID) (*User, error)
Create(ctx context.Context, u *User) error
}
If a user is successfully created, it should update the underlying User
passed to Create
, otherwise an error should be returned.
Add Create to Mock Implementation
You're probably getting a warning now since our mockUserRepository
does not yet implement the Get
method. We add the implementation inside ~/mocks/user_repository.go
. This will also allow us to test our UserService.Signup
method. As always, check out the testify docs for more information about creating these mocks.
// Create is a mock for UserRepository Create
func (m *MockUserRepository) Create(ctx context.Context, u *model.User) error {
ret := m.Called(ctx, u)
var r0 error
if ret.Get(0) != nil {
r0 = ret.Get(0).(error)
}
return r0
}
UserService Signup
We've already scaffolded out this method inside of ~/service/user_service.go
. Let's now add the implementation details.
If you look at the top of the file, you'll see that we've already provided access to a model.UserRepository
in our UserService struct
which will allow us to access the Create
method.
// Signup reaches our to a UserRepository to sign up the user.
// UserRepository Create should handle checking for user exists conflicts
func (s *UserService) Signup(ctx context.Context, u *model.User) error {
if err := s.UserRepository.Create(ctx, u); err != nil {
return err
}
// If we get around to adding events, we'd Publish it here
// err := s.EventsBroker.PublishUserUpdated(u, true)
// if err != nil {
// return nil, apperrors.NewInternal()
// }
return nil
}
Wow! That was really easy!
Not so fast! We don't want to store passwords as plain text because that would make life really easy for hackers if our database were breached.
A few tutorials back, I said that we'd have to add more complexity and logic to some of our service methods, so here we go! We're going to add a functions for encrypting a password and another for comparing a supplied password with the encrypted password.
Before continuing, let me warn you that I'm not even a cryptography novice, but I'm going to add some code based on what I've found out there for generating a "salt" and then hashing the password with scrypt.
Password Utility Functions
Lets add a file called ~/service/passwords.go
. You could also create a sub-package or a root-level package. There are many opinions on how to structure utility applications out there, but I'm just going to created a file inside of our service layer as we don't plan to use this code elsewhere (maybe this is a dumb decision).
package service
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
"golang.org/x/crypto/scrypt"
)
func hashPassword(password string) (string, error) {
// example for making salt - https://play.golang.org/p/_Aw6WeWC42I
salt := make([]byte, 32)
_, err := rand.Read(salt)
if err != nil {
return "", err
}
// using recommended cost parameters from - https://godoc.org/golang.org/x/crypto/scrypt
shash, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)
if err != nil {
return "", err
}
// return hex-encoded string with salt appended to password
hashedPW := fmt.Sprintf("%s.%s", hex.EncodeToString(shash), hex.EncodeToString(salt))
return hashedPW, nil
}
func comparePasswords(storedPassword string, suppliedPassword string) (bool, error) {
pwsalt := strings.Split(storedPassword, ".")
// check supplied password salted with hash
salt, err := hex.DecodeString(pwsalt[1])
if err != nil {
return false, fmt.Errorf("Unable to verify user password")
}
shash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32)
return hex.EncodeToString(shash) == pwsalt[0], nil
}
The hashPassword
function creates a hashed version of the provided password by:
- Creating a 32-byte salt using golang's random package.
- Creating a key, or hash, from a byte slice of the provided password, the salt we just created, and so-called cost parameters. The cost parameters provide a tradeoff between computational power required to encrypt the password and difficulty of recreating the encrypted password via brute-force attacks. We'll use the cost parameters recommended by the scrypt docs. The final parameter, 32, corresponds to the number of bytes for the key/hash.
- In order to check a user's password when they try to login at a later date, we'll need the same salt we just generated. So what we'll do is store the salt with the hashed password. We do this by using
fmt.Sprintf
, appending the salt to the hashed password with a "." in between. We encode both the hash and salt as hexadecimal strings.
We also create a comparePassword function. This function:
- Splits the string we created in the previous function to separate the salt and hashed password.
- Hashes the supplied password, or what the user enters when logging in with the salt.
- Compares the above value with the hashed password from the database. If the two match, we know the user has entered the correct password!
Applying hashPassword to Signup
Let's update our Signup
method to use the hashed password!
func (s *UserService) Signup(ctx context.Context, u *model.User) error {
pw, err := hashPassword(u.Password)
if err != nil {
log.Printf("Unable to signup user for email: %v\n", u.Email)
return apperrors.NewInternal()
}
// now I realize why I originally used Signup(ctx, email, password)
// then created a user. It's somewhat un-natural to mutate the user here
u.Password = pw
if err := s.UserRepository.Create(ctx, u); err != nil {
return err
}
// ...
return nil
}
That's it for this service method. We'll be able to test it by using the UserRepository mock we previously updated!
Unit Test UserService Signup
Unit tests for the Signup method have been added below and can be found in ~/service/user_service_test.go
. I want to mention that I did learn something new in creating this test.
In the success case, we chain on a Run
method to our testify mock call, which executes some code on the provided arguments to the Create
method call. In this case, we add a UID
to the user, which mocks actual repository behavior.
func TestSignup(t *testing.T) {
t.Run("Success", func(t *testing.T) {
uid, _ := uuid.NewRandom()
mockUser := &model.User{
Email: "bob@bob.com",
Password: "howdyhoneighbor!",
}
mockUserRepository := new(mocks.MockUserRepository)
us := NewUserService(&USConfig{
UserRepository: mockUserRepository,
})
// We can use Run method to modify the user when the Create method is called.
// We can then chain on a Return method to return no error
mockUserRepository.
On("Create", mock.AnythingOfType("*context.emptyCtx"), mockUser).
Run(func(args mock.Arguments) {
userArg := args.Get(1).(*model.User) // arg 0 is context, arg 1 is *User
userArg.UID = uid
}).Return(nil)
ctx := context.TODO()
err := us.Signup(ctx, mockUser)
assert.NoError(t, err)
// assert user now has a userID
assert.Equal(t, uid, mockUser.UID)
mockUserRepository.AssertExpectations(t)
})
t.Run("Error", func(t *testing.T) {
mockUser := &model.User{
Email: "bob@bob.com",
Password: "howdyhoneighbor!",
}
mockUserRepository := new(mocks.MockUserRepository)
us := NewUserService(&USConfig{
UserRepository: mockUserRepository,
})
mockErr := apperrors.NewConflict("email", mockUser.Email)
// We can use Run method to modify the user when the Create method is called.
// We can then chain on a Return method to return no error
mockUserRepository.
On("Create", mock.AnythingOfType("*context.emptyCtx"), mockUser).
Return(mockErr)
ctx := context.TODO()
err := us.Signup(ctx, mockUser)
// assert error is error we response with in mock
assert.EqualError(t, err, mockErr.Error())
mockUserRepository.AssertExpectations(t)
})
}
Make sure to run the tests, and also fail them, to make sure the tests work as expected.
go test -v ./service
.
PGUserRepository Get and FindByID
We can now take a couple of different paths. We can either work on the TokenService or on the UserRepository. We'll work on the TokenService next time as it requires a decent amount of explanation.
Let's create a new folder called repository
for our concrete repository implementations. Inside, add pg_user_repository.go
, where we clearly name this implementation as one which uses Postgres.
Inside of this file, we're going to add an import to a new package called SQLX, which adds some utility methods on top of the built-in golang SQL library. This SQLX will be the only dependency of our UserRepository.
We'll also use a factory to initialize our UserRepository as in the service layer implementations.
package repository
import (
"github.com/jmoiron/sqlx"
)
// PGUserRepository is data/repository implementation
// of service layer UserRepository
type PGUserRepository struct {
DB *sqlx.DB
}
// NewUserRepository is a factory for initializing User Repositories
func NewUserRepository(db *sqlx.DB) model.UserRepository {
return &PGUserRepository{
DB: db,
}
}
Also, I recently realized that my service and repository structs should probably be package private (starting with a lower-case letter). Our factories return an interface, and we don't want users to instantiate repository or service structs manually. That's the whole purpose of this factory pattern! We'll fix this in a few tutorials.
If your IDE or editor is any good, you should see an error because our implementation of a UserRepository does not have a Create
method! We'll also need a FindByID
method since we defined that method in our UserRepository
interface while working on the "me" handler.
// Create reaches out to database SQLX api
func (r *PGUserRepository) Create(ctx context.Context, u *model.User) error {
query := "INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *"
if err := r.DB.Get(u, query, u.Email, u.Password); err != nil {
// check unique constraint
if err, ok := err.(*pq.Error); ok && err.Code.Name() == "unique_violation" {
log.Printf("Could not create a user with email: %v. Reason: %v\n", u.Email, err.Code.Name())
return apperrors.NewConflict("email", u.Email)
}
log.Printf("Could not create a user with email: %v. Reason: %v\n", u.Email, err)
return apperrors.NewInternal()
}
return nil
}
// FindByID fetches user by id
func (r *PGUserRepository) FindByID(ctx context.Context, uid uuid.UUID) (*model.User, error) {
user := &model.User{}
query := "SELECT * FROM users WHERE uid=$1"
// we need to actually check errors as it could be something other than not found
if err := r.DB.Get(user, query, uid); err != nil {
return user, apperrors.NewNotFound("uid", uid.String())
}
return user, nil
}
In these methods, we create query strings, then execute them with application data by calling SQLX methods.
The Get
method on the DB (sqlx connection) for inserting may seem a little strange, but this is a way we can use "Returning" in the SQL query and apply it to the user, u
.
We then check for a very specific error returned by the SQL library when a unique violation occurs. If that is the case, let's return an apperrors.NewConflict
error. Otherwise, we'll return an InternalServerError.
We'll configure the "email" column of our user database table to be unique in a couple of tutorials, so any attempt to add an existing email will produce a unique violation error.
Let's leave this for now. We'll inject a db connection, and in turn inject this repository into the UserService in a couple tutorials. I just wanted to give you an idea of what the repository layer will look like.
Setup a Docker Container for Postgres
Now I want to do something that doesn't necessarily fit in with this tutorial, but will save us some time later. Let's set up a Postgres container for our docker development environment.
We can setup a basic development Postgres container by adding the following to our docker-compose.yml file (at the project root).
Make sure to add the dependency
key to the config of our account
service. This makes sure to not spin up the container if there's an issue spinning up the postgres-auth
container.
Also set up a named volume for the Postgres container's data. You can add this volume under the volumes
key with no indentation (at the same indentation as the services
key).
...
postgres-auth:
image: "postgres:alpine"
environment:
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
# Set a volume for data and initial sql script
# May configure initial db for future demo
volumes:
- "pgdata_auth:/var/lib/postgresql/data"
# - ./init:/docker-entrypoint-initdb.d/
command: ["postgres", "-c", "log_statement=all"]
...
account:
build:
context: ./auth
target: builder
image: account # if we don't give image name, traefik won't create router 🤷♂️
expose:
- "8080" # seems necessary for Traefik to have internal expose of port
labels:
- "traefik.enable=true"
- "traefik.http.routers.account.rule=Host(`wordmem.test`) && PathPrefix(`/api/account`)"
environment:
- ENV=dev
volumes:
- ./auth:/go/src/app
depends_on:
- postgres-auth
volumes:
pgdata_auth:
Let's make sure we can run this.
cd ..
to get to the root folder and run
docker-compose up
.
Hopefully you get logs showing your account and postgres containers up and running!
Conclusion
Alright, I'm going to make an abrupt stop here. Thanks again for joining along!
Next time we're going to work on creating our idToken (which also serves as an access token), as well as a refreshToken. We'll describe the differences between these tokens and how to create them.
Until then, ¡cuídense!
Top comments (1)
This has to be one of the best tutorials I've ever gone through. I'm learning so much. I am refactoring this code to use the gorm.io/gorm package, since I use and like it. This has allowed me to get even stronger with the gorm code and overall how to interact with databases and GO.