diff --git a/apiserver/plane/app/views/user/base.py b/apiserver/plane/app/views/user/base.py
index 5d757ef57..ac0c3d711 100644
--- a/apiserver/plane/app/views/user/base.py
+++ b/apiserver/plane/app/views/user/base.py
@@ -35,6 +35,8 @@ from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
+from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
+from plane.utils.host import base_host
class UserEndpoint(BaseViewSet):
@@ -192,6 +194,11 @@ class UserEndpoint(BaseViewSet):
user.last_logout_time = timezone.now()
user.save()
+ # Send an email to the user
+ user_deactivation_email.delay(
+ base_host(request=request, is_app=True), user.id
+ )
+
# Logout the user
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/authentication/adapter/base.py b/apiserver/plane/authentication/adapter/base.py
index 5876e934f..906d55700 100644
--- a/apiserver/plane/authentication/adapter/base.py
+++ b/apiserver/plane/authentication/adapter/base.py
@@ -18,6 +18,8 @@ from plane.db.models import (
)
from plane.license.utils.instance_value import get_configuration_value
from .error import AuthenticationException, AUTHENTICATION_ERROR_CODES
+from plane.bgtasks.user_activation_email_task import user_activation_email
+from plane.authentication.utils.host import base_host
class Adapter:
@@ -120,6 +122,13 @@ class Adapter:
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
user.token_updated_at = timezone.now()
+ # If user is not active, send the activation email and set the user as active
+ if not user.is_active:
+ user_activation_email.delay(
+ base_host(request=self.request), user.id
+ )
+ # Set user as active
+ user.is_active = True
user.save()
return user
@@ -168,12 +177,6 @@ class Adapter:
# Create profile
Profile.objects.create(user=user)
- if not user.is_active:
- raise AuthenticationException(
- AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
-
# Save user data
user = self.save_user_data(user=user)
diff --git a/apiserver/plane/authentication/views/app/check.py b/apiserver/plane/authentication/views/app/check.py
index 5b3ac7337..6af8859fc 100644
--- a/apiserver/plane/authentication/views/app/check.py
+++ b/apiserver/plane/authentication/views/app/check.py
@@ -95,17 +95,7 @@ class EmailCheckEndpoint(APIView):
# If existing user
if existing_user:
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- return Response(
- exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
- )
-
+ # Return response
return Response(
{
"existing": True,
diff --git a/apiserver/plane/authentication/views/app/email.py b/apiserver/plane/authentication/views/app/email.py
index f21e431a4..08a3e8b01 100644
--- a/apiserver/plane/authentication/views/app/email.py
+++ b/apiserver/plane/authentication/views/app/email.py
@@ -107,22 +107,6 @@ class SignInAuthEndpoint(View):
)
return HttpResponseRedirect(url)
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = urljoin(
- base_host(request=request, is_app=True),
- "sign-in?" + urlencode(params),
- )
- return HttpResponseRedirect(url)
-
try:
provider = EmailProvider(
request=request,
@@ -222,22 +206,6 @@ class SignUpAuthEndpoint(View):
if existing_user:
# Existing User
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = urljoin(
- base_host(request=request, is_app=True),
- "?" + urlencode(params),
- )
- return HttpResponseRedirect(url)
-
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
error_message="USER_ALREADY_EXIST",
diff --git a/apiserver/plane/authentication/views/app/magic.py b/apiserver/plane/authentication/views/app/magic.py
index bb3c72534..8b2d1accd 100644
--- a/apiserver/plane/authentication/views/app/magic.py
+++ b/apiserver/plane/authentication/views/app/magic.py
@@ -112,22 +112,6 @@ class MagicSignInEndpoint(View):
)
return HttpResponseRedirect(url)
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = urljoin(
- base_host(request=request, is_app=True),
- "sign-in?" + urlencode(params),
- )
- return HttpResponseRedirect(url)
-
try:
provider = MagicCodeProvider(
request=request,
diff --git a/apiserver/plane/authentication/views/space/check.py b/apiserver/plane/authentication/views/space/check.py
index a86a29c09..560ae0e31 100644
--- a/apiserver/plane/authentication/views/space/check.py
+++ b/apiserver/plane/authentication/views/space/check.py
@@ -93,17 +93,7 @@ class EmailCheckSpaceEndpoint(APIView):
# If existing user
if existing_user:
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- return Response(
- exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
- )
-
+ # Return response
return Response(
{
"existing": True,
diff --git a/apiserver/plane/authentication/views/space/email.py b/apiserver/plane/authentication/views/space/email.py
index 7a5613a75..4329ed26d 100644
--- a/apiserver/plane/authentication/views/space/email.py
+++ b/apiserver/plane/authentication/views/space/email.py
@@ -89,19 +89,6 @@ class SignInAuthSpaceEndpoint(View):
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
return HttpResponseRedirect(url)
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
- return HttpResponseRedirect(url)
-
try:
provider = EmailProvider(
request=request, key=email, code=password, is_signup=False
@@ -178,19 +165,6 @@ class SignUpAuthSpaceEndpoint(View):
existing_user = User.objects.filter(email=email).first()
if existing_user:
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
- return HttpResponseRedirect(url)
-
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
error_message="USER_ALREADY_EXIST",
diff --git a/apiserver/plane/authentication/views/space/magic.py b/apiserver/plane/authentication/views/space/magic.py
index 52a1d0422..838039f96 100644
--- a/apiserver/plane/authentication/views/space/magic.py
+++ b/apiserver/plane/authentication/views/space/magic.py
@@ -101,18 +101,6 @@ class MagicSignInSpaceEndpoint(View):
return HttpResponseRedirect(url)
# Active User
- if not existing_user.is_active:
- exc = AuthenticationException(
- error_code=AUTHENTICATION_ERROR_CODES[
- "USER_ACCOUNT_DEACTIVATED"
- ],
- error_message="USER_ACCOUNT_DEACTIVATED",
- )
- params = exc.get_error_dict()
- if next_path:
- params["next_path"] = str(next_path)
- url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
- return HttpResponseRedirect(url)
try:
provider = MagicCodeProvider(
request=request, key=f"magic_{email}", code=code
diff --git a/apiserver/plane/bgtasks/user_activation_email_task.py b/apiserver/plane/bgtasks/user_activation_email_task.py
new file mode 100644
index 000000000..2fdfc4ddb
--- /dev/null
+++ b/apiserver/plane/bgtasks/user_activation_email_task.py
@@ -0,0 +1,70 @@
+# Python imports
+import logging
+
+# Django imports
+from django.core.mail import EmailMultiAlternatives, get_connection
+from django.template.loader import render_to_string
+from django.utils.html import strip_tags
+
+# Third party imports
+from celery import shared_task
+
+# Module imports
+from plane.db.models import User
+from plane.license.utils.instance_value import get_email_configuration
+from plane.utils.exception_logger import log_exception
+
+
+@shared_task
+def user_activation_email(current_site, user_id):
+ try:
+ # Send email to user when account is activated
+ user = User.objects.get(id=user_id)
+ subject = f"{user.first_name or user.display_name or user.email} has been activated on Plane"
+
+ context = {
+ "email": str(user.email),
+ "profile_url": current_site + "/profile",
+ }
+
+ # Send email to user
+ html_content = render_to_string(
+ "emails/user/user_activation.html", context
+ )
+
+ text_content = strip_tags(html_content)
+ # Configure email connection from the database
+ (
+ EMAIL_HOST,
+ EMAIL_HOST_USER,
+ EMAIL_HOST_PASSWORD,
+ EMAIL_PORT,
+ EMAIL_USE_TLS,
+ EMAIL_USE_SSL,
+ EMAIL_FROM,
+ ) = get_email_configuration()
+
+ 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",
+ )
+
+ msg = EmailMultiAlternatives(
+ subject=subject,
+ body=text_content,
+ from_email=EMAIL_FROM,
+ to=[user.email],
+ connection=connection,
+ )
+
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+ logging.getLogger("plane").info("Email sent successfully.")
+ return
+ except Exception as e:
+ log_exception(e)
+ return
diff --git a/apiserver/plane/bgtasks/user_deactivation_email_task.py b/apiserver/plane/bgtasks/user_deactivation_email_task.py
new file mode 100644
index 000000000..fa8523d50
--- /dev/null
+++ b/apiserver/plane/bgtasks/user_deactivation_email_task.py
@@ -0,0 +1,72 @@
+# Python imports
+import logging
+
+# Django imports
+from django.core.mail import EmailMultiAlternatives, get_connection
+from django.template.loader import render_to_string
+from django.utils.html import strip_tags
+
+# Third party imports
+from celery import shared_task
+
+# Module imports
+from plane.db.models import User
+from plane.license.utils.instance_value import get_email_configuration
+from plane.utils.exception_logger import log_exception
+
+
+@shared_task
+def user_deactivation_email(current_site, user_id):
+ try:
+ # Send email to user when account is deactivated
+ user = User.objects.get(id=user_id)
+ subject = f"{user.first_name or user.display_name or user.email} has been deactivated on Plane"
+
+ context = {
+ "email": str(user.email),
+ "login_url": current_site + "/login",
+ }
+
+ # Send email to user
+ html_content = render_to_string(
+ "emails/user/user_deactivation.html", context
+ )
+
+ text_content = strip_tags(html_content)
+ # Configure email connection from the database
+ (
+ EMAIL_HOST,
+ EMAIL_HOST_USER,
+ EMAIL_HOST_PASSWORD,
+ EMAIL_PORT,
+ EMAIL_USE_TLS,
+ EMAIL_USE_SSL,
+ EMAIL_FROM,
+ ) = get_email_configuration()
+
+ 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",
+ )
+
+ # Send email
+ msg = EmailMultiAlternatives(
+ subject=subject,
+ body=text_content,
+ from_email=EMAIL_FROM,
+ to=[user.email],
+ connection=connection,
+ )
+
+ # Attach HTML content
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+ logging.getLogger("plane").info("Email sent successfully.")
+ return
+ except Exception as e:
+ log_exception(e)
+ return
diff --git a/apiserver/plane/utils/host.py b/apiserver/plane/utils/host.py
new file mode 100644
index 000000000..4046c1e20
--- /dev/null
+++ b/apiserver/plane/utils/host.py
@@ -0,0 +1,42 @@
+# Python imports
+from urllib.parse import urlsplit
+
+# Django imports
+from django.conf import settings
+
+
+def base_host(request, is_admin=False, is_space=False, is_app=False):
+ """Utility function to return host / origin from the request"""
+ # Calculate the base origin from request
+ base_origin = str(
+ request.META.get("HTTP_ORIGIN")
+ or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
+ or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
+ )
+
+ # Admin redirections
+ if is_admin:
+ if settings.ADMIN_BASE_URL:
+ return settings.ADMIN_BASE_URL
+ else:
+ return base_origin + "/god-mode/"
+
+ # Space redirections
+ if is_space:
+ if settings.SPACE_BASE_URL:
+ return settings.SPACE_BASE_URL
+ else:
+ return base_origin + "/spaces/"
+
+ # App Redirection
+ if is_app:
+ if settings.APP_BASE_URL:
+ return settings.APP_BASE_URL
+ else:
+ return base_origin
+
+ return base_origin
+
+
+def user_ip(request):
+ return str(request.META.get("REMOTE_ADDR"))
diff --git a/apiserver/templates/emails/user/user_activation.html b/apiserver/templates/emails/user/user_activation.html
new file mode 100644
index 000000000..1ec60e955
--- /dev/null
+++ b/apiserver/templates/emails/user/user_activation.html
@@ -0,0 +1,1570 @@
+
+
+
+
+
+
+
+ Your Plane account is now active
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You are back inside Plane
+ again.
+
+
+ |
+
+
+
+
+
+ Your account is reactivated and you
+ can now access your issues and other
+ data if the project is still active.
+ Alternatively, you can create a new
+ workspace and start adding projects
+ to it.
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+
+ Despite our popularity, we
+ are humbly early-stage. We
+ are shipping fast, so please
+ reach out to us with feature
+ requests, major and minor
+ nits, and anything else you
+ find missing. We read every message, tweet, and conversation
+ and update our public roadmap.
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This email was sent to
+ {{email}}. Please delete if
+ you aren't the intended
+ recipient.
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
diff --git a/apiserver/templates/emails/user/user_deactivation.html b/apiserver/templates/emails/user/user_deactivation.html
new file mode 100644
index 000000000..b6bc7b768
--- /dev/null
+++ b/apiserver/templates/emails/user/user_deactivation.html
@@ -0,0 +1,1571 @@
+
+
+
+
+
+
+
+ Your Plane account has been deactivated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You are out of Plane
+ now.
+
+
+ |
+
+
+
+
+
+ You have deactivated your account
+ successfully. Your issues, cycles,
+ and other project data will still be
+ accessible if the project is active
+ and there are other members in it.
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+
+ Despite our popularity, we
+ are humbly early-stage. We
+ are shipping fast, so please
+ reach out to us with feature
+ requests, major and minor
+ nits, and anything else you
+ find missing. We read every message, tweet, and conversation
+ and update our public roadmap.
+
+
+ |
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This email was sent to
+ {{email}}. Please delete if
+ you aren't the intended
+ recipient.
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+