[WEB - 1742] chore: user activation and deactivation workflow (#4944)
* chore: user deactivation workflow * dev: activation deactivation template
This commit is contained in:
parent
90339b1c62
commit
1a37c1542d
13 changed files with 3343 additions and 114 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
70
apiserver/plane/bgtasks/user_activation_email_task.py
Normal file
70
apiserver/plane/bgtasks/user_activation_email_task.py
Normal file
|
|
@ -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
|
||||
72
apiserver/plane/bgtasks/user_deactivation_email_task.py
Normal file
72
apiserver/plane/bgtasks/user_deactivation_email_task.py
Normal file
|
|
@ -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
|
||||
42
apiserver/plane/utils/host.py
Normal file
42
apiserver/plane/utils/host.py
Normal file
|
|
@ -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"))
|
||||
1570
apiserver/templates/emails/user/user_activation.html
Normal file
1570
apiserver/templates/emails/user/user_activation.html
Normal file
File diff suppressed because it is too large
Load diff
1571
apiserver/templates/emails/user/user_deactivation.html
Normal file
1571
apiserver/templates/emails/user/user_deactivation.html
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue