mirror of
https://github.com/aykhans/slash-e.git
synced 2026-09-23 14:26:49 +00:00
chore: update license validation
This commit is contained in:
@@ -76,7 +76,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
<div className="w-full flex justify-between items-center mt-4">
|
||||
<div>
|
||||
{subscription.plan === PlanType.FREE && (
|
||||
<Link href="https://yourselfhosted.lemonsqueezy.com/checkout/buy/947e9a56-c93a-4294-8d71-2ea4b0f3ec51" target="_blank">
|
||||
<Link href="https://yourselfhosted.gumroad.com/l/slash-pro" target="_blank">
|
||||
Buy a license key
|
||||
<Icon.ExternalLink className="w-4 h-auto ml-1" />
|
||||
</Link>
|
||||
@@ -179,7 +179,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
<Link
|
||||
className="w-full"
|
||||
underline="none"
|
||||
href="https://yourselfhosted.lemonsqueezy.com/checkout/buy/947e9a56-c93a-4294-8d71-2ea4b0f3ec51"
|
||||
href="https://yourselfhosted.gumroad.com/l/slash-pro"
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="w-full bg-gradient-to-r from-pink-500 to-purple-500 shadow hover:opacity-80">Get Pro License</Button>
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package gumroad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// The base API URL for the Gumroad API.
|
||||
baseAPIURL = "https://api.gumroad.com"
|
||||
// The product ID from Gumroad (base64 encoded).
|
||||
productID = "mjSmJtQJcYNtu4vdxeQWNg=="
|
||||
)
|
||||
|
||||
type Purchase struct {
|
||||
SellerID string `json:"seller_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Permalink string `json:"permalink"`
|
||||
ProductPermalink string `json:"product_permalink"`
|
||||
Email string `json:"email"`
|
||||
Price int `json:"price"`
|
||||
GumroadFee int `json:"gumroad_fee"`
|
||||
Currency string `json:"currency"`
|
||||
Quantity int `json:"quantity"`
|
||||
SaleID string `json:"sale_id"`
|
||||
SaleTimestamp string `json:"sale_timestamp"`
|
||||
PurchaserID string `json:"purchaser_id,omitempty"`
|
||||
SubscriptionID string `json:"subscription_id,omitempty"`
|
||||
LicenseKey string `json:"license_key"`
|
||||
IsMultiseatLicense bool `json:"is_multiseat_license"`
|
||||
IPCountry string `json:"ip_country"`
|
||||
Recurrence string `json:"recurrence,omitempty"`
|
||||
Refunded bool `json:"refunded"`
|
||||
Disputed bool `json:"disputed"`
|
||||
DisputeWon bool `json:"dispute_won"`
|
||||
Chargebacked bool `json:"chargebacked"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
SubscriptionEndedAt *string `json:"subscription_ended_at"`
|
||||
SubscriptionCancelledAt *string `json:"subscription_cancelled_at"`
|
||||
SubscriptionFailedAt *string `json:"subscription_failed_at"`
|
||||
}
|
||||
|
||||
type ValidateLicenseKeyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Uses int `json:"uses"`
|
||||
Purchase *Purchase `json:"purchase"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
|
||||
type ActiveLicenseKeyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Uses int `json:"uses"`
|
||||
Purchase *Purchase `json:"purchase"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
|
||||
func ValidateLicenseKey(licenseKey string, instanceName string) (*ValidateLicenseKeyResponse, error) {
|
||||
data := map[string]string{
|
||||
"product_id": productID,
|
||||
"license_key": licenseKey,
|
||||
}
|
||||
if instanceName != "" {
|
||||
data["increment_uses_count"] = "false"
|
||||
}
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal data")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v2/licenses/verify", baseAPIURL), bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create request")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to do request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response ValidateLicenseKeyResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if response.Message != nil && *response.Message != "" {
|
||||
return nil, errors.New(*response.Message)
|
||||
}
|
||||
|
||||
// Validate the response
|
||||
if response.Success && response.Purchase != nil {
|
||||
purchase := response.Purchase
|
||||
|
||||
// Check if purchase was refunded/chargebacked
|
||||
if purchase.Refunded {
|
||||
return nil, errors.New("license key has been refunded")
|
||||
}
|
||||
if purchase.Chargebacked {
|
||||
return nil, errors.New("license key has been chargebacked")
|
||||
}
|
||||
if purchase.Disputed && !purchase.DisputeWon {
|
||||
return nil, errors.New("license key is disputed")
|
||||
}
|
||||
|
||||
// Check subscription status (for membership products)
|
||||
if purchase.SubscriptionEndedAt != nil && *purchase.SubscriptionEndedAt != "" {
|
||||
return nil, errors.New("subscription has ended")
|
||||
}
|
||||
if purchase.SubscriptionCancelledAt != nil && *purchase.SubscriptionCancelledAt != "" {
|
||||
return nil, errors.New("subscription has been cancelled")
|
||||
}
|
||||
if purchase.SubscriptionFailedAt != nil && *purchase.SubscriptionFailedAt != "" {
|
||||
return nil, errors.New("subscription payment failed")
|
||||
}
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func ActiveLicenseKey(licenseKey string, instanceName string) (*ActiveLicenseKeyResponse, error) {
|
||||
data := map[string]string{
|
||||
"product_id": productID,
|
||||
"license_key": licenseKey,
|
||||
"increment_uses_count": "true",
|
||||
}
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal data")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v2/licenses/verify", baseAPIURL), bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create request")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to do request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response ActiveLicenseKeyResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if response.Message != nil && *response.Message != "" {
|
||||
return nil, errors.New(*response.Message)
|
||||
}
|
||||
|
||||
// Validate the response
|
||||
if response.Success && response.Purchase != nil {
|
||||
purchase := response.Purchase
|
||||
|
||||
// Check if purchase was refunded/chargebacked
|
||||
if purchase.Refunded {
|
||||
return nil, errors.New("license key has been refunded")
|
||||
}
|
||||
if purchase.Chargebacked {
|
||||
return nil, errors.New("license key has been chargebacked")
|
||||
}
|
||||
if purchase.Disputed && !purchase.DisputeWon {
|
||||
return nil, errors.New("license key is disputed")
|
||||
}
|
||||
|
||||
// Check subscription status (for membership products)
|
||||
if purchase.SubscriptionEndedAt != nil && *purchase.SubscriptionEndedAt != "" {
|
||||
return nil, errors.New("subscription has ended")
|
||||
}
|
||||
if purchase.SubscriptionCancelledAt != nil && *purchase.SubscriptionCancelledAt != "" {
|
||||
return nil, errors.New("subscription has been cancelled")
|
||||
}
|
||||
if purchase.SubscriptionFailedAt != nil && *purchase.SubscriptionFailedAt != "" {
|
||||
return nil, errors.New("subscription payment failed")
|
||||
}
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
v1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/profile"
|
||||
"github.com/yourselfhosted/slash/server/service/license/gumroad"
|
||||
"github.com/yourselfhosted/slash/server/service/license/lemonsqueezy"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
@@ -159,18 +160,31 @@ func validateLicenseKey(licenseKey string) (*ValidateResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Try to validate the license key with the license server.
|
||||
validateResponse, err := lemonsqueezy.ValidateLicenseKey(licenseKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to validate license key")
|
||||
// Try to validate the license key with Gumroad.
|
||||
gumroadResponse, err := gumroad.ValidateLicenseKey(licenseKey, "")
|
||||
if err == nil && gumroadResponse.Success {
|
||||
result := &ValidateResult{
|
||||
Plan: v1pb.PlanType_PRO,
|
||||
Features: getDefaultFeatures(v1pb.PlanType_PRO),
|
||||
Seats: -1, // Unlimited seats for Gumroad subscriptions
|
||||
}
|
||||
if validateResponse.Valid {
|
||||
// For subscription products, set expiration based on recurrence
|
||||
if gumroadResponse.Purchase != nil && gumroadResponse.Purchase.Recurrence != "" {
|
||||
// Subscription is active, set expiration to 1 year from now
|
||||
result.ExpiresTime = time.Now().AddDate(1, 0, 0)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Try to validate the license key with LemonSqueezy (fallback for existing customers).
|
||||
lemonsqueezyResponse, err := lemonsqueezy.ValidateLicenseKey(licenseKey, "")
|
||||
if err == nil && lemonsqueezyResponse.Valid {
|
||||
result := &ValidateResult{
|
||||
Plan: v1pb.PlanType_PRO,
|
||||
Features: getDefaultFeatures(v1pb.PlanType_PRO),
|
||||
}
|
||||
if validateResponse.LicenseKey.ExpiresAt != nil && *validateResponse.LicenseKey.ExpiresAt != "" {
|
||||
expiresTime, err := time.Parse(time.RFC3339Nano, *validateResponse.LicenseKey.ExpiresAt)
|
||||
if lemonsqueezyResponse.LicenseKey.ExpiresAt != nil && *lemonsqueezyResponse.LicenseKey.ExpiresAt != "" {
|
||||
expiresTime, err := time.Parse(time.RFC3339Nano, *lemonsqueezyResponse.LicenseKey.ExpiresAt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse license key expires time")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user