Skip to content

Commit 44a582d

Browse files
authored
Merge pull request #198 from LAA-Software-Engineering/feat/issue-117-book-ownership
feat: book ownership and JWT user_id (fix #117)
2 parents 09069f1 + f36df72 commit 44a582d

16 files changed

Lines changed: 339 additions & 73 deletions

docs/docs.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ const docTemplate = `{
257257
"type": "string"
258258
}
259259
},
260+
"403": {
261+
"description": "Forbidden",
262+
"schema": {
263+
"type": "string"
264+
}
265+
},
260266
"404": {
261267
"description": "book not found",
262268
"schema": {
@@ -307,6 +313,12 @@ const docTemplate = `{
307313
"type": "string"
308314
}
309315
},
316+
"403": {
317+
"description": "Forbidden",
318+
"schema": {
319+
"type": "string"
320+
}
321+
},
310322
"404": {
311323
"description": "book not found",
312324
"schema": {
@@ -450,6 +462,9 @@ const docTemplate = `{
450462
"id": {
451463
"type": "integer"
452464
},
465+
"owner_id": {
466+
"type": "integer"
467+
},
453468
"title": {
454469
"type": "string"
455470
},

docs/swagger.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@
251251
"type": "string"
252252
}
253253
},
254+
"403": {
255+
"description": "Forbidden",
256+
"schema": {
257+
"type": "string"
258+
}
259+
},
254260
"404": {
255261
"description": "book not found",
256262
"schema": {
@@ -301,6 +307,12 @@
301307
"type": "string"
302308
}
303309
},
310+
"403": {
311+
"description": "Forbidden",
312+
"schema": {
313+
"type": "string"
314+
}
315+
},
304316
"404": {
305317
"description": "book not found",
306318
"schema": {
@@ -444,6 +456,9 @@
444456
"id": {
445457
"type": "integer"
446458
},
459+
"owner_id": {
460+
"type": "integer"
461+
},
447462
"title": {
448463
"type": "string"
449464
},

docs/swagger.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ definitions:
88
type: string
99
id:
1010
type: integer
11+
owner_id:
12+
type: integer
1113
title:
1214
type: string
1315
updated_at:
@@ -168,6 +170,10 @@ paths:
168170
description: Unauthorized
169171
schema:
170172
type: string
173+
"403":
174+
description: Forbidden
175+
schema:
176+
type: string
171177
"404":
172178
description: book not found
173179
schema:
@@ -237,6 +243,10 @@ paths:
237243
description: Unauthorized
238244
schema:
239245
type: string
246+
"403":
247+
description: Forbidden
248+
schema:
249+
type: string
240250
"404":
241251
description: book not found
242252
schema:

pkg/api/book_routes_auth_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func testBookWriteMiddlewareStack(t *testing.T) *gin.Engine {
3636
func TestBookWriteRoutesRequireAPIKeyAndJWT(t *testing.T) {
3737
const apiKey = testAPISecretKey
3838

39-
token, err := auth.GenerateToken("book-writer")
39+
token, err := auth.GenerateToken("book-writer", 1)
4040
if err != nil {
4141
t.Fatalf("GenerateToken: %v", err)
4242
}
@@ -141,7 +141,7 @@ func TestBookWriteRoutesRequireAPIKeyAndJWT(t *testing.T) {
141141
func TestBookWriteRoutesRejectTamperedJWT(t *testing.T) {
142142
const apiKey = testAPISecretKey
143143

144-
token, err := auth.GenerateToken("u1")
144+
token, err := auth.GenerateToken("u1", 1)
145145
if err != nil {
146146
t.Fatalf("GenerateToken: %v", err)
147147
}
@@ -175,7 +175,7 @@ func TestBookWriteRoutesRejectTamperedJWT(t *testing.T) {
175175
func TestBookWriteRoutesConcurrentAuthorizedRequests(t *testing.T) {
176176
const apiKey = testAPISecretKey
177177

178-
token, err := auth.GenerateToken("concurrent-user")
178+
token, err := auth.GenerateToken("concurrent-user", 1)
179179
if err != nil {
180180
t.Fatalf("GenerateToken: %v", err)
181181
}

pkg/api/books.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"golang-rest-api-template/pkg/cache"
10+
"golang-rest-api-template/pkg/middleware"
1011
"golang-rest-api-template/pkg/models"
1112
"golang-rest-api-template/pkg/repository"
1213
"golang-rest-api-template/pkg/service"
@@ -80,6 +81,19 @@ func parseOffsetLimit(c *gin.Context) (offset, limit int, ok bool) {
8081
return o, l, true
8182
}
8283

84+
// contextUserID returns the authenticated users.id set by middleware.JWTAuth.
85+
func contextUserID(c *gin.Context) (uint, bool) {
86+
if c == nil {
87+
return 0, false
88+
}
89+
v, ok := c.Get(middleware.ContextUserID)
90+
if !ok {
91+
return 0, false
92+
}
93+
id, ok := v.(uint)
94+
return id, ok && id > 0
95+
}
96+
8397
// @BasePath /api/v1
8498

8599
// Healthcheck godoc
@@ -146,12 +160,17 @@ func (h *bookHandler) FindBooks(c *gin.Context) {
146160
// @Failure 500 {string} string "Internal Server Error"
147161
// @Router /books [post]
148162
func (h *bookHandler) CreateBook(c *gin.Context) {
163+
ownerID, ok := contextUserID(c)
164+
if !ok {
165+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
166+
return
167+
}
149168
var input models.CreateBook
150169
if err := c.ShouldBindJSON(&input); err != nil {
151170
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
152171
return
153172
}
154-
book, err := h.svc.CreateBook(c.Request.Context(), input.Title, input.Author)
173+
book, err := h.svc.CreateBook(c.Request.Context(), ownerID, input.Title, input.Author)
155174
if err != nil {
156175
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"})
157176
return
@@ -199,10 +218,16 @@ func (h *bookHandler) FindBook(c *gin.Context) {
199218
// @Success 200 {object} models.Book "Successfully updated book"
200219
// @Failure 400 {string} string "Bad Request"
201220
// @Failure 401 {string} string "Unauthorized"
221+
// @Failure 403 {string} string "Forbidden"
202222
// @Failure 404 {string} string "book not found"
203223
// @Failure 500 {string} string "Internal Server Error"
204224
// @Router /books/{id} [put]
205225
func (h *bookHandler) UpdateBook(c *gin.Context) {
226+
actorID, ok := contextUserID(c)
227+
if !ok {
228+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
229+
return
230+
}
206231
var input models.UpdateBook
207232
id, ok := parseIDParam(c)
208233
if !ok {
@@ -212,12 +237,16 @@ func (h *bookHandler) UpdateBook(c *gin.Context) {
212237
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
213238
return
214239
}
215-
book, err := h.svc.UpdateBook(c.Request.Context(), id, input.Title, input.Author)
240+
book, err := h.svc.UpdateBook(c.Request.Context(), actorID, id, input.Title, input.Author)
216241
if err != nil {
217242
if repository.IsBookNotFound(err) {
218243
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
219244
return
220245
}
246+
if errors.Is(err, service.ErrBookForbidden) {
247+
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
248+
return
249+
}
221250
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update book"})
222251
return
223252
}
@@ -234,19 +263,29 @@ func (h *bookHandler) UpdateBook(c *gin.Context) {
234263
// @Param id path string true "Book ID"
235264
// @Success 204 "Successfully deleted book"
236265
// @Failure 401 {string} string "Unauthorized"
266+
// @Failure 403 {string} string "Forbidden"
237267
// @Failure 404 {string} string "book not found"
238268
// @Failure 500 {string} string "Internal Server Error"
239269
// @Router /books/{id} [delete]
240270
func (h *bookHandler) DeleteBook(c *gin.Context) {
271+
actorID, ok := contextUserID(c)
272+
if !ok {
273+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
274+
return
275+
}
241276
id, ok := parseIDParam(c)
242277
if !ok {
243278
return
244279
}
245-
if err := h.svc.DeleteBook(c.Request.Context(), id); err != nil {
280+
if err := h.svc.DeleteBook(c.Request.Context(), actorID, id); err != nil {
246281
if repository.IsBookNotFound(err) {
247282
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
248283
return
249284
}
285+
if errors.Is(err, service.ErrBookForbidden) {
286+
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
287+
return
288+
}
250289
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete book"})
251290
return
252291
}

0 commit comments

Comments
 (0)