handlers moved to new system, upload need to be fixed and resolvers must be tested

This commit is contained in:
r8zavetr8v 2025-01-02 16:08:22 -08:00
parent 7ce2b41ce7
commit 87985762bf
11 changed files with 210 additions and 258 deletions

View File

@ -26,7 +26,7 @@ func main() {
engine := filesengine.NewFilesEngine(nil, nil) engine := filesengine.NewFilesEngine(nil, nil)
go app.New(pg, engine). go app.New(ctx, pg, engine).
Run(ctx) Run(ctx)
<-ctx.Done() <-ctx.Done()

View File

@ -2,14 +2,12 @@ package app
import ( import (
"context" "context"
"errors"
"net/http"
"git.optclblast.xyz/draincloud/draincloud-core/internal/app/handlers" "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" filesengine "git.optclblast.xyz/draincloud/draincloud-core/internal/files_engine"
"git.optclblast.xyz/draincloud/draincloud-core/internal/processor" "git.optclblast.xyz/draincloud/draincloud-core/internal/processor"
resolvedispatcher "git.optclblast.xyz/draincloud/draincloud-core/internal/resolve_dispatcher" 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" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -23,12 +21,18 @@ type DrainCloud struct {
} }
func New( func New(
ctx context.Context,
database storage.Database, database storage.Database,
filesEngine *filesengine.FilesEngine, filesEngine *filesengine.FilesEngine,
) *DrainCloud { ) *DrainCloud {
mux := gin.Default() mux := gin.Default()
dispatcher := resolvedispatcher.New() dispatcher := resolvedispatcher.New()
dispatcher.RegisterResolver(
ctx,
auth.AuthResolverV1Name,
auth.NewAuthResolver(database),
)
d := &DrainCloud{ d := &DrainCloud{
database: database, database: database,
@ -39,16 +43,19 @@ func New(
// Built-in auth component of DrainCloud-Core // Built-in auth component of DrainCloud-Core
authGroup := mux.Group("/auth") authGroup := mux.Group("/auth")
{ {
// authGroup.POST("/register", d.Register)
authGroup.POST("/register", d.ginProcessor.Process( authGroup.POST("/register", d.ginProcessor.Process(
handlers.NewRegisterHandler(database), handlers.NewRegisterHandler(database),
)) ))
authGroup.POST("/logon", d.Login) authGroup.POST("/logon", d.ginProcessor.Process(
handlers.NewLogonHandler(database),
))
} }
filesGroup := mux.Group("/files") filesGroup := mux.Group("/files")
{ {
filesGroup.POST("/upload", d.UploadFile) filesGroup.POST("/upload", d.ginProcessor.Process(
handlers.NewUploadFileHandler(filesEngine),
))
} }
d.mux = mux d.mux = mux
@ -59,23 +66,3 @@ func New(
func (d *DrainCloud) Run(ctx context.Context) error { func (d *DrainCloud) Run(ctx context.Context) error {
return d.mux.Run() 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",
})
}
}

View File

@ -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
}

View File

@ -1,84 +1,100 @@
package app package handlers
import ( import (
"bytes" "bytes"
"context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain" "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/logger"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
var ( type LogonHandler struct {
ErrorAccessDenied = errors.New("access denied") *handler.BaseHandler
ErrorSessionExpired = errors.New("session expired") authStorage storage.AuthStorage
) }
func (d *DrainCloud) Login(ctx *gin.Context) { func NewLogonHandler(
logger.Debug(ctx, "[Login] new request") authStorage storage.AuthStorage,
) *LogonHandler {
h := &LogonHandler{
authStorage: authStorage,
BaseHandler: handler.New().
WithName("logonv1").
WithRequiredResolveParams(),
}
h.WithProcessFunc(h.process)
return h
}
req := new(domain.LoginRequest) func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error {
err := ctx.BindJSON(req) logger.Debug(ctx, "[Logon] new request")
body := new(domain.LogonRequest)
err := json.Unmarshal(req.Body, body)
if err != nil { if err != nil {
logger.Error(ctx, "[Login] failed to bind request", logger.Err(err)) logger.Error(ctx, "[Logon] failed to bind request", logger.Err(err))
ctx.JSON(http.StatusBadRequest, map[string]string{ w.Write(ctx, map[string]string{
"error": "bad request", "error": "bad request",
}) }, handler.WithCode(http.StatusBadRequest))
return return nil
} }
resp, err := d.login(ctx, req) session, err := h.getSession(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)
}
func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain.LoginResponse, error) {
session, err := d.getSession(ctx)
if err != nil && !errors.Is(err, http.ErrNoCookie) { if err != nil && !errors.Is(err, http.ErrNoCookie) {
return nil, err return err
} }
if session != nil { if session != nil {
if err := validateSession(ctx, session); err != nil { if err := validateSession(ctx, req, session); err != nil {
// TODO add audit log entry // 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())) 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, 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) passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil { if err != nil {
logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err)) logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err))
return nil, fmt.Errorf("failed to generate password hash: %w", 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 { if err != nil {
return nil, fmt.Errorf("failed to fetch user by login: %w", err) return nil, fmt.Errorf("failed to fetch user by login: %w", err)
} }
if bytes.Equal(passwordHash, user.PasswordHash) { if bytes.Equal(passwordHash, user.PasswordHash) {
logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal") logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal")
return nil, ErrorAccessDenied return nil, errs.ErrorAccessDenied
} }
sessionCreatedAt := time.Now() sessionCreatedAt := time.Now()
@ -88,20 +104,20 @@ func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain.
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate a session token: %w", err) 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) csrfToken, err := generateSessionToken(100)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate a csrf token: %w", err) 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() sessionID, err := uuid.NewV7()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate session id: %w", err) 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, ID: sessionID,
SessionToken: sessionToken, SessionToken: sessionToken,
CsrfToken: csrfToken, CsrfToken: csrfToken,
@ -114,17 +130,17 @@ func (d *DrainCloud) login(ctx *gin.Context, req *domain.LoginRequest) (*domain.
// TODO add audit log entry // TODO add audit log entry
return &domain.LoginResponse{ return &domain.LogonResponse{
Ok: true, Ok: true,
}, nil }, nil
} }
func (d *DrainCloud) getSession(ctx *gin.Context) (*models.Session, error) { func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*models.Session, error) {
token, err := ctx.Cookie(sessionTokenCookie) token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to fetch csrf cookie from request: %w", err) 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") 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 { if err != nil {
return nil, fmt.Errorf("failed to fetch session from repo: %w", err) 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 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 { if session == nil {
return ErrorAccessDenied return errs.ErrorAccessDenied
} }
csrfToken, err := ctx.Cookie(csrfTokenCookie) csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err) return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
} }
if session.CsrfToken != csrfToken { if session.CsrfToken != csrfToken {
return ErrorAccessDenied return errs.ErrorAccessDenied
} }
if session.ExpiredAt.Before(time.Now()) { if session.ExpiredAt.Before(time.Now()) {
return ErrorSessionExpired return errs.ErrorSessionExpired
} }
return nil return nil

View File

@ -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]
// }

View File

@ -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)
}
}

View File

@ -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]
}

View File

@ -10,12 +10,12 @@ type RegisterResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
type LoginRequest struct { type LogonRequest struct {
Login string `json:"login"` Login string `json:"login"`
Password string `json:"password"` Password string `json:"password"`
} }
type LoginResponse struct { type LogonResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Message string `json:"message"` Message string `json:"message"`
} }

View File

@ -6,8 +6,20 @@ import (
"git.optclblast.xyz/draincloud/draincloud-core/internal/common" "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 { 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) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net/http" "net/http"
"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -17,8 +18,15 @@ func wrapGin(ctx *gin.Context) ginWriter {
} }
} }
func (w ginWriter) Write(ctx context.Context, resp any) { func (w ginWriter) Write(ctx context.Context, resp any, opts ...handler.WriteOption) {
w.ctx.JSON(http.StatusOK, resp) 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) { func (w ginWriter) SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) {

View File

@ -15,6 +15,10 @@ import (
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
) )
const (
AuthResolverV1Name = "auth.v1"
)
const ( const (
csrfTokenCookie = "__Csrf_token" csrfTokenCookie = "__Csrf_token"
sessionTokenCookie = "__Session_token" sessionTokenCookie = "__Session_token"
@ -24,6 +28,12 @@ type AuthResolver struct {
authStorage storage.AuthStorage authStorage storage.AuthStorage
} }
func NewAuthResolver(authStorage storage.AuthStorage) *AuthResolver {
return &AuthResolver{
authStorage: authStorage,
}
}
func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request) error { func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request) error {
return r.authorize(ctx, req) return r.authorize(ctx, req)
} }