Refactoring, added generic Store
This commit is contained in:
parent
4a4917beed
commit
ed012d3715
52
api.go
52
api.go
|
@ -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
|
||||
}
|
18
cmd.go
18
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))
|
||||
}
|
||||
|
|
8
go.mod
8
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
|
||||
)
|
||||
|
|
9
go.sum
9
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=
|
||||
|
|
126
handlers.go
126
handlers.go
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
}
|
109
mongostorage.go
109
mongostorage.go
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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/")))
|
||||
}
|
16
storage.go
16
storage.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
@ -35,6 +35,7 @@ type ApiKey struct {
|
|||
type Folder struct {
|
||||
Id primitive.ObjectID `bson:"_id" json:"id,omitempty"`
|
||||
Name string
|
||||
ParentID primitive.ObjectID
|
||||
Icon []byte
|
||||
}
|
||||
|
||||
|
@ -42,14 +43,19 @@ 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 {
|
Loading…
Reference in New Issue