Add entry API endpoints

This commit is contained in:
Kevin Kandlbinder 2022-03-01 13:17:17 +01:00
parent c8d1c33cb4
commit 1e071ffed4
8 changed files with 325 additions and 31 deletions

View file

@ -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!")
}
}

2
go.mod
View file

@ -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

3
go.sum
View file

@ -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=

View file

@ -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"))

View file

@ -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
}

View file

@ -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}`))
}

View file

@ -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)
}

View file

@ -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,