Skip to content

Commit d423fed

Browse files
authored
Feature/codegen server apis (#309)
* WIP to codegen the rest APIs * It works! * Delete obsolete code * Cleanup * Restore loading functionality * Fix interceptors * Remove extraneous checks * Small cleanup * Removed unused component * More cleanup * Remove unused route * Rating is a separate route * Removed unused method
1 parent 989159c commit d423fed

File tree

31 files changed

+485
-723
lines changed

31 files changed

+485
-723
lines changed

Makefile

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,23 @@ uninstall:
4343

4444

4545
# ---- CODEGEN ----
46-
$(CLIENT_CODEGEN_DIR): $(CLIENT_INSTALL_DIR) openapi.yaml
46+
$(CLIENT_CODEGEN_DIR): $(CLIENT_INSTALL_DIR) openapi.yaml models.yaml
4747
cd static && npm run codegen
4848

49-
$(CODEGEN_DIR): openapi.yaml
49+
$(CODEGEN_DIR): openapi.yaml models.yaml
5050
rm -rf $@
5151
mkdir -p $@/models
5252
oapi-codegen -generate types,skip-prune -package models models.yaml > $@/models/models.go
53-
mkdir -p $@/api
54-
oapi-codegen -generate types -package api -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/api.go
53+
mkdir -p $@/api/public
54+
oapi-codegen -generate types,chi-server -package public -include-tags=public -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/public/public.go
55+
mkdir -p $@/api/viewer
56+
oapi-codegen -generate types,chi-server -package viewer -include-tags=viewer -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/viewer/viewer.go
57+
mkdir -p $@/api/editor
58+
oapi-codegen -generate types,chi-server -package editor -include-tags=editor -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/editor/editor.go
59+
mkdir -p $@/api/admin
60+
oapi-codegen -generate types,chi-server -package admin -include-tags=admin -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/admin/admin.go
61+
mkdir -p $@/api/adminOrSelf
62+
oapi-codegen -generate types,chi-server -package adminOrSelf -include-tags=adminOrSelf -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/adminOrSelf/adminOrSelf.go
5563

5664

5765
# ---- LINT ----

api/api.go

Lines changed: 11 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import (
66
"fmt"
77
"log"
88
"net/http"
9-
"net/url"
10-
"strconv"
119

1210
"github.com/chadweimer/gomp/conf"
1311
"github.com/chadweimer/gomp/db"
12+
"github.com/chadweimer/gomp/generated/api/admin"
13+
"github.com/chadweimer/gomp/generated/api/adminOrSelf"
14+
"github.com/chadweimer/gomp/generated/api/editor"
15+
"github.com/chadweimer/gomp/generated/api/public"
16+
"github.com/chadweimer/gomp/generated/api/viewer"
1417
"github.com/chadweimer/gomp/upload"
1518
"github.com/go-chi/chi/v5"
1619
"github.com/go-chi/chi/v5/middleware"
@@ -22,21 +25,6 @@ var errMismatchedId = errors.New("id in the path does not match the one specifie
2225

2326
// ---- End Standard Errors ----
2427

25-
// ---- Begin Route Keys ----
26-
27-
type routeKey string
28-
29-
const (
30-
destRecipeIdKey routeKey = "destRecipeId"
31-
filterIdKey routeKey = "filterId"
32-
imageIdKey routeKey = "imageId"
33-
noteIdKey routeKey = "noteId"
34-
recipeIdKey routeKey = "recipeId"
35-
userIdKey routeKey = "userId"
36-
)
37-
38-
// ---- End Route Keys ----
39-
4028
// ---- Begin Context Keys ----
4129

4230
type contextKey struct {
@@ -72,69 +60,20 @@ func NewHandler(cfg *conf.Config, upl upload.Driver, db db.Driver) http.Handler
7260
r.Use(middleware.SetHeader("Content-Type", "application/json"))
7361
r.Route("/v1", func(r chi.Router) {
7462
// Public
75-
r.Get("/app/info", h.getAppInfo)
76-
r.Get("/app/configuration", h.getAppConfiguration)
77-
r.Post("/auth", h.postAuthenticate)
63+
public.HandlerFromMux(h, r)
7864
r.NotFound(h.notFound)
7965

80-
// Authenticated
8166
r.Group(func(r chi.Router) {
8267
r.Use(h.requireAuthentication)
8368

84-
r.Get("/recipes", h.getRecipes)
85-
r.Get(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.getRecipe)
86-
r.Get(fmt.Sprintf("/recipes/{%s}/image", recipeIdKey), h.getRecipeMainImage)
87-
r.Get(fmt.Sprintf("/recipes/{%s}/images", recipeIdKey), h.getRecipeImages)
88-
r.Get(fmt.Sprintf("/recipes/{%s}/notes", recipeIdKey), h.getRecipeNotes)
89-
r.Get(fmt.Sprintf("/recipes/{%s}/links", recipeIdKey), h.getRecipeLinks)
90-
69+
// Viewer
70+
viewer.HandlerFromMux(h, r)
9171
// Editor
92-
r.Group(func(r chi.Router) {
93-
r.Use(h.requireEditor)
94-
95-
r.Post("/recipes", h.postRecipe)
96-
r.Put(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.putRecipe)
97-
r.Delete(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.deleteRecipe)
98-
r.Put(fmt.Sprintf("/recipes/{%s}/state", recipeIdKey), h.putRecipeState)
99-
r.Put(fmt.Sprintf("/recipes/{%s}/rating", recipeIdKey), h.putRecipeRating)
100-
r.Put(fmt.Sprintf("/recipes/{%s}/image", recipeIdKey), h.putRecipeMainImage)
101-
r.Post(fmt.Sprintf("/recipes/{%s}/images", recipeIdKey), h.postRecipeImage)
102-
r.Put(fmt.Sprintf("/recipes/{%s}/links/{%s}", recipeIdKey, destRecipeIdKey), h.putRecipeLink)
103-
r.Delete(fmt.Sprintf("/recipes/{%s}/links/{%s}", recipeIdKey, destRecipeIdKey), h.deleteRecipeLink)
104-
r.Delete(fmt.Sprintf("/recipes/{%s}/images/{%s}", recipeIdKey, imageIdKey), h.deleteImage)
105-
r.Post(fmt.Sprintf("/recipes/{%s}/notes", recipeIdKey), h.postNote)
106-
r.Put(fmt.Sprintf("/recipes/{%s}/notes/{%s}", recipeIdKey, noteIdKey), h.putNote)
107-
r.Delete(fmt.Sprintf("/recipes/{%s}/notes/{%s}", recipeIdKey, noteIdKey), h.deleteNote)
108-
r.Post("/uploads", h.postUpload)
109-
})
110-
72+
editor.HandlerFromMux(h, r.With(h.requireEditor))
11173
// Admin
112-
r.Group(func(r chi.Router) {
113-
r.Use(h.requireAdmin)
114-
115-
r.Put("/app/configuration", h.putAppConfiguration)
116-
r.Get("/users", h.getUsers)
117-
r.Post("/users", h.postUser)
118-
119-
// Don't allow deleting self
120-
r.With(h.disallowSelf).Delete(fmt.Sprintf("/users/{%s}", userIdKey), h.deleteUser)
121-
})
122-
74+
admin.HandlerFromMux(h, r.With(h.requireAdmin))
12375
// Admin or Self
124-
r.Group(func(r chi.Router) {
125-
r.Use(h.requireAdminUnlessSelf)
126-
127-
r.Get(fmt.Sprintf("/users/{%s}", userIdKey), h.getUser)
128-
r.Put(fmt.Sprintf("/users/{%s}", userIdKey), h.putUser)
129-
r.Put(fmt.Sprintf("/users/{%s}/password", userIdKey), h.putUserPassword)
130-
r.Get(fmt.Sprintf("/users/{%s}/settings", userIdKey), h.getUserSettings)
131-
r.Put(fmt.Sprintf("/users/{%s}/settings", userIdKey), h.putUserSettings)
132-
r.Get(fmt.Sprintf("/users/{%s}/filters", userIdKey), h.getUserFilters)
133-
r.Post(fmt.Sprintf("/users/{%s}/filters", userIdKey), h.postUserFilter)
134-
r.Get(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.getUserFilter)
135-
r.Put(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.putUserFilter)
136-
r.Delete(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.deleteUserFilter)
137-
})
76+
adminOrSelf.HandlerFromMux(h, r.With(h.requireAdminUnlessSelf))
13877
})
13978
})
14079

@@ -176,45 +115,10 @@ func (h *apiHandler) notFound(resp http.ResponseWriter, req *http.Request) {
176115
h.Error(resp, http.StatusNotFound, fmt.Errorf("%s is not a valid API endpoint", req.URL.Path))
177116
}
178117

179-
func getParam(values url.Values, key string) string {
180-
val, _ := url.QueryUnescape(values.Get(key))
181-
return val
182-
}
183-
184-
func getParams(values url.Values, key string) []string {
185-
var vals []string
186-
if rawVals, ok := values[key]; ok {
187-
for _, rawVal := range rawVals {
188-
safeVal, err := url.QueryUnescape(rawVal)
189-
if err == nil && safeVal != "" {
190-
vals = append(vals, safeVal)
191-
}
192-
}
193-
}
194-
195-
return vals
196-
}
197-
198118
func readJSONFromRequest(req *http.Request, data interface{}) error {
199119
return json.NewDecoder(req.Body).Decode(data)
200120
}
201121

202-
func getResourceIdFromUrl(req *http.Request, idKey routeKey) (int64, error) {
203-
idStr := chi.URLParam(req, string(idKey))
204-
205-
// Special case for userId
206-
if idKey == userIdKey && idStr == "current" {
207-
return getResourceIdFromCtx(req, currentUserIdCtxKey)
208-
}
209-
210-
id, err := strconv.ParseInt(idStr, 10, 64)
211-
if err != nil {
212-
return 0, fmt.Errorf("failed to parse %s from URL, value = %s: %v", idKey, idStr, err)
213-
}
214-
215-
return id, nil
216-
}
217-
218122
func getResourceIdFromCtx(req *http.Request, idKey *contextKey) (int64, error) {
219123
idVal := req.Context().Value(idKey)
220124

api/app.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import (
88
"github.com/chadweimer/gomp/metadata"
99
)
1010

11-
func (h *apiHandler) getAppInfo(resp http.ResponseWriter, req *http.Request) {
11+
func (h apiHandler) GetInfo(resp http.ResponseWriter, req *http.Request) {
1212
info := models.AppInfo{
1313
Version: metadata.BuildVersion,
1414
}
1515

1616
h.OK(resp, info)
1717
}
1818

19-
func (h *apiHandler) getAppConfiguration(resp http.ResponseWriter, req *http.Request) {
19+
func (h apiHandler) GetConfiguration(resp http.ResponseWriter, req *http.Request) {
2020
cfg, err := h.db.AppConfiguration().Read()
2121
if err != nil {
2222
fullErr := fmt.Errorf("reading application configuration: %v", err)
@@ -27,7 +27,7 @@ func (h *apiHandler) getAppConfiguration(resp http.ResponseWriter, req *http.Req
2727
h.OK(resp, cfg)
2828
}
2929

30-
func (h *apiHandler) putAppConfiguration(resp http.ResponseWriter, req *http.Request) {
30+
func (h apiHandler) SaveConfiguration(resp http.ResponseWriter, req *http.Request) {
3131
var cfg models.AppConfiguration
3232
if err := readJSONFromRequest(req, &cfg); err != nil {
3333
h.Error(resp, http.StatusBadRequest, err)

api/authentication.go

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import (
1111
"time"
1212

1313
"github.com/chadweimer/gomp/db"
14-
gen "github.com/chadweimer/gomp/generated/api"
14+
"github.com/chadweimer/gomp/generated/api/public"
1515
"github.com/chadweimer/gomp/generated/models"
16+
"github.com/go-chi/chi/v5"
1617
"github.com/golang-jwt/jwt/v4"
1718
)
1819

19-
func (h *apiHandler) postAuthenticate(resp http.ResponseWriter, req *http.Request) {
20-
var credentials gen.Credentials
20+
func (h apiHandler) Authenticate(resp http.ResponseWriter, req *http.Request) {
21+
var credentials public.Credentials
2122
if err := readJSONFromRequest(req, &credentials); err != nil {
2223
h.Error(resp, http.StatusBadRequest, err)
2324
return
@@ -40,10 +41,10 @@ func (h *apiHandler) postAuthenticate(resp http.ResponseWriter, req *http.Reques
4041
h.Error(resp, http.StatusInternalServerError, err)
4142
}
4243

43-
h.OK(resp, gen.AuthenticationResponse{Token: tokenStr, User: *user})
44+
h.OK(resp, public.AuthenticationResponse{Token: tokenStr, User: *user})
4445
}
4546

46-
func (h *apiHandler) requireAuthentication(next http.Handler) http.Handler {
47+
func (h apiHandler) requireAuthentication(next http.Handler) http.Handler {
4748
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
4849
token, err := h.getAuthTokenFromRequest(req)
4950
if err != nil {
@@ -75,7 +76,7 @@ func (h *apiHandler) requireAuthentication(next http.Handler) http.Handler {
7576
})
7677
}
7778

78-
func (h *apiHandler) requireAdmin(next http.Handler) http.Handler {
79+
func (h apiHandler) requireAdmin(next http.Handler) http.Handler {
7980
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
8081
if err := h.verifyUserIsAdmin(req); err != nil {
8182
h.Error(resp, http.StatusForbidden, err)
@@ -86,9 +87,9 @@ func (h *apiHandler) requireAdmin(next http.Handler) http.Handler {
8687
})
8788
}
8889

89-
func (h *apiHandler) requireAdminUnlessSelf(next http.Handler) http.Handler {
90+
func (h apiHandler) requireAdminUnlessSelf(next http.Handler) http.Handler {
9091
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
91-
urlId, err := getResourceIdFromUrl(req, userIdKey)
92+
urlId, err := getUserIdFromUrl(req)
9293
if err != nil {
9394
h.Error(resp, http.StatusBadRequest, err)
9495
return
@@ -111,31 +112,7 @@ func (h *apiHandler) requireAdminUnlessSelf(next http.Handler) http.Handler {
111112
})
112113
}
113114

114-
func (h *apiHandler) disallowSelf(next http.Handler) http.Handler {
115-
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
116-
urlId, err := getResourceIdFromUrl(req, userIdKey)
117-
if err != nil {
118-
h.Error(resp, http.StatusBadRequest, err)
119-
return
120-
}
121-
ctxId, err := getResourceIdFromCtx(req, currentUserIdCtxKey)
122-
if err != nil {
123-
h.Error(resp, http.StatusUnauthorized, err)
124-
return
125-
}
126-
127-
// Don't allow operating on the current user (e.g., for deleting)
128-
if urlId == ctxId {
129-
err := fmt.Errorf("endpoint '%s' disallowed on current user", req.URL.Path)
130-
h.Error(resp, http.StatusForbidden, err)
131-
return
132-
}
133-
134-
next.ServeHTTP(resp, req)
135-
})
136-
}
137-
138-
func (h *apiHandler) requireEditor(next http.Handler) http.Handler {
115+
func (h apiHandler) requireEditor(next http.Handler) http.Handler {
139116
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
140117
if err := h.verifyUserIsEditor(req); err != nil {
141118
h.Error(resp, http.StatusForbidden, err)
@@ -146,7 +123,7 @@ func (h *apiHandler) requireEditor(next http.Handler) http.Handler {
146123
})
147124
}
148125

149-
func (h *apiHandler) getAuthTokenFromRequest(req *http.Request) (*jwt.Token, error) {
126+
func (h apiHandler) getAuthTokenFromRequest(req *http.Request) (*jwt.Token, error) {
150127
authHeader := req.Header.Get("Authorization")
151128
if authHeader == "" {
152129
return nil, errors.New("authorization header missing")
@@ -181,7 +158,7 @@ func (h *apiHandler) getAuthTokenFromRequest(req *http.Request) (*jwt.Token, err
181158
return nil, errors.New("invalid token")
182159
}
183160

184-
func (h *apiHandler) getUserIdFromToken(token *jwt.Token) (int64, error) {
161+
func (h apiHandler) getUserIdFromToken(token *jwt.Token) (int64, error) {
185162
claims := token.Claims.(*jwt.RegisteredClaims)
186163
userId, err := strconv.ParseInt(claims.Subject, 10, 64)
187164
if err != nil {
@@ -192,7 +169,7 @@ func (h *apiHandler) getUserIdFromToken(token *jwt.Token) (int64, error) {
192169
return userId, nil
193170
}
194171

195-
func (h *apiHandler) verifyUserExists(userId int64) (*models.User, error) {
172+
func (h apiHandler) verifyUserExists(userId int64) (*models.User, error) {
196173
// Verify this is a valid user in the DB
197174
user, err := h.db.Users().Read(userId)
198175
if err != nil {
@@ -207,7 +184,7 @@ func (h *apiHandler) verifyUserExists(userId int64) (*models.User, error) {
207184
return &user.User, nil
208185
}
209186

210-
func (h *apiHandler) verifyUserIsAdmin(req *http.Request) error {
187+
func (h apiHandler) verifyUserIsAdmin(req *http.Request) error {
211188
accessLevel := req.Context().Value(currentUserAccessLevelCtxKey).(models.AccessLevel)
212189
if accessLevel != models.AccessLevelAdmin {
213190
return fmt.Errorf("endpoint '%s' requires admin rights", req.URL.Path)
@@ -216,11 +193,27 @@ func (h *apiHandler) verifyUserIsAdmin(req *http.Request) error {
216193
return nil
217194
}
218195

219-
func (h *apiHandler) verifyUserIsEditor(req *http.Request) error {
196+
func (h apiHandler) verifyUserIsEditor(req *http.Request) error {
220197
accessLevel := req.Context().Value(currentUserAccessLevelCtxKey).(models.AccessLevel)
221198
if accessLevel != models.AccessLevelAdmin && accessLevel != models.AccessLevelEditor {
222199
return fmt.Errorf("endpoint '%s' requires edit rights", req.URL.Path)
223200
}
224201

225202
return nil
226203
}
204+
205+
func getUserIdFromUrl(req *http.Request) (int64, error) {
206+
idStr := chi.URLParam(req, "userId")
207+
208+
// Assume current user if not in the route
209+
if idStr == "" {
210+
return getResourceIdFromCtx(req, currentUserIdCtxKey)
211+
}
212+
213+
id, err := strconv.ParseInt(idStr, 10, 64)
214+
if err != nil {
215+
return 0, fmt.Errorf("failed to parse user id from URL, value = %s: %v", idStr, err)
216+
}
217+
218+
return id, nil
219+
}

0 commit comments

Comments
 (0)