v0.0.26
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
admin 2025-08-28 13:59:48 +03:00
parent 61e4d6fad3
commit 0647b62048
8 changed files with 40 additions and 588 deletions

18
go.mod
View File

@ -5,10 +5,6 @@ go 1.25rc3
require (
github.com/99designs/gqlgen v0.17.77
github.com/aws/aws-sdk-go v1.55.8
github.com/aws/aws-sdk-go-v2 v1.38.2
github.com/aws/aws-sdk-go-v2/config v1.31.4
github.com/aws/aws-sdk-go-v2/credentials v1.18.8
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.2
github.com/caarlos0/env/v8 v8.0.0
github.com/go-chi/chi/v5 v5.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
@ -26,20 +22,6 @@ require (
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

View File

@ -140,7 +140,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
FollowUser func(childComplexity int, followingID int) int
LikePost func(childComplexity int, postID int) int
@ -180,7 +180,6 @@ type ComplexityRoot struct {
Query struct {
Comments func(childComplexity int, postID int) int
GenerateUploadURL func(childComplexity int, filename string) int
GetChat func(childComplexity int, user1Id int, user2Id int) int
GetChatMessages func(childComplexity int, chatID int, limit int, offset int) int
GetFollowers func(childComplexity int, userID int, limit *int, offset *int) int
@ -235,12 +234,6 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
UploadURL struct {
ExpiresAt func(childComplexity int) int
Key func(childComplexity int) int
URL func(childComplexity int) int
}
User struct {
Avatar func(childComplexity int) int
CreatedAt func(childComplexity int) int
@ -284,7 +277,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)
@ -317,7 +310,6 @@ type QueryResolver interface {
Post(ctx context.Context, id int) (*domain.Post, error)
Posts(ctx context.Context) ([]*domain.Post, error)
PostsPaginated(ctx context.Context, limit int, offset int) ([]*domain.Post, error)
GenerateUploadURL(ctx context.Context, filename string) (*UploadURL, error)
GetUserPosts(ctx context.Context, userID int) ([]*domain.Post, error)
User(ctx context.Context, id int) (*domain.User, error)
Users(ctx context.Context) ([]*domain.User, error)
@ -741,7 +733,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 {
@ -1028,18 +1020,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Query.Comments(childComplexity, args["postID"].(int)), true
case "Query.generateUploadURL":
if e.complexity.Query.GenerateUploadURL == nil {
break
}
args, err := ec.field_Query_generateUploadURL_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.GenerateUploadURL(childComplexity, args["filename"].(string)), true
case "Query.getChat":
if e.complexity.Query.GetChat == nil {
break
@ -1369,27 +1349,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.UnfollowResult.Success(childComplexity), true
case "UploadURL.expiresAt":
if e.complexity.UploadURL.ExpiresAt == nil {
break
}
return e.complexity.UploadURL.ExpiresAt(childComplexity), true
case "UploadURL.key":
if e.complexity.UploadURL.Key == nil {
break
}
return e.complexity.UploadURL.Key(childComplexity), true
case "UploadURL.url":
if e.complexity.UploadURL.URL == nil {
break
}
return e.complexity.UploadURL.URL(childComplexity), true
case "User.avatar":
if e.complexity.User.Avatar == nil {
break
@ -1509,7 +1468,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,
)
@ -1727,11 +1685,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
}
@ -1936,17 +1899,6 @@ func (ec *executionContext) field_Query_comments_args(ctx context.Context, rawAr
return args, nil
}
func (ec *executionContext) field_Query_generateUploadURL_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := processArgField(ctx, rawArgs, "filename", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["filename"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_getChatMessages_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@ -4567,7 +4519,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)
@ -6589,69 +6541,6 @@ func (ec *executionContext) fieldContext_Query_postsPaginated(ctx context.Contex
return fc, nil
}
func (ec *executionContext) _Query_generateUploadURL(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_generateUploadURL(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().GenerateUploadURL(rctx, fc.Args["filename"].(string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*UploadURL)
fc.Result = res
return ec.marshalNUploadURL2ᚖtailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_generateUploadURL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "url":
return ec.fieldContext_UploadURL_url(ctx, field)
case "key":
return ec.fieldContext_UploadURL_key(ctx, field)
case "expiresAt":
return ec.fieldContext_UploadURL_expiresAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UploadURL", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_generateUploadURL_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Query_getUserPosts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_getUserPosts(ctx, field)
if err != nil {
@ -8632,138 +8521,6 @@ func (ec *executionContext) fieldContext_UnfollowResult_message(_ context.Contex
return fc, nil
}
func (ec *executionContext) _UploadURL_url(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_url(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _UploadURL_key(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_key(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Key, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_key(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _UploadURL_expiresAt(ctx context.Context, field graphql.CollectedField, obj *UploadURL) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UploadURL_expiresAt(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.ExpiresAt, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UploadURL_expiresAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UploadURL",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *domain.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_id(ctx, field)
if err != nil {
@ -11348,40 +11105,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", "s3TempKey"}
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 "s3TempKey":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("s3TempKey"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.S3TempKey = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj any) (domain.LoginInput, error) {
var it domain.LoginInput
asMap := map[string]any{}
@ -13073,28 +12796,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "generateUploadURL":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_generateUploadURL(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx,
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "getUserPosts":
field := field
@ -13810,55 +13511,6 @@ func (ec *executionContext) _UnfollowResult(ctx context.Context, sel ast.Selecti
return out
}
var uploadURLImplementors = []string{"UploadURL"}
func (ec *executionContext) _UploadURL(ctx context.Context, sel ast.SelectionSet, obj *UploadURL) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, uploadURLImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UploadURL")
case "url":
out.Values[i] = ec._UploadURL_url(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "key":
out.Values[i] = ec._UploadURL_key(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "expiresAt":
out.Values[i] = ec._UploadURL_expiresAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var userImplementors = []string{"User"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *domain.User) graphql.Marshaler {
@ -14701,11 +14353,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)
}
@ -15260,20 +14907,6 @@ func (ec *executionContext) marshalNUnfollowResult2ᚖtailly_back_v2ᚋinternal
return ec._UnfollowResult(ctx, sel, v)
}
func (ec *executionContext) marshalNUploadURL2tailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx context.Context, sel ast.SelectionSet, v UploadURL) graphql.Marshaler {
return ec._UploadURL(ctx, sel, &v)
}
func (ec *executionContext) marshalNUploadURL2ᚖtailly_back_v2ᚋinternalᚋhttpᚋgraphᚐUploadURL(ctx context.Context, sel ast.SelectionSet, v *UploadURL) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._UploadURL(ctx, sel, v)
}
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)
}

View File

@ -9,11 +9,6 @@ import (
"strconv"
)
type CreatePostInput struct {
Title string `json:"title"`
S3TempKey string `json:"s3TempKey"`
}
type FollowResult struct {
Success bool `json:"success"`
Message string `json:"message"`
@ -78,12 +73,6 @@ type UnfollowResult struct {
Message string `json:"message"`
}
type UploadURL struct {
URL string `json:"url"`
Key string `json:"key"`
ExpiresAt string `json:"expiresAt"`
}
type MessageStatus string
const (

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"tailly_back_v2/internal/domain"
"tailly_back_v2/pkg/S3"
"time"
)
@ -125,38 +124,19 @@ 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)
}
post, err := r.Services.Post.CreateWithS3Upload(ctx, userID, input.Title, input.S3TempKey)
post, err := r.Services.Post.Create(ctx, userID, title, content)
if err != nil {
return nil, fmt.Errorf("failed to create post: %w", err)
}
return post, nil
}
// GenerateUploadURL is the resolver for the generateUploadURL field.
func (r *queryResolver) GenerateUploadURL(ctx context.Context, filename string) (*UploadURL, error) {
userID, err := getUserIDFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("unauthorized: %w", err)
}
url, key, err := S3.GeneratePresignedUploadURL(userID, filename)
if err != nil {
return nil, fmt.Errorf("failed to generate upload URL: %w", err)
}
return &UploadURL{
URL: url,
Key: key,
ExpiresAt: time.Now().Add(15 * time.Minute).Format(time.RFC3339),
}, nil
}
// DeletePost is the resolver for the deletePost field.
func (r *mutationResolver) DeletePost(ctx context.Context, id int) (bool, error) {
userID, err := getUserIDFromContext(ctx)

View File

@ -170,26 +170,12 @@ type MarkNotificationReadResult {
message: String!
}
scalar Upload
input CreatePostInput {
title: String!
s3TempKey: String!
}
type UploadURL {
url: String!
key: String!
expiresAt: String!
}
# Запросы (получение данных)
type Query {
me: User! # Получить текущего пользователя
post(id: Int!): Post! # Получить пост по ID
posts: [Post!]! # Получить все посты
postsPaginated(limit: Int!, offset: Int!): [Post!]!
generateUploadURL(filename: String!): UploadURL!
getUserPosts(userId: Int!): [Post!]!
user(id: Int!): User! # Получить пользователя по ID
users: [User!]!
@ -200,14 +186,19 @@ type Query {
comments(postID: Int!): [Comment!]!
# Получить количество подписчиков
getFollowersCount(userId: Int!): Int!
# Получить количество подписок
getFollowingCount(userId: Int!): Int!
# Проверить подписку
isFollowing(followingId: Int!): Boolean!
# Получить список подписчиков
getFollowers(userId: Int!, limit: Int = 20, offset: Int = 0): FollowersResponse!
# Получить список подписок
getFollowing(userId: Int!, limit: Int = 20, offset: Int = 0): FollowingResponse!
# Получить уведомления о подписках
getSubscriptionNotifications(
unreadOnly: Boolean = false
@ -228,7 +219,8 @@ type Mutation {
refreshTokens(refreshToken: String!): Tokens!
# Создание поста
createPost(input: CreatePostInput!): Post!
createPost(title: String!, content: String!): Post!
# Создание комментария
createComment(postId: Int!, content: String!): Comment!

View File

@ -32,7 +32,6 @@ type PostService interface {
Update(ctx context.Context, id int, title, content string) (*domain.Post, error)
Delete(ctx context.Context, id int) error
GetPaginated(ctx context.Context, limit, offset int) ([]*domain.Post, error)
CreateWithS3Upload(ctx context.Context, authorID int, title, s3TempKey string) (*domain.Post, error)
}
// Реализация сервиса постов
@ -45,66 +44,6 @@ func NewPostService(postRepo repository.PostRepository) PostService {
return &postService{postRepo: postRepo}
}
func (s *postService) CreateWithS3Upload(ctx context.Context, authorID int, title, s3Key string) (*domain.Post, error) {
const op = "service/postService.CreateWithS3Upload"
// Валидация
if s3Key == "" {
return nil, errors.New("S3 key cannot be empty")
}
// Проверяем что ключ принадлежит пользователю и находится в правильной папке
if !isValidUserPostKey(authorID, s3Key) {
return nil, errors.New("invalid S3 key for user")
}
// Формируем URL для модерации
imageURL := fmt.Sprintf("https://s3.regru.cloud/tailly/%s", s3Key)
// Модерация изображения по URL
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.CheckImageURL(ctx, imageURL)
if err != nil {
// Удаляем файл при ошибке модерации
S3.DeleteFromS3(imageURL)
return nil, fmt.Errorf("%s: image moderation failed: %w", op, err)
}
if !allowed {
// Удаляем файл если не прошло модерацию
S3.DeleteFromS3(imageURL)
return nil, errors.New("image rejected by moderation service")
}
// Создаем пост с уже готовым URL
post := &domain.Post{
Title: title,
Content: imageURL,
AuthorID: authorID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.postRepo.Create(ctx, post); err != nil {
// Если не удалось сохранить пост, удаляем файл из S3
S3.DeleteFromS3(imageURL)
return nil, fmt.Errorf("%s: failed to save post: %w", op, err)
}
return post, nil
}
// Новая функция проверки ключа
func isValidUserPostKey(userID int, key string) bool {
// Проверяем что ключ принадлежит пользователю и находится в папке posts
// Формат: posts/{userID}/{timestamp}_{filename}
return strings.HasPrefix(key, fmt.Sprintf("posts/%d/", userID))
}
// Создание нового поста
func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) {
const op = "service/postService.Create"

View File

@ -1,40 +1,15 @@
package S3
import (
"context"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
)
// Получаем S3 клиент
func getS3Client() (*s3.Client, error) {
cfg, err := awsConfig.LoadDefaultConfig(context.TODO(),
awsConfig.WithRegion("ru-central1"),
awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
)),
)
if err != nil {
return nil, err
}
// Используем virtual-hosted-style endpoint
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://s3.regru.cloud")
o.UsePathStyle = false // Отключаем path-style
})
return client, nil
}
func DeleteFromS3(imageURL string) error {
const op = "s3.DeleteFromS3"
@ -45,12 +20,20 @@ func DeleteFromS3(imageURL string) error {
bucket := parts[0]
key := parts[1]
client, err := getS3Client()
if err != nil {
return fmt.Errorf("%s: failed to create S3 client: %w", op, err)
}
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String("ru-central1"),
Endpoint: aws.String("https://s3.regru.cloud"),
S3ForcePathStyle: aws.Bool(true),
Credentials: credentials.NewStaticCredentials(
"TJ946G2S1Z5FEI3I7DQQ",
"C2H2aITHRDpek8H921yhnrINZwDoADsjW3F6HURl",
"",
),
}))
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
svc := s3.New(sess)
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
@ -58,48 +41,13 @@ func DeleteFromS3(imageURL string) error {
return fmt.Errorf("%s: failed to delete object from S3: %w", op, err)
}
// Ждем пока объект исчезнет
waiter := s3.NewObjectNotExistsWaiter(client)
err = waiter.Wait(context.TODO(), &s3.HeadObjectInput{
err = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, 30*time.Second)
})
if err != nil {
return fmt.Errorf("%s: failed to wait for object deletion: %w", op, err)
}
return nil
}
func GeneratePresignedUploadURL(userID int, filename string) (string, string, error) {
const op = "s3.GeneratePresignedUploadURL"
client, err := getS3Client()
if err != nil {
return "", "", fmt.Errorf("%s: failed to create S3 client: %w", op, err)
}
uniqueKey := fmt.Sprintf("posts/%d/%d_%s", userID, time.Now().UnixNano(), filename)
presignClient := s3.NewPresignClient(client)
result, err := presignClient.PresignPutObject(context.TODO(),
&s3.PutObjectInput{
Bucket: aws.String("tailly"),
Key: aws.String(uniqueKey),
ContentLength: aws.Int64(10 * 1024 * 1024),
// Добавляем CORS headers через метаданные
Metadata: map[string]string{
"Access-Control-Allow-Origin": "https://tailly.ru",
"Access-Control-Allow-Methods": "PUT, POST, DELETE",
"Access-Control-Allow-Headers": "*",
},
},
s3.WithPresignExpires(15*time.Minute),
)
if err != nil {
return "", "", fmt.Errorf("%s: failed to generate presigned URL: %w", op, err)
}
return result.URL, uniqueKey, nil
}

View File

@ -3,9 +3,8 @@ package moderation
import (
"context"
pb "tailly_back_v2/pkg/moderation/proto"
"google.golang.org/grpc"
pb "tailly_back_v2/pkg/moderation/proto"
)
type ModerationClient struct {
@ -36,16 +35,6 @@ func (c *ModerationClient) CheckImage(ctx context.Context, imageData []byte) (bo
return resp.OverallDecision == "allowed", nil
}
func (c *ModerationClient) CheckImageURL(ctx context.Context, imageURL string) (bool, error) {
resp, err := c.client.CheckImage(ctx, &pb.ImageRequest{
ImageUrl: imageURL, // ← Передаем URL вместо данных!
})
if err != nil {
return false, err
}
return resp.OverallDecision == "allowed", nil
}
func (c *ModerationClient) Close() {
c.conn.Close()
}