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
|
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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
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):
|
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()
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
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
|
# 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()
|
|
@ -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',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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
|
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
|
||||||
|
|
||||||
|
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