From 79fa860b28416be4ce303236fc3b8d0043528bb6 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:48:40 +0530 Subject: [PATCH] chore: instance (#2955) --- apiserver/.env.example | 6 +- apiserver/plane/api/serializers/issue.py | 24 ++++-- apiserver/plane/api/serializers/user.py | 6 +- apiserver/plane/app/serializers/user.py | 8 +- apiserver/plane/app/views/auth_extended.py | 32 ++++--- apiserver/plane/app/views/authentication.py | 29 ++++--- apiserver/plane/app/views/oauth.py | 8 -- .../plane/bgtasks/analytic_plot_export.py | 85 +++++++++++-------- .../plane/bgtasks/forgot_password_task.py | 7 +- .../plane/bgtasks/magic_link_code_task.py | 8 +- apiserver/plane/bgtasks/user_count_task.py | 6 +- .../bgtasks/workspace_invitation_task.py | 9 +- apiserver/plane/license/api/views/instance.py | 78 +++++++++-------- .../management/commands/register_instance.py | 16 +--- apiserver/plane/settings/common.py | 8 +- .../templates/emails/auth/magic_signin.html | 42 +++++---- .../templates/emails/exports/issues.html | 9 -- deploy/coolify/coolify-docker-compose.yml | 3 - deploy/selfhost/docker-compose.yml | 18 ++-- deploy/selfhost/variables.env | 12 +-- 20 files changed, 203 insertions(+), 211 deletions(-) delete mode 100644 apiserver/templates/emails/exports/issues.html diff --git a/apiserver/.env.example b/apiserver/.env.example index ace1e07b1..37178b398 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -14,6 +14,11 @@ PGHOST="plane-db" PGDATABASE="plane" DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE} +# Oauth variables +GOOGLE_CLIENT_ID="" +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" + # Redis Settings REDIS_HOST="plane-redis" REDIS_PORT="6379" @@ -50,7 +55,6 @@ NGINX_PORT=80 # SignUps ENABLE_SIGNUP="1" - # Enable Email/Password Signup ENABLE_EMAIL_PASSWORD="1" diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 2dbdddfc6..10b3a4f85 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -21,7 +21,8 @@ from plane.db.models import ( from .base import BaseSerializer from .cycle import CycleSerializer, CycleLiteSerializer from .module import ModuleSerializer, ModuleLiteSerializer - +from .user import UserLiteSerializer +from .state import StateLiteSerializer class IssueSerializer(BaseSerializer): assignees = serializers.ListField( @@ -331,12 +332,23 @@ class ModuleIssueSerializer(BaseSerializer): ] -class IssueExpandSerializer(BaseSerializer): - # Serialize the related cycle. It's a OneToOne relation. - cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True) +class LabelLiteSerializer(BaseSerializer): - # 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) + labels = LabelLiteSerializer(read_only=True, many=True) + assignees = UserLiteSerializer(read_only=True, many=True) + state = StateLiteSerializer(read_only=True) class Meta: model = Issue @@ -349,4 +361,4 @@ class IssueExpandSerializer(BaseSerializer): "updated_by", "created_at", "updated_at", - ] \ No newline at end of file + ] diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index e5a77da93..42b6c3967 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -11,10 +11,6 @@ class UserLiteSerializer(BaseSerializer): "first_name", "last_name", "avatar", - "is_bot", "display_name", ] - read_only_fields = [ - "id", - "is_bot", - ] \ No newline at end of file + read_only_fields = fields \ No newline at end of file diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index 5c9c69e5c..1b94758e8 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -169,8 +169,8 @@ class ChangePasswordSerializer(serializers.Serializer): Serializer for password change endpoint. """ old_password = serializers.CharField(required=True) - new_password = serializers.CharField(required=True) - confirm_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True, min_length=8) + confirm_password = serializers.CharField(required=True, min_length=8) def validate(self, data): if data.get("old_password") == data.get("new_password"): @@ -187,9 +187,7 @@ class ChangePasswordSerializer(serializers.Serializer): class ResetPasswordSerializer(serializers.Serializer): - model = User - """ Serializer for password change endpoint. """ - new_password = serializers.CharField(required=True) + new_password = serializers.CharField(required=True, min_length=8) diff --git a/apiserver/plane/app/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py index 1de511f89..2dc0fa983 100644 --- a/apiserver/plane/app/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -105,17 +105,21 @@ class ForgotPasswordEndpoint(BaseAPIView): def post(self, request): email = request.data.get("email") - if User.objects.filter(email=email).exists(): - user = User.objects.get(email=email) - uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) - token = PasswordResetTokenGenerator().make_token(user) + try: + validate_email(email) + except ValidationError: + 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") - + # send the forgot password email forgot_password.delay( user.first_name, user.email, uidb64, token, current_site ) - return Response( {"message": "Check your email to reset your password"}, status=status.HTTP_200_OK, @@ -130,14 +134,18 @@ class ResetPasswordEndpoint(BaseAPIView): def post(self, request, uidb64, token): try: + # Decode the id from the uidb64 id = smart_str(urlsafe_base64_decode(uidb64)) user = User.objects.get(id=id) + + # check if the token is valid for the user if not PasswordResetTokenGenerator().check_token(user, token): return Response( {"error": "Token is invalid"}, status=status.HTTP_401_UNAUTHORIZED, ) + # Reset the password serializer = ResetPasswordSerializer(data=request.data) if serializer.is_valid(): # set_password also hashes the password that the user will get @@ -145,9 +153,9 @@ class ResetPasswordEndpoint(BaseAPIView): user.is_password_autoset = False user.save() + # Log the user in # Generate access token for the user access_token, refresh_token = get_tokens_for_user(user) - data = { "access_token": access_token, "refresh_token": refresh_token, @@ -166,7 +174,6 @@ class ResetPasswordEndpoint(BaseAPIView): class ChangePasswordEndpoint(BaseAPIView): def post(self, request): serializer = ChangePasswordSerializer(data=request.data) - user = User.objects.get(pk=request.user.id) if serializer.is_valid(): if not user.check_password(serializer.data.get("old_password")): @@ -218,16 +225,15 @@ class EmailCheckEndpoint(BaseAPIView): ] def post(self, request): - # get the email - # Check the instance registration instance = Instance.objects.first() - if instance is None: + 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 configurations instance_configuration = InstanceConfiguration.objects.values("key", "value") email = request.data.get("email", False) @@ -267,7 +273,7 @@ class EmailCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - + # Create the user with default values user = User.objects.create( email=email, username=uuid.uuid4().hex, @@ -325,7 +331,7 @@ class EmailCheckEndpoint(BaseAPIView): first_time=True, ) # 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 else: if type == "magic_code": diff --git a/apiserver/plane/app/views/authentication.py b/apiserver/plane/app/views/authentication.py index 87118b7d5..487cdae48 100644 --- a/apiserver/plane/app/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -10,14 +10,12 @@ from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.conf import settings -from django.contrib.auth.hashers import make_password # Third party imports from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status from rest_framework_simplejwt.tokens import RefreshToken - from sentry_sdk import capture_message # Module imports @@ -33,7 +31,6 @@ from plane.settings.redis import redis_instance from plane.license.models import InstanceConfiguration, Instance from plane.license.utils.instance_value import get_configuration_value 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 @@ -49,6 +46,14 @@ class SignUpEndpoint(BaseAPIView): permission_classes = (AllowAny,) 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") email = request.data.get("email", False) @@ -71,6 +76,7 @@ class SignUpEndpoint(BaseAPIView): 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 ( get_configuration_value( instance_configuration, @@ -124,6 +130,14 @@ class SignInEndpoint(BaseAPIView): permission_classes = (AllowAny,) 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) password = request.data.get("password", False) @@ -144,14 +158,6 @@ class SignInEndpoint(BaseAPIView): 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 user = User.objects.filter(email=email).first() @@ -288,6 +294,7 @@ class MagicSignInEndpoint(BaseAPIView): ] 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( diff --git a/apiserver/plane/app/views/oauth.py b/apiserver/plane/app/views/oauth.py index 0dd7fbaf0..826ec4b05 100644 --- a/apiserver/plane/app/views/oauth.py +++ b/apiserver/plane/app/views/oauth.py @@ -303,14 +303,6 @@ class OauthEndpoint(BaseAPIView): instance_configuration = InstanceConfiguration.objects.values( "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 ( get_configuration_value( instance_configuration, diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index e32c2fd68..3d5724605 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -1,7 +1,8 @@ # Python imports import csv import io -import os +import requests +import json # Django imports 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.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters -from plane.license.models import InstanceConfiguration -from plane.license.utils.instance_value import get_configuration_value +from plane.license.models import InstanceConfiguration, Instance +from plane.license.utils.instance_value import get_email_configuration row_mapping = { "state__name": "State", @@ -43,7 +44,7 @@ CYCLE_ID = "issue_cycle__cycle_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.""" subject = "Your Export is ready" 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( key__startswith="EMAIL_" ).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( - host=get_configuration_value( - instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST") - ), - port=int( - get_configuration_value( - 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"), - ) - ), + host=EMAIL_HOST, + port=int(EMAIL_PORT), + username=EMAIL_HOST_USER, + password=EMAIL_HOST_PASSWORD, + use_tls=bool(EMAIL_USE_TLS), ) msg = EmailMultiAlternatives( subject=subject, body=text_content, - from_email=get_configuration_value( - instance_configuration, - "EMAIL_FROM", - os.environ.get("EMAIL_FROM", "Team Plane "), - ), + from_email=EMAIL_FROM, to=[email], connection=connection, ) msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue()) msg.send(fail_silently=False) + return def get_assignee_details(slug, filters): @@ -463,8 +475,11 @@ def analytic_export_task(email, data, slug): ) 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: + print(e) if settings.DEBUG: print(e) capture_exception(e) + return diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 5348a05da..33cd40dc8 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -40,13 +40,10 @@ def forgot_password(first_name, email, uidb64, token, current_site): ) = get_email_configuration(instance_configuration=instance_configuration) # 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 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", @@ -61,7 +58,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): } _ = 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, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 2caec2b60..a152b4c7f 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -21,7 +21,6 @@ from plane.license.utils.instance_value import get_email_configuration @shared_task def magic_link(email, key, token, current_site): try: - instance_configuration = InstanceConfiguration.objects.filter( key__startswith="EMAIL_" ).values("key", "value") @@ -36,13 +35,10 @@ def magic_link(email, key, token, current_site): ) = get_email_configuration(instance_configuration=instance_configuration) # 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 instance = Instance.objects.first() - # send the emails through control center - license_engine_base_url = os.environ.get("LICENSE_ENGINE_BASE_URL", False) - headers = { "Content-Type": "application/json", "x-instance-id": instance.instance_id, @@ -55,7 +51,7 @@ def magic_link(email, key, token, current_site): } _ = 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, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/user_count_task.py b/apiserver/plane/bgtasks/user_count_task.py index f93c3364e..dd8b19e7d 100644 --- a/apiserver/plane/bgtasks/user_count_task.py +++ b/apiserver/plane/bgtasks/user_count_task.py @@ -32,13 +32,9 @@ def update_user_instance_user_count(): "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 _ = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index 2cbfdaff7..fe8708ed7 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -51,15 +51,10 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): ) = get_email_configuration(instance_configuration=instance_configuration) # 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 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 = { "Content-Type": "application/json", "x-instance-id": instance.instance_id, @@ -73,7 +68,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): "email": email, } _ = 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, data=json.dumps(payload), ) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 7edc50b27..7dbc5e4b4 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.contrib.auth.hashers import make_password from django.core.validators import validate_email from django.core.exceptions import ValidationError +from django.conf import settings # Third party imports 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.settings.redis import redis_instance from plane.bgtasks.magic_link_code_task import magic_link -from plane.license.utils.instance_value import get_configuration_value class InstanceEndpoint(BaseAPIView): @@ -57,25 +57,17 @@ class InstanceEndpoint(BaseAPIView): # Load JSON content from the 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"} payload = { - "instance_key": os.environ.get("INSTANCE_KEY"), + "instance_key":settings.INSTANCE_KEY, "version": data.get("version", 0.1), "machine_signature": os.environ.get("MACHINE_SIGNATURE"), "user_count": User.objects.filter(is_bot=False).count(), } response = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) @@ -130,6 +122,24 @@ class InstanceEndpoint(BaseAPIView): serializer = InstanceSerializer(instance, data=request.data, partial=True) if serializer.is_valid(): 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.errors, status=status.HTTP_400_BAD_REQUEST) @@ -251,7 +261,6 @@ class AdminMagicSignInGenerateEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - if not email: return Response( {"error": "Please provide a valid email address"}, @@ -409,13 +418,6 @@ class AdminSetUserPasswordEndpoint(BaseAPIView): 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 headers = { "Content-Type": "application/json", @@ -423,14 +425,14 @@ class AdminSetUserPasswordEndpoint(BaseAPIView): "x-api-key": instance.api_key, } _ = requests.patch( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps({"is_setup_done": True}), ) # Also register the user as admin _ = requests.post( - f"{license_engine_base_url}/api/instances/users/register/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/", headers=headers, data=json.dumps( { @@ -472,24 +474,20 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): 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, + if not instance.is_signup_screen_visited: + instance.is_signup_screen_visited = True + instance.save() + # set the headers + headers = { + "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) diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index b486ecb0b..0970d8093 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -6,6 +6,7 @@ import requests # Django imports from django.core.management.base import BaseCommand, CommandError from django.utils import timezone +from django.conf import settings # Module imports from plane.license.models import Instance @@ -30,31 +31,22 @@ class Command(BaseCommand): data = json.load(file) 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: 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"} payload = { - "instance_key": instance_key, + "instance_key": settings.INSTANCE_KEY, "version": data.get("version", 0.1), "machine_signature": machine_signature, "user_count": User.objects.filter(is_bot=False).count(), } response = requests.post( - f"{license_engine_base_url}/api/instances/", + f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", headers=headers, data=json.dumps(payload), ) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index ba2c941ef..3eca1dee8 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -324,4 +324,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1 # Posthog settings POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False) -POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) \ No newline at end of file +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") diff --git a/apiserver/templates/emails/auth/magic_signin.html b/apiserver/templates/emails/auth/magic_signin.html index 4e47487bd..ba469db7e 100644 --- a/apiserver/templates/emails/auth/magic_signin.html +++ b/apiserver/templates/emails/auth/magic_signin.html @@ -149,20 +149,20 @@ padding-top: 10px !important; } .r13-o { - border-bottom-color: #efefef !important; + border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; - border-left-color: #efefef !important; + border-left-color: #d9e4ff !important; border-left-width: 1px !important; - border-right-color: #efefef !important; + border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; - border-top-color: #efefef !important; + border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { - background-color: #e3e6f1 !important; + background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; @@ -225,16 +225,11 @@ padding-top: 5px !important; } .r24-o { - border-style: solid !important; - margin-right: 8px !important; - width: 32px !important; - } - .r25-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } - .r26-i { + .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; @@ -664,17 +659,17 @@ width="100%" class="r13-o" style=" - background-color: #e3e6f1; - border-bottom-color: #efefef; + background-color: #ecf1ff; + border-bottom-color: #d9e4ff; border-bottom-width: 1px; border-collapse: separate; - border-left-color: #efefef; + border-left-color: #d9e4ff; border-left-width: 1px; border-radius: 5px; - border-right-color: #efefef; + border-right-color: #d9e4ff; border-right-width: 1px; border-style: solid; - border-top-color: #efefef; + border-top-color: #d9e4ff; border-top-width: 1px; table-layout: fixed; width: 100%; @@ -690,7 +685,7 @@ font-family: georgia, serif; font-size: 16px; word-break: break-word; - background-color: #e3e6f1; + background-color: #ecf1ff; border-radius: 4px; line-height: 3; padding-bottom: 10px; @@ -714,10 +709,10 @@

Please copy and paste this on the screen where you @@ -1251,13 +1246,14 @@ role="presentation" width="100%" class="r22-o" - class="r24-o" style=" + table-layout: fixed; + width: 100%; + " > - - Dear {{username}},
- 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.
- 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 engineering@plane.so. We're here to help!
- Thank you for using Plane. We hope this export will aid you in effectively managing your projects.
- Regards, - Team Plane - diff --git a/deploy/coolify/coolify-docker-compose.yml b/deploy/coolify/coolify-docker-compose.yml index 6dd361d34..2a21c61a8 100644 --- a/deploy/coolify/coolify-docker-compose.yml +++ b/deploy/coolify/coolify-docker-compose.yml @@ -71,7 +71,6 @@ services: - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - WEB_URL=$SERVICE_FQDN_PLANE_8082 - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - plane-db - plane-redis @@ -117,7 +116,6 @@ services: - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - api - plane-db @@ -164,7 +162,6 @@ services: - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} - - LICENSE_ENGINE_BASE_URL=${LICENSE_ENGINE_BASE_URL:-"https://control-center.plane.so"} depends_on: - api - plane-db diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 7eec3fd5a..ba0c28827 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -5,15 +5,16 @@ x-app-env : &app-env - NGINX_PORT=${NGINX_PORT:-80} - WEB_URL=${WEB_URL:-http://localhost} - DEBUG=${DEBUG:-0} - - NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} - - NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} + - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated + - 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_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"} + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-""} + - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-""} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - DOCKERIZED=${DOCKERIZED:-1} # deprecated - 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:-2} #DB SETTINGS @@ -28,12 +29,12 @@ x-app-env : &app-env - REDIS_HOST=${REDIS_HOST:-plane-redis} - REDIS_PORT=${REDIS_PORT:-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_USER=${EMAIL_HOST_USER:-""} - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""} - EMAIL_PORT=${EMAIL_PORT:-587} - - EMAIL_FROM=${EMAIL_FROM:-"Team Plane <team@mailer.plane.so>"} + - EMAIL_FROM=${EMAIL_FROM:-"Team Plane "} - EMAIL_USE_TLS=${EMAIL_USE_TLS:-1} - EMAIL_USE_SSL=${EMAIL_USE_SSL:-0} - 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_KEY=${OPENAI_API_KEY:-"sk-"} - 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_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1} - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} + # Application secret - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} # DATA STORE SETTINGS - USE_MINIO=${USE_MINIO:-1} diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 10ca42879..6be9ca2f4 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -10,10 +10,12 @@ DEBUG=0 NEXT_PUBLIC_ENABLE_OAUTH=0 NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces SENTRY_DSN="" +SENTRY_ENVIRONMENT="production" +GOOGLE_CLIENT_ID="" +GITHUB_CLIENT_ID="" GITHUB_CLIENT_SECRET="" DOCKERIZED=1 # deprecated CORS_ALLOWED_ORIGINS="http://localhost" -SENTRY_ENVIRONMENT="production" #DB SETTINGS PGHOST=plane-db @@ -34,7 +36,7 @@ EMAIL_HOST="" EMAIL_HOST_USER="" EMAIL_HOST_PASSWORD="" EMAIL_PORT=587 -EMAIL_FROM="Team Plane <team@mailer.plane.so>" +EMAIL_FROM="Team Plane " EMAIL_USE_TLS=1 EMAIL_USE_SSL=0 @@ -63,9 +65,3 @@ FILE_SIZE_LIMIT=5242880 # Gunicorn Workers GUNICORN_WORKERS=2 - -# Admin Email -ADMIN_EMAIL="" - -# License Engine url -LICENSE_ENGINE_BASE_URL="" \ No newline at end of file