diff --git a/apiserver/bin/instance_configuration.py b/apiserver/bin/instance_configuration.py deleted file mode 100644 index b30875057..000000000 --- a/apiserver/bin/instance_configuration.py +++ /dev/null @@ -1,49 +0,0 @@ -import os, sys - - -sys.path.append("/code") - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") - -import django - -django.setup() - - -def load_config(): - from plane.license.models import InstanceConfiguration - - config_keys = { - # Authentication Settings - "GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"), - "GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"), - "GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"), - "ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"), - "ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), - "ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), - # Email Settings - "EMAIL_HOST": os.environ.get("EMAIL_HOST", ""), - "EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""), - "EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"), - "EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"), - "EMAIL_FROM": os.environ.get("EMAIL_FROM", ""), - "EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"), - "EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"), - # Open AI Settings - "OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"), - "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"), - "GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), - } - - for key, value in config_keys.items(): - obj, created = InstanceConfiguration.objects.get_or_create( - key=key - ) - obj.value = value - obj.save() - - print(f"{key} loaded with value from environment variable.") - - -if __name__ == "__main__": - load_config() diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff index 92834b8c1..44f251155 100755 --- a/apiserver/bin/takeoff +++ b/apiserver/bin/takeoff @@ -4,9 +4,9 @@ python manage.py wait_for_db python manage.py migrate # Register instance -python bin/instance_registration.py +python manage.py register_instance # Load the configuration variable -python bin/instance_configuration.py +python manage.py configure_instance # Create the default bucket python bin/bucket_script.py diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index 1d65f3d68..237d8d6bf 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -12,8 +12,9 @@ from sentry_sdk import capture_exception # Module imports from .base import BaseAPIView -from plane.license.models import Instance -from plane.license.utils.instance_value import get_configuration_value +from plane.license.models import Instance, InstanceConfiguration +from plane.license.utils.instance_value import get_configuration_value + class ConfigurationEndpoint(BaseAPIView): permission_classes = [ @@ -21,27 +22,75 @@ class ConfigurationEndpoint(BaseAPIView): ] def get(self, request): - instance_configuration = Instance.objects.values("key", "value") + instance_configuration = InstanceConfiguration.objects.values("key", "value") data = {} # Authentication - data["google_client_id"] = get_configuration_value(instance_configuration, "GOOGLE_CLIENT_ID") - data["github_client_id"] = get_configuration_value(instance_configuration,"GITHUB_CLIENT_ID") - data["github_app_name"] = get_configuration_value(instance_configuration, "GITHUB_APP_NAME") + data["google_client_id"] = get_configuration_value( + instance_configuration, + "GOOGLE_CLIENT_ID", + os.environ.get("GOOGLE_CLIENT_ID", None), + ) + data["github_client_id"] = get_configuration_value( + instance_configuration, + "GITHUB_CLIENT_ID", + os.environ.get("GITHUB_CLIENT_ID", None), + ) + data["github_app_name"] = get_configuration_value( + instance_configuration, + "GITHUB_APP_NAME", + os.environ.get("GITHUB_APP_NAME", None), + ) data["magic_login"] = ( - bool(get_configuration_value(instance_configuration, "EMAIL_HOST_USER")) and bool(get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD")) - ) and get_configuration_value(instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0") == "1" + bool( + get_configuration_value( + instance_configuration, + "EMAIL_HOST_USER", + os.environ.get("GITHUB_APP_NAME", None), + ), + ) + and bool( + get_configuration_value( + instance_configuration, + "EMAIL_HOST_PASSWORD", + os.environ.get("GITHUB_APP_NAME", None), + ) + ) + ) and get_configuration_value( + instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0" + ) == "1" data["email_password_login"] = ( - get_configuration_value(instance_configuration, "ENABLE_EMAIL_PASSWORD", "0") == "1" + get_configuration_value( + instance_configuration, "ENABLE_EMAIL_PASSWORD", "0" + ) + == "1" ) # Slack client - data["slack_client_id"] = get_configuration_value(instance_configuration, "SLACK_CLIENT_ID") - + data["slack_client_id"] = get_configuration_value( + instance_configuration, + "SLACK_CLIENT_ID", + os.environ.get("SLACK_CLIENT_ID", None), + ) + # Posthog - data["posthog_api_key"] = get_configuration_value(instance_configuration, "POSTHOG_API_KEY") - data["posthog_host"] = get_configuration_value(instance_configuration, "POSTHOG_HOST") + data["posthog_api_key"] = get_configuration_value( + instance_configuration, + "POSTHOG_API_KEY", + os.environ.get("POSTHOG_API_KEY", None), + ) + data["posthog_host"] = get_configuration_value( + instance_configuration, + "POSTHOG_HOST", + os.environ.get("POSTHOG_HOST", None), + ) # Unsplash - data["has_unsplash_configured"] = bool(get_configuration_value(instance_configuration, "UNSPLASH_ACCESS_KEY")) + data["has_unsplash_configured"] = bool( + get_configuration_value( + instance_configuration, + "UNSPLASH_ACCESS_KEY", + os.environ.get("UNSPLASH_ACCESS_KEY", None), + ) + ) return Response(data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 19ee2306e..8cccc2299 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -61,7 +61,7 @@ def send_export_email(email, slug, csv_buffer): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) msg.send(fail_silently=False) diff --git a/apiserver/plane/bgtasks/email_verification_task.py b/apiserver/plane/bgtasks/email_verification_task.py index aedc48e0e..220f96390 100644 --- a/apiserver/plane/bgtasks/email_verification_task.py +++ b/apiserver/plane/bgtasks/email_verification_task.py @@ -46,7 +46,7 @@ def email_verification(first_name, email, token, current_site): ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index fa4e2ea45..676ca5a9c 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -42,7 +42,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index e6d047e9c..8d64029ee 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -18,8 +18,6 @@ def magic_link(email, key, token, current_site): realtivelink = f"/magic-sign-in/?password={token}&key={key}" abs_url = current_site + realtivelink - from_email_string = settings.EMAIL_FROM - subject = "Login for Plane" context = {"magic_url": abs_url, "code": token} @@ -38,7 +36,7 @@ def magic_link(email, key, token, current_site): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index 43c7ed87b..e6571d795 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -56,7 +56,7 @@ def project_invitation(email, project_id, token, current_site, invitor): use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection) msg.attach_alternative(html_content, "text/html") msg.send() return diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 08f4e7204..477c7f6fa 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -11,7 +11,7 @@ from slack_sdk import WebClient from slack_sdk.errors import SlackApiError # Module imports -from plane.db.models import User, Workspace, WorkspaceMemberInvite +from plane.db.models import Workspace, WorkspaceMemberInvite, User from plane.license.models import InstanceConfiguration from plane.license.utils.instance_value import get_configuration_value @@ -19,7 +19,6 @@ from plane.license.utils.instance_value import get_configuration_value @shared_task def workspace_invitation(email, workspace_id, token, current_site, invitor): try: - user = User.objects.get(email=invitor) workspace = Workspace.objects.get(pk=workspace_id) @@ -28,9 +27,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): ) # Relative link - relative_link = ( - f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}" - ) + relative_link = f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}" # The complete url including the domain abs_url = current_site + relative_link @@ -57,17 +54,33 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): workspace_member_invite.message = text_content workspace_member_invite.save() - instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value") + instance_configuration = InstanceConfiguration.objects.filter( + key__startswith="EMAIL_" + ).values("key", "value") connection = get_connection( host=get_configuration_value(instance_configuration, "EMAIL_HOST"), - port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")), + port=int( + get_configuration_value(instance_configuration, "EMAIL_PORT", "587") + ), username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"), - password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"), - use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")), - use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), + password=get_configuration_value( + instance_configuration, "EMAIL_HOST_PASSWORD" + ), + use_tls=bool( + get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1") + ), + use_ssl=bool( + get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0") + ), ) # Initiate email alternatives - msg = EmailMultiAlternatives(subject=subject, text_content=text_content, from_email=settings.EMAIL_FROM, to=[email], connection=connection) + msg = EmailMultiAlternatives( + subject=subject, + body=text_content, + from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), + to=[email], + connection=connection, + ) msg.attach_alternative(html_content, "text/html") msg.send() diff --git a/apiserver/plane/db/management/commands/create_bucket.py b/apiserver/plane/db/management/commands/create_bucket.py new file mode 100644 index 000000000..054523bf9 --- /dev/null +++ b/apiserver/plane/db/management/commands/create_bucket.py @@ -0,0 +1,71 @@ +# Python imports +import boto3 +import json +from botocore.exceptions import ClientError + +# Django imports +from django.core.management import BaseCommand +from django.conf import settings + +class Command(BaseCommand): + help = "Create the default bucket for the instance" + + def set_bucket_public_policy(self, s3_client, bucket_name): + public_policy = { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject"], + "Resource": [f"arn:aws:s3:::{bucket_name}/*"] + }] + } + + try: + s3_client.put_bucket_policy( + Bucket=bucket_name, + Policy=json.dumps(public_policy) + ) + self.stdout.write(self.style.SUCCESS(f"Public read access policy set for bucket '{bucket_name}'.")) + except ClientError as e: + self.stdout.write(self.style.ERROR(f"Error setting public read access policy: {e}")) + + + def handle(self, *args, **options): + # Create a session using the credentials from Django settings + try: + session = boto3.session.Session( + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + # Create an S3 client using the session + s3_client = session.client('s3', endpoint_url=settings.AWS_S3_ENDPOINT_URL) + bucket_name = settings.AWS_STORAGE_BUCKET_NAME + + self.stdout.write(self.style.NOTICE("Checking bucket...")) + + # Check if the bucket exists + s3_client.head_bucket(Bucket=bucket_name) + + self.set_bucket_public_policy(s3_client, bucket_name) + except ClientError as e: + error_code = int(e.response['Error']['Code']) + bucket_name = settings.AWS_STORAGE_BUCKET_NAME + if error_code == 404: + # Bucket does not exist, create it + self.stdout.write(self.style.WARNING(f"Bucket '{bucket_name}' does not exist. Creating bucket...")) + try: + s3_client.create_bucket(Bucket=bucket_name) + self.stdout.write(self.style.SUCCESS(f"Bucket '{bucket_name}' created successfully.")) + self.set_bucket_public_policy(s3_client, bucket_name) + except ClientError as create_error: + self.stdout.write(self.style.ERROR(f"Failed to create bucket: {create_error}")) + elif error_code == 403: + # Access to the bucket is forbidden + self.stdout.write(self.style.ERROR(f"Access to the bucket '{bucket_name}' is forbidden. Check permissions.")) + else: + # Another ClientError occurred + self.stdout.write(self.style.ERROR(f"Failed to check bucket: {e}")) + except Exception as ex: + # Handle any other exception + self.stdout.write(self.style.ERROR(f"An error occurred: {ex}")) \ No newline at end of file diff --git a/apiserver/plane/license/api/permissions/instance.py b/apiserver/plane/license/api/permissions/instance.py index 86583ee95..1d1845f12 100644 --- a/apiserver/plane/license/api/permissions/instance.py +++ b/apiserver/plane/license/api/permissions/instance.py @@ -7,17 +7,27 @@ from plane.license.models import Instance, InstanceAdmin class InstanceOwnerPermission(BasePermission): def has_permission(self, request, view): + + if request.user.is_anonymous: + return False + instance = Instance.objects.first() return InstanceAdmin.objects.filter( role=20, instance=instance, + user=request.user, ).exists() class InstanceAdminPermission(BasePermission): def has_permission(self, request, view): + + if request.user.is_anonymous: + return False + instance = Instance.objects.first() return InstanceAdmin.objects.filter( role__gte=15, instance=instance, + user=request.user, ).exists() diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index 8fd8edda1..b8c990522 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -28,6 +28,7 @@ class InstanceAdminSerializer(BaseSerializer): class Meta: model = InstanceAdmin + fields = "__all__" read_only_fields = [ "id", "instance", diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index b51702398..ecab0740c 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -9,7 +9,11 @@ from rest_framework.permissions import AllowAny # Module imports from plane.api.views import BaseAPIView from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration -from plane.license.api.serializers import InstanceSerializer, InstanceAdminSerializer, InstanceConfigurationSerializer +from plane.license.api.serializers import ( + InstanceSerializer, + InstanceAdminSerializer, + InstanceConfigurationSerializer, +) from plane.license.api.permissions import ( InstanceOwnerPermission, InstanceAdminPermission, @@ -75,7 +79,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView): instance.primary_owner = user instance.primary_email = user.email instance.save(update_fields=["owner", "email"]) - + # Add the user to admin _ = InstanceAdmin.objects.get_or_create( instance=instance, @@ -90,11 +94,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView): class InstanceAdminEndpoint(BaseAPIView): def get_permissions(self): - if self.request.method == "GET": - self.permission_classes = [ - AllowAny, - ] - elif self.request.method in ["POST", "DELETE"]: + if self.request.method in ["POST", "DELETE"]: self.permission_classes = [ InstanceOwnerPermission, ] @@ -150,21 +150,23 @@ class InstanceAdminEndpoint(BaseAPIView): class InstanceConfigurationEndpoint(BaseAPIView): - - permission_classes = [InstanceAdminEndpoint,] + permission_classes = [ + InstanceAdminPermission, + ] def get(self, request): instance_configurations = InstanceConfiguration.objects.all() serializer = InstanceConfigurationSerializer(instance_configurations, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - + def patch(self, request): key = request.data.get("key", False) if not key: - return Response({"error": "Key is required"}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"error": "Key is required"}, status=status.HTTP_400_BAD_REQUEST + ) configuration = InstanceConfiguration.objects.get(key=key) configuration.value = request.data.get("value") configuration.save() serializer = InstanceConfigurationSerializer(configuration) return Response(serializer.data, status=status.HTTP_200_OK) - diff --git a/apiserver/plane/license/management/__init__.py b/apiserver/plane/license/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/management/commands/__init__.py b/apiserver/plane/license/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py new file mode 100644 index 000000000..d71d9f590 --- /dev/null +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -0,0 +1,46 @@ +# Python imports +import os + +# Django imports +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +# Module imports +from plane.license.models import InstanceConfiguration + +class Command(BaseCommand): + help = "Configure instance variables" + + def handle(self, *args, **options): + config_keys = { + # Authentication Settings + "GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID"), + "GITHUB_CLIENT_ID": os.environ.get("GITHUB_CLIENT_ID"), + "GITHUB_CLIENT_SECRET": os.environ.get("GITHUB_CLIENT_SECRET"), + "ENABLE_SIGNUP": os.environ.get("ENABLE_SIGNUP", "1"), + "ENABLE_EMAIL_PASSWORD": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"), + "ENABLE_MAGIC_LINK_LOGIN": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "0"), + # Email Settings + "EMAIL_HOST": os.environ.get("EMAIL_HOST", ""), + "EMAIL_HOST_USER": os.environ.get("EMAIL_HOST_USER", ""), + "EMAIL_HOST_PASSWORD": os.environ.get("EMAIL_HOST_PASSWORD"), + "EMAIL_PORT": os.environ.get("EMAIL_PORT", "587"), + "EMAIL_FROM": os.environ.get("EMAIL_FROM", ""), + "EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"), + "EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"), + # Open AI Settings + "OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"), + "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"), + "GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), + } + + for key, value in config_keys.items(): + obj, created = InstanceConfiguration.objects.get_or_create( + key=key + ) + if created: + obj.value = value + obj.save() + self.stdout.write(self.style.SUCCESS(f"{key} loaded with value from environment variable.")) + else: + self.stdout.write(self.style.WARNING(f"{key} configuration already exists")) \ No newline at end of file diff --git a/apiserver/bin/instance_registration.py b/apiserver/plane/license/management/commands/register_instance.py similarity index 64% rename from apiserver/bin/instance_registration.py rename to apiserver/plane/license/management/commands/register_instance.py index 854251595..855a3a035 100644 --- a/apiserver/bin/instance_registration.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -1,41 +1,43 @@ # Python imports -import os, sys import json -import uuid +import os import requests +import uuid # Django imports +from django.core.management.base import BaseCommand, CommandError from django.utils import timezone +from django.core.exceptions import ValidationError +from django.core.validators import validate_email + +# Module imports +from plane.db.models import User +from plane.license.models import Instance, InstanceAdmin -sys.path.append("/code") - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production") - -import django - -django.setup() - - -def instance_registration(): - try: - # Module imports - from plane.db.models import User - from plane.license.models import Instance, InstanceAdmin +class Command(BaseCommand): + help = "Check if instance in registered else register" + def handle(self, *args, **options): # Check if the instance is registered instance = Instance.objects.first() # If instance is None then register this instance if instance is None: - with open("/code/package.json", "r") as file: + with open("package.json", "r") as file: # Load JSON content from the file data = json.load(file) admin_email = os.environ.get("ADMIN_EMAIL") + + try: + validate_email(admin_email) + except ValidationError: + CommandError(f"{admin_email} is not a valid ADMIN_EMAIL") + # Raise an exception if the admin email is not provided if not admin_email: - raise Exception("ADMIN_EMAIL is required") + raise CommandError("ADMIN_EMAIL is required") # Check if the admin email user exists user = User.objects.filter(email=admin_email).first() @@ -49,7 +51,7 @@ def instance_registration(): license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL") if not license_engine_base_url: - raise Exception("LICENSE_ENGINE_BASE_URL is required") + raise CommandError("LICENSE_ENGINE_BASE_URL is required") headers = {"Content-Type": "application/json"} @@ -84,19 +86,19 @@ def instance_registration(): role=20, ) - print(f"Instance succesfully registered with owner: {instance.primary_owner.email}") + self.stdout.write( + self.style.SUCCESS( + f"Instance succesfully registered with owner: {instance.primary_owner.email}" + ) + ) return - print("Instance could not be registered") + self.stdout.write(self.style.WARNING("Instance could not be registered")) return else: - print( - f"Instance already registered with instance owner: {instance.primary_owner.email}" + self.stdout.write( + self.style.SUCCESS( + f"Instance already registered with instance owner: {instance.primary_owner.email}" + ) ) return - except ImportError: - raise ImportError - - -if __name__ == "__main__": - instance_registration() diff --git a/apiserver/plane/license/migrations/0001_initial.py b/apiserver/plane/license/migrations/0001_initial.py index 6e9d63eb9..db620a18e 100644 --- a/apiserver/plane/license/migrations/0001_initial.py +++ b/apiserver/plane/license/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-13 14:31 +# Generated by Django 4.2.5 on 2023-11-15 14:22 from django.conf import settings from django.db import migrations, models @@ -15,6 +15,34 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Instance', + 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_name', models.CharField(max_length=255)), + ('whitelist_emails', models.TextField(blank=True, null=True)), + ('instance_id', models.CharField(max_length=25, unique=True)), + ('license_key', models.CharField(blank=True, max_length=256, null=True)), + ('api_key', models.CharField(max_length=16)), + ('version', models.CharField(max_length=10)), + ('primary_email', models.CharField(max_length=256)), + ('last_checked_at', models.DateTimeField()), + ('namespace', models.CharField(blank=True, max_length=50, null=True)), + ('is_telemetry_enabled', models.BooleanField(default=True)), + ('is_support_required', models.BooleanField(default=True)), + ('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')), + ('primary_owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL)), + ('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': 'Instance', + 'verbose_name_plural': 'Instances', + 'db_table': 'instances', + 'ordering': ('-created_at',), + }, + ), migrations.CreateModel( name='InstanceConfiguration', fields=[ @@ -34,30 +62,21 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Instance', + name='InstanceAdmin', 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_name', models.CharField(max_length=255)), - ('whitelist_emails', models.TextField(blank=True, null=True)), - ('instance_id', models.CharField(max_length=25, unique=True)), - ('license_key', models.CharField(blank=True, max_length=256, null=True)), - ('api_key', models.CharField(max_length=16)), - ('version', models.CharField(max_length=10)), - ('email', models.CharField(max_length=256)), - ('last_checked_at', models.DateTimeField()), - ('namespace', models.CharField(blank=True, max_length=50, null=True)), - ('is_telemetry_enabled', models.BooleanField(default=True)), - ('is_support_required', models.BooleanField(default=True)), + ('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)), ('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')), - ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), + ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')), ('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')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name': 'Instance', - 'verbose_name_plural': 'Instances', - 'db_table': 'instances', + 'verbose_name': 'Instance Admin', + 'verbose_name_plural': 'Instance Admins', + 'db_table': 'instance_admins', 'ordering': ('-created_at',), }, ), diff --git a/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py b/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py deleted file mode 100644 index 7589239b7..000000000 --- a/apiserver/plane/license/migrations/0002_rename_email_instance_primary_email_and_more.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 4.2.5 on 2023-11-14 10:14 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('license', '0001_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='instance', - old_name='email', - new_name='primary_email', - ), - migrations.RemoveField( - model_name='instance', - name='owner', - ), - migrations.AddField( - model_name='instance', - name='primary_owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_primary_owner', to=settings.AUTH_USER_MODEL), - ), - migrations.CreateModel( - name='InstanceAdmin', - 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)), - ('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)), - ('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')), - ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admins', to='license.instance')), - ('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')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instance_owner', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Instance Admin', - 'verbose_name_plural': 'Instance Admins', - 'db_table': 'instance_admins', - 'ordering': ('-created_at',), - }, - ), - ] diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 856d2d37e..c3d1c6adb 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -5,6 +5,7 @@ import ssl import certifi from datetime import timedelta from urllib.parse import urlparse + # Django imports from django.core.management.utils import get_random_secret_key @@ -236,7 +237,6 @@ if AWS_S3_ENDPOINT_URL: AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:" - # JWT Auth Configuration SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080), @@ -332,4 +332,3 @@ SCOUT_NAME = "Plane" # Set the variable true if running in docker environment DOCKERIZED = int(os.environ.get("DOCKERIZED", 1)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 - diff --git a/web/layouts/admin-layout/header.tsx b/web/layouts/admin-layout/header.tsx new file mode 100644 index 000000000..f2a3e2266 --- /dev/null +++ b/web/layouts/admin-layout/header.tsx @@ -0,0 +1,21 @@ +import { FC } from "react"; +// ui +import { Breadcrumbs } from "@plane/ui"; +// icons +import { Settings } from "lucide-react"; + +export const InstanceAdminHeader: FC = () => ( +