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/pkg/auth"
|
||||
"tailly_back_v2/pkg/database"
|
||||
"tailly_back_v2/pkg/encryption"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -38,31 +39,52 @@ func main() {
|
||||
cfg.Auth.RefreshTokenExpiry,
|
||||
)
|
||||
|
||||
// Инициализация чата
|
||||
chatHub := ws.NewChatHub()
|
||||
go chatHub.Run()
|
||||
|
||||
// Репозитории
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
postRepo := repository.NewPostRepository(db)
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
likeRepo := repository.NewLikeRepository(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(
|
||||
service.NewAuthService(userRepo, *tokenAuth),
|
||||
service.NewAuthService(userRepo, tokenAuth, mailService),
|
||||
service.NewUserService(userRepo),
|
||||
service.NewPostService(postRepo),
|
||||
service.NewCommentService(commentRepo),
|
||||
service.NewLikeService(likeRepo),
|
||||
service.NewChatService(chatRepo),
|
||||
service.NewEncryptionService(vaultService, userRepo),
|
||||
)
|
||||
// Инициализация чата
|
||||
chatHub := ws.NewChatHub()
|
||||
go chatHub.Run()
|
||||
|
||||
chatService := service.NewChatService(
|
||||
chatRepo,
|
||||
userRepo,
|
||||
chatHub,
|
||||
encryptionService,
|
||||
)
|
||||
// HTTP сервер
|
||||
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
|
||||
|
||||
resolver:
|
||||
filename: internal/http/graph
|
||||
dir: internal/http/graph/resolvers
|
||||
layout: follow-schema
|
||||
package: graph
|
||||
skip_generation:
|
||||
- "*_resolvers.go"
|
||||
- "resolvers.go"
|
||||
|
||||
models:
|
||||
User:
|
||||
model: tailly_back_v2/internal/domain.User
|
||||
Post:
|
||||
model: tailly_back_v2/internal/domain.Post
|
||||
RegisterInput:
|
||||
model: tailly_back_v2/internal/domain.RegisterInput
|
||||
LoginInput:
|
||||
model: tailly_back_v2/internal/domain.LoginInput
|
||||
Comment:
|
||||
model: tailly_back_v2/internal/domain.Comment
|
||||
Like:
|
||||
model: tailly_back_v2/internal/domain.like
|
||||
Tokens:
|
||||
model: tailly_back_v2/internal/domain.Tokens
|
||||
model: tailly_back_v2/internal/domain.Like
|
||||
Device:
|
||||
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:
|
||||
model: tailly_back_v2/internal/domain.Chat
|
||||
Message:
|
||||
model: tailly_back_v2/internal/domain.Message
|
||||
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:
|
||||
- "tailly_back_v2/internal/domain"
|
||||
@ -1,13 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/caarlos0/env/v8"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
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 {
|
||||
DSN string `env:"DB_DSN,required"`
|
||||
@ -18,4 +19,26 @@ type Config struct {
|
||||
AccessTokenExpiry time.Duration `env:"ACCESS_TOKEN_EXPIRY" env-default:"15m"`
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -10,6 +9,8 @@ type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
EmailConfirmationToken string `json:"-"`
|
||||
EmailConfirmedAt *time.Time `json:"emailConfirmedAt,omitempty"`
|
||||
Password string `json:"-"` // Пароль не должен возвращаться в ответах
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
@ -49,7 +50,6 @@ type Tokens struct {
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
// Типы для аутентификации
|
||||
type RegisterInput struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
@ -60,13 +60,3 @@ type LoginInput struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type MutationResolver interface {
|
||||
CreatePost(ctx context.Context, title string, content string) (*Post, error)
|
||||
AddComment(ctx context.Context, postID int, content string) (*Comment, error)
|
||||
}
|
||||
|
||||
type QueryResolver interface {
|
||||
Posts(ctx context.Context) ([]*Post, error)
|
||||
Post(ctx context.Context, id int) (*Post, error)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/service"
|
||||
_ "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! # Уникальный идентификатор
|
||||
username: String! # Имя пользователя
|
||||
email: String! # Email (уникальный)
|
||||
emailConfirmedAt: String # Дата подтверждения email (может быть null)
|
||||
createdAt: String! # Дата создания
|
||||
updatedAt: String! # Дата обновления
|
||||
}
|
||||
@ -43,9 +44,21 @@ type Like {
|
||||
type Tokens {
|
||||
accessToken: String! # Access токен (короткоживущий)
|
||||
refreshToken: String! # Refresh токен (долгоживущий)
|
||||
emailConfirmed: Boolean! # Флаг подтверждения email
|
||||
}
|
||||
|
||||
# Входные данные для регистрации
|
||||
input RegisterInput {
|
||||
username: String! # Имя пользователя
|
||||
email: String! # Email
|
||||
password: String! # Пароль
|
||||
}
|
||||
|
||||
# Входные данные для входа
|
||||
input LoginInput {
|
||||
email: String! # Email
|
||||
password: String! # Пароль
|
||||
}
|
||||
|
||||
type Message {
|
||||
id: ID!
|
||||
@ -62,19 +75,21 @@ type ChatSession {
|
||||
unreadCount: Int!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageReceived: Message!
|
||||
type Session {
|
||||
id: ID!
|
||||
device: Device!
|
||||
startedAt: String!
|
||||
lastActiveAt: String!
|
||||
isCurrent: Boolean!
|
||||
}
|
||||
|
||||
input RegisterInput {
|
||||
username: String!
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
email: String!
|
||||
password: String!
|
||||
type Device {
|
||||
id: ID!
|
||||
name: String!
|
||||
type: String!
|
||||
ipAddress: String!
|
||||
location: String!
|
||||
lastActiveAt: String!
|
||||
}
|
||||
|
||||
# Запросы (получение данных)
|
||||
@ -85,6 +100,8 @@ type Query {
|
||||
user(id: ID!): User! # Получить пользователя по ID
|
||||
getChatHistory(userId: ID!): [Message!]!
|
||||
getUserChats: [ChatSession!]!
|
||||
mySessions: [Session!]!
|
||||
activeSessions: [Session!]!
|
||||
}
|
||||
|
||||
# Мутации (изменение данных)
|
||||
@ -109,9 +126,22 @@ type Mutation {
|
||||
|
||||
# Удаление лайка
|
||||
unlikePost(postId: ID!): Boolean!
|
||||
|
||||
updateProfile(username: String!, email: String!): User!
|
||||
changePassword(oldPassword: String!, newPassword: String!): Boolean!
|
||||
sendMessage(receiverId: ID!, content: String!): Message!
|
||||
markAsRead(messageId: ID!): Boolean!
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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"
|
||||
"tailly_back_v2/internal/config"
|
||||
"tailly_back_v2/internal/http/graph"
|
||||
"tailly_back_v2/internal/http/handlers"
|
||||
"tailly_back_v2/internal/http/middleware"
|
||||
"tailly_back_v2/internal/service"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
@ -42,13 +43,21 @@ func (s *Server) configureRouter() {
|
||||
"http://localhost:3000", // React dev server
|
||||
"https://your-production.app", // Продакшен домен
|
||||
}
|
||||
|
||||
// Логирование
|
||||
logger := log.New(os.Stdout, "HTTP: ", log.LstdFlags)
|
||||
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)
|
||||
// Middleware
|
||||
|
||||
s.router.Use(middleware.CORS(allowedOrigins))
|
||||
s.router.Use(middleware.AuthMiddleware(s.tokenAuth))
|
||||
|
||||
@ -72,7 +81,6 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Отдельный endpoint для метрик Prometheus
|
||||
func (s *Server) configureMetrics() {
|
||||
prometheus.MustRegister(
|
||||
middleware.httpRequestsTotal,
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -15,6 +16,7 @@ type UserRepository interface {
|
||||
Create(ctx context.Context, user *domain.User) error
|
||||
GetByID(ctx context.Context, id int) (*domain.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
GetByConfirmationToken(ctx context.Context, token string) (*domain.User, error)
|
||||
Update(ctx context.Context, user *domain.User) 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 {
|
||||
query := `
|
||||
INSERT INTO users (username, email, password, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO users (username, email, password, email_confirmation_token, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
@ -38,6 +40,7 @@ func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
|
||||
user.Username,
|
||||
user.Email,
|
||||
user.Password,
|
||||
user.EmailConfirmationToken,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
).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) {
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
user := &domain.User{}
|
||||
var confirmedAt sql.NullTime
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.Email,
|
||||
&user.Password,
|
||||
&user.EmailConfirmationToken,
|
||||
&confirmedAt,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
@ -66,7 +73,111 @@ func (r *userRepository) GetByID(ctx context.Context, id int) (*domain.User, err
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
if confirmedAt.Valid {
|
||||
user.EmailConfirmedAt = &confirmedAt.Time
|
||||
}
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"tailly_back_v2/internal/domain"
|
||||
"tailly_back_v2/internal/repository"
|
||||
"tailly_back_v2/pkg/auth"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Интерфейс сервиса аутентификации
|
||||
type AuthService interface {
|
||||
Register(ctx context.Context, input RegisterInput) (*domain.User, error)
|
||||
Login(ctx context.Context, email, password string) (*domain.Tokens, error)
|
||||
RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error)
|
||||
ConfirmEmail(ctx context.Context, token string) error
|
||||
}
|
||||
|
||||
// Реализация сервиса аутентификации
|
||||
type authService struct {
|
||||
userRepo repository.UserRepository
|
||||
tokenAuth auth.TokenAuth
|
||||
mailer MailService
|
||||
}
|
||||
|
||||
// Конструктор сервиса
|
||||
func NewAuthService(userRepo repository.UserRepository, tokenAuth auth.TokenAuth) AuthService {
|
||||
func NewAuthService(
|
||||
userRepo repository.UserRepository,
|
||||
tokenAuth auth.TokenAuth,
|
||||
mailer MailService,
|
||||
) AuthService {
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
tokenAuth: tokenAuth,
|
||||
mailer: mailer,
|
||||
}
|
||||
}
|
||||
|
||||
// Входные данные для регистрации
|
||||
type RegisterInput struct {
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Регистрация нового пользователя
|
||||
func (s *authService) Register(ctx context.Context, input RegisterInput) (*domain.User, error) {
|
||||
// Проверка существования пользователя
|
||||
_, err := s.userRepo.GetByEmail(ctx, input.Email)
|
||||
if err == nil {
|
||||
return nil, errors.New("user with this email already exists")
|
||||
@ -47,17 +51,23 @@ func (s *authService) Register(ctx context.Context, input RegisterInput) (*domai
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Создание пользователя
|
||||
user := &domain.User{
|
||||
Username: input.Username,
|
||||
Email: input.Email,
|
||||
Password: hashedPassword,
|
||||
EmailConfirmationToken: confirmationToken,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@ -66,10 +76,13 @@ func (s *authService) Register(ctx context.Context, input RegisterInput) (*domai
|
||||
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
|
||||
}
|
||||
|
||||
// Вход в систему
|
||||
func (s *authService) Login(ctx context.Context, email, password string) (*domain.Tokens, error) {
|
||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
@ -79,33 +92,51 @@ func (s *authService) Login(ctx context.Context, email, password string) (*domai
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверка пароля
|
||||
if !auth.CheckPasswordHash(password, user.Password) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Получаем информацию об устройстве (если используется)
|
||||
//if deviceInfo, ok := ctx.Value("device_info").(*domain.Device); ok {
|
||||
// // Здесь должна быть проверка isNewDevice и вызов notificationService
|
||||
// // if isNewDevice { ... }
|
||||
//}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// Обновление токенов
|
||||
func (s *authService) RefreshTokens(ctx context.Context, refreshToken string) (*domain.Tokens, error) {
|
||||
userID, err := s.tokenAuth.ValidateRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
// Проверка существования пользователя
|
||||
_, err = s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
if _, err = s.userRepo.GetByID(ctx, userID); err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
// Генерация новых токенов
|
||||
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
|
||||
|
||||
// Services объединяет все сервисы приложения
|
||||
import _ "tailly_back_v2/internal/repository"
|
||||
|
||||
type Services struct {
|
||||
Auth AuthService
|
||||
User UserService
|
||||
Post PostService
|
||||
Comment CommentService
|
||||
Like LikeService
|
||||
Chat ChatService
|
||||
}
|
||||
|
||||
func NewServices(
|
||||
@ -16,7 +16,6 @@ func NewServices(
|
||||
postService PostService,
|
||||
commentService CommentService,
|
||||
likeService LikeService,
|
||||
chatService ChatService,
|
||||
) *Services {
|
||||
return &Services{
|
||||
Auth: authService,
|
||||
@ -24,6 +23,5 @@ func NewServices(
|
||||
Post: postService,
|
||||
Comment: commentService,
|
||||
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