github.com/dhax/go-base@v0.0.0-20231004214136-8be7e5c1972b/api/app/account.go (about)

     1  package app
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/go-chi/chi/v5"
    11  	"github.com/go-chi/render"
    12  	validation "github.com/go-ozzo/ozzo-validation"
    13  
    14  	"github.com/dhax/go-base/auth/jwt"
    15  	"github.com/dhax/go-base/auth/pwdless"
    16  )
    17  
    18  // The list of error types returned from account resource.
    19  var (
    20  	ErrAccountValidation = errors.New("account validation error")
    21  )
    22  
    23  // AccountStore defines database operations for account.
    24  type AccountStore interface {
    25  	Get(id int) (*pwdless.Account, error)
    26  	Update(*pwdless.Account) error
    27  	Delete(*pwdless.Account) error
    28  	UpdateToken(*jwt.Token) error
    29  	DeleteToken(*jwt.Token) error
    30  }
    31  
    32  // AccountResource implements account management handler.
    33  type AccountResource struct {
    34  	Store AccountStore
    35  }
    36  
    37  // NewAccountResource creates and returns an account resource.
    38  func NewAccountResource(store AccountStore) *AccountResource {
    39  	return &AccountResource{
    40  		Store: store,
    41  	}
    42  }
    43  
    44  func (rs *AccountResource) router() *chi.Mux {
    45  	r := chi.NewRouter()
    46  	r.Use(rs.accountCtx)
    47  	r.Get("/", rs.get)
    48  	r.Put("/", rs.update)
    49  	r.Delete("/", rs.delete)
    50  	r.Route("/token/{tokenID}", func(r chi.Router) {
    51  		r.Put("/", rs.updateToken)
    52  		r.Delete("/", rs.deleteToken)
    53  	})
    54  	return r
    55  }
    56  
    57  func (rs *AccountResource) accountCtx(next http.Handler) http.Handler {
    58  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    59  		claims := jwt.ClaimsFromCtx(r.Context())
    60  		log(r).WithField("account_id", claims.ID)
    61  		account, err := rs.Store.Get(claims.ID)
    62  		if err != nil {
    63  			// account deleted while access token still valid
    64  			render.Render(w, r, ErrUnauthorized)
    65  			return
    66  		}
    67  		ctx := context.WithValue(r.Context(), ctxAccount, account)
    68  		next.ServeHTTP(w, r.WithContext(ctx))
    69  	})
    70  }
    71  
    72  type accountRequest struct {
    73  	*pwdless.Account
    74  	// override protected data here, although not really necessary here
    75  	// as we limit updated database columns in store as well
    76  	ProtectedID     int      `json:"id"`
    77  	ProtectedActive bool     `json:"active"`
    78  	ProtectedRoles  []string `json:"roles"`
    79  }
    80  
    81  func (d *accountRequest) Bind(r *http.Request) error {
    82  	// d.ProtectedActive = true
    83  	// d.ProtectedRoles = []string{}
    84  	return nil
    85  }
    86  
    87  type accountResponse struct {
    88  	*pwdless.Account
    89  }
    90  
    91  func newAccountResponse(a *pwdless.Account) *accountResponse {
    92  	resp := &accountResponse{Account: a}
    93  	return resp
    94  }
    95  
    96  func (rs *AccountResource) get(w http.ResponseWriter, r *http.Request) {
    97  	acc := r.Context().Value(ctxAccount).(*pwdless.Account)
    98  	render.Respond(w, r, newAccountResponse(acc))
    99  }
   100  
   101  func (rs *AccountResource) update(w http.ResponseWriter, r *http.Request) {
   102  	acc := r.Context().Value(ctxAccount).(*pwdless.Account)
   103  	data := &accountRequest{Account: acc}
   104  	if err := render.Bind(r, data); err != nil {
   105  		render.Render(w, r, ErrInvalidRequest(err))
   106  		return
   107  	}
   108  
   109  	if err := rs.Store.Update(acc); err != nil {
   110  		switch err.(type) {
   111  		case validation.Errors:
   112  			render.Render(w, r, ErrValidation(ErrAccountValidation, err.(validation.Errors)))
   113  			return
   114  		}
   115  		render.Render(w, r, ErrRender(err))
   116  		return
   117  	}
   118  
   119  	render.Respond(w, r, newAccountResponse(acc))
   120  }
   121  
   122  func (rs *AccountResource) delete(w http.ResponseWriter, r *http.Request) {
   123  	acc := r.Context().Value(ctxAccount).(*pwdless.Account)
   124  	if err := rs.Store.Delete(acc); err != nil {
   125  		render.Render(w, r, ErrRender(err))
   126  		return
   127  	}
   128  	render.Respond(w, r, http.NoBody)
   129  }
   130  
   131  type tokenRequest struct {
   132  	Identifier  string
   133  	ProtectedID int `json:"id"`
   134  }
   135  
   136  func (d *tokenRequest) Bind(r *http.Request) error {
   137  	d.Identifier = strings.TrimSpace(d.Identifier)
   138  	return nil
   139  }
   140  
   141  func (rs *AccountResource) updateToken(w http.ResponseWriter, r *http.Request) {
   142  	id, err := strconv.Atoi(chi.URLParam(r, "tokenID"))
   143  	if err != nil {
   144  		render.Render(w, r, ErrBadRequest)
   145  		return
   146  	}
   147  	data := &tokenRequest{}
   148  	if err := render.Bind(r, data); err != nil {
   149  		render.Render(w, r, ErrInvalidRequest(err))
   150  		return
   151  	}
   152  	acc := r.Context().Value(ctxAccount).(*pwdless.Account)
   153  	for _, t := range acc.Token {
   154  		if t.ID == id {
   155  			if err := rs.Store.UpdateToken(&jwt.Token{
   156  				ID:         t.ID,
   157  				Identifier: data.Identifier,
   158  			}); err != nil {
   159  				render.Render(w, r, ErrInvalidRequest(err))
   160  				return
   161  			}
   162  		}
   163  	}
   164  	render.Respond(w, r, http.NoBody)
   165  }
   166  
   167  func (rs *AccountResource) deleteToken(w http.ResponseWriter, r *http.Request) {
   168  	id, err := strconv.Atoi(chi.URLParam(r, "tokenID"))
   169  	if err != nil {
   170  		render.Render(w, r, ErrBadRequest)
   171  		return
   172  	}
   173  	acc := r.Context().Value(ctxAccount).(*pwdless.Account)
   174  	for _, t := range acc.Token {
   175  		if t.ID == id {
   176  			rs.Store.DeleteToken(&jwt.Token{ID: t.ID})
   177  		}
   178  	}
   179  	render.Respond(w, r, http.NoBody)
   180  }