v0.0.1
This commit is contained in:
commit
a5a90bed7e
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/tailly_back_v2.iml" filepath="$PROJECT_DIR$/.idea/tailly_back_v2.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/tailly_back_v2.iml
generated
Normal file
9
.idea/tailly_back_v2.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
90
cmd/server/main.go
Normal file
90
cmd/server/main.go
Normal file
@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"tailly_back_v2/internal/config"
|
||||
"tailly_back_v2/internal/http"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"tailly_back_v2/internal/service"
|
||||
"tailly_back_v2/internal/ws"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
"tailly_back_v2/pkg/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Загрузка конфигурации
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Инициализация БД
|
||||
db, err := database.NewPostgres(cfg.Database.DSN)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect to database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Инициализация зависимостей
|
||||
tokenAuth := auth.NewTokenAuth(
|
||||
cfg.Auth.AccessTokenSecret,
|
||||
cfg.Auth.RefreshTokenSecret,
|
||||
cfg.Auth.AccessTokenExpiry,
|
||||
cfg.Auth.RefreshTokenExpiry,
|
||||
)
|
||||
|
||||
// Репозитории
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
postRepo := repository.NewPostRepository(db)
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
likeRepo := repository.NewLikeRepository(db)
|
||||
chatRepo := repository.NewChatRepository(db)
|
||||
// Сервисы
|
||||
services := service.NewServices(
|
||||
service.NewAuthService(userRepo, *tokenAuth),
|
||||
service.NewUserService(userRepo),
|
||||
service.NewPostService(postRepo),
|
||||
service.NewCommentService(commentRepo),
|
||||
service.NewLikeService(likeRepo),
|
||||
service.NewChatService(chatRepo),
|
||||
)
|
||||
// Инициализация чата
|
||||
chatHub := ws.NewChatHub()
|
||||
go chatHub.Run()
|
||||
|
||||
chatService := service.NewChatService(
|
||||
chatRepo,
|
||||
userRepo,
|
||||
chatHub,
|
||||
encryptionService,
|
||||
)
|
||||
// HTTP сервер
|
||||
server := http.NewServer(cfg, services, tokenAuth)
|
||||
|
||||
// Запуск сервера в отдельной горутине
|
||||
go func() {
|
||||
if err := server.Run(); err != nil {
|
||||
log.Printf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Ожидание сигнала завершения
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
// Graceful shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Printf("server shutdown error: %v", err)
|
||||
}
|
||||
|
||||
log.Println("server stopped")
|
||||
}
|
||||
30
go.mod
Normal file
30
go.mod
Normal file
@ -0,0 +1,30 @@
|
||||
module tailly_back_v2
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.72
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/vektah/gqlparser/v2 v2.5.25
|
||||
golang.org/x/crypto v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
70
go.sum
Normal file
70
go.sum
Normal file
@ -0,0 +1,70 @@
|
||||
github.com/99designs/gqlgen v0.17.72 h1:2JDAuutIYtAN26BAtigfLZFnTN53fpYbIENL8bVgAKY=
|
||||
github.com/99designs/gqlgen v0.17.72/go.mod h1:BoL4C3j9W2f95JeWMrSArdDNGWmZB9MOS2EMHJDZmUc=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
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/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.25 h1:FmWtFEa+invTIzWlWK6Vk7BVEZU/97QBzeI8Z1JjGt8=
|
||||
github.com/vektah/gqlparser/v2 v2.5.25/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
41
gqlgen.yml
Normal file
41
gqlgen.yml
Normal file
@ -0,0 +1,41 @@
|
||||
schema:
|
||||
- internal/http/graph/schema.graphql
|
||||
|
||||
exec:
|
||||
filename: internal/http/graph/generated.go
|
||||
package: graph
|
||||
|
||||
model:
|
||||
filename: internal/http/graph/models_gen.go
|
||||
package: graph
|
||||
|
||||
resolver:
|
||||
filename: internal/http/graph
|
||||
layout: follow-schema
|
||||
package: graph
|
||||
skip_generation:
|
||||
- "*_resolvers.go"
|
||||
- "resolvers.go"
|
||||
models:
|
||||
User:
|
||||
model: tailly_back_v2/internal/domain.User
|
||||
Post:
|
||||
model: tailly_back_v2/internal/domain.Post
|
||||
RegisterInput:
|
||||
model: tailly_back_v2/internal/domain.RegisterInput
|
||||
LoginInput:
|
||||
model: tailly_back_v2/internal/domain.LoginInput
|
||||
Comment:
|
||||
model: tailly_back_v2/internal/domain.Comment
|
||||
Like:
|
||||
model: tailly_back_v2/internal/domain.like
|
||||
Tokens:
|
||||
model: tailly_back_v2/internal/domain.Tokens
|
||||
Chat:
|
||||
model: tailly_back_v2/internal/domain.Chat
|
||||
Message:
|
||||
model: tailly_back_v2/internal/domain.Message
|
||||
WSMessage:
|
||||
model: tailly_back_v2/internal/domain.WSMessage
|
||||
autobind:
|
||||
- "tailly_back_v2/internal/domain"
|
||||
21
internal/config/config.go
Normal file
21
internal/config/config.go
Normal file
@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Host string `env:"SERVER_HOST" env-default:"localhost"`
|
||||
Port string `env:"SERVER_PORT" env-default:"3006"`
|
||||
}
|
||||
Database struct {
|
||||
DSN string `env:"DB_DSN,required"`
|
||||
}
|
||||
Auth struct {
|
||||
AccessTokenSecret string `env:"ACCESS_TOKEN_SECRET,required"`
|
||||
RefreshTokenSecret string `env:"REFRESH_TOKEN_SECRET,required"`
|
||||
AccessTokenExpiry time.Duration `env:"ACCESS_TOKEN_EXPIRY" env-default:"15m"`
|
||||
RefreshTokenExpiry time.Duration `env:"REFRESH_TOKEN_EXPIRY" env-default:"168h"` // 7 дней
|
||||
}
|
||||
}
|
||||
5
internal/config/prometheus.yml
Normal file
5
internal/config/prometheus.yml
Normal file
@ -0,0 +1,5 @@
|
||||
scrape_configs:
|
||||
- job_name: 'blog_api'
|
||||
scrape_interval: 15s
|
||||
static_configs:
|
||||
- targets: ['host.docker.internal:9100']
|
||||
28
internal/domain/chat.go
Normal file
28
internal/domain/chat.go
Normal file
@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Chat struct {
|
||||
ID int `json:"id"`
|
||||
User1ID int `json:"user1Id"`
|
||||
User2ID int `json:"user2Id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID int `json:"id"`
|
||||
ChatID int `json:"chatId"`
|
||||
SenderID int `json:"senderId"`
|
||||
Content string `json:"content"`
|
||||
Status string `json:"status"` // "sent", "delivered", "read"
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type WSMessage struct {
|
||||
Type string `json:"type"`
|
||||
MessageID int `json:"messageId"`
|
||||
ChatID int `json:"chatId"`
|
||||
SenderID int `json:"senderId"`
|
||||
Content string `json:"content"`
|
||||
Recipient int `json:"recipient"`
|
||||
}
|
||||
72
internal/domain/models.go
Normal file
72
internal/domain/models.go
Normal file
@ -0,0 +1,72 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Пользователь
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"-"` // Пароль не должен возвращаться в ответах
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Пост в блоге
|
||||
type Post struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
AuthorID int `json:"authorId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Комментарий к посту
|
||||
type Comment struct {
|
||||
ID int `json:"id"`
|
||||
Content string `json:"content"`
|
||||
PostID int `json:"postId"`
|
||||
AuthorID int `json:"authorId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Лайк к посту
|
||||
type Like struct {
|
||||
ID int `json:"id"`
|
||||
PostID int `json:"postId"`
|
||||
UserID int `json:"userId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// Токены для аутентификации
|
||||
type Tokens struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
// Типы для аутентификации
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type MutationResolver interface {
|
||||
CreatePost(ctx context.Context, title string, content string) (*Post, error)
|
||||
AddComment(ctx context.Context, postID int, content string) (*Comment, error)
|
||||
}
|
||||
|
||||
type QueryResolver interface {
|
||||
Posts(ctx context.Context) ([]*Post, error)
|
||||
Post(ctx context.Context, id int) (*Post, error)
|
||||
}
|
||||
38
internal/http/graph/chat_resolvers.go
Normal file
38
internal/http/graph/chat_resolvers.go
Normal file
@ -0,0 +1,38 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
type chatSessionResolver struct{ *Resolver }
|
||||
|
||||
// User is the resolver for the user field.
|
||||
func (r *chatSessionResolver) User(ctx context.Context, obj *domain.ChatSession) (*domain.User, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// LastMessage is the resolver for the lastMessage field.
|
||||
func (r *chatSessionResolver) LastMessage(ctx context.Context, obj *domain.ChatSession) (*domain.Message, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// UnreadCount is the resolver for the unreadCount field.
|
||||
func (r *chatSessionResolver) UnreadCount(ctx context.Context, obj *domain.ChatSession) (int, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// Sender is the resolver for the sender field.
|
||||
func (r *messageResolver) Sender(ctx context.Context, obj *domain.Message) (*domain.User, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// Receiver is the resolver for the receiver field.
|
||||
func (r *messageResolver) Receiver(ctx context.Context, obj *domain.Message) (*domain.User, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *messageResolver) CreatedAt(ctx context.Context, obj *domain.Message) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
29
internal/http/graph/comment_resolvers.go
Normal file
29
internal/http/graph/comment_resolvers.go
Normal file
@ -0,0 +1,29 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
// Comment is the resolver for the comment field.
|
||||
type commentResolver struct{ *Resolver }
|
||||
|
||||
// Author is the resolver for the author field.
|
||||
func (r *commentResolver) Author(ctx context.Context, obj *domain.Comment) (*domain.User, error) {
|
||||
return r.services.User.GetByID(ctx, obj.AuthorID)
|
||||
}
|
||||
|
||||
// Post is the resolver for the post field.
|
||||
func (r *commentResolver) Post(ctx context.Context, obj *domain.Comment) (*domain.Post, error) {
|
||||
return r.services.Post.GetByID(ctx, obj.PostID)
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *commentResolver) CreatedAt(ctx context.Context, obj *domain.Comment) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// UpdatedAt is the resolver for the updatedAt field.
|
||||
func (r *commentResolver) UpdatedAt(ctx context.Context, obj *domain.Comment) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
9181
internal/http/graph/generated.go
Normal file
9181
internal/http/graph/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
24
internal/http/graph/like_resolvers.go
Normal file
24
internal/http/graph/like_resolvers.go
Normal file
@ -0,0 +1,24 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
// LikeResolver реализует методы для работы с лайками в GraphQL
|
||||
type likeResolver struct{ *Resolver }
|
||||
|
||||
// User возвращает пользователя, который поставил лайк
|
||||
func (r *likeResolver) User(ctx context.Context, obj *domain.Like) (*domain.User, error) {
|
||||
return r.services.User.GetByID(ctx, obj.UserID)
|
||||
}
|
||||
|
||||
// Post возвращает пост, который был лайкнут
|
||||
func (r *likeResolver) Post(ctx context.Context, obj *domain.Like) (*domain.Post, error) {
|
||||
return r.services.Post.GetByID(ctx, obj.PostID)
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *likeResolver) CreatedAt(ctx context.Context, obj *domain.Like) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
12
internal/http/graph/models_gen.go
Normal file
12
internal/http/graph/models_gen.go
Normal file
@ -0,0 +1,12 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package graph
|
||||
|
||||
type Mutation struct {
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
}
|
||||
|
||||
type Subscription struct {
|
||||
}
|
||||
48
internal/http/graph/post_resolvers.go
Normal file
48
internal/http/graph/post_resolvers.go
Normal file
@ -0,0 +1,48 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
type postResolver struct{ *Resolver }
|
||||
|
||||
// Author is the resolver for the author field.
|
||||
func (r *postResolver) Author(ctx context.Context, obj *domain.Post) (*domain.User, error) {
|
||||
return r.services.User.GetByID(ctx, obj.AuthorID)
|
||||
}
|
||||
|
||||
// Comments is the resolver for the comments field.
|
||||
func (r *postResolver) Comments(ctx context.Context, obj *domain.Post) ([]*domain.Comment, error) {
|
||||
return r.services.Comment.GetByPostID(ctx, obj.ID)
|
||||
}
|
||||
|
||||
// Likes is the resolver for the likes field.
|
||||
func (r *postResolver) Likes(ctx context.Context, obj *domain.Post) ([]*domain.Like, error) {
|
||||
return r.services.Like.GetByPostID(ctx, obj.ID)
|
||||
}
|
||||
|
||||
// Post.likesCount - получение количества лайков для поста
|
||||
func (r *postResolver) LikesCount(ctx context.Context, obj *domain.Post) (int, error) {
|
||||
return r.services.Like.GetCountForPost(ctx, obj.ID)
|
||||
}
|
||||
|
||||
// Post.isLiked - проверка, поставил ли текущий пользователь лайк
|
||||
func (r *postResolver) IsLiked(ctx context.Context, obj *domain.Post) (bool, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, nil // Не авторизован - считаем что не лайкал
|
||||
}
|
||||
|
||||
return r.services.Like.CheckIfLiked(ctx, userID, obj.ID)
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *postResolver) CreatedAt(ctx context.Context, obj *domain.Post) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// UpdatedAt is the resolver for the updatedAt field.
|
||||
func (r *postResolver) UpdatedAt(ctx context.Context, obj *domain.Post) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
221
internal/http/graph/resolvers.go
Normal file
221
internal/http/graph/resolvers.go
Normal file
@ -0,0 +1,221 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/service"
|
||||
)
|
||||
|
||||
// Реализуем корневые резолверы
|
||||
type Resolver struct {
|
||||
services *service.Services
|
||||
}
|
||||
|
||||
// Ensure все интерфейсы реализованы
|
||||
var _ domain.QueryResolver = (*Resolver)(nil)
|
||||
var _ domain.MutationResolver = (*Resolver)(nil)
|
||||
|
||||
func NewResolver(services *service.Services) *Resolver {
|
||||
return &Resolver{services: services}
|
||||
}
|
||||
|
||||
// ChatSession returns ChatSessionResolver implementation.
|
||||
func (r *Resolver) ChatSession() ChatSessionResolver { return &chatSessionResolver{r} }
|
||||
|
||||
// Comment returns CommentResolver implementation.
|
||||
func (r *Resolver) Comment() CommentResolver { return &commentResolver{r} }
|
||||
|
||||
// Like returns LikeResolver implementation.
|
||||
func (r *Resolver) Like() LikeResolver { return &likeResolver{r} }
|
||||
|
||||
// Message returns MessageResolver implementation.
|
||||
func (r *Resolver) Message() MessageResolver { return &messageResolver{r} }
|
||||
|
||||
// Mutation returns MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
// Post returns PostResolver implementation.
|
||||
func (r *Resolver) Post() PostResolver { return &postResolver{r} }
|
||||
|
||||
// Query returns QueryResolver implementation.
|
||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
|
||||
// Subscription returns SubscriptionResolver implementation.
|
||||
func (r *Resolver) Subscription() SubscriptionResolver { return &subscriptionResolver{r} }
|
||||
|
||||
// User returns UserResolver implementation.
|
||||
func (r *Resolver) User() UserResolver { return &userResolver{r} }
|
||||
|
||||
// Query resolvers
|
||||
func (r *Resolver) Me(ctx context.Context) (*domain.User, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.services.User.GetByID(ctx, userID)
|
||||
}
|
||||
|
||||
func (r *Resolver) Posts(ctx context.Context) ([]*domain.Post, error) {
|
||||
return r.services.Post.GetAll(ctx)
|
||||
}
|
||||
|
||||
// Mutation resolvers
|
||||
func (r *Resolver) Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error) {
|
||||
return r.services.Auth.Register(ctx, service.RegisterInput{
|
||||
Username: input.Username,
|
||||
Email: input.Email,
|
||||
Password: input.Password,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Resolver) Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error) {
|
||||
return r.services.Auth.Login(ctx, input.Email, input.Password)
|
||||
}
|
||||
|
||||
// mutationResolver реализует MutationResolver интерфейс
|
||||
type mutationResolver struct{ *Resolver }
|
||||
|
||||
// Register is the resolver for the register field.
|
||||
func (r *mutationResolver) Register(ctx context.Context, input domain.RegisterInput) (*domain.User, error) {
|
||||
return r.services.Auth.Register(ctx, service.RegisterInput(input))
|
||||
}
|
||||
|
||||
// Login is the resolver for the login field.
|
||||
func (r *mutationResolver) Login(ctx context.Context, input domain.LoginInput) (*domain.Tokens, error) {
|
||||
return r.services.Auth.Login(ctx, input.Email, input.Password)
|
||||
}
|
||||
|
||||
// RefreshTokens is the resolver for the refreshTokens field.
|
||||
func (r *mutationResolver) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) {
|
||||
return r.services.Auth.RefreshTokens(ctx, refreshToken)
|
||||
}
|
||||
|
||||
// CreatePost is the resolver for the createPost field.
|
||||
func (r *mutationResolver) CreatePost(ctx context.Context, title string, content string) (*domain.Post, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.services.Post.Create(ctx, userID, title, content)
|
||||
}
|
||||
|
||||
// CreateComment is the resolver for the createComment field.
|
||||
func (r *mutationResolver) CreateComment(ctx context.Context, postID string, content string) (*domain.Comment, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.services.Comment.Create(ctx, postID, userID, content)
|
||||
}
|
||||
|
||||
// Mutation: likePost - добавление лайка к посту
|
||||
func (r *mutationResolver) LikePost(ctx context.Context, postID string) (*domain.Like, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, не поставил ли пользователь уже лайк
|
||||
liked, err := r.services.Like.CheckIfLiked(ctx, userID, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if liked {
|
||||
return nil, gqlerror.Errorf("you have already liked this post")
|
||||
}
|
||||
|
||||
return r.services.Like.LikePost(ctx, userID, postID)
|
||||
}
|
||||
|
||||
// Mutation: unlikePost - удаление лайка с поста
|
||||
func (r *mutationResolver) UnlikePost(ctx context.Context, postID string) (bool, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Проверяем, ставил ли пользователь лайк
|
||||
liked, err := r.services.Like.CheckIfLiked(ctx, userID, postID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !liked {
|
||||
return false, gqlerror.Errorf("you haven't liked this post yet")
|
||||
}
|
||||
|
||||
if err := r.services.Like.UnlikePost(ctx, userID, postID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Mutation.changePassword - смена пароля
|
||||
func (r *mutationResolver) ChangePassword(ctx context.Context, oldPassword string, newPassword string) (bool, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.services.User.ChangePassword(ctx, userID, oldPassword, newPassword); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendMessage is the resolver for the sendMessage field.
|
||||
func (r *mutationResolver) SendMessage(ctx context.Context, receiverID string, content string) (*domain.Message, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// MarkAsRead is the resolver for the markAsRead field.
|
||||
func (r *mutationResolver) MarkAsRead(ctx context.Context, messageID string) (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// queryResolver реализует QueryResolver интерфейс
|
||||
type queryResolver struct{ *Resolver }
|
||||
|
||||
// Me is the resolver for the me field.
|
||||
func (r *queryResolver) Me(ctx context.Context) (*domain.User, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.services.User.GetByID(ctx, userID)
|
||||
}
|
||||
|
||||
// Post is the resolver for the post field.
|
||||
func (r *queryResolver) Post(ctx context.Context, id string) (*domain.Post, error) {
|
||||
return r.services.Post.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// Posts is the resolver for the posts field.
|
||||
func (r *queryResolver) Posts(ctx context.Context) ([]*domain.Post, error) {
|
||||
return r.services.Post.GetAll(ctx)
|
||||
}
|
||||
|
||||
// GetChatHistory is the resolver for the getChatHistory field.
|
||||
func (r *queryResolver) GetChatHistory(ctx context.Context, userID string) ([]*domain.Message, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// GetUserChats is the resolver for the getUserChats field.
|
||||
func (r *queryResolver) GetUserChats(ctx context.Context) ([]*domain.ChatSession, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// getUserIDFromContext извлекает userID из контекста
|
||||
func getUserIDFromContext(ctx context.Context) (int, error) {
|
||||
userID, ok := ctx.Value("userID").(int)
|
||||
if !ok {
|
||||
return 0, errors.New("unauthorized")
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
117
internal/http/graph/schema.graphql
Normal file
117
internal/http/graph/schema.graphql
Normal file
@ -0,0 +1,117 @@
|
||||
# Тип пользователя
|
||||
type User {
|
||||
id: ID! # Уникальный идентификатор
|
||||
username: String! # Имя пользователя
|
||||
email: String! # Email (уникальный)
|
||||
createdAt: String! # Дата создания
|
||||
updatedAt: String! # Дата обновления
|
||||
}
|
||||
|
||||
# Пост в блоге
|
||||
type Post {
|
||||
id: ID! # Уникальный идентификатор
|
||||
title: String! # Заголовок поста
|
||||
content: String! # Содержание поста
|
||||
author: User! # Автор поста
|
||||
comments: [Comment!]! # Комментарии к посту
|
||||
likes: [Like!]! # Лайки к посту
|
||||
likesCount: Int!
|
||||
isLiked: Boolean!
|
||||
createdAt: String! # Дата создания
|
||||
updatedAt: String! # Дата обновления
|
||||
}
|
||||
|
||||
# Комментарий к посту
|
||||
type Comment {
|
||||
id: ID! # Уникальный идентификатор
|
||||
content: String! # Текст комментария
|
||||
post: Post! # Пост, к которому относится
|
||||
author: User! # Автор комментария
|
||||
createdAt: String! # Дата создания
|
||||
updatedAt: String! # Дата обновления
|
||||
}
|
||||
|
||||
# Лайк к посту
|
||||
type Like {
|
||||
id: ID! # Уникальный идентификатор
|
||||
post: Post! # Пост, который лайкнули
|
||||
user: User! # Пользователь, который поставил лайк
|
||||
createdAt: String! # Дата создания
|
||||
}
|
||||
|
||||
# Токены для аутентификации
|
||||
type Tokens {
|
||||
accessToken: String! # Access токен (короткоживущий)
|
||||
refreshToken: String! # Refresh токен (долгоживущий)
|
||||
}
|
||||
|
||||
|
||||
|
||||
type Message {
|
||||
id: ID!
|
||||
sender: User!
|
||||
receiver: User!
|
||||
content: String!
|
||||
createdAt: String!
|
||||
status: String!
|
||||
}
|
||||
|
||||
type ChatSession {
|
||||
user: User!
|
||||
lastMessage: Message!
|
||||
unreadCount: Int!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageReceived: Message!
|
||||
}
|
||||
|
||||
input RegisterInput {
|
||||
username: String!
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
# Запросы (получение данных)
|
||||
type Query {
|
||||
me: User! # Получить текущего пользователя
|
||||
post(id: ID!): Post! # Получить пост по ID
|
||||
posts: [Post!]! # Получить все посты
|
||||
user(id: ID!): User! # Получить пользователя по ID
|
||||
getChatHistory(userId: ID!): [Message!]!
|
||||
getUserChats: [ChatSession!]!
|
||||
}
|
||||
|
||||
# Мутации (изменение данных)
|
||||
type Mutation {
|
||||
# Регистрация нового пользователя
|
||||
register(input: RegisterInput!): User!
|
||||
|
||||
# Вход в систему
|
||||
login(input: LoginInput!): Tokens!
|
||||
|
||||
# Обновление токенов
|
||||
refreshTokens(refreshToken: String!): Tokens!
|
||||
|
||||
# Создание поста
|
||||
createPost(title: String!, content: String!): Post!
|
||||
|
||||
# Создание комментария
|
||||
createComment(postId: ID!, content: String!): Comment!
|
||||
|
||||
# Лайк поста
|
||||
likePost(postId: ID!): Like!
|
||||
|
||||
# Удаление лайка
|
||||
unlikePost(postId: ID!): Boolean!
|
||||
|
||||
updateProfile(username: String!, email: String!): User!
|
||||
changePassword(oldPassword: String!, newPassword: String!): Boolean!
|
||||
sendMessage(receiverId: ID!, content: String!): Message!
|
||||
markAsRead(messageId: ID!): Boolean!
|
||||
}
|
||||
3
internal/http/graph/schema.resolvers.go
Normal file
3
internal/http/graph/schema.resolvers.go
Normal file
@ -0,0 +1,3 @@
|
||||
package graph
|
||||
|
||||
// THIS CODE WILL BE UPDATED WITH SCHEMA CHANGES. PREVIOUS IMPLEMENTATION FOR SCHEMA CHANGES WILL BE KEPT IN THE COMMENT SECTION. IMPLEMENTATION FOR UNCHANGED SCHEMA WILL BE KEPT.
|
||||
13
internal/http/graph/subscription_resolvers.go
Normal file
13
internal/http/graph/subscription_resolvers.go
Normal file
@ -0,0 +1,13 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
type subscriptionResolver struct{ *Resolver }
|
||||
|
||||
// MessageReceived is the resolver for the messageReceived field.
|
||||
func (r *subscriptionResolver) MessageReceived(ctx context.Context) (<-chan *domain.Message, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
34
internal/http/graph/user_resolvers.go
Normal file
34
internal/http/graph/user_resolvers.go
Normal file
@ -0,0 +1,34 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
// userResolver реализует методы для работы с пользователями
|
||||
type userResolver struct{ *Resolver }
|
||||
|
||||
// Query.user - получение пользователя по ID
|
||||
func (r *queryResolver) User(ctx context.Context, id string) (*domain.User, error) {
|
||||
return r.services.User.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// CreatedAt is the resolver for the createdAt field.
|
||||
func (r *userResolver) CreatedAt(ctx context.Context, obj *domain.User) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// UpdatedAt is the resolver for the updatedAt field.
|
||||
func (r *userResolver) UpdatedAt(ctx context.Context, obj *domain.User) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// Mutation.updateProfile - обновление профиля
|
||||
func (r *mutationResolver) UpdateProfile(ctx context.Context, username string, email string) (*domain.User, error) {
|
||||
userID, err := getUserIDFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.services.User.UpdateProfile(ctx, userID, username, email)
|
||||
}
|
||||
79
internal/http/handlers/chat.go
Normal file
79
internal/http/handlers/chat.go
Normal file
@ -0,0 +1,79 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/service"
|
||||
"tailly_back_v2/internal/ws"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type ChatHandler struct {
|
||||
chatService service.ChatService
|
||||
hub *ws.ChatHub
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // В production заменить на проверку origin
|
||||
},
|
||||
}
|
||||
|
||||
func (h *ChatHandler) WebSocketConnection(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := strconv.Atoi(chi.URLParam(r, "userID"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid user ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client := &ws.Client{
|
||||
UserID: userID,
|
||||
Send: make(chan *domain.Message, 256),
|
||||
}
|
||||
|
||||
h.hub.Register(client)
|
||||
|
||||
// Горутина для чтения сообщений
|
||||
go h.readPump(conn, client)
|
||||
// Горутина для записи сообщений
|
||||
go h.writePump(conn, client)
|
||||
}
|
||||
|
||||
func (h *ChatHandler) readPump(conn *websocket.Conn, client *ws.Client) {
|
||||
defer func() {
|
||||
h.hub.Unregister(client)
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// Обработка входящих сообщений (если нужно)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) writePump(conn *websocket.Conn, client *ws.Client) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
message, ok := <-client.Send
|
||||
if !ok {
|
||||
conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(message); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
64
internal/http/middleware/auth.go
Normal file
64
internal/http/middleware/auth.go
Normal file
@ -0,0 +1,64 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
authorizationHeader = "Authorization"
|
||||
bearerPrefix = "Bearer "
|
||||
userIDKey = "userID" // Ключ для хранения userID в контексте
|
||||
)
|
||||
|
||||
// AuthMiddleware проверяет JWT токен и добавляет userID в контекст
|
||||
func AuthMiddleware(tokenAuth *auth.TokenAuth) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Пропускаем OPTIONS запросы (для CORS)
|
||||
if r.Method == http.MethodOptions {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем заголовок Authorization
|
||||
header := r.Header.Get(authorizationHeader)
|
||||
if header == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем формат заголовка
|
||||
if !strings.HasPrefix(header, bearerPrefix) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем токен
|
||||
token := strings.TrimPrefix(header, bearerPrefix)
|
||||
if token == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Валидируем токен
|
||||
userID, err := tokenAuth.ValidateAccessToken(token)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем userID в контекст
|
||||
ctx := context.WithValue(r.Context(), userIDKey, userID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserIDFromContext извлекает userID из контекста
|
||||
func GetUserIDFromContext(ctx context.Context) (int, bool) {
|
||||
userID, ok := ctx.Value(userIDKey).(int)
|
||||
return userID, ok
|
||||
}
|
||||
54
internal/http/middleware/cors.go
Normal file
54
internal/http/middleware/cors.go
Normal file
@ -0,0 +1,54 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CORS middleware настраивает политику кросс-доменных запросов
|
||||
func CORS(allowedOrigins []string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Проверяем, разрешен ли источник
|
||||
if isOriginAllowed(origin, allowedOrigins) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
"Accept, Content-Type, Content-Length, Accept-Encoding, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS")
|
||||
}
|
||||
|
||||
// Обрабатываем предварительные OPTIONS-запросы
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// isOriginAllowed проверяет разрешен ли домен для CORS
|
||||
func isOriginAllowed(origin string, allowedOrigins []string) bool {
|
||||
if origin == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Разрешаем все источники в development
|
||||
if len(allowedOrigins) == 1 && allowedOrigins[0] == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Точноe сравнение с разрешенными доменами
|
||||
for _, allowed := range allowedOrigins {
|
||||
if strings.EqualFold(origin, allowed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
83
internal/http/middleware/logging.go
Normal file
83
internal/http/middleware/logging.go
Normal file
@ -0,0 +1,83 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggingMiddleware логирует входящие HTTP-запросы
|
||||
func LoggingMiddleware(logger *log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Логируем основные параметры запроса
|
||||
logData := map[string]interface{}{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"query": r.URL.RawQuery,
|
||||
"ip": r.RemoteAddr,
|
||||
}
|
||||
|
||||
// Чтение тела запроса (для логирования)
|
||||
var bodyBytes []byte
|
||||
if r.Body != nil {
|
||||
bodyBytes, _ = io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if len(bodyBytes) > 0 {
|
||||
logData["body_size"] = len(bodyBytes)
|
||||
// Для JSON-запросов логируем тело
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
logData["body"] = string(bodyBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Перехват ответа
|
||||
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
|
||||
// Обработка запроса
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
// Дополняем данные для логирования
|
||||
duration := time.Since(start)
|
||||
logData["status"] = rw.status
|
||||
logData["duration"] = duration.String()
|
||||
logData["response_size"] = rw.size
|
||||
|
||||
// Форматированный вывод лога
|
||||
logger.Printf(
|
||||
"%s %s %d %s | IP: %s | Duration: %s | Body: %d bytes",
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
rw.status,
|
||||
http.StatusText(rw.status),
|
||||
r.RemoteAddr,
|
||||
duration,
|
||||
len(bodyBytes),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Кастомный responseWriter для перехвата статуса и размера ответа
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
size, err := rw.ResponseWriter.Write(b)
|
||||
rw.size += size
|
||||
return size, err
|
||||
}
|
||||
67
internal/http/middleware/metrics.go
Normal file
67
internal/http/middleware/metrics.go
Normal file
@ -0,0 +1,67 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
httpRequestsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
},
|
||||
[]string{"method", "path", "status"},
|
||||
)
|
||||
|
||||
httpRequestDuration = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "Duration of HTTP requests",
|
||||
Buckets: []float64{0.1, 0.5, 1, 2.5, 5, 10},
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
|
||||
httpResponseSize = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "http_response_size_bytes",
|
||||
Help: "Size of HTTP responses",
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
)
|
||||
|
||||
// MetricsMiddleware собирает метрики для Prometheus
|
||||
func MetricsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rw := &responseWriter{w, http.StatusOK, 0}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
status := strconv.Itoa(rw.status)
|
||||
|
||||
// Регистрируем метрики
|
||||
httpRequestsTotal.WithLabelValues(
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
status,
|
||||
).Inc()
|
||||
|
||||
httpRequestDuration.WithLabelValues(
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
).Observe(duration)
|
||||
|
||||
httpResponseSize.WithLabelValues(
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
).Observe(float64(rw.size))
|
||||
})
|
||||
}
|
||||
91
internal/http/server.go
Normal file
91
internal/http/server.go
Normal file
@ -0,0 +1,91 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"tailly_back_v2/internal/config"
|
||||
"tailly_back_v2/internal/http/graph"
|
||||
"tailly_back_v2/internal/http/middleware"
|
||||
"tailly_back_v2/internal/service"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
cfg *config.Config
|
||||
services *service.Services
|
||||
tokenAuth *auth.TokenAuth
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, services *service.Services, tokenAuth *auth.TokenAuth) *Server {
|
||||
s := &Server{
|
||||
router: chi.NewRouter(),
|
||||
cfg: cfg,
|
||||
services: services,
|
||||
tokenAuth: tokenAuth,
|
||||
}
|
||||
|
||||
s.configureRouter()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) configureRouter() {
|
||||
allowedOrigins := []string{
|
||||
"http://localhost:3000", // React dev server
|
||||
"https://your-production.app", // Продакшен домен
|
||||
}
|
||||
// Логирование
|
||||
logger := log.New(os.Stdout, "HTTP: ", log.LstdFlags)
|
||||
s.router.Use(middleware.LoggingMiddleware(logger))
|
||||
|
||||
// Метрики
|
||||
s.router.Use(middleware.MetricsMiddleware)
|
||||
// Middleware
|
||||
s.router.Use(middleware.CORS(allowedOrigins))
|
||||
s.router.Use(middleware.AuthMiddleware(s.tokenAuth))
|
||||
|
||||
// GraphQL handler
|
||||
resolver := graph.NewResolver(s.services)
|
||||
srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{
|
||||
Resolvers: resolver,
|
||||
}))
|
||||
|
||||
// Routes
|
||||
s.router.Handle("/", playground.Handler("GraphQL playground", "/query"))
|
||||
s.router.Handle("/query", srv)
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
return http.ListenAndServe(s.cfg.Server.Host+":"+s.cfg.Server.Port, s.router)
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
// Здесь можно добавить логику graceful shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// Отдельный endpoint для метрик Prometheus
|
||||
func (s *Server) configureMetrics() {
|
||||
prometheus.MustRegister(
|
||||
middleware.httpRequestsTotal,
|
||||
middleware.httpRequestDuration,
|
||||
middleware.httpResponseSize,
|
||||
)
|
||||
|
||||
metricsRouter := chi.NewRouter()
|
||||
metricsRouter.Get("/metrics", promhttp.Handler().ServeHTTP)
|
||||
|
||||
go func() {
|
||||
if err := http.ListenAndServe(":9100", metricsRouter); err != nil {
|
||||
log.Printf("Metrics server error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
206
internal/repository/chat_repository.go
Normal file
206
internal/repository/chat_repository.go
Normal file
@ -0,0 +1,206 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMessageNotFound = errors.New("message not found")
|
||||
ErrChatNotFound = errors.New("chat not found")
|
||||
)
|
||||
|
||||
type ChatRepository interface {
|
||||
// Основные методы сообщений
|
||||
SaveMessage(ctx context.Context, message *domain.Message) error
|
||||
GetMessageByID(ctx context.Context, id int) (*domain.Message, error)
|
||||
GetMessagesByChat(ctx context.Context, chatID int, limit, offset int) ([]*domain.Message, error)
|
||||
UpdateMessageStatus(ctx context.Context, id int, status string) error
|
||||
DeleteMessage(ctx context.Context, id int) error
|
||||
|
||||
// Методы чатов
|
||||
CreateChat(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error)
|
||||
GetChatByID(ctx context.Context, id int) (*domain.Chat, error)
|
||||
GetUserChats(ctx context.Context, userID int) ([]*domain.Chat, error)
|
||||
GetChatByParticipants(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error)
|
||||
}
|
||||
|
||||
type chatRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewChatRepository(db *sql.DB) ChatRepository {
|
||||
return &chatRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *chatRepository) SaveMessage(ctx context.Context, message *domain.Message) error {
|
||||
query := `
|
||||
INSERT INTO messages (chat_id, sender_id, content, status, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
message.ChatID,
|
||||
message.SenderID,
|
||||
message.Content,
|
||||
message.Status,
|
||||
message.CreatedAt,
|
||||
).Scan(&message.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *chatRepository) GetMessageByID(ctx context.Context, id int) (*domain.Message, error) {
|
||||
query := `
|
||||
SELECT id, chat_id, sender_id, content, status, created_at
|
||||
FROM messages
|
||||
WHERE id = $1
|
||||
`
|
||||
message := &domain.Message{}
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&message.ID,
|
||||
&message.ChatID,
|
||||
&message.SenderID,
|
||||
&message.Content,
|
||||
&message.Status,
|
||||
&message.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrMessageNotFound
|
||||
}
|
||||
return message, err
|
||||
}
|
||||
|
||||
func (r *chatRepository) GetMessagesByChat(ctx context.Context, chatID int, limit, offset int) ([]*domain.Message, error) {
|
||||
query := `
|
||||
SELECT id, chat_id, sender_id, content, status, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
rows, err := r.db.QueryContext(ctx, query, chatID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []*domain.Message
|
||||
for rows.Next() {
|
||||
var message domain.Message
|
||||
if err := rows.Scan(
|
||||
&message.ID,
|
||||
&message.ChatID,
|
||||
&message.SenderID,
|
||||
&message.Content,
|
||||
&message.Status,
|
||||
&message.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages = append(messages, &message)
|
||||
}
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (r *chatRepository) UpdateMessageStatus(ctx context.Context, id int, status string) error {
|
||||
query := `
|
||||
UPDATE messages
|
||||
SET status = $1
|
||||
WHERE id = $2
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, query, status, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *chatRepository) DeleteMessage(ctx context.Context, id int) error {
|
||||
query := `DELETE FROM messages WHERE id = $1`
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *chatRepository) CreateChat(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error) {
|
||||
query := `
|
||||
INSERT INTO chats (user1_id, user2_id, created_at)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`
|
||||
chat := &domain.Chat{
|
||||
User1ID: user1ID,
|
||||
User2ID: user2ID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
err := r.db.QueryRowContext(ctx, query, user1ID, user2ID, chat.CreatedAt).Scan(&chat.ID)
|
||||
return chat, err
|
||||
}
|
||||
|
||||
func (r *chatRepository) GetChatByID(ctx context.Context, id int) (*domain.Chat, error) {
|
||||
query := `
|
||||
SELECT id, user1_id, user2_id, created_at
|
||||
FROM chats
|
||||
WHERE id = $1
|
||||
`
|
||||
chat := &domain.Chat{}
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&chat.ID,
|
||||
&chat.User1ID,
|
||||
&chat.User2ID,
|
||||
&chat.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrChatNotFound
|
||||
}
|
||||
return chat, err
|
||||
}
|
||||
|
||||
func (r *chatRepository) GetUserChats(ctx context.Context, userID int) ([]*domain.Chat, error) {
|
||||
query := `
|
||||
SELECT id, user1_id, user2_id, created_at
|
||||
FROM chats
|
||||
WHERE user1_id = $1 OR user2_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := r.db.QueryContext(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var chats []*domain.Chat
|
||||
for rows.Next() {
|
||||
var chat domain.Chat
|
||||
if err := rows.Scan(
|
||||
&chat.ID,
|
||||
&chat.User1ID,
|
||||
&chat.User2ID,
|
||||
&chat.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chats = append(chats, &chat)
|
||||
}
|
||||
return chats, nil
|
||||
}
|
||||
|
||||
func (r *chatRepository) GetChatByParticipants(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error) {
|
||||
query := `
|
||||
SELECT id, user1_id, user2_id, created_at
|
||||
FROM chats
|
||||
WHERE (user1_id = $1 AND user2_id = $2)
|
||||
OR (user1_id = $2 AND user2_id = $1)
|
||||
LIMIT 1
|
||||
`
|
||||
chat := &domain.Chat{}
|
||||
err := r.db.QueryRowContext(ctx, query, user1ID, user2ID).Scan(
|
||||
&chat.ID,
|
||||
&chat.User1ID,
|
||||
&chat.User2ID,
|
||||
&chat.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrChatNotFound
|
||||
}
|
||||
return chat, err
|
||||
}
|
||||
80
internal/repository/comment_repository.go
Normal file
80
internal/repository/comment_repository.go
Normal file
@ -0,0 +1,80 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCommentNotFound = errors.New("comment not found")
|
||||
)
|
||||
|
||||
type CommentRepository interface {
|
||||
Create(ctx context.Context, comment *domain.Comment) error
|
||||
GetByID(ctx context.Context, id int) (*domain.Comment, error)
|
||||
GetByPostID(ctx context.Context, postID int) ([]*domain.Comment, error)
|
||||
Update(ctx context.Context, comment *domain.Comment) error
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
type commentRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCommentRepository(db *sql.DB) CommentRepository {
|
||||
return &commentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *commentRepository) Create(ctx context.Context, comment *domain.Comment) error {
|
||||
query := `
|
||||
INSERT INTO comments (content, post_id, author_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
comment.Content,
|
||||
comment.PostID,
|
||||
comment.AuthorID,
|
||||
comment.CreatedAt,
|
||||
comment.UpdatedAt,
|
||||
).Scan(&comment.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *commentRepository) GetByPostID(ctx context.Context, postID int) ([]*domain.Comment, error) {
|
||||
query := `
|
||||
SELECT id, content, post_id, author_id, created_at, updated_at
|
||||
FROM comments
|
||||
WHERE post_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var comments []*domain.Comment
|
||||
for rows.Next() {
|
||||
comment := &domain.Comment{}
|
||||
err := rows.Scan(
|
||||
&comment.ID,
|
||||
&comment.Content,
|
||||
&comment.PostID,
|
||||
&comment.AuthorID,
|
||||
&comment.CreatedAt,
|
||||
&comment.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
}
|
||||
|
||||
return comments, nil
|
||||
}
|
||||
96
internal/repository/like_repository.go
Normal file
96
internal/repository/like_repository.go
Normal file
@ -0,0 +1,96 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLikeNotFound = errors.New("like not found")
|
||||
ErrLikeAlreadyExists = errors.New("like already exists")
|
||||
)
|
||||
|
||||
type LikeRepository interface {
|
||||
Create(ctx context.Context, like *domain.Like) error
|
||||
GetByID(ctx context.Context, id int) (*domain.Like, error)
|
||||
GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error)
|
||||
GetByUserAndPost(ctx context.Context, userID, postID int) (*domain.Like, error)
|
||||
Delete(ctx context.Context, id int) error
|
||||
DeleteByUserAndPost(ctx context.Context, userID, postID int) error
|
||||
}
|
||||
|
||||
type likeRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewLikeRepository(db *sql.DB) LikeRepository {
|
||||
return &likeRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *likeRepository) Create(ctx context.Context, like *domain.Like) error {
|
||||
// Проверяем, существует ли уже лайк
|
||||
_, err := r.GetByUserAndPost(ctx, like.UserID, like.PostID)
|
||||
if err == nil {
|
||||
return ErrLikeAlreadyExists
|
||||
} else if err != ErrLikeNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO likes (post_id, user_id, created_at)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err = r.db.QueryRowContext(ctx, query,
|
||||
like.PostID,
|
||||
like.UserID,
|
||||
like.CreatedAt,
|
||||
).Scan(&like.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *likeRepository) GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error) {
|
||||
query := `
|
||||
SELECT id, post_id, user_id, created_at
|
||||
FROM likes
|
||||
WHERE post_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var likes []*domain.Like
|
||||
for rows.Next() {
|
||||
like := &domain.Like{}
|
||||
err := rows.Scan(
|
||||
&like.ID,
|
||||
&like.PostID,
|
||||
&like.UserID,
|
||||
&like.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
likes = append(likes, like)
|
||||
}
|
||||
|
||||
return likes, nil
|
||||
}
|
||||
|
||||
func (r *likeRepository) DeleteByUserAndPost(ctx context.Context, userID, postID int) error {
|
||||
query := `
|
||||
DELETE FROM likes
|
||||
WHERE user_id = $1 AND post_id = $2
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, userID, postID)
|
||||
return err
|
||||
}
|
||||
104
internal/repository/post_repository.go
Normal file
104
internal/repository/post_repository.go
Normal file
@ -0,0 +1,104 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPostNotFound = errors.New("post not found")
|
||||
)
|
||||
|
||||
type PostRepository interface {
|
||||
Create(ctx context.Context, post *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)
|
||||
Update(ctx context.Context, post *domain.Post) error
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
type postRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewPostRepository(db *sql.DB) PostRepository {
|
||||
return &postRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *postRepository) Create(ctx context.Context, post *domain.Post) error {
|
||||
query := `
|
||||
INSERT INTO posts (title, content, author_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
post.Title,
|
||||
post.Content,
|
||||
post.AuthorID,
|
||||
post.CreatedAt,
|
||||
post.UpdatedAt,
|
||||
).Scan(&post.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *postRepository) GetByID(ctx context.Context, id int) (*domain.Post, error) {
|
||||
query := `
|
||||
SELECT id, title, content, author_id, created_at, updated_at
|
||||
FROM posts
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
post := &domain.Post{}
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&post.ID,
|
||||
&post.Title,
|
||||
&post.Content,
|
||||
&post.AuthorID,
|
||||
&post.CreatedAt,
|
||||
&post.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrPostNotFound
|
||||
}
|
||||
|
||||
return post, err
|
||||
}
|
||||
|
||||
func (r *postRepository) GetAll(ctx context.Context) ([]*domain.Post, error) {
|
||||
query := `
|
||||
SELECT id, title, content, author_id, created_at, updated_at
|
||||
FROM posts
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var posts []*domain.Post
|
||||
for rows.Next() {
|
||||
post := &domain.Post{}
|
||||
err := rows.Scan(
|
||||
&post.ID,
|
||||
&post.Title,
|
||||
&post.Content,
|
||||
&post.AuthorID,
|
||||
&post.CreatedAt,
|
||||
&post.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
posts = append(posts, post)
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
72
internal/repository/user_repository.go
Normal file
72
internal/repository/user_repository.go
Normal file
@ -0,0 +1,72 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *domain.User) error
|
||||
GetByID(ctx context.Context, id int) (*domain.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
Update(ctx context.Context, user *domain.User) error
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *sql.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
||||
query := `
|
||||
INSERT INTO users (username, email, password, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query,
|
||||
user.Username,
|
||||
user.Email,
|
||||
user.Password,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
).Scan(&user.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, error) {
|
||||
query := `
|
||||
SELECT id, username, email, password, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
user := &domain.User{}
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.Password,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
// Остальные методы аналогично...
|
||||
111
internal/service/auth_service.go
Normal file
111
internal/service/auth_service.go
Normal file
@ -0,0 +1,111 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Интерфейс сервиса аутентификации
|
||||
type AuthService interface {
|
||||
Register(ctx context.Context, input RegisterInput) (*domain.User, error)
|
||||
Login(ctx context.Context, email, password string) (*domain.Tokens, error)
|
||||
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
|
||||
}
|
||||
|
||||
// Реализация сервиса аутентификации
|
||||
type authService struct {
|
||||
userRepo repository.UserRepository
|
||||
tokenAuth auth.TokenAuth
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewAuthService(userRepo repository.UserRepository, tokenAuth auth.TokenAuth) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
tokenAuth: tokenAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// Входные данные для регистрации
|
||||
type RegisterInput struct {
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Регистрация нового пользователя
|
||||
func (s *authService) Register(ctx context.Context, input RegisterInput) (*domain.User, error) {
|
||||
// Проверка существования пользователя
|
||||
_, err := s.userRepo.GetByEmail(ctx, input.Email)
|
||||
if err == nil {
|
||||
return nil, errors.New("user with this email already exists")
|
||||
} else if err != repository.ErrUserNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Хеширование пароля
|
||||
hashedPassword, err := auth.HashPassword(input.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создание пользователя
|
||||
user := &domain.User{
|
||||
Username: input.Username,
|
||||
Email: input.Email,
|
||||
Password: hashedPassword,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Вход в систему
|
||||
func (s *authService) Login(ctx context.Context, email, password string) (*domain.Tokens, error) {
|
||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if err == repository.ErrUserNotFound {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверка пароля
|
||||
if !auth.CheckPasswordHash(password, user.Password) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Генерация токенов
|
||||
tokens, err := s.tokenAuth.GenerateTokens(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// Обновление токенов
|
||||
func (s *authService) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) {
|
||||
userID, err := s.tokenAuth.ValidateRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
// Проверка существования пользователя
|
||||
_, err = s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// Генерация новых токенов
|
||||
return s.tokenAuth.GenerateTokens(userID)
|
||||
}
|
||||
152
internal/service/chat_service.go
Normal file
152
internal/service/chat_service.go
Normal file
@ -0,0 +1,152 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"tailly_back_v2/internal/ws"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatService interface {
|
||||
SendMessage(ctx context.Context, senderID, chatID int, content string) (*domain.Message, error)
|
||||
GetChatMessages(ctx context.Context, chatID, userID int, limit, offset int) ([]*domain.Message, error)
|
||||
MarkAsRead(ctx context.Context, messageID int) error
|
||||
DeleteMessage(ctx context.Context, messageID, userID int) error
|
||||
GetUserChats(ctx context.Context, userID int) ([]*domain.Chat, error)
|
||||
GetOrCreateChat(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error)
|
||||
}
|
||||
|
||||
type chatService struct {
|
||||
chatRepo repository.ChatRepository
|
||||
userRepo repository.UserRepository
|
||||
hub *ws.ChatHub
|
||||
encryptor EncryptionService
|
||||
}
|
||||
|
||||
func NewChatService(
|
||||
chatRepo repository.ChatRepository,
|
||||
userRepo repository.UserRepository,
|
||||
hub *ws.ChatHub,
|
||||
encryptor EncryptionService,
|
||||
) ChatService {
|
||||
return &chatService{
|
||||
chatRepo: chatRepo,
|
||||
userRepo: userRepo,
|
||||
hub: hub,
|
||||
encryptor: encryptor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chatService) SendMessage(ctx context.Context, senderID, chatID int, content string) (*domain.Message, error) {
|
||||
// Проверяем существование чата
|
||||
chat, err := s.chatRepo.GetChatByID(ctx, chatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что отправитель является участником чата
|
||||
if senderID != chat.User1ID && senderID != chat.User2ID {
|
||||
return nil, errors.New("user is not a participant of this chat")
|
||||
}
|
||||
|
||||
// Шифруем сообщение (если используется E2EE)
|
||||
encryptedContent, err := s.encryptor.EncryptMessage(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
message := &domain.Message{
|
||||
ChatID: chatID,
|
||||
SenderID: senderID,
|
||||
Content: encryptedContent,
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.chatRepo.SaveMessage(ctx, message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Отправляем через WebSocket
|
||||
recipientID := chat.User1ID
|
||||
if senderID == chat.User1ID {
|
||||
recipientID = chat.User2ID
|
||||
}
|
||||
|
||||
s.hub.Broadcast(&domain.WSMessage{
|
||||
Type: "new_message",
|
||||
MessageID: message.ID,
|
||||
ChatID: chatID,
|
||||
SenderID: senderID,
|
||||
Content: encryptedContent,
|
||||
Recipient: recipientID,
|
||||
})
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (s *chatService) GetChatMessages(ctx context.Context, chatID, userID int, limit, offset int) ([]*domain.Message, error) {
|
||||
// Проверяем доступ пользователя к чату
|
||||
chat, err := s.chatRepo.GetChatByID(ctx, chatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userID != chat.User1ID && userID != chat.User2ID {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
messages, err := s.chatRepo.GetMessagesByChat(ctx, chatID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Расшифровываем сообщения (если используется E2EE)
|
||||
for i := range messages {
|
||||
decrypted, err := s.encryptor.DecryptMessage(messages[i].Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages[i].Content = decrypted
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (s *chatService) MarkAsRead(ctx context.Context, messageID int) error {
|
||||
return s.chatRepo.UpdateMessageStatus(ctx, messageID, "read")
|
||||
}
|
||||
|
||||
func (s *chatService) DeleteMessage(ctx context.Context, messageID, userID int) error {
|
||||
message, err := s.chatRepo.GetMessageByID(ctx, messageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if message.SenderID != userID {
|
||||
return errors.New("only sender can delete the message")
|
||||
}
|
||||
|
||||
return s.chatRepo.DeleteMessage(ctx, messageID)
|
||||
}
|
||||
|
||||
func (s *chatService) GetUserChats(ctx context.Context, userID int) ([]*domain.Chat, error) {
|
||||
return s.chatRepo.GetUserChats(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *chatService) GetOrCreateChat(ctx context.Context, user1ID, user2ID int) (*domain.Chat, error) {
|
||||
// Проверяем существование чата
|
||||
chat, err := s.chatRepo.GetChatByParticipants(ctx, user1ID, user2ID)
|
||||
if err == nil {
|
||||
return chat, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, repository.ErrChatNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создаем новый чат
|
||||
return s.chatRepo.CreateChat(ctx, user1ID, user2ID)
|
||||
}
|
||||
132
internal/service/comment_service.go
Normal file
132
internal/service/comment_service.go
Normal file
@ -0,0 +1,132 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Интерфейс сервиса комментариев
|
||||
type CommentService interface {
|
||||
Create(ctx context.Context, postID, authorID int, content string) (*domain.Comment, error)
|
||||
GetByID(ctx context.Context, id int) (*domain.Comment, error)
|
||||
GetByPostID(ctx context.Context, postID int) ([]*domain.Comment, error)
|
||||
Update(ctx context.Context, id int, content string) (*domain.Comment, error)
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
// Реализация сервиса комментариев
|
||||
type commentService struct {
|
||||
commentRepo repository.CommentRepository
|
||||
postRepo repository.PostRepository // Добавляем для проверки существования поста
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewCommentService(
|
||||
commentRepo repository.CommentRepository,
|
||||
postRepo repository.PostRepository,
|
||||
) CommentService {
|
||||
return &commentService{
|
||||
commentRepo: commentRepo,
|
||||
postRepo: postRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Создание комментария
|
||||
func (s *commentService) Create(ctx context.Context, postID, authorID int, content string) (*domain.Comment, error) {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Валидация контента
|
||||
if content == "" {
|
||||
return nil, errors.New("comment content cannot be empty")
|
||||
}
|
||||
if len(content) > 1000 {
|
||||
return nil, errors.New("comment is too long")
|
||||
}
|
||||
|
||||
comment := &domain.Comment{
|
||||
Content: content,
|
||||
PostID: postID,
|
||||
AuthorID: authorID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.commentRepo.Create(ctx, comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// Получение комментария по ID
|
||||
func (s *commentService) GetByID(ctx context.Context, id int) (*domain.Comment, error) {
|
||||
comment, err := s.commentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCommentNotFound) {
|
||||
return nil, errors.New("comment not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// Получение комментариев для поста
|
||||
func (s *commentService) GetByPostID(ctx context.Context, postID int) ([]*domain.Comment, error) {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments, err := s.commentRepo.GetByPostID(ctx, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Возвращаем пустой слайс вместо nil
|
||||
if comments == nil {
|
||||
return []*domain.Comment{}, nil
|
||||
}
|
||||
|
||||
return comments, nil
|
||||
}
|
||||
|
||||
// Обновление комментария
|
||||
func (s *commentService) Update(ctx context.Context, id int, content string) (*domain.Comment, error) {
|
||||
// Валидация контента
|
||||
if content == "" {
|
||||
return nil, errors.New("comment content cannot be empty")
|
||||
}
|
||||
|
||||
// Получаем существующий комментарий
|
||||
comment, err := s.commentRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
comment.Content = content
|
||||
comment.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.commentRepo.Update(ctx, comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// Удаление комментария
|
||||
func (s *commentService) Delete(ctx context.Context, id int) error {
|
||||
return s.commentRepo.Delete(ctx, id)
|
||||
}
|
||||
129
internal/service/like_service.go
Normal file
129
internal/service/like_service.go
Normal file
@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Интерфейс сервиса лайков
|
||||
type LikeService interface {
|
||||
LikePost(ctx context.Context, userID, postID int) (*domain.Like, error)
|
||||
UnlikePost(ctx context.Context, userID, postID int) error
|
||||
GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error)
|
||||
GetCountForPost(ctx context.Context, postID int) (int, error)
|
||||
CheckIfLiked(ctx context.Context, userID, postID int) (bool, error)
|
||||
}
|
||||
|
||||
// Реализация сервиса лайков
|
||||
type likeService struct {
|
||||
likeRepo repository.LikeRepository
|
||||
postRepo repository.PostRepository // Для проверки существования поста
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewLikeService(
|
||||
likeRepo repository.LikeRepository,
|
||||
postRepo repository.PostRepository,
|
||||
) LikeService {
|
||||
return &likeService{
|
||||
likeRepo: likeRepo,
|
||||
postRepo: postRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Поставить лайк посту
|
||||
func (s *likeService) LikePost(ctx context.Context, userID, postID int) (*domain.Like, error) {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, не поставил ли пользователь уже лайк
|
||||
if liked, err := s.CheckIfLiked(ctx, userID, postID); err != nil {
|
||||
return nil, err
|
||||
} else if liked {
|
||||
return nil, errors.New("you have already liked this post")
|
||||
}
|
||||
|
||||
like := &domain.Like{
|
||||
PostID: postID,
|
||||
UserID: userID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.likeRepo.Create(ctx, like); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return like, nil
|
||||
}
|
||||
|
||||
// Убрать лайк с поста
|
||||
func (s *likeService) UnlikePost(ctx context.Context, userID, postID int) error {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return errors.New("post not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем, ставил ли пользователь лайк
|
||||
if liked, err := s.CheckIfLiked(ctx, userID, postID); err != nil {
|
||||
return err
|
||||
} else if !liked {
|
||||
return errors.New("you haven't liked this post yet")
|
||||
}
|
||||
|
||||
return s.likeRepo.DeleteByUserAndPost(ctx, userID, postID)
|
||||
}
|
||||
|
||||
// Получить все лайки для поста
|
||||
func (s *likeService) GetByPostID(ctx context.Context, postID int) ([]*domain.Like, error) {
|
||||
// Проверяем существование поста
|
||||
if _, err := s.postRepo.GetByID(ctx, postID); err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
likes, err := s.likeRepo.GetByPostID(ctx, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Возвращаем пустой слайс вместо nil
|
||||
if likes == nil {
|
||||
return []*domain.Like{}, nil
|
||||
}
|
||||
|
||||
return likes, nil
|
||||
}
|
||||
|
||||
// Получить количество лайков для поста
|
||||
func (s *likeService) GetCountForPost(ctx context.Context, postID int) (int, error) {
|
||||
likes, err := s.GetByPostID(ctx, postID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(likes), nil
|
||||
}
|
||||
|
||||
// Проверить, поставил ли пользователь лайк
|
||||
func (s *likeService) CheckIfLiked(ctx context.Context, userID, postID int) (bool, error) {
|
||||
_, err := s.likeRepo.GetByUserAndPost(ctx, userID, postID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrLikeNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
120
internal/service/post_service.go
Normal file
120
internal/service/post_service.go
Normal file
@ -0,0 +1,120 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Интерфейс сервиса постов
|
||||
type PostService interface {
|
||||
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)
|
||||
Update(ctx context.Context, id int, title, content string) (*domain.Post, error)
|
||||
Delete(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
// Реализация сервиса постов
|
||||
type postService struct {
|
||||
postRepo repository.PostRepository
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewPostService(postRepo repository.PostRepository) PostService {
|
||||
return &postService{postRepo: postRepo}
|
||||
}
|
||||
|
||||
// Создание нового поста
|
||||
func (s *postService) Create(ctx context.Context, authorID int, title, content string) (*domain.Post, error) {
|
||||
// Валидация данных
|
||||
if title == "" {
|
||||
return nil, errors.New("post title cannot be empty")
|
||||
}
|
||||
if content == "" {
|
||||
return nil, errors.New("post content cannot be empty")
|
||||
}
|
||||
|
||||
post := &domain.Post{
|
||||
Title: title,
|
||||
Content: content,
|
||||
AuthorID: authorID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.postRepo.Create(ctx, post); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// Получение поста по ID
|
||||
func (s *postService) GetByID(ctx context.Context, id int) (*domain.Post, error) {
|
||||
post, err := s.postRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrPostNotFound) {
|
||||
return nil, errors.New("post not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// Получение всех постов
|
||||
func (s *postService) GetAll(ctx context.Context) ([]*domain.Post, error) {
|
||||
posts, err := s.postRepo.GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Возвращаем пустой слайс вместо nil
|
||||
if posts == nil {
|
||||
return []*domain.Post{}, nil
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// Получение постов автора
|
||||
func (s *postService) GetByAuthorID(ctx context.Context, authorID int) ([]*domain.Post, error) {
|
||||
posts, err := s.postRepo.GetByAuthorID(ctx, authorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if posts == nil {
|
||||
return []*domain.Post{}, nil
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
// Обновление поста
|
||||
func (s *postService) Update(ctx context.Context, id int, title, content string) (*domain.Post, error) {
|
||||
// Получаем существующий пост
|
||||
post, err := s.postRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
post.Title = title
|
||||
post.Content = content
|
||||
post.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.postRepo.Update(ctx, post); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// Удаление поста
|
||||
func (s *postService) Delete(ctx context.Context, id int) error {
|
||||
return s.postRepo.Delete(ctx, id)
|
||||
}
|
||||
29
internal/service/services.go
Normal file
29
internal/service/services.go
Normal file
@ -0,0 +1,29 @@
|
||||
package service
|
||||
|
||||
// Services объединяет все сервисы приложения
|
||||
type Services struct {
|
||||
Auth AuthService
|
||||
User UserService
|
||||
Post PostService
|
||||
Comment CommentService
|
||||
Like LikeService
|
||||
Chat ChatService
|
||||
}
|
||||
|
||||
func NewServices(
|
||||
authService AuthService,
|
||||
userService UserService,
|
||||
postService PostService,
|
||||
commentService CommentService,
|
||||
likeService LikeService,
|
||||
chatService ChatService,
|
||||
) *Services {
|
||||
return &Services{
|
||||
Auth: authService,
|
||||
User: userService,
|
||||
Post: postService,
|
||||
Comment: commentService,
|
||||
Like: likeService,
|
||||
Chat: chatService,
|
||||
}
|
||||
}
|
||||
88
internal/service/user_service.go
Normal file
88
internal/service/user_service.go
Normal file
@ -0,0 +1,88 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
GetByID(ctx context.Context, id int) (*domain.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
UpdateProfile(ctx context.Context, id int, username, email string) (*domain.User, error)
|
||||
ChangePassword(ctx context.Context, id int, oldPassword, newPassword string) error
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
tokenAuth *auth.TokenAuth
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepository, tokenAuth *auth.TokenAuth) UserService {
|
||||
return &userService{
|
||||
userRepo: userRepo,
|
||||
tokenAuth: tokenAuth,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userService) GetByID(ctx context.Context, id int) (*domain.User, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrUserNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Очищаем пароль перед возвратом
|
||||
user.Password = ""
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *userService) UpdateProfile(ctx context.Context, id int, username, email string) (*domain.User, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем email на уникальность
|
||||
if email != user.Email {
|
||||
if _, err := s.userRepo.GetByEmail(ctx, email); err == nil {
|
||||
return nil, errors.New("email already in use")
|
||||
}
|
||||
}
|
||||
|
||||
user.Username = username
|
||||
user.Email = email
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Очищаем пароль перед возвратом
|
||||
user.Password = ""
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *userService) ChangePassword(ctx context.Context, id int, oldPassword, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !auth.CheckPasswordHash(oldPassword, user.Password) {
|
||||
return errors.New("invalid current password")
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password = hashedPassword
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
62
internal/ws/hub.go
Normal file
62
internal/ws/hub.go
Normal file
@ -0,0 +1,62 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"tailly_back_v2/internal/domain"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
UserID int
|
||||
Send chan *domain.Message
|
||||
}
|
||||
|
||||
type ChatHub struct {
|
||||
clients map[int]*Client
|
||||
register chan *Client
|
||||
unregister chan *Client
|
||||
broadcast chan *domain.Message
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewChatHub() *ChatHub {
|
||||
return &ChatHub{
|
||||
clients: make(map[int]*Client),
|
||||
register: make(chan *Client),
|
||||
unregister: make(chan *Client),
|
||||
broadcast: make(chan *domain.Message),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHub) Run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
h.mutex.Lock()
|
||||
h.clients[client.UserID] = client
|
||||
h.mutex.Unlock()
|
||||
|
||||
case client := <-h.unregister:
|
||||
h.mutex.Lock()
|
||||
if _, ok := h.clients[client.UserID]; ok {
|
||||
close(client.Send)
|
||||
delete(h.clients, client.UserID)
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
|
||||
case message := <-h.broadcast:
|
||||
h.mutex.RLock()
|
||||
// Отправляем отправителю и получателю
|
||||
if sender, ok := h.clients[message.SenderID]; ok {
|
||||
sender.Send <- message
|
||||
}
|
||||
if receiver, ok := h.clients[message.ReceiverID]; ok {
|
||||
receiver.Send <- message
|
||||
}
|
||||
h.mutex.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHub) Broadcast(message *domain.Message) {
|
||||
h.broadcast <- message
|
||||
}
|
||||
133
pkg/auth/auth.go
Normal file
133
pkg/auth/auth.go
Normal file
@ -0,0 +1,133 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
)
|
||||
|
||||
type TokenAuth struct {
|
||||
accessTokenSecret string
|
||||
refreshTokenSecret string
|
||||
accessTokenExpiry time.Duration
|
||||
refreshTokenExpiry time.Duration
|
||||
}
|
||||
|
||||
func NewTokenAuth(accessSecret, refreshSecret string, accessExpiry, refreshExpiry time.Duration) *TokenAuth {
|
||||
return &TokenAuth{
|
||||
accessTokenSecret: accessSecret,
|
||||
refreshTokenSecret: refreshSecret,
|
||||
accessTokenExpiry: accessExpiry,
|
||||
refreshTokenExpiry: refreshExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateTokens создает пару access и refresh токенов
|
||||
func (a *TokenAuth) GenerateTokens(userID int) (*domain.Tokens, error) {
|
||||
accessToken, err := a.generateAccessToken(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := a.generateRefreshToken(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.Tokens{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAccessToken создает access токен (короткоживущий)
|
||||
func (a *TokenAuth) generateAccessToken(userID int) (string, error) {
|
||||
claims := jwt.RegisteredClaims{
|
||||
Subject: fmt.Sprintf("%d", userID),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.accessTokenExpiry)),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(a.accessTokenSecret))
|
||||
}
|
||||
|
||||
// generateRefreshToken создает refresh токен (долгоживущий)
|
||||
func (a *TokenAuth) generateRefreshToken(userID int) (string, error) {
|
||||
claims := jwt.RegisteredClaims{
|
||||
Subject: fmt.Sprintf("%d", userID),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.refreshTokenExpiry)),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(a.refreshTokenSecret))
|
||||
}
|
||||
|
||||
// ValidateAccessToken проверяет access токен и возвращает userID
|
||||
func (a *TokenAuth) ValidateAccessToken(tokenString string) (int, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(a.accessTokenSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
|
||||
var userID int
|
||||
_, err := fmt.Sscanf(claims.Subject, "%d", &userID)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidToken
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
return 0, ErrInvalidToken
|
||||
}
|
||||
|
||||
// ValidateRefreshToken проверяет refresh токен и возвращает userID
|
||||
func (a *TokenAuth) ValidateRefreshToken(tokenString string) (int, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(a.refreshTokenSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
|
||||
var userID int
|
||||
_, err := fmt.Sscanf(claims.Subject, "%d", &userID)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidToken
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
return 0, ErrInvalidToken
|
||||
}
|
||||
|
||||
// HashPassword хеширует пароль
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash проверяет пароль с хешем
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
36
pkg/database/postgres.go
Normal file
36
pkg/database/postgres.go
Normal file
@ -0,0 +1,36 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Postgres struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgres(dsn string) (*sql.DB, error) {
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open db: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем соединение
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping db: %w", err)
|
||||
}
|
||||
|
||||
// Настройка пула соединений
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(25)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user