dev: instance refactor (#3015)

* dev: remove license engine communication

* dev: remove license engine base url

* dev: update instance configuration function

* chore: removed the print statement

* chore: changed config variables

* chore: cleanup

* chore: added SKIP_ENV_VAR

* chore: changed the EMAIL_FROM

* dev: patch endpoint for workspace

* dev: custom port for takeoff script

* chore: changed my sequence

* fix: update operaton for member invitations in workspace

* clean-up: remove logs

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Nikhil 2023-12-07 14:51:27 +05:30 committed by Aaryan Khandelwal
parent bacddcb348
commit 882c43dcc6
23 changed files with 485 additions and 546 deletions

View File

@ -27,4 +27,4 @@ python manage.py configure_instance
# Create the default bucket # Create the default bucket
python manage.py create_bucket python manage.py create_bucket
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile - exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -

View File

@ -95,6 +95,16 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class Meta: class Meta:
model = WorkspaceMemberInvite model = WorkspaceMemberInvite
fields = "__all__" fields = "__all__"
read_only_fields = [
"id",
"email",
"token",
"workspace",
"message",
"responded_at",
"created_at",
"updated_at",
]
class TeamSerializer(BaseSerializer): class TeamSerializer(BaseSerializer):

View File

@ -65,6 +65,7 @@ urlpatterns = [
{ {
"delete": "destroy", "delete": "destroy",
"get": "retrieve", "get": "retrieve",
"patch": "partial_update",
} }
), ),
name="workspace-invitations", name="workspace-invitations",

View File

@ -34,7 +34,7 @@ from plane.app.serializers import (
from plane.db.models import User, WorkspaceMemberInvite from plane.db.models import User, WorkspaceMemberInvite
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.forgot_password_task import forgot_password from plane.bgtasks.forgot_password_task import forgot_password
from plane.license.models import Instance, InstanceConfiguration from plane.license.models import Instance
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.bgtasks.magic_link_code_task import magic_link from plane.bgtasks.magic_link_code_task import magic_link
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
@ -321,8 +321,19 @@ class EmailCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# Get the configurations # Get configuration values
instance_configuration = InstanceConfiguration.objects.values("key", "value") ENABLE_SIGNUP, ENABLE_MAGIC_LINK_LOGIN = get_configuration_value(
[
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP"),
},
{
"key": "ENABLE_MAGIC_LINK_LOGIN",
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
},
]
)
email = request.data.get("email", False) email = request.data.get("email", False)
@ -347,12 +358,7 @@ class EmailCheckEndpoint(BaseAPIView):
if user is None: if user is None:
# Create the user # Create the user
if ( if (
get_configuration_value( ENABLE_SIGNUP == "0"
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
== "0"
and not WorkspaceMemberInvite.objects.filter( and not WorkspaceMemberInvite.objects.filter(
email=email, email=email,
).exists() ).exists()
@ -372,13 +378,8 @@ class EmailCheckEndpoint(BaseAPIView):
is_password_autoset=True, is_password_autoset=True,
) )
if not bool( if not bool(
get_configuration_value( ENABLE_MAGIC_LINK_LOGIN,
instance_configuration,
"ENABLE_MAGIC_LINK_LOGIN",
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
),
): ):
return Response( return Response(
{"error": "Magic link sign in is disabled."}, {"error": "Magic link sign in is disabled."},
@ -386,16 +387,15 @@ class EmailCheckEndpoint(BaseAPIView):
) )
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium="MAGIC_LINK",
medium="MAGIC_LINK", first_time=True,
first_time=True, )
)
key, token, current_attempt = generate_magic_token(email=email) key, token, current_attempt = generate_magic_token(email=email)
if not current_attempt: if not current_attempt:
return Response( return Response(
@ -413,28 +413,21 @@ class EmailCheckEndpoint(BaseAPIView):
else: else:
if user.is_password_autoset: if user.is_password_autoset:
## Generate a random token ## Generate a random token
if not bool( if not bool(ENABLE_MAGIC_LINK_LOGIN):
get_configuration_value(
instance_configuration,
"ENABLE_MAGIC_LINK_LOGIN",
os.environ.get("ENABLE_MAGIC_LINK_LOGIN"),
),
):
return Response( return Response(
{"error": "Magic link sign in is disabled."}, {"error": "Magic link sign in is disabled."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium="MAGIC_LINK",
medium="MAGIC_LINK", first_time=False,
first_time=False, )
)
# Generate magic token # Generate magic token
key, token, current_attempt = generate_magic_token(email=email) key, token, current_attempt = generate_magic_token(email=email)
@ -454,16 +447,15 @@ class EmailCheckEndpoint(BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
else: else:
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium="EMAIL",
medium="EMAIL", first_time=False,
first_time=False, )
)
# User should enter password to login # User should enter password to login
return Response( return Response(

View File

@ -27,7 +27,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
) )
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.license.models import InstanceConfiguration, Instance from plane.license.models import Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
@ -52,8 +52,6 @@ class SignUpEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
instance_configuration = InstanceConfiguration.objects.values("key", "value")
email = request.data.get("email", False) email = request.data.get("email", False)
password = request.data.get("password", False) password = request.data.get("password", False)
## Raise exception if any of the above are missing ## Raise exception if any of the above are missing
@ -73,14 +71,20 @@ class SignUpEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
# get configuration values
# Get configuration values
ENABLE_SIGNUP, = get_configuration_value(
[
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP"),
},
]
)
# If the sign up is not enabled and the user does not have invite disallow him from creating the account # If the sign up is not enabled and the user does not have invite disallow him from creating the account
if ( if (
get_configuration_value( ENABLE_SIGNUP == "0"
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
== "0"
and not WorkspaceMemberInvite.objects.filter( and not WorkspaceMemberInvite.objects.filter(
email=email, email=email,
).exists() ).exists()
@ -169,16 +173,17 @@ class SignInEndpoint(BaseAPIView):
# Create the user # Create the user
else: else:
# Get the configurations ENABLE_SIGNUP, = get_configuration_value(
instance_configuration = InstanceConfiguration.objects.values("key", "value") [
{
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP"),
},
]
)
# Create the user # Create the user
if ( if (
get_configuration_value( ENABLE_SIGNUP == "0"
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
== "0"
and not WorkspaceMemberInvite.objects.filter( and not WorkspaceMemberInvite.objects.filter(
email=email, email=email,
).exists() ).exists()
@ -264,16 +269,15 @@ class SignInEndpoint(BaseAPIView):
workspace_member_invites.delete() workspace_member_invites.delete()
project_member_invites.delete() project_member_invites.delete()
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium="EMAIL",
medium="EMAIL", first_time=False,
first_time=False, )
)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
data = { data = {
@ -347,16 +351,15 @@ class MagicSignInEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium="MAGIC_LINK",
medium="MAGIC_LINK", first_time=False,
first_time=False, )
)
user.is_active = True user.is_active = True
user.is_email_verified = True user.is_email_verified = True

View File

@ -11,7 +11,6 @@ from rest_framework.response import Response
# Module imports # Module imports
from .base import BaseAPIView from .base import BaseAPIView
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
@ -21,89 +20,101 @@ class ConfigurationEndpoint(BaseAPIView):
] ]
def get(self, request): def get(self, request):
instance_configuration = InstanceConfiguration.objects.values("key", "value")
# Get all the configuration
(
GOOGLE_CLIENT_ID,
GITHUB_CLIENT_ID,
GITHUB_APP_NAME,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
ENABLE_MAGIC_LINK_LOGIN,
ENABLE_EMAIL_PASSWORD,
SLACK_CLIENT_ID,
POSTHOG_API_KEY,
POSTHOG_HOST,
UNSPLASH_ACCESS_KEY,
OPENAI_API_KEY,
) = get_configuration_value(
[
{
"key": "GOOGLE_CLIENT_ID",
"default": os.environ.get("GOOGLE_CLIENT_ID", None),
},
{
"key": "GITHUB_CLIENT_ID",
"default": os.environ.get("GITHUB_CLIENT_ID", None),
},
{
"key": "GITHUB_APP_NAME",
"default": os.environ.get("GITHUB_APP_NAME", None),
},
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER", None),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD", None),
},
{
"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", "1"),
},
{
"key": "POSTHOG_API_KEY",
"default": os.environ.get("POSTHOG_API_KEY", "1"),
},
{
"key": "POSTHOG_HOST",
"default": os.environ.get("POSTHOG_HOST", "1"),
},
{
"key": "UNSPLASH_ACCESS_KEY",
"default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"),
},
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", "1"),
},
]
)
data = {} data = {}
# Authentication # Authentication
data["google_client_id"] = get_configuration_value( data["google_client_id"] = GOOGLE_CLIENT_ID
instance_configuration, data["github_client_id"] = GITHUB_CLIENT_ID
"GOOGLE_CLIENT_ID", data["github_app_name"] = GITHUB_APP_NAME
os.environ.get("GOOGLE_CLIENT_ID", None),
)
data["github_client_id"] = get_configuration_value(
instance_configuration,
"GITHUB_CLIENT_ID",
os.environ.get("GITHUB_CLIENT_ID", None),
)
data["github_app_name"] = get_configuration_value(
instance_configuration,
"GITHUB_APP_NAME",
os.environ.get("GITHUB_APP_NAME", None),
)
data["magic_login"] = ( data["magic_login"] = (
bool( bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
get_configuration_value( ) and ENABLE_MAGIC_LINK_LOGIN == "1"
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER", None),
),
)
and bool(
get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD", None),
)
)
) and get_configuration_value(
instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "1"
) == "1"
data["email_password_login"] = ( data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1"
get_configuration_value(
instance_configuration, "ENABLE_EMAIL_PASSWORD", "1"
)
== "1"
)
# Slack client # Slack client
data["slack_client_id"] = get_configuration_value( data["slack_client_id"] = SLACK_CLIENT_ID
instance_configuration,
"SLACK_CLIENT_ID",
os.environ.get("SLACK_CLIENT_ID", None),
)
# Posthog # Posthog
data["posthog_api_key"] = get_configuration_value( data["posthog_api_key"] = POSTHOG_API_KEY
instance_configuration, data["posthog_host"] = POSTHOG_HOST
"POSTHOG_API_KEY",
os.environ.get("POSTHOG_API_KEY", None),
)
data["posthog_host"] = get_configuration_value(
instance_configuration,
"POSTHOG_HOST",
os.environ.get("POSTHOG_HOST", None),
)
# Unsplash # Unsplash
data["has_unsplash_configured"] = bool( data["has_unsplash_configured"] = UNSPLASH_ACCESS_KEY
get_configuration_value(
instance_configuration,
"UNSPLASH_ACCESS_KEY",
os.environ.get("UNSPLASH_ACCESS_KEY", None),
)
)
# Open AI settings # Open AI settings
data["has_openai_configured"] = bool( data["has_openai_configured"] = bool(OPENAI_API_KEY)
get_configuration_value(
instance_configuration,
"OPENAI_API_KEY",
os.environ.get("OPENAI_API_KEY", None),
)
)
# File size settings
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880)) data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# is self managed
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1"))) data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)

View File

@ -1,6 +1,7 @@
# Python imports # Python imports
import requests import requests
import os import os
# Third party imports # Third party imports
from openai import OpenAI from openai import OpenAI
from rest_framework.response import Response from rest_framework.response import Response
@ -15,23 +16,31 @@ from plane.app.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project from plane.db.models import Workspace, Project
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.utils.integrations.github import get_release_notes from plane.utils.integrations.github import get_release_notes
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
class GPTIntegrationEndpoint(BaseAPIView): class GPTIntegrationEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", None),
},
{
"key": "GPT_ENGINE",
"default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
},
]
)
# Get the configuration value # Get the configuration value
instance_configuration = InstanceConfiguration.objects.values("key", "value")
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY", os.environ.get("OPENAI_API_KEY"))
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE", os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"))
# Check the keys # Check the keys
if not api_key or not gpt_engine: if not OPENAI_API_KEY or not GPT_ENGINE:
return Response( return Response(
{"error": "OpenAI API key and engine is required"}, {"error": "OpenAI API key and engine is required"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -48,11 +57,11 @@ class GPTIntegrationEndpoint(BaseAPIView):
final_text = task + "\n" + prompt final_text = task + "\n" + prompt
client = OpenAI( client = OpenAI(
api_key=api_key, api_key=OPENAI_API_KEY,
) )
response = client.chat.completions.create( response = client.chat.completions.create(
model=gpt_engine, model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}], messages=[{"role": "user", "content": final_text}],
) )
@ -79,13 +88,17 @@ class ReleaseNotesEndpoint(BaseAPIView):
class UnsplashEndpoint(BaseAPIView): class UnsplashEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
instance_configuration = InstanceConfiguration.objects.values("key", "value") UNSPLASH_ACCESS_KEY, = get_configuration_value(
unsplash_access_key = get_configuration_value(instance_configuration, "UNSPLASH_ACCESS_KEY", os.environ.get("UNSPLASH_ACCESS_KEY")) [
{
"key": "UNSPLASH_ACCESS_KEY",
"default": os.environ.get("UNSPLASH_ACCESS_KEY"),
}
]
)
# Check unsplash access key # Check unsplash access key
if not unsplash_access_key: if not UNSPLASH_ACCESS_KEY:
return Response([], status=status.HTTP_200_OK) return Response([], status=status.HTTP_200_OK)
# Query parameters # Query parameters
@ -94,9 +107,9 @@ class UnsplashEndpoint(BaseAPIView):
per_page = request.GET.get("per_page", 20) per_page = request.GET.get("per_page", 20)
url = ( url = (
f"https://api.unsplash.com/search/photos/?client_id={unsplash_access_key}&query={query}&page=${page}&per_page={per_page}" f"https://api.unsplash.com/search/photos/?client_id={UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
if query if query
else f"https://api.unsplash.com/photos/?client_id={unsplash_access_key}&page={page}&per_page={per_page}" else f"https://api.unsplash.com/photos/?client_id={UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
) )
headers = { headers = {

View File

@ -30,7 +30,7 @@ from plane.db.models import (
) )
from plane.bgtasks.event_tracking_task import auth_events from plane.bgtasks.event_tracking_task import auth_events
from .base import BaseAPIView from .base import BaseAPIView
from plane.license.models import InstanceConfiguration, Instance from plane.license.models import Instance
from plane.license.utils.instance_value import get_configuration_value from plane.license.utils.instance_value import get_configuration_value
@ -147,18 +147,20 @@ class OauthEndpoint(BaseAPIView):
id_token = request.data.get("credential", False) id_token = request.data.get("credential", False)
client_id = request.data.get("clientId", False) client_id = request.data.get("clientId", False)
instance_configuration = InstanceConfiguration.objects.values( GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID = get_configuration_value(
"key", "value" [
{
"key": "GOOGLE_CLIENT_ID",
"default": os.environ.get("GOOGLE_CLIENT_ID"),
},
{
"key": "GITHUB_CLIENT_ID",
"default": os.environ.get("GITHUB_CLIENT_ID"),
},
]
) )
if not get_configuration_value(
instance_configuration, if not GOOGLE_CLIENT_ID or not GITHUB_CLIENT_ID:
"GOOGLE_CLIENT_ID",
os.environ.get("GOOGLE_CLIENT_ID"),
) or not get_configuration_value(
instance_configuration,
"GITHUB_CLIENT_ID",
os.environ.get("GITHUB_CLIENT_ID"),
):
return Response( return Response(
{"error": "Github or Google login is not configured"}, {"error": "Github or Google login is not configured"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -278,16 +280,15 @@ class OauthEndpoint(BaseAPIView):
) )
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium=medium.upper(),
medium=medium.upper(), first_time=False,
first_time=False, )
)
access_token, refresh_token = get_tokens_for_user(user) access_token, refresh_token = get_tokens_for_user(user)
@ -298,17 +299,16 @@ class OauthEndpoint(BaseAPIView):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
except User.DoesNotExist: except User.DoesNotExist:
## Signup Case ENABLE_SIGNUP, = get_configuration_value(
instance_configuration = InstanceConfiguration.objects.values( [
"key", "value" {
"key": "ENABLE_SIGNUP",
"default": os.environ.get("ENABLE_SIGNUP", "0"),
}
]
) )
if ( if (
get_configuration_value( ENABLE_SIGNUP == "0"
instance_configuration,
"ENABLE_SIGNUP",
os.environ.get("ENABLE_SIGNUP", "0"),
)
== "0"
and not WorkspaceMemberInvite.objects.filter( and not WorkspaceMemberInvite.objects.filter(
email=email, email=email,
).exists() ).exists()
@ -411,16 +411,15 @@ class OauthEndpoint(BaseAPIView):
project_member_invites.delete() project_member_invites.delete()
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: auth_events.delay(
auth_events.delay( user=user.id,
user=user.id, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="SIGN_IN",
event_name="SIGN_IN", medium=medium.upper(),
medium=medium.upper(), first_time=True,
first_time=True, )
)
SocialLoginConnection.objects.update_or_create( SocialLoginConnection.objects.update_or_create(
medium=medium, medium=medium,

View File

@ -408,15 +408,14 @@ class WorkspaceJoinEndpoint(BaseAPIView):
workspace_invite.delete() workspace_invite.delete()
# Send event # Send event
if settings.POSTHOG_API_KEY and settings.POSTHOG_HOST: workspace_invite_event.delay(
workspace_invite_event.delay( user=user.id if user is not None else None,
user=user.id if user is not None else None, email=email,
email=email, user_agent=request.META.get("HTTP_USER_AGENT"),
user_agent=request.META.get("HTTP_USER_AGENT"), ip=request.META.get("REMOTE_ADDR"),
ip=request.META.get("REMOTE_ADDR"), event_name="MEMBER_ACCEPTED",
event_name="MEMBER_ACCEPTED", accepted_from="EMAIL",
accepted_from="EMAIL", )
)
return Response( return Response(
{"message": "Workspace Invitation Accepted"}, {"message": "Workspace Invitation Accepted"},

View File

@ -18,7 +18,6 @@ from sentry_sdk import capture_exception
from plane.db.models import Issue from plane.db.models import Issue
from plane.utils.analytics_plot import build_graph_plot from plane.utils.analytics_plot import build_graph_plot
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
row_mapping = { row_mapping = {
@ -52,11 +51,6 @@ def send_export_email(email, slug, csv_buffer, rows):
csv_buffer.seek(0) csv_buffer.seek(0)
# Configure email connection from the database
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
( (
EMAIL_HOST, EMAIL_HOST,
EMAIL_HOST_USER, EMAIL_HOST_USER,
@ -64,7 +58,7 @@ def send_export_email(email, slug, csv_buffer, rows):
EMAIL_PORT, EMAIL_PORT,
EMAIL_USE_TLS, EMAIL_USE_TLS,
EMAIL_FROM, EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration()
connection = get_connection( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View File

@ -1,50 +1,78 @@
import uuid import uuid
import os
from posthog import Posthog # third party imports
from django.conf import settings
#third party imports
from celery import shared_task from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
from posthog import Posthog
# module imports
from plane.license.utils.instance_value import get_configuration_value
def posthogConfiguration():
POSTHOG_API_KEY, POSTHOG_HOST = get_configuration_value(
[
{
"key": "POSTHOG_API_KEY",
"default": os.environ.get("POSTHOG_API_KEY", None),
},
{
"key": "POSTHOG_HOST",
"default": os.environ.get("POSTHOG_HOST", None),
},
]
)
if POSTHOG_API_KEY and POSTHOG_HOST:
return POSTHOG_API_KEY, POSTHOG_HOST
else:
return None, None
@shared_task @shared_task
def auth_events(user, email, user_agent, ip, event_name, medium, first_time): def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
try: try:
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST) POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
posthog.capture(
email, if POSTHOG_API_KEY and POSTHOG_HOST:
event=event_name, posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
properties={ posthog.capture(
"event_id": uuid.uuid4().hex, email,
"user": {"email": email, "id": str(user)}, event=event_name,
"device_ctx": { properties={
"ip": ip, "event_id": uuid.uuid4().hex,
"user_agent": user_agent, "user": {"email": email, "id": str(user)},
}, "device_ctx": {
"medium": medium, "ip": ip,
"first_time": first_time "user_agent": user_agent,
} },
) "medium": medium,
"first_time": first_time
}
)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
@shared_task @shared_task
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from): def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
try: try:
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST) POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
posthog.capture(
email, if POSTHOG_API_KEY and POSTHOG_HOST:
event=event_name, posthog = Posthog(POSTHOG_API_KEY, host=POSTHOG_HOST)
properties={ posthog.capture(
"event_id": uuid.uuid4().hex, email,
"user": {"email": email, "id": str(user)}, event=event_name,
"device_ctx": { properties={
"ip": ip, "event_id": uuid.uuid4().hex,
"user_agent": user_agent, "user": {"email": email, "id": str(user)},
}, "device_ctx": {
"accepted_from": accepted_from "ip": ip,
} "user_agent": user_agent,
) },
"accepted_from": accepted_from
}
)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)

View File

@ -14,7 +14,6 @@ from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
@ -26,10 +25,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
) )
abs_url = str(current_site) + relative_link abs_url = str(current_site) + relative_link
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
( (
EMAIL_HOST, EMAIL_HOST,
EMAIL_HOST_USER, EMAIL_HOST_USER,
@ -37,7 +32,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
EMAIL_PORT, EMAIL_PORT,
EMAIL_USE_TLS, EMAIL_USE_TLS,
EMAIL_FROM, EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration()
subject = "A new password to your Plane account has been requested" subject = "A new password to your Plane account has been requested"
@ -51,9 +46,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
text_content = strip_tags(html_content) text_content = strip_tags(html_content)
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
connection = get_connection( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,
port=int(EMAIL_PORT), port=int(EMAIL_PORT),

View File

@ -14,17 +14,12 @@ from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
def magic_link(email, key, token, current_site): def magic_link(email, key, token, current_site):
try: try:
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
( (
EMAIL_HOST, EMAIL_HOST,
EMAIL_HOST_USER, EMAIL_HOST_USER,
@ -32,7 +27,7 @@ def magic_link(email, key, token, current_site):
EMAIL_PORT, EMAIL_PORT,
EMAIL_USE_TLS, EMAIL_USE_TLS,
EMAIL_FROM, EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration()
# Send the mail # Send the mail
subject = f"Your unique Plane login code is {token}" subject = f"Your unique Plane login code is {token}"

View File

@ -13,7 +13,6 @@ from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.db.models import Project, User, ProjectMemberInvite from plane.db.models import Project, User, ProjectMemberInvite
from plane.license.models import InstanceConfiguration
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
@shared_task @shared_task
@ -47,7 +46,6 @@ def project_invitation(email, project_id, token, current_site, invitor):
project_member_invite.save() project_member_invite.save()
# Configure email connection from the database # Configure email connection from the database
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
( (
EMAIL_HOST, EMAIL_HOST,
EMAIL_HOST_USER, EMAIL_HOST_USER,
@ -55,7 +53,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
EMAIL_PORT, EMAIL_PORT,
EMAIL_USE_TLS, EMAIL_USE_TLS,
EMAIL_FROM, EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration()
connection = get_connection( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View File

@ -17,7 +17,6 @@ from slack_sdk.errors import SlackApiError
# Module imports # Module imports
from plane.db.models import Workspace, WorkspaceMemberInvite, User from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.license.models import InstanceConfiguration, Instance
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
@ -37,9 +36,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
# The complete url including the domain # The complete url including the domain
abs_url = str(current_site) + relative_link abs_url = str(current_site) + relative_link
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
( (
EMAIL_HOST, EMAIL_HOST,
@ -48,7 +44,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
EMAIL_PORT, EMAIL_PORT,
EMAIL_USE_TLS, EMAIL_USE_TLS,
EMAIL_FROM, EMAIL_FROM,
) = get_email_configuration(instance_configuration=instance_configuration) ) = get_email_configuration()
# Subject of the email # Subject of the email
subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane" subject = f"{user.first_name or user.display_name or user.email} has invited you to join them in {workspace.name} on Plane"
@ -69,9 +65,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
workspace_member_invite.message = text_content workspace_member_invite.message = text_content
workspace_member_invite.save() workspace_member_invite.save()
instance_configuration = InstanceConfiguration.objects.filter(
key__startswith="EMAIL_"
).values("key", "value")
connection = get_connection( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,
port=int(EMAIL_PORT), port=int(EMAIL_PORT),

View File

@ -28,10 +28,6 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.file_asset_task.delete_file_asset", "task": "plane.bgtasks.file_asset_task.delete_file_asset",
"schedule": crontab(hour=0, minute=0), "schedule": crontab(hour=0, minute=0),
}, },
"check-instance-verification": {
"task": "plane.license.bgtasks.instance_verification_task.instance_verification_task",
"schedule": crontab(minute=0, hour='*/4'),
},
} }
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.

View File

@ -1,136 +0,0 @@
# Python imports
import os
import json
import requests
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import User
from plane.license.models import Instance, InstanceAdmin
def instance_verification(instance):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
headers = {"Content-Type": "application/json"}
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"machine_signature": os.environ.get("MACHINE_SIGNATURE", "machine-signature"),
"user_count": User.objects.filter(is_bot=False).count(),
}
# Register the instance
response = requests.post(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers,
data=json.dumps(payload),
timeout=30,
)
# check response status
if response.status_code == 201:
data = response.json()
# Update instance
instance.instance_id = data.get("id")
instance.license_key = data.get("license_key")
instance.api_key = data.get("api_key")
instance.version = data.get("version")
instance.user_count = data.get("user_count", 0)
instance.is_verified = True
instance.save()
else:
return
def admin_verification(instance):
# Save the user in control center
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
# Get all the unverified instance admins
instance_admins = InstanceAdmin.objects.filter(is_verified=False).select_related(
"user"
)
updated_instance_admin = []
# Verify the instance admin
for instance_admin in instance_admins:
instance_admin.is_verified = True
# Create the admin
response = requests.post(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/users/register/",
headers=headers,
data=json.dumps(
{
"email": str(instance_admin.user.email),
"signup_mode": "EMAIL",
"is_admin": True,
}
),
timeout=30,
)
updated_instance_admin.append(instance_admin)
# update all the instance admins
InstanceAdmin.objects.bulk_update(
updated_instance_admin, ["is_verified"], batch_size=10
)
return
def instance_user_count(instance):
try:
instance_users = User.objects.filter(is_bot=False).count()
# Update the count in the license engine
payload = {
"user_count": instance_users,
}
# Save the user in control center
headers = {
"Content-Type": "application/json",
"x-instance-id": instance.instance_id,
"x-api-key": instance.api_key,
}
# Update the license engine
_ = requests.post(
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/",
headers=headers,
data=json.dumps(payload),
timeout=30,
)
return
except requests.RequestException:
return
@shared_task
def instance_verification_task():
try:
# Get the first instance
instance = Instance.objects.first()
# Only register instance if it is not verified
if not instance.is_verified:
instance_verification(instance=instance)
# Admin verifications
admin_verification(instance=instance)
# Update user count
instance_user_count(instance=instance)
return
except requests.RequestException:
return

View File

@ -30,13 +30,11 @@ class Command(BaseCommand):
# Load JSON content from the file # Load JSON content from the file
data = json.load(file) data = json.load(file)
machine_signature = options.get("machine_signature", False) machine_signature = options.get("machine_signature", "machine-signature")
if not machine_signature: if not machine_signature:
raise CommandError("Machine signature is required") raise CommandError("Machine signature is required")
# Check if machine is online
headers = {"Content-Type": "application/json"}
payload = { payload = {
"instance_key": settings.INSTANCE_KEY, "instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1), "version": data.get("version", 0.1),
@ -44,51 +42,21 @@ class Command(BaseCommand):
"user_count": User.objects.filter(is_bot=False).count(), "user_count": User.objects.filter(is_bot=False).count(),
} }
try: instance = Instance.objects.create(
response = requests.post( instance_name="Plane Free",
f"{settings.LICENSE_ENGINE_BASE_URL}/api/instances/", instance_id=secrets.token_hex(12),
headers=headers, license_key=None,
data=json.dumps(payload), api_key=secrets.token_hex(8),
timeout=30 version=payload.get("version"),
) last_checked_at=timezone.now(),
user_count=payload.get("user_count", 0),
)
if response.status_code == 201: self.stdout.write(
data = response.json() self.style.SUCCESS(
# Create instance f"Instance registered"
instance = Instance.objects.create(
instance_name="Plane Free",
instance_id=data.get("id"),
license_key=data.get("license_key"),
api_key=data.get("api_key"),
version=data.get("version"),
last_checked_at=timezone.now(),
user_count=data.get("user_count", 0),
is_verified=True,
)
self.stdout.write(
self.style.SUCCESS(
f"Instance successfully registered and verified"
)
)
return
except requests.RequestException as _e:
instance = Instance.objects.create(
instance_name="Plane Free",
instance_id=secrets.token_hex(12),
license_key=None,
api_key=secrets.token_hex(8),
version=payload.get("version"),
last_checked_at=timezone.now(),
user_count=payload.get("user_count", 0),
) )
self.stdout.write( )
self.style.SUCCESS(
f"Instance successfully registered"
)
)
return
raise CommandError("Instance could not be registered")
else: else:
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(

View File

@ -1,63 +1,71 @@
# Python imports
import os import os
# Django imports
from django.conf import settings
# Module imports
from plane.license.models import InstanceConfiguration
from plane.license.utils.encryption import decrypt_data
# Helper function to return value from the passed key # Helper function to return value from the passed key
def get_configuration_value(query, key, default=None): def get_configuration_value(keys):
for item in query: environment_list = []
if item["key"] == key: if settings.SKIP_ENV_VAR:
return item.get("value", default) # Get the configurations
return default instance_configuration = InstanceConfiguration.objects.values(
"key", "value", "is_encrypted"
)
for key in keys:
for item in instance_configuration:
if key.get("key") == item.get("key"):
if item.get("is_encrypted", False):
environment_list.append(decrypt_data(item.get("value")))
else:
environment_list.append(item.get("value"))
break
else:
environment_list.append(key.get("default"))
else:
# Get the configuration from os
for key in keys:
environment_list.append(os.environ.get(key.get("key"), key.get("default")))
return tuple(environment_list)
def get_email_configuration(instance_configuration): def get_email_configuration():
# Get the configuration variables
EMAIL_HOST_USER = get_configuration_value(
instance_configuration,
"EMAIL_HOST_USER",
os.environ.get("EMAIL_HOST_USER", None),
)
EMAIL_HOST_PASSWORD = get_configuration_value(
instance_configuration,
"EMAIL_HOST_PASSWORD",
os.environ.get("EMAIL_HOST_PASSWORD", None),
)
EMAIL_HOST = get_configuration_value(
instance_configuration,
"EMAIL_HOST",
os.environ.get("EMAIL_HOST", None),
)
EMAIL_FROM = get_configuration_value(
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", None),
)
EMAIL_USE_TLS = get_configuration_value(
instance_configuration,
"EMAIL_USE_TLS",
os.environ.get("EMAIL_USE_TLS", "1"),
)
EMAIL_PORT = get_configuration_value(
instance_configuration,
"EMAIL_PORT",
587,
)
EMAIL_FROM = get_configuration_value(
instance_configuration,
"EMAIL_FROM",
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
)
return ( return (
EMAIL_HOST, get_configuration_value(
EMAIL_HOST_USER, [
EMAIL_HOST_PASSWORD, {
EMAIL_PORT, "key": "EMAIL_HOST",
EMAIL_USE_TLS, "default": os.environ.get("EMAIL_HOST"),
EMAIL_FROM, },
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER"),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
},
{
"key": "EMAIL_PORT",
"default": os.environ.get("EMAIL_PORT", 587),
},
{
"key": "EMAIL_USE_TLS",
"default": os.environ.get("EMAIL_USE_TLS", "1"),
},
{
"key": "EMAIL_FROM",
"default": os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
},
]
)
) )

View File

@ -287,7 +287,6 @@ CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task", "plane.bgtasks.issue_automation_task",
"plane.bgtasks.exporter_expired_task", "plane.bgtasks.exporter_expired_task",
"plane.bgtasks.file_asset_task", "plane.bgtasks.file_asset_task",
"plane.license.bgtasks.instance_verification_task",
) )
# Sentry Settings # Sentry Settings
@ -328,12 +327,10 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False) POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False) POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# License engine base url
LICENSE_ENGINE_BASE_URL = os.environ.get(
"LICENSE_ENGINE_BASE_URL", "https://control-center.plane.so"
)
# instance key # instance key
INSTANCE_KEY = os.environ.get( INSTANCE_KEY = os.environ.get(
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3" "INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
) )
# Skip environment variable configuration
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"

View File

@ -40,7 +40,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { const {
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation }, workspaceMember: { removeMember, updateMember, updateMemberInvitation, deleteWorkspaceInvitation },
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace }, user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
} = useMobxStore(); } = useMobxStore();
// states // states
@ -206,15 +206,26 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
onChange={(value: TUserWorkspaceRole | undefined) => { onChange={(value: TUserWorkspaceRole | undefined) => {
if (!workspaceSlug || !value) return; if (!workspaceSlug || !value) return;
updateMember(workspaceSlug.toString(), member.id, { if (!member?.status)
role: value, updateMemberInvitation(workspaceSlug.toString(), member.id, {
}).catch(() => { role: value,
setToastAlert({ }).catch(() => {
type: "error", setToastAlert({
title: "Error!", type: "error",
message: "An error occurred while updating member role. Please try again.", title: "Error!",
message: "An error occurred while updating member role. Please try again.",
});
});
else
updateMember(workspaceSlug.toString(), member.id, {
role: value,
}).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "An error occurred while updating member role. Please try again.",
});
}); });
});
}} }}
disabled={!hasRoleChangeAccess} disabled={!hasRoleChangeAccess}
placement="bottom-end" placement="bottom-end"

View File

@ -166,6 +166,18 @@ export class WorkspaceService extends APIService {
}); });
} }
async updateWorkspaceInvitation(
workspaceSlug: string,
invitationId: string,
data: Partial<IWorkspaceMember>
): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async deleteWorkspaceInvitations(workspaceSlug: string, invitationId: string): Promise<any> { async deleteWorkspaceInvitations(workspaceSlug: string, invitationId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/`)
.then((response) => response?.data) .then((response) => response?.data)

View File

@ -19,6 +19,11 @@ export interface IWorkspaceMemberStore {
updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>; updateMember: (workspaceSlug: string, memberId: string, data: Partial<IWorkspaceMember>) => Promise<void>;
removeMember: (workspaceSlug: string, memberId: string) => Promise<void>; removeMember: (workspaceSlug: string, memberId: string) => Promise<void>;
inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise<any>; inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise<any>;
updateMemberInvitation: (
workspaceSlug: string,
memberId: string,
data: Partial<IWorkspaceMemberInvitation>
) => Promise<void>;
deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise<void>; deleteWorkspaceInvitation: (workspaceSlug: string, memberId: string) => Promise<void>;
// computed // computed
workspaceMembers: IWorkspaceMember[] | null; workspaceMembers: IWorkspaceMember[] | null;
@ -53,6 +58,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
updateMember: action, updateMember: action,
removeMember: action, removeMember: action,
inviteMembersToWorkspace: action, inviteMembersToWorkspace: action,
updateMemberInvitation: action,
deleteWorkspaceInvitation: action, deleteWorkspaceInvitation: action,
// computed // computed
workspaceMembers: computed, workspaceMembers: computed,
@ -183,6 +189,55 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
} }
}; };
/**
* update workspace member invitation using workspace slug and member id and data
* @param workspaceSlug
* @param memberId
* @param data
*/
updateMemberInvitation = async (
workspaceSlug: string,
memberId: string,
data: Partial<IWorkspaceMemberInvitation>
) => {
const originalMemberInvitations = [...this.memberInvitations?.[workspaceSlug]]; // in case of error, we will revert back to original members
const memberInvitations = [...this.memberInvitations?.[workspaceSlug]];
const index = memberInvitations.findIndex((m) => m.id === memberId);
memberInvitations[index] = { ...memberInvitations[index], ...data };
// optimistic update
runInAction(() => {
this.loader = true;
this.error = null;
this.memberInvitations = {
...this.memberInvitations,
[workspaceSlug]: memberInvitations,
};
});
try {
await this.workspaceService.updateWorkspaceInvitation(workspaceSlug, memberId, data);
runInAction(() => {
this.loader = false;
this.error = null;
});
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
this.memberInvitations = {
...this.memberInvitations,
[workspaceSlug]: originalMemberInvitations,
};
});
throw error;
}
};
/** /**
* delete the workspace invitation * delete the workspace invitation
* @param workspaceSlug * @param workspaceSlug