mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
hot fix #1
This commit is contained in:
@@ -0,0 +1,522 @@
|
||||
package eshop
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RevolutAccountType represents the type of Revolut account
|
||||
type RevolutAccountType string
|
||||
|
||||
const (
|
||||
RevolutAccountTypePro RevolutAccountType = "revolut_pro"
|
||||
RevolutAccountTypeBusiness RevolutAccountType = "business"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// RevolutOAuthService handles OAuth2 authentication for both Revolut Pro and Business accounts
|
||||
type RevolutOAuthService struct {
|
||||
cfg *config.Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// 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: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
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",
|
||||
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",
|
||||
AuthBaseURL: "https://business.revolut.com",
|
||||
TokenURL: "https://b2b.revolut.com/api/1.0/auth/token",
|
||||
APIBaseURL: "https://merchant.revolut.com/api/1.0",
|
||||
}
|
||||
}
|
||||
default:
|
||||
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)
|
||||
|
||||
if accountType == RevolutAccountTypePro {
|
||||
params := map[string]string{
|
||||
"client_id": config.ClientID,
|
||||
"redirect_uri": s.cfg.RevolutWebhookURL,
|
||||
"response_type": "code",
|
||||
"scope": "checkout_extension",
|
||||
"code_challenge_method": "S256",
|
||||
"code_challenge": codeChallenge,
|
||||
"response_mode": "query",
|
||||
"state": state,
|
||||
"integration_type": "CUSTOM_PLUGIN",
|
||||
"rwa_auth_type": "auth",
|
||||
}
|
||||
|
||||
query := buildQueryString(params)
|
||||
return fmt.Sprintf("%s/s/select-user-type?%s", config.AuthBaseURL, query), nil
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"client_id": config.ClientID,
|
||||
"redirect_uri": s.cfg.RevolutWebhookURL,
|
||||
"response_type": "code",
|
||||
"code_challenge_method": "S256",
|
||||
"code_challenge": codeChallenge,
|
||||
"response_mode": "query",
|
||||
"prompt": "select_account",
|
||||
"state": state,
|
||||
}
|
||||
|
||||
query := buildQueryString(params)
|
||||
return fmt.Sprintf("%s/signin?%s", config.AuthBaseURL, query), nil
|
||||
}
|
||||
|
||||
// buildQueryString builds a query string from a map
|
||||
func buildQueryString(params map[string]string) string {
|
||||
var parts []string
|
||||
for k, v := range params {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
// 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 := map[string]string{
|
||||
"client_id": config.ClientID,
|
||||
"code": code,
|
||||
"code_verifier": codeVerifier,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": s.cfg.RevolutWebhookURL,
|
||||
}
|
||||
|
||||
body := buildFormData(data)
|
||||
|
||||
req, err := http.NewRequest("POST", config.TokenURL, strings.NewReader(body))
|
||||
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()
|
||||
|
||||
bodyBytes, 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(bodyBytes))
|
||||
}
|
||||
|
||||
var token RevolutOAuthToken
|
||||
if err := json.Unmarshal(bodyBytes, &token); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token response: %w", err)
|
||||
}
|
||||
|
||||
token.Scope = fmt.Sprintf("%s account_type:%s", token.Scope, accountType)
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// buildFormData builds form data from a map
|
||||
func buildFormData(data map[string]string) string {
|
||||
var parts []string
|
||||
for k, v := range data {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(parts, "&")
|
||||
}
|
||||
|
||||
// StoreOAuthToken stores the OAuth token with account type information
|
||||
func (s *RevolutOAuthService) StoreOAuthToken(accountType RevolutAccountType, token *RevolutOAuthToken) error {
|
||||
expiresAt := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
|
||||
logger.Info("Storing OAuth token for %s: expires at %v", accountType, expiresAt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStoredOAuthToken retrieves stored OAuth token and account type
|
||||
func (s *RevolutOAuthService) GetStoredOAuthToken() (*RevolutOAuthToken, RevolutAccountType, error) {
|
||||
return nil, "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// RefreshAccessToken refreshes the access token using refresh token
|
||||
func (s *RevolutOAuthService) RefreshAccessToken(accountType RevolutAccountType, refreshToken string) (*RevolutOAuthToken, error) {
|
||||
config := s.GetOAuthConfig(accountType)
|
||||
|
||||
data := map[string]string{
|
||||
"client_id": config.ClientID,
|
||||
"refresh_token": refreshToken,
|
||||
"grant_type": "refresh_token",
|
||||
}
|
||||
|
||||
body := buildFormData(data)
|
||||
|
||||
req, err := http.NewRequest("POST", config.TokenURL, strings.NewReader(body))
|
||||
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()
|
||||
|
||||
bodyBytes, 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(bodyBytes))
|
||||
}
|
||||
|
||||
var token RevolutOAuthToken
|
||||
if err := json.Unmarshal(bodyBytes, &token); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse refresh response: %w", err)
|
||||
}
|
||||
|
||||
token.Scope = fmt.Sprintf("%s account_type:%s", token.Scope, accountType)
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// RevolutOAuthController handles OAuth authentication for Revolut Pro
|
||||
type RevolutOAuthController struct {
|
||||
DB *gorm.DB
|
||||
Config *config.Config
|
||||
OAuthService *RevolutOAuthService
|
||||
}
|
||||
|
||||
// NewRevolutOAuthController creates a new OAuth controller
|
||||
func NewRevolutOAuthController(db *gorm.DB, cfg *config.Config) *RevolutOAuthController {
|
||||
return &RevolutOAuthController{
|
||||
DB: db,
|
||||
Config: cfg,
|
||||
OAuthService: NewRevolutOAuthService(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// OAuthStart initiates the OAuth flow for both Revolut Pro and Business
|
||||
func (ctrl *RevolutOAuthController) OAuthStart(c *gin.Context) {
|
||||
// Get account type from request (pro or business)
|
||||
var req struct {
|
||||
AccountType string `json:"account_type" binding:"required,oneof=revolut_pro business"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("Invalid OAuth start request: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid account type. Use 'revolut_pro' or 'business'"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to RevolutAccountType
|
||||
var accountType RevolutAccountType
|
||||
if req.AccountType == "revolut_pro" {
|
||||
accountType = RevolutAccountTypePro
|
||||
} else {
|
||||
accountType = RevolutAccountTypeBusiness
|
||||
}
|
||||
|
||||
// Generate PKCE verifier and challenge
|
||||
verifier, err := GenerateCodeVerifier()
|
||||
if err != nil {
|
||||
logger.Error("Failed to generate code verifier: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initiate OAuth"})
|
||||
return
|
||||
}
|
||||
|
||||
challenge := GenerateCodeChallenge(verifier)
|
||||
|
||||
// Generate state token for security
|
||||
state := fmt.Sprintf("revolut_oauth_%s_%d", accountType, time.Now().UnixNano())
|
||||
|
||||
// Store verifier and state in session/temporary storage
|
||||
// For now, we'll use a simple approach - in production, use Redis or database
|
||||
// TODO: Store code_verifier, state, and account_type securely
|
||||
logger.Info("OAuth session created: state=%s, account_type=%s", state, accountType)
|
||||
|
||||
// Generate authorization URL
|
||||
authURL, err := ctrl.OAuthService.GenerateAuthURL(accountType, state, challenge)
|
||||
if err != nil {
|
||||
logger.Error("Failed to generate auth URL: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate authorization URL"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return the authorization URL for frontend to redirect
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"authorization_url": authURL,
|
||||
"state": state,
|
||||
"account_type": req.AccountType,
|
||||
})
|
||||
}
|
||||
|
||||
// OAuthCallback handles the OAuth callback from Revolut
|
||||
func (ctrl *RevolutOAuthController) OAuthCallback(c *gin.Context) {
|
||||
// Get query parameters
|
||||
code := c.Query("code")
|
||||
state := c.Query("state")
|
||||
errorParam := c.Query("error")
|
||||
|
||||
if errorParam != "" {
|
||||
logger.Error("OAuth error: %s", errorParam)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "OAuth authorization failed"})
|
||||
return
|
||||
}
|
||||
|
||||
if code == "" || state == "" {
|
||||
logger.Error("Missing OAuth callback parameters")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required parameters"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Retrieve stored session data and verify state + account type
|
||||
// For now, we'll extract account type from state - in production, verify from secure storage
|
||||
var accountType RevolutAccountType = RevolutAccountTypePro // Default
|
||||
|
||||
// Extract account type from state if available
|
||||
if strings.Contains(state, "business") {
|
||||
accountType = RevolutAccountTypeBusiness
|
||||
}
|
||||
|
||||
// TODO: Retrieve code_verifier from session
|
||||
codeVerifier := "stored_code_verifier" // This should come from secure storage
|
||||
|
||||
// Exchange code for access token
|
||||
token, err := ctrl.OAuthService.ExchangeCodeForToken(accountType, code, codeVerifier)
|
||||
if err != nil {
|
||||
logger.Error("Failed to exchange code for token: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to obtain access token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Store the OAuth token securely
|
||||
if err := ctrl.OAuthService.StoreOAuthToken(accountType, token); err != nil {
|
||||
logger.Error("Failed to store OAuth token: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store access token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Mark Revolut as configured
|
||||
if err := ctrl.updateRevolutConfig(true); err != nil {
|
||||
logger.Error("Failed to update Revolut config: %v", err)
|
||||
// Continue anyway - token is stored
|
||||
}
|
||||
|
||||
logger.Info("Revolut OAuth authentication successful for %s", accountType)
|
||||
|
||||
// Redirect to success page or return success response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Revolut %s account connected successfully", accountType),
|
||||
"account_type": string(accountType),
|
||||
"token_info": map[string]interface{}{
|
||||
"token_type": token.TokenType,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"scope": token.Scope,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// OAuthStatus returns the current OAuth authentication status
|
||||
func (ctrl *RevolutOAuthController) OAuthStatus(c *gin.Context) {
|
||||
// Check if we have a stored OAuth token
|
||||
token, accountType, err := ctrl.OAuthService.GetStoredOAuthToken()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"authenticated": false,
|
||||
"message": "No Revolut account connected",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if token is still valid
|
||||
if token.ExpiresIn > 0 {
|
||||
// TODO: Check actual expiration time from stored data
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"authenticated": true,
|
||||
"account_type": string(accountType),
|
||||
"token_type": token.TokenType,
|
||||
"scope": token.Scope,
|
||||
"expires_in": token.ExpiresIn,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"authenticated": false,
|
||||
"message": "Token expired, please re-authenticate",
|
||||
})
|
||||
}
|
||||
|
||||
// OAuthDisconnect removes the stored OAuth token
|
||||
func (ctrl *RevolutOAuthController) OAuthDisconnect(c *gin.Context) {
|
||||
// TODO: Remove stored OAuth token from database
|
||||
logger.Info("Revolut OAuth disconnected by user")
|
||||
|
||||
// Mark Revolut as disabled
|
||||
if err := ctrl.updateRevolutConfig(false); err != nil {
|
||||
logger.Error("Failed to update Revolut config: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Revolut account disconnected",
|
||||
})
|
||||
}
|
||||
|
||||
// updateRevolutConfig updates the Revolut configuration in the database
|
||||
func (ctrl *RevolutOAuthController) updateRevolutConfig(enabled bool) error {
|
||||
// Update the main settings table
|
||||
var settings models.Settings
|
||||
if err := ctrl.DB.First(&settings).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Create settings if not found
|
||||
settings = models.Settings{
|
||||
RevolutEnabled: enabled,
|
||||
}
|
||||
return ctrl.DB.Create(&settings).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Update existing settings
|
||||
settings.RevolutEnabled = enabled
|
||||
return ctrl.DB.Save(&settings).Error
|
||||
}
|
||||
|
||||
// RefreshToken refreshes the OAuth access token
|
||||
func (ctrl *RevolutOAuthController) RefreshToken(c *gin.Context) {
|
||||
// Get current token and account type
|
||||
token, accountType, err := ctrl.OAuthService.GetStoredOAuthToken()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No refresh token available"})
|
||||
return
|
||||
}
|
||||
|
||||
if token.RefreshToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No refresh token available"})
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh the token
|
||||
newToken, err := ctrl.OAuthService.RefreshAccessToken(accountType, token.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Store the new token
|
||||
if err := ctrl.OAuthService.StoreOAuthToken(accountType, newToken); err != nil {
|
||||
logger.Error("Failed to store refreshed token: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store refreshed token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Token refreshed successfully",
|
||||
"token_info": map[string]interface{}{
|
||||
"token_type": newToken.TokenType,
|
||||
"expires_in": newToken.ExpiresIn,
|
||||
"scope": newToken.Scope,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user