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 }