Setting up the permissions model
Next, let’s head to our internal/data package and add a PermissionModel to manage the interactions with our new tables. For now, the only thing we want to include in this model is a GetAllForUser() method to return all permission codes for a specific user. The idea is that we’ll be able to use this in our handlers and middleware like so:
// Return a slice of the permission codes for the user with ID = 1. This would return // something like []string{"movies:read", "movies:write"}. app.models.Permissions.GetAllForUser(1)
Behind the scenes, the SQL statement that we need to fetch the permission codes for a specific user looks like this:
SELECT permissions.code FROM permissions INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id INNER JOIN users ON users_permissions.user_id = users.id WHERE users.id = $1
In this query we are using the INNER JOIN clause to join our permissions table to our users_permissions table, and then using it again to join that to the users table. Then we use the WHERE clause to filter the result, leaving only rows which relate to a specific user ID.
Let’s go ahead and set up the new PermissionModel. First create a new internal/data/permissions.go file:
$ touch internal/data/permissions.go
And then add the following code:
package data import ( "context" "database/sql" "time" ) // Define a Permissions slice, which we will use to hold the permission codes (such as // "movies:read" and "movies:write") for a single user. type Permissions []string // Add a helper method to check whether the Permissions slice contains a specific // permission code. func (p Permissions) Include(code string) bool { return slices.Contains(p, code) } // Define the PermissionModel type. type PermissionModel struct { DB *sql.DB } // The GetAllForUser() method returns all permission codes for a specific user in a // Permissions slice. The code in this method should feel very familiar --- it uses the // standard pattern that we've already seen before for retrieving multiple data rows in // an SQL query. func (m PermissionModel) GetAllForUser(userID int64) (Permissions, error) { query := ` SELECT permissions.code FROM permissions INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id INNER JOIN users ON users_permissions.user_id = users.id WHERE users.id = $1` ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() rows, err := m.DB.QueryContext(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var permissions Permissions for rows.Next() { var permission string err := rows.Scan(&permission) if err != nil { return nil, err } permissions = append(permissions, permission) } if err = rows.Err(); err != nil { return nil, err } return permissions, nil }
Then the last thing we need to do is add the PermissionModel to our parent Model struct, so it’s available to our handlers and middleware. Like so:
package data ... type Models struct { Movies MovieModel Permissions PermissionModel // Add a new Permissions field. Tokens TokenModel Users UserModel } func NewModels(db *sql.DB) Models { return Models{ Movies: MovieModel{DB: db}, Permissions: PermissionModel{DB: db}, // Initialize a new PermissionModel instance. Tokens: TokenModel{DB: db}, Users: UserModel{DB: db}, } }