From ed012d3715232df3a9ab4de8e7b8ff7f6c80ad46 Mon Sep 17 00:00:00 2001 From: nefrace Date: Sun, 11 Dec 2022 02:05:03 +0300 Subject: [PATCH] Refactoring, added generic Store --- api.go | 52 --------------- cmd.go | 18 ++--- go.mod | 8 ++- go.sum | 9 +++ handlers.go | 126 ----------------------------------- middleware.go | 27 -------- mongostorage.go | 109 ------------------------------ server/api.go | 77 +++++++++++++++++++++ server/handlersAuth.go | 99 +++++++++++++++++++++++++++ server/handlersUser.go | 21 ++++++ server/middleware.go | 53 +++++++++++++++ server/server.go | 44 ++++++++++++ storage.go | 16 ----- storage/mongoStore.go | 90 +++++++++++++++++++++++++ storage/mongostorage.go | 110 ++++++++++++++++++++++++++++++ storage/storage.go | 29 ++++++++ types.go => storage/types.go | 18 +++-- 17 files changed, 559 insertions(+), 347 deletions(-) delete mode 100644 api.go delete mode 100644 handlers.go delete mode 100644 middleware.go delete mode 100644 mongostorage.go create mode 100644 server/api.go create mode 100644 server/handlersAuth.go create mode 100644 server/handlersUser.go create mode 100644 server/middleware.go create mode 100644 server/server.go delete mode 100644 storage.go create mode 100644 storage/mongoStore.go create mode 100644 storage/mongostorage.go create mode 100644 storage/storage.go rename types.go => storage/types.go (78%) diff --git a/api.go b/api.go deleted file mode 100644 index 631a54f..0000000 --- a/api.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" -) - -type ApiError struct { - Err string - Status int -} - -func (e ApiError) Error() string { - return e.Err -} - -type apiFunc func(http.ResponseWriter, *http.Request) error - -func MakeHTTPHandler(f apiFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := f(w, r); err != nil { - if e, ok := err.(ApiError); ok { - WriteJSON(w, e.Status, e) - return - } - WriteJSON(w, 500, ApiError{Err: err.Error(), Status: http.StatusInternalServerError}) - return - } - } -} - -func StatusAndContent(w http.ResponseWriter, status int, contentType string) { - w.Header().Add("Content-Type", contentType) - w.WriteHeader(status) -} - -func WriteJSON(w http.ResponseWriter, status int, v any) error { - StatusAndContent(w, status, "application/json") - return json.NewEncoder(w).Encode(v) -} - -func WritePlain(w http.ResponseWriter, status int, text string) error { - StatusAndContent(w, status, "text/plain") - _, err := w.Write([]byte(text)) - return err -} - -func WriteBlob(w http.ResponseWriter, status int, contentType string, blob []byte) error { - StatusAndContent(w, status, contentType) - _, err := w.Write(blob) - return err -} diff --git a/cmd.go b/cmd.go index d5aecab..82e8b65 100644 --- a/cmd.go +++ b/cmd.go @@ -6,6 +6,8 @@ import ( "net/http" "time" + "git.nefrace.ru/nefrace/nashboard/server" + "git.nefrace.ru/nefrace/nashboard/storage" "github.com/gorilla/mux" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -27,15 +29,15 @@ func run() { panic(err) } }() - storage := MongodbStorage{db: db} + s := storage.MongodbStorage{Db: db} router := mux.NewRouter() - server := Server{ - host: ":4000", - mux: router, - db: storage, + srv := server.Server{ + Host: ":4000", + Mux: router, + Db: s, } - server.InitHandlers() - log.Println("Server starting at ", server.host) - log.Fatal(http.ListenAndServe(server.host, router)) + srv.InitHandlers() + log.Println("Server starting at ", srv.Host) + log.Fatal(http.ListenAndServe(srv.Host, router)) } diff --git a/go.mod b/go.mod index a260272..476c895 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,14 @@ module git.nefrace.ru/nefrace/nashboard go 1.19 -require go.mongodb.org/mongo-driver v1.11.0 +require ( + github.com/gorilla/mux v1.8.0 + go.mongodb.org/mongo-driver v1.11.0 + golang.org/x/crypto v0.4.0 +) require ( github.com/golang/snappy v0.0.1 // indirect - github.com/gorilla/mux v1.8.0 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pkg/errors v0.9.1 // indirect @@ -14,7 +17,6 @@ require ( github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - golang.org/x/crypto v0.4.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index f7e9b81..5a63507 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,29 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -43,8 +50,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers.go b/handlers.go deleted file mode 100644 index 2ae05b6..0000000 --- a/handlers.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "encoding/json" - "io" - "log" - "net/http" - "time" - - "github.com/gorilla/mux" - "go.mongodb.org/mongo-driver/bson/primitive" - "golang.org/x/crypto/bcrypt" -) - -type Server struct { - host string - mux *mux.Router - db Storage -} - -var ErrUnauthorized ApiError = ApiError{Status: http.StatusUnauthorized, Err: "Unauthorized"} -var ErrBadRequest ApiError = ApiError{Status: http.StatusBadRequest, Err: "bad request"} - -func (s Server) InitHandlers() { - r := s.mux - apiRouter := r.PathPrefix("/api").Subrouter() - - authRouter := apiRouter.PathPrefix("/user").Subrouter() - me := authRouter.Path("/me").Subrouter() - me.Use(s.AuthorizedOnly) - me.Path("").HandlerFunc(MakeHTTPHandler(s.handleGetUser)) - authRouter.Path("/reg").Methods("POST").HandlerFunc(MakeHTTPHandler(s.handleRegister)) - authRouter.Path("/login").Methods("POST").HandlerFunc(MakeHTTPHandler(s.handleLogin)) - - r.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/"))) -} - -var anon User = User{ - Id: primitive.ObjectID{}, - Username: "Anonymous", - IsAdmin: false, -} - -func (s Server) handleGetUser(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := ctx.Value(CtxUser("user")).(*User) - - return WriteJSON(w, 200, user) -} - -type registerForm struct { - Username string - Password string -} - -func (s Server) handleRegister(w http.ResponseWriter, r *http.Request) error { - var form registerForm - err := DecodeJSON(r.Body, &form) - if err != nil { - return ApiError{Status: http.StatusBadRequest, Err: "bad credentials"} - } - password, err := bcrypt.GenerateFromPassword([]byte(form.Password), 0) - if err != nil { - log.Println("Can not generate hash from password: ", err) - return ApiError{Status: http.StatusInternalServerError, Err: "cannot register"} - } - newUser := User{ - Id: primitive.NewObjectID(), - Username: form.Username, - Password: password, - IsAdmin: true, - } - if err = s.db.CreateUser(&newUser); err != nil { - log.Println("Can not create user: ", err) - return ApiError{Status: http.StatusInternalServerError, Err: "cannot register"} - } - - return WriteJSON(w, http.StatusCreated, &newUser) -} - -type loginForm struct { - Username string - Password string -} - -func (s Server) handleLogin(w http.ResponseWriter, r *http.Request) error { - var form loginForm - err := DecodeJSON(r.Body, &form) - if err != nil { - log.Println("Can not decode json from login: ", err) - return ErrBadRequest - } - user, err := s.db.GetUserByName(form.Username) - if err != nil { - log.Println("Can not find user: ", err) - return ErrBadRequest - } - err = bcrypt.CompareHashAndPassword(user.Password, []byte(form.Password)) - if err != nil { - log.Println("Can not compare password and hash: ", err) - return ErrBadRequest - } - - ses := Session{ - Id: primitive.NewObjectID(), - Token: GenSessionToken(), - UserID: user.Id, - LoggedAt: time.Now(), - IsAdmin: true, - ExpiresAt: time.Now().Add(42 * time.Hour * 24), - IP: r.RemoteAddr, - } - user.LastLogin = time.Now() - if err := s.db.UpdateUser(user); err != nil { - log.Println("Can not update user: ", err) - } - if err = s.db.CreateSession(&ses); err != nil { - log.Println("Can not store session: ", err) - return ErrBadRequest - } - return WriteJSON(w, 200, ses) -} - -func DecodeJSON(r io.Reader, v any) error { - return json.NewDecoder(r).Decode(v) -} diff --git a/middleware.go b/middleware.go deleted file mode 100644 index 398f556..0000000 --- a/middleware.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "context" - "net/http" -) - -type CtxUser string - -func (s Server) AuthorizedOnly(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sessionHeader := r.Header.Get("SessionID") - session, err := s.db.GetSessionByToken(sessionHeader) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - user, err := s.db.GetUserByID(session.UserID) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - ctx := r.Context() - ctx = context.WithValue(ctx, CtxUser("user"), user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} diff --git a/mongostorage.go b/mongostorage.go deleted file mode 100644 index 5981426..0000000 --- a/mongostorage.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "context" - "log" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" -) - -type MongodbStorage struct { - db *mongo.Client -} - -func (m MongodbStorage) CreateUser(user *User) error { - ctx := context.TODO() - _, err := m.Collection("users").InsertOne(ctx, user) - if err != nil { - if e, ok := err.(mongo.WriteError); ok { - log.Println("Error creating new user: ", e) - return e - } - } - return nil -} - -func (m MongodbStorage) GetUsers() ([]User, error) { - ctx := context.TODO() - cur, err := m.Collection("users").Find(ctx, bson.D{}) - if err != nil { - log.Println("Error fetching users: ", err) - return nil, err - } - var res []User - err = cur.All(ctx, &res) - if err != nil { - log.Println("Error collection users to slice: ", err) - return nil, err - } - return res, nil -} - -func (m MongodbStorage) GetUserByName(username string) (*User, error) { - ctx := context.TODO() - res := m.Collection("users").FindOne(ctx, bson.D{{Key: "username", Value: username}}) - if res.Err() != nil { - return nil, res.Err() - } - var user User - res.Decode(&user) - return &user, nil -} - -func (m MongodbStorage) GetUserByID(id primitive.ObjectID) (*User, error) { - ctx := context.TODO() - res := m.Collection("users").FindOne(ctx, bson.D{{Key: "_id", Value: id}}) - if res.Err() != nil { - return nil, res.Err() - } - var user User - res.Decode(&user) - return &user, nil -} - -func (m MongodbStorage) UpdateUser(user *User) error { - ctx := context.TODO() - _, err := m.Collection("users").ReplaceOne(ctx, bson.D{{Key: "_id", Value: user.Id}}, user) - return err -} - -func (m MongodbStorage) DeleteUser(id primitive.ObjectID) error { - ctx := context.TODO() - _, err := m.Collection("users").DeleteOne(ctx, bson.D{{Key: "_id", Value: id}}) - return err -} - -func (m MongodbStorage) CreateSession(s *Session) error { - ctx := context.TODO() - _, err := m.Collection("sessions").InsertOne(ctx, s) - if err != nil { - if e, ok := err.(mongo.WriteError); ok { - log.Println("Error creating new session: ", e) - return e - } - } - return nil -} - -func (m MongodbStorage) GetSessionByToken(token string) (*Session, error) { - ctx := context.TODO() - res := m.Collection("sessions").FindOne(ctx, bson.D{{Key: "token", Value: token}}) - if res.Err() != nil { - return nil, res.Err() - } - var session Session - res.Decode(&session) - return &session, nil -} - -func (m MongodbStorage) DeleteSession(id primitive.ObjectID) error { - ctx := context.TODO() - _, err := m.Collection("users").DeleteOne(ctx, bson.D{{Key: "_id", Value: id}}) - return err -} - -func (m MongodbStorage) Collection(col string) *mongo.Collection { - return m.db.Database("nash").Collection(col) -} diff --git a/server/api.go b/server/api.go new file mode 100644 index 0000000..bff3918 --- /dev/null +++ b/server/api.go @@ -0,0 +1,77 @@ +package server + +import ( + "encoding/json" + "io" + "log" + "net/http" + "runtime/debug" +) + +type ApiError struct { + Err string + Status int +} + +func (e ApiError) Error() string { + return e.Err +} + +type apiFunc func(Context) error + +func MakeHTTPHandler(f apiFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := Context{w, r} + if err := f(ctx); err != nil { + if e, ok := err.(ApiError); ok { + log.Println("API ERROR: ", e.Error(), e.Status) + ctx.WriteJSON(e.Status, e) + return + } + log.Println("INTERNAL ERROR: ", err) + debug.PrintStack() + ctx.WriteJSON(500, &ApiError{Err: "internal error", Status: http.StatusInternalServerError}) + return + } + } +} + +type Context struct { + W http.ResponseWriter + R *http.Request +} + +func (c Context) Header(header string, value string) *Context { + c.W.Header().Add(header, value) + return &c +} + +func (c Context) ContentType(contentType string) *Context { + return c.Header("Content-Type", contentType) +} + +func (c Context) Status(status int) *Context { + c.W.WriteHeader(status) + return &c +} + +func (c Context) WriteJSON(status int, v any) error { + c.ContentType("application/json").Status(status) + return json.NewEncoder(c.W).Encode(v) +} + +func (c Context) WritePlain(status int, text string) error { + c.ContentType("text/plain").Status(status) + _, err := c.W.Write([]byte(text)) + return err +} + +func (c Context) WriteBlob(status int, contentType string, blob []byte) error { + c.ContentType(contentType).Status(status) + _, err := c.W.Write(blob) + return err +} + +func DecodeJSON(r io.Reader, v any) error { + return json.NewDecoder(r).Decode(v) +} diff --git a/server/handlersAuth.go b/server/handlersAuth.go new file mode 100644 index 0000000..b88e490 --- /dev/null +++ b/server/handlersAuth.go @@ -0,0 +1,99 @@ +package server + +import ( + "log" + "net/http" + "time" + + "git.nefrace.ru/nefrace/nashboard/storage" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "golang.org/x/crypto/bcrypt" +) + +type registerForm struct { + Username string + Password string +} + +func (s Server) handleRegister(c Context) error { + var form registerForm + err := DecodeJSON(c.R.Body, &form) + if err != nil { + return ApiError{Status: http.StatusBadRequest, Err: "bad credentials"} + } + _, err = s.Db.GetUser(&bson.D{{Key: "username", Value: form.Username}}) + if err == nil { + return ApiError{Status: http.StatusBadRequest, Err: "user exists"} + } + password, err := bcrypt.GenerateFromPassword([]byte(form.Password), 0) + if err != nil { + log.Println("Can not generate hash from password: ", err) + return ApiError{Status: http.StatusInternalServerError, Err: "cannot register"} + } + newUser := storage.User{ + Id: primitive.NewObjectID(), + Username: form.Username, + Password: password, + IsAdmin: true, + } + if _, err = s.Db.CreateUser(&newUser); err != nil { + log.Println("Can not create user: ", err) + return ApiError{Status: http.StatusInternalServerError, Err: "cannot register"} + } + + return c.WriteJSON(http.StatusCreated, &newUser) +} + +type loginForm struct { + Username string + Password string +} + +func (s Server) handleLogin(c Context) error { + var form loginForm + err := DecodeJSON(c.R.Body, &form) + if err != nil { + log.Println("Can not decode json from login: ", err) + return ErrBadRequest + } + user, err := s.Db.GetUser(&bson.D{{Key: "username", Value: form.Username}}) + if err != nil { + log.Println("Can not find user: ", err) + return ErrBadRequest + } + err = bcrypt.CompareHashAndPassword(user.Password, []byte(form.Password)) + if err != nil { + log.Println("Can not compare password and hash: ", err) + return ErrBadRequest + } + + ses := storage.Session{ + Id: primitive.NewObjectID(), + Token: storage.GenSessionToken(), + UserID: user.Id, + LoggedAt: time.Now(), + IsAdmin: true, + ExpiresAt: time.Now().Add(42 * time.Hour * 24), + IP: c.R.RemoteAddr, + } + user.LastLogin = time.Now() + if err := s.Db.UpdateUser(user); err != nil { + log.Println("Can not update user: ", err) + } + if _, err := s.Db.CreateSession(&ses); err != nil { + log.Println("Can not store session: ", err) + return ErrBadRequest + } + return c.WriteJSON(200, ses.Token) +} + +func (s Server) handleLogout(c Context) error { + header := c.R.Header.Get("SessionID") + session, err := s.Db.GetSessionByToken(header) + if err != nil { + return ApiError{Status: 400, Err: "can't logout"} + } + s.Db.DeleteSession(session.Id) + return c.WriteJSON(200, "logged out succesfully") +} diff --git a/server/handlersUser.go b/server/handlersUser.go new file mode 100644 index 0000000..a808b7d --- /dev/null +++ b/server/handlersUser.go @@ -0,0 +1,21 @@ +package server + +import ( + "git.nefrace.ru/nefrace/nashboard/storage" + "go.mongodb.org/mongo-driver/bson" +) + +func (s Server) handleGetMe(c Context) error { + ctx := c.R.Context() + user := ctx.Value(CtxValue("user")).(*storage.User) + return c.WriteJSON(200, user) +} + +func (s Server) handleAllUsers(c Context) error { + users, err := s.Db.GetUsers(&bson.D{}) + if err != nil { + + return ApiError{Status: 500, Err: "can't collect users"} + } + return c.WriteJSON(200, users) +} diff --git a/server/middleware.go b/server/middleware.go new file mode 100644 index 0000000..b1d3de8 --- /dev/null +++ b/server/middleware.go @@ -0,0 +1,53 @@ +package server + +import ( + "context" + "log" + "net/http" + + "git.nefrace.ru/nefrace/nashboard/storage" +) + +type CtxValue string + +func (s Server) UserInContext(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + user, isAuthorized := s.authorizedByHeader(r) + if isAuthorized { + ctx = context.WithValue(ctx, CtxValue("user"), user) + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (s Server) AuthorizedOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := r.Context().Value(CtxValue("user")).(*storage.User) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + +func (s Server) authorizedByHeader(r *http.Request) (*storage.User, bool) { + sessionHeader := r.Header.Get("SessionID") + session, err := s.Db.GetSessionByToken(sessionHeader) + if err != nil { + return nil, false + } + user, err := s.Db.GetUserByID(session.UserID) + if err != nil { + return nil, false + } + return user, true +} + +func (s Server) Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Print("HTTP ", r.URL) + next.ServeHTTP(w, r) + }) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..964b245 --- /dev/null +++ b/server/server.go @@ -0,0 +1,44 @@ +package server + +import ( + "net/http" + + "git.nefrace.ru/nefrace/nashboard/storage" + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Server struct { + Host string + Mux *mux.Router + Db storage.Storage +} + +var ErrUnauthorized ApiError = ApiError{Status: http.StatusUnauthorized, Err: "Unauthorized"} +var ErrBadRequest ApiError = ApiError{Status: http.StatusBadRequest, Err: "bad request"} + +var anon storage.User = storage.User{ + Id: primitive.ObjectID{}, + Username: "Anonymous", + IsAdmin: false, +} + +func (s Server) InitHandlers() { + r := s.Mux + r.Use(s.Logger) + r.Use(s.UserInContext) + apiRouter := r.PathPrefix("/api").Subrouter() + + authRouter := apiRouter.PathPrefix("/auth").Subrouter() + authRouter.Path("/reg").Methods("POST").HandlerFunc(MakeHTTPHandler(s.handleRegister)) + authRouter.Path("/login").Methods("POST").HandlerFunc(MakeHTTPHandler(s.handleLogin)) + authRouter.Path("/logout").Methods("POST").HandlerFunc(MakeHTTPHandler(s.handleLogout)) + + userRouter := apiRouter.PathPrefix("/user").Subrouter() + userRouter.Use(s.AuthorizedOnly) + userRouter.Use(s.AuthorizedOnly) + userRouter.Path("/me").HandlerFunc(MakeHTTPHandler(s.handleGetMe)) + userRouter.Path("/all").HandlerFunc(MakeHTTPHandler(s.handleAllUsers)) + + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/"))) +} diff --git a/storage.go b/storage.go deleted file mode 100644 index f27a595..0000000 --- a/storage.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import "go.mongodb.org/mongo-driver/bson/primitive" - -type Storage interface { - CreateUser(*User) error - GetUsers() ([]User, error) - GetUserByName(string) (*User, error) - GetUserByID(primitive.ObjectID) (*User, error) - UpdateUser(*User) error - DeleteUser(primitive.ObjectID) error - - CreateSession(*Session) error - GetSessionByToken(string) (*Session, error) - DeleteSession(primitive.ObjectID) error -} diff --git a/storage/mongoStore.go b/storage/mongoStore.go new file mode 100644 index 0000000..a6b4316 --- /dev/null +++ b/storage/mongoStore.go @@ -0,0 +1,90 @@ +package storage + +import ( + "context" + "log" + "reflect" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +func NewStore[T any](db *MongodbStorage, collection string) Store[T] { + return Store[T]{ + Db: db.Db, + Coll: db.Collection(collection), + } +} + +type Store[T any] struct { + Db *mongo.Client + Coll *mongo.Collection +} + +func (s Store[T]) InsertOne(ctx context.Context, item *T) (primitive.ObjectID, error) { + result, err := s.Coll.InsertOne(ctx, item) + if err != nil { + if e, ok := err.(mongo.WriteError); ok { + log.Printf("Error writing new item of type %v: %v ", reflect.TypeOf(item), e) + return primitive.NilObjectID, e + } + log.Printf("Error creating new item of type %v: %v ", reflect.TypeOf(item), err) + return primitive.NilObjectID, err + } + rid, _ := result.InsertedID.(primitive.ObjectID) + return rid, nil +} + +func (s Store[T]) GetById(ctx context.Context, id primitive.ObjectID) (*T, error) { + res := s.Coll.FindOne(ctx, bson.D{{Key: "_id", Value: id}}) + if res.Err() != nil { + return nil, res.Err() + } + var item T + res.Decode(&item) + return &item, nil +} + +func (s Store[T]) GetOne(ctx context.Context, filter *bson.D) (*T, error) { + f := getFilter(filter) + res := s.Coll.FindOne(ctx, f) + if res.Err() != nil { + return nil, res.Err() + } + var item T + res.Decode(&item) + return &item, nil +} + +func (s Store[T]) GetMany(ctx context.Context, filter *bson.D) ([]*T, error) { + f := getFilter(filter) + cur, err := s.Coll.Find(ctx, f) + if err != nil { + log.Println("Error fetching items: ", err) + return nil, err + } + var res []*T + err = cur.All(ctx, &res) + if err != nil { + log.Println("Error collecting items to slice: ", err) + return nil, err + } + return res, nil +} +func (s Store[T]) DeleteByID(ctx context.Context, id primitive.ObjectID) error { + _, err := s.Coll.DeleteOne(ctx, bson.D{{Key: "_id", Value: id}}) + return err +} + +func (s Store[T]) ReplaceByID(ctx context.Context, id primitive.ObjectID, item *T) error { + _, err := s.Coll.ReplaceOne(ctx, bson.D{{Key: "_id", Value: id}}, item) + return err +} + +func getFilter(f *bson.D) bson.D { + if f == nil { + return bson.D{} + } + return *f +} diff --git a/storage/mongostorage.go b/storage/mongostorage.go new file mode 100644 index 0000000..044f08c --- /dev/null +++ b/storage/mongostorage.go @@ -0,0 +1,110 @@ +package storage + +import ( + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type MongodbStorage struct { + Db *mongo.Client +} + +func (m MongodbStorage) CreateUser(user *User) (primitive.ObjectID, error) { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.InsertOne(ctx, user) +} + +func (m MongodbStorage) GetUserByID(id primitive.ObjectID) (*User, error) { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.GetById(ctx, id) +} + +func (m MongodbStorage) GetUser(filter *bson.D) (*User, error) { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.GetOne(ctx, filter) +} + +func (m MongodbStorage) GetUsers(filter *bson.D) ([]*User, error) { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.GetMany(ctx, filter) +} + +func (m MongodbStorage) UpdateUser(user *User) error { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.ReplaceByID(ctx, user.Id, user) +} + +func (m MongodbStorage) DeleteUser(id primitive.ObjectID) error { + ctx := context.TODO() + store := NewStore[User](&m, "users") + return store.DeleteByID(ctx, id) +} + +func (m MongodbStorage) CreateSession(s *Session) (primitive.ObjectID, error) { + ctx := context.TODO() + store := NewStore[Session](&m, "sessions") + return store.InsertOne(ctx, s) +} + +func (m MongodbStorage) GetSessionByToken(token string) (*Session, error) { + ctx := context.TODO() + store := NewStore[Session](&m, "sessions") + return store.GetOne(ctx, &bson.D{{Key: "token", Value: token}}) +} + +func (m MongodbStorage) GetUserSessions(user *User) ([]*Session, error) { + ctx := context.TODO() + store := NewStore[Session](&m, "sessions") + return store.GetMany(ctx, &bson.D{{Key: "userid", Value: user.Id}}) +} + +func (m MongodbStorage) DeleteSession(id primitive.ObjectID) error { + ctx := context.TODO() + store := NewStore[Session](&m, "sessions") + return store.DeleteByID(ctx, id) +} + +func (m MongodbStorage) CreateBookmark(bookmark *Bookmark) (primitive.ObjectID, error) { + ctx := context.TODO() + store := NewStore[Bookmark](&m, "bookmarks") + return store.InsertOne(ctx, bookmark) + +} +func (m MongodbStorage) GetBookmarkByID(id primitive.ObjectID) (*Bookmark, error) { + ctx := context.TODO() + store := NewStore[Bookmark](&m, "bookmarks") + return store.GetById(ctx, id) +} + +func (m MongodbStorage) GetBookmark(filter *bson.D) (*Bookmark, error) { + ctx := context.TODO() + store := NewStore[Bookmark](&m, "bookmarks") + return store.GetOne(ctx, filter) +} + +func (m MongodbStorage) GetBookmarks(filter *bson.D) ([]*Bookmark, error) { + ctx := context.TODO() + store := NewStore[Bookmark](&m, "bookmarks") + return store.GetMany(ctx, filter) +} + +func (m MongodbStorage) GetUserBookmarks(user *User, filter *bson.D) ([]*Bookmark, error) { + passedFilter := getFilter(filter) + f := bson.D{{Key: "userid", Value: user.Id}} + f = append(f, passedFilter...) + ctx := context.TODO() + store := NewStore[Bookmark](&m, "bookmarks") + return store.GetMany(ctx, &f) +} + +func (m MongodbStorage) Collection(col string) *mongo.Collection { + return m.Db.Database("nash").Collection(col) +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..b5a2c4d --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,29 @@ +package storage + +import ( + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Storage interface { + // Users + CreateUser(*User) (primitive.ObjectID, error) + GetUser(*bson.D) (*User, error) + GetUsers(*bson.D) ([]*User, error) + GetUserByID(primitive.ObjectID) (*User, error) + UpdateUser(*User) error + DeleteUser(primitive.ObjectID) error + + // Sessions + CreateSession(*Session) (primitive.ObjectID, error) + GetSessionByToken(string) (*Session, error) + GetUserSessions(*User) ([]*Session, error) + DeleteSession(primitive.ObjectID) error + + // Bookmarks + CreateBookmark(*Bookmark) (primitive.ObjectID, error) + GetBookmarkByID(primitive.ObjectID) (*Bookmark, error) + GetBookmark(*bson.D) (*Bookmark, error) + GetBookmarks(*bson.D) ([]*Bookmark, error) + GetUserBookmarks(*User, *bson.D) ([]*Bookmark, error) +} diff --git a/types.go b/storage/types.go similarity index 78% rename from types.go rename to storage/types.go index b5991c3..8f8f971 100644 --- a/types.go +++ b/storage/types.go @@ -1,4 +1,4 @@ -package main +package storage import ( "crypto/rand" @@ -18,7 +18,7 @@ type User struct { type Session struct { Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` - Token string + Token string `json:"-"` UserID primitive.ObjectID IP string LoggedAt time.Time @@ -33,23 +33,29 @@ type ApiKey struct { } type Folder struct { - Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` - Name string - Icon []byte + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Name string + ParentID primitive.ObjectID + Icon []byte } type Bookmark struct { Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` Name string Url string + UserID primitive.ObjectID FolderID primitive.ObjectID - Icon []byte + Tags []string + // Icon []byte + Created time.Time } type Note struct { Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` Name string + Tags []string Content string + Created time.Time } func GenSessionToken() string {