diff --git a/internal/db/db.go b/internal/db/db.go index ebe9ad6..a016080 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -91,8 +91,6 @@ func GetEntries(first int64, cursor *primitive.ObjectID) ([]*model.DBEntry, erro return nil, err } - log.Println("DBG1") - return object, nil } @@ -144,6 +142,57 @@ func GetListByID(id primitive.ObjectID) (*model.DBHashList, error) { return &object, nil } +func GetListByName(name string) (*model.DBHashList, error) { + db := DbClient.Database(viper.GetString("bot.mongo.database")) + + res := db.Collection(viper.GetString("bot.mongo.collection.lists")).FindOne(context.TODO(), bson.D{{"name", name}}) + if res.Err() != nil { + return nil, res.Err() + } + + object := model.DBHashList{} + + err := res.Decode(&object) + if err != nil { + return nil, err + } + + return &object, nil +} + +func GetLists(first int64, cursor *primitive.ObjectID) ([]*model.DBHashList, 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.lists")).Find(context.TODO(), filter, &opts) + if err != nil { + return nil, res.Err() + } + + var object []*model.DBHashList + + err = res.All(context.TODO(), &object) + if err != nil { + return nil, err + } + + return object, nil +} + func SaveUser(user *model.DBUser) error { db := DbClient.Database(viper.GetString("bot.mongo.database")) diff --git a/internal/db/model/list.go b/internal/db/model/list.go index 6b2f57a..d5827ee 100644 --- a/internal/db/model/list.go +++ b/internal/db/model/list.go @@ -4,6 +4,7 @@ import "go.mongodb.org/mongo-driver/bson/primitive" type DBHashList struct { ID primitive.ObjectID `bson:"_id" json:"id"` + Name string `bson:"name" json:"name"` Tags []string `bson:"tags" json:"tags"` // Tags of this list for discovery, and sorting Comments []*DBComment `bson:"comments" json:"comments"` // Comments regarding this list Maintainers []*primitive.ObjectID `bson:"maintainers" json:"maintainers"` // Maintainers contains references to the users who may edit this list diff --git a/internal/db/model/user.go b/internal/db/model/user.go index cd4717f..5651feb 100644 --- a/internal/db/model/user.go +++ b/internal/db/model/user.go @@ -12,6 +12,8 @@ type DBUser struct { Username string `bson:"username" json:"username"` // Username is the username the user has HashedPassword string `bson:"password" json:"password"` // HashedPassword contains the bcrypt-ed password + Admin *bool `bson:"admin,omitempty" json:"admin,omitempty"` // If set to true this user will have all privileges + MatrixLinks []*string `bson:"matrix_links" json:"matrix_links"` // MatrixLinks is the matrix-users this user has verified ownership over PendingMatrixLinks []*string `bson:"pending_matrix_links" json:"pending_matrix_links"` // PendingMatrixLinks is the matrix-users pending verification diff --git a/internal/web/api/api.go b/internal/web/api/api.go index fb10e98..471ffd1 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -31,6 +31,16 @@ func SetupAPI() chi.Router { r.Get("/{id}", apiHandleBotEntry) }) + router.Route("/lists", func(r chi.Router) { + r.Use(checkAuthMiddleware) + + r.Get("/", apiHandleBotListsList) + r.Post("/", apiHandleBotListsPost) + + r.Get("/by-name/{name}", apiHandleBotListByName) + r.Get("/{id}", apiHandleBotList) + }) + router.Route("/test", func(r chi.Router) { r.Use(checkAuthMiddleware) diff --git a/internal/web/api/bot_lists.go b/internal/web/api/bot_lists.go new file mode 100644 index 0000000..11079ac --- /dev/null +++ b/internal/web/api/bot_lists.go @@ -0,0 +1,211 @@ +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 apiListPostBody struct { + Name string + Tags []string + Comment *string + Maintainers []*primitive.ObjectID +} + +func apiHandleBotListsPost(res http.ResponseWriter, req *http.Request) { + var body apiListPostBody + + err := json.NewDecoder(req.Body).Decode(&body) + if err != nil { + writeJSONError(res, http.StatusBadRequest, errors.New("malformed body")) + return + } + + existingEntry, err := db.GetListByName(body.Name) + if err == nil { + writeJSONError(res, http.StatusConflict, fmt.Errorf("name taken: %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 + } + + newList := model.DBHashList{ + ID: primitive.NewObjectID(), + Name: body.Name, + Tags: body.Tags, + Comments: nil, + Maintainers: body.Maintainers, + } + + listedSelf := false + + for _, maintainer := range newList.Maintainers { + if maintainer.Hex() == userId.Hex() { + listedSelf = true + break + } + } + + if !listedSelf { + newList.Maintainers = append(newList.Maintainers, &userId) + } + + if body.Comment != nil && *body.Comment != "" { + newList.Comments = append(newList.Comments, &model.DBComment{ + CommentedBy: &userId, + Content: *body.Comment, + Timestamp: time.Now(), + }) + } + + err = db.SaveList(&newList) + if err != nil { + // TODO: LOG THIS ERROR + writeJSONError(res, http.StatusInternalServerError, errors.New("database error")) + return + } + + encoded, err := json.Marshal(newList) + 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 apiHandleBotListsList(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.GetLists(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 apiHandleBotList(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.GetListByID(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 apiHandleBotListByName(res http.ResponseWriter, req *http.Request) { + requestedName := chi.URLParam(req, "name") + + entry, err := db.GetListByName(requestedName) + 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) +}