mirror of
https://github.com/tavo-wasd-gh/conex-builder.git
synced 2025-07-09 01:03:29 -06:00
481 lines
12 KiB
Go
481 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
var (
|
|
baseURL string
|
|
clientID string
|
|
clientSecret string
|
|
planID string
|
|
returnUrl string
|
|
cancelUrl string
|
|
|
|
exists bool
|
|
err error
|
|
|
|
dbHost string
|
|
dbPort string
|
|
dbUser string
|
|
dbPass string
|
|
dbName string
|
|
db *sql.DB
|
|
query string
|
|
)
|
|
|
|
func init() {
|
|
// Load .env
|
|
if err := godotenv.Load(); err != nil {
|
|
log.Fatalf("Error loading .env file: %v", err)
|
|
}
|
|
|
|
// Set variables
|
|
baseURL = os.Getenv("BASE_URL")
|
|
clientID = os.Getenv("CLIENT_ID")
|
|
clientSecret = os.Getenv("CLIENT_SECRET")
|
|
planID = os.Getenv("PLAN_ID")
|
|
returnUrl = os.Getenv("RETURN_URL")
|
|
cancelUrl = os.Getenv("CANCEL_URL")
|
|
// DB creds
|
|
dbHost = os.Getenv("DB_HOST")
|
|
dbPort = os.Getenv("DB_PORT")
|
|
dbUser = os.Getenv("DB_USER")
|
|
dbPass = os.Getenv("DB_PASS")
|
|
dbName = os.Getenv("DB_NAME")
|
|
|
|
// Error if empty
|
|
if baseURL == "" || clientID == "" || clientSecret == "" || planID == "" || returnUrl == "" || cancelUrl == "" ||
|
|
dbHost == "" || dbPort == "" || dbUser == "" || dbPass == "" || dbName == "" {
|
|
log.Fatalf("Error setting credentials")
|
|
}
|
|
|
|
// Connect to DB
|
|
var err error
|
|
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
dbHost, dbPort, dbUser, dbPass, dbName)
|
|
db, err = sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
|
|
// Ping DB
|
|
if err = db.Ping(); err != nil {
|
|
log.Fatalf("Failed to ping database: %v", err)
|
|
}
|
|
}
|
|
|
|
type CreateOrderResponse struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
type OrderResponse struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
PurchaseUnits []struct {
|
|
Payments struct {
|
|
Captures []struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
CreateTime time.Time `json:"create_time"`
|
|
} `json:"captures"`
|
|
} `json:"payments"`
|
|
} `json:"purchase_units"`
|
|
Payer struct {
|
|
Name struct {
|
|
GivenName string `json:"given_name"`
|
|
Surname string `json:"surname"`
|
|
} `json:"name"`
|
|
EmailAddress string `json:"email_address"`
|
|
Phone struct {
|
|
PhoneType string `json:"phone_type"`
|
|
PhoneNumber struct {
|
|
NationalNumber string `json:"national_number"`
|
|
} `json:"phone_number"`
|
|
} `json:"phone"`
|
|
Address struct {
|
|
CountryCode string `json:"country_code"`
|
|
} `json:"address"`
|
|
} `json:"payer"`
|
|
}
|
|
|
|
type SubscriptionResponse struct {
|
|
Status string `json:"status"`
|
|
StatusUpdateTime time.Time `json:"status_update_time"`
|
|
StartTime time.Time `json:"start_time"`
|
|
Subscriber struct {
|
|
Name struct {
|
|
GivenName string `json:"given_name"`
|
|
Surname string `json:"surname"`
|
|
} `json:"name"`
|
|
EmailAddress string `json:"email_address"`
|
|
} `json:"subscriber"`
|
|
CreateTime time.Time `json:"create_time"`
|
|
}
|
|
|
|
type Cart struct {
|
|
Directory string `json:"directory"`
|
|
}
|
|
|
|
func main() {
|
|
http.HandleFunc("/api/order", CreateOrder)
|
|
http.HandleFunc("/api/order/", CaptureOrder)
|
|
http.HandleFunc("/api/paypal/subscribe", CreateSubscription)
|
|
http.HandleFunc("/api/paypal/subscribe/", CaptureSubscription)
|
|
http.Handle("/", http.FileServer(http.Dir("./public")))
|
|
|
|
// Channel to listen for signals
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
// Run the server in a goroutine so that it doesn't block
|
|
go func() {
|
|
log.Println("Starting server on :8080...")
|
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
|
log.Fatalf("Could not listen on :8080: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
<-stop // Shutdown signal recieved
|
|
log.Println("Server shutdown gracefully.")
|
|
}
|
|
|
|
func Token() (string, error) {
|
|
// Create
|
|
req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`))
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error creating request: %v", err)
|
|
}
|
|
|
|
// Send
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error sending request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Decode
|
|
var result struct {
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return "", fmt.Errorf("Error decoding response: %v", err)
|
|
}
|
|
|
|
// Return
|
|
return result.AccessToken, nil
|
|
}
|
|
|
|
func RegisterOrder(order OrderResponse, directory string) {
|
|
var (
|
|
capture string
|
|
status string
|
|
name string
|
|
surname string
|
|
email string
|
|
phone string
|
|
country string
|
|
date time.Time
|
|
)
|
|
|
|
for _, Unit := range order.PurchaseUnits {
|
|
for _, Capture := range Unit.Payments.Captures {
|
|
capture = Capture.ID
|
|
status = Capture.Status
|
|
date = Capture.CreateTime
|
|
}
|
|
}
|
|
|
|
name = order.Payer.Name.GivenName
|
|
surname = order.Payer.Name.Surname
|
|
email = order.Payer.EmailAddress
|
|
phone = order.Payer.Phone.PhoneNumber.NationalNumber
|
|
country = order.Payer.Address.CountryCode
|
|
|
|
// Register Payment
|
|
_, err = db.Exec(`INSERT INTO payments (id, client, directory, status, step, date) VALUES ($1, $2, $3, $4, $5, $6);`,
|
|
capture, email, directory, status, "REGISTERED", date)
|
|
if err != nil {
|
|
fmt.Printf("$v", err) // TODO consider logging in server
|
|
}
|
|
|
|
// Register Client
|
|
err = db.QueryRow(`SELECT EXISTS(SELECT 1 FROM clients WHERE email = $1);`, email).Scan(&exists)
|
|
if err != nil {
|
|
fmt.Printf("$v", err) // TODO consider logging in server
|
|
}
|
|
if !exists {
|
|
_, err = db.Exec(`INSERT INTO clients (email, name, surname, phone, country) VALUES ($1, $2, $3, $4, $5);`,
|
|
email, name, surname, phone, country)
|
|
if err != nil {
|
|
fmt.Printf("$v", err) // TODO consider logging in server
|
|
}
|
|
}
|
|
|
|
// Register Site
|
|
_, err = db.Exec(`INSERT INTO sites (directory, client, status, ends) VALUES ($1, $2, $3, $4);`,
|
|
directory, email, "ACTIVE", date.AddDate(1, 0, 0)) // Ends a year later
|
|
if err != nil {
|
|
fmt.Printf("$v", err) // TODO consider logging in server
|
|
}
|
|
}
|
|
|
|
func CreateOrder(w http.ResponseWriter, r *http.Request) {
|
|
token, err := Token()
|
|
if err != nil {
|
|
http.Error(w, "Failed to get access token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
data := `{
|
|
"intent": "CAPTURE",
|
|
"purchase_units": [{
|
|
"amount": {
|
|
"currency_code": "USD",
|
|
"value": "20.00"
|
|
}
|
|
}],
|
|
"payment_source": {
|
|
"paypal": {
|
|
"address" : {
|
|
"country_code": "CR"
|
|
}
|
|
}
|
|
},
|
|
"application_context": {
|
|
"shipping_preference": "NO_SHIPPING"
|
|
}
|
|
}`
|
|
|
|
// Create
|
|
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data)))
|
|
if err != nil {
|
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Send
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Decode
|
|
var result CreateOrderResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"id": result.ID})
|
|
return
|
|
}
|
|
|
|
func CaptureOrder(w http.ResponseWriter, r *http.Request) {
|
|
// Read body from order
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Parse to get directory
|
|
var cart Cart
|
|
err = json.Unmarshal(body, &cart)
|
|
if err != nil {
|
|
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
directory := cart.Directory
|
|
|
|
// Get orderID
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/orders/")
|
|
parts := strings.Split(path, "/")
|
|
orderID := parts[0]
|
|
if orderID == "" {
|
|
http.Error(w, "Failed to get orderID from client URL", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
token, err := Token()
|
|
if err != nil {
|
|
http.Error(w, "Failed to get access token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create
|
|
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil)
|
|
if err != nil {
|
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Check if directory already exists
|
|
err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists)
|
|
if err != nil {
|
|
http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if exists {
|
|
http.Error(w, "This directory ID is already taken", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Send, PAYMENT MADE HERE
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Decode
|
|
var order OrderResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&order); err != nil {
|
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
RegisterOrder(order, directory)
|
|
|
|
// Respond
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(order); err != nil {
|
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
|
// Read body from order
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Parse to get directory
|
|
var cart Cart
|
|
err = json.Unmarshal(body, &cart)
|
|
if err != nil {
|
|
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
directory := cart.Directory
|
|
|
|
token, err := Token()
|
|
if err != nil {
|
|
http.Error(w, "Failed to get access token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"plan_id": planID,
|
|
"application_context": map[string]string{
|
|
"shipping_preference": "NO_SHIPPING",
|
|
"return_url": returnUrl,
|
|
"cancel_url": cancelUrl,
|
|
},
|
|
}
|
|
jsonData, err := json.Marshal(payload)
|
|
if err != nil {
|
|
http.Error(w, "Server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create request
|
|
log.Printf("Creating request")
|
|
req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Check if directory already exists
|
|
err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists)
|
|
if err != nil {
|
|
http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if exists {
|
|
http.Error(w, "This directory ID is already taken", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Send
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Prefer", "return=representation")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Decode
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Respond
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
|
}
|
|
log.Printf("sent response to client")
|
|
}
|
|
|
|
// Capture just like CaptureOrder, but with response from paypal
|
|
// https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
|
|
func CaptureSubscription(w http.ResponseWriter, r *http.Request) {
|
|
// Read body from order
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Parse to get directory
|
|
var cart Cart
|
|
err = json.Unmarshal(body, &cart)
|
|
if err != nil {
|
|
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
// directory := cart.Directory
|
|
|
|
// Get subID
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/subscribe/")
|
|
parts := strings.Split(path, "/")
|
|
subID := parts[0]
|
|
if subID == "" {
|
|
http.Error(w, "Failed to get subID from client URL", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|