v0.0.2
This commit is contained in:
parent
a5a90bed7e
commit
0d4b8b203e
13
.env
Normal file
13
.env
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
SERVER_HOST=localhost
|
||||||
|
SERVER_PORT=3006
|
||||||
|
DB_DSN=postgres://user:password@localhost:5432/blog?sslmode=disable
|
||||||
|
ACCESS_TOKEN_SECRET="e5159eb2a712a47fb578284c36d507664f922f968ee99fa0b68260208d85688b"
|
||||||
|
REFRESH_TOKEN_SECRET="75afdd4abdc49f647c57bfa700f0cc01fb931b5f6b2176386f93f64692179946"
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=user@example.com
|
||||||
|
SMTP_PASSWORD=yourpassword
|
||||||
|
SMTP_FROM=noreply@example.com
|
||||||
|
APP_URL=https://your-app.com
|
||||||
|
VAULT_ADDR=http://localhost:8200
|
||||||
|
VAULT_TOKEN=s.ваш_токен
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"tailly_back_v2/internal/ws"
|
"tailly_back_v2/internal/ws"
|
||||||
"tailly_back_v2/pkg/auth"
|
"tailly_back_v2/pkg/auth"
|
||||||
"tailly_back_v2/pkg/database"
|
"tailly_back_v2/pkg/database"
|
||||||
|
"tailly_back_v2/pkg/encryption"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,31 +39,52 @@ func main() {
|
|||||||
cfg.Auth.RefreshTokenExpiry,
|
cfg.Auth.RefreshTokenExpiry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Инициализация чата
|
||||||
|
chatHub := ws.NewChatHub()
|
||||||
|
go chatHub.Run()
|
||||||
|
|
||||||
// Репозитории
|
// Репозитории
|
||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
postRepo := repository.NewPostRepository(db)
|
postRepo := repository.NewPostRepository(db)
|
||||||
commentRepo := repository.NewCommentRepository(db)
|
commentRepo := repository.NewCommentRepository(db)
|
||||||
likeRepo := repository.NewLikeRepository(db)
|
likeRepo := repository.NewLikeRepository(db)
|
||||||
chatRepo := repository.NewChatRepository(db)
|
chatRepo := repository.NewChatRepository(db)
|
||||||
|
// Инициализация MailService
|
||||||
|
mailService, err := service.NewMailService(
|
||||||
|
cfg.SMTP.From,
|
||||||
|
cfg.SMTP.Host,
|
||||||
|
cfg.SMTP.Port,
|
||||||
|
cfg.SMTP.Username,
|
||||||
|
cfg.SMTP.Password,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create mail service: %v", err)
|
||||||
|
}
|
||||||
|
vaultService := encryption.NewVaultService(
|
||||||
|
cfg.Vault.Address,
|
||||||
|
cfg.Vault.Token,
|
||||||
|
)
|
||||||
|
auditService := service.NewAuditService(repository.NewAuditRepository(db))
|
||||||
|
recoveryService := service.NewRecoveryService(
|
||||||
|
repository.NewRecoveryRepository(db),
|
||||||
|
repository.NewUserRepository(db),
|
||||||
|
repository.NewSessionRepository(db),
|
||||||
|
repository.NewDeviceRepository(db),
|
||||||
|
mailService,
|
||||||
|
encryptionService,
|
||||||
|
)
|
||||||
|
|
||||||
// Сервисы
|
// Сервисы
|
||||||
services := service.NewServices(
|
services := service.NewServices(
|
||||||
service.NewAuthService(userRepo, *tokenAuth),
|
service.NewAuthService(userRepo, tokenAuth, mailService),
|
||||||
service.NewUserService(userRepo),
|
service.NewUserService(userRepo),
|
||||||
service.NewPostService(postRepo),
|
service.NewPostService(postRepo),
|
||||||
service.NewCommentService(commentRepo),
|
service.NewCommentService(commentRepo),
|
||||||
service.NewLikeService(likeRepo),
|
service.NewLikeService(likeRepo),
|
||||||
service.NewChatService(chatRepo),
|
service.NewChatService(chatRepo),
|
||||||
|
service.NewEncryptionService(vaultService, userRepo),
|
||||||
)
|
)
|
||||||
// Инициализация чата
|
|
||||||
chatHub := ws.NewChatHub()
|
|
||||||
go chatHub.Run()
|
|
||||||
|
|
||||||
chatService := service.NewChatService(
|
|
||||||
chatRepo,
|
|
||||||
userRepo,
|
|
||||||
chatHub,
|
|
||||||
encryptionService,
|
|
||||||
)
|
|
||||||
// HTTP сервер
|
// HTTP сервер
|
||||||
server := http.NewServer(cfg, services, tokenAuth)
|
server := http.NewServer(cfg, services, tokenAuth)
|
||||||
|
|
||||||
|
|||||||
30
go.mod
30
go.mod
@ -1,30 +0,0 @@
|
|||||||
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
70
go.sum
@ -1,70 +0,0 @@
|
|||||||
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=
|
|
||||||
39
gqlgen.yml
39
gqlgen.yml
@ -10,32 +10,49 @@ model:
|
|||||||
package: graph
|
package: graph
|
||||||
|
|
||||||
resolver:
|
resolver:
|
||||||
filename: internal/http/graph
|
dir: internal/http/graph/resolvers
|
||||||
layout: follow-schema
|
layout: follow-schema
|
||||||
package: graph
|
package: graph
|
||||||
skip_generation:
|
|
||||||
- "*_resolvers.go"
|
|
||||||
- "resolvers.go"
|
|
||||||
models:
|
models:
|
||||||
User:
|
User:
|
||||||
model: tailly_back_v2/internal/domain.User
|
model: tailly_back_v2/internal/domain.User
|
||||||
Post:
|
Post:
|
||||||
model: tailly_back_v2/internal/domain.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:
|
Comment:
|
||||||
model: tailly_back_v2/internal/domain.Comment
|
model: tailly_back_v2/internal/domain.Comment
|
||||||
Like:
|
Like:
|
||||||
model: tailly_back_v2/internal/domain.like
|
model: tailly_back_v2/internal/domain.Like
|
||||||
Tokens:
|
Device:
|
||||||
model: tailly_back_v2/internal/domain.Tokens
|
model: tailly_back_v2/internal/domain.Device
|
||||||
|
Session:
|
||||||
|
model: tailly_back_v2/internal/domain.Session
|
||||||
|
RecoveryRequest:
|
||||||
|
model: tailly_back_v2/internal/domain.RecoveryRequest
|
||||||
|
RecoveryMethod:
|
||||||
|
model: tailly_back_v2/internal/domain.RecoveryMethod
|
||||||
|
Notification:
|
||||||
|
model: tailly_back_v2/internal/domain.Notification
|
||||||
|
NotificationPreferences:
|
||||||
|
model: tailly_back_v2/internal/domain.NotificationPreferences
|
||||||
|
EncryptionKey:
|
||||||
|
model: tailly_back_v2/internal/domain.EncryptionKey
|
||||||
|
KeyExchange:
|
||||||
|
model: tailly_back_v2/internal/domain.KeyExchange
|
||||||
Chat:
|
Chat:
|
||||||
model: tailly_back_v2/internal/domain.Chat
|
model: tailly_back_v2/internal/domain.Chat
|
||||||
Message:
|
Message:
|
||||||
model: tailly_back_v2/internal/domain.Message
|
model: tailly_back_v2/internal/domain.Message
|
||||||
WSMessage:
|
WSMessage:
|
||||||
model: tailly_back_v2/internal/domain.WSMessage
|
model: tailly_back_v2/internal/domain.WSMessage
|
||||||
|
AuditLog:
|
||||||
|
model: tailly_back_v2/internal/domain.AuditLog
|
||||||
|
RegisterInput:
|
||||||
|
model: tailly_back_v2/internal/domain.RegisterInput
|
||||||
|
LoginInput:
|
||||||
|
model: tailly_back_v2/internal/domain.LoginInput
|
||||||
|
Tokens:
|
||||||
|
model: tailly_back_v2/internal/domain.Tokens
|
||||||
|
|
||||||
autobind:
|
autobind:
|
||||||
- "tailly_back_v2/internal/domain"
|
- "tailly_back_v2/internal/domain"
|
||||||
@ -1,13 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/caarlos0/env/v8"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server struct {
|
Server struct {
|
||||||
Host string `env:"SERVER_HOST" env-default:"localhost"`
|
Host string `env:"SERVER_HOST" env-default:"localhost"`
|
||||||
Port string `env:"SERVER_PORT" env-default:"3006"`
|
Port string `env:"SERVER_PORT" env-default:"8080"`
|
||||||
}
|
}
|
||||||
Database struct {
|
Database struct {
|
||||||
DSN string `env:"DB_DSN,required"`
|
DSN string `env:"DB_DSN,required"`
|
||||||
@ -18,4 +19,26 @@ type Config struct {
|
|||||||
AccessTokenExpiry time.Duration `env:"ACCESS_TOKEN_EXPIRY" env-default:"15m"`
|
AccessTokenExpiry time.Duration `env:"ACCESS_TOKEN_EXPIRY" env-default:"15m"`
|
||||||
RefreshTokenExpiry time.Duration `env:"REFRESH_TOKEN_EXPIRY" env-default:"168h"` // 7 дней
|
RefreshTokenExpiry time.Duration `env:"REFRESH_TOKEN_EXPIRY" env-default:"168h"` // 7 дней
|
||||||
}
|
}
|
||||||
|
SMTP struct {
|
||||||
|
Host string `env:"SMTP_HOST,required"`
|
||||||
|
Port int `env:"SMTP_PORT,required"`
|
||||||
|
Username string `env:"SMTP_USERNAME,required"`
|
||||||
|
Password string `env:"SMTP_PASSWORD,required"`
|
||||||
|
From string `env:"SMTP_FROM,required"`
|
||||||
|
}
|
||||||
|
App struct {
|
||||||
|
URL string `env:"APP_URL,required"`
|
||||||
|
}
|
||||||
|
Vault struct {
|
||||||
|
Address string `env:"VAULT_ADDR,required"`
|
||||||
|
Token string `env:"VAULT_TOKEN,required"`
|
||||||
|
} `envPrefix:"VAULT_"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
cfg := &Config{}
|
||||||
|
if err := env.Parse(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
28
internal/domain/audit.go
Normal file
28
internal/domain/audit.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditLog struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID *int `json:"userId,omitempty"` // nil для неаутентифицированных действий
|
||||||
|
Action string `json:"action"` // "login", "password_change", "recovery_initiated"
|
||||||
|
EntityType string `json:"entityType"` // "user", "device", "session"
|
||||||
|
EntityID *int `json:"entityId,omitempty"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
Metadata string `json:"metadata"` // JSON с дополнительными данными
|
||||||
|
Status string `json:"status"` // "success", "failed"
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditFilter struct {
|
||||||
|
UserID *int
|
||||||
|
Action string
|
||||||
|
EntityType string
|
||||||
|
DateFrom time.Time
|
||||||
|
DateTo time.Time
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
19
internal/domain/encryption.go
Normal file
19
internal/domain/encryption.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type EncryptionKey struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
PublicKey string `json:"publicKey"` // В формате PEM
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyExchange struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
InitiatorID int `json:"initiatorId"`
|
||||||
|
ReceiverID int `json:"receiverId"`
|
||||||
|
SessionKey string `json:"sessionKey"` // Зашифрованный ключ
|
||||||
|
Status string `json:"status"` // "pending", "confirmed"
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,6 +9,8 @@ type User struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
EmailConfirmationToken string `json:"-"`
|
||||||
|
EmailConfirmedAt *time.Time `json:"emailConfirmedAt,omitempty"`
|
||||||
Password string `json:"-"` // Пароль не должен возвращаться в ответах
|
Password string `json:"-"` // Пароль не должен возвращаться в ответах
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
@ -49,7 +50,6 @@ type Tokens struct {
|
|||||||
RefreshToken string `json:"refreshToken"`
|
RefreshToken string `json:"refreshToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Типы для аутентификации
|
|
||||||
type RegisterInput struct {
|
type RegisterInput struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -60,13 +60,3 @@ type LoginInput struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
22
internal/domain/notification.go
Normal file
22
internal/domain/notification.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
Type string `json:"type"` // "new_device", "login_attempt"
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data string `json:"data"` // JSON
|
||||||
|
IsRead bool `json:"isRead"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationPreferences struct {
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
EmailNewDevice bool `json:"emailNewDevice"`
|
||||||
|
PushNewDevice bool `json:"pushNewDevice"`
|
||||||
|
EmailLoginAttempt bool `json:"emailLoginAttempt"`
|
||||||
|
PushLoginAttempt bool `json:"pushLoginAttempt"`
|
||||||
|
}
|
||||||
22
internal/domain/recovery.go
Normal file
22
internal/domain/recovery.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RecoveryRequest struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
Token string `json:"-"`
|
||||||
|
NewDevice *Device `json:"newDevice,omitempty"`
|
||||||
|
Status string `json:"status"` // "pending", "completed", "expired"
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecoveryMethod struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
MethodType string `json:"methodType"` // "email", "phone", "totp"
|
||||||
|
Value string `json:"value"` // email/phone number
|
||||||
|
IsPrimary bool `json:"isPrimary"`
|
||||||
|
VerifiedAt time.Time `json:"verifiedAt"`
|
||||||
|
}
|
||||||
22
internal/domain/session.go
Normal file
22
internal/domain/session.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
ConfirmationToken string `json:"-"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
DeviceID int `json:"deviceId"`
|
||||||
|
StartedAt time.Time `json:"startedAt"`
|
||||||
|
EndedAt time.Time `json:"endedAt,omitempty"`
|
||||||
|
}
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
15
internal/http/graph/context.go
Normal file
15
internal/http/graph/context.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
|
||||||
|
|
||||||
package graph
|
|
||||||
|
|
||||||
type Mutation struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type Subscription struct {
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@ -1,221 +1,6 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
_ "tailly_back_v2/internal/domain"
|
||||||
"errors"
|
_ "tailly_back_v2/internal/service"
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ type User {
|
|||||||
id: ID! # Уникальный идентификатор
|
id: ID! # Уникальный идентификатор
|
||||||
username: String! # Имя пользователя
|
username: String! # Имя пользователя
|
||||||
email: String! # Email (уникальный)
|
email: String! # Email (уникальный)
|
||||||
|
emailConfirmedAt: String # Дата подтверждения email (может быть null)
|
||||||
createdAt: String! # Дата создания
|
createdAt: String! # Дата создания
|
||||||
updatedAt: String! # Дата обновления
|
updatedAt: String! # Дата обновления
|
||||||
}
|
}
|
||||||
@ -43,9 +44,21 @@ type Like {
|
|||||||
type Tokens {
|
type Tokens {
|
||||||
accessToken: String! # Access токен (короткоживущий)
|
accessToken: String! # Access токен (короткоживущий)
|
||||||
refreshToken: String! # Refresh токен (долгоживущий)
|
refreshToken: String! # Refresh токен (долгоживущий)
|
||||||
|
emailConfirmed: Boolean! # Флаг подтверждения email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Входные данные для регистрации
|
||||||
|
input RegisterInput {
|
||||||
|
username: String! # Имя пользователя
|
||||||
|
email: String! # Email
|
||||||
|
password: String! # Пароль
|
||||||
|
}
|
||||||
|
|
||||||
|
# Входные данные для входа
|
||||||
|
input LoginInput {
|
||||||
|
email: String! # Email
|
||||||
|
password: String! # Пароль
|
||||||
|
}
|
||||||
|
|
||||||
type Message {
|
type Message {
|
||||||
id: ID!
|
id: ID!
|
||||||
@ -62,19 +75,21 @@ type ChatSession {
|
|||||||
unreadCount: Int!
|
unreadCount: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Session {
|
||||||
messageReceived: Message!
|
id: ID!
|
||||||
|
device: Device!
|
||||||
|
startedAt: String!
|
||||||
|
lastActiveAt: String!
|
||||||
|
isCurrent: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input RegisterInput {
|
type Device {
|
||||||
username: String!
|
id: ID!
|
||||||
email: String!
|
name: String!
|
||||||
password: String!
|
type: String!
|
||||||
}
|
ipAddress: String!
|
||||||
|
location: String!
|
||||||
input LoginInput {
|
lastActiveAt: String!
|
||||||
email: String!
|
|
||||||
password: String!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Запросы (получение данных)
|
# Запросы (получение данных)
|
||||||
@ -85,6 +100,8 @@ type Query {
|
|||||||
user(id: ID!): User! # Получить пользователя по ID
|
user(id: ID!): User! # Получить пользователя по ID
|
||||||
getChatHistory(userId: ID!): [Message!]!
|
getChatHistory(userId: ID!): [Message!]!
|
||||||
getUserChats: [ChatSession!]!
|
getUserChats: [ChatSession!]!
|
||||||
|
mySessions: [Session!]!
|
||||||
|
activeSessions: [Session!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Мутации (изменение данных)
|
# Мутации (изменение данных)
|
||||||
@ -109,9 +126,22 @@ type Mutation {
|
|||||||
|
|
||||||
# Удаление лайка
|
# Удаление лайка
|
||||||
unlikePost(postId: ID!): Boolean!
|
unlikePost(postId: ID!): Boolean!
|
||||||
|
|
||||||
updateProfile(username: String!, email: String!): User!
|
updateProfile(username: String!, email: String!): User!
|
||||||
changePassword(oldPassword: String!, newPassword: String!): Boolean!
|
changePassword(oldPassword: String!, newPassword: String!): Boolean!
|
||||||
sendMessage(receiverId: ID!, content: String!): Message!
|
sendMessage(receiverId: ID!, content: String!): Message!
|
||||||
markAsRead(messageId: ID!): Boolean!
|
markAsRead(messageId: ID!): Boolean!
|
||||||
|
terminateSession(sessionId: ID!): Boolean!
|
||||||
|
renameDevice(deviceId: ID!, name: String!): Device!
|
||||||
|
# Запрос на подтверждение email
|
||||||
|
requestEmailConfirmation: Boolean!
|
||||||
|
|
||||||
|
# Подтверждение email по токену
|
||||||
|
confirmEmail(token: String!): Boolean!
|
||||||
|
|
||||||
|
# Повторная отправка подтверждения email
|
||||||
|
resendEmailConfirmation: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
messageReceived: Message!
|
||||||
}
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"tailly_back_v2/internal/domain"
|
"tailly_back_v2/internal/domain"
|
||||||
|
|||||||
40
internal/http/handlers/notification.go
Normal file
40
internal/http/handlers/notification.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
"tailly_back_v2/internal/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
var notificationUpgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // Настроить правильно для production
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NotificationHandler) HandleWebSocket(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 := notificationUpgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &ws.NotificationClient{
|
||||||
|
UserID: userID,
|
||||||
|
Send: make(chan *domain.Notification, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
h.hub.Register(client)
|
||||||
|
|
||||||
|
go h.writePump(conn, client)
|
||||||
|
go h.readPump(conn, client)
|
||||||
|
}
|
||||||
56
internal/http/middleware/audit.go
Normal file
56
internal/http/middleware/audit.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"tailly_back_v2/internal/service"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AuditMiddleware(audit service.AuditService, action string) 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()
|
||||||
|
rw := &responseRecorder{w, http.StatusOK, 0}
|
||||||
|
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
status := "success"
|
||||||
|
if rw.status >= 400 {
|
||||||
|
status = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
audit.LogEvent(
|
||||||
|
r.Context(),
|
||||||
|
action,
|
||||||
|
"request",
|
||||||
|
nil,
|
||||||
|
r,
|
||||||
|
status,
|
||||||
|
map[string]interface{}{
|
||||||
|
"method": r.Method,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
"status": rw.status,
|
||||||
|
"duration": duration.String(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *responseRecorder) WriteHeader(status int) {
|
||||||
|
r.status = status
|
||||||
|
r.ResponseWriter.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *responseRecorder) Write(b []byte) (int, error) {
|
||||||
|
size, err := r.ResponseWriter.Write(b)
|
||||||
|
r.size += size
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
@ -56,9 +56,3 @@ func AuthMiddleware(tokenAuth *auth.TokenAuth) func(http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDFromContext извлекает userID из контекста
|
|
||||||
func GetUserIDFromContext(ctx context.Context) (int, bool) {
|
|
||||||
userID, ok := ctx.Value(userIDKey).(int)
|
|
||||||
return userID, ok
|
|
||||||
}
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"tailly_back_v2/internal/config"
|
"tailly_back_v2/internal/config"
|
||||||
"tailly_back_v2/internal/http/graph"
|
"tailly_back_v2/internal/http/graph"
|
||||||
|
"tailly_back_v2/internal/http/handlers"
|
||||||
"tailly_back_v2/internal/http/middleware"
|
"tailly_back_v2/internal/http/middleware"
|
||||||
"tailly_back_v2/internal/service"
|
"tailly_back_v2/internal/service"
|
||||||
"tailly_back_v2/pkg/auth"
|
"tailly_back_v2/pkg/auth"
|
||||||
@ -42,13 +43,21 @@ func (s *Server) configureRouter() {
|
|||||||
"http://localhost:3000", // React dev server
|
"http://localhost:3000", // React dev server
|
||||||
"https://your-production.app", // Продакшен домен
|
"https://your-production.app", // Продакшен домен
|
||||||
}
|
}
|
||||||
|
|
||||||
// Логирование
|
// Логирование
|
||||||
logger := log.New(os.Stdout, "HTTP: ", log.LstdFlags)
|
logger := log.New(os.Stdout, "HTTP: ", log.LstdFlags)
|
||||||
s.router.Use(middleware.LoggingMiddleware(logger))
|
s.router.Use(middleware.LoggingMiddleware(logger))
|
||||||
|
|
||||||
|
// Регистрация middleware
|
||||||
|
s.router.Use(middleware.AuditMiddleware(auditService, "http_request"))
|
||||||
|
|
||||||
|
// Регистрация обработчиков
|
||||||
|
handler.RegisterRecoveryHandlers(router, recoveryService)
|
||||||
|
handler.RegisterAuditHandlers(router, auditService, authMiddleware)
|
||||||
|
|
||||||
// Метрики
|
// Метрики
|
||||||
s.router.Use(middleware.MetricsMiddleware)
|
s.router.Use(middleware.MetricsMiddleware)
|
||||||
// Middleware
|
|
||||||
s.router.Use(middleware.CORS(allowedOrigins))
|
s.router.Use(middleware.CORS(allowedOrigins))
|
||||||
s.router.Use(middleware.AuthMiddleware(s.tokenAuth))
|
s.router.Use(middleware.AuthMiddleware(s.tokenAuth))
|
||||||
|
|
||||||
@ -72,7 +81,6 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отдельный endpoint для метрик Prometheus
|
|
||||||
func (s *Server) configureMetrics() {
|
func (s *Server) configureMetrics() {
|
||||||
prometheus.MustRegister(
|
prometheus.MustRegister(
|
||||||
middleware.httpRequestsTotal,
|
middleware.httpRequestsTotal,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"tailly_back_v2/internal/domain"
|
"tailly_back_v2/internal/domain"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -15,6 +16,7 @@ type UserRepository interface {
|
|||||||
Create(ctx context.Context, user *domain.User) error
|
Create(ctx context.Context, user *domain.User) error
|
||||||
GetByID(ctx context.Context, id int) (*domain.User, error)
|
GetByID(ctx context.Context, id int) (*domain.User, error)
|
||||||
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
GetByConfirmationToken(ctx context.Context, token string) (*domain.User, error)
|
||||||
Update(ctx context.Context, user *domain.User) error
|
Update(ctx context.Context, user *domain.User) error
|
||||||
Delete(ctx context.Context, id int) error
|
Delete(ctx context.Context, id int) error
|
||||||
}
|
}
|
||||||
@ -29,8 +31,8 @@ func NewUserRepository(db *sql.DB) UserRepository {
|
|||||||
|
|
||||||
func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO users (username, email, password, created_at, updated_at)
|
INSERT INTO users (username, email, password, email_confirmation_token, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
|||||||
user.Username,
|
user.Username,
|
||||||
user.Email,
|
user.Email,
|
||||||
user.Password,
|
user.Password,
|
||||||
|
user.EmailConfirmationToken,
|
||||||
user.CreatedAt,
|
user.CreatedAt,
|
||||||
user.UpdatedAt,
|
user.UpdatedAt,
|
||||||
).Scan(&user.ID)
|
).Scan(&user.ID)
|
||||||
@ -47,17 +50,21 @@ func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
|||||||
|
|
||||||
func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, error) {
|
func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, username, email, password, created_at, updated_at
|
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
user := &domain.User{}
|
user := &domain.User{}
|
||||||
|
var confirmedAt sql.NullTime
|
||||||
|
|
||||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.Email,
|
&user.Email,
|
||||||
&user.Password,
|
&user.Password,
|
||||||
|
&user.EmailConfirmationToken,
|
||||||
|
&confirmedAt,
|
||||||
&user.CreatedAt,
|
&user.CreatedAt,
|
||||||
&user.UpdatedAt,
|
&user.UpdatedAt,
|
||||||
)
|
)
|
||||||
@ -66,7 +73,111 @@ func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, err
|
|||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if confirmedAt.Valid {
|
||||||
|
user.EmailConfirmedAt = &confirmedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Остальные методы аналогично...
|
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE email = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
user := &domain.User{}
|
||||||
|
var confirmedAt sql.NullTime
|
||||||
|
|
||||||
|
err := r.db.QueryRowContext(ctx, query, email).Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.Username,
|
||||||
|
&user.Email,
|
||||||
|
&user.Password,
|
||||||
|
&user.EmailConfirmationToken,
|
||||||
|
&confirmedAt,
|
||||||
|
&user.CreatedAt,
|
||||||
|
&user.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirmedAt.Valid {
|
||||||
|
user.EmailConfirmedAt = &confirmedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) GetByConfirmationToken(ctx context.Context, token string) (*domain.User, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, username, email, password, email_confirmation_token, email_confirmed_at, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE email_confirmation_token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
user := &domain.User{}
|
||||||
|
var confirmedAt sql.NullTime
|
||||||
|
|
||||||
|
err := r.db.QueryRowContext(ctx, query, token).Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.Username,
|
||||||
|
&user.Email,
|
||||||
|
&user.Password,
|
||||||
|
&user.EmailConfirmationToken,
|
||||||
|
&confirmedAt,
|
||||||
|
&user.CreatedAt,
|
||||||
|
&user.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if confirmedAt.Valid {
|
||||||
|
user.EmailConfirmedAt = &confirmedAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Update(ctx context.Context, user *domain.User) error {
|
||||||
|
query := `
|
||||||
|
UPDATE users
|
||||||
|
SET username = $1,
|
||||||
|
email = $2,
|
||||||
|
password = $3,
|
||||||
|
email_confirmation_token = $4,
|
||||||
|
email_confirmed_at = $5,
|
||||||
|
updated_at = $6
|
||||||
|
WHERE id = $7
|
||||||
|
`
|
||||||
|
|
||||||
|
var confirmedAt interface{}
|
||||||
|
if user.EmailConfirmedAt != nil {
|
||||||
|
confirmedAt = *user.EmailConfirmedAt
|
||||||
|
} else {
|
||||||
|
confirmedAt = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
user.Username,
|
||||||
|
user.Email,
|
||||||
|
user.Password,
|
||||||
|
user.EmailConfirmationToken,
|
||||||
|
confirmedAt,
|
||||||
|
time.Now(), // Обновляем updated_at
|
||||||
|
user.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *userRepository) Delete(ctx context.Context, id int) error {
|
||||||
|
query := `DELETE FROM users WHERE id = $1`
|
||||||
|
_, err := r.db.ExecContext(ctx, query, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
83
internal/service/audit_service.go
Normal file
83
internal/service/audit_service.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
"tailly_back_v2/internal/repository"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditService interface {
|
||||||
|
LogEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
action string,
|
||||||
|
entityType string,
|
||||||
|
entityID *int,
|
||||||
|
r *http.Request,
|
||||||
|
status string,
|
||||||
|
metadata interface{},
|
||||||
|
) error
|
||||||
|
GetEvents(ctx context.Context, filter domain.AuditFilter) ([]*domain.AuditLog, error)
|
||||||
|
ExportEvents(ctx context.Context, filter domain.AuditFilter) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type auditService struct {
|
||||||
|
auditRepo repository.AuditRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *auditService) LogEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
action string,
|
||||||
|
entityType string,
|
||||||
|
entityID *int,
|
||||||
|
r *http.Request,
|
||||||
|
status string,
|
||||||
|
metadata interface{},
|
||||||
|
) error {
|
||||||
|
var userID *int
|
||||||
|
if ctxUserID, ok := ctx.Value("userID").(int); ok {
|
||||||
|
userID = &ctxUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataJSON, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := r.Header.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
log := &domain.AuditLog{
|
||||||
|
UserID: userID,
|
||||||
|
Action: action,
|
||||||
|
EntityType: entityType,
|
||||||
|
EntityID: entityID,
|
||||||
|
IPAddress: ip,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
Metadata: string(metadataJSON),
|
||||||
|
Status: status,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.auditRepo.Save(ctx, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *auditService) GetEvents(ctx context.Context, filter domain.AuditFilter) ([]*domain.AuditLog, error) {
|
||||||
|
if filter.Limit == 0 {
|
||||||
|
filter.Limit = 50
|
||||||
|
}
|
||||||
|
return s.auditRepo.Get(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *auditService) ExportEvents(ctx context.Context, filter domain.AuditFilter) ([]byte, error) {
|
||||||
|
logs, err := s.auditRepo.Get(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(logs)
|
||||||
|
}
|
||||||
@ -2,44 +2,48 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"tailly_back_v2/internal/domain"
|
"tailly_back_v2/internal/domain"
|
||||||
"tailly_back_v2/internal/repository"
|
"tailly_back_v2/internal/repository"
|
||||||
"tailly_back_v2/pkg/auth"
|
"tailly_back_v2/pkg/auth"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Интерфейс сервиса аутентификации
|
|
||||||
type AuthService interface {
|
type AuthService interface {
|
||||||
Register(ctx context.Context, input RegisterInput) (*domain.User, error)
|
Register(ctx context.Context, input RegisterInput) (*domain.User, error)
|
||||||
Login(ctx context.Context, email, password string) (*domain.Tokens, error)
|
Login(ctx context.Context, email, password string) (*domain.Tokens, error)
|
||||||
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
|
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
|
||||||
|
ConfirmEmail(ctx context.Context, token string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Реализация сервиса аутентификации
|
|
||||||
type authService struct {
|
type authService struct {
|
||||||
userRepo repository.UserRepository
|
userRepo repository.UserRepository
|
||||||
tokenAuth auth.TokenAuth
|
tokenAuth auth.TokenAuth
|
||||||
|
mailer MailService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конструктор сервиса
|
func NewAuthService(
|
||||||
func NewAuthService(userRepo repository.UserRepository, tokenAuth auth.TokenAuth) AuthService {
|
userRepo repository.UserRepository,
|
||||||
|
tokenAuth auth.TokenAuth,
|
||||||
|
mailer MailService,
|
||||||
|
) AuthService {
|
||||||
return &authService{
|
return &authService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
tokenAuth: tokenAuth,
|
tokenAuth: tokenAuth,
|
||||||
|
mailer: mailer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Входные данные для регистрации
|
|
||||||
type RegisterInput struct {
|
type RegisterInput struct {
|
||||||
Username string
|
Username string
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Регистрация нового пользователя
|
|
||||||
func (s *authService) Register(ctx context.Context, input RegisterInput) (*domain.User, error) {
|
func (s *authService) Register(ctx context.Context, input RegisterInput) (*domain.User, error) {
|
||||||
// Проверка существования пользователя
|
|
||||||
_, err := s.userRepo.GetByEmail(ctx, input.Email)
|
_, err := s.userRepo.GetByEmail(ctx, input.Email)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil, errors.New("user with this email already exists")
|
return nil, errors.New("user with this email already exists")
|
||||||
@ -47,17 +51,23 @@ func (s *authService) Register(ctx context.Context, input RegisterInput) (*domai
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Хеширование пароля
|
// Генерация токена подтверждения
|
||||||
|
token := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(token); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate confirmation token: %w", err)
|
||||||
|
}
|
||||||
|
confirmationToken := base64.URLEncoding.EncodeToString(token)
|
||||||
|
|
||||||
hashedPassword, err := auth.HashPassword(input.Password)
|
hashedPassword, err := auth.HashPassword(input.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание пользователя
|
|
||||||
user := &domain.User{
|
user := &domain.User{
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
|
EmailConfirmationToken: confirmationToken,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
@ -66,10 +76,13 @@ func (s *authService) Register(ctx context.Context, input RegisterInput) (*domai
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.mailer.SendConfirmationEmail(user.Email, confirmationToken); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send confirmation email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вход в систему
|
|
||||||
func (s *authService) Login(ctx context.Context, email, password string) (*domain.Tokens, error) {
|
func (s *authService) Login(ctx context.Context, email, password string) (*domain.Tokens, error) {
|
||||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -79,33 +92,51 @@ func (s *authService) Login(ctx context.Context, email, password string) (*domai
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка пароля
|
|
||||||
if !auth.CheckPasswordHash(password, user.Password) {
|
if !auth.CheckPasswordHash(password, user.Password) {
|
||||||
return nil, errors.New("invalid credentials")
|
return nil, errors.New("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерация токенов
|
// Проверка подтверждения email (добавьте эту проверку)
|
||||||
|
if user.EmailConfirmedAt == nil {
|
||||||
|
return nil, errors.New("email not confirmed")
|
||||||
|
}
|
||||||
|
|
||||||
tokens, err := s.tokenAuth.GenerateTokens(user.ID)
|
tokens, err := s.tokenAuth.GenerateTokens(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Получаем информацию об устройстве (если используется)
|
||||||
|
//if deviceInfo, ok := ctx.Value("device_info").(*domain.Device); ok {
|
||||||
|
// // Здесь должна быть проверка isNewDevice и вызов notificationService
|
||||||
|
// // if isNewDevice { ... }
|
||||||
|
//}
|
||||||
|
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление токенов
|
|
||||||
func (s *authService) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) {
|
func (s *authService) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) {
|
||||||
userID, err := s.tokenAuth.ValidateRefreshToken(refreshToken)
|
userID, err := s.tokenAuth.ValidateRefreshToken(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid refresh token")
|
return nil, errors.New("invalid refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка существования пользователя
|
if _, err = s.userRepo.GetByID(ctx, userID); err != nil {
|
||||||
_, err = s.userRepo.GetByID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("user not found")
|
return nil, errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерация новых токенов
|
|
||||||
return s.tokenAuth.GenerateTokens(userID)
|
return s.tokenAuth.GenerateTokens(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *authService) ConfirmEmail(ctx context.Context, token string) error {
|
||||||
|
user, err := s.userRepo.GetByConfirmationToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid or expired confirmation token")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user.EmailConfirmedAt = &now
|
||||||
|
user.EmailConfirmationToken = ""
|
||||||
|
|
||||||
|
return s.userRepo.Update(ctx, user)
|
||||||
|
}
|
||||||
|
|||||||
65
internal/service/encryption_service.go
Normal file
65
internal/service/encryption_service.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"tailly_back_v2/internal/repository"
|
||||||
|
|
||||||
|
"tailly_back_v2/pkg/encryption"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncryptionService interface {
|
||||||
|
EncryptMessage(ctx context.Context, senderID int, receiverID int, message string) (string, error)
|
||||||
|
DecryptMessage(ctx context.Context, receiverID int, encryptedMsg string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type encryptionService struct {
|
||||||
|
vault *encryption.VaultService
|
||||||
|
userRepo repository.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncryptionService(vault *encryption.VaultService, userRepo repository.UserRepository) EncryptionService {
|
||||||
|
return &encryptionService{
|
||||||
|
vault: vault,
|
||||||
|
userRepo: userRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *encryptionService) EncryptMessage(ctx context.Context, senderID, receiverID int, message string) (string, error) {
|
||||||
|
// Проверяем существование получателя
|
||||||
|
_, err := s.userRepo.GetByID(ctx, receiverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get receiver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Шифруем через Vault (используем userID получателя как ключ)
|
||||||
|
encrypted, err := s.vault.Encrypt(fmt.Sprintf("user-%d", receiverID), []byte(message))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encrypt message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *encryptionService) DecryptMessage(ctx context.Context, receiverID int, encryptedMsg string) (string, error) {
|
||||||
|
// Проверяем существование получателя
|
||||||
|
_, err := s.userRepo.GetByID(ctx, receiverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to verify receiver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Декодируем из base64
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(encryptedMsg)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Расшифровываем через Vault
|
||||||
|
decrypted, err := s.vault.Decrypt(fmt.Sprintf("user-%d", receiverID), ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decrypt message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decrypted), nil
|
||||||
|
}
|
||||||
95
internal/service/mail_service.go
Normal file
95
internal/service/mail_service.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MailService interface {
|
||||||
|
SendEmail(to, subject, body string) error
|
||||||
|
SendTemplateEmail(to, subject string, templateName string, data interface{}) error
|
||||||
|
SendConfirmationEmail(to, token string) error
|
||||||
|
SendPasswordResetEmail(to, token string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mailService struct {
|
||||||
|
from string
|
||||||
|
smtpHost string
|
||||||
|
smtpPort int
|
||||||
|
smtpUser string
|
||||||
|
smtpPass string
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var templateFS embed.FS
|
||||||
|
|
||||||
|
func NewMailService(from, smtpHost string, smtpPort int, smtpUser, smtpPass string) (MailService, error) {
|
||||||
|
// Загружаем шаблоны писем
|
||||||
|
templates, err := template.ParseFS(templateFS, "templates/*.html")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse email templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mailService{
|
||||||
|
from: from,
|
||||||
|
smtpHost: smtpHost,
|
||||||
|
smtpPort: smtpPort,
|
||||||
|
smtpUser: smtpUser,
|
||||||
|
smtpPass: smtpPass,
|
||||||
|
templates: templates,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mailService) SendEmail(to, subject, body string) error {
|
||||||
|
mail := gomail.NewMessage()
|
||||||
|
mail.SetHeader("From", s.from)
|
||||||
|
mail.SetHeader("To", to)
|
||||||
|
mail.SetHeader("Subject", subject)
|
||||||
|
mail.SetBody("text/html", body)
|
||||||
|
|
||||||
|
dialer := gomail.NewDialer(s.smtpHost, s.smtpPort, s.smtpUser, s.smtpPass)
|
||||||
|
return dialer.DialAndSend(mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mailService) SendTemplateEmail(to, subject string, templateName string, data interface{}) error {
|
||||||
|
var body bytes.Buffer
|
||||||
|
if err := s.templates.ExecuteTemplate(&body, templateName+".html", data); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.SendEmail(to, subject, body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mailService) SendConfirmationEmail(to, token string) error {
|
||||||
|
data := struct {
|
||||||
|
Email string
|
||||||
|
Token string
|
||||||
|
Year int
|
||||||
|
}{
|
||||||
|
Email: to,
|
||||||
|
Token: token,
|
||||||
|
Year: time.Now().Year(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.SendTemplateEmail(to, "Подтверждение email", "confirmation", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mailService) SendPasswordResetEmail(to, token string) error {
|
||||||
|
data := struct {
|
||||||
|
Email string
|
||||||
|
Token string
|
||||||
|
Year int
|
||||||
|
}{
|
||||||
|
Email: to,
|
||||||
|
Token: token,
|
||||||
|
Year: time.Now().Year(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.SendTemplateEmail(to, "Сброс пароля", "password_reset", data)
|
||||||
|
}
|
||||||
93
internal/service/notification_service.go
Normal file
93
internal/service/notification_service.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
"tailly_back_v2/internal/repository"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationService interface {
|
||||||
|
NotifyNewDevice(ctx context.Context, userID int, device *domain.Device) error
|
||||||
|
GetNotifications(ctx context.Context, userID int) ([]*domain.Notification, error)
|
||||||
|
MarkAsRead(ctx context.Context, notificationID int) error
|
||||||
|
UpdatePreferences(ctx context.Context, prefs *domain.NotificationPreferences) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationService struct {
|
||||||
|
repo repository.NotificationRepository
|
||||||
|
userRepo repository.UserRepository
|
||||||
|
deviceRepo repository.DeviceRepository
|
||||||
|
mailer MailService
|
||||||
|
pusher PushService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notificationService) NotifyNewDevice(ctx context.Context, userID int, device *domain.Device) error {
|
||||||
|
user, err := s.userRepo.GetByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs, err := s.repo.GetPreferences(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем уведомление в БД
|
||||||
|
data, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"device_id": device.ID,
|
||||||
|
"ip_address": device.IPAddress,
|
||||||
|
"user_agent": device.UserAgent,
|
||||||
|
"created_at": device.CreatedAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
notification := &domain.Notification{
|
||||||
|
UserID: userID,
|
||||||
|
Type: "new_device",
|
||||||
|
Title: "New device connected",
|
||||||
|
Message: fmt.Sprintf("New login from %s (%s)", device.IPAddress, device.UserAgent),
|
||||||
|
Data: string(data),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Save(ctx, notification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем email уведомление
|
||||||
|
if prefs.EmailNewDevice {
|
||||||
|
s.mailer.SendEmail(
|
||||||
|
user.Email,
|
||||||
|
"New Device Alert",
|
||||||
|
fmt.Sprintf(`
|
||||||
|
<h1>New Device Alert</h1>
|
||||||
|
<p>Your account was accessed from:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Device: %s</li>
|
||||||
|
<li>IP: %s</li>
|
||||||
|
<li>Location: %s</li>
|
||||||
|
<li>Time: %s</li>
|
||||||
|
</ul>
|
||||||
|
<p>If this wasn't you, please terminate the session immediately.</p>
|
||||||
|
`, device.UserAgent, device.IPAddress, device.Location, device.CreatedAt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем push уведомление
|
||||||
|
if prefs.PushNewDevice {
|
||||||
|
s.pusher.SendPush(
|
||||||
|
userID,
|
||||||
|
"New Device Alert",
|
||||||
|
fmt.Sprintf("New login from %s", device.IPAddress),
|
||||||
|
map[string]interface{}{
|
||||||
|
"type": "new_device",
|
||||||
|
"device_id": device.ID,
|
||||||
|
"session_id": ctx.Value("sessionID"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
119
internal/service/recovery_service.go
Normal file
119
internal/service/recovery_service.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
"tailly_back_v2/internal/repository"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecoveryService interface {
|
||||||
|
InitiateRecovery(ctx context.Context, email string, newDevice *domain.Device) (string, error)
|
||||||
|
VerifyRecovery(ctx context.Context, token string) (*domain.Session, error)
|
||||||
|
AddRecoveryMethod(ctx context.Context, userID int, method *domain.RecoveryMethod) error
|
||||||
|
GetRecoveryMethods(ctx context.Context, userID int) ([]*domain.RecoveryMethod, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type recoveryService struct {
|
||||||
|
recoveryRepo repository.RecoveryRepository
|
||||||
|
userRepo repository.UserRepository
|
||||||
|
sessionRepo repository.SessionRepository
|
||||||
|
deviceRepo repository.DeviceRepository
|
||||||
|
mailer MailService
|
||||||
|
encryptor EncryptionService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recoveryService) InitiateRecovery(ctx context.Context, email string, newDevice *domain.Device) (string, error) {
|
||||||
|
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что у пользователя есть методы восстановления
|
||||||
|
methods, err := s.recoveryRepo.GetMethods(ctx, user.ID)
|
||||||
|
if err != nil || len(methods) == 0 {
|
||||||
|
return "", errors.New("no recovery methods available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем токен восстановления
|
||||||
|
token := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(token); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := base64.URLEncoding.EncodeToString(token)
|
||||||
|
|
||||||
|
req := &domain.RecoveryRequest{
|
||||||
|
UserID: user.ID,
|
||||||
|
Token: tokenStr,
|
||||||
|
NewDevice: newDevice,
|
||||||
|
Status: "pending",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.recoveryRepo.SaveRequest(ctx, req); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем письмо на все методы восстановления
|
||||||
|
for _, method := range methods {
|
||||||
|
switch method.MethodType {
|
||||||
|
case "email":
|
||||||
|
if err := s.mailer.SendRecoveryEmail(
|
||||||
|
method.Value,
|
||||||
|
tokenStr,
|
||||||
|
newDevice.IPAddress,
|
||||||
|
newDevice.UserAgent,
|
||||||
|
); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// case "phone": отправка SMS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recoveryService) VerifyRecovery(ctx context.Context, token string) (*domain.Session, error) {
|
||||||
|
req, err := s.recoveryRepo.GetRequestByToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid recovery token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(req.ExpiresAt) {
|
||||||
|
return nil, errors.New("recovery token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем новое устройство
|
||||||
|
if err := s.deviceRepo.Save(ctx, req.NewDevice); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем сессию
|
||||||
|
session := &domain.Session{
|
||||||
|
UserID: req.UserID,
|
||||||
|
DeviceID: req.NewDevice.ID,
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.sessionRepo.Save(ctx, session); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем новые ключи шифрования
|
||||||
|
if _, err := s.encryptor.GenerateKeyPair(ctx, req.UserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Помечаем запрос как выполненный
|
||||||
|
req.Status = "completed"
|
||||||
|
if err := s.recoveryRepo.UpdateRequest(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
// Services объединяет все сервисы приложения
|
import _ "tailly_back_v2/internal/repository"
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
Auth AuthService
|
Auth AuthService
|
||||||
User UserService
|
User UserService
|
||||||
Post PostService
|
Post PostService
|
||||||
Comment CommentService
|
Comment CommentService
|
||||||
Like LikeService
|
Like LikeService
|
||||||
Chat ChatService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServices(
|
func NewServices(
|
||||||
@ -16,7 +16,6 @@ func NewServices(
|
|||||||
postService PostService,
|
postService PostService,
|
||||||
commentService CommentService,
|
commentService CommentService,
|
||||||
likeService LikeService,
|
likeService LikeService,
|
||||||
chatService ChatService,
|
|
||||||
) *Services {
|
) *Services {
|
||||||
return &Services{
|
return &Services{
|
||||||
Auth: authService,
|
Auth: authService,
|
||||||
@ -24,6 +23,5 @@ func NewServices(
|
|||||||
Post: postService,
|
Post: postService,
|
||||||
Comment: commentService,
|
Comment: commentService,
|
||||||
Like: likeService,
|
Like: likeService,
|
||||||
Chat: chatService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
internal/service/session_service.go
Normal file
103
internal/service/session_service.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
"tailly_back_v2/internal/repository"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionService interface {
|
||||||
|
InitiateSession(ctx context.Context, device *domain.Device) error
|
||||||
|
ConfirmSession(ctx context.Context, token string) (*domain.Session, error)
|
||||||
|
GetActiveSessions(ctx context.Context, userID int) ([]*domain.Session, error)
|
||||||
|
TerminateSession(ctx context.Context, sessionID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionService struct {
|
||||||
|
sessionRepo repository.SessionRepository
|
||||||
|
deviceRepo repository.DeviceRepository
|
||||||
|
mailer MailService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionService) InitiateSession(ctx context.Context, device *domain.Device) error {
|
||||||
|
// Генерация токена подтверждения
|
||||||
|
token := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(token); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
device.ConfirmationToken = base64.URLEncoding.EncodeToString(token)
|
||||||
|
device.ExpiresAt = time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
if err := s.deviceRepo.Save(ctx, device); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправка письма
|
||||||
|
user, err := s.userRepo.GetByID(ctx, device.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.mailer.SendSessionConfirmation(
|
||||||
|
user.Email,
|
||||||
|
device.ConfirmationToken,
|
||||||
|
device.IPAddress,
|
||||||
|
device.UserAgent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionService) ConfirmSession(ctx context.Context, token string) (*domain.Session, error) {
|
||||||
|
device, err := s.deviceRepo.GetByToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(device.ExpiresAt) {
|
||||||
|
return nil, errors.New("confirmation token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &domain.Session{
|
||||||
|
UserID: device.UserID,
|
||||||
|
DeviceID: device.ID,
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.sessionRepo.Save(ctx, session); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionService) GetUserSessions(ctx context.Context, userID int) ([]*domain.Session, error) {
|
||||||
|
sessions, err := s.sessionRepo.GetActiveByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSessionID, _ := ctx.Value("sessionID").(int)
|
||||||
|
|
||||||
|
for _, session := range sessions {
|
||||||
|
session.IsCurrent = (session.ID == currentSessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionService) Terminate(ctx context.Context, userID, sessionID int) error {
|
||||||
|
session, err := s.sessionRepo.GetByID(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.UserID != userID {
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.sessionRepo.Terminate(ctx, sessionID)
|
||||||
|
}
|
||||||
14
internal/service/templates/confirmation.html
Normal file
14
internal/service/templates/confirmation.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Подтверждение email</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Подтвердите ваш email</h1>
|
||||||
|
<p>Для завершения регистрации перейдите по ссылке:</p>
|
||||||
|
<a href="{{.AppURL}}/confirm-email?token={{.Token}}">Подтвердить email</a>
|
||||||
|
<p>С уважением,<br>Команда сервиса</p>
|
||||||
|
<p>© {{.Year}}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
internal/service/templates/password_reset.html
Normal file
14
internal/service/templates/password_reset.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Сброс пароля</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Сброс пароля</h1>
|
||||||
|
<p>Для сброса пароля перейдите по ссылке:</p>
|
||||||
|
<a href="{{.AppURL}}/reset-password?token={{.Token}}">Сбросить пароль</a>
|
||||||
|
<p>Ссылка действительна 24 часа.</p>
|
||||||
|
<p>© {{.Year}}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
internal/ws/notification_hub.go
Normal file
59
internal/ws/notification_hub.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package ws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"tailly_back_v2/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationClient struct {
|
||||||
|
UserID int
|
||||||
|
Send chan *domain.Notification
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationHub struct {
|
||||||
|
clients map[int]*NotificationClient
|
||||||
|
register chan *NotificationClient
|
||||||
|
unregister chan *NotificationClient
|
||||||
|
broadcast chan *domain.Notification
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationHub() *NotificationHub {
|
||||||
|
return &NotificationHub{
|
||||||
|
clients: make(map[int]*NotificationClient),
|
||||||
|
register: make(chan *NotificationClient),
|
||||||
|
unregister: make(chan *NotificationClient),
|
||||||
|
broadcast: make(chan *domain.Notification),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NotificationHub) Run() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case client := <-h.register:
|
||||||
|
h.mu.Lock()
|
||||||
|
h.clients[client.UserID] = client
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
case client := <-h.unregister:
|
||||||
|
h.mu.Lock()
|
||||||
|
if c, ok := h.clients[client.UserID]; ok {
|
||||||
|
close(c.Send)
|
||||||
|
delete(h.clients, client.UserID)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
case notification := <-h.broadcast:
|
||||||
|
h.mu.RLock()
|
||||||
|
if client, ok := h.clients[notification.UserID]; ok {
|
||||||
|
select {
|
||||||
|
case client.Send <- notification:
|
||||||
|
default:
|
||||||
|
close(client.Send)
|
||||||
|
delete(h.clients, notification.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
pkg/encryption/vault_service.go
Normal file
89
pkg/encryption/vault_service.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package encryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VaultService struct {
|
||||||
|
addr string
|
||||||
|
token string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVaultService(addr, token string) *VaultService {
|
||||||
|
return &VaultService{
|
||||||
|
addr: addr,
|
||||||
|
token: token,
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultService) Encrypt(keyID string, plaintext []byte) ([]byte, error) {
|
||||||
|
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"plaintext": base64.StdEncoding.EncodeToString(plaintext),
|
||||||
|
})
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(
|
||||||
|
"POST",
|
||||||
|
fmt.Sprintf("%s/v1/transit/encrypt/%s", v.addr, keyID),
|
||||||
|
bytes.NewBuffer(reqBody),
|
||||||
|
)
|
||||||
|
req.Header.Set("X-Vault-Token", v.token)
|
||||||
|
|
||||||
|
resp, err := v.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(
|
||||||
|
result["data"].(map[string]interface{})["ciphertext"].(string),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultService) Decrypt(keyID string, ciphertext []byte) ([]byte, error) {
|
||||||
|
reqBody, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"ciphertext": string(ciphertext),
|
||||||
|
})
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(
|
||||||
|
"POST",
|
||||||
|
fmt.Sprintf("%s/v1/transit/decrypt/%s", v.addr, keyID),
|
||||||
|
bytes.NewBuffer(reqBody),
|
||||||
|
)
|
||||||
|
req.Header.Set("X-Vault-Token", v.token)
|
||||||
|
|
||||||
|
resp, err := v.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := base64.StdEncoding.DecodeString(
|
||||||
|
result["data"].(map[string]interface{})["plaintext"].(string),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user