mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
350 lines
11 KiB
Go
350 lines
11 KiB
Go
package eshop
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"fotbal-club/internal/config"
|
|
"fotbal-club/internal/models"
|
|
"fotbal-club/pkg/httpclient"
|
|
)
|
|
|
|
// RevolutOAuthService handles OAuth2 authentication for both Revolut Pro and Business accounts
|
|
type RevolutOAuthService struct {
|
|
cfg *config.Config
|
|
client *http.Client
|
|
}
|
|
|
|
// RevolutOAuthToken represents the OAuth token response
|
|
type RevolutOAuthToken struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
// RevolutAccountType represents the type of Revolut account
|
|
type RevolutAccountType string
|
|
|
|
const (
|
|
RevolutAccountTypePro RevolutAccountType = "revolut_pro"
|
|
RevolutAccountTypeBusiness RevolutAccountType = "business"
|
|
)
|
|
|
|
// RevolutOAuthConfig holds OAuth configuration for different account types
|
|
type RevolutOAuthConfig struct {
|
|
AccountType RevolutAccountType `json:"account_type"`
|
|
ClientID string `json:"client_id"`
|
|
AuthBaseURL string `json:"auth_base_url"`
|
|
TokenURL string `json:"token_url"`
|
|
APIBaseURL string `json:"api_base_url"`
|
|
}
|
|
|
|
// NewRevolutOAuthService creates a new OAuth service instance
|
|
func NewRevolutOAuthService(cfg *config.Config) *RevolutOAuthService {
|
|
return &RevolutOAuthService{
|
|
cfg: cfg,
|
|
client: httpclient.DefaultClient(),
|
|
}
|
|
}
|
|
|
|
// GetOAuthConfig returns configuration for the specified account type and environment
|
|
func (s *RevolutOAuthService) GetOAuthConfig(accountType RevolutAccountType) RevolutOAuthConfig {
|
|
isSandbox := s.cfg.RevolutEnvironment == "sandbox"
|
|
|
|
switch accountType {
|
|
case RevolutAccountTypePro:
|
|
if isSandbox {
|
|
return RevolutOAuthConfig{
|
|
AccountType: RevolutAccountTypePro,
|
|
ClientID: "sandbox_pro_client_id", // Get from Revolut for sandbox
|
|
AuthBaseURL: "https://sandbox-checkout.revolut.com",
|
|
TokenURL: "https://sandbox-checkout.revolut.com/api/connect/oauth/token",
|
|
APIBaseURL: "https://sandbox-merchant.revolut.com/api/1.0",
|
|
}
|
|
} else {
|
|
return RevolutOAuthConfig{
|
|
AccountType: RevolutAccountTypePro,
|
|
ClientID: "9cda975e-016c-4b49-b5c6-37d1285ba046", // Production client ID
|
|
AuthBaseURL: "https://checkout.revolut.com",
|
|
TokenURL: "https://checkout.revolut.com/api/connect/oauth/token",
|
|
APIBaseURL: "https://merchant.revolut.com/api/1.0",
|
|
}
|
|
}
|
|
case RevolutAccountTypeBusiness:
|
|
if isSandbox {
|
|
return RevolutOAuthConfig{
|
|
AccountType: RevolutAccountTypeBusiness,
|
|
ClientID: "sandbox_business_client_id",
|
|
AuthBaseURL: "https://sandbox-business.revolut.com",
|
|
TokenURL: "https://sandbox-business.revolut.com/api/1.0/auth/token",
|
|
APIBaseURL: "https://sandbox-merchant.revolut.com/api/1.0",
|
|
}
|
|
} else {
|
|
return RevolutOAuthConfig{
|
|
AccountType: RevolutAccountTypeBusiness,
|
|
ClientID: "diiToLZlMJOPtWhdFTxQ", // Production business client ID
|
|
AuthBaseURL: "https://business.revolut.com",
|
|
TokenURL: "https://b2b.revolut.com/api/1.0/auth/token",
|
|
APIBaseURL: "https://merchant.revolut.com/api/1.0",
|
|
}
|
|
}
|
|
default:
|
|
// Default to Revolut Pro
|
|
return s.GetOAuthConfig(RevolutAccountTypePro)
|
|
}
|
|
}
|
|
|
|
// GenerateAuthURL generates the OAuth2 authorization URL for both account types
|
|
func (s *RevolutOAuthService) GenerateAuthURL(accountType RevolutAccountType, state string, codeChallenge string) (string, error) {
|
|
config := s.GetOAuthConfig(accountType)
|
|
|
|
// For Revolut Pro, use the select-user-type endpoint
|
|
if accountType == RevolutAccountTypePro {
|
|
params := url.Values{}
|
|
params.Set("client_id", config.ClientID)
|
|
params.Set("redirect_uri", s.cfg.RevolutWebhookURL)
|
|
params.Set("response_type", "code")
|
|
params.Set("scope", "checkout_extension")
|
|
params.Set("code_challenge_method", "S256")
|
|
params.Set("code_challenge", codeChallenge)
|
|
params.Set("response_mode", "query")
|
|
params.Set("state", state)
|
|
params.Set("integration_type", "CUSTOM_PLUGIN")
|
|
params.Set("rwa_auth_type", "auth")
|
|
|
|
authURL := fmt.Sprintf("%s/s/select-user-type?%s", config.AuthBaseURL, params.Encode())
|
|
return authURL, nil
|
|
}
|
|
|
|
// For Revolut Business, use direct SSO
|
|
params := url.Values{}
|
|
params.Set("client_id", config.ClientID)
|
|
params.Set("redirect_uri", s.cfg.RevolutWebhookURL)
|
|
params.Set("response_type", "code")
|
|
params.Set("code_challenge_method", "S256")
|
|
params.Set("code_challenge", codeChallenge)
|
|
params.Set("response_mode", "query")
|
|
params.Set("prompt", "select_account")
|
|
params.Set("state", state)
|
|
|
|
authURL := fmt.Sprintf("%s/signin?%s", config.AuthBaseURL, params.Encode())
|
|
return authURL, nil
|
|
}
|
|
|
|
// GenerateCodeVerifier generates a PKCE code verifier
|
|
func GenerateCodeVerifier() (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// GenerateCodeChallenge generates a PKCE code challenge from verifier
|
|
func GenerateCodeChallenge(verifier string) string {
|
|
h := sha256.New()
|
|
h.Write([]byte(verifier))
|
|
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// ExchangeCodeForToken exchanges authorization code for access token
|
|
func (s *RevolutOAuthService) ExchangeCodeForToken(accountType RevolutAccountType, code, codeVerifier string) (*RevolutOAuthToken, error) {
|
|
config := s.GetOAuthConfig(accountType)
|
|
|
|
data := url.Values{}
|
|
data.Set("client_id", config.ClientID)
|
|
data.Set("code", code)
|
|
data.Set("code_verifier", codeVerifier)
|
|
data.Set("grant_type", "authorization_code")
|
|
data.Set("redirect_uri", s.cfg.RevolutWebhookURL)
|
|
|
|
req, err := http.NewRequest("POST", config.TokenURL, strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create token request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute token request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read token response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("token API error: status %d, response: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var token RevolutOAuthToken
|
|
if err := json.Unmarshal(body, &token); err != nil {
|
|
return nil, fmt.Errorf("failed to parse token response: %w", err)
|
|
}
|
|
|
|
// Store account type with token for later use
|
|
token.Scope = fmt.Sprintf("%s account_type:%s", token.Scope, accountType)
|
|
|
|
return &token, nil
|
|
}
|
|
|
|
// RefreshAccessToken refreshes the access token using refresh token
|
|
func (s *RevolutOAuthService) RefreshAccessToken(accountType RevolutAccountType, refreshToken string) (*RevolutOAuthToken, error) {
|
|
config := s.GetOAuthConfig(accountType)
|
|
|
|
data := url.Values{}
|
|
data.Set("client_id", config.ClientID)
|
|
data.Set("refresh_token", refreshToken)
|
|
data.Set("grant_type", "refresh_token")
|
|
|
|
req, err := http.NewRequest("POST", config.TokenURL, strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create refresh request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute refresh request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read refresh response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("refresh API error: status %d, response: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var token RevolutOAuthToken
|
|
if err := json.Unmarshal(body, &token); err != nil {
|
|
return nil, fmt.Errorf("failed to parse refresh response: %w", err)
|
|
}
|
|
|
|
// Store account type with token
|
|
token.Scope = fmt.Sprintf("%s account_type:%s", token.Scope, accountType)
|
|
|
|
return &token, nil
|
|
}
|
|
|
|
// StoreOAuthToken stores the OAuth token with account type information
|
|
func (s *RevolutOAuthService) StoreOAuthToken(accountType RevolutAccountType, token *RevolutOAuthToken) error {
|
|
// Store token in database or secure storage
|
|
// You would typically store:
|
|
// - access_token
|
|
// - refresh_token
|
|
// - expires_at (calculated from expires_in)
|
|
// - token_type
|
|
// - scope (includes account_type)
|
|
// - account_type
|
|
|
|
expiresAt := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
|
|
|
|
// TODO: Implement database storage with account type
|
|
fmt.Printf("Storing OAuth token for %s: expires at %v\n", accountType, expiresAt)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStoredOAuthToken retrieves stored OAuth token and account type
|
|
func (s *RevolutOAuthService) GetStoredOAuthToken() (*RevolutOAuthToken, RevolutAccountType, error) {
|
|
// TODO: Implement database retrieval
|
|
return nil, "", fmt.Errorf("not implemented")
|
|
}
|
|
|
|
// CreatePaymentWithOAuth creates a payment using OAuth token instead of API key
|
|
func (s *RevolutOAuthService) CreatePaymentWithOAuth(order *models.EshopOrder) (*PaymentResult, error) {
|
|
// Get stored OAuth token and account type
|
|
token, accountType, err := s.GetStoredOAuthToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get OAuth token: %w", err)
|
|
}
|
|
|
|
// Check if token needs refresh
|
|
if token.ExpiresIn > 0 && token.RefreshToken != "" {
|
|
// Token might be expired, refresh it
|
|
newToken, err := s.RefreshAccessToken(accountType, token.RefreshToken)
|
|
if err == nil {
|
|
token = newToken
|
|
s.StoreOAuthToken(accountType, token)
|
|
}
|
|
}
|
|
|
|
// Create order using OAuth token
|
|
return s.createOrder(order, token.AccessToken, accountType)
|
|
}
|
|
|
|
// createOrder creates a Revolut order using OAuth access token
|
|
func (s *RevolutOAuthService) createOrder(order *models.EshopOrder, accessToken string, accountType RevolutAccountType) (*PaymentResult, error) {
|
|
config := s.GetOAuthConfig(accountType)
|
|
|
|
// Create order request
|
|
orderReq := map[string]interface{}{
|
|
"amount": order.TotalAmountCents,
|
|
"currency": order.Currency,
|
|
"description": fmt.Sprintf("Order %s", order.OrderNumber),
|
|
"merchant_order_id": fmt.Sprintf("%d", order.ID),
|
|
}
|
|
|
|
orderJSON, err := json.Marshal(orderReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal order request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", config.APIBaseURL+"/orders", bytes.NewBuffer(orderJSON))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create order request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute order request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read order response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
return nil, fmt.Errorf("order API error: status %d, response: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var orderResp map[string]interface{}
|
|
if err := json.Unmarshal(body, &orderResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse order response: %w", err)
|
|
}
|
|
|
|
orderID, ok := orderResp["id"].(string)
|
|
if !ok || orderID == "" {
|
|
return nil, fmt.Errorf("order response missing id")
|
|
}
|
|
|
|
return &PaymentResult{
|
|
RedirectURL: fmt.Sprintf("%s/checkout/%s", config.APIBaseURL, orderID),
|
|
ProviderPaymentID: orderID,
|
|
RawPayloadJSON: string(body),
|
|
}, nil
|
|
}
|