ZhangZhihui's Blog  

 

 

In errors.go:

func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
    message := "you must be authenticated to access this resource"
    app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
    message := "your user account must be activated to access this resource"
    app.errorResponse(w, r, http.StatusForbidden, message)
}

 

In middleware.go:

func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })
}

 

 

In routes.go:

    // Use the requireActivatedUser() middleware on /v1/movies** endpoints.
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requireActivatedUser(app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requireActivatedUser(app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requireActivatedUser(app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requireActivatedUser(app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requireActivatedUser(app.deleteMovieHandler))

 

zzh@ZZHPC:~$ curl -i localhost:4000/v1/movies/1
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 07:55:28 GMT
Content-Length: 69

{
    "error": "you must be authenticated to access this resource"
}

 

zzh@ZZHPC:~$ curl -i -d '{"email": "alice@example.com", "password": "pa55word"}' localhost:4000/v1/tokens/authentication
HTTP/1.1 201 Created
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 08:04:48 GMT
Content-Length: 143

{
    "authentication_token": {
        "token": "NCSFGSV7Q62PZ3FVP4MDKBOQCQ",
        "expiry": "2024-11-23T16:04:48.300138435+08:00"
    }
}
zzh@ZZHPC:~$ curl -i -H "Authorization: Bearer NCSFGSV7Q62PZ3FVP4MDKBOQCQ" localhost:4000/v1/movies/1
HTTP/1.1 403 Forbidden
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 08:05:28 GMT
Content-Length: 79

{
    "error": "your user account must be activated to access this resource"
}

 

zzh@ZZHPC:~$ BODY='{"email": "ZhangZhihuiAAA@126.com", "password": "pa55word"}'

zzh@ZZHPC:~$ curl -i -H "Authorization: Bearer RULKBNEGFJBHRE3ADD7HUGVHSQ" localhost:4000/v1/movies/1
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Authorization
Date: Fri, 22 Nov 2024 07:59:07 GMT
Content-Length: 222

{
    "movie": {
        "id": 1,
        "title": "Moana",
        "year": 2016,
        "runtime": "107 mins",
        "genres": [
            "animation",
            "adventure"
        ],
        "version": 1
    }
}

 

func (app *application) requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    // Rather than returning this http.HandlerFunc we assign it to the variable fn.
    fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })

    return app.requireAuthenticatedUser(fn)
}

 

 

 

 

zzh@ZZHPC:/zdata/Github/greenlight$ migrate create -seq -ext .sql -dir ./migrations add_permissions
/zdata/Github/greenlight/migrations/000006_add_permissions.up.sql
/zdata/Github/greenlight/migrations/000006_add_permissions.down.sql

 

CREATE TABLE IF NOT EXISTS permission (
    id   bigserial PRIMARY KEY,
    code text      NOT NULL
);

CREATE TABLE IF NOT EXISTS user_permission (
    user_id       bigint NOT NULL REFERENCES users ON DELETE CASCADE,
    permission_id bigint NOT NULL REFERENCES permission ON DELETE CASCADE,
    PRIMARY KEY (user_id, permission_id)
);

INSERT INTO permission (code)
VALUES
    ('movie:read'),
    ('movie:write');

 

DROP TABLE IF EXISTS user_permission;
DROP TABLE IF EXISTS permission;

 

zzh@ZZHPC:/zdata/Github/greenlight$ make migrate_up
6/u add_permissions (16.965477ms)

 

 

package data

import (
    "context"
    "slices"
    "time"
)

// Permissions stores the permission codes for a single user.
type Permissions []string

// Include checks whether the Permissions slice contains a specific permission code.
func (p Permissions) Include(code string) bool {
    return slices.Contains(p, code)
}

// PermissionModel struct wraps a database connection pool wrapper.
type PermissionModel struct {
    DB *PoolWrapper
}

// GetAllForUser returns all permission codes for a specific user.
func (m PermissionModel) GetAllForUser(userID int64) (Permissions, error) {
    query := `SELECT p.code 
                FROM permissions p 
               INNER JOIN user_permission up ON up.permission_id = p.id 
               INNER JOIN users u ON up.user_id = u.id 
               WHERE u.id = $1`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    rows, err := m.DB.Pool.Query(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
}

 

func (app *application) requirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
    fn := func(w http.ResponseWriter, r *http.Request)  {
        user := app.contextGetUser(r)

        permissions, err := app.models.Permission.GetAllForUser(user.ID)
        if err != nil {
            app.serverErrorResponse(w, r, err)
            return
        }

        if !permissions.Include(code) {
            app.notPermittedResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    }

    return app.requireActivatedUser(fn)
}

 

    // Use the requirePermission() middleware on /v1/movies** endpoints.
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requirePermission("movie:read", app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requirePermission("movie:write", app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requirePermission("movie:read", app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requirePermission("movie:write", app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requirePermission("movie:write", app.deleteMovieHandler))

 

Insert some records into the user_permission table:

greenlight=> INSERT INTO user_permission
greenlight-> SELECT id, (SELECT id FROM permission WHERE code = 'movie:read') FROM users;
INSERT 0 2

greenlight=> INSERT INTO user_permission VALUES (9, 2);
INSERT 0 1
greenlight=> SELECT * FROM user_permission;
 user_id | permission_id 
---------+---------------
       1 |             1
       9 |             1
       9 |             2
(3 rows)

Activated the user whose id is 1:

greenlight=> UPDATE users SET activated = true WHERE id = 1;
UPDATE 1

 

zzh@ZZHPC:~$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
zzh@ZZHPC:~$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "JYL3P33DFLDVICZHLI46EEM73E",
        "expiry": "2024-11-23T17:29:37.512601321+08:00"
    }
}
zzh@ZZHPC:~$ curl -H "Authorization: Bearer JYL3P33DFLDVICZHLI46EEM73E" localhost:4000/v1/movies/1
{
    "movie": {
        "id": 1,
        "title": "Moana",
        "year": 2016,
        "runtime": "107 mins",
        "genres": [
            "animation",
            "adventure"
        ],
        "version": 1
    }
}
zzh@ZZHPC:~$ curl -X DELETE -H "Authorization: Bearer JYL3P33DFLDVICZHLI46EEM73E" localhost:4000/v1/movies/1
{
    "error": "your user account doesn't have the necessary permissions to access this resource"
}

 

zzh@ZZHPC:~$ BODY='{"email": "ZhangZhihuiAAA@126.com", "password": "pa55word"}'
zzh@ZZHPC:~$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "RZKLILBMKFV3QJEEREALBHDBYE",
        "expiry": "2024-11-23T17:37:57.567581398+08:00"
    }
}
zzh@ZZHPC:~$ curl -X DELETE -H "Authorization: Bearer RZKLILBMKFV3QJEEREALBHDBYE" localhost:4000/v1/movies/1
{
    "message": "movie successfully deleted"
}

 

In permission.go:

// AddForUser adds the provided permissions for a specific user.
func (m PermissionModel) AddForUser(userID int64, codes ...string) error {
    query := `INSERT INTO user_permission 
              SELECT $1, id 
                FROM permission 
               WHERE code = ANY($2)`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    _, err := m.DB.Pool.Exec(ctx, query, userID, codes)
    return err
}

 

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Password string `json:"password"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    user := &data.User{
        Name:      input.Name,
        Email:     input.Email,
        Activated: false,
    }

    err = user.Password.Set(input.Password)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    v := validator.New()

    if data.ValidateUser(v, user); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Insert the user data into the database.
    err = app.models.User.Insert(user)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrDuplicateEmail):
            v.AddError("email", "a user with this email address already exists")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Add the "movie:read" permission for the new user.
    err = app.models.Permission.AddForUser(user.ID, "movie:read")
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // After the user record is created in the database, generate a new activation token
    // for the user.
    token, err := app.models.Token.New(user.ID, 3*24*time.Hour, data.ScopeActivation)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Send the welcome email in background.
    app.background(func() {
        data := map[string]any{
            "activationToken": token.Plaintext,
            "userID":          user.ID,
        }

        err = app.emailSender.Send(user.Email, "user_welcome.html", data)
        if err != nil {
            app.logger.Error(err.Error())
        }
    })

    err = app.writeJSON(w, http.StatusCreated, envelope{"user": user}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

 

Let’s check that this is working correctly by registering a brand-new user.

greenlight=> DELETE FROM users where id = 9;
DELETE 1
greenlight=> SELECT id, name, email FROM users;
 id |    name     |       email       
----+-------------+-------------------
  1 | Alice Smith | alice@example.com
(1 row)

greenlight=> SELECT * FROM user_permission;
 user_id | permission_id 
---------+---------------
       1 |             1
(1 row)

 

greenlight=> SELECT u.email, p.code
greenlight->   FROM users u
greenlight->  INNER JOIN user_permission up ON u.id = up.user_id
greenlight->  INNER JOIN permission p ON up.permission_id = p.id
greenlight->  WHERE u.email = 'ZhangZhihuiAAA@126.com';
         email          |    code    
------------------------+------------
 ZhangZhihuiAAA@126.com | movie:read
(1 row)

 

posted on 2024-11-22 16:09  ZhangZhihuiAAA  阅读(5)  评论(0编辑  收藏  举报