DEV Community

Cover image for 08 - Implement Signup in Service and Repository Layers
Jacob Goodwin
Jacob Goodwin

Posted on • Updated on

08 - Implement Signup in Service and Repository Layers

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.

08 Account Application Progress

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, "")
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

The hashPassword function creates a hashed version of the provided password by:

  1. Creating a 32-byte salt using golang's random package.
  2. 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.
  3. 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:

  1. Splits the string we created in the previous function to separate the salt and hashed password.
  2. Hashes the supplied password, or what the user enters when logging in with the salt.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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!

Docker Logs)

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!

Oldest comments (1)

Collapse
 
bab014 profile image
Bret Beatty

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.