From 87985762bf148af2a50f1fcd4b7c7f9b3bc799e8 Mon Sep 17 00:00:00 2001 From: optclblast Date: Thu, 2 Jan 2025 16:08:22 -0800 Subject: [PATCH] handlers moved to new system, upload need to be fixed and resolvers must be tested --- cmd/main.go | 2 +- internal/app/app.go | 39 ++---- internal/app/auth.go | 63 --------- internal/app/{login.go => handlers/logon.go} | 124 ++++++++++-------- internal/app/handlers/upload_file.go | 91 +++++++++++++ internal/app/middleware.go | 28 ---- internal/app/upload_file.go | 81 ------------ internal/domain/requests.go | 4 +- internal/handler/handler.go | 14 +- internal/processor/gin_writer.go | 12 +- .../{auth_resolver.go => auth_resolver_v1.go} | 10 ++ 11 files changed, 210 insertions(+), 258 deletions(-) delete mode 100644 internal/app/auth.go rename internal/app/{login.go => handlers/logon.go} (50%) create mode 100644 internal/app/handlers/upload_file.go delete mode 100644 internal/app/middleware.go delete mode 100644 internal/app/upload_file.go rename internal/resolvers/auth/{auth_resolver.go => auth_resolver_v1.go} (93%) diff --git a/cmd/main.go b/cmd/main.go index a20812a..f5ce44d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,7 +26,7 @@ func main() { engine := filesengine.NewFilesEngine(nil, nil) - go app.New(pg, engine). + go app.New(ctx, pg, engine). Run(ctx) <-ctx.Done() diff --git a/internal/app/app.go b/internal/app/app.go index 90b02cf..b295687 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,14 +2,12 @@ package app import ( "context" - "errors" - "net/http" "git.optclblast.xyz/draincloud/draincloud-core/internal/app/handlers" - "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" filesengine "git.optclblast.xyz/draincloud/draincloud-core/internal/files_engine" "git.optclblast.xyz/draincloud/draincloud-core/internal/processor" resolvedispatcher "git.optclblast.xyz/draincloud/draincloud-core/internal/resolve_dispatcher" + "git.optclblast.xyz/draincloud/draincloud-core/internal/resolvers/auth" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" "github.com/gin-gonic/gin" ) @@ -23,12 +21,18 @@ type DrainCloud struct { } func New( + ctx context.Context, database storage.Database, filesEngine *filesengine.FilesEngine, ) *DrainCloud { mux := gin.Default() dispatcher := resolvedispatcher.New() + dispatcher.RegisterResolver( + ctx, + auth.AuthResolverV1Name, + auth.NewAuthResolver(database), + ) d := &DrainCloud{ database: database, @@ -39,16 +43,19 @@ func New( // Built-in auth component of DrainCloud-Core authGroup := mux.Group("/auth") { - // authGroup.POST("/register", d.Register) authGroup.POST("/register", d.ginProcessor.Process( handlers.NewRegisterHandler(database), )) - authGroup.POST("/logon", d.Login) + authGroup.POST("/logon", d.ginProcessor.Process( + handlers.NewLogonHandler(database), + )) } filesGroup := mux.Group("/files") { - filesGroup.POST("/upload", d.UploadFile) + filesGroup.POST("/upload", d.ginProcessor.Process( + handlers.NewUploadFileHandler(filesEngine), + )) } d.mux = mux @@ -59,23 +66,3 @@ func New( func (d *DrainCloud) Run(ctx context.Context) error { return d.mux.Run() } - -func writeError(ctx *gin.Context, err error) { - switch { - case errors.Is(err, ErrorAccessDenied): - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusForbidden, - Message: err.Error(), - }) - case errors.Is(err, ErrorSessionExpired): - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusForbidden, - Message: err.Error(), - }) - default: - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusInternalServerError, - Message: "Internal Error", - }) - } -} diff --git a/internal/app/auth.go b/internal/app/auth.go deleted file mode 100644 index 808e7ed..0000000 --- a/internal/app/auth.go +++ /dev/null @@ -1,63 +0,0 @@ -package app - -import ( - "crypto/rand" - "encoding/base64" - "errors" - "fmt" - "log/slog" - "net/http" - - "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" - "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" - "github.com/gin-gonic/gin" -) - -const ( - csrfTokenCookie = "__Csrf_token" - sessionTokenCookie = "__Session_token" -) - -var ( - ErrorUnauthorized = errors.New("unauthorized") -) - -func (d *DrainCloud) authorize(ctx *gin.Context) (*models.Session, error) { - session, err := d.getSession(ctx) - if err != nil && !errors.Is(err, http.ErrNoCookie) { - return nil, ErrorUnauthorized - } - - if session == nil { - return nil, ErrorUnauthorized - } - - if err := validateSession(ctx, session); err != nil { - // TODO add audit log entry - return nil, ErrorUnauthorized - } - - logger.Debug(ctx, "[authorize] user authorized", slog.String("session_id", session.ID.String())) - - return session, nil -} - -func validateLoginAndPassword(login, password string) error { - if len(login) < 4 { - return fmt.Errorf("login must be longer than 8 chars") - } - - if len(password) < 6 { - return fmt.Errorf("password must be longer than 8 chars") - } - - return nil -} - -func generateSessionToken(length int) (string, error) { - bytes := make([]byte, length) - if _, err := rand.Read(bytes); err != nil { - return "", fmt.Errorf("failed to generate token: %w", err) - } - return base64.URLEncoding.EncodeToString(bytes), nil -} diff --git a/internal/app/login.go b/internal/app/handlers/logon.go similarity index 50% rename from internal/app/login.go rename to internal/app/handlers/logon.go index 133614f..518075c 100644 --- a/internal/app/login.go +++ b/internal/app/handlers/logon.go @@ -1,84 +1,100 @@ -package app +package handlers import ( "bytes" + "context" + "encoding/json" "errors" "fmt" "log/slog" "net/http" "time" + "git.optclblast.xyz/draincloud/draincloud-core/internal/common" "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" + "git.optclblast.xyz/draincloud/draincloud-core/internal/errs" + "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" + "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" - "github.com/gin-gonic/gin" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) -var ( - ErrorAccessDenied = errors.New("access denied") - ErrorSessionExpired = errors.New("session expired") -) - -func (d *DrainCloud) Login(ctx *gin.Context) { - logger.Debug(ctx, "[Login] new request") - - req := new(domain.LoginRequest) - err := ctx.BindJSON(req) - if err != nil { - logger.Error(ctx, "[Login] failed to bind request", logger.Err(err)) - ctx.JSON(http.StatusBadRequest, map[string]string{ - "error": "bad request", - }) - return - } - - resp, err := d.login(ctx, req) - if err != nil { - logger.Error(ctx, "[Login] failed to login user", logger.Err(err)) - ctx.JSON(http.StatusInternalServerError, map[string]string{ - "error": err.Error(), - }) - return - } - - ctx.JSON(http.StatusOK, resp) +type LogonHandler struct { + *handler.BaseHandler + authStorage storage.AuthStorage } -func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain.LoginResponse, error) { - session, err := d.getSession(ctx) +func NewLogonHandler( + authStorage storage.AuthStorage, +) *LogonHandler { + h := &LogonHandler{ + authStorage: authStorage, + BaseHandler: handler.New(). + WithName("logonv1"). + WithRequiredResolveParams(), + } + h.WithProcessFunc(h.process) + return h +} + +func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error { + logger.Debug(ctx, "[Logon] new request") + + body := new(domain.LogonRequest) + err := json.Unmarshal(req.Body, body) + if err != nil { + logger.Error(ctx, "[Logon] failed to bind request", logger.Err(err)) + w.Write(ctx, map[string]string{ + "error": "bad request", + }, handler.WithCode(http.StatusBadRequest)) + return nil + } + + session, err := h.getSession(ctx, req) if err != nil && !errors.Is(err, http.ErrNoCookie) { - return nil, err + return err } if session != nil { - if err := validateSession(ctx, session); err != nil { + if err := validateSession(ctx, req, session); err != nil { // TODO add audit log entry - return nil, err + return err } - logger.Debug(ctx, "[login] user is already logged in", slog.String("session_id", session.ID.String())) - return &domain.LoginResponse{ + w.Write(ctx, &domain.LogonResponse{ Ok: true, - }, nil + }) + return nil } - logger.Debug(ctx, "[login] session not found. trying to authorize") + logger.Debug(ctx, "[login] session not founh. trying to authorize") + resp, err := h.login(ctx, body, session, w) + if err != nil { + logger.Error(ctx, "[Logon] failed to login user", logger.Err(err)) + return err + } + + w.Write(ctx, resp) + return nil +} + +func (h *LogonHandler) login(ctx context.Context, req *domain.LogonRequest, session *models.Session, w handler.Writer) (*domain.LogonResponse, error) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) if err != nil { logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err)) return nil, fmt.Errorf("failed to generate password hash: %w", err) } - user, err := d.database.GetUserByLogin(ctx, req.Login) + user, err := h.authStorage.GetUserByLogin(ctx, req.Login) if err != nil { return nil, fmt.Errorf("failed to fetch user by login: %w", err) } if bytes.Equal(passwordHash, user.PasswordHash) { logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal") - return nil, ErrorAccessDenied + return nil, errs.ErrorAccessDenied } sessionCreatedAt := time.Now() @@ -88,20 +104,20 @@ func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain. if err != nil { return nil, fmt.Errorf("failed to generate a session token: %w", err) } - ctx.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true) + w.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true) csrfToken, err := generateSessionToken(100) if err != nil { return nil, fmt.Errorf("failed to generate a csrf token: %w", err) } - ctx.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false) + w.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false) sessionID, err := uuid.NewV7() if err != nil { return nil, fmt.Errorf("failed to generate session id: %w", err) } - if _, err = d.database.AddSession(ctx, &models.Session{ + if _, err = h.authStorage.AddSession(ctx, &models.Session{ ID: sessionID, SessionToken: sessionToken, CsrfToken: csrfToken, @@ -114,17 +130,17 @@ func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain. // TODO add audit log entry - return &domain.LoginResponse{ + return &domain.LogonResponse{ Ok: true, }, nil } -func (d *DrainCloud) getSession(ctx *gin.Context) (*models.Session, error) { - token, err := ctx.Cookie(sessionTokenCookie) +func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*models.Session, error) { + token, err := common.GetValue[string](req.Metadata, sessionTokenCookie) if err != nil { return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err) } - csrfToken, err := ctx.Cookie(csrfTokenCookie) + csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie) if err != nil { return nil, fmt.Errorf("failed to fetch csrf cookie from request: %w", err) } @@ -133,7 +149,7 @@ func (d *DrainCloud) getSession(ctx *gin.Context) (*models.Session, error) { return nil, fmt.Errorf("session token or csrf token is empty") } - session, err := d.database.GetSession(ctx, token) + session, err := h.authStorage.GetSession(ctx, token) if err != nil { return nil, fmt.Errorf("failed to fetch session from repo: %w", err) } @@ -141,22 +157,22 @@ func (d *DrainCloud) getSession(ctx *gin.Context) (*models.Session, error) { return session, nil } -func validateSession(ctx *gin.Context, session *models.Session) error { +func validateSession(ctx context.Context, req *common.Request, session *models.Session) error { if session == nil { - return ErrorAccessDenied + return errs.ErrorAccessDenied } - csrfToken, err := ctx.Cookie(csrfTokenCookie) + csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie) if err != nil { return fmt.Errorf("failed to fetch csrf cookie from request: %w", err) } if session.CsrfToken != csrfToken { - return ErrorAccessDenied + return errs.ErrorAccessDenied } if session.ExpiredAt.Before(time.Now()) { - return ErrorSessionExpired + return errs.ErrorSessionExpired } return nil diff --git a/internal/app/handlers/upload_file.go b/internal/app/handlers/upload_file.go new file mode 100644 index 0000000..46fc5fb --- /dev/null +++ b/internal/app/handlers/upload_file.go @@ -0,0 +1,91 @@ +package handlers + +import ( + "context" + + "git.optclblast.xyz/draincloud/draincloud-core/internal/common" + filesengine "git.optclblast.xyz/draincloud/draincloud-core/internal/files_engine" + "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" + "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" + "git.optclblast.xyz/draincloud/draincloud-core/internal/resolvers/auth" +) + +const ( + maxFileSize = 10 << 30 +) + +type UploadFileHandler struct { + *handler.BaseHandler + filesEngine *filesengine.FilesEngine +} + +func NewUploadFileHandler( + filesEngine *filesengine.FilesEngine, +) *UploadFileHandler { + h := &UploadFileHandler{ + filesEngine: filesEngine, + BaseHandler: handler.New(). + WithName("uploadfilev1"). + WithRequiredResolveParams( + auth.AuthResolverV1Name, + ), + } + h.WithProcessFunc(h.process) + return h +} + +func (d *UploadFileHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error { + if err := req.RawReq.ParseMultipartForm(maxFileSize); err != nil { + logger.Error(ctx, "uploadFile handler error", logger.Err(err)) + return err + } + + // if err := d.uploadFile(ctx, userID); err != nil { + // logger.Error(ctx, "uploadFile handle", logger.Err(err)) + // writeError(ctx, err) + // return + // } + return nil +} + +// func (d *UploadFileHandler) uploadFile(ctx context.Context, req *common.Request) error { +// title := ctx.PostForm("file") +// logger.Info(ctx, "uploadFile", slog.Any("postForm data", spew.Sdump(title))) + +// file, header, err := req.RawReq.FormFile("file") +// if err != nil { +// return err +// } +// logger.Info(ctx, "uploadFile", slog.Any("header", spew.Sdump(header))) + +// data, err := io.ReadAll(file) +// if err != nil { +// return err +// } + +// ext := parseExtension(header.Filename) + +// id, err := d.filesEngine.SaveFile(ctx, filesengine.File{ +// Name: header.Filename, +// UserID: userID, +// Data: data, +// Ext: ext, +// Size: int64(len(data)), +// Type: "", // че такое type? +// }) +// if err != nil { +// return fmt.Errorf("failed to save file: %w", err) +// } +// logger.Debug(ctx, "new file id", "id", id) + +// return nil +// } + +// func parseExtension(filename string) string { +// parts := strings.Split(filename, ".") +// if len(parts) == 0 { +// return "" +// } + +// return parts[len(parts)-1] +// } diff --git a/internal/app/middleware.go b/internal/app/middleware.go deleted file mode 100644 index 40bd7f3..0000000 --- a/internal/app/middleware.go +++ /dev/null @@ -1,28 +0,0 @@ -package app - -import ( - "git.optclblast.xyz/draincloud/draincloud-core/internal/reqcontext" - "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" - "github.com/gin-gonic/gin" -) - -type authorizer interface { - authorize(ctx *gin.Context) (*models.Session, error) -} - -func WithAuth(handler gin.HandlerFunc, auth authorizer) gin.HandlerFunc { - return func(ctx *gin.Context) { - sess, err := auth.authorize(ctx) - if err != nil { - writeError(ctx, err) - ctx.Abort() - return - } - - authCtx := reqcontext.WithSession(ctx.Request.Context(), sess) - authCtx = reqcontext.WithUserID(authCtx, sess.UserID) - ctx.Request = ctx.Request.WithContext(authCtx) - - handler(ctx) - } -} diff --git a/internal/app/upload_file.go b/internal/app/upload_file.go deleted file mode 100644 index 8ca83b8..0000000 --- a/internal/app/upload_file.go +++ /dev/null @@ -1,81 +0,0 @@ -package app - -import ( - "fmt" - "io" - "log/slog" - "strings" - - filesengine "git.optclblast.xyz/draincloud/draincloud-core/internal/files_engine" - "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" - "git.optclblast.xyz/draincloud/draincloud-core/internal/reqcontext" - "github.com/davecgh/go-spew/spew" - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -const ( - maxFileSize = 10 << 30 -) - -func (d *DrainCloud) UploadFile(ctx *gin.Context) { - if err := ctx.Request.ParseMultipartForm(maxFileSize); err != nil { - logger.Error(ctx, "uploadFile handler error", logger.Err(err)) - writeError(ctx, err) - return - } - - userID, err := reqcontext.GetUserID(ctx) - if err != nil { - writeError(ctx, ErrorAccessDenied) - return - } - - if err := d.uploadFile(ctx, userID); err != nil { - logger.Error(ctx, "uploadFile handle", logger.Err(err)) - writeError(ctx, err) - return - } -} - -func (d *DrainCloud) uploadFile(ctx *gin.Context, userID uuid.UUID) error { - title := ctx.PostForm("file") - logger.Info(ctx, "uploadFile", slog.Any("postForm data", spew.Sdump(title))) - - file, header, err := ctx.Request.FormFile("file") - if err != nil { - return err - } - logger.Info(ctx, "uploadFile", slog.Any("header", spew.Sdump(header))) - - data, err := io.ReadAll(file) - if err != nil { - return err - } - - ext := parseExtension(header.Filename) - - id, err := d.filesEngine.SaveFile(ctx, filesengine.File{ - Name: header.Filename, - UserID: userID, - Data: data, - Ext: ext, - Size: int64(len(data)), - Type: "", // че такое type? - }) - if err != nil { - return fmt.Errorf("failed to save file: %w", err) - } - logger.Debug(ctx, "new file id", "id", id) - - return nil -} - -func parseExtension(filename string) string { - parts := strings.Split(filename, ".") - if len(parts) == 0 { - return "" - } - - return parts[len(parts)-1] -} diff --git a/internal/domain/requests.go b/internal/domain/requests.go index 65113e5..eadfc37 100644 --- a/internal/domain/requests.go +++ b/internal/domain/requests.go @@ -10,12 +10,12 @@ type RegisterResponse struct { Message string `json:"message"` } -type LoginRequest struct { +type LogonRequest struct { Login string `json:"login"` Password string `json:"password"` } -type LoginResponse struct { +type LogonResponse struct { Ok bool `json:"ok"` Message string `json:"message"` } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 716e865..65161c0 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -6,8 +6,20 @@ import ( "git.optclblast.xyz/draincloud/draincloud-core/internal/common" ) +type WriteOptions struct { + Code int +} + +type WriteOption func(opts *WriteOptions) + +func WithCode(code int) WriteOption { + return func(opts *WriteOptions) { + opts.Code = code + } +} + type Writer interface { - Write(ctx context.Context, resp any) + Write(ctx context.Context, resp any, opts ...WriteOption) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) } diff --git a/internal/processor/gin_writer.go b/internal/processor/gin_writer.go index fbdf225..3248210 100644 --- a/internal/processor/gin_writer.go +++ b/internal/processor/gin_writer.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" "github.com/gin-gonic/gin" ) @@ -17,8 +18,15 @@ func wrapGin(ctx *gin.Context) ginWriter { } } -func (w ginWriter) Write(ctx context.Context, resp any) { - w.ctx.JSON(http.StatusOK, resp) +func (w ginWriter) Write(ctx context.Context, resp any, opts ...handler.WriteOption) { + params := &handler.WriteOptions{ + Code: http.StatusOK, + } + for _, o := range opts { + o(params) + } + + w.ctx.JSON(params.Code, resp) } func (w ginWriter) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) { diff --git a/internal/resolvers/auth/auth_resolver.go b/internal/resolvers/auth/auth_resolver_v1.go similarity index 93% rename from internal/resolvers/auth/auth_resolver.go rename to internal/resolvers/auth/auth_resolver_v1.go index 49f3554..8f3aaeb 100644 --- a/internal/resolvers/auth/auth_resolver.go +++ b/internal/resolvers/auth/auth_resolver_v1.go @@ -15,6 +15,10 @@ import ( "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" ) +const ( + AuthResolverV1Name = "auth.v1" +) + const ( csrfTokenCookie = "__Csrf_token" sessionTokenCookie = "__Session_token" @@ -24,6 +28,12 @@ type AuthResolver struct { authStorage storage.AuthStorage } +func NewAuthResolver(authStorage storage.AuthStorage) *AuthResolver { + return &AuthResolver{ + authStorage: authStorage, + } +} + func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request) error { return r.authorize(ctx, req) }