From 4a4917beed7fc53c2b1aef587dbc13f9fc886521 Mon Sep 17 00:00:00 2001 From: nefrace Date: Fri, 9 Dec 2022 00:58:18 +0300 Subject: [PATCH] Init --- api.go | 52 +++++++++++++++++++ cmd.go | 41 +++++++++++++++ go.mod | 20 ++++++++ go.sum | 50 ++++++++++++++++++ handlers.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 21 ++++++++ middleware.go | 27 ++++++++++ mongostorage.go | 109 +++++++++++++++++++++++++++++++++++++++ static/index.html | 11 ++++ storage.go | 16 ++++++ types.go | 61 ++++++++++++++++++++++ 11 files changed, 534 insertions(+) create mode 100644 api.go create mode 100644 cmd.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers.go create mode 100644 main.go create mode 100644 middleware.go create mode 100644 mongostorage.go create mode 100644 static/index.html create mode 100644 storage.go create mode 100644 types.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..631a54f --- /dev/null +++ b/api.go @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..d5aecab --- /dev/null +++ b/cmd.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func run() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + db, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://root:example@localhost:27017")) + if err != nil { + log.Fatal("Can not connect to db: ", err) + } + if err := db.Ping(ctx, nil); err != nil { + log.Fatal("Can not connect to db: ", err) + } + cancel() + // + defer func() { + if err := db.Disconnect(context.TODO()); err != nil { + panic(err) + } + }() + storage := MongodbStorage{db: db} + + router := mux.NewRouter() + server := Server{ + host: ":4000", + mux: router, + db: storage, + } + server.InitHandlers() + log.Println("Server starting at ", server.host) + log.Fatal(http.ListenAndServe(server.host, router)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a260272 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.nefrace.ru/nefrace/nashboard + +go 1.19 + +require go.mongodb.org/mongo-driver v1.11.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 + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + 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 new file mode 100644 index 0000000..f7e9b81 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE= +go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..2ae05b6 --- /dev/null +++ b/handlers.go @@ -0,0 +1,126 @@ +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/main.go b/main.go new file mode 100644 index 0000000..26fb11e --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("Not enough args") + return + } + switch os.Args[1] { + case "run": + log.Println("Running") + run() + default: + fmt.Println("invalid command") + } +} diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..398f556 --- /dev/null +++ b/middleware.go @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..5981426 --- /dev/null +++ b/mongostorage.go @@ -0,0 +1,109 @@ +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/static/index.html b/static/index.html new file mode 100644 index 0000000..cf1ccbb --- /dev/null +++ b/static/index.html @@ -0,0 +1,11 @@ + + + + + + NashBoard + + + Hello world! + + \ No newline at end of file diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..f27a595 --- /dev/null +++ b/storage.go @@ -0,0 +1,16 @@ +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/types.go b/types.go new file mode 100644 index 0000000..b5991c3 --- /dev/null +++ b/types.go @@ -0,0 +1,61 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type User struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Username string + Password []byte `json:"-"` + IsAdmin bool + LastLogin time.Time +} + +type Session struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Token string + UserID primitive.ObjectID + IP string + LoggedAt time.Time + ExpiresAt time.Time + IsAdmin bool +} + +type ApiKey struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + UserID primitive.ObjectID + Key string +} + +type Folder struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Name string + Icon []byte +} + +type Bookmark struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Name string + Url string + FolderID primitive.ObjectID + Icon []byte +} + +type Note struct { + Id primitive.ObjectID `bson:"_id" json:"id,omitempty"` + Name string + Content string +} + +func GenSessionToken() string { + b := make([]byte, 128) + if _, err := rand.Read(b); err != nil { + return "" + } + return hex.EncodeToString(b) +}