[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.cache import cache_response, invalidate_cache
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
from plane.authentication.utils.host import user_ip
|
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):
|
class UserEndpoint(BaseViewSet):
|
||||||
|
|
@ -192,6 +194,11 @@ class UserEndpoint(BaseViewSet):
|
||||||
user.last_logout_time = timezone.now()
|
user.last_logout_time = timezone.now()
|
||||||
user.save()
|
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 the user
|
||||||
logout(request)
|
logout(request)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
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 plane.license.utils.instance_value import get_configuration_value
|
||||||
from .error import AuthenticationException, AUTHENTICATION_ERROR_CODES
|
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:
|
class Adapter:
|
||||||
|
|
@ -120,6 +122,13 @@ class Adapter:
|
||||||
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
|
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
|
||||||
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
|
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
|
||||||
user.token_updated_at = timezone.now()
|
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()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
@ -168,12 +177,6 @@ class Adapter:
|
||||||
# Create profile
|
# Create profile
|
||||||
Profile.objects.create(user=user)
|
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
|
# Save user data
|
||||||
user = self.save_user_data(user=user)
|
user = self.save_user_data(user=user)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,17 +95,7 @@ class EmailCheckEndpoint(APIView):
|
||||||
|
|
||||||
# If existing user
|
# If existing user
|
||||||
if existing_user:
|
if existing_user:
|
||||||
if not existing_user.is_active:
|
# Return response
|
||||||
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,
|
"existing": True,
|
||||||
|
|
|
||||||
|
|
@ -107,22 +107,6 @@ class SignInAuthEndpoint(View):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(url)
|
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:
|
try:
|
||||||
provider = EmailProvider(
|
provider = EmailProvider(
|
||||||
request=request,
|
request=request,
|
||||||
|
|
@ -222,22 +206,6 @@ class SignUpAuthEndpoint(View):
|
||||||
|
|
||||||
if existing_user:
|
if existing_user:
|
||||||
# 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(
|
exc = AuthenticationException(
|
||||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||||
error_message="USER_ALREADY_EXIST",
|
error_message="USER_ALREADY_EXIST",
|
||||||
|
|
|
||||||
|
|
@ -112,22 +112,6 @@ class MagicSignInEndpoint(View):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(url)
|
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:
|
try:
|
||||||
provider = MagicCodeProvider(
|
provider = MagicCodeProvider(
|
||||||
request=request,
|
request=request,
|
||||||
|
|
|
||||||
|
|
@ -93,17 +93,7 @@ class EmailCheckSpaceEndpoint(APIView):
|
||||||
|
|
||||||
# If existing user
|
# If existing user
|
||||||
if existing_user:
|
if existing_user:
|
||||||
if not existing_user.is_active:
|
# Return response
|
||||||
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,
|
"existing": True,
|
||||||
|
|
|
||||||
|
|
@ -89,19 +89,6 @@ class SignInAuthSpaceEndpoint(View):
|
||||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||||
return HttpResponseRedirect(url)
|
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:
|
try:
|
||||||
provider = EmailProvider(
|
provider = EmailProvider(
|
||||||
request=request, key=email, code=password, is_signup=False
|
request=request, key=email, code=password, is_signup=False
|
||||||
|
|
@ -178,19 +165,6 @@ class SignUpAuthSpaceEndpoint(View):
|
||||||
existing_user = User.objects.filter(email=email).first()
|
existing_user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
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(
|
exc = AuthenticationException(
|
||||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||||
error_message="USER_ALREADY_EXIST",
|
error_message="USER_ALREADY_EXIST",
|
||||||
|
|
|
||||||
|
|
@ -101,18 +101,6 @@ class MagicSignInSpaceEndpoint(View):
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
# Active User
|
# 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:
|
try:
|
||||||
provider = MagicCodeProvider(
|
provider = MagicCodeProvider(
|
||||||
request=request, key=f"magic_{email}", code=code
|
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