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 className="w-full flex justify-between items-center mt-4">
|
||||||
<div>
|
<div>
|
||||||
{subscription.plan === PlanType.FREE && (
|
{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
|
Buy a license key
|
||||||
<Icon.ExternalLink className="w-4 h-auto ml-1" />
|
<Icon.ExternalLink className="w-4 h-auto ml-1" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -179,7 +179,7 @@ const SubscriptionSetting: React.FC = () => {
|
|||||||
<Link
|
<Link
|
||||||
className="w-full"
|
className="w-full"
|
||||||
underline="none"
|
underline="none"
|
||||||
href="https://yourselfhosted.lemonsqueezy.com/checkout/buy/947e9a56-c93a-4294-8d71-2ea4b0f3ec51"
|
href="https://yourselfhosted.gumroad.com/l/slash-pro"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button className="w-full bg-gradient-to-r from-pink-500 to-purple-500 shadow hover:opacity-80">Get Pro License</Button>
|
<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"
|
v1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
"github.com/yourselfhosted/slash/server/profile"
|
"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/server/service/license/lemonsqueezy"
|
||||||
"github.com/yourselfhosted/slash/store"
|
"github.com/yourselfhosted/slash/store"
|
||||||
)
|
)
|
||||||
@@ -159,18 +160,31 @@ func validateLicenseKey(licenseKey string) (*ValidateResult, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to validate the license key with the license server.
|
// Try to validate the license key with Gumroad.
|
||||||
validateResponse, err := lemonsqueezy.ValidateLicenseKey(licenseKey, "")
|
gumroadResponse, err := gumroad.ValidateLicenseKey(licenseKey, "")
|
||||||
if err != nil {
|
if err == nil && gumroadResponse.Success {
|
||||||
return nil, errors.Wrap(err, "failed to validate license key")
|
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{
|
result := &ValidateResult{
|
||||||
Plan: v1pb.PlanType_PRO,
|
Plan: v1pb.PlanType_PRO,
|
||||||
Features: getDefaultFeatures(v1pb.PlanType_PRO),
|
Features: getDefaultFeatures(v1pb.PlanType_PRO),
|
||||||
}
|
}
|
||||||
if validateResponse.LicenseKey.ExpiresAt != nil && *validateResponse.LicenseKey.ExpiresAt != "" {
|
if lemonsqueezyResponse.LicenseKey.ExpiresAt != nil && *lemonsqueezyResponse.LicenseKey.ExpiresAt != "" {
|
||||||
expiresTime, err := time.Parse(time.RFC3339Nano, *validateResponse.LicenseKey.ExpiresAt)
|
expiresTime, err := time.Parse(time.RFC3339Nano, *lemonsqueezyResponse.LicenseKey.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to parse license key expires time")
|
return nil, errors.Wrap(err, "failed to parse license key expires time")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user