Handlers, DB, Cache
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@ __debug_bin*
|
|||||||
static
|
static
|
||||||
testdata/
|
testdata/
|
||||||
/payouts.properties
|
/payouts.properties
|
||||||
|
/payouts.db
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/fx/fxevent"
|
||||||
|
|
||||||
"payouts/internal/api"
|
"payouts/internal/api"
|
||||||
"payouts/internal/config"
|
"payouts/internal/config"
|
||||||
"payouts/internal/log"
|
"payouts/internal/log"
|
||||||
|
"payouts/internal/service/cache"
|
||||||
|
"payouts/internal/service/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
app := fx.New(
|
app := fx.New(
|
||||||
api.Module,
|
api.Module,
|
||||||
|
cache.Module,
|
||||||
config.Module,
|
config.Module,
|
||||||
|
database.Module,
|
||||||
|
|
||||||
log.Module,
|
log.Module,
|
||||||
|
fx.WithLogger(func() fxevent.Logger {
|
||||||
|
// log internal fx events just to stdout json because app config isn't read yet
|
||||||
|
return &fxevent.SlogLogger{Logger: slog.New(slog.NewJSONHandler(os.Stdout, nil))}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
app.Run()
|
app.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,23 @@ Log.FileEnabled = false
|
|||||||
# type: sqlite, postgres
|
# type: sqlite, postgres
|
||||||
Database.Type =
|
Database.Type =
|
||||||
# connection string:
|
# connection string:
|
||||||
# sqlite: flibooks.db
|
# sqlite: payouts.db
|
||||||
# postgres: host=127.0.0.1 user=gorm password=gorm dbname=gorm port=5432 sslmode=disable
|
# postgres: host=127.0.0.1 user=gorm password=gorm dbname=gorm port=5432 sslmode=disable
|
||||||
Database.Connection =
|
Database.Connection =
|
||||||
# DB log level
|
# DB log level
|
||||||
Database.LogLevel = Info
|
Database.LogLevel = Info
|
||||||
|
# Trace all requests
|
||||||
|
Database.TraceRequests = false
|
||||||
|
|
||||||
|
# Session cache TTL
|
||||||
|
Cache.TTL = 24h
|
||||||
|
|
||||||
|
# Yookassa related props
|
||||||
|
# Base API Url
|
||||||
|
YooKassa.BaseUrl = https://api.yookassa.ru/v3
|
||||||
|
# Base API key/secret
|
||||||
|
YooKassa.ApiBaseKey =
|
||||||
|
YooKassa.ApiBaseSecret =
|
||||||
|
# Payments API key/secret
|
||||||
|
YooKassa.ApiPaymentKey =
|
||||||
|
YooKassa.ApiPaymentSecret =
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -5,13 +5,17 @@ go 1.24.4
|
|||||||
require (
|
require (
|
||||||
github.com/go-viper/encoding/javaproperties v0.1.0
|
github.com/go-viper/encoding/javaproperties v0.1.0
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||||
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/ogier/pflag v0.0.1
|
github.com/ogier/pflag v0.0.1
|
||||||
github.com/orandin/slog-gorm v1.4.0
|
github.com/orandin/slog-gorm v1.4.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/samber/slog-multi v1.7.1
|
github.com/samber/slog-multi v1.7.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
go.uber.org/fx v1.24.0
|
go.uber.org/fx v1.24.0
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
@@ -47,9 +51,8 @@ require (
|
|||||||
go.uber.org/zap v1.26.0 // indirect
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
22
go.sum
22
go.sum
@@ -15,6 +15,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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/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/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -25,6 +27,10 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
|||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||||
|
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
@@ -100,14 +106,14 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"payouts/internal/api/payment"
|
||||||
|
"payouts/internal/api/user"
|
||||||
"payouts/internal/api/version"
|
"payouts/internal/api/version"
|
||||||
appConfig "payouts/internal/config"
|
appConfig "payouts/internal/config"
|
||||||
"payouts/internal/service/monitoring"
|
"payouts/internal/service/monitoring"
|
||||||
@@ -18,8 +20,11 @@ import (
|
|||||||
|
|
||||||
// Module is a fx module
|
// Module is a fx module
|
||||||
var Module = fx.Options(
|
var Module = fx.Options(
|
||||||
|
user.Module,
|
||||||
|
payment.Module,
|
||||||
version.Module,
|
version.Module,
|
||||||
monitoring.Module,
|
monitoring.Module,
|
||||||
|
|
||||||
fx.Invoke(RegisterRoutes),
|
fx.Invoke(RegisterRoutes),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +37,8 @@ type Params struct {
|
|||||||
|
|
||||||
AppConfig *appConfig.App
|
AppConfig *appConfig.App
|
||||||
|
|
||||||
|
PaymentHandler payment.Handler
|
||||||
|
UserHandler user.Handler
|
||||||
Version version.Handler
|
Version version.Handler
|
||||||
|
|
||||||
Metrics monitoring.Metrics
|
Metrics monitoring.Metrics
|
||||||
@@ -54,11 +61,16 @@ func RegisterRoutes(p Params, lc fx.Lifecycle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiRouter := router.PathPrefix(BaseRoute).Subrouter()
|
apiRouter := router.PathPrefix(BaseRoute).Subrouter()
|
||||||
apiRouter.HandleFunc("/test", func(http.ResponseWriter, *http.Request) {
|
|
||||||
slog.Info("Test called", slog.String("sample", "value"))
|
|
||||||
|
|
||||||
})
|
userRouter := apiRouter.PathPrefix(user.BaseRoute).Subrouter()
|
||||||
// data
|
userRouter.HandleFunc(user.RegisterRoute, p.UserHandler.UserRegister).Methods(http.MethodPost)
|
||||||
|
userRouter.HandleFunc(user.LoginRoute, p.UserHandler.UserLogin).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
paymentRouter := apiRouter.PathPrefix(payment.BaseRoute).Subrouter()
|
||||||
|
paymentRouter.HandleFunc(payment.CreateRoute, p.PaymentHandler.PaymentCreate).Methods(http.MethodPost)
|
||||||
|
paymentRouter.HandleFunc(payment.CallbackRoute, p.PaymentHandler.PaymentCallback).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
// collect api metrics
|
||||||
apiRouter.Use(p.Metrics.GetMiddleware())
|
apiRouter.Use(p.Metrics.GetMiddleware())
|
||||||
|
|
||||||
router.Handle(p.AppConfig.Metrics.Endpoint, promhttp.Handler())
|
router.Handle(p.AppConfig.Metrics.Endpoint, promhttp.Handler())
|
||||||
|
|||||||
17
internal/api/payment/module.go
Normal file
17
internal/api/payment/module.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Options(
|
||||||
|
fx.Provide(NewPaymentHandler),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
GetSbpBanks(http.ResponseWriter, *http.Request)
|
||||||
|
PaymentCreate(http.ResponseWriter, *http.Request)
|
||||||
|
PaymentCallback(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
||||||
54
internal/api/payment/payment_handler.go
Normal file
54
internal/api/payment/payment_handler.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"payouts/internal/config"
|
||||||
|
"payouts/internal/service/cache"
|
||||||
|
"payouts/internal/service/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BaseRoute = "/payment"
|
||||||
|
CreateRoute = "/create"
|
||||||
|
CallbackRoute = "/callback"
|
||||||
|
BanksRoute = "/sbp/banks"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paymentHandler struct {
|
||||||
|
dbService database.Service
|
||||||
|
cacheService cache.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params represents the module input params
|
||||||
|
type Params struct {
|
||||||
|
fx.In
|
||||||
|
|
||||||
|
AppConfig *config.App
|
||||||
|
DbService database.Service
|
||||||
|
CacheService cache.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaymentHandler(p Params) (Handler, error) {
|
||||||
|
return &paymentHandler{
|
||||||
|
dbService: p.DbService,
|
||||||
|
cacheService: p.CacheService,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSbpBanks implements [Handler].
|
||||||
|
func (p *paymentHandler) GetSbpBanks(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentCreate implements [Handler].
|
||||||
|
func (p *paymentHandler) PaymentCreate(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentCallback implements [Handler].
|
||||||
|
func (p *paymentHandler) PaymentCallback(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
16
internal/api/user/module.go
Normal file
16
internal/api/user/module.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
UserRegister(http.ResponseWriter, *http.Request)
|
||||||
|
UserLogin(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Options(
|
||||||
|
fx.Provide(NewUserHandler),
|
||||||
|
)
|
||||||
144
internal/api/user/user_handler.go
Normal file
144
internal/api/user/user_handler.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"payouts/internal/config"
|
||||||
|
"payouts/internal/models"
|
||||||
|
"payouts/internal/service/cache"
|
||||||
|
"payouts/internal/service/database"
|
||||||
|
"payouts/internal/service/database/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BaseRoute = "/user"
|
||||||
|
RegisterRoute = "/register"
|
||||||
|
LoginRoute = "/login"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userHandler struct {
|
||||||
|
ttl time.Duration
|
||||||
|
dbService database.Service
|
||||||
|
cacheService cache.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params represents the module input params
|
||||||
|
type Params struct {
|
||||||
|
fx.In
|
||||||
|
|
||||||
|
AppConfig *config.App
|
||||||
|
DbService database.Service
|
||||||
|
CacheService cache.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserHandler(p Params) (Handler, error) {
|
||||||
|
return &userHandler{
|
||||||
|
ttl: p.AppConfig.Cache.TTL,
|
||||||
|
dbService: p.DbService,
|
||||||
|
cacheService: p.CacheService,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRegister implements [Handler].
|
||||||
|
func (u *userHandler) UserRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
errResponse := func(message string, err error, status int) {
|
||||||
|
http.Error(w, errors.Join(errors.New(message), err).Error(), status)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := models.UserRegister{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get password hash", slog.String("error", err.Error()))
|
||||||
|
errResponse("failed to decode request body", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Passwd != user.PasswdCfm || len(user.Passwd) == 0 || len(user.Phone) == 0 || len(user.TIN) == 0 {
|
||||||
|
slog.Error("No required parameters passed")
|
||||||
|
errResponse("invalid parameters", nil, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Passwd), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get password hash", slog.String("error", err.Error()))
|
||||||
|
errResponse("internal error", nil, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.PasswdHash = string(hashedPassword)
|
||||||
|
|
||||||
|
ormUser := orm.User{}
|
||||||
|
copier.Copy(&ormUser, user)
|
||||||
|
|
||||||
|
// todo: add data validation
|
||||||
|
err = u.dbService.CreateUser(ormUser, database.WithContext(r.Context()))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create user", slog.String("error", err.Error()))
|
||||||
|
errResponse("failed to create user", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserLogin implements [Handler].
|
||||||
|
func (u *userHandler) UserLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
errResponse := func(message string, err error, status int) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
http.Error(w, errors.Join(errors.New(message), err).Error(), status)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := models.UserLoginReq{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
errResponse("failed to decode request body", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.Phone) == 0 || len(user.Passwd) == 0 {
|
||||||
|
slog.Error("No required parameters passed")
|
||||||
|
errResponse("invalid parameters", nil, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ormUser, err := u.dbService.GetUser(orm.User{Phone: user.Phone}, database.WithContext(r.Context()))
|
||||||
|
if err != nil {
|
||||||
|
errResponse("invalid credentials", nil, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(ormUser.PasswdHash), []byte(user.Passwd))
|
||||||
|
if err != nil {
|
||||||
|
errResponse("invalid credentials", nil, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := uuid.New().String()
|
||||||
|
|
||||||
|
u.cacheService.PutSession(sessionId, ormUser)
|
||||||
|
|
||||||
|
resp := models.UserLoginResp{
|
||||||
|
Token: sessionId,
|
||||||
|
TokenTtl: time.Now().Add(u.ttl).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
errResponse("failed to encode response", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,18 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
logging "payouts/internal/log/config"
|
logging "payouts/internal/log/config"
|
||||||
|
cache "payouts/internal/service/cache/config"
|
||||||
database "payouts/internal/service/database/config"
|
database "payouts/internal/service/database/config"
|
||||||
monitoring "payouts/internal/service/monitoring/config"
|
monitoring "payouts/internal/service/monitoring/config"
|
||||||
|
yookassa "payouts/internal/service/yookassa/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Server Server
|
Server Server
|
||||||
Metrics monitoring.Metrics
|
Metrics monitoring.Metrics
|
||||||
Database database.Database
|
Database database.Database
|
||||||
|
Cache cache.Cache
|
||||||
Log logging.Log
|
Log logging.Log
|
||||||
|
|
||||||
|
YooKassa yookassa.YooKassa
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
ConfigPathArg = "config-path"
|
ConfigPathArg = "config-path"
|
||||||
ConfigPathDefault = "./payouts.properties"
|
ConfigPathDefault = "./payouts.properties"
|
||||||
|
envConfigFile = "CONFIG_PATH"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Module = fx.Provide(NewAppConfig)
|
var Module = fx.Provide(NewAppConfig)
|
||||||
@@ -36,7 +38,7 @@ func getConfigData(filePath string) (string, string, string) {
|
|||||||
func NewAppConfig() (*App, error) {
|
func NewAppConfig() (*App, error) {
|
||||||
mainConfig := &App{}
|
mainConfig := &App{}
|
||||||
|
|
||||||
configPaths := []string{ConfigPathDefault}
|
configPaths := []string{ConfigPathDefault, os.Getenv(envConfigFile)}
|
||||||
|
|
||||||
configPath := pflag.String(ConfigPathArg, "", "")
|
configPath := pflag.String(ConfigPathArg, "", "")
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|||||||
20
internal/models/user.go
Normal file
20
internal/models/user.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type UserRegister struct {
|
||||||
|
TIN string `json:"tin"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Passwd string `json:"password,omitempty"`
|
||||||
|
PasswdCfm string `json:"password_cfm,omitempty"`
|
||||||
|
PasswdHash string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserLoginReq struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Passwd string `json:"password"`
|
||||||
|
PasswdHash string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserLoginResp struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
TokenTtl int64 `json:"token_ttl"`
|
||||||
|
}
|
||||||
39
internal/service/cache/cache_service.go
vendored
Normal file
39
internal/service/cache/cache_service.go
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jellydator/ttlcache/v3"
|
||||||
|
|
||||||
|
"payouts/internal/service/database/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheService struct {
|
||||||
|
cache *ttlcache.Cache[string, orm.User]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionCache(ttl time.Duration) (Service, error) {
|
||||||
|
return &cacheService{
|
||||||
|
cache: ttlcache.New(ttlcache.WithTTL[string, orm.User](ttl)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutSession implements [Service].
|
||||||
|
func (c *cacheService) PutSession(sessionID string, user orm.User) {
|
||||||
|
c.cache.Set(sessionID, user, ttlcache.DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserFromSession implements [Service].
|
||||||
|
func (c *cacheService) GetSession(sessionId string) (orm.User, error) {
|
||||||
|
if !c.cache.Has(sessionId) {
|
||||||
|
return orm.User{}, NoSessionFound
|
||||||
|
}
|
||||||
|
cachedItem := c.cache.Get(sessionId)
|
||||||
|
|
||||||
|
return cachedItem.Value(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartBackground implements [Service].
|
||||||
|
func (c *cacheService) StartBackground() {
|
||||||
|
c.cache.Start()
|
||||||
|
}
|
||||||
7
internal/service/cache/config/cache.go
vendored
Normal file
7
internal/service/cache/config/cache.go
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
50
internal/service/cache/module.go
vendored
Normal file
50
internal/service/cache/module.go
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"payouts/internal/config"
|
||||||
|
"payouts/internal/service/database/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Options(
|
||||||
|
fx.Provide(New),
|
||||||
|
|
||||||
|
fx.Invoke(StartCache),
|
||||||
|
)
|
||||||
|
|
||||||
|
var NoSessionFound = errors.New("no session found")
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
PutSession(string, orm.User)
|
||||||
|
GetSession(string) (orm.User, error)
|
||||||
|
StartBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
fx.In
|
||||||
|
|
||||||
|
AppConfig *config.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(p Params) (Service, error) {
|
||||||
|
return NewSessionCache(p.AppConfig.Cache.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers the api routes and starts the http server
|
||||||
|
func StartCache(s Service, lc fx.Lifecycle) {
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(c context.Context) error {
|
||||||
|
go func() {
|
||||||
|
s.StartBackground()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ type Database struct {
|
|||||||
Type string
|
Type string
|
||||||
Connection string
|
Connection string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
TraceRequests bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
slogGorm "github.com/orandin/slog-gorm"
|
slogGorm "github.com/orandin/slog-gorm"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"payouts/internal/service/database/config"
|
||||||
|
"payouts/internal/service/database/orm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dbService struct {
|
type dbService struct {
|
||||||
@@ -14,31 +19,67 @@ type dbService struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDatabaseService(dbType, connection, logLevel string) (DatabaseService, error) {
|
func NewDatabaseService(conf config.Database) (Service, error) {
|
||||||
|
|
||||||
var dialector gorm.Dialector
|
var dialector gorm.Dialector
|
||||||
switch dbType {
|
switch conf.Type {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
dialector = sqlite.Open(connection)
|
dialector = sqlite.Open(conf.Connection)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
dialector = postgres.Open(connection)
|
dialector = postgres.Open(conf.Connection)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown dbType")
|
return nil, errors.New("unknown dbType")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
level := slog.LevelInfo
|
||||||
|
level.UnmarshalText([]byte(conf.LogLevel))
|
||||||
|
|
||||||
|
slogGormOpts := []slogGorm.Option{
|
||||||
|
slogGorm.SetLogLevel(slogGorm.DefaultLogType, level),
|
||||||
|
}
|
||||||
|
if conf.TraceRequests {
|
||||||
|
slogGormOpts = append(slogGormOpts, slogGorm.WithTraceAll())
|
||||||
|
}
|
||||||
|
|
||||||
|
slogGorm := slogGorm.New(slogGormOpts...)
|
||||||
|
|
||||||
db, err := gorm.Open(dialector, &gorm.Config{
|
db, err := gorm.Open(dialector, &gorm.Config{
|
||||||
Logger: slogGorm.New(),
|
Logger: slogGorm,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
db.DB()
|
db.DB()
|
||||||
db.AutoMigrate()
|
db.AutoMigrate(&orm.User{})
|
||||||
// db.LogMode(true)
|
|
||||||
}
|
}
|
||||||
result := &dbService{}
|
result := &dbService{}
|
||||||
result.dbType = dbType
|
result.dbType = conf.Type
|
||||||
result.db = db
|
result.db = db
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getParams(options ...Optional) *params {
|
||||||
|
params := ¶ms{
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(params)
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUser implements [Service].
|
||||||
|
func (d *dbService) CreateUser(userModel orm.User, opts ...Optional) error {
|
||||||
|
p := getParams(opts...)
|
||||||
|
|
||||||
|
return gorm.G[orm.User](d.db).Create(p.ctx, &userModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser implements [Service].
|
||||||
|
func (d *dbService) GetUser(userModel orm.User, opts ...Optional) (orm.User, error) {
|
||||||
|
p := getParams(opts...)
|
||||||
|
|
||||||
|
userResp, err := gorm.G[orm.User](d.db).Where(&userModel).First(p.ctx)
|
||||||
|
return userResp, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"payouts/internal/config"
|
"context"
|
||||||
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"payouts/internal/config"
|
||||||
|
"payouts/internal/service/database/orm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Module = fx.Options(
|
var Module = fx.Options(
|
||||||
fx.Provide(New),
|
fx.Provide(New),
|
||||||
)
|
)
|
||||||
|
|
||||||
type DatabaseService interface {
|
type params struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
type Optional func(*params)
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context) Optional {
|
||||||
|
return func(p *params) {
|
||||||
|
p.ctx = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
CreateUser(user orm.User, opts ...Optional) error
|
||||||
|
GetUser(user orm.User, opts ...Optional) (orm.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params represents the module input params
|
// Params represents the module input params
|
||||||
@@ -21,6 +37,6 @@ type Params struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPersistence instantiates the persistence module
|
// NewPersistence instantiates the persistence module
|
||||||
func New(p Params) (DatabaseService, error) {
|
func New(p Params) (Service, error) {
|
||||||
return NewDatabaseService(p.AppConfig.Database.Type, p.AppConfig.Database.Connection, p.AppConfig.Database.LogLevel)
|
return NewDatabaseService(p.AppConfig.Database)
|
||||||
}
|
}
|
||||||
|
|||||||
10
internal/service/database/orm/user.go
Normal file
10
internal/service/database/orm/user.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package orm
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
gorm.Model
|
||||||
|
TIN string
|
||||||
|
Phone string `gorm:"uniqueIndex:idx_phone"`
|
||||||
|
PasswdHash string
|
||||||
|
}
|
||||||
10
internal/service/yookassa/config/yookassa.go
Normal file
10
internal/service/yookassa/config/yookassa.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type YooKassa struct {
|
||||||
|
BaseUrl string
|
||||||
|
|
||||||
|
ApiBaseKey string
|
||||||
|
ApiBaseSecret string
|
||||||
|
ApiPaymentKey string
|
||||||
|
ApiPaymentSecret string
|
||||||
|
}
|
||||||
37
internal/service/yookassa/module.go
Normal file
37
internal/service/yookassa/module.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package yookassa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"payouts/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Options(
|
||||||
|
fx.Provide(New),
|
||||||
|
)
|
||||||
|
|
||||||
|
type params struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
type Optional func(*params)
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context) Optional {
|
||||||
|
return func(p *params) {
|
||||||
|
p.ctx = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Param struct {
|
||||||
|
fx.In
|
||||||
|
|
||||||
|
AppConfig *config.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(p Param) (Service, error) {
|
||||||
|
return NewYookassaService(p.AppConfig.YooKassa)
|
||||||
|
}
|
||||||
13
internal/service/yookassa/yookassa_service.go
Normal file
13
internal/service/yookassa/yookassa_service.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package yookassa
|
||||||
|
|
||||||
|
import "payouts/internal/service/yookassa/config"
|
||||||
|
|
||||||
|
type yookassaService struct {
|
||||||
|
conf config.YooKassa
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYookassaService(conf config.YooKassa) (Service, error) {
|
||||||
|
return &yookassaService{
|
||||||
|
conf: conf,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user