Creating a users model
Now that the routes are set up, we need to create a new users
database table and a database model to access it.
Start by connecting to MySQL from your terminal window as the root
user and execute the following SQL statement to setup the users
table:
USE snippetbox; CREATE TABLE users ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, hashed_password CHAR(60) NOT NULL, created DATETIME NOT NULL ); ALTER TABLE users ADD CONSTRAINT users_uc_email UNIQUE (email);
There’s a couple of things worth pointing out about this table:
The
id
field is an autoincrementing integer field and the primary key for the table. This means that the user ID values are guaranteed to be unique positive integers (1, 2, 3… etc).The type of the
hashed_password
field isCHAR(60)
. This is because we’ll be storing bcrypt hashes of the user passwords in the database — not the passwords themselves — and the hashes are always exactly 60 characters long.We’ve also added a
UNIQUE
constraint on theemail
column and named itusers_uc_email
. This constraint ensures that we won’t end up with two users who have the same email address. If we try to insert a record in this table with a duplicate email, MySQL will throw anERROR 1062: ER_DUP_ENTRY
error.
Building the model in Go
Next let’s setup a model so that we can easily work with the new users
table. We’ll follow the same pattern that we used earlier in the book for modeling access to the snippets
table, so hopefully this should feel familiar and straightforward.
First, open up the internal/models/errors.go
file that you created earlier and define a couple of new error types:
package models import ( "errors" ) var ( ErrNoRecord = errors.New("models: no matching record found") // Add a new ErrInvalidCredentials error. We'll use this later if a user // tries to login with an incorrect email address or password. ErrInvalidCredentials = errors.New("models: invalid credentials") // Add a new ErrDuplicateEmail error. We'll use this later if a user // tries to signup with an email address that's already in use. ErrDuplicateEmail = errors.New("models: duplicate email") )
Then create a new file at internal/models/users.go
:
$ touch internal/models/users.go
…and define a new User
struct (to hold the data for a specific user) and a UserModel
struct (with some placeholder methods for interacting with our database). Like so:
package models import ( "database/sql" "time" ) // Define a new User struct. Notice how the field names and types align // with the columns in the database "users" table? type User struct { ID int Name string Email string HashedPassword []byte Created time.Time } // Define a new UserModel struct which wraps a database connection pool. type UserModel struct { DB *sql.DB } // We'll use the Insert method to add a new record to the "users" table. func (m *UserModel) Insert(name, email, password string) error { return nil } // We'll use the Authenticate method to verify whether a user exists with // the provided email address and password. This will return the relevant // user ID if they do. func (m *UserModel) Authenticate(email, password string) (int, error) { return 0, nil } // We'll use the Exists method to check if a user exists with a specific ID. func (m *UserModel) Exists(id int) (bool, error) { return false, nil }
The final stage is to add a new field to our application
struct so that we can make this model available to our handlers. Update the main.go
file as follows:
package main ... // Add a new users field to the application struct. type application struct { logger *slog.Logger snippets *models.SnippetModel users *models.UserModel templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager } func main() { ... // Initialize a models.UserModel instance and add it to the application // dependencies. app := &application{ logger: logger, snippets: &models.SnippetModel{DB: db}, users: &models.UserModel{DB: db}, templateCache: templateCache, formDecoder: formDecoder, sessionManager: sessionManager, } tlsConfig := &tls.Config{ CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, } srv := &http.Server{ Addr: *addr, Handler: app.routes(), ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), TLSConfig: tlsConfig, IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } logger.Info("starting server", "addr", srv.Addr) err = srv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem") logger.Error(err.Error()) os.Exit(1) } ...
Make sure that all the files are all saved, then go ahead and try to run the application. At this stage you should find that it compiles correctly without any problems.