forked from github/plane
Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance
This commit is contained in:
commit
6045e659bc
@ -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()
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
71
apiserver/plane/db/management/commands/create_bucket.py
Normal file
71
apiserver/plane/db/management/commands/create_bucket.py
Normal 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}"))
|
@ -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()
|
||||
|
@ -28,6 +28,7 @@ class InstanceAdminSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InstanceAdmin
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"instance",
|
||||
|
@ -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)
|
||||
|
||||
|
0
apiserver/plane/license/management/__init__.py
Normal file
0
apiserver/plane/license/management/__init__.py
Normal 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"))
|
@ -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()
|
@ -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',),
|
||||
},
|
||||
),
|
||||
|
@ -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',),
|
||||
},
|
||||
),
|
||||
]
|
@ -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
|
||||
|
||||
|
21
web/layouts/admin-layout/header.tsx
Normal file
21
web/layouts/admin-layout/header.tsx
Normal 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>
|
||||
);
|
3
web/layouts/admin-layout/index.ts
Normal file
3
web/layouts/admin-layout/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./layout";
|
||||
export * from "./sidebar";
|
||||
export * from "./header";
|
32
web/layouts/admin-layout/layout.tsx
Normal file
32
web/layouts/admin-layout/layout.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
26
web/layouts/admin-layout/sidebar.tsx
Normal file
26
web/layouts/admin-layout/sidebar.tsx
Normal 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
16
web/pages/admin/index.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user