diff --git a/cmd/server.go b/cmd/server.go index 4b5fc08..ba68fa1 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -4,13 +4,11 @@ Copyright © 2024 Lukas Werner package cmd import ( - "database/sql" - "encoding/json" "fmt" "log" "net/http" - "strings" + mark_http "github.com/lukasmwerner/mark/http" "github.com/lukasmwerner/mark/store" "github.com/spf13/cobra" ) @@ -27,116 +25,12 @@ var serverCmd = &cobra.Command{ return } - http.Handle("GET /api/bookmarks/search", AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("q") - if query == "" { - http.Error(w, "Missing query parameter", http.StatusBadRequest) - return - } - bookmarks, err := store.SearchBookmarks(db, query) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - jsonBytes, err := json.Marshal(bookmarks) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(jsonBytes) - }))) - - http.Handle("POST /api/bookmarks", AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - var bookmark store.Bookmark - if err := json.NewDecoder(r.Body).Decode(&bookmark); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - id, err := store.InsertBookmark(db, bookmark) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - w.Write(fmt.Appendf([]byte{}, `{"id": %d}`, id)) - }))) - - http.Handle("PATCH /api/bookmarks", AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var submittedBookmark store.Bookmark - if err := json.NewDecoder(r.Body).Decode(&submittedBookmark); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - originalBookmarkUrl := r.URL.Query().Get("url") - - var originalBookmark store.Bookmark - originalBookmark.Url = originalBookmarkUrl - - if err := store.UpdateBookmark(db, originalBookmark, submittedBookmark); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) - }))) - - http.Handle("GET /api/bookmarks", AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - url := r.URL.Query().Get("url") - - bookmarks, err := store.GetBookmark(db, url) - if err == sql.ErrNoRows { - http.Error(w, "Bookmark not found", http.StatusNotFound) - return - } - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(bookmarks) - }))) + mark_http.RegisterRoutes(db, http.DefaultServeMux) log.Fatal(http.ListenAndServe(":1990", nil)) }, } -func AuthRequired(db *store.DB, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - bearer := r.Header.Get("Authorization") - if bearer == "" { - http.Error(w, "Missing Authorization header", http.StatusUnauthorized) - return - } - - token := strings.TrimPrefix(bearer, "Bearer ") - - if token == "" { - http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) - return - } - - exists, err := store.HasKey(db, token) - if err != nil { - fmt.Println(err) - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - if exists { - next.ServeHTTP(w, r) - } else { - fmt.Println("Unauthorized, token not found: ", token[:10]) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - } - }) -} - func init() { rootCmd.AddCommand(serverCmd) diff --git a/http/api.go b/http/api.go new file mode 100644 index 0000000..cdbfe55 --- /dev/null +++ b/http/api.go @@ -0,0 +1,106 @@ +package http + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + + "github.com/lukasmwerner/mark/store" +) + +func SearchBookmarksHandler(db *store.DB) http.Handler { + return AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Missing query parameter", http.StatusBadRequest) + return + } + bookmarks, err := store.SearchBookmarks(db, query) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + jsonBytes, err := json.Marshal(bookmarks) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(jsonBytes) + })) +} + +func CreateBookmarkHandler(db *store.DB) http.Handler { + return AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var bookmark store.Bookmark + if err := json.NewDecoder(r.Body).Decode(&bookmark); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + id, err := store.InsertBookmark(db, bookmark) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + w.Write(fmt.Appendf([]byte{}, `{"id": %d}`, id)) + })) +} + +func UpdateBookmarkHandler(db *store.DB) http.Handler { + return AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var submittedBookmark store.Bookmark + if err := json.NewDecoder(r.Body).Decode(&submittedBookmark); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + originalBookmarkUrl := r.URL.Query().Get("url") + + var originalBookmark store.Bookmark + originalBookmark.Url = originalBookmarkUrl + + if err := store.UpdateBookmark(db, originalBookmark, submittedBookmark); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })) +} + +func GetBookmarkHandler(db *store.DB) http.Handler { + return AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + url := r.URL.Query().Get("url") + + bookmarks, err := store.GetBookmark(db, url) + if err == sql.ErrNoRows { + http.Error(w, "Bookmark not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(bookmarks) + })) +} + +func StatsHandler(db *store.DB) http.Handler { + return AuthRequired(db, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + stats := struct { + Count int `json:"bookmark_count"` + }{} + + stats.Count = store.CountBookmarks(db) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(stats) + + })) +} diff --git a/http/middlewares.go b/http/middlewares.go new file mode 100644 index 0000000..cbc2421 --- /dev/null +++ b/http/middlewares.go @@ -0,0 +1,41 @@ +package http + +import ( + "fmt" + "net/http" + "strings" + + "github.com/lukasmwerner/mark/store" +) + +func AuthRequired(db *store.DB, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + bearer := r.Header.Get("Authorization") + if bearer == "" { + http.Error(w, "Missing Authorization header", http.StatusUnauthorized) + return + } + + token := strings.TrimPrefix(bearer, "Bearer ") + + if token == "" { + http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) + return + } + + exists, err := store.HasKey(db, token) + if err != nil { + fmt.Println(err) + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + if exists { + next.ServeHTTP(w, r) + } else { + fmt.Println("Unauthorized, token not found: ", token[:10]) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + }) +} diff --git a/http/routes.go b/http/routes.go new file mode 100644 index 0000000..37e067e --- /dev/null +++ b/http/routes.go @@ -0,0 +1,43 @@ +package http + +import ( + "net/http" + + "github.com/lukasmwerner/mark/store" +) + +type Handler func(*store.DB) http.Handler + +type Route struct { + Pattern string + Handler Handler +} + +var routes = []Route{ + { + Pattern: "GET /api/bookmarks/search", + Handler: SearchBookmarksHandler, + }, + { + Pattern: "POST /api/bookmarks", + Handler: CreateBookmarkHandler, + }, + { + Pattern: "PATCH /api/bookmarks", + Handler: UpdateBookmarkHandler, + }, + { + Pattern: "GET /api/bookmarks", + Handler: GetBookmarkHandler, + }, + { + Pattern: "GET /api/stats", + Handler: StatsHandler, + }, +} + +func RegisterRoutes(db *store.DB, mux *http.ServeMux) { + for _, route := range routes { + mux.Handle(route.Pattern, route.Handler(db)) + } +}