mirror of
https://github.com/aykhans/slash-e.git
synced 2025-06-14 20:07:50 +00:00
chore: update server services
This commit is contained in:
24
server/service/license/cache.go
Normal file
24
server/service/license/cache.go
Normal file
@ -0,0 +1,24 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var (
|
||||
licenseCache = cache.New(24*time.Hour, 24*time.Hour)
|
||||
)
|
||||
|
||||
func SetLicenseCache(licenseKey, instanceName string, license LicenseKey) {
|
||||
licenseCache.Set(fmt.Sprintf("%s-%s", licenseKey, instanceName), license, 24*time.Hour)
|
||||
}
|
||||
|
||||
func GetLicenseCache(licenseKey, instanceName string) *LicenseKey {
|
||||
cache, ok := licenseCache.Get(fmt.Sprintf("%s-%s", licenseKey, instanceName))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return cache.(*LicenseKey)
|
||||
}
|
94
server/service/license/license.go
Normal file
94
server/service/license/license.go
Normal file
@ -0,0 +1,94 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/profile"
|
||||
"github.com/boojack/slash/store"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
CachedSubscription *apiv2pb.Subscription
|
||||
}
|
||||
|
||||
// NewLicenseService creates a new LicenseService.
|
||||
func NewLicenseService(profile *profile.Profile, store *store.Store) *LicenseService {
|
||||
return &LicenseService{
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
CachedSubscription: &apiv2pb.Subscription{
|
||||
Plan: apiv2pb.PlanType_FREE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LicenseService) LoadSubscription(ctx context.Context) (*apiv2pb.Subscription, error) {
|
||||
workspaceSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get workspace setting")
|
||||
}
|
||||
subscription := &apiv2pb.Subscription{
|
||||
Plan: apiv2pb.PlanType_FREE,
|
||||
}
|
||||
licenseKey := ""
|
||||
if workspaceSetting != nil {
|
||||
licenseKey = workspaceSetting.GetLicenseKey()
|
||||
}
|
||||
if licenseKey == "" {
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
validateResponse, err := validateLicenseKey(licenseKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to validate license key")
|
||||
}
|
||||
if validateResponse.Valid {
|
||||
subscription.Plan = apiv2pb.PlanType_PRO
|
||||
if validateResponse.LicenseKey.ExpiresAt != nil && *validateResponse.LicenseKey.ExpiresAt != "" {
|
||||
expiresTime, err := time.Parse("2006-01-02 15:04:05", *validateResponse.LicenseKey.ExpiresAt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse license key expires time")
|
||||
}
|
||||
subscription.ExpiresTime = timestamppb.New(expiresTime)
|
||||
}
|
||||
startedTime, err := time.Parse("2006-01-02 15:04:05", validateResponse.LicenseKey.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse license key created time")
|
||||
}
|
||||
subscription.StartedTime = timestamppb.New(startedTime)
|
||||
}
|
||||
s.CachedSubscription = subscription
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
func (s *LicenseService) UpdateSubscription(ctx context.Context, licenseKey string) (*apiv2pb.Subscription, error) {
|
||||
if licenseKey == "" {
|
||||
return nil, errors.New("license key is required")
|
||||
}
|
||||
validateResponse, err := validateLicenseKey(licenseKey, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to validate license key")
|
||||
}
|
||||
if !validateResponse.Valid {
|
||||
return nil, errors.New("invalid license key")
|
||||
}
|
||||
_, err = s.Store.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY,
|
||||
Value: &storepb.WorkspaceSetting_LicenseKey{
|
||||
LicenseKey: licenseKey,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to upsert workspace setting")
|
||||
}
|
||||
return s.LoadSubscription(ctx)
|
||||
}
|
144
server/service/license/requests.go
Normal file
144
server/service/license/requests.go
Normal file
@ -0,0 +1,144 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// The base API URL for the Lemon Squeezy API.
|
||||
baseAPIURL = "https://api.lemonsqueezy.com"
|
||||
// The store ID for the yourselfhosted store.
|
||||
// Link: https://yourselfhosted.lemonsqueezy.com
|
||||
storeID = 15634
|
||||
// The product ID for the subscription pro product.
|
||||
// Link: https://yourselfhosted.lemonsqueezy.com/checkout/buy/d03a2696-8a8b-49c9-9e19-d425e3884fd7
|
||||
subscriptionProProductID = 98995
|
||||
)
|
||||
|
||||
type LicenseKey struct {
|
||||
ID int32 `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Key string `json:"key"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt *string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LicenseKeyMeta struct {
|
||||
StoreID int32 `json:"store_id"`
|
||||
OrderID int32 `json:"order_id"`
|
||||
OrderItemID int32 `json:"order_item_id"`
|
||||
ProductID int32 `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
VariantID int32 `json:"variant_id"`
|
||||
VariantName string `json:"variant_name"`
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
CustomerEmail string `json:"customer_email"`
|
||||
}
|
||||
|
||||
type ValidateLicenseKeyResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Error *string `json:"error"`
|
||||
LicenseKey *LicenseKey `json:"license_key"`
|
||||
Meta *LicenseKeyMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type ActiveLicenseKeyResponse struct {
|
||||
Activated bool `json:"activated"`
|
||||
Error *string `json:"error"`
|
||||
LicenseKey *LicenseKey `json:"license_key"`
|
||||
Meta *LicenseKeyMeta `json:"meta"`
|
||||
}
|
||||
|
||||
func validateLicenseKey(licenseKey string, instanceName string) (*ValidateLicenseKeyResponse, error) {
|
||||
data := map[string]string{"license_key": licenseKey}
|
||||
if instanceName != "" {
|
||||
data["instance_name"] = instanceName
|
||||
}
|
||||
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/v1/licenses/validate", 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
|
||||
}
|
||||
if response.Error == nil {
|
||||
if response.Meta == nil {
|
||||
return nil, errors.New("meta is nil")
|
||||
}
|
||||
if response.Meta.StoreID != storeID || response.Meta.ProductID != subscriptionProProductID {
|
||||
return nil, errors.New("invalid store or product id")
|
||||
}
|
||||
}
|
||||
licenseCache.Set("key", "value", 24*time.Hour)
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func activeLicenseKey(licenseKey string, instanceName string) (*ActiveLicenseKeyResponse, error) {
|
||||
data := map[string]string{"license_key": licenseKey, "instance_name": instanceName}
|
||||
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/v1/licenses/activate", 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
|
||||
}
|
||||
if response.Error == nil {
|
||||
if response.Meta == nil {
|
||||
return nil, errors.New("meta is nil")
|
||||
}
|
||||
if response.Meta.StoreID != storeID || response.Meta.ProductID != subscriptionProProductID {
|
||||
return nil, errors.New("invalid store or product id")
|
||||
}
|
||||
}
|
||||
return &response, nil
|
||||
}
|
68
server/service/license/requests_test.go
Normal file
68
server/service/license/requests_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateLicenseKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
expected bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Testing license key",
|
||||
key: "26B383EE-95B2-4458-9C58-B376BD6183B1",
|
||||
expected: false,
|
||||
err: errors.New("invalid store or product id"),
|
||||
},
|
||||
{
|
||||
name: "invalid key",
|
||||
key: "invalid-key",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response, err := validateLicenseKey(tt.key, "test-instance")
|
||||
if tt.err != nil {
|
||||
require.EqualError(t, err, tt.err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, response.Valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveLicenseKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Testing license key",
|
||||
key: "26B383EE-95B2-4458-9C58-B376BD6183B1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid key",
|
||||
key: "invalid-key",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response, err := activeLicenseKey(tt.key, "test-instance")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, response.Activated)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user