This commit is contained in:
parent
36b559d486
commit
470b0b5342
1
go.mod
1
go.mod
@ -36,6 +36,7 @@ require (
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // 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/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
|
||||
1
go.sum
1
go.sum
@ -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/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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
|
||||
|
||||
@ -106,7 +106,7 @@ type ComplexityRoot struct {
|
||||
ConfirmEmail func(childComplexity int, token string) int
|
||||
CreateChat func(childComplexity int, user1Id int, user2Id int) 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
|
||||
LikePost func(childComplexity int, postID int) 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)
|
||||
Login(ctx context.Context, input domain.LoginInput) (*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)
|
||||
LikePost(ctx context.Context, postID int) (*domain.Like, 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 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":
|
||||
if e.complexity.Mutation.DeletePost == nil {
|
||||
@ -1003,7 +1003,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
|
||||
opCtx := graphql.GetOperationContext(ctx)
|
||||
ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)}
|
||||
inputUnmarshalMap := graphql.BuildUnmarshalerMap(
|
||||
ec.unmarshalInputCreatePostInput,
|
||||
ec.unmarshalInputLoginInput,
|
||||
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) {
|
||||
var err error
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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 {
|
||||
ec.Error(ctx, err)
|
||||
@ -8125,40 +8129,6 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field
|
||||
|
||||
// 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) {
|
||||
var it domain.LoginInput
|
||||
asMap := map[string]any{}
|
||||
@ -10570,11 +10540,6 @@ func (ec *executionContext) marshalNComment2ᚖtailly_back_v2ᚋinternalᚋdomai
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return ec._User(ctx, sel, &v)
|
||||
}
|
||||
|
||||
@ -7,15 +7,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
type CreatePostInput struct {
|
||||
Title string `json:"title"`
|
||||
Image graphql.Upload `json:"image"`
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ package graph
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"time"
|
||||
)
|
||||
@ -125,35 +124,13 @@ func (r *mutationResolver) UnlikePost(ctx context.Context, postID int) (bool, er
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unauthorized: %w", err)
|
||||
}
|
||||
contentType := input.Image.ContentType
|
||||
size := input.Image.Size
|
||||
|
||||
if size > 10_000_000 {
|
||||
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)
|
||||
post, err := r.Services.Post.Create(ctx, userID, title, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create post: %w", err)
|
||||
}
|
||||
|
||||
@ -119,14 +119,6 @@ type Query {
|
||||
comments(postID: Int!): [Comment!]!
|
||||
}
|
||||
|
||||
scalar Upload
|
||||
|
||||
input CreatePostInput {
|
||||
title: String!
|
||||
image: Upload!
|
||||
}
|
||||
|
||||
|
||||
# Мутации (изменение данных)
|
||||
type Mutation {
|
||||
# Регистрация нового пользователя
|
||||
@ -139,7 +131,7 @@ type Mutation {
|
||||
refreshTokens(refreshToken: String!): Tokens!
|
||||
|
||||
# Создание поста
|
||||
createPost(input: CreatePostInput!): Post!
|
||||
createPost(title: String!, content: String!): Post!
|
||||
|
||||
# Создание комментария
|
||||
createComment(postId: Int!, content: String!): Comment!
|
||||
|
||||
@ -3,9 +3,12 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"strings"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
@ -21,7 +24,7 @@ import (
|
||||
|
||||
// Интерфейс сервиса постов
|
||||
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)
|
||||
GetAll(ctx context.Context) ([]*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"
|
||||
|
||||
// Валидация
|
||||
if len(imageData) == 0 {
|
||||
return nil, errors.New("image data cannot be empty")
|
||||
if content == "" {
|
||||
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)")
|
||||
}
|
||||
|
||||
// Модерация изображения - передаем БИНАРНЫЕ ДАННЫЕ
|
||||
// Проверяем валидность изображения
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to create moderation client: %w", op, err)
|
||||
}
|
||||
defer modClient.Close()
|
||||
|
||||
allowed, err := modClient.CheckImage(ctx, imageData) // ← Прямая передача bytes
|
||||
allowed, err := modClient.CheckImage(ctx, imgData)
|
||||
if err != nil {
|
||||
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{
|
||||
Bucket: aws.String("tailly"),
|
||||
Key: aws.String(s3Key),
|
||||
Body: bytes.NewReader(imageData), // ← Бинарные данные
|
||||
Body: bytes.NewReader(imgData),
|
||||
ContentType: aws.String(contentType),
|
||||
})
|
||||
if err != nil {
|
||||
@ -109,46 +150,6 @@ func (s *postService) Create(ctx context.Context, authorID int, title string, im
|
||||
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
|
||||
func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) {
|
||||
post, err := s.postRepo.GetByID(ctx, id)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user