diff --git a/backend/cmd/api/api.go b/backend/cmd/api/api.go index e09dd1d..4c0bacd 100644 --- a/backend/cmd/api/api.go +++ b/backend/cmd/api/api.go @@ -57,6 +57,9 @@ func (app *application) mount() http.Handler { r.Use(middleware.CleanPath) r.Use(middleware.Recoverer) + /// CSRF and SESSION TOKEN STUFF: + // Store session token in https or whatever only so js can't access it, and the store csrf locally using react or whatever, remove the cookie, and then include it in the http header under 'X-CSRF-Token' or as a hidden input in an html form + r.Use(middleware.Throttle(100)) // temporary or removable. throttles the whole thing to 1000 concurrent requests // r.Use(cors.Handler(cors.Options{ // AllowedOrigins: []string{env.GetString("CORS_ALLOWED_ORIGIN", "http://localhost:5174")}, @@ -96,7 +99,7 @@ func (app *application) mount() http.Handler { r.Route("/user", func(r chi.Router) { r.Route("/{userID}", func(r chi.Router) { - r.Use(app.AuthSessionMiddleware, app.CSRFCheckMiddleware, app.CheckUserMatchingMiddleware) + r.Use(app.AuthSessionMiddleware, app.CSRFCheckMiddleware, app.CheckUserMatchingMiddleware) // consider the actual use of the user matching middleware and whether an admin should be used instead r.Get("/", app.getUserHandler) @@ -104,16 +107,20 @@ func (app *application) mount() http.Handler { r.Get("/", app.getUsersGroupsHandler) r.Route("/{groupID}", func(r chi.Router) { + r.Get("/", app.getUsersGroupHandler) r.Delete("/", app.removeUserGroupHandler) // maybe this should expect authentication headers to reverify the password when deleting a group you own. - r.Put("/moderator", app.addGroupModeratorHandler) + r.Put("/owner", app.setGroupOwnerHandler) + r.Get("/owner", app.getGroupOwnerHandler) + + r.Post("/moderator", app.addGroupModeratorHandler) r.Delete("/moderator/{secondaryuserID}", app.removeModeratorPriviligesHandler) r.Get("/users", app.getGroupUsersHandler) - r.Delete("/users/{secondaryuserID}", app.removeUserFromGroupHandler) + r.Post("/users", app.addGroupUserHandler) // needs to create a new user/do the whole invite thing if they don't already exist + r.Delete("/users/{secondaryuserID}", app.removeUserFromGroupHandler) // needs to check if the user is an owner/moderator - r.Put("/owner", app.setGroupOwnerHandler) }) }) @@ -130,6 +137,7 @@ func (app *application) mount() http.Handler { r.Get("/", app.getReceiptImageHandler) r.Put("/", app.changeReceiptImageHandler) r.Delete("/", app.deleteReceiptImageHandler) + r.Post("/confirm", app.confirmImageCreationHandler) }) }) }) @@ -140,7 +148,7 @@ func (app *application) mount() http.Handler { }) - r.Use(app.CSRFCheckMiddleware) + // r.Use(app.CSRFCheckMiddleware) r.Group(func(r chi.Router) { r.Use(app.AuthSessionMiddleware) diff --git a/backend/cmd/api/auth.go b/backend/cmd/api/auth.go index 6905b03..59c4ec6 100644 --- a/backend/cmd/api/auth.go +++ b/backend/cmd/api/auth.go @@ -89,3 +89,5 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque func (app *application) refreshTokenHandler(w http.ResponseWriter, r *http.Request) { } + +// func (app *application) diff --git a/backend/cmd/api/groups.go b/backend/cmd/api/groups.go new file mode 100644 index 0000000..b46e6e3 --- /dev/null +++ b/backend/cmd/api/groups.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "net/http" + "strconv" + + "git.ewellenr.ca/receipt_indexer/backend/internal/storage" + "github.com/go-chi/chi/v5" +) + +func (app *application) getUserGroups(ctx context.Context, userID int64) (*storage.UserGroups, error) { + if !app.config.redisCfg.enabled { + return app.store.Groups.GetUserGroups(ctx, userID) + } + + usergroups, err := app.cacheStorage.UserGroups.Get(ctx, userID) + if err != nil { + return nil, err + } + + if usergroups == nil { + usergroups, err = app.store.Groups.GetUserGroups(ctx, userID) + if err != nil { + return nil, err + } + + if err := app.cacheStorage.UserGroups.Set(ctx, usergroups); err != nil { + return nil, err + } + } + + return usergroups, nil +} + +func (app *application) getGroup(ctx context.Context, groupID int64) (*storage.Group, error) { + if !app.config.redisCfg.enabled { + return app.store.Groups.GetByID(ctx, groupID) + + } + + group, err := app.cacheStorage.Groups.Get(ctx, groupID) + if err != nil { + return nil, err + } + + if group == nil { + group, err = app.store.Groups.GetByID(ctx, groupID) + if err != nil { + return nil, err + } + + if err := app.cacheStorage.Groups.Set(ctx, group); err != nil { + return nil, err + } + } + + return group, nil +} + +func (app *application) getUsersGroupsHandler(w http.ResponseWriter, r *http.Request) { + user, err := app.getUserFromURL(w, r) + if err != nil { // assume that the writing was already handled + return + } + + usergroups, err := app.getUserGroups(r.Context(), user.ID) + if err != nil { + switch err { + case storage.ErrNotFound: + app.notFoundResponse(w, r, err) + return + default: + app.internalServerError(w, r, err) + return + } + } + + if err := app.jsonResponse(w, http.StatusOK, usergroups); err != nil { + app.internalServerError(w, r, err) + } +} + +func (app *application) getUsersGroupHandler(w http.ResponseWriter, r *http.Request) { + groupID, err := strconv.ParseInt(chi.URLParam(r, "groupID"), 10, 64) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + + group, err := app.getGroup(r.Context(), groupID) + if err != nil { + switch err { + case storage.ErrNotFound: + app.notFoundResponse(w, r, err) + return + default: + app.internalServerError(w, r, err) + return + } + } + + if err := app.jsonResponse(w, http.StatusOK, group); err != nil { + app.internalServerError(w, r, err) + } +} diff --git a/backend/cmd/api/json.go b/backend/cmd/api/json.go index c6d93fb..382cd68 100644 --- a/backend/cmd/api/json.go +++ b/backend/cmd/api/json.go @@ -5,6 +5,8 @@ import ( "net/http" ) +// copied from https://github.com/sikozonpc/GopherSocial/blob/main/cmd/api/json.go + func writeJSON(w http.ResponseWriter, status int, data any) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) @@ -18,3 +20,21 @@ func writeJSONError(w http.ResponseWriter, status int, message string) error { return writeJSON(w, status, &envelope{Error: message}) } + +func readJSON(w http.ResponseWriter, r *http.Request, data any) error { + maxBytes := 1_048_578 // 1mb + r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) + + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + return decoder.Decode(data) +} + +func (app *application) jsonResponse(w http.ResponseWriter, status int, data any) error { + type envelope struct { + Data any `json:"data"` + } + + return writeJSON(w, status, &envelope{Data: data}) +} diff --git a/backend/cmd/api/users.go b/backend/cmd/api/users.go index 861c7ed..eba84b1 100644 --- a/backend/cmd/api/users.go +++ b/backend/cmd/api/users.go @@ -3,16 +3,47 @@ package main import ( "context" "net/http" + "strconv" - auth_storage "git.ewellenr.ca/receipt_indexer/backend/internal/storage/auth" + "git.ewellenr.ca/receipt_indexer/backend/internal/storage" + "github.com/go-chi/chi/v5" ) type userKey string const userCtx userKey = "user" +func (app *application) getUserFromURL(w http.ResponseWriter, r *http.Request) (*storage.User, error) { + userID, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64) + if err != nil { + app.badRequestResponse(w, r, err) + return nil, err + } + + user, err := app.getUser(r.Context(), userID) + if err != nil { + switch err { + case storage.ErrNotFound: + app.notFoundResponse(w, r, err) + return nil, err + default: + app.internalServerError(w, r, err) + return nil, err + } + } + return user, nil +} + func (app *application) getUserHandler(w http.ResponseWriter, r *http.Request) { + user, err := app.getUserFromURL(w, r) + if err != nil { // assume that the writing was already handled + return + } + + if err := app.jsonResponse(w, http.StatusOK, user); err != nil { + app.internalServerError(w, r, err) + } } func (app *application) getUsersHandler(w http.ResponseWriter, r *http.Request) { @@ -20,7 +51,7 @@ func (app *application) getUsersHandler(w http.ResponseWriter, r *http.Request) func (app *application) deleteUserHandler(w http.ResponseWriter, r *http.Request) { } -func (app *application) getUser(ctx context.Context, userID int64) (*auth_storage.User, error) { +func (app *application) getUser(ctx context.Context, userID int64) (*storage.User, error) { if !app.config.redisCfg.enabled { return app.auth.Users.GetByID(ctx, userID) } @@ -44,7 +75,7 @@ func (app *application) getUser(ctx context.Context, userID int64) (*auth_storag return user, nil } -func getUserFromContext(r *http.Request) *auth_storage.User { - user, _ := r.Context().Value(userCtx).(*auth_storage.User) +func getUserFromContext(r *http.Request) *storage.User { + user, _ := r.Context().Value(userCtx).(*storage.User) return user }