base
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user