package payout import ( "encoding/json" "errors" "fmt" "log/slog" "net" "net/http" "regexp" "go.uber.org/fx" "payouts/internal/config" "payouts/internal/models" "payouts/internal/service/cache" "payouts/internal/service/database" "payouts/internal/service/database/orm" "payouts/internal/service/yookassa" yookassaConf "payouts/internal/service/yookassa/config" ) const ( BaseRoute = "/payout" CreateRoute = "/create" CallbackRoute = "/callback" BanksRoute = "/sbp/banks" ) var authHeaderRe = regexp.MustCompile(`^Bearer\s+(.*)$`) type payoutHandler struct { dbService database.Service cacheService cache.Service yooKassa yookassa.Service yookassaConf yookassaConf.YooKassa } // Params represents the module input params type Params struct { fx.In AppConfig *config.App DbService database.Service YooKassa yookassa.Service CacheService cache.Service } func NewPayoutHandler(p Params) (Handler, error) { return &payoutHandler{ dbService: p.DbService, cacheService: p.CacheService, yooKassa: p.YooKassa, yookassaConf: p.YooKassa.GetConfig(), }, nil } func (p *payoutHandler) getSession(r *http.Request) (*orm.User, error) { authHeaderValue := r.Header.Get("Authorization") if len(authHeaderValue) == 0 { return nil, errors.New("no valid auth header") } matches := authHeaderRe.FindStringSubmatch(authHeaderValue) if matches == nil { return nil, errors.New("no valid auth header") } sessionId := matches[1] userSession, err := p.cacheService.GetSession(sessionId) if err != nil { return nil, errors.New("session not found") } return &userSession, nil } func (p *payoutHandler) checkAllowedIpCallback(ipStr string) bool { ipWithoutPort, _, _ := net.SplitHostPort(ipStr) ip := net.ParseIP(ipWithoutPort) if ip == nil { slog.Error(fmt.Sprintf("Invalid IP: %s", ipStr)) return false } for _, subnetStr := range p.yookassaConf.AllowedCallbackSubnets { _, ipNet, err := net.ParseCIDR(subnetStr) if err != nil { slog.Error(fmt.Sprintf("Invalid subnet CIDR: %v", err)) continue } if ipNet.Contains(ip) { return true } } return false } // GetSbpBanks implements [Handler]. func (p *payoutHandler) GetSbpBanks(w http.ResponseWriter, r *http.Request) { panic("unimplemented") } // PaymentCreate implements [Handler]. func (p *payoutHandler) PayoutCreate(w http.ResponseWriter, r *http.Request) { errResponse := func(message string, err error, status int) { http.Error(w, errors.Join(errors.New(message), err).Error(), status) } userSession, err := p.getSession(r) if err != nil { errResponse("unauthorized", err, http.StatusUnauthorized) return } payoutReq := models.PayoutReq{ PayoutType: models.TypeSBP, } decoder := json.NewDecoder(r.Body) err = decoder.Decode(&payoutReq) if err != nil { slog.Error("Failed to decode request body", slog.String("error", err.Error())) errResponse("failed to decode request body", err, http.StatusBadRequest) return } slog.Debug(fmt.Sprintf("Received create payload request: %v from user %v", payoutReq, userSession)) err = p.yooKassa.CreatePayout(payoutReq, userSession) if err != nil { slog.Error("Failed to create payout request", slog.String("error", err.Error())) errResponse("failed to create payout request", err, http.StatusBadRequest) return } } // PaymentCallback implements [Handler]. func (p *payoutHandler) PayoutCallback(w http.ResponseWriter, r *http.Request) { inData := map[string]any{} decoder := json.NewDecoder(r.Body) decoder.Decode(&inData) // todo: check also the X-real-ip and/or X-Forwarded-For if !p.checkAllowedIpCallback(r.RemoteAddr) { slog.Error(fmt.Sprintf("Callback came from unallowed ip: %s", r.RemoteAddr)) http.Error(w, "unallowed", http.StatusForbidden) return } slog.Info(fmt.Sprintf("Received callback from %s with object %v with headers %v", r.RemoteAddr, inData, r.Header)) }