chore: update license validation

This commit is contained in:
Steven
2025-12-17 21:03:49 +08:00
parent d5e7c50682
commit 8083411f3f
3 changed files with 227 additions and 9 deletions
@@ -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>
+204
View File
@@ -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
}
+21 -7
View File
@@ -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
}
// 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
}
if validateResponse.Valid {
// 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")
}