Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance

This commit is contained in:
pablohashescobar 2023-11-16 06:46:48 +00:00
commit 6045e659bc
25 changed files with 402 additions and 193 deletions

View File

@ -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()

View File

@ -4,9 +4,9 @@ python manage.py wait_for_db
python manage.py migrate python manage.py migrate
# Register instance # Register instance
python bin/instance_registration.py python manage.py register_instance
# Load the configuration variable # Load the configuration variable
python bin/instance_configuration.py python manage.py configure_instance
# Create the default bucket # Create the default bucket
python bin/bucket_script.py python bin/bucket_script.py

View File

@ -12,8 +12,9 @@ from sentry_sdk import capture_exception
# Module imports # Module imports
from .base import BaseAPIView from .base import BaseAPIView
from plane.license.models import Instance from plane.license.models import Instance, InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
class ConfigurationEndpoint(BaseAPIView): class ConfigurationEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
@ -21,27 +22,75 @@ class ConfigurationEndpoint(BaseAPIView):
] ]
def get(self, request): def get(self, request):
instance_configuration = Instance.objects.values("key", "value") instance_configuration = InstanceConfiguration.objects.values("key", "value")
data = {} data = {}
# Authentication # Authentication
data["google_client_id"] = get_configuration_value(instance_configuration, "GOOGLE_CLIENT_ID") data["google_client_id"] = get_configuration_value(
data["github_client_id"] = get_configuration_value(instance_configuration,"GITHUB_CLIENT_ID") instance_configuration,
data["github_app_name"] = get_configuration_value(instance_configuration, "GITHUB_APP_NAME") "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"] = ( data["magic_login"] = (
bool(get_configuration_value(instance_configuration, "EMAIL_HOST_USER")) and bool(get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD")) bool(
) and get_configuration_value(instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0") == "1" 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"] = ( 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 # 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 # Posthog
data["posthog_api_key"] = get_configuration_value(instance_configuration, "POSTHOG_API_KEY") data["posthog_api_key"] = get_configuration_value(
data["posthog_host"] = get_configuration_value(instance_configuration, "POSTHOG_HOST") 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 # 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) return Response(data, status=status.HTTP_200_OK)

View File

@ -61,7 +61,7 @@ def send_export_email(email, slug, csv_buffer):
use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), 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.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
msg.send(fail_silently=False) msg.send(fail_silently=False)

View File

@ -46,7 +46,7 @@ def email_verification(first_name, email, token, current_site):
) )
# Initiate email alternatives # 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.attach_alternative(html_content, "text/html")
msg.send() msg.send()
return return

View File

@ -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")), use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")),
) )
# Initiate email alternatives # 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.attach_alternative(html_content, "text/html")
msg.send() msg.send()
return return

View File

@ -18,8 +18,6 @@ def magic_link(email, key, token, current_site):
realtivelink = f"/magic-sign-in/?password={token}&key={key}" realtivelink = f"/magic-sign-in/?password={token}&key={key}"
abs_url = current_site + realtivelink abs_url = current_site + realtivelink
from_email_string = settings.EMAIL_FROM
subject = "Login for Plane" subject = "Login for Plane"
context = {"magic_url": abs_url, "code": token} 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")), use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")),
) )
# Initiate email alternatives # 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.attach_alternative(html_content, "text/html")
msg.send() msg.send()
return return

View File

@ -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")), use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")),
) )
# Initiate email alternatives # 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.attach_alternative(html_content, "text/html")
msg.send() msg.send()
return return

View File

@ -11,7 +11,7 @@ from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError from slack_sdk.errors import SlackApiError
# Module imports # 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.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value 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 @shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor): def workspace_invitation(email, workspace_id, token, current_site, invitor):
try: try:
user = User.objects.get(email=invitor) user = User.objects.get(email=invitor)
workspace = Workspace.objects.get(pk=workspace_id) 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
relative_link = ( relative_link = f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}"
f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}"
)
# The complete url including the domain # The complete url including the domain
abs_url = current_site + relative_link 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.message = text_content
workspace_member_invite.save() 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( connection = get_connection(
host=get_configuration_value(instance_configuration, "EMAIL_HOST"), 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"), username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"), password=get_configuration_value(
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")), instance_configuration, "EMAIL_HOST_PASSWORD"
use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")), ),
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 # 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.attach_alternative(html_content, "text/html")
msg.send() msg.send()

View File

@ -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}"))

View File

@ -7,17 +7,27 @@ from plane.license.models import Instance, InstanceAdmin
class InstanceOwnerPermission(BasePermission): class InstanceOwnerPermission(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
if request.user.is_anonymous:
return False
instance = Instance.objects.first() instance = Instance.objects.first()
return InstanceAdmin.objects.filter( return InstanceAdmin.objects.filter(
role=20, role=20,
instance=instance, instance=instance,
user=request.user,
).exists() ).exists()
class InstanceAdminPermission(BasePermission): class InstanceAdminPermission(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
if request.user.is_anonymous:
return False
instance = Instance.objects.first() instance = Instance.objects.first()
return InstanceAdmin.objects.filter( return InstanceAdmin.objects.filter(
role__gte=15, role__gte=15,
instance=instance, instance=instance,
user=request.user,
).exists() ).exists()

View File

@ -28,6 +28,7 @@ class InstanceAdminSerializer(BaseSerializer):
class Meta: class Meta:
model = InstanceAdmin model = InstanceAdmin
fields = "__all__"
read_only_fields = [ read_only_fields = [
"id", "id",
"instance", "instance",

View File

@ -9,7 +9,11 @@ from rest_framework.permissions import AllowAny
# Module imports # Module imports
from plane.api.views import BaseAPIView from plane.api.views import BaseAPIView
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration 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 ( from plane.license.api.permissions import (
InstanceOwnerPermission, InstanceOwnerPermission,
InstanceAdminPermission, InstanceAdminPermission,
@ -75,7 +79,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView):
instance.primary_owner = user instance.primary_owner = user
instance.primary_email = user.email instance.primary_email = user.email
instance.save(update_fields=["owner", "email"]) instance.save(update_fields=["owner", "email"])
# Add the user to admin # Add the user to admin
_ = InstanceAdmin.objects.get_or_create( _ = InstanceAdmin.objects.get_or_create(
instance=instance, instance=instance,
@ -90,11 +94,7 @@ class TransferPrimaryOwnerEndpoint(BaseAPIView):
class InstanceAdminEndpoint(BaseAPIView): class InstanceAdminEndpoint(BaseAPIView):
def get_permissions(self): def get_permissions(self):
if self.request.method == "GET": if self.request.method in ["POST", "DELETE"]:
self.permission_classes = [
AllowAny,
]
elif self.request.method in ["POST", "DELETE"]:
self.permission_classes = [ self.permission_classes = [
InstanceOwnerPermission, InstanceOwnerPermission,
] ]
@ -150,21 +150,23 @@ class InstanceAdminEndpoint(BaseAPIView):
class InstanceConfigurationEndpoint(BaseAPIView): class InstanceConfigurationEndpoint(BaseAPIView):
permission_classes = [
permission_classes = [InstanceAdminEndpoint,] InstanceAdminPermission,
]
def get(self, request): def get(self, request):
instance_configurations = InstanceConfiguration.objects.all() instance_configurations = InstanceConfiguration.objects.all()
serializer = InstanceConfigurationSerializer(instance_configurations, many=True) serializer = InstanceConfigurationSerializer(instance_configurations, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request): def patch(self, request):
key = request.data.get("key", False) key = request.data.get("key", False)
if not key: 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 = InstanceConfiguration.objects.get(key=key)
configuration.value = request.data.get("value") configuration.value = request.data.get("value")
configuration.save() configuration.save()
serializer = InstanceConfigurationSerializer(configuration) serializer = InstanceConfigurationSerializer(configuration)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -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"))

View File

@ -1,41 +1,43 @@
# Python imports # Python imports
import os, sys
import json import json
import uuid import os
import requests import requests
import uuid
# Django imports # Django imports
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone 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") class Command(BaseCommand):
help = "Check if instance in registered else register"
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
def handle(self, *args, **options):
# Check if the instance is registered # Check if the instance is registered
instance = Instance.objects.first() instance = Instance.objects.first()
# If instance is None then register this instance # If instance is None then register this instance
if instance is None: 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 # Load JSON content from the file
data = json.load(file) data = json.load(file)
admin_email = os.environ.get("ADMIN_EMAIL") 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 # Raise an exception if the admin email is not provided
if not admin_email: if not admin_email:
raise Exception("ADMIN_EMAIL is required") raise CommandError("ADMIN_EMAIL is required")
# Check if the admin email user exists # Check if the admin email user exists
user = User.objects.filter(email=admin_email).first() 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") license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
if not 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"} headers = {"Content-Type": "application/json"}
@ -84,19 +86,19 @@ def instance_registration():
role=20, 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 return
print("Instance could not be registered") self.stdout.write(self.style.WARNING("Instance could not be registered"))
return return
else: else:
print( self.stdout.write(
f"Instance already registered with instance owner: {instance.primary_owner.email}" self.style.SUCCESS(
f"Instance already registered with instance owner: {instance.primary_owner.email}"
)
) )
return return
except ImportError:
raise ImportError
if __name__ == "__main__":
instance_registration()

View File

@ -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.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -15,6 +15,34 @@ class Migration(migrations.Migration):
] ]
operations = [ 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( migrations.CreateModel(
name='InstanceConfiguration', name='InstanceConfiguration',
fields=[ fields=[
@ -34,30 +62,21 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Instance', name='InstanceAdmin',
fields=[ fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified 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)), ('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)), ('role', models.PositiveIntegerField(choices=[(20, 'Owner'), (15, 'Admin')], default=15)),
('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)),
('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')), ('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')), ('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={ options={
'verbose_name': 'Instance', 'verbose_name': 'Instance Admin',
'verbose_name_plural': 'Instances', 'verbose_name_plural': 'Instance Admins',
'db_table': 'instances', 'db_table': 'instance_admins',
'ordering': ('-created_at',), 'ordering': ('-created_at',),
}, },
), ),

View File

@ -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',),
},
),
]

View File

@ -5,6 +5,7 @@ import ssl
import certifi import certifi
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
# Django imports # Django imports
from django.core.management.utils import get_random_secret_key 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}:" AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
# JWT Auth Configuration # JWT Auth Configuration
SIMPLE_JWT = { SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080), "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080),
@ -332,4 +332,3 @@ SCOUT_NAME = "Plane"
# Set the variable true if running in docker environment # Set the variable true if running in docker environment
DOCKERIZED = int(os.environ.get("DOCKERIZED", 1)) == 1 DOCKERIZED = int(os.environ.get("DOCKERIZED", 1)) == 1
USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1

View File

@ -0,0 +1,21 @@
import { FC } from "react";
// ui
import { Breadcrumbs } from "@plane/ui";
// icons
import { Settings } from "lucide-react";
export const InstanceAdminHeader: FC = () => (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 h-[3.75rem] items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
label="General"
/>
</Breadcrumbs>
</div>
</div>
</div>
);

View File

@ -0,0 +1,3 @@
export * from "./layout";
export * from "./sidebar";
export * from "./header";

View File

@ -0,0 +1,32 @@
import { FC, ReactNode } from "react";
// layouts
import { UserAuthWrapper } from "layouts/auth-layout";
// components
import { InstanceAdminSidebar } from "./sidebar";
import { InstanceAdminHeader } from "./header";
export interface IInstanceAdminLayout {
children: ReactNode;
}
export const InstanceAdminLayout: FC<IInstanceAdminLayout> = (props) => {
const { children } = props;
return (
<>
<UserAuthWrapper>
<div className="relative flex h-screen w-full overflow-hidden">
<InstanceAdminSidebar />
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
<InstanceAdminHeader />
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
<>{children}</>
</div>
</div>
</main>
</div>
</UserAuthWrapper>
</>
);
};

View File

@ -0,0 +1,26 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { WorkspaceHelpSection } from "components/workspace";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
export interface IAppSidebar {}
export const InstanceAdminSidebar: FC<IAppSidebar> = observer(() => {
// store
const { theme: themStore } = useMobxStore();
return (
<div
id="app-sidebar"
className={`fixed md:relative inset-y-0 flex flex-col bg-custom-sidebar-background-100 h-full flex-shrink-0 flex-grow-0 border-r border-custom-sidebar-border-200 z-20 duration-300 ${
themStore?.sidebarCollapsed ? "" : "md:w-[280px]"
} ${themStore?.sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
>
<div className="flex h-full w-full flex-1 flex-col">
<WorkspaceHelpSection />
</div>
</div>
);
});

16
web/pages/admin/index.tsx Normal file
View File

@ -0,0 +1,16 @@
import { ReactElement } from "react";
// layouts
import { InstanceAdminLayout } from "layouts/admin-layout";
// types
import { NextPageWithLayout } from "types/app";
const InstanceAdminPage: NextPageWithLayout = () => {
console.log("admin page");
return <div>Admin Page</div>;
};
InstanceAdminPage.getLayout = function getLayout(page: ReactElement) {
return <InstanceAdminLayout>{page}</InstanceAdminLayout>;
};
export default InstanceAdminPage;