diff --git a/.golangci.yaml b/.golangci.yaml index a7082e2..c905a23 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -51,6 +51,9 @@ linters-settings: disabled: true - name: early-return disabled: true + - name: exported + arguments: + - "disableStutteringCheck" gocritic: disabled-checks: - ifElseChain diff --git a/api/v1/v1.go b/api/v1/v1.go index d13aa6d..e4bac4f 100644 --- a/api/v1/v1.go +++ b/api/v1/v1.go @@ -2,19 +2,22 @@ package v1 import ( "github.com/boojack/slash/server/profile" + "github.com/boojack/slash/server/service/license" "github.com/boojack/slash/store" "github.com/labstack/echo/v4" ) type APIV1Service struct { - Profile *profile.Profile - Store *store.Store + Profile *profile.Profile + Store *store.Store + LicenseService *license.LicenseService } -func NewAPIV1Service(profile *profile.Profile, store *store.Store) *APIV1Service { +func NewAPIV1Service(profile *profile.Profile, store *store.Store, licenseService *license.LicenseService) *APIV1Service { return &APIV1Service{ - Profile: profile, - Store: store, + Profile: profile, + Store: store, + LicenseService: licenseService, } } diff --git a/api/v2/subscription_service.go b/api/v2/subscription_service.go index e30ee74..6b87e7c 100644 --- a/api/v2/subscription_service.go +++ b/api/v2/subscription_service.go @@ -2,71 +2,36 @@ package v2 import ( "context" - "time" - "github.com/boojack/slash/plugin/license" 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/server/service/license" "github.com/boojack/slash/store" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" ) type SubscriptionService struct { apiv2pb.UnimplementedSubscriptionServiceServer - Profile *profile.Profile - Store *store.Store + Profile *profile.Profile + Store *store.Store + LicenseService *license.LicenseService } // NewSubscriptionService creates a new SubscriptionService. -func NewSubscriptionService(profile *profile.Profile, store *store.Store) *SubscriptionService { +func NewSubscriptionService(profile *profile.Profile, store *store.Store, licenseService *license.LicenseService) *SubscriptionService { return &SubscriptionService{ - Profile: profile, - Store: store, + Profile: profile, + Store: store, + LicenseService: licenseService, } } func (s *SubscriptionService) GetSubscription(ctx context.Context, _ *apiv2pb.GetSubscriptionRequest) (*apiv2pb.GetSubscriptionResponse, error) { - workspaceSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ - Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY, - }) + subscription, err := s.LicenseService.LoadSubscription(ctx) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err) - } - subscription := &apiv2pb.Subscription{ - Plan: apiv2pb.PlanType_FREE, - } - licenseKey := "" - if workspaceSetting != nil { - licenseKey = workspaceSetting.GetLicenseKey() - } - if licenseKey == "" { - return &apiv2pb.GetSubscriptionResponse{ - Subscription: subscription, - }, nil - } - - validateResponse, err := license.ValidateLicenseKey(licenseKey, "") - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to validate license key: %v", err) - } - 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, status.Errorf(codes.Internal, "failed to parse license key expired time: %v", err) - } - subscription.ExpiresTime = timestamppb.New(expiresTime) - } - startedTime, err := time.Parse("2006-01-02 15:04:05", validateResponse.LicenseKey.CreatedAt) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to parse license key created time: %v", err) - } - subscription.StartedTime = timestamppb.New(startedTime) + return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err) } return &apiv2pb.GetSubscriptionResponse{ Subscription: subscription, @@ -74,25 +39,11 @@ func (s *SubscriptionService) GetSubscription(ctx context.Context, _ *apiv2pb.Ge } func (s *SubscriptionService) UpdateSubscription(ctx context.Context, request *apiv2pb.UpdateSubscriptionRequest) (*apiv2pb.UpdateSubscriptionResponse, error) { - licenseKey := request.LicenseKey - if licenseKey == "" { - return nil, status.Errorf(codes.InvalidArgument, "license key is required") - } - validateResponse, err := license.ValidateLicenseKey(licenseKey, "") + subscription, err := s.LicenseService.UpdateSubscription(ctx, request.LicenseKey) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to validate license key: %v", err) + return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err) } - if !validateResponse.Valid { - return nil, status.Errorf(codes.InvalidArgument, "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, status.Errorf(codes.Internal, "failed to update license key: %v", err) - } - return &apiv2pb.UpdateSubscriptionResponse{}, nil + return &apiv2pb.UpdateSubscriptionResponse{ + Subscription: subscription, + }, nil } diff --git a/api/v2/v2.go b/api/v2/v2.go index 7665772..b0b2b3c 100644 --- a/api/v2/v2.go +++ b/api/v2/v2.go @@ -6,6 +6,7 @@ import ( apiv2pb "github.com/boojack/slash/proto/gen/api/v2" "github.com/boojack/slash/server/profile" + "github.com/boojack/slash/server/service/license" "github.com/boojack/slash/store" grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/improbable-eng/grpc-web/go/grpcweb" @@ -16,22 +17,23 @@ import ( ) type APIV2Service struct { - Secret string - Profile *profile.Profile - Store *store.Store + Secret string + Profile *profile.Profile + Store *store.Store + LicenseService *license.LicenseService grpcServer *grpc.Server grpcServerPort int } -func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, grpcServerPort int) *APIV2Service { +func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, licenseService *license.LicenseService, grpcServerPort int) *APIV2Service { authProvider := NewGRPCAuthInterceptor(store, secret) grpcServer := grpc.NewServer( grpc.ChainUnaryInterceptor( authProvider.AuthenticationInterceptor, ), ) - apiv2pb.RegisterSubscriptionServiceServer(grpcServer, NewSubscriptionService(profile, store)) + apiv2pb.RegisterSubscriptionServiceServer(grpcServer, NewSubscriptionService(profile, store, licenseService)) apiv2pb.RegisterWorkspaceServiceServer(grpcServer, NewWorkspaceService(profile, store)) apiv2pb.RegisterUserServiceServer(grpcServer, NewUserService(secret, store)) apiv2pb.RegisterUserSettingServiceServer(grpcServer, NewUserSettingService(store)) @@ -42,6 +44,7 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store Secret: secret, Profile: profile, Store: store, + LicenseService: licenseService, grpcServer: grpcServer, grpcServerPort: grpcServerPort, } diff --git a/server/server.go b/server/server.go index b872a67..be246fd 100644 --- a/server/server.go +++ b/server/server.go @@ -12,6 +12,7 @@ import ( apiv2 "github.com/boojack/slash/api/v2" storepb "github.com/boojack/slash/proto/gen/store" "github.com/boojack/slash/server/profile" + "github.com/boojack/slash/server/service/license" "github.com/boojack/slash/store" "github.com/google/uuid" "github.com/labstack/echo/v4" @@ -24,6 +25,8 @@ type Server struct { Profile *profile.Profile Store *store.Store + licenseService *license.LicenseService + // API services. apiV2Service *apiv2.APIV2Service } @@ -34,10 +37,13 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store e.HideBanner = true e.HidePort = true + licenseService := license.NewLicenseService(profile, store) + s := &Server{ - e: e, - Profile: profile, - Store: store, + e: e, + Profile: profile, + Store: store, + licenseService: licenseService, } e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ @@ -90,10 +96,10 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store rootGroup := e.Group("") // Register API v1 routes. - apiV1Service := apiv1.NewAPIV1Service(profile, store) + apiV1Service := apiv1.NewAPIV1Service(profile, store, licenseService) apiV1Service.Start(rootGroup, secret) - s.apiV2Service = apiv2.NewAPIV2Service(secret, profile, store, s.Profile.Port+1) + s.apiV2Service = apiv2.NewAPIV2Service(secret, profile, store, licenseService, s.Profile.Port+1) // Register gRPC gateway as api v2. if err := s.apiV2Service.RegisterGateway(ctx, e); err != nil { return nil, fmt.Errorf("failed to register gRPC gateway: %w", err) diff --git a/plugin/license/cache.go b/server/service/license/cache.go similarity index 100% rename from plugin/license/cache.go rename to server/service/license/cache.go diff --git a/server/service/license/license.go b/server/service/license/license.go new file mode 100644 index 0000000..7d82881 --- /dev/null +++ b/server/service/license/license.go @@ -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) +} diff --git a/plugin/license/license.go b/server/service/license/requests.go similarity index 95% rename from plugin/license/license.go rename to server/service/license/requests.go index 9b65502..4d60e29 100644 --- a/plugin/license/license.go +++ b/server/service/license/requests.go @@ -22,7 +22,6 @@ const ( subscriptionProProductID = 98995 ) -//revive:disable-next-line type LicenseKey struct { ID int32 `json:"id"` Status string `json:"status"` @@ -31,7 +30,6 @@ type LicenseKey struct { ExpiresAt *string `json:"updated_at"` } -//revive:disable-next-line type LicenseKeyMeta struct { StoreID int32 `json:"store_id"` OrderID int32 `json:"order_id"` @@ -59,7 +57,7 @@ type ActiveLicenseKeyResponse struct { Meta *LicenseKeyMeta `json:"meta"` } -func ValidateLicenseKey(licenseKey string, instanceName string) (*ValidateLicenseKeyResponse, error) { +func validateLicenseKey(licenseKey string, instanceName string) (*ValidateLicenseKeyResponse, error) { data := map[string]string{"license_key": licenseKey} if instanceName != "" { data["instance_name"] = instanceName @@ -104,7 +102,7 @@ func ValidateLicenseKey(licenseKey string, instanceName string) (*ValidateLicens return &response, nil } -func ActiveLicenseKey(licenseKey string, instanceName string) (*ActiveLicenseKeyResponse, error) { +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 { diff --git a/plugin/license/license_test.go b/server/service/license/requests_test.go similarity index 90% rename from plugin/license/license_test.go rename to server/service/license/requests_test.go index c71581e..08ea883 100644 --- a/plugin/license/license_test.go +++ b/server/service/license/requests_test.go @@ -29,7 +29,7 @@ func TestValidateLicenseKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - response, err := ValidateLicenseKey(tt.key, "test-instance") + response, err := validateLicenseKey(tt.key, "test-instance") if tt.err != nil { require.EqualError(t, err, tt.err.Error()) return @@ -60,7 +60,7 @@ func TestActiveLicenseKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - response, err := ActiveLicenseKey(tt.key, "test-instance") + response, err := activeLicenseKey(tt.key, "test-instance") require.NoError(t, err) require.Equal(t, tt.expected, response.Activated) })