Skip to content

Commit 1c743b3

Browse files
authored
feat: add OAuth2 Password endpoint requiring two scopes (#35)
* gen-1751: add endpoint requiring two scopes * gen-1751: allow reusing token in clientCredentials tests
1 parent 22340d6 commit 1c743b3

File tree

4 files changed

+75
-8
lines changed

4 files changed

+75
-8
lines changed

cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func main() {
9595
oauth2router.HandleFunc("/ecommerce/products", ecommerce.HandleListProducts).Methods(http.MethodGet)
9696
oauth2router.HandleFunc("/ecommerce/products", ecommerce.HandleCreateProduct).Methods(http.MethodPost)
9797
oauth2router.HandleFunc("/ecommerce/products/{id}", ecommerce.HandleFetchProduct).Methods(http.MethodGet)
98+
oauth2router.HandleFunc("/ecommerce/products/{id}", ecommerce.HandleUpdateProduct).Methods(http.MethodPut)
9899
oauth2router.HandleFunc("/ecommerce/products/{id}", ecommerce.HandleDeleteProduct).Methods(http.MethodDelete)
99100
oauth2router.HandleFunc("/ecommerce/products/{id}/inventory", ecommerce.HandleUpdateProductStock).Methods(http.MethodPut)
100101

internal/clientcredentials/service.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ func handleBasicAuth(authHeader string) (clientID, clientSecret string, ok bool)
3838
return creds[0], creds[1], true
3939
}
4040

41-
4241
func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
4342
var clientID, clientSecret string
4443
err := r.ParseForm()
@@ -65,8 +64,6 @@ func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
6564
return
6665
}
6766

68-
69-
7067
if clientID == "" || clientSecret == "" {
7168
http.Error(w, "invalid_request", http.StatusBadRequest)
7269
return
@@ -112,7 +109,7 @@ func HandleTokenRequest(w http.ResponseWriter, r *http.Request) {
112109
response := tokenResponse{
113110
AccessToken: accessToken,
114111
TokenType: tokenType,
115-
ExpiresIn: 0,
112+
ExpiresIn: 120,
116113
}
117114

118115
if err := json.NewEncoder(w).Encode(response); err != nil {

internal/ecommerce/models.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ type NewProductForm struct {
2222
Price float64 `json:"price"`
2323
}
2424

25-
type ProductForm struct {
26-
Name string `json:"name"`
27-
Description string `json:"description"`
28-
Price float64 `json:"price"`
25+
type ProductUpdateForm struct {
26+
Name *string `json:"name"`
27+
Description *string `json:"description"`
28+
Price *float64 `json:"price"`
2929
}
3030

3131
type ProductInventoryUpdateForm struct {

internal/ecommerce/service.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,75 @@ func HandleFetchProduct(rw http.ResponseWriter, r *http.Request) {
177177
}
178178
}
179179

180+
func HandleUpdateProduct(rw http.ResponseWriter, r *http.Request) {
181+
rw.Header().Set("Content-Type", "application/json")
182+
183+
scopes, scopesFound := middleware.OAuth2Scopes(r)
184+
if !scopesFound || !scopes.Has([]string{"products:read", "products:create"}) {
185+
http.Error(rw, `{"error": "insufficient scopes"}`, http.StatusForbidden)
186+
return
187+
}
188+
189+
enc := json.NewEncoder(rw)
190+
enc.SetIndent("", " ")
191+
192+
vars := mux.Vars(r)
193+
rawID, ok := vars["id"]
194+
if !ok {
195+
http.Error(rw, `{"error": "{id} is required"}`, http.StatusBadRequest)
196+
return
197+
}
198+
199+
productID, err := strconv.ParseUint(rawID, 10, 64)
200+
if err != nil {
201+
http.Error(rw, `{"error": "{id} must a uint64"}`, http.StatusBadRequest)
202+
return
203+
}
204+
205+
var form ProductUpdateForm
206+
err = json.NewDecoder(r.Body).Decode(&form)
207+
if err != nil {
208+
http.Error(rw, `{"error": "could not decode request"}`, http.StatusBadRequest)
209+
return
210+
}
211+
212+
// Seed cannot be 0 otherwise faker picks a random one
213+
faker := gofakeit.New(productID + 1)
214+
p := faker.Product()
215+
created := faker.DateRange(
216+
time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
217+
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
218+
).Truncate(24 * time.Hour)
219+
updated := faker.DateRange(
220+
created,
221+
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
222+
).Truncate(24 * time.Hour)
223+
224+
name := p.Name
225+
if form.Name != nil {
226+
name = *form.Name
227+
}
228+
description := p.Description
229+
if form.Description != nil {
230+
description = *form.Description
231+
}
232+
price := p.Price
233+
if form.Price != nil {
234+
price = *form.Price
235+
}
236+
237+
if err := enc.Encode(Product{
238+
ID: rawID,
239+
Name: name,
240+
Price: price,
241+
Description: description,
242+
CreatedAt: created,
243+
UpdatedAt: updated,
244+
}); err != nil {
245+
http.Error(rw, `{"error": "could not encode response"}`, http.StatusInternalServerError)
246+
}
247+
}
248+
180249
func HandleDeleteProduct(rw http.ResponseWriter, r *http.Request) {
181250
scopes, scopesFound := middleware.OAuth2Scopes(r)
182251
if !scopesFound || !scopes.Has([]string{"products:delete"}) {

0 commit comments

Comments
 (0)