Refactoring, added generic Store
This commit is contained in:
77
server/api.go
Normal file
77
server/api.go
Normal file
@ -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)
|
||||
}
|
99
server/handlersAuth.go
Normal file
99
server/handlersAuth.go
Normal file
@ -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")
|
||||
}
|
21
server/handlersUser.go
Normal file
21
server/handlersUser.go
Normal file
@ -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)
|
||||
}
|
53
server/middleware.go
Normal file
53
server/middleware.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
44
server/server.go
Normal file
44
server/server.go
Normal file
@ -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/")))
|
||||
}
|
Reference in New Issue
Block a user