chore: instance (#2955)

This commit is contained in:
Nikhil 2023-12-04 14:48:40 +05:30 committed by Aaryan Khandelwal
parent 66b728db90
commit aa15a36693
20 changed files with 203 additions and 211 deletions

View File

@ -14,6 +14,11 @@ PGHOST="plane-db"
PGDATABASE="plane" PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Oauth variables
GOOGLE_CLIENT_ID=""
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Redis Settings # Redis Settings
REDIS_HOST="plane-redis" REDIS_HOST="plane-redis"
REDIS_PORT="6379" REDIS_PORT="6379"
@ -50,7 +55,6 @@ NGINX_PORT=80
# SignUps # SignUps
ENABLE_SIGNUP="1" ENABLE_SIGNUP="1"
# Enable Email/Password Signup # Enable Email/Password Signup
ENABLE_EMAIL_PASSWORD="1" ENABLE_EMAIL_PASSWORD="1"

View File

@ -21,7 +21,8 @@ from plane.db.models import (
from .base import BaseSerializer from .base import BaseSerializer
from .cycle import CycleSerializer, CycleLiteSerializer from .cycle import CycleSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleLiteSerializer from .module import ModuleSerializer, ModuleLiteSerializer
from .user import UserLiteSerializer
from .state import StateLiteSerializer
class IssueSerializer(BaseSerializer): class IssueSerializer(BaseSerializer):
assignees = serializers.ListField( assignees = serializers.ListField(
@ -331,12 +332,23 @@ class ModuleIssueSerializer(BaseSerializer):
] ]
class IssueExpandSerializer(BaseSerializer): class LabelLiteSerializer(BaseSerializer):
# Serialize the related cycle. It's a OneToOne relation.
cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
# Serialize the related module. It's a OneToOne relation. class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
class IssueExpandSerializer(BaseSerializer):
cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True)
module = ModuleLiteSerializer(source="issue_module.module", read_only=True) module = ModuleLiteSerializer(source="issue_module.module", read_only=True)
labels = LabelLiteSerializer(read_only=True, many=True)
assignees = UserLiteSerializer(read_only=True, many=True)
state = StateLiteSerializer(read_only=True)
class Meta: class Meta:
model = Issue model = Issue
@ -349,4 +361,4 @@ class IssueExpandSerializer(BaseSerializer):
"updated_by", "updated_by",
"created_at", "created_at",
"updated_at", "updated_at",
] ]

View File

@ -11,10 +11,6 @@ class UserLiteSerializer(BaseSerializer):
"first_name", "first_name",
"last_name", "last_name",
"avatar", "avatar",
"is_bot",
"display_name", "display_name",
] ]
read_only_fields = [ read_only_fields = fields
"id",
"is_bot",
]

View File

@ -169,8 +169,8 @@ class ChangePasswordSerializer(serializers.Serializer):
Serializer for password change endpoint. Serializer for password change endpoint.
""" """
old_password = serializers.CharField(required=True) old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True, min_length=8)
confirm_password = serializers.CharField(required=True) confirm_password = serializers.CharField(required=True, min_length=8)
def validate(self, data): def validate(self, data):
if data.get("old_password") == data.get("new_password"): if data.get("old_password") == data.get("new_password"):
@ -187,9 +187,7 @@ class ChangePasswordSerializer(serializers.Serializer):
class ResetPasswordSerializer(serializers.Serializer): class ResetPasswordSerializer(serializers.Serializer):
model = User
""" """
Serializer for password change endpoint. Serializer for password change endpoint.
""" """
new_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True, min_length=8)

View File

@ -105,17 +105,21 @@ class ForgotPasswordEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
email = request.data.get("email") email = request.data.get("email")
if User.objects.filter(email=email).exists(): try:
user = User.objects.get(email=email) validate_email(email)
uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) except ValidationError:
token = PasswordResetTokenGenerator().make_token(user) return Response({"error": "Please enter a valid email"}, status=status.HTTP_400_BAD_REQUEST)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = get_tokens_for_user(user=user)
current_site = request.META.get("HTTP_ORIGIN") current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay( forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site user.first_name, user.email, uidb64, token, current_site
) )
return Response( return Response(
{"message": "Check your email to reset your password"}, {"message": "Check your email to reset your password"},
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
@ -130,14 +134,18 @@ class ResetPasswordEndpoint(BaseAPIView):
def post(self, request, uidb64, token): def post(self, request, uidb64, token):
try: try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64)) id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id) user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token): if not PasswordResetTokenGenerator().check_token(user, token):
return Response( return Response(
{"error": "Token is invalid"}, {"error": "Token is invalid"},
status=status.HTTP_401_UNAUTHORIZED, status=status.HTTP_401_UNAUTHORIZED,
) )
# Reset the password
serializer = ResetPasswordSerializer(data=request.data) serializer = ResetPasswordSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
# set_password also hashes the password that the user will get # set_password also hashes the password that the user will get
@ -145,9 +153,9 @@ class ResetPasswordEndpoint(BaseAPIView):
user.is_password_autoset = False user.is_password_autoset = False
user.save() user.save()
# Log the user in
# Generate access token for the user # Generate access token for the user
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
@ -166,7 +174,6 @@ class ResetPasswordEndpoint(BaseAPIView):
class ChangePasswordEndpoint(BaseAPIView): class ChangePasswordEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
serializer = ChangePasswordSerializer(data=request.data) serializer = ChangePasswordSerializer(data=request.data)
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)
if serializer.is_valid(): if serializer.is_valid():
if not user.check_password(serializer.data.get("old_password")): if not user.check_password(serializer.data.get("old_password")):
@ -218,16 +225,15 @@ class EmailCheckEndpoint(BaseAPIView):
] ]
def post(self, request): def post(self, request):
# get the email
# Check the instance registration # Check the instance registration
instance = Instance.objects.first() instance = Instance.objects.first()
if instance is None: if instance is None or not instance.is_setup_done:
return Response( return Response(
{"error": "Instance is not configured"}, {"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Get the configurations
instance_configuration = InstanceConfiguration.objects.values("key", "value") instance_configuration = InstanceConfiguration.objects.values("key", "value")
email = request.data.get("email", False) email = request.data.get("email", False)
@ -267,7 +273,7 @@ class EmailCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Create the user with default values
user = User.objects.create( user = User.objects.create(
email=email, email=email,
username=uuid.uuid4().hex, username=uuid.uuid4().hex,
@ -325,7 +331,7 @@ class EmailCheckEndpoint(BaseAPIView):
first_time=True, first_time=True,
) )
# Automatically send the email # Automatically send the email
return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_400_BAD_REQUEST) return Response({"is_password_autoset": user.is_password_autoset}, status=status.HTTP_200_OK)
# Existing user # Existing user
else: else:
if type == "magic_code": if type == "magic_code":

View File

@ -10,14 +10,12 @@ from django.utils import timezone
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework import status from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from sentry_sdk import capture_message from sentry_sdk import capture_message
# Module imports # Module imports
@ -33,7 +31,6 @@ from plane.settings.redis import redis_instance
from plane.license.models import InstanceConfiguration, Instance from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
from plane.bgtasks.magic_link_code_task import magic_link
from plane.bgtasks.user_count_task import update_user_instance_user_count from plane.bgtasks.user_count_task import update_user_instance_user_count
@ -49,6 +46,14 @@ class SignUpEndpoint(BaseAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def post(self, request): def post(self, request):
# Check if the instance configuration is done
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
instance_configuration = InstanceConfiguration.objects.values("key", "value") instance_configuration = InstanceConfiguration.objects.values("key", "value")
email = request.data.get("email", False) email = request.data.get("email", False)
@ -71,6 +76,7 @@ class SignUpEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# If the sign up is not enabled and the user does not have invite disallow him from creating the account
if ( if (
get_configuration_value( get_configuration_value(
instance_configuration, instance_configuration,
@ -124,6 +130,14 @@ class SignInEndpoint(BaseAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
def post(self, request): def post(self, request):
# Check if the instance configuration is done
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
email = request.data.get("email", False) email = request.data.get("email", False)
password = request.data.get("password", False) password = request.data.get("password", False)
@ -144,14 +158,6 @@ class SignInEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Check if the instance setup is done or not
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user # Get the user
user = User.objects.filter(email=email).first() user = User.objects.filter(email=email).first()
@ -288,6 +294,7 @@ class MagicSignInEndpoint(BaseAPIView):
] ]
def post(self, request): def post(self, request):
# Check if the instance configuration is done
instance = Instance.objects.first() instance = Instance.objects.first()
if instance is None or not instance.is_setup_done: if instance is None or not instance.is_setup_done:
return Response( return Response(

View File

@ -303,14 +303,6 @@ class OauthEndpoint(BaseAPIView):
instance_configuration = InstanceConfiguration.objects.values( instance_configuration = InstanceConfiguration.objects.values(
"key", "value" "key", "value"
) )
# Check if instance is registered or not
instance = Instance.objects.first()
if instance is None and not instance.is_setup_done:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
if ( if (
get_configuration_value( get_configuration_value(
instance_configuration, instance_configuration,

View File

@ -1,7 +1,8 @@
# Python imports # Python imports
import csv import csv
import io import io
import os import requests
import json
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
@ -17,8 +18,8 @@ from sentry_sdk import capture_exception
from plane.db.models import Issue from plane.db.models import Issue
from plane.utils.analytics_plot import build_graph_plot from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.license.models import InstanceConfiguration from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_email_configuration
row_mapping = { row_mapping = {
"state__name": "State", "state__name": "State",
@ -43,7 +44,7 @@ CYCLE_ID = "issue_cycle__cycle_id"
MODULE_ID = "issue_module__module_id" MODULE_ID = "issue_module__module_id"
def send_export_email(email, slug, csv_buffer): def send_export_email(email, slug, csv_buffer, rows):
"""Helper function to send export email.""" """Helper function to send export email."""
subject = "Your Export is ready" subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {}) html_content = render_to_string("emails/exports/analytics.html", {})
@ -55,47 +56,58 @@ def send_export_email(email, slug, csv_buffer):
instance_configuration = InstanceConfiguration.objects.filter( instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_" key__startswith="EMAIL_"
).values("key", "value") ).values("key", "value")
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration)
# Send the email if the users don't have smtp configured
if EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD:
# Check the instance registration
instance = Instance.objects.first()
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
payload = {
"email": email,
"slug": slug,
"rows": rows,
}
_ = requests.post(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/analytics/",
headers=headers,
json=payload,
)
return
connection = get_connection( connection = get_connection(
host=get_configuration_value( host=EMAIL_HOST,
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") port=int(EMAIL_PORT),
), username=EMAIL_HOST_USER,
port=int( password=EMAIL_HOST_PASSWORD,
get_configuration_value( use_tls=bool(EMAIL_USE_TLS),
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
)
),
username=get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER"),
),
password=get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD"),
),
use_tls=bool(
get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
),
) )
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject=subject, subject=subject,
body=text_content, body=text_content,
from_email=get_configuration_value( from_email=EMAIL_FROM,
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
),
to=[email], to=[email],
connection=connection, 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)
return
def get_assignee_details(slug, filters): def get_assignee_details(slug, filters):
@ -463,8 +475,11 @@ def analytic_export_task(email, data, slug):
) )
csv_buffer = generate_csv_from_rows(rows) csv_buffer = generate_csv_from_rows(rows)
send_export_email(email, slug, csv_buffer) send_export_email(email, slug, csv_buffer, rows)
return
except Exception as e: except Exception as e:
print(e)
if settings.DEBUG: if settings.DEBUG:
print(e) print(e)
capture_exception(e) capture_exception(e)
return

View File

@ -40,13 +40,10 @@ def forgot_password(first_name, email, uidb64, token, current_site):
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration(instance_configuration=instance_configuration)
# Send the email if the users don't have smtp configured # Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
# Check the instance registration # Check the instance registration
instance = Instance.objects.first() instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
# headers # headers
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -61,7 +58,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
} }
_ = requests.post( _ = requests.post(
f"{license_engine_base_url}/api/instances/users/forgot-password/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/forgot-password/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )

View File

@ -21,7 +21,6 @@ from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
def magic_link(email, key, token, current_site): def magic_link(email, key, token, current_site):
try: try:
instance_configuration = InstanceConfiguration.objects.filter( instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_" key__startswith="EMAIL_"
).values("key", "value") ).values("key", "value")
@ -36,13 +35,10 @@ def magic_link(email, key, token, current_site):
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration(instance_configuration=instance_configuration)
# Send the email if the users don't have smtp configured # Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
# Check the instance registration # Check the instance registration
instance = Instance.objects.first() instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-instance-id": instance.instance_id, "x-instance-id": instance.instance_id,
@ -55,7 +51,7 @@ def magic_link(email, key, token, current_site):
} }
_ = requests.post( _ = requests.post(
f"{license_engine_base_url}/api/instances/users/magic-code/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/magic-code/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )

View File

@ -32,13 +32,9 @@ def update_user_instance_user_count():
"x-api-key": instance.api_key, "x-api-key": instance.api_key,
} }
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")
# Update the license engine # Update the license engine
_ = requests.post( _ = requests.post(
f"{license_engine_base_url}/api/instances/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )

View File

@ -51,15 +51,10 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration(instance_configuration=instance_configuration)
# Send the email if the users don't have smtp configured # Send the email if the users don't have smtp configured
if not EMAIL_HOST or not EMAIL_HOST_USER or not EMAIL_HOST_PASSWORD: if not (EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD):
# Check the instance registration # Check the instance registration
instance = Instance.objects.first() instance = Instance.objects.first()
# send the emails through control center
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
if not license_engine_base_url:
raise Exception("License engine base url is required")
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-instance-id": instance.instance_id, "x-instance-id": instance.instance_id,
@ -73,7 +68,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
"email": email, "email": email,
} }
_ = requests.post( _ = requests.post(
f"{license_engine_base_url}/api/instances/users/workspace-invitation/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/workspace-invitation/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )

View File

@ -11,6 +11,7 @@ from django.utils import timezone
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.conf import settings
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -34,7 +35,6 @@ from plane.db.models import User
from plane.license.utils.encryption import encrypt_data from plane.license.utils.encryption import encrypt_data
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link from plane.bgtasks.magic_link_code_task import magic_link
from plane.license.utils.instance_value import get_configuration_value
class InstanceEndpoint(BaseAPIView): class InstanceEndpoint(BaseAPIView):
@ -57,25 +57,17 @@ class InstanceEndpoint(BaseAPIView):
# Load JSON content from the file # Load JSON content from the file
data = json.load(file) data = json.load(file)
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
if not license_engine_base_url:
raise Response(
{"error": "LICENSE_ENGINE_BASE_URL is required"},
status=status.HTTP_400_BAD_REQUEST,
)
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
payload = { payload = {
"instance_key": os.environ.get("INSTANCE_KEY"), "instance_key":settings.INSTANCE_KEY,
"version": data.get("version", 0.1), "version": data.get("version", 0.1),
"machine_signature": os.environ.get("MACHINE_SIGNATURE"), "machine_signature": os.environ.get("MACHINE_SIGNATURE"),
"user_count": User.objects.filter(is_bot=False).count(), "user_count": User.objects.filter(is_bot=False).count(),
} }
response = requests.post( response = requests.post(
f"{license_engine_base_url}/api/instances/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )
@ -130,6 +122,24 @@ class InstanceEndpoint(BaseAPIView):
serializer = InstanceSerializer(instance, data=request.data, partial=True) serializer = InstanceSerializer(instance, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
# Save the user in control center
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
# Update instance settings in the license engine
_ = requests.patch(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers,
data=json.dumps(
{
"is_support_required": serializer.data["is_support_required"],
"is_telemetry_enabled": serializer.data["is_telemetry_enabled"],
"version": serializer.data["version"],
}
),
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -251,7 +261,6 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if not email: if not email:
return Response( return Response(
{"error": "Please provide a valid email address"}, {"error": "Please provide a valid email address"},
@ -409,13 +418,6 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False)
if not license_engine_base_url:
return Response(
{"error": "License engine base url is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Save the user in control center # Save the user in control center
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -423,14 +425,14 @@ class AdminSetUserPasswordEndpoint(BaseAPIView):
"x-api-key": instance.api_key, "x-api-key": instance.api_key,
} }
_ = requests.patch( _ = requests.patch(
f"{license_engine_base_url}/api/instances/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers, headers=headers,
data=json.dumps({"is_setup_done": True}), data=json.dumps({"is_setup_done": True}),
) )
# Also register the user as admin # Also register the user as admin
_ = requests.post( _ = requests.post(
f"{license_engine_base_url}/api/instances/users/register/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
headers=headers, headers=headers,
data=json.dumps( data=json.dumps(
{ {
@ -472,24 +474,20 @@ class SignUpScreenVisitedEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) if not instance.is_signup_screen_visited:
instance.is_signup_screen_visited = True
if not license_engine_base_url: instance.save()
return Response( # set the headers
{"error": "License engine base url is required"}, headers = {
status=status.HTTP_400_BAD_REQUEST, "Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
# create the payload
payload = {"is_signup_screen_visited": True}
_ = requests.patch(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers,
data=json.dumps(payload),
) )
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
payload = {"is_signup_screen_visited": True}
response = requests.patch(
f"{license_engine_base_url}/api/instances/",
headers=headers,
data=json.dumps(payload),
)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -6,6 +6,7 @@ import requests
# Django imports # Django imports
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone from django.utils import timezone
from django.conf import settings
# Module imports # Module imports
from plane.license.models import Instance from plane.license.models import Instance
@ -30,31 +31,22 @@ class Command(BaseCommand):
data = json.load(file) data = json.load(file)
machine_signature = options.get("machine_signature", False) machine_signature = options.get("machine_signature", False)
instance_key = os.environ.get("INSTANCE_KEY", False)
# Raise an exception if the admin email is not provided
if not instance_key:
raise CommandError("INSTANCE_KEY is required")
if not machine_signature: if not machine_signature:
raise CommandError("Machine signature is required") raise CommandError("Machine signature is required")
license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL")
if not license_engine_base_url:
raise CommandError("LICENSE_ENGINE_BASE_URL is required")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
payload = { payload = {
"instance_key": instance_key, "instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1), "version": data.get("version", 0.1),
"machine_signature": machine_signature, "machine_signature": machine_signature,
"user_count": User.objects.filter(is_bot=False).count(), "user_count": User.objects.filter(is_bot=False).count(),
} }
response = requests.post( response = requests.post(
f"{license_engine_base_url}/api/instances/", f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers, headers=headers,
data=json.dumps(payload), data=json.dumps(payload),
) )

View File

@ -324,4 +324,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
# Posthog settings # Posthog settings
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False) POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# License engine base url
LICENSE_ENGINE_BASE_URL = os.environ.get("LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so")
# instance key
INSTANCE_KEY = os.environ.get("INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3")

View File

@ -149,20 +149,20 @@
padding-top: 10px !important; padding-top: 10px !important;
} }
.r13-o { .r13-o {
border-bottom-color: #efefef !important; border-bottom-color: #d9e4ff !important;
border-bottom-width: 1px !important; border-bottom-width: 1px !important;
border-left-color: #efefef !important; border-left-color: #d9e4ff !important;
border-left-width: 1px !important; border-left-width: 1px !important;
border-right-color: #efefef !important; border-right-color: #d9e4ff !important;
border-right-width: 1px !important; border-right-width: 1px !important;
border-style: solid !important; border-style: solid !important;
border-top-color: #efefef !important; border-top-color: #d9e4ff !important;
border-top-width: 1px !important; border-top-width: 1px !important;
margin: 0 auto 0 0 !important; margin: 0 auto 0 0 !important;
width: 100% !important; width: 100% !important;
} }
.r14-i { .r14-i {
background-color: #e3e6f1 !important; background-color: #ecf1ff !important;
padding-bottom: 10px !important; padding-bottom: 10px !important;
padding-left: 10px !important; padding-left: 10px !important;
padding-right: 10px !important; padding-right: 10px !important;
@ -225,16 +225,11 @@
padding-top: 5px !important; padding-top: 5px !important;
} }
.r24-o { .r24-o {
border-style: solid !important;
margin-right: 8px !important;
width: 32px !important;
}
.r25-o {
border-style: solid !important; border-style: solid !important;
margin-right: 0px !important; margin-right: 0px !important;
width: 32px !important; width: 32px !important;
} }
.r26-i { .r25-i {
padding-bottom: 0px !important; padding-bottom: 0px !important;
padding-top: 5px !important; padding-top: 5px !important;
text-align: center !important; text-align: center !important;
@ -664,17 +659,17 @@
width="100%" width="100%"
class="r13-o" class="r13-o"
style=" style="
background-color: #e3e6f1; background-color: #ecf1ff;
border-bottom-color: #efefef; border-bottom-color: #d9e4ff;
border-bottom-width: 1px; border-bottom-width: 1px;
border-collapse: separate; border-collapse: separate;
border-left-color: #efefef; border-left-color: #d9e4ff;
border-left-width: 1px; border-left-width: 1px;
border-radius: 5px; border-radius: 5px;
border-right-color: #efefef; border-right-color: #d9e4ff;
border-right-width: 1px; border-right-width: 1px;
border-style: solid; border-style: solid;
border-top-color: #efefef; border-top-color: #d9e4ff;
border-top-width: 1px; border-top-width: 1px;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
@ -690,7 +685,7 @@
font-family: georgia, serif; font-family: georgia, serif;
font-size: 16px; font-size: 16px;
word-break: break-word; word-break: break-word;
background-color: #e3e6f1; background-color: #ecf1ff;
border-radius: 4px; border-radius: 4px;
line-height: 3; line-height: 3;
padding-bottom: 10px; padding-bottom: 10px;
@ -714,10 +709,10 @@
<p style="margin: 0"> <p style="margin: 0">
<span <span
style=" style="
color: #716c6c; color: #5f5e5e;
font-family: Arial, font-family: Arial,
helvetica, sans-serif; helvetica, sans-serif;
font-size: 15px; font-size: 14px;
" "
>Please copy and paste this >Please copy and paste this
on the screen where you on the screen where you
@ -1251,13 +1246,14 @@
role="presentation" role="presentation"
width="100%" width="100%"
class="r22-o" class="r22-o"
class="r24-o"
style=" style="
table-layout: fixed;
width: 100%;
"
> >
<tr> <tr>
<td <td
class="r23-i" class="r23-i"
class="r17-i"
style=" style="
font-size: 0px; font-size: 0px;
line-height: 0px; line-height: 0px;
@ -1309,7 +1305,7 @@
border="0" border="0"
role="presentation" role="presentation"
width="100%" width="100%"
class="r25-o" class="r24-o"
style=" style="
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
@ -1441,7 +1437,7 @@
<td <td
align="center" align="center"
valign="top" valign="top"
class="r26-i nl2go-default-textstyle" class="r25-i nl2go-default-textstyle"
style=" style="
color: #3b3f44; color: #3b3f44;
font-family: georgia, serif; font-family: georgia, serif;

View File

@ -1,9 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
Dear {{username}},<br/>
Your requested Issue's data has been successfully exported from Plane. The export includes all relevant information about issues you requested from your selected projects.</br>
Please find the attachment and download the CSV file. If you have any questions or need further assistance, please don't hesitate to contact our support team at <a href = "mailto: engineering@plane.com">engineering@plane.so</a>. We're here to help!</br>
Thank you for using Plane. We hope this export will aid you in effectively managing your projects.</br>
Regards,
Team Plane
</html>

View File

@ -71,7 +71,6 @@ services:
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
- WEB_URL=$SERVICE_FQDN_PLANE_8082 - WEB_URL=$SERVICE_FQDN_PLANE_8082
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
depends_on: depends_on:
- plane-db - plane-db
- plane-redis - plane-redis
@ -117,7 +116,6 @@ services:
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
depends_on: depends_on:
- api - api
- plane-db - plane-db
@ -164,7 +162,6 @@ services:
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"}
depends_on: depends_on:
- api - api
- plane-db - plane-db

View File

@ -5,15 +5,16 @@ x-app-env : &app-env
- NGINX_PORT=${NGINX_PORT:-80} - NGINX_PORT=${NGINX_PORT:-80}
- WEB_URL=${WEB_URL:-http://localhost} - WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0} - DEBUG=${DEBUG:-0}
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} - NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} # deprecated
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated
- SENTRY_DSN=${SENTRY_DSN:-""} - SENTRY_DSN=${SENTRY_DSN:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-""}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-""}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
- DOCKERIZED=${DOCKERIZED:-1} # deprecated - DOCKERIZED=${DOCKERIZED:-1} # deprecated
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""} - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
- ADMIN_EMAIL=${ADMIN_EMAIL:-""}
- LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-""}
# Gunicorn Workers # Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
#DB SETTINGS #DB SETTINGS
@ -28,12 +29,12 @@ x-app-env : &app-env
- REDIS_HOST=${REDIS_HOST:-plane-redis} - REDIS_HOST=${REDIS_HOST:-plane-redis}
- REDIS_PORT=${REDIS_PORT:-6379} - REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/} - REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
# EMAIL SETTINGS # EMAIL SETTINGS - Deprecated can be configured through admin panel
- EMAIL_HOST=${EMAIL_HOST:-""} - EMAIL_HOST=${EMAIL_HOST:-""}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-""} - EMAIL_HOST_USER=${EMAIL_HOST_USER:-""}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""} - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""}
- EMAIL_PORT=${EMAIL_PORT:-587} - EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_FROM=${EMAIL_FROM:-"Team Plane &lt;team@mailer.plane.so&gt;"} - EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-1} - EMAIL_USE_TLS=${EMAIL_USE_TLS:-1}
- EMAIL_USE_SSL=${EMAIL_USE_SSL:-0} - EMAIL_USE_SSL=${EMAIL_USE_SSL:-0}
- DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so} - DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so}
@ -42,10 +43,11 @@ x-app-env : &app-env
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1} - OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"} - OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"} - GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
# LOGIN/SIGNUP SETTINGS # LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
- ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1} - ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1}
- ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0}
# Application secret
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
# DATA STORE SETTINGS # DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1} - USE_MINIO=${USE_MINIO:-1}

View File

@ -10,10 +10,12 @@ DEBUG=0
NEXT_PUBLIC_ENABLE_OAUTH=0 NEXT_PUBLIC_ENABLE_OAUTH=0
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
SENTRY_DSN="" SENTRY_DSN=""
SENTRY_ENVIRONMENT="production"
GOOGLE_CLIENT_ID=""
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET="" GITHUB_CLIENT_SECRET=""
DOCKERIZED=1 # deprecated DOCKERIZED=1 # deprecated
CORS_ALLOWED_ORIGINS="http://localhost" CORS_ALLOWED_ORIGINS="http://localhost"
SENTRY_ENVIRONMENT="production"
#DB SETTINGS #DB SETTINGS
PGHOST=plane-db PGHOST=plane-db
@ -34,7 +36,7 @@ EMAIL_HOST=""
EMAIL_HOST_USER="" EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD="" EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_FROM="Team Plane &lt;team@mailer.plane.so&gt;" EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS=1 EMAIL_USE_TLS=1
EMAIL_USE_SSL=0 EMAIL_USE_SSL=0
@ -63,9 +65,3 @@ FILE_SIZE_LIMIT=5242880
# Gunicorn Workers # Gunicorn Workers
GUNICORN_WORKERS=2 GUNICORN_WORKERS=2
# Admin Email
ADMIN_EMAIL=""
# License Engine url
LICENSE_ENGINE_BASE_URL=""