diff --git a/apiserver/package.json b/apiserver/package.json new file mode 100644 index 000000000..c622ae496 --- /dev/null +++ b/apiserver/package.json @@ -0,0 +1,4 @@ +{ + "name": "plane-api", + "version": "0.13.2" +} \ No newline at end of file diff --git a/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py index 4890ec9d5..e69de29bb 100644 --- a/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py @@ -1,21 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-18 12:04 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import plane.db.models.issue - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0045_issueactivity_epoch_workspacemember_issue_props_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='issueproperty', - name='properties', - field=models.JSONField(default=plane.db.models.issue.get_default_properties), - ), - ] diff --git a/apiserver/plane/license/__init__.py b/apiserver/plane/license/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/api/__init__.py b/apiserver/plane/license/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/api/views/__init__.py b/apiserver/plane/license/api/views/__init__.py new file mode 100644 index 000000000..6bc69fcc3 --- /dev/null +++ b/apiserver/plane/license/api/views/__init__.py @@ -0,0 +1,3 @@ +from .product import ProductEndpoint +from .checkout import CheckoutEndpoint +from .instance import InstanceEndpoint \ No newline at end of file diff --git a/apiserver/plane/license/api/views/checkout.py b/apiserver/plane/license/api/views/checkout.py new file mode 100644 index 000000000..8ac3e2e53 --- /dev/null +++ b/apiserver/plane/license/api/views/checkout.py @@ -0,0 +1,71 @@ +# Python imports +import os +import json +import requests + +# Django imports +from django.conf import settings + +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.api.views.base import BaseAPIView +from plane.db.models import Workspace, WorkspaceMember +from plane.license.models import License + +class CheckoutEndpoint(BaseAPIView): + + def post(self, request, slug): + LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "") + + license = License.objects.first() + + if license is None: + return Response({"error": "Instance is not activated"}, status=status.HTTP_400_BAD_REQUEST) + + + price_id = request.data.get("price_id", False) + + if not price_id : + return Response( + {"error": "Price ID is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + total_workspace_members = WorkspaceMember.objects.filter(workspace__slug=slug).count() + + payload = { + "user": { + "id": str(request.user.id), + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "email": request.user.email, + }, + "workspace": { + "id": str(workspace.id), + "name": str(workspace.name), + "slug": str(slug), + }, + "priceId": price_id, + "seats": total_workspace_members, + "return_url": settings.WEB_URL, + } + + headers = { + "Content-Type": "application/json", + "X-Api-Key": str(license.api_key), + } + + response = requests.post( + f"{LICENSE_ENGINE_BASE_URL}/api/checkout/create-session", + data=json.dumps(payload), + headers=headers, + ) + + if response.status_code == 200: + return Response(response.json(), status=status.HTTP_200_OK) + + return Response({"error": "Unable to create a checkout try again later"}, status=response.status_code) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py new file mode 100644 index 000000000..a34181539 --- /dev/null +++ b/apiserver/plane/license/api/views/instance.py @@ -0,0 +1,79 @@ +# Python imports +import os +import json +import requests + +# Third party imports +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import AllowAny + +# Module imports +from plane.api.views import BaseAPIView +from plane.license.models import License + + +class InstanceEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def post(self, request): + email = request.data.get("email", False) + + with open("package.json", "r") as file: + # Load JSON content from the file + data = json.load(file) + + if not email: + return Response( + {"error": "Email is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "") + + headers = {"Content-Type": "application/json"} + + payload = {"email": email, "version": data.get("version", 0.1)} + + response = requests.post( + f"{LICENSE_ENGINE_BASE_URL}/api/instances", + headers=headers, + data=json.dumps(payload), + ) + + if response.status_code == 201: + data = response.json() + license = License.objects.create( + instance_id=data.get("id"), + license_key=data.get("license_key"), + api_key=data.get("api_key"), + version=data.get("version"), + email=data.get("email"), + ) + return Response( + { + "id": str(license.instance_id), + "message": "Instance registered succesfully", + }, + status=status.HTTP_200_OK, + ) + + return Response( + {"error": "Unable to create instance"}, status=response.status_code + ) + + def get(self, request): + license = License.objects.first() + + if license is None: + return Response({"activated": False}, status=status.HTTP_200_OK) + + data = { + "instance_id": license.instance_id, + "version": license.version, + "activated": True, + } + + return Response(data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/license/api/views/product.py b/apiserver/plane/license/api/views/product.py new file mode 100644 index 000000000..b8e37d839 --- /dev/null +++ b/apiserver/plane/license/api/views/product.py @@ -0,0 +1,38 @@ +# Python imports +import os +import requests + +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.api.views.base import BaseAPIView +from plane.license.models import License + + +class ProductEndpoint(BaseAPIView): + def get(self, request, slug): + LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "") + + license = License.objects.first() + + if license is None: + return Response( + {"error": "Instance is not activated"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # # Request the licensing engine + + response = requests.get( + f"{LICENSE_ENGINE_BASE_URL}/api/products", + headers={ + "X-Api-Key": license.api_key, + }, + ) + if response.status_code == 200: + return Response(response.json(), status=status.HTTP_200_OK) + return Response( + {"error": "Unable to fetch products"}, status=response.status_code + ) diff --git a/apiserver/plane/license/apps.py b/apiserver/plane/license/apps.py new file mode 100644 index 000000000..400e98155 --- /dev/null +++ b/apiserver/plane/license/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LicenseConfig(AppConfig): + name = "plane.license" diff --git a/apiserver/plane/license/migrations/0001_initial.py b/apiserver/plane/license/migrations/0001_initial.py new file mode 100644 index 000000000..8b9765ce2 --- /dev/null +++ b/apiserver/plane/license/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.3 on 2023-10-26 14:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='License', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('instance_id', models.CharField(max_length=12, unique=True)), + ('license_key', models.CharField(max_length=64)), + ('api_key', models.CharField(max_length=16)), + ('version', models.CharField(max_length=10)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ], + options={ + 'verbose_name': 'License', + 'verbose_name_plural': 'Licenses', + 'db_table': 'licenses', + 'ordering': ('-created_at',), + }, + ), + ] diff --git a/apiserver/plane/license/migrations/0002_license_email.py b/apiserver/plane/license/migrations/0002_license_email.py new file mode 100644 index 000000000..d8163c6d3 --- /dev/null +++ b/apiserver/plane/license/migrations/0002_license_email.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.3 on 2023-10-26 15:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('license', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='license', + name='email', + field=models.CharField(default='email@plane.so', max_length=256), + preserve_default=False, + ), + ] diff --git a/apiserver/plane/license/migrations/0003_alter_license_license_key.py b/apiserver/plane/license/migrations/0003_alter_license_license_key.py new file mode 100644 index 000000000..d4fcca54b --- /dev/null +++ b/apiserver/plane/license/migrations/0003_alter_license_license_key.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-10-26 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('license', '0002_license_email'), + ] + + operations = [ + migrations.AlterField( + model_name='license', + name='license_key', + field=models.CharField(max_length=256), + ), + ] diff --git a/apiserver/plane/license/migrations/0004_alter_license_instance_id.py b/apiserver/plane/license/migrations/0004_alter_license_instance_id.py new file mode 100644 index 000000000..a211468b3 --- /dev/null +++ b/apiserver/plane/license/migrations/0004_alter_license_instance_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-10-26 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('license', '0003_alter_license_license_key'), + ] + + operations = [ + migrations.AlterField( + model_name='license', + name='instance_id', + field=models.CharField(max_length=25, unique=True), + ), + ] diff --git a/apiserver/plane/license/migrations/__init__.py b/apiserver/plane/license/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/models/__init__.py b/apiserver/plane/license/models/__init__.py new file mode 100644 index 000000000..496931e0c --- /dev/null +++ b/apiserver/plane/license/models/__init__.py @@ -0,0 +1 @@ +from .license import License \ No newline at end of file diff --git a/apiserver/plane/license/models/license.py b/apiserver/plane/license/models/license.py new file mode 100644 index 000000000..95268d1c0 --- /dev/null +++ b/apiserver/plane/license/models/license.py @@ -0,0 +1,19 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models import BaseModel + + +class License(BaseModel): + instance_id = models.CharField(max_length=25, unique=True) + license_key = models.CharField(max_length=256) + api_key = models.CharField(max_length=16) + version = models.CharField(max_length=10) + email = models.CharField(max_length=256) + + class Meta: + verbose_name = "License" + verbose_name_plural = "Licenses" + db_table = "licenses" + ordering = ("-created_at",) diff --git a/apiserver/plane/license/urls.py b/apiserver/plane/license/urls.py new file mode 100644 index 000000000..0e83ba268 --- /dev/null +++ b/apiserver/plane/license/urls.py @@ -0,0 +1,21 @@ +from django.urls import path + +from plane.license.api.views import ProductEndpoint, CheckoutEndpoint, InstanceEndpoint + +urlpatterns = [ + path( + "workspaces//products/", + ProductEndpoint.as_view(), + name="products", + ), + path( + "workspaces//create-checkout-session/", + CheckoutEndpoint.as_view(), + name="checkout", + ), + path( + "instances/", + InstanceEndpoint.as_view(), + name="instance", + ), +] diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 27da44d9c..b6c7240af 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ "plane.utils", "plane.web", "plane.middleware", + "plane.license", # Third-party things "rest_framework", "rest_framework.authtoken", diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 2b83ef8cf..b3183b2ed 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -14,6 +14,7 @@ urlpatterns = [ # path("admin/", admin.site.urls), path("", TemplateView.as_view(template_name="index.html")), path("api/", include("plane.api.urls")), + path("api/licenses/", include("plane.license.urls")), path("", include("plane.web.urls")), ]