From 1e071ffed470bc40ac104522bdecfbe7aee9f9d7 Mon Sep 17 00:00:00 2001 From: Kevin Kandlbinder Date: Tue, 1 Mar 2022 13:17:17 +0100 Subject: [PATCH] Add entry API endpoints --- cmd/root.go | 4 - go.mod | 2 +- go.sum | 3 +- internal/db/db.go | 35 +++++ internal/db/model/comment.go | 6 +- internal/web/api/api.go | 60 ++++++--- internal/web/api/bot_entries.go | 230 ++++++++++++++++++++++++++++++++ internal/web/web.go | 16 ++- 8 files changed, 325 insertions(+), 31 deletions(-) create mode 100644 internal/web/api/bot_entries.go diff --git a/cmd/root.go b/cmd/root.go index bba1485..2b358f4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -78,8 +78,4 @@ func loadConfig() { if err := viper.ReadInConfig(); err == nil { log.Println("Using config file:", viper.ConfigFileUsed()) } - - if viper.GetString("bot.web.secret") == "hunter2" { - log.Println("Web secret is not set! YOUR INSTALLATION IS INSECURE!") - } } diff --git a/go.mod b/go.mod index e746fe0..265146e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/Unkn0wnCat/matrix-veles go 1.16 require ( + github.com/go-chi/chi/v5 v5.0.7 github.com/golang-jwt/jwt/v4 v4.3.0 - github.com/gorilla/mux v1.8.0 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.1 go.mongodb.org/mongo-driver v1.8.3 diff --git a/go.sum b/go.sum index eda4395..6f9352c 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -209,7 +211,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= diff --git a/internal/db/db.go b/internal/db/db.go index c5d536c..ebe9ad6 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -61,6 +61,41 @@ func GetEntryByID(id primitive.ObjectID) (*model.DBEntry, error) { return &object, nil } +func GetEntries(first int64, cursor *primitive.ObjectID) ([]*model.DBEntry, error) { + db := DbClient.Database(viper.GetString("bot.mongo.database")) + + opts := options.FindOptions{ + Limit: &first, + } + + filter := bson.M{} + + if cursor != nil { + filter = bson.M{ + "_id": bson.M{ + "$gt": *cursor, + }, + } + log.Println(filter) + } + + res, err := db.Collection(viper.GetString("bot.mongo.collection.entries")).Find(context.TODO(), filter, &opts) + if err != nil { + return nil, res.Err() + } + + var object []*model.DBEntry + + err = res.All(context.TODO(), &object) + if err != nil { + return nil, err + } + + log.Println("DBG1") + + return object, nil +} + func GetEntryByHash(hash string) (*model.DBEntry, error) { db := DbClient.Database(viper.GetString("bot.mongo.database")) diff --git a/internal/db/model/comment.go b/internal/db/model/comment.go index df91fe2..ea8f986 100644 --- a/internal/db/model/comment.go +++ b/internal/db/model/comment.go @@ -1,8 +1,12 @@ package model -import "go.mongodb.org/mongo-driver/bson/primitive" +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) type DBComment struct { + Timestamp time.Time `bson:"timestamp" json:"timestamp"` CommentedBy *primitive.ObjectID `bson:"commented_by" json:"commented_by"` // CommentedBy contains a reference to the user who commented Content string `bson:"content" json:"content"` // Content is the body of the comment } diff --git a/internal/web/api/api.go b/internal/web/api/api.go index bdfcb77..fb10e98 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -4,28 +4,52 @@ import ( "context" "encoding/json" "errors" - "github.com/gorilla/mux" + "github.com/go-chi/chi/v5" "net/http" "strings" ) -func SetupAPI(router *mux.Router) { - router.NotFoundHandler = NotFoundHandler{} - router.MethodNotAllowedHandler = MethodNotAllowedHandler{} +func SetupAPI() chi.Router { + router := chi.NewRouter() - router.Path("/auth/login").Methods("POST").HandlerFunc(apiHandleAuthLogin) - router.Path("/auth/register").Methods("POST").HandlerFunc(apiHandleAuthRegister) + router.NotFound(notFoundHandler) + router.MethodNotAllowed(methodNotAllowedHandler) - bot := router.PathPrefix("/bot").Subrouter() - bot.Use(checkAuthMiddleware) + //router.NotFoundHandler = NotFoundHandler{} + //router.MethodNotAllowedHandler = MethodNotAllowedHandler{} - bot.Path("/test").HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - writer.WriteHeader(200) + router.Post("/auth/login", apiHandleAuthLogin) + router.Post("/auth/register", apiHandleAuthRegister) - claims := request.Context().Value("claims").(jwtClaims) + router.Route("/entries", func(r chi.Router) { + r.Use(checkAuthMiddleware) - writer.Write([]byte(`hello ` + claims.Username)) + r.Get("/", apiHandleBotEntriesList) + r.Post("/", apiHandleBotEntriesPost) + + r.Get("/by-hash/{hash}", apiHandleBotEntryByHash) + r.Get("/{id}", apiHandleBotEntry) }) + + router.Route("/test", func(r chi.Router) { + r.Use(checkAuthMiddleware) + + r.Get("/", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + + claims := request.Context().Value("claims").(jwtClaims) + + writer.Write([]byte(`hello ` + claims.Username)) + }) + }) + + return router +} + +func getClaims(request *http.Request) jwtClaims { + claims := request.Context().Value("claims").(jwtClaims) + + return claims } func checkAuthMiddleware(next http.Handler) http.Handler { @@ -69,18 +93,14 @@ func writeJSONError(res http.ResponseWriter, statusCode int, err error) { _, _ = res.Write(enc) } -type NotFoundHandler struct{} - -func (NotFoundHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { +func notFoundHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "application/json") res.WriteHeader(http.StatusNotFound) - _, _ = res.Write([]byte(`{"error": "not_found","error_code":404}`)) + _, _ = res.Write([]byte(`{"error": "not found","error_code":404}`)) } -type MethodNotAllowedHandler struct{} - -func (MethodNotAllowedHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { +func methodNotAllowedHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "application/json") res.WriteHeader(http.StatusMethodNotAllowed) - _, _ = res.Write([]byte(`{"error": "method_not_allowed","error_code":405}`)) + _, _ = res.Write([]byte(`{"error": "method not allowed","error_code":405}`)) } diff --git a/internal/web/api/bot_entries.go b/internal/web/api/bot_entries.go new file mode 100644 index 0000000..e23d5d6 --- /dev/null +++ b/internal/web/api/bot_entries.go @@ -0,0 +1,230 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/Unkn0wnCat/matrix-veles/internal/db" + "github.com/Unkn0wnCat/matrix-veles/internal/db/model" + "github.com/go-chi/chi/v5" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "log" + "net/http" + "net/url" + "strconv" + "time" +) + +type apiEntryPostBody struct { + Hash string `json:"hash"` + Tags []string `json:"tags"` + PartOf []*primitive.ObjectID `json:"part_of"` + FileURL string `json:"file_url"` + Comment *string `json:"comment"` +} + +func apiHandleBotEntriesPost(res http.ResponseWriter, req *http.Request) { + var body apiEntryPostBody + + err := json.NewDecoder(req.Body).Decode(&body) + if err != nil { + writeJSONError(res, http.StatusBadRequest, errors.New("malformed body")) + return + } + + existingEntry, err := db.GetEntryByHash(body.Hash) + if err == nil { + writeJSONError(res, http.StatusConflict, fmt.Errorf("hash already in database: %s", existingEntry.ID)) + return + } + if !errors.Is(err, mongo.ErrNoDocuments) { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + user := getClaims(req) + userId, err := primitive.ObjectIDFromHex(user.Subject) + if err != nil { + // TODO: LOG THIS ERROR + log.Println(userId) + writeJSONError(res, http.StatusInternalServerError, errors.New("internal corruption 0x01")) + return + } + + for i, partOf := range body.PartOf { + list, err := db.GetListByID(*partOf) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + writeJSONError(res, http.StatusNotFound, fmt.Errorf("invalid partof value at index %d - not found", i)) + return + } + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + authorized := false + + for _, maintainer := range list.Maintainers { + log.Println(maintainer, userId) + if maintainer.Hex() == userId.Hex() { + authorized = true + break + } + } + + if !authorized { + writeJSONError(res, http.StatusUnauthorized, fmt.Errorf("invalid partof value at index %d - not authorized", i)) + return + } + } + + newEntry := model.DBEntry{ + ID: primitive.NewObjectID(), + Tags: body.Tags, + PartOf: body.PartOf, + HashValue: body.Hash, + FileURL: body.FileURL, + Timestamp: time.Now(), + AddedBy: &userId, + Comments: nil, + } + + if body.Comment != nil && *body.Comment != "" { + newEntry.Comments = append(newEntry.Comments, &model.DBComment{ + CommentedBy: &userId, + Content: *body.Comment, + Timestamp: time.Now(), + }) + } + + err = db.SaveEntry(&newEntry) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + encoded, err := json.Marshal(newEntry) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("could not marshal data")) + return + } + + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(encoded) +} + +func apiHandleBotEntriesList(res http.ResponseWriter, req *http.Request) { + requestUri, err := url.ParseRequestURI(req.RequestURI) + if err != nil { + writeJSONError(res, http.StatusBadRequest, errors.New("unable to parse uri")) + return + } + + first := int64(50) + var cursor *primitive.ObjectID + + if requestUri.Query().Has("first") { + first2, err := strconv.Atoi(requestUri.Query().Get("first")) + if err != nil { + writeJSONError(res, http.StatusBadRequest, errors.New("malformed query")) + return + } + first = int64(first2) + } + + if requestUri.Query().Has("cursor") { + cursor2, err := primitive.ObjectIDFromHex(requestUri.Query().Get("cursor")) + if err != nil { + writeJSONError(res, http.StatusBadRequest, errors.New("malformed query")) + return + } + cursor = &cursor2 + } + + entries, err := db.GetEntries(first, cursor) + if err != nil { + if !errors.Is(err, mongo.ErrNoDocuments) { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write([]byte("[]")) + return + } + + encoded, err := json.Marshal(entries) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("could not marshal data")) + return + } + + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(encoded) +} + +func apiHandleBotEntry(res http.ResponseWriter, req *http.Request) { + requestedId := chi.URLParam(req, "id") + objectId, err := primitive.ObjectIDFromHex(requestedId) + if err != nil { + writeJSONError(res, http.StatusNotFound, errors.New("malformed id")) + return + } + + entry, err := db.GetEntryByID(objectId) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + writeJSONError(res, http.StatusNotFound, errors.New("not found")) + return + } + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + encoded, err := json.Marshal(entry) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("could not marshal data")) + return + } + + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(encoded) +} + +func apiHandleBotEntryByHash(res http.ResponseWriter, req *http.Request) { + requestedHash := chi.URLParam(req, "hash") + + entry, err := db.GetEntryByHash(requestedHash) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + writeJSONError(res, http.StatusNotFound, errors.New("not found")) + return + } + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + encoded, err := json.Marshal(entry) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("could not marshal data")) + return + } + + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusOK) + res.Write(encoded) +} diff --git a/internal/web/web.go b/internal/web/web.go index c3c88d2..223adf8 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -2,7 +2,8 @@ package web import ( "github.com/Unkn0wnCat/matrix-veles/internal/web/api" - "github.com/gorilla/mux" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/spf13/viper" "log" "net/http" @@ -10,12 +11,19 @@ import ( ) func StartServer() { - r := mux.NewRouter() + if viper.GetString("bot.web.secret") == "hunter2" { + log.Println("Web secret is not set! REFUSING TO START WEB SERVER!") + return + } + + r := chi.NewRouter() + + r.Use(middleware.Logger) + r.HandleFunc("/", HomeHandler) //r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - apiRouter := r.PathPrefix("/api").Subrouter() - api.SetupAPI(apiRouter) + r.Mount("/api", api.SetupAPI()) srv := &http.Server{ Handler: r,