[WEB-1309] fix: auth fixes (#4456)
* dev: magic link login and email password disable * dev: user account deactivation * dev: change nginx conf routes * feat: changemod space * fix: space app dir fixes * dev: invalidate cache for instances when creating workspace * dev: update email templates for test email * dev: fix build errors * fix: auth fixes and improvement (#4452) * chore: change password api updated and missing password error code added * chore: auth helper updated * chore: disable send code input suggestion * chore: change password function updated * fix: application error on sign in page * chore: change password validation added and enhancement * dev: space base path in web * dev: admin user deactivated * dev: user and instance admin session endpoint * fix: last_workspace_id endpoint updated * fix: magic sign in and email password check added --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com>
This commit is contained in:
parent
ab6f1ef780
commit
9b7b23f5a2
44 changed files with 1114 additions and 319 deletions
|
|
@ -11,6 +11,7 @@ from plane.app.views import (
|
|||
UserEndpoint,
|
||||
UserIssueCompletedGraphEndpoint,
|
||||
UserWorkspaceDashboardEndpoint,
|
||||
UserSessionEndpoint,
|
||||
## End User
|
||||
## Workspaces
|
||||
UserWorkSpacesEndpoint,
|
||||
|
|
@ -29,6 +30,11 @@ urlpatterns = [
|
|||
),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/session/",
|
||||
UserSessionEndpoint.as_view(),
|
||||
name="user-session",
|
||||
),
|
||||
path(
|
||||
"users/me/settings/",
|
||||
UserEndpoint.as_view(
|
||||
|
|
|
|||
|
|
@ -222,4 +222,4 @@ from .error_404 import custom_404_view
|
|||
|
||||
from .exporter.base import ExportIssuesEndpoint
|
||||
from .notification.base import MarkAllReadNotificationViewSet
|
||||
from .user.base import AccountEndpoint, ProfileEndpoint
|
||||
from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.utils import timezone
|
|||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import (
|
||||
|
|
@ -180,6 +181,25 @@ class UserEndpoint(BaseViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UserSessionEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
if request.user.is_authenticated:
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
serializer = UserMeSerializer(user)
|
||||
data = {"is_authenticated": True}
|
||||
data["user"] = serializer.data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"is_authenticated": False}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
@invalidate_cache(path="/api/users/me/")
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@invalidate_cache(path="/api/instances/", user=False)
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
|
|
@ -151,8 +152,12 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
|
||||
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/", multiple=True, user=False
|
||||
)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ class Adapter:
|
|||
user.save()
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationException(
|
||||
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
# Update user details
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ AUTHENTICATION_ERROR_CODES = {
|
|||
"INVALID_EMAIL": 5005,
|
||||
"EMAIL_REQUIRED": 5010,
|
||||
"SIGNUP_DISABLED": 5015,
|
||||
"MAGIC_LINK_LOGIN_DISABLED": 5016,
|
||||
"PASSWORD_LOGIN_DISABLED": 5018,
|
||||
"USER_ACCOUNT_DEACTIVATED": 5019,
|
||||
# Password strength
|
||||
"INVALID_PASSWORD": 5020,
|
||||
"SMTP_NOT_CONFIGURED": 5025,
|
||||
|
|
@ -35,6 +38,7 @@ AUTHENTICATION_ERROR_CODES = {
|
|||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||
# Change password
|
||||
"INCORRECT_OLD_PASSWORD": 5135,
|
||||
"MISSING_PASSWORD": 5138,
|
||||
"INVALID_NEW_PASSWORD": 5140,
|
||||
# set passowrd
|
||||
"PASSWORD_ALREADY_SET": 5145,
|
||||
|
|
@ -47,6 +51,7 @@ AUTHENTICATION_ERROR_CODES = {
|
|||
"ADMIN_AUTHENTICATION_FAILED": 5175,
|
||||
"ADMIN_USER_ALREADY_EXIST": 5180,
|
||||
"ADMIN_USER_DOES_NOT_EXIST": 5185,
|
||||
"ADMIN_USER_DEACTIVATED": 5190,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
# Python imports
|
||||
import os
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.adapter.credential import CredentialAdapter
|
||||
from plane.db.models import User
|
||||
|
|
@ -5,6 +8,7 @@ from plane.authentication.adapter.error import (
|
|||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class EmailProvider(CredentialAdapter):
|
||||
|
|
@ -23,6 +27,21 @@ class EmailProvider(CredentialAdapter):
|
|||
self.code = code
|
||||
self.is_signup = is_signup
|
||||
|
||||
(ENABLE_EMAIL_PASSWORD,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"default": os.environ.get("ENABLE_EMAIL_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if ENABLE_EMAIL_PASSWORD == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ENABLE_EMAIL_PASSWORD"],
|
||||
error_message="ENABLE_EMAIL_PASSWORD",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
if self.is_signup:
|
||||
# Check if the user already exists
|
||||
|
|
|
|||
|
|
@ -26,23 +26,20 @@ class MagicCodeProvider(CredentialAdapter):
|
|||
code=None,
|
||||
):
|
||||
|
||||
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
|
||||
get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
(
|
||||
EMAIL_HOST,
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if not (EMAIL_HOST):
|
||||
|
|
@ -52,6 +49,15 @@ class MagicCodeProvider(CredentialAdapter):
|
|||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
if ENABLE_MAGIC_LINK_LOGIN == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"MAGIC_LINK_LOGIN_DISABLED"
|
||||
],
|
||||
error_message="MAGIC_LINK_LOGIN_DISABLED",
|
||||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
super().__init__(request, self.provider)
|
||||
self.key = key
|
||||
self.code = code
|
||||
|
|
|
|||
|
|
@ -24,62 +24,59 @@ class EmailCheckSignUpEndpoint(APIView):
|
|||
]
|
||||
|
||||
def post(self, request):
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# check if the account is the deactivated
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ALREADY_EXIST"
|
||||
],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class EmailCheckSignInEndpoint(APIView):
|
||||
|
||||
|
|
@ -88,61 +85,59 @@ class EmailCheckSignInEndpoint(APIView):
|
|||
]
|
||||
|
||||
def post(self, request):
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
# Raise different exception when user is not active
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -90,7 +90,9 @@ class SignInAuthEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
|
|
@ -105,6 +107,22 @@ 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, key=email, code=password, is_signup=False
|
||||
|
|
@ -197,7 +215,26 @@ class SignUpAuthEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -95,7 +95,26 @@ class MagicSignInEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not 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),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
|
|
@ -167,8 +186,25 @@ class MagicSignUpEndpoint(View):
|
|||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing user
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
if not 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)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
|
|
|||
|
|
@ -63,23 +63,13 @@ class ForgotPasswordEndpoint(APIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
|
||||
get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_USER",
|
||||
"default": os.environ.get("EMAIL_HOST_USER"),
|
||||
},
|
||||
{
|
||||
"key": "EMAIL_HOST_PASSWORD",
|
||||
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
(EMAIL_HOST,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if not (EMAIL_HOST):
|
||||
|
|
|
|||
|
|
@ -36,55 +36,60 @@ class CSRFTokenEndpoint(APIView):
|
|||
|
||||
class ChangePasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
if serializer.is_valid():
|
||||
if not user.check_password(serializer.data.get("old_password")):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check the password score
|
||||
results = zxcvbn(serializer.data.get("new_password"))
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
old_password = request.data.get("old_password", False)
|
||||
new_password = request.data.get("new_password", False)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
if not old_password or not new_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old or new password is missing"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
if not user.check_password(old_password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check the password score
|
||||
results = zxcvbn(new_password)
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(new_password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
class SetUserPasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,17 @@ 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(
|
||||
{
|
||||
"existing": True,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,10 @@ class SignInAuthSpaceEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
|
|
@ -98,6 +101,22 @@ class SignInAuthSpaceEndpoint(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_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
try:
|
||||
provider = EmailProvider(
|
||||
request=request, key=email, code=password, is_signup=False
|
||||
|
|
@ -185,7 +204,26 @@ class SignUpAuthSpaceEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
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 = urljoin(
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -90,11 +90,14 @@ class MagicSignInSpaceEndpoint(View):
|
|||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
params = {
|
||||
"error_code": "USER_DOES_NOT_EXIST",
|
||||
"error_message": "User could not be found with the given email.",
|
||||
}
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
|
|
@ -103,6 +106,22 @@ 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 = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
provider = MagicCodeProvider(
|
||||
request=request, key=f"magic_{email}", code=code
|
||||
|
|
@ -155,8 +174,25 @@ class MagicSignUpSpaceEndpoint(View):
|
|||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
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 = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
# Python imports
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
from django.contrib.auth import logout
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Module imports
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
|
|
@ -37,10 +40,10 @@ class Command(BaseCommand):
|
|||
timeout=30,
|
||||
)
|
||||
# Prepare email details
|
||||
subject = "Email Notification from Plane"
|
||||
message = (
|
||||
"This is a sample email notification sent from Plane application."
|
||||
)
|
||||
subject = "Test email from Plane"
|
||||
|
||||
html_content = render_to_string("emails/test_email.html")
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
|
||||
|
||||
|
|
@ -48,11 +51,14 @@ class Command(BaseCommand):
|
|||
try:
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=message,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver_email],
|
||||
to=[
|
||||
receiver_email,
|
||||
],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
self.stdout.write(self.style.SUCCESS("Email successfully sent"))
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ from .admin import (
|
|||
InstanceAdminSignUpEndpoint,
|
||||
InstanceAdminUserMeEndpoint,
|
||||
InstanceAdminSignOutEndpoint,
|
||||
InstanceAdminUserSessionEndpoint,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -316,6 +316,20 @@ class InstanceAdminSignInEndpoint(View):
|
|||
# Fetch the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# is_active
|
||||
if not user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"ADMIN_USER_DEACTIVATED"
|
||||
],
|
||||
error_message="ADMIN_USER_DEACTIVATED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Error out if the user is not present
|
||||
if not user:
|
||||
exc = AuthenticationException(
|
||||
|
|
@ -395,6 +409,30 @@ class InstanceAdminUserMeEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
|
||||
class InstanceAdminUserSessionEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
if (
|
||||
request.user.is_authenticated
|
||||
and InstanceAdmin.objects.filter(user=request.user).exists()
|
||||
):
|
||||
serializer = InstanceAdminMeSerializer(request.user)
|
||||
data = {"is_authenticated": True}
|
||||
data["user"] = serializer.data
|
||||
return Response(
|
||||
data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"is_authenticated": False}, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class InstanceAdminSignOutEndpoint(View):
|
||||
|
||||
permission_classes = [
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from plane.license.api.views import (
|
|||
SignUpScreenVisitedEndpoint,
|
||||
InstanceAdminUserMeEndpoint,
|
||||
InstanceAdminSignOutEndpoint,
|
||||
InstanceAdminUserSessionEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -28,6 +29,11 @@ urlpatterns = [
|
|||
InstanceAdminUserMeEndpoint.as_view(),
|
||||
name="instance-admins",
|
||||
),
|
||||
path(
|
||||
"admins/session/",
|
||||
InstanceAdminUserSessionEndpoint.as_view(),
|
||||
name="instance-admin-session",
|
||||
),
|
||||
path(
|
||||
"admins/sign-out/",
|
||||
InstanceAdminSignOutEndpoint.as_view(),
|
||||
|
|
|
|||
6
apiserver/templates/emails/test_email.html
Normal file
6
apiserver/templates/emails/test_email.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<p>This is a test email sent to verify if email configuration is working as expected in your Plane instance.</p>
|
||||
|
||||
<p>Regards,</br> Team Plane </p>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue