From 286a0fe826f4bdfe5faaf32350978bac856c5df0 Mon Sep 17 00:00:00 2001 From: optclblast Date: Sun, 29 Dec 2024 15:15:44 -0800 Subject: [PATCH 1/4] +1 --- go.mod | 1 + internal/app/app.go | 7 +- internal/common/request.go | 32 ++++++- internal/processor/gin_processor.go | 103 ++++++++++++++++++++++ internal/processor/gin_writer.go | 22 +++++ internal/processor/processor.go | 50 +---------- internal/resolve_dispatcher/dispatcher.go | 48 ++++++++++ 7 files changed, 213 insertions(+), 50 deletions(-) create mode 100644 internal/processor/gin_processor.go create mode 100644 internal/processor/gin_writer.go create mode 100644 internal/resolve_dispatcher/dispatcher.go diff --git a/go.mod b/go.mod index fd4c72d..4d3dc38 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.28.0 + golang.org/x/sync v0.8.0 ) require ( diff --git a/internal/app/app.go b/internal/app/app.go index 9c08abd..3956fef 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,8 @@ import ( "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/handler" + "git.optclblast.xyz/draincloud/draincloud-core/internal/processor" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" "github.com/gin-gonic/gin" ) @@ -15,6 +17,8 @@ type DrainCloud struct { mux *gin.Engine database storage.Database filesEngine *filesengine.FilesEngine + + ginProcessor processor.Processor[gin.HandlerFunc] } func New( @@ -31,7 +35,8 @@ func New( // Built-in auth component of DrainCloud-Core authGroup := mux.Group("/auth") { - authGroup.POST("/register", d.Register) + // authGroup.POST("/register", d.Register) + authGroup.POST("/register", d.ginProcessor.Process(&handler.Handler{})) authGroup.POST("/logon", d.Login) } diff --git a/internal/common/request.go b/internal/common/request.go index c3548ef..4f44ac7 100644 --- a/internal/common/request.go +++ b/internal/common/request.go @@ -13,6 +13,34 @@ type RequestPool struct { sp sync.Pool } +func (p *RequestPool) Get() *Request { + r, _ := p.sp.Get().(*Request) + return r +} + +func (p *RequestPool) Put(r *Request) { + r.ID = "" + r.Metadata = make(map[string]any) + r.ResolveValues = sync.Map{} + r.Session = nil + r.User = nil + r.Body = nil + p.sp.Put(r) +} + +func NewRequestPool() *RequestPool { + return &RequestPool{ + sp: sync.Pool{ + New: func() any { + return &Request{ + ResolveValues: sync.Map{}, + Metadata: make(map[string]any), + } + }, + }, + } +} + type Request struct { ID string Session *models.Session @@ -22,8 +50,8 @@ type Request struct { Body []byte } -// NewRequest builds a new *Request struct from raw http Request. No auth data validated. -func NewRequest(pool *RequestPool, req *http.Request) *Request { +// NewRequestFromHttp builds a new *Request struct from raw http Request. No auth data validated. +func NewRequestFromHttp(pool *RequestPool, req *http.Request) *Request { out := pool.sp.Get().(*Request) cookies := req.Cookies() diff --git a/internal/processor/gin_processor.go b/internal/processor/gin_processor.go new file mode 100644 index 0000000..94bf8bc --- /dev/null +++ b/internal/processor/gin_processor.go @@ -0,0 +1,103 @@ +package processor + +import ( + "context" + "errors" + "fmt" + "net/http" + + "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" + resolvedispatcher "git.optclblast.xyz/draincloud/draincloud-core/internal/resolve_dispatcher" + "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +type GinProcessor struct { + rp *common.RequestPool + authStorage storage.AuthStorage + resolveDispatcher *resolvedispatcher.ResolveDispatcher +} + +func NewGinProcessor( + authStorage storage.AuthStorage, + resolveDispatcher *resolvedispatcher.ResolveDispatcher, +) *GinProcessor { + return &GinProcessor{ + rp: common.NewRequestPool(), + authStorage: authStorage, + resolveDispatcher: resolveDispatcher, + } +} + +func (p *GinProcessor) Process(handler *handler.Handler) gin.HandlerFunc { + return func(ctx *gin.Context) { + req := p.rp.Get() + ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "__request_id", req.ID)) + + // 1. Resolve the resolvers, collect all data required + // 2. Try process oprional resolvers + err := p.resolve(ctx, handler, req) + if err != nil { + p.writeError(ctx, err) + return + } + + // 3. Call preprocessinf fn's, middlewares etc. + // .... + // 4. Call handler.ProcessFn + if err = handler.ProcessFn(ctx, req, wrapGin(ctx)); err != nil { + p.writeError(ctx, err) + return + } + } +} + +func (p *GinProcessor) resolve(ctx context.Context, h *handler.Handler, req *common.Request) error { + eg, ctx := errgroup.WithContext(ctx) + for _, r := range h.RequiredResolveParams { + resolver, err := p.resolveDispatcher.GetResolver(r) + if err != nil { + return fmt.Errorf("failed to resolve '%s' param: no resolver provided: %w", r, err) + } + + resolveValueName := r + eg.Go(func() error { + if resolveErr := resolver.Resolve(ctx, req); resolveErr != nil { + return fmt.Errorf("failed to resolve '%s' value: %w", resolveValueName, resolveErr) + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + return err + } + + return nil +} + +func (p *GinProcessor) writeError(ctx *gin.Context, err error) { + logger.Error(ctx, "error process request", logger.Err(err)) + switch { + case errors.Is(err, errs.ErrorAccessDenied): + ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ + Code: http.StatusForbidden, + Message: err.Error(), + }) + case errors.Is(err, errs.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/processor/gin_writer.go b/internal/processor/gin_writer.go new file mode 100644 index 0000000..a3c100e --- /dev/null +++ b/internal/processor/gin_writer.go @@ -0,0 +1,22 @@ +package processor + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" +) + +type ginWriter struct { + ctx *gin.Context +} + +func wrapGin(ctx *gin.Context) ginWriter { + return ginWriter{ + ctx: ctx, + } +} + +func (w ginWriter) Write(ctx context.Context, resp any) { + w.ctx.JSON(http.StatusOK, resp) +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 6fbcc66..aac927a 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -1,51 +1,7 @@ package processor -import ( - "errors" - "net/http" +import "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" - "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/storage" - "github.com/gin-gonic/gin" -) - -type Processor struct { - rp *common.RequestPool - authStorage storage.AuthStorage -} - -func (p *Processor) Process(handler *handler.Handler) gin.HandlerFunc { - return func(ctx *gin.Context) { - //req := common.NewRequest(p.rp, ctx.Request) - // if handler.WithAuth { - // if err := p.authorize(ctx, req); err != nil { - // p.writeError(ctx, err) - // return - // } - // } - - } -} - -func (p *Processor) writeError(ctx *gin.Context, err error) { - switch { - case errors.Is(err, errs.ErrorAccessDenied): - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusForbidden, - Message: err.Error(), - }) - case errors.Is(err, errs.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", - }) - } +type Processor[H any] interface { + Process(*handler.Handler) H } diff --git a/internal/resolve_dispatcher/dispatcher.go b/internal/resolve_dispatcher/dispatcher.go new file mode 100644 index 0000000..78a2469 --- /dev/null +++ b/internal/resolve_dispatcher/dispatcher.go @@ -0,0 +1,48 @@ +package resolvedispatcher + +import ( + "context" + "fmt" + "sync" + + "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" + "git.optclblast.xyz/draincloud/draincloud-core/internal/resolvers" +) + +type ResolveDispatcher struct { + m sync.RWMutex + router map[string]resolvers.Resolver +} + +func New() *ResolveDispatcher { + return &ResolveDispatcher{ + router: map[string]resolvers.Resolver{}, + } +} + +func (d *ResolveDispatcher) RegisterResolver( + ctx context.Context, + resolverName string, + resolver resolvers.Resolver, +) { + d.m.Lock() + defer d.m.Unlock() + + if _, ok := d.router[resolverName]; ok { + logger.Fatal(ctx, fmt.Sprintf("resolver '%s' is already registered in router", resolverName)) + } + + d.router[resolverName] = resolver +} + +func (d *ResolveDispatcher) GetResolver(name string) (resolvers.Resolver, error) { + d.m.RLock() + defer d.m.RUnlock() + + res, ok := d.router[name] + if !ok { + return nil, fmt.Errorf("resolver '%s' not found", name) + } + + return res, nil +} -- 2.45.2 From 0202bd5dbbd01e54f9dcc4f1233987d4430d4fec Mon Sep 17 00:00:00 2001 From: optclblast Date: Mon, 30 Dec 2024 15:35:31 -0800 Subject: [PATCH 2/4] refactoring and fixes. test with /auth/register handler --- internal/app/app.go | 14 ++- internal/app/handlers/auth.go | 37 +++++++ internal/app/handlers/register.go | 120 +++++++++++++++++++++++ internal/common/request.go | 9 ++ internal/handler/handler.go | 40 +++++++- internal/processor/gin_processor.go | 10 +- internal/processor/gin_writer.go | 4 + internal/processor/processor.go | 2 +- internal/resolvers/auth/auth_resolver.go | 2 +- internal/resolvers/ip/ip.go | 7 ++ 10 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 internal/app/handlers/auth.go create mode 100644 internal/app/handlers/register.go create mode 100644 internal/resolvers/ip/ip.go diff --git a/internal/app/app.go b/internal/app/app.go index 3956fef..90b02cf 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,10 +5,11 @@ import ( "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/handler" "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/storage" "github.com/gin-gonic/gin" ) @@ -27,16 +28,21 @@ func New( ) *DrainCloud { mux := gin.Default() + dispatcher := resolvedispatcher.New() + d := &DrainCloud{ - database: database, - filesEngine: filesEngine, + database: database, + filesEngine: filesEngine, + ginProcessor: processor.NewGinProcessor(database, dispatcher), } // Built-in auth component of DrainCloud-Core authGroup := mux.Group("/auth") { // authGroup.POST("/register", d.Register) - authGroup.POST("/register", d.ginProcessor.Process(&handler.Handler{})) + authGroup.POST("/register", d.ginProcessor.Process( + handlers.NewRegisterHandler(database), + )) authGroup.POST("/logon", d.Login) } diff --git a/internal/app/handlers/auth.go b/internal/app/handlers/auth.go new file mode 100644 index 0000000..0f89e53 --- /dev/null +++ b/internal/app/handlers/auth.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" +) + +const ( + csrfTokenCookie = "__Csrf_token" + sessionTokenCookie = "__Session_token" +) + +var ( + ErrorUnauthorized = errors.New("unauthorized") +) + +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/handlers/register.go b/internal/app/handlers/register.go new file mode 100644 index 0000000..d35aba6 --- /dev/null +++ b/internal/app/handlers/register.go @@ -0,0 +1,120 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "git.optclblast.xyz/draincloud/draincloud-core/internal/common" + "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" + "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/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +type RegisterHandler struct { + *handler.BaseHandler + authStorage storage.AuthStorage +} + +func NewRegisterHandler( + authStorage storage.AuthStorage, +) *RegisterHandler { + h := &RegisterHandler{ + authStorage: authStorage, + BaseHandler: handler.New(). + WithName("registerv1"). + WithRequiredResolveParams(), + } + h.WithProcessFunc(h.process) + return h +} + +func (h *RegisterHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error { + regReq := new(domain.RegisterRequest) + + if err := json.Unmarshal(req.Body, regReq); err != nil { + return err + } + + resp, err := h.register(ctx, regReq, w) + if err != nil { + return fmt.Errorf("failed to register user: %w", err) + } + + w.Write(ctx, resp) + + return nil +} + +func (d *RegisterHandler) register( + ctx context.Context, + req *domain.RegisterRequest, + w handler.Writer, +) (*domain.RegisterResponse, error) { + if err := validateLoginAndPassword(req.Login, req.Password); err != nil { + return nil, fmt.Errorf("invalid creds: %w", err) + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) + if err != nil { + logger.Error(ctx, "[register] failed to generate password hash", logger.Err(err)) + return nil, fmt.Errorf("failed to generate password hash: %w", err) + } + + userID, err := uuid.NewV7() + if err != nil { + return nil, fmt.Errorf("failed to generate user id: %w", err) + } + + user := &models.User{ + ID: userID, + Username: req.Login, + Login: req.Login, + PasswordHash: passwordHash, + } + + err = d.authStorage.AddUser(ctx, userID, user.Login, user.Username, user.PasswordHash) + if err != nil { + return nil, fmt.Errorf("failed to add new user: %w", err) + } + + sessionCreatedAt := time.Now() + sessionExpiredAt := sessionCreatedAt.Add(time.Hour * 24 * 7) + + sessionToken, err := generateSessionToken(100) + if err != nil { + return nil, fmt.Errorf("failed to generate a session token: %w", err) + } + 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) + } + 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.authStorage.AddSession(ctx, &models.Session{ + ID: sessionID, + SessionToken: sessionToken, + CsrfToken: csrfToken, + UserID: user.ID, + CreatedAt: sessionCreatedAt, + ExpiredAt: sessionExpiredAt, + }); err != nil { + return nil, fmt.Errorf("failed to save session: %w", err) + } + + return &domain.RegisterResponse{ + Ok: true, + }, nil +} diff --git a/internal/common/request.go b/internal/common/request.go index 4f44ac7..602769d 100644 --- a/internal/common/request.go +++ b/internal/common/request.go @@ -1,10 +1,13 @@ package common import ( + "context" "fmt" + "io" "net/http" "sync" + "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" "github.com/google/uuid" ) @@ -67,6 +70,12 @@ func NewRequestFromHttp(pool *RequestPool, req *http.Request) *Request { out.Metadata[hname] = hval } + body, err := io.ReadAll(req.Body) + if err != nil { + logger.Error(context.TODO(), "failed to read request body", logger.Err(err)) + } + out.Body = body + reqID := uuid.NewString() out.ID = reqID return out diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 6edeb1c..73c8c65 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -8,10 +8,48 @@ import ( type Writer interface { Write(ctx context.Context, resp any) + SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) } -type Handler struct { +type Handler interface { + GetName() string + GetRequiredResolveParams() []string + GetProcessFn() func(ctx context.Context, req *common.Request, w Writer) error +} + +type BaseHandler struct { Name string RequiredResolveParams []string ProcessFn func(ctx context.Context, req *common.Request, w Writer) error } + +func New() *BaseHandler { + return new(BaseHandler) +} + +func (h *BaseHandler) WithName(name string) *BaseHandler { + h.Name = name + return h +} + +func (h *BaseHandler) WithRequiredResolveParams(params ...string) *BaseHandler { + h.RequiredResolveParams = params + return h +} + +func (h *BaseHandler) WithProcessFunc(fn func(ctx context.Context, req *common.Request, w Writer) error) *BaseHandler { + h.ProcessFn = fn + return h +} + +func (h *BaseHandler) GetName() string { + return h.Name +} + +func (h *BaseHandler) GetRequiredResolveParams() []string { + return h.RequiredResolveParams +} + +func (h *BaseHandler) GetProcessFn() func(ctx context.Context, req *common.Request, w Writer) error { + return h.ProcessFn +} diff --git a/internal/processor/gin_processor.go b/internal/processor/gin_processor.go index 94bf8bc..6a2aff1 100644 --- a/internal/processor/gin_processor.go +++ b/internal/processor/gin_processor.go @@ -34,9 +34,9 @@ func NewGinProcessor( } } -func (p *GinProcessor) Process(handler *handler.Handler) gin.HandlerFunc { +func (p *GinProcessor) Process(handler handler.Handler) gin.HandlerFunc { return func(ctx *gin.Context) { - req := p.rp.Get() + req := common.NewRequestFromHttp(p.rp, ctx.Request) ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "__request_id", req.ID)) // 1. Resolve the resolvers, collect all data required @@ -50,16 +50,16 @@ func (p *GinProcessor) Process(handler *handler.Handler) gin.HandlerFunc { // 3. Call preprocessinf fn's, middlewares etc. // .... // 4. Call handler.ProcessFn - if err = handler.ProcessFn(ctx, req, wrapGin(ctx)); err != nil { + if err = handler.GetProcessFn()(ctx, req, wrapGin(ctx)); err != nil { p.writeError(ctx, err) return } } } -func (p *GinProcessor) resolve(ctx context.Context, h *handler.Handler, req *common.Request) error { +func (p *GinProcessor) resolve(ctx context.Context, h handler.Handler, req *common.Request) error { eg, ctx := errgroup.WithContext(ctx) - for _, r := range h.RequiredResolveParams { + for _, r := range h.GetRequiredResolveParams() { resolver, err := p.resolveDispatcher.GetResolver(r) if err != nil { return fmt.Errorf("failed to resolve '%s' param: no resolver provided: %w", r, err) diff --git a/internal/processor/gin_writer.go b/internal/processor/gin_writer.go index a3c100e..fbdf225 100644 --- a/internal/processor/gin_writer.go +++ b/internal/processor/gin_writer.go @@ -20,3 +20,7 @@ func wrapGin(ctx *gin.Context) ginWriter { func (w ginWriter) Write(ctx context.Context, resp any) { w.ctx.JSON(http.StatusOK, resp) } + +func (w ginWriter) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) { + w.ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index aac927a..1c8b8a8 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -3,5 +3,5 @@ package processor import "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" type Processor[H any] interface { - Process(*handler.Handler) H + Process(handler.Handler) H } diff --git a/internal/resolvers/auth/auth_resolver.go b/internal/resolvers/auth/auth_resolver.go index 93b1a31..49f3554 100644 --- a/internal/resolvers/auth/auth_resolver.go +++ b/internal/resolvers/auth/auth_resolver.go @@ -73,7 +73,7 @@ func (d *AuthResolver) getSession(ctx context.Context, req *common.Request) (*mo return session, nil } -func validateSession(ctx context.Context, req *common.Request, session *models.Session) error { +func validateSession(_ context.Context, req *common.Request, session *models.Session) error { if session == nil { return errs.ErrorAccessDenied } diff --git a/internal/resolvers/ip/ip.go b/internal/resolvers/ip/ip.go new file mode 100644 index 0000000..c8a4531 --- /dev/null +++ b/internal/resolvers/ip/ip.go @@ -0,0 +1,7 @@ +package ip + +type IpResolver struct{} + +func New() *IpResolver { + return new(IpResolver) +} -- 2.45.2 From f44813431d433147b89785d8d2e6484129af5fc4 Mon Sep 17 00:00:00 2001 From: optclblast Date: Thu, 2 Jan 2025 10:44:28 -0800 Subject: [PATCH 3/4] +1 --- .../app/handlers/{auth.go => auth_common.go} | 0 internal/app/register.go | 105 ------------------ internal/common/request.go | 24 ++-- internal/common/request_test.go | 17 ++- internal/resolvers/ip/ip.go | 7 -- 5 files changed, 27 insertions(+), 126 deletions(-) rename internal/app/handlers/{auth.go => auth_common.go} (100%) delete mode 100644 internal/app/register.go delete mode 100644 internal/resolvers/ip/ip.go diff --git a/internal/app/handlers/auth.go b/internal/app/handlers/auth_common.go similarity index 100% rename from internal/app/handlers/auth.go rename to internal/app/handlers/auth_common.go diff --git a/internal/app/register.go b/internal/app/register.go deleted file mode 100644 index f5eb28d..0000000 --- a/internal/app/register.go +++ /dev/null @@ -1,105 +0,0 @@ -package app - -import ( - "fmt" - "net/http" - "time" - - "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" - "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" - "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "golang.org/x/crypto/bcrypt" -) - -func (d *DrainCloud) Register(ctx *gin.Context) { - logger.Debug(ctx, "[register] new request") - - // TODO check if registration is enabled - - req := new(domain.RegisterRequest) - err := ctx.BindJSON(req) - if err != nil { - logger.Error(ctx, "[register] failed to bind request", logger.Err(err)) - ctx.JSON(http.StatusBadRequest, map[string]string{ - "error": "bad request", - }) - return - } - - resp, err := d.register(ctx, req) - if err != nil { - logger.Error(ctx, "[register] failed to register user", logger.Err(err)) - ctx.JSON(http.StatusInternalServerError, map[string]string{ - "error": err.Error(), - }) - return - } - - ctx.JSON(http.StatusOK, resp) -} - -func (d *DrainCloud) register(ctx *gin.Context, req *domain.RegisterRequest) (*domain.RegisterResponse, error) { - if err := validateLoginAndPassword(req.Login, req.Password); err != nil { - return nil, fmt.Errorf("invalid creds: %w", err) - } - - passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10) - if err != nil { - logger.Error(ctx, "[register] failed to generate password hash", logger.Err(err)) - return nil, fmt.Errorf("failed to generate password hash: %w", err) - } - - userID, err := uuid.NewV7() - if err != nil { - return nil, fmt.Errorf("failed to generate user id: %w", err) - } - - user := &models.User{ - ID: userID, - Username: req.Login, - Login: req.Login, - PasswordHash: passwordHash, - } - - err = d.database.AddUser(ctx, userID, user.Login, user.Username, user.PasswordHash) - if err != nil { - return nil, fmt.Errorf("failed to add new user: %w", err) - } - - sessionCreatedAt := time.Now() - sessionExpiredAt := sessionCreatedAt.Add(time.Hour * 24 * 7) - - sessionToken, err := generateSessionToken(100) - 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) - - 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) - - 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{ - ID: sessionID, - SessionToken: sessionToken, - CsrfToken: csrfToken, - UserID: user.ID, - CreatedAt: sessionCreatedAt, - ExpiredAt: sessionExpiredAt, - }); err != nil { - return nil, fmt.Errorf("failed to save session: %w", err) - } - - return &domain.RegisterResponse{ - Ok: true, - }, nil -} diff --git a/internal/common/request.go b/internal/common/request.go index 602769d..aab27ed 100644 --- a/internal/common/request.go +++ b/internal/common/request.go @@ -23,8 +23,8 @@ func (p *RequestPool) Get() *Request { func (p *RequestPool) Put(r *Request) { r.ID = "" - r.Metadata = make(map[string]any) - r.ResolveValues = sync.Map{} + r.Metadata = &sync.Map{} + r.ResolveValues = &sync.Map{} r.Session = nil r.User = nil r.Body = nil @@ -36,8 +36,8 @@ func NewRequestPool() *RequestPool { sp: sync.Pool{ New: func() any { return &Request{ - ResolveValues: sync.Map{}, - Metadata: make(map[string]any), + ResolveValues: &sync.Map{}, + Metadata: &sync.Map{}, } }, }, @@ -48,9 +48,10 @@ type Request struct { ID string Session *models.Session User *models.User - ResolveValues sync.Map - Metadata map[string]any + ResolveValues *sync.Map + Metadata *sync.Map Body []byte + RawReq *http.Request } // NewRequestFromHttp builds a new *Request struct from raw http Request. No auth data validated. @@ -60,14 +61,15 @@ func NewRequestFromHttp(pool *RequestPool, req *http.Request) *Request { cookies := req.Cookies() headers := req.Header - out.Metadata = make(map[string]any, len(cookies)) + out.Metadata = &sync.Map{} + out.RawReq = req for _, cookie := range cookies { - out.Metadata[cookie.Name] = cookie.Value + out.Metadata.Store(cookie.Name, cookie.Value) } for hname, hval := range headers { - out.Metadata[hname] = hval + out.Metadata.Store(hname, hval) } body, err := io.ReadAll(req.Body) @@ -81,12 +83,12 @@ func NewRequestFromHttp(pool *RequestPool, req *http.Request) *Request { return out } -func GetValue[T any](vals map[string]any, key string) (T, error) { +func GetValue[T any](vals *sync.Map, key string) (T, error) { var out T if vals == nil { return out, fmt.Errorf("nil vals map") } - rawVal, ok := vals[key] + rawVal, ok := vals.Load(key) if !ok { return out, fmt.Errorf("value not found in resolve values set") } diff --git a/internal/common/request_test.go b/internal/common/request_test.go index 9898dd3..ae7f93c 100644 --- a/internal/common/request_test.go +++ b/internal/common/request_test.go @@ -2,6 +2,7 @@ package common import ( "reflect" + "sync" "testing" ) @@ -65,7 +66,7 @@ func TestGetValue_string(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetValue[string](tt.args.vals, tt.args.key) + got, err := GetValue[string](_mapToSyncMap(tt.args.vals), tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr) return @@ -151,7 +152,7 @@ func TestGetValue_struct(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetValue[val](tt.args.vals, tt.args.key) + got, err := GetValue[val](_mapToSyncMap(tt.args.vals), tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr) return @@ -237,7 +238,7 @@ func TestGetValue_structptr(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetValue[*val](tt.args.vals, tt.args.key) + got, err := GetValue[*val](_mapToSyncMap(tt.args.vals), tt.args.key) if (err != nil) != tt.wantErr { t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr) return @@ -248,3 +249,13 @@ func TestGetValue_structptr(t *testing.T) { }) } } + +func _mapToSyncMap(m map[string]any) *sync.Map { + out := &sync.Map{} + + for k, v := range m { + out.Store(k, v) + } + + return out +} diff --git a/internal/resolvers/ip/ip.go b/internal/resolvers/ip/ip.go deleted file mode 100644 index c8a4531..0000000 --- a/internal/resolvers/ip/ip.go +++ /dev/null @@ -1,7 +0,0 @@ -package ip - -type IpResolver struct{} - -func New() *IpResolver { - return new(IpResolver) -} -- 2.45.2 From d0f10cec62b7894533277b8d52e42619ec89b0fb Mon Sep 17 00:00:00 2001 From: optclblast Date: Thu, 2 Jan 2025 15:33:15 -0800 Subject: [PATCH 4/4] +1 --- internal/app/handlers/register.go | 1 + internal/common/request.go | 15 +++++++++------ internal/handler/handler.go | 11 +++++++++++ internal/processor/gin_processor.go | 8 ++++++-- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/app/handlers/register.go b/internal/app/handlers/register.go index d35aba6..dcae355 100644 --- a/internal/app/handlers/register.go +++ b/internal/app/handlers/register.go @@ -118,3 +118,4 @@ func (d *RegisterHandler) register( Ok: true, }, nil } + diff --git a/internal/common/request.go b/internal/common/request.go index aab27ed..b7bda24 100644 --- a/internal/common/request.go +++ b/internal/common/request.go @@ -45,13 +45,16 @@ func NewRequestPool() *RequestPool { } type Request struct { - ID string - Session *models.Session - User *models.User + ID string + Session *models.Session + User *models.User + // ResolveValues - data required to process request. ResolveValues *sync.Map - Metadata *sync.Map - Body []byte - RawReq *http.Request + // Metadata - an additional data, usually added with preprocessing. + Metadata *sync.Map + // Request body + Body []byte + RawReq *http.Request } // NewRequestFromHttp builds a new *Request struct from raw http Request. No auth data validated. diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 73c8c65..716e865 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -15,12 +15,14 @@ type Handler interface { GetName() string GetRequiredResolveParams() []string GetProcessFn() func(ctx context.Context, req *common.Request, w Writer) error + GetPreprocessFn() func(ctx context.Context, req *common.Request, w Writer) error } type BaseHandler struct { Name string RequiredResolveParams []string ProcessFn func(ctx context.Context, req *common.Request, w Writer) error + PreprocessFn func(ctx context.Context, req *common.Request, w Writer) error } func New() *BaseHandler { @@ -42,6 +44,11 @@ func (h *BaseHandler) WithProcessFunc(fn func(ctx context.Context, req *common.R return h } +func (h *BaseHandler) WithPreprocessFunc(fn func(ctx context.Context, req *common.Request, w Writer) error) *BaseHandler { + h.PreprocessFn = fn + return h +} + func (h *BaseHandler) GetName() string { return h.Name } @@ -53,3 +60,7 @@ func (h *BaseHandler) GetRequiredResolveParams() []string { func (h *BaseHandler) GetProcessFn() func(ctx context.Context, req *common.Request, w Writer) error { return h.ProcessFn } + +func (h *BaseHandler) GetPreprocessFn() func(ctx context.Context, req *common.Request, w Writer) error { + return h.PreprocessFn +} diff --git a/internal/processor/gin_processor.go b/internal/processor/gin_processor.go index 6a2aff1..b6433e9 100644 --- a/internal/processor/gin_processor.go +++ b/internal/processor/gin_processor.go @@ -47,8 +47,12 @@ func (p *GinProcessor) Process(handler handler.Handler) gin.HandlerFunc { return } - // 3. Call preprocessinf fn's, middlewares etc. - // .... + // 3. Call preprocessing fn's, middlewares etc. + if err = handler.GetPreprocessFn()(ctx, req, wrapGin(ctx)); err != nil { + p.writeError(ctx, err) + return + } + // 4. Call handler.ProcessFn if err = handler.GetProcessFn()(ctx, req, wrapGin(ctx)); err != nil { p.writeError(ctx, err) -- 2.45.2