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)
go app.New(pg, engine).
go app.New(ctx, pg, engine).
Run(ctx)
<-ctx.Done()

View File

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

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 (
"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

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"`
}
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"`
}

View File

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

View File

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

View File

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