This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,4 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
@@ -0,0 +1,5 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .instance import InstanceAdminPermission
@@ -0,0 +1,18 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Third party imports
from rest_framework.permissions import BasePermission
# Module imports
from plane.license.models import Instance, InstanceAdmin
class InstanceAdminPermission(BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous:
return False
instance = Instance.objects.first()
return InstanceAdmin.objects.filter(role__gte=15, instance=instance, user=request.user).exists()
@@ -0,0 +1,9 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .instance import InstanceSerializer
from .configuration import InstanceConfigurationSerializer
from .admin import InstanceAdminSerializer, InstanceAdminMeSerializer
from .workspace import WorkspaceSerializer
@@ -0,0 +1,42 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Module imports
from .base import BaseSerializer
from plane.db.models import User
from plane.app.serializers import UserAdminLiteSerializer
from plane.license.models import InstanceAdmin
class InstanceAdminMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"avatar",
"avatar_url",
"cover_image",
"date_joined",
"display_name",
"email",
"first_name",
"last_name",
"is_active",
"is_bot",
"is_email_verified",
"user_timezone",
"username",
"is_password_autoset",
"is_email_verified",
]
read_only_fields = fields
class InstanceAdminSerializer(BaseSerializer):
user_detail = UserAdminLiteSerializer(source="user", read_only=True)
class Meta:
model = InstanceAdmin
fields = "__all__"
read_only_fields = ["id", "instance", "user"]
@@ -0,0 +1,9 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from rest_framework import serializers
class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True)
@@ -0,0 +1,21 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .base import BaseSerializer
from plane.license.models import InstanceConfiguration
from plane.license.utils.encryption import decrypt_data
class InstanceConfigurationSerializer(BaseSerializer):
class Meta:
model = InstanceConfiguration
fields = "__all__"
def to_representation(self, instance):
data = super().to_representation(instance)
# Decrypt secrets value
if instance.is_encrypted and instance.value is not None:
data["value"] = decrypt_data(instance.value)
return data
@@ -0,0 +1,17 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Module imports
from plane.license.models import Instance
from plane.app.serializers import BaseSerializer
from plane.app.serializers import UserAdminLiteSerializer
class InstanceSerializer(BaseSerializer):
primary_owner_details = UserAdminLiteSerializer(source="primary_owner", read_only=True)
class Meta:
model = Instance
fields = "__all__"
read_only_fields = ["id", "email", "last_checked_at", "is_setup_done"]
@@ -0,0 +1,12 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .base import BaseSerializer
from plane.db.models import User
class UserLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = ["id", "email", "first_name", "last_name"]
@@ -0,0 +1,41 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Third Party Imports
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserLiteSerializer
from plane.db.models import Workspace
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
class WorkspaceSerializer(BaseSerializer):
owner = UserLiteSerializer(read_only=True)
logo_url = serializers.CharField(read_only=True)
total_projects = serializers.IntegerField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
def validate_slug(self, value):
# Check if the slug is restricted
if value in RESTRICTED_WORKSPACE_SLUGS:
raise serializers.ValidationError("Slug is not valid")
# Check uniqueness case-insensitively
if Workspace.objects.filter(slug__iexact=value).exists():
raise serializers.ValidationError("Slug is already in use")
return value
class Meta:
model = Workspace
fields = "__all__"
read_only_fields = [
"id",
"created_by",
"updated_by",
"created_at",
"updated_at",
"owner",
"logo_url",
]
@@ -0,0 +1,28 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .instance import InstanceEndpoint, SignUpScreenVisitedEndpoint
from .configuration import (
EmailCredentialCheckEndpoint,
InstanceConfigurationEndpoint,
DisableEmailFeatureEndpoint,
)
from .admin import (
InstanceAdminEndpoint,
InstanceAdminSignInEndpoint,
InstanceAdminSignUpEndpoint,
InstanceAdminUserMeEndpoint,
InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
)
from .workspace import (
InstanceWorkSpaceAvailabilityCheckEndpoint,
InstanceWorkSpaceEndpoint,
)
@@ -0,0 +1,397 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
from urllib.parse import urlencode, urljoin
import uuid
# Django imports
from django.http import HttpResponseRedirect
from django.views import View
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from django.contrib.auth import logout
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
# Module imports
from .base import BaseAPIView
from plane.license.api.permissions import InstanceAdminPermission
from plane.license.api.serializers import (
InstanceAdminMeSerializer,
InstanceAdminSerializer,
)
from plane.license.models import Instance, InstanceAdmin
from plane.db.models import User, Profile
from plane.utils.cache import cache_response, invalidate_cache
from plane.authentication.utils.login import user_login
from plane.authentication.utils.host import base_host, user_ip
from plane.authentication.utils.password_validation import is_password_policy_valid
from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES,
AuthenticationException,
)
from plane.utils.ip_address import get_client_ip
from plane.utils.path_validator import get_safe_redirect_url
class InstanceAdminEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
@invalidate_cache(path="/api/instances/", user=False)
# Create an instance admin
def post(self, request):
email = request.data.get("email", False)
role = request.data.get("role", 20)
if not email:
return Response({"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST)
instance = Instance.objects.first()
if instance is None:
return Response(
{"error": "Instance is not registered yet"},
status=status.HTTP_403_FORBIDDEN,
)
# Fetch the user
user = User.objects.get(email=email)
instance_admin = InstanceAdmin.objects.create(instance=instance, user=user, role=role)
serializer = InstanceAdminSerializer(instance_admin)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@cache_response(60 * 60 * 2, user=False)
def get(self, request):
instance = Instance.objects.first()
if instance is None:
return Response(
{"error": "Instance is not registered yet"},
status=status.HTTP_403_FORBIDDEN,
)
instance_admins = InstanceAdmin.objects.filter(instance=instance)
serializer = InstanceAdminSerializer(instance_admins, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(path="/api/instances/", user=False)
def delete(self, request, pk):
instance = Instance.objects.first()
InstanceAdmin.objects.filter(instance=instance, pk=pk).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class InstanceAdminSignUpEndpoint(View):
permission_classes = [AllowAny]
@invalidate_cache(path="/api/instances/", user=False)
def post(self, request):
# Check instance first
instance = Instance.objects.first()
if instance is None:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# check if the instance has already an admin registered
if InstanceAdmin.objects.first():
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_ALREADY_EXIST"],
error_message="ADMIN_ALREADY_EXIST",
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Get the email and password from all the user
email = request.POST.get("email", False)
password = request.POST.get("password", False)
first_name = request.POST.get("first_name", False)
last_name = request.POST.get("last_name", "")
company_name = request.POST.get("company_name", "")
is_telemetry_enabled = request.POST.get("is_telemetry_enabled", True)
# return error if the email and password is not present
if not email or not password or not first_name:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME"],
error_message="REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME",
payload={
"email": email,
"first_name": first_name,
"last_name": last_name,
"company_name": company_name,
"is_telemetry_enabled": is_telemetry_enabled,
},
)
url = urljoin(
base_host(
request=request,
is_admin=True,
),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Validate the email
email = email.strip().lower()
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_ADMIN_EMAIL"],
error_message="INVALID_ADMIN_EMAIL",
payload={
"email": email,
"first_name": first_name,
"last_name": last_name,
"company_name": company_name,
"is_telemetry_enabled": is_telemetry_enabled,
},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check if already a user exists or not
# Existing user
if User.objects.filter(email=email).exists():
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_ALREADY_EXIST"],
error_message="ADMIN_USER_ALREADY_EXIST",
payload={
"email": email,
"first_name": first_name,
"last_name": last_name,
"company_name": company_name,
"is_telemetry_enabled": is_telemetry_enabled,
},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
else:
if not is_password_policy_valid(password):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["PASSWORD_TOO_WEAK"],
error_message="PASSWORD_TOO_WEAK",
payload={
"email": email,
"first_name": first_name,
"last_name": last_name,
"company_name": company_name,
"is_telemetry_enabled": is_telemetry_enabled,
},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
user = User.objects.create(
first_name=first_name,
last_name=last_name,
email=email,
username=uuid.uuid4().hex,
password=make_password(password),
is_password_autoset=False,
)
_ = Profile.objects.create(user=user, company_name=company_name)
# settings last active for the user
user.is_active = True
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = get_client_ip(request=request)
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# Register the user as an instance admin
_ = InstanceAdmin.objects.create(user=user, instance=instance)
# Make the setup flag True
instance.is_setup_done = True
instance.instance_name = company_name
instance.is_telemetry_enabled = is_telemetry_enabled
instance.save()
# get tokens for user
user_login(request=request, user=user, is_admin=True)
url = urljoin(base_host(request=request, is_admin=True), "general/")
return HttpResponseRedirect(url)
class InstanceAdminSignInEndpoint(View):
permission_classes = [AllowAny]
@invalidate_cache(path="/api/instances/", user=False)
def post(self, request):
# Check instance first
instance = Instance.objects.first()
if instance is None:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INSTANCE_NOT_CONFIGURED"],
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Get email and password
email = request.POST.get("email", False)
password = request.POST.get("password", False)
# return error if the email and password is not present
if not email or not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["REQUIRED_ADMIN_EMAIL_PASSWORD"],
error_message="REQUIRED_ADMIN_EMAIL_PASSWORD",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Validate the email
email = email.strip().lower()
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_ADMIN_EMAIL"],
error_message="INVALID_ADMIN_EMAIL",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Fetch the user
user = User.objects.filter(email=email).first()
# Error out if the user is not present
if not user:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DOES_NOT_EXIST"],
error_message="ADMIN_USER_DOES_NOT_EXIST",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# is_active
if not user.is_active:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_USER_DEACTIVATED"],
error_message="ADMIN_USER_DEACTIVATED",
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check password of the user
if not user.check_password(password):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_AUTHENTICATION_FAILED"],
error_message="ADMIN_AUTHENTICATION_FAILED",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check if the user is an instance admin
if not InstanceAdmin.objects.filter(instance=instance, user=user):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ADMIN_AUTHENTICATION_FAILED"],
error_message="ADMIN_AUTHENTICATION_FAILED",
payload={"email": email},
)
url = urljoin(
base_host(request=request, is_admin=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# settings last active for the user
user.is_active = True
user.last_active = timezone.now()
user.last_login_time = timezone.now()
user.last_login_ip = get_client_ip(request=request)
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
user.save()
# get tokens for user
user_login(request=request, user=user, is_admin=True)
url = urljoin(base_host(request=request, is_admin=True), "general/")
return HttpResponseRedirect(url)
class InstanceAdminUserMeEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
def get(self, request):
serializer = InstanceAdminMeSerializer(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
class InstanceAdminUserSessionEndpoint(BaseAPIView):
permission_classes = [AllowAny]
def get(self, request):
if request.user.is_authenticated and InstanceAdmin.objects.filter(user=request.user).exists():
serializer = InstanceAdminMeSerializer(request.user)
data = {"is_authenticated": True}
data["user"] = serializer.data
return Response(data, status=status.HTTP_200_OK)
else:
return Response({"is_authenticated": False}, status=status.HTTP_200_OK)
class InstanceAdminSignOutEndpoint(View):
permission_classes = [InstanceAdminPermission]
def post(self, request):
# Get user
try:
user = User.objects.get(pk=request.user.id)
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()
# Log the user out
logout(request)
url = get_safe_redirect_url(base_url=base_host(request=request, is_admin=True), next_path="")
return HttpResponseRedirect(url)
except Exception:
url = get_safe_redirect_url(base_url=base_host(request=request, is_admin=True), next_path="")
return HttpResponseRedirect(url)
@@ -0,0 +1,119 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import zoneinfo
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError
# Django imports
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
# Third part imports
from rest_framework import status
from rest_framework.filters import SearchFilter
from rest_framework.response import Response
from rest_framework.views import APIView
# Module imports
from plane.license.api.permissions import InstanceAdminPermission
from plane.authentication.session import BaseSessionAuthentication
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
class TimezoneMixin:
"""
This enables timezone conversion according
to the user set timezone
"""
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if request.user.is_authenticated:
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
else:
timezone.deactivate()
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [InstanceAdminPermission]
filter_backends = (DjangoFilterBackend, SearchFilter)
authentication_classes = [BaseSessionAuthentication]
filterset_fields = []
search_fields = []
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def handle_exception(self, exc):
"""
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
try:
response = super().handle_exception(exc)
return response
except Exception as e:
if isinstance(e, IntegrityError):
return Response(
{"error": "The payload is not valid"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ValidationError):
return Response(
{"error": "Please provide valid detail"},
status=status.HTTP_400_BAD_REQUEST,
)
if isinstance(e, ObjectDoesNotExist):
return Response(
{"error": "The required object does not exist."},
status=status.HTTP_404_NOT_FOUND,
)
if isinstance(e, KeyError):
return Response(
{"error": "The required key does not exist."},
status=status.HTTP_400_BAD_REQUEST,
)
log_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def dispatch(self, request, *args, **kwargs):
try:
response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG:
from django.db import connection
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
return response
except Exception as exc:
response = self.handle_exception(exc)
return exc
@property
def fields(self):
fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
return fields if fields else None
@property
def expand(self):
expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
return expand if expand else None
@@ -0,0 +1,170 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
from smtplib import (
SMTPAuthenticationError,
SMTPConnectError,
SMTPRecipientsRefused,
SMTPSenderRefused,
SMTPServerDisconnected,
)
# Django imports
from django.core.mail import BadHeaderError, EmailMultiAlternatives, get_connection
from django.db.models import Q, Case, When, Value
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseAPIView
from plane.license.api.permissions import InstanceAdminPermission
from plane.license.models import InstanceConfiguration
from plane.license.api.serializers import InstanceConfigurationSerializer
from plane.license.utils.encryption import encrypt_data
from plane.utils.cache import cache_response, invalidate_cache
from plane.license.utils.instance_value import get_email_configuration
class InstanceConfigurationEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
@cache_response(60 * 60 * 2, user=False)
def get(self, request):
instance_configurations = InstanceConfiguration.objects.all()
serializer = InstanceConfigurationSerializer(instance_configurations, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(path="/api/instances/configurations/", user=False)
@invalidate_cache(path="/api/instances/", user=False)
def patch(self, request):
configurations = InstanceConfiguration.objects.filter(key__in=request.data.keys())
bulk_configurations = []
for configuration in configurations:
value = request.data.get(configuration.key, configuration.value)
if configuration.is_encrypted:
configuration.value = encrypt_data(value)
else:
configuration.value = value
bulk_configurations.append(configuration)
InstanceConfiguration.objects.bulk_update(bulk_configurations, ["value"], batch_size=100)
serializer = InstanceConfigurationSerializer(configurations, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class DisableEmailFeatureEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
@invalidate_cache(path="/api/instances/", user=False)
def delete(self, request):
try:
InstanceConfiguration.objects.filter(
Q(
key__in=[
"EMAIL_HOST",
"EMAIL_HOST_USER",
"EMAIL_HOST_PASSWORD",
"ENABLE_SMTP",
"EMAIL_PORT",
"EMAIL_FROM",
]
)
).update(value=Case(When(key="ENABLE_SMTP", then=Value("0")), default=Value("")))
return Response(status=status.HTTP_200_OK)
except Exception:
return Response(
{"error": "Failed to disable email configuration"},
status=status.HTTP_400_BAD_REQUEST,
)
class EmailCredentialCheckEndpoint(BaseAPIView):
def post(self, request):
receiver_email = request.data.get("receiver_email", False)
if not receiver_email:
return Response(
{"error": "Receiver email is required"},
status=status.HTTP_400_BAD_REQUEST,
)
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
# Configure all the connections
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
username=EMAIL_HOST_USER,
password=EMAIL_HOST_PASSWORD,
use_tls=EMAIL_USE_TLS == "1",
use_ssl=EMAIL_USE_SSL == "1",
)
# Prepare email details
subject = "Email Notification from Plane"
message = "This is a sample email notification sent from Plane application."
# Send the email
try:
msg = EmailMultiAlternatives(
subject=subject,
body=message,
from_email=EMAIL_FROM,
to=[receiver_email],
connection=connection,
)
msg.send(fail_silently=False)
return Response({"message": "Email successfully sent."}, status=status.HTTP_200_OK)
except BadHeaderError:
return Response({"error": "Invalid email header."}, status=status.HTTP_400_BAD_REQUEST)
except SMTPAuthenticationError:
return Response(
{"error": "Invalid credentials provided"},
status=status.HTTP_400_BAD_REQUEST,
)
except SMTPConnectError:
return Response(
{"error": "Could not connect with the SMTP server."},
status=status.HTTP_400_BAD_REQUEST,
)
except SMTPSenderRefused:
return Response(
{"error": "From address is invalid."},
status=status.HTTP_400_BAD_REQUEST,
)
except SMTPServerDisconnected:
return Response(
{"error": "SMTP server disconnected unexpectedly."},
status=status.HTTP_400_BAD_REQUEST,
)
except SMTPRecipientsRefused:
return Response(
{"error": "All recipient addresses were refused."},
status=status.HTTP_400_BAD_REQUEST,
)
except TimeoutError:
return Response(
{"error": "Timeout error while trying to connect to the SMTP server."},
status=status.HTTP_400_BAD_REQUEST,
)
except ConnectionError:
return Response(
{"error": "Network connection error. Please check your internet connection."},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception:
return Response(
{"error": "Could not send email. Please check your configuration"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -0,0 +1,214 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
# Module imports
from plane.app.views import BaseAPIView
from plane.db.models import Workspace
from plane.license.api.permissions import InstanceAdminPermission
from plane.license.api.serializers import InstanceSerializer
from plane.license.models import Instance
from plane.license.utils.instance_value import get_configuration_value
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
class InstanceEndpoint(BaseAPIView):
def get_permissions(self):
if self.request.method == "PATCH":
return [InstanceAdminPermission()]
return [AllowAny()]
@cache_response(60 * 60 * 2, user=False)
@method_decorator(cache_control(private=True, max_age=12))
def get(self, request):
instance = Instance.objects.first()
# get the instance
if instance is None:
return Response(
{"is_activated": False, "is_setup_done": False},
status=status.HTTP_200_OK,
)
# Return instance
serializer = InstanceSerializer(instance)
data = serializer.data
data["is_activated"] = True
# Get all the configuration
(
ENABLE_SIGNUP,
DISABLE_WORKSPACE_CREATION,
IS_GOOGLE_ENABLED,
IS_GITHUB_ENABLED,
GITHUB_APP_NAME,
IS_GITLAB_ENABLED,
IS_GITEA_ENABLED,
EMAIL_HOST,
ENABLE_MAGIC_LINK_LOGIN,
ENABLE_EMAIL_PASSWORD,
SLACK_CLIENT_ID,
POSTHOG_API_KEY,
POSTHOG_HOST,
UNSPLASH_ACCESS_KEY,
LLM_API_KEY,
IS_INTERCOM_ENABLED,
INTERCOM_APP_ID,
) = get_configuration_value(
[
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP", "0"),
},
{
"key": "DISABLE_WORKSPACE_CREATION",
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
},
{
"key": "IS_GOOGLE_ENABLED",
"default": os.environ.get("IS_GOOGLE_ENABLED", "0"),
},
{
"key": "IS_GITHUB_ENABLED",
"default": os.environ.get("IS_GITHUB_ENABLED", "0"),
},
{
"key": "GITHUB_APP_NAME",
"default": os.environ.get("GITHUB_APP_NAME", ""),
},
{
"key": "IS_GITLAB_ENABLED",
"default": os.environ.get("IS_GITLAB_ENABLED", "0"),
},
{
"key": "IS_GITEA_ENABLED",
"default": os.environ.get("IS_GITEA_ENABLED", "0"),
},
{"key": "EMAIL_HOST", "default": os.environ.get("EMAIL_HOST", "")},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
},
{
"key": "ENABLE_EMAIL_PASSWORD",
"default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
},
{
"key": "SLACK_CLIENT_ID",
"default": os.environ.get("SLACK_CLIENT_ID", None),
},
{
"key": "POSTHOG_API_KEY",
"default": os.environ.get("POSTHOG_API_KEY", None),
},
{
"key": "POSTHOG_HOST",
"default": os.environ.get("POSTHOG_HOST", None),
},
{
"key": "UNSPLASH_ACCESS_KEY",
"default": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
},
{
"key": "LLM_API_KEY",
"default": os.environ.get("LLM_API_KEY", ""),
},
# Intercom settings
{
"key": "IS_INTERCOM_ENABLED",
"default": os.environ.get("IS_INTERCOM_ENABLED", "1"),
},
{
"key": "INTERCOM_APP_ID",
"default": os.environ.get("INTERCOM_APP_ID", ""),
},
]
)
data = {}
# Authentication
data["enable_signup"] = ENABLE_SIGNUP == "1"
data["is_workspace_creation_disabled"] = DISABLE_WORKSPACE_CREATION == "1"
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
data["is_gitea_enabled"] = IS_GITEA_ENABLED == "1"
data["is_magic_login_enabled"] = ENABLE_MAGIC_LINK_LOGIN == "1"
data["is_email_password_enabled"] = ENABLE_EMAIL_PASSWORD == "1"
# Github app name
data["github_app_name"] = str(GITHUB_APP_NAME)
# Slack client
data["slack_client_id"] = SLACK_CLIENT_ID
# Posthog
data["posthog_api_key"] = POSTHOG_API_KEY
data["posthog_host"] = POSTHOG_HOST
# Unsplash
data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY)
# Open AI settings
data["has_llm_configured"] = bool(LLM_API_KEY)
# File size settings
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# is smtp configured
data["is_smtp_configured"] = bool(EMAIL_HOST)
# Intercom settings
data["is_intercom_enabled"] = IS_INTERCOM_ENABLED == "1"
data["intercom_app_id"] = INTERCOM_APP_ID
# Base URL
data["admin_base_url"] = settings.ADMIN_BASE_URL
data["space_base_url"] = settings.SPACE_BASE_URL
data["app_base_url"] = settings.APP_BASE_URL
data["instance_changelog_url"] = settings.INSTANCE_CHANGELOG_URL
data["is_self_managed"] = settings.IS_SELF_MANAGED
instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() >= 1
response_data = {"config": data, "instance": instance_data}
return Response(response_data, status=status.HTTP_200_OK)
@invalidate_cache(path="/api/instances/", user=False)
def patch(self, request):
# Get the instance
instance = Instance.objects.first()
serializer = InstanceSerializer(instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SignUpScreenVisitedEndpoint(BaseAPIView):
permission_classes = [AllowAny]
@invalidate_cache(path="/api/instances/", user=False)
def post(self, request):
instance = Instance.objects.first()
if instance is None:
return Response(
{"error": "Instance is not configured"},
status=status.HTTP_400_BAD_REQUEST,
)
instance.is_signup_screen_visited = True
instance.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,110 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Third party imports
from rest_framework.response import Response
from rest_framework import status
from django.db import IntegrityError
from django.db.models import OuterRef, Func, F
# Module imports
from plane.app.views.base import BaseAPIView
from plane.license.api.permissions import InstanceAdminPermission
from plane.db.models import Workspace, WorkspaceMember, Project
from plane.license.api.serializers import WorkspaceSerializer
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
class InstanceWorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
def get(self, request):
slug = request.GET.get("slug", False)
if not slug or slug == "":
return Response(
{"error": "Workspace Slug is required"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.filter(slug__iexact=slug).exists() or slug in RESTRICTED_WORKSPACE_SLUGS
return Response({"status": not workspace}, status=status.HTTP_200_OK)
class InstanceWorkSpaceEndpoint(BaseAPIView):
model = Workspace
serializer_class = WorkspaceSerializer
permission_classes = [InstanceAdminPermission]
def get(self, request):
project_count = (
Project.objects.filter(workspace_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
member_count = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member__is_bot=False, is_active=True)
.select_related("owner")
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
workspaces = Workspace.objects.annotate(total_projects=project_count, total_members=member_count)
# Add search functionality
search = request.query_params.get("search", None)
if search:
workspaces = workspaces.filter(name__icontains=search)
return self.paginate(
request=request,
queryset=workspaces,
on_results=lambda results: WorkspaceSerializer(results, many=True).data,
max_per_page=10,
default_per_page=10,
)
def post(self, request):
try:
serializer = WorkspaceSerializer(data=request.data)
slug = request.data.get("slug", False)
name = request.data.get("name", False)
if not name or not slug:
return Response(
{"error": "Both name and slug are required"},
status=status.HTTP_400_BAD_REQUEST,
)
if len(name) > 80 or len(slug) > 48:
return Response(
{"error": "The maximum length for name is 80 and for slug is 48"},
status=status.HTTP_400_BAD_REQUEST,
)
if serializer.is_valid(raise_exception=True):
serializer.save(owner=request.user)
# Create Workspace member
_ = WorkspaceMember.objects.create(
workspace_id=serializer.data["id"],
member=request.user,
role=20,
company_role=request.data.get("company_role", ""),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
status=status.HTTP_400_BAD_REQUEST,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"slug": "The workspace with the slug already exists"},
status=status.HTTP_409_CONFLICT,
)