reset to v0.0.23
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
madipo2611 2025-08-20 09:42:50 +03:00
parent 36b559d486
commit 470b0b5342
7 changed files with 66 additions and 152 deletions

1
go.mod
View File

@ -36,6 +36,7 @@ require (
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect github.com/sosodev/duration v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect golang.org/x/text v0.27.0 // indirect

1
go.sum
View File

@ -79,6 +79,7 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=

View File

@ -106,7 +106,7 @@ type ComplexityRoot struct {
ConfirmEmail func(childComplexity int, token string) int ConfirmEmail func(childComplexity int, token string) int
CreateChat func(childComplexity int, user1Id int, user2Id int) int CreateChat func(childComplexity int, user1Id int, user2Id int) int
CreateComment func(childComplexity int, postID int, content string) int CreateComment func(childComplexity int, postID int, content string) int
CreatePost func(childComplexity int, input CreatePostInput) int CreatePost func(childComplexity int, title string, content string) int
DeletePost func(childComplexity int, id int) int DeletePost func(childComplexity int, id int) int
LikePost func(childComplexity int, postID int) int LikePost func(childComplexity int, postID int) int
Login func(childComplexity int, input domain.LoginInput) int Login func(childComplexity int, input domain.LoginInput) int
@ -206,7 +206,7 @@ type MutationResolver interface {
Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error) Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error)
Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error) Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error)
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
CreatePost(ctx context.Context, input CreatePostInput) (*domain.Post, error) CreatePost(ctx context.Context, title string, content string) (*domain.Post, error)
CreateComment(ctx context.Context, postID int, content string) (*domain.Comment, error) CreateComment(ctx context.Context, postID int, content string) (*domain.Comment, error)
LikePost(ctx context.Context, postID int) (*domain.Like, error) LikePost(ctx context.Context, postID int) (*domain.Like, error)
UnlikePost(ctx context.Context, postID int) (bool, error) UnlikePost(ctx context.Context, postID int) (bool, error)
@ -534,7 +534,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false return 0, false
} }
return e.complexity.Mutation.CreatePost(childComplexity, args["input"].(CreatePostInput)), true return e.complexity.Mutation.CreatePost(childComplexity, args["title"].(string), args["content"].(string)), true
case "Mutation.deletePost": case "Mutation.deletePost":
if e.complexity.Mutation.DeletePost == nil { if e.complexity.Mutation.DeletePost == nil {
@ -1003,7 +1003,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
opCtx := graphql.GetOperationContext(ctx) opCtx := graphql.GetOperationContext(ctx)
ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)}
inputUnmarshalMap := graphql.BuildUnmarshalerMap( inputUnmarshalMap := graphql.BuildUnmarshalerMap(
ec.unmarshalInputCreatePostInput,
ec.unmarshalInputLoginInput, ec.unmarshalInputLoginInput,
ec.unmarshalInputRegisterInput, ec.unmarshalInputRegisterInput,
) )
@ -1221,11 +1220,16 @@ func (ec *executionContext) field_Mutation_createComment_args(ctx context.Contex
func (ec *executionContext) field_Mutation_createPost_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_createPost_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
arg0, err := processArgField(ctx, rawArgs, "input", ec.unmarshalNCreatePostInput2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐCreatePostInput) arg0, err := processArgField(ctx, rawArgs, "title", ec.unmarshalNString2string)
if err != nil { if err != nil {
return nil, err return nil, err
} }
args["input"] = arg0 args["title"] = arg0
arg1, err := processArgField(ctx, rawArgs, "content", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["content"] = arg1
return args, nil return args, nil
} }
@ -3081,7 +3085,7 @@ func (ec *executionContext) _Mutation_createPost(ctx context.Context, field grap
}() }()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreatePost(rctx, fc.Args["input"].(CreatePostInput)) return ec.resolvers.Mutation().CreatePost(rctx, fc.Args["title"].(string), fc.Args["content"].(string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -8125,40 +8129,6 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field
// region **************************** input.gotpl ***************************** // region **************************** input.gotpl *****************************
func (ec *executionContext) unmarshalInputCreatePostInput(ctx context.Context, obj any) (CreatePostInput, error) {
var it CreatePostInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"title", "image"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "title":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("title"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Title = data
case "image":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("image"))
data, err := ec.unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
if err != nil {
return it, err
}
it.Image = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj any) (domain.LoginInput, error) { func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj any) (domain.LoginInput, error) {
var it domain.LoginInput var it domain.LoginInput
asMap := map[string]any{} asMap := map[string]any{}
@ -10570,11 +10540,6 @@ func (ec *executionContext) marshalNComment2ᚖtailly_back_v2ᚋinternalᚋdomai
return ec._Comment(ctx, sel, v) return ec._Comment(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNCreatePostInput2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐCreatePostInput(ctx context.Context, v any) (CreatePostInput, error) {
res, err := ec.unmarshalInputCreatePostInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNDevice2tailly_back_v2ᚋinternalᚋdomainᚐDevice(ctx context.Context, sel ast.SelectionSet, v domain.Device) graphql.Marshaler { func (ec *executionContext) marshalNDevice2tailly_back_v2ᚋinternalᚋdomainᚐDevice(ctx context.Context, sel ast.SelectionSet, v domain.Device) graphql.Marshaler {
return ec._Device(ctx, sel, &v) return ec._Device(ctx, sel, &v)
} }
@ -10883,22 +10848,6 @@ func (ec *executionContext) marshalNTokens2ᚖtailly_back_v2ᚋinternalᚋdomain
return ec._Tokens(ctx, sel, v) return ec._Tokens(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v any) (graphql.Upload, error) {
res, err := graphql.UnmarshalUpload(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, sel ast.SelectionSet, v graphql.Upload) graphql.Marshaler {
_ = sel
res := graphql.MarshalUpload(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
func (ec *executionContext) marshalNUser2tailly_back_v2ᚋinternalᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v domain.User) graphql.Marshaler { func (ec *executionContext) marshalNUser2tailly_back_v2ᚋinternalᚋdomainᚐUser(ctx context.Context, sel ast.SelectionSet, v domain.User) graphql.Marshaler {
return ec._User(ctx, sel, &v) return ec._User(ctx, sel, &v)
} }

View File

@ -7,15 +7,8 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"github.com/99designs/gqlgen/graphql"
) )
type CreatePostInput struct {
Title string `json:"title"`
Image graphql.Upload `json:"image"`
}
type Mutation struct { type Mutation struct {
} }

View File

@ -3,7 +3,6 @@ package graph
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"tailly_back_v2/internal/domain" "tailly_back_v2/internal/domain"
"time" "time"
) )
@ -125,35 +124,13 @@ func (r *mutationResolver) UnlikePost(ctx context.Context, postID int) (bool, er
} }
// CreatePost is the resolver for the createPost field. // CreatePost is the resolver for the createPost field.
func (r *mutationResolver) CreatePost(ctx context.Context, input CreatePostInput) (*domain.Post, error) { func (r *mutationResolver) CreatePost(ctx context.Context, title string, content string) (*domain.Post, error) {
userID, err := getUserIDFromContext(ctx) userID, err := getUserIDFromContext(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("unauthorized: %w", err) return nil, fmt.Errorf("unauthorized: %w", err)
} }
contentType := input.Image.ContentType
size := input.Image.Size
if size > 10_000_000 { post, err := r.Services.Post.Create(ctx, userID, title, content)
return nil, fmt.Errorf("image too large (max 10MB), got %d bytes", size)
}
// Проверяем поддерживаемые форматы
supportedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/webp": true,
}
if !supportedTypes[contentType] {
return nil, fmt.Errorf("unsupported image format: %s", contentType)
}
// Читаем бинарные данные
imageData, err := io.ReadAll(input.Image.File)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
post, err := r.Services.Post.Create(ctx, userID, input.Title, imageData, contentType, size)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create post: %w", err) return nil, fmt.Errorf("failed to create post: %w", err)
} }

View File

@ -119,14 +119,6 @@ type Query {
comments(postID: Int!): [Comment!]! comments(postID: Int!): [Comment!]!
} }
scalar Upload
input CreatePostInput {
title: String!
image: Upload!
}
# Мутации (изменение данных) # Мутации (изменение данных)
type Mutation { type Mutation {
# Регистрация нового пользователя # Регистрация нового пользователя
@ -139,7 +131,7 @@ type Mutation {
refreshTokens(refreshToken: String!): Tokens! refreshTokens(refreshToken: String!): Tokens!
# Создание поста # Создание поста
createPost(input: CreatePostInput!): Post! createPost(title: String!, content: String!): Post!
# Создание комментария # Создание комментария
createComment(postId: Int!, content: String!): Comment! createComment(postId: Int!, content: String!): Comment!

View File

@ -3,9 +3,12 @@ package service
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"image/jpeg"
"image/png"
"strings" "strings"
"tailly_back_v2/internal/domain" "tailly_back_v2/internal/domain"
"tailly_back_v2/internal/repository" "tailly_back_v2/internal/repository"
@ -21,7 +24,7 @@ import (
// Интерфейс сервиса постов // Интерфейс сервиса постов
type PostService interface { type PostService interface {
Create(ctx context.Context, authorID int, title string, imageData []byte, contentType string, size int64) (*domain.Post, error) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error)
GetByID(ctx context.Context, id int) (*domain.Post, error) GetByID(ctx context.Context, id int) (*domain.Post, error)
GetAll(ctx context.Context) ([]*domain.Post, error) GetAll(ctx context.Context) ([]*domain.Post, error)
GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error) GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error)
@ -40,25 +43,63 @@ func NewPostService(postRepo repository.PostRepository) PostService {
} }
// Создание нового поста // Создание нового поста
func (s *postService) Create(ctx context.Context, authorID int, title string, imageData []byte, contentType string, size int64) (*domain.Post, error) { func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) {
const op = "service/postService.Create" const op = "service/postService.Create"
// Валидация // Валидация
if len(imageData) == 0 { if content == "" {
return nil, errors.New("image data cannot be empty") return nil, errors.New("post content cannot be empty")
} }
if len(imageData) > 10_000_000 {
// Определяем формат и извлекаем данные
var base64Data, ext, contentType string
switch {
case strings.HasPrefix(content, "data:image/jpeg;base64,"):
base64Data = strings.TrimPrefix(content, "data:image/jpeg;base64,")
ext = "jpg"
contentType = "image/jpeg"
case strings.HasPrefix(content, "data:image/png;base64,"):
base64Data = strings.TrimPrefix(content, "data:image/png;base64,")
ext = "png"
contentType = "image/png"
case strings.HasPrefix(content, "data:image/webp;base64,"):
base64Data = strings.TrimPrefix(content, "data:image/webp;base64,")
ext = "webp"
contentType = "image/webp"
default:
return nil, errors.New("invalid image format, expected JPEG, PNG or WebP")
}
// Декодируем base64
imgData, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return nil, fmt.Errorf("%s: failed to decode base64: %w", op, err)
}
// Проверяем размер
if len(imgData) > 10_000_000 {
return nil, errors.New("image too large (max 10MB)") return nil, errors.New("image too large (max 10MB)")
} }
// Модерация изображения - передаем БИНАРНЫЕ ДАННЫЕ // Проверяем валидность изображения
switch ext {
case "jpg":
if _, err := jpeg.Decode(bytes.NewReader(imgData)); err != nil {
return nil, fmt.Errorf("%s: invalid JPEG image: %w", op, err)
}
case "png":
if _, err := png.Decode(bytes.NewReader(imgData)); err != nil {
return nil, fmt.Errorf("%s: invalid PNG image: %w", op, err)
}
}
// Модерация изображения
modClient, err := moderation.NewModerationClient("tailly_censor:50051") modClient, err := moderation.NewModerationClient("tailly_censor:50051")
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: failed to create moderation client: %w", op, err) return nil, fmt.Errorf("%s: failed to create moderation client: %w", op, err)
} }
defer modClient.Close() defer modClient.Close()
allowed, err := modClient.CheckImage(ctx, imageData) // ← Прямая передача bytes allowed, err := modClient.CheckImage(ctx, imgData)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: image moderation failed: %w", op, err) return nil, fmt.Errorf("%s: image moderation failed: %w", op, err)
} }
@ -85,7 +126,7 @@ func (s *postService) Create(ctx context.Context, authorID int, title string, im
_, err = uploader.Upload(&s3manager.UploadInput{ _, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String("tailly"), Bucket: aws.String("tailly"),
Key: aws.String(s3Key), Key: aws.String(s3Key),
Body: bytes.NewReader(imageData), // ← Бинарные данные Body: bytes.NewReader(imgData),
ContentType: aws.String(contentType), ContentType: aws.String(contentType),
}) })
if err != nil { if err != nil {
@ -109,46 +150,6 @@ func (s *postService) Create(ctx context.Context, authorID int, title string, im
return post, nil return post, nil
} }
func getContentTypeAndExtension(filename string) (string, string) {
switch {
case strings.HasSuffix(strings.ToLower(filename), ".jpg"), strings.HasSuffix(strings.ToLower(filename), ".jpeg"):
return "image/jpeg", "jpg"
case strings.HasSuffix(strings.ToLower(filename), ".png"):
return "image/png", "png"
case strings.HasSuffix(strings.ToLower(filename), ".webp"):
return "image/webp", "webp"
default:
return "", ""
}
}
func isValidImage(data []byte, ext string) bool {
if len(data) < 4 {
return false
}
switch ext {
case "jpg", "jpeg":
// JPEG: FF D8 FF
return data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF
case "png":
// PNG: 89 50 4E 47
return data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47
case "webp":
// WebP: RIFF....WEBP
return len(data) > 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "WEBP"
default:
return false
}
}
func getFileExtension(filename string) string {
if idx := strings.LastIndex(filename, "."); idx != -1 {
return filename[idx:]
}
return ".jpg"
}
// Получение поста по ID // Получение поста по ID
func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) { func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) {
post, err := s.postRepo.GetByID(ctx, id) post, err := s.postRepo.GetByID(ctx, id)