[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:
Anmol Singh Bhatia 2024-05-14 20:53:51 +05:30 committed by GitHub
parent ab6f1ef780
commit 9b7b23f5a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1114 additions and 319 deletions

View File

@ -14,6 +14,7 @@
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/constants": "*",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",

View File

@ -11,6 +11,7 @@ from plane.app.views import (
UserEndpoint, UserEndpoint,
UserIssueCompletedGraphEndpoint, UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint, UserWorkspaceDashboardEndpoint,
UserSessionEndpoint,
## End User ## End User
## Workspaces ## Workspaces
UserWorkSpacesEndpoint, UserWorkSpacesEndpoint,
@ -29,6 +30,11 @@ urlpatterns = [
), ),
name="users", name="users",
), ),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path( path(
"users/me/settings/", "users/me/settings/",
UserEndpoint.as_view( UserEndpoint.as_view(

View File

@ -222,4 +222,4 @@ from .error_404 import custom_404_view
from .exporter.base import ExportIssuesEndpoint from .exporter.base import ExportIssuesEndpoint
from .notification.base import MarkAllReadNotificationViewSet from .notification.base import MarkAllReadNotificationViewSet
from .user.base import AccountEndpoint, ProfileEndpoint from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint

View File

@ -6,6 +6,7 @@ from django.utils import timezone
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny
# Module imports # Module imports
from plane.app.serializers import ( from plane.app.serializers import (
@ -180,6 +181,25 @@ class UserEndpoint(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) 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): class UpdateUserOnBoardedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/") @invalidate_cache(path="/api/users/me/")

View File

@ -96,6 +96,7 @@ class WorkSpaceViewSet(BaseViewSet):
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/") @invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/instances/", user=False)
def create(self, request): def create(self, request):
try: try:
serializer = WorkSpaceSerializer(data=request.data) serializer = WorkSpaceSerializer(data=request.data)
@ -151,8 +152,12 @@ class WorkSpaceViewSet(BaseViewSet):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False) @invalidate_cache(
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False) 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): def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs) return super().destroy(request, *args, **kwargs)

View File

@ -100,6 +100,12 @@ class Adapter:
user.save() user.save()
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",
)
# Update user details # Update user details
user.last_login_medium = self.provider user.last_login_medium = self.provider
user.last_active = timezone.now() user.last_active = timezone.now()

View File

@ -4,6 +4,9 @@ AUTHENTICATION_ERROR_CODES = {
"INVALID_EMAIL": 5005, "INVALID_EMAIL": 5005,
"EMAIL_REQUIRED": 5010, "EMAIL_REQUIRED": 5010,
"SIGNUP_DISABLED": 5015, "SIGNUP_DISABLED": 5015,
"MAGIC_LINK_LOGIN_DISABLED": 5016,
"PASSWORD_LOGIN_DISABLED": 5018,
"USER_ACCOUNT_DEACTIVATED": 5019,
# Password strength # Password strength
"INVALID_PASSWORD": 5020, "INVALID_PASSWORD": 5020,
"SMTP_NOT_CONFIGURED": 5025, "SMTP_NOT_CONFIGURED": 5025,
@ -35,6 +38,7 @@ AUTHENTICATION_ERROR_CODES = {
"EXPIRED_PASSWORD_TOKEN": 5130, "EXPIRED_PASSWORD_TOKEN": 5130,
# Change password # Change password
"INCORRECT_OLD_PASSWORD": 5135, "INCORRECT_OLD_PASSWORD": 5135,
"MISSING_PASSWORD": 5138,
"INVALID_NEW_PASSWORD": 5140, "INVALID_NEW_PASSWORD": 5140,
# set passowrd # set passowrd
"PASSWORD_ALREADY_SET": 5145, "PASSWORD_ALREADY_SET": 5145,
@ -47,6 +51,7 @@ AUTHENTICATION_ERROR_CODES = {
"ADMIN_AUTHENTICATION_FAILED": 5175, "ADMIN_AUTHENTICATION_FAILED": 5175,
"ADMIN_USER_ALREADY_EXIST": 5180, "ADMIN_USER_ALREADY_EXIST": 5180,
"ADMIN_USER_DOES_NOT_EXIST": 5185, "ADMIN_USER_DOES_NOT_EXIST": 5185,
"ADMIN_USER_DEACTIVATED": 5190,
} }

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Module imports # Module imports
from plane.authentication.adapter.credential import CredentialAdapter from plane.authentication.adapter.credential import CredentialAdapter
from plane.db.models import User from plane.db.models import User
@ -5,6 +8,7 @@ from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES, AUTHENTICATION_ERROR_CODES,
AuthenticationException, AuthenticationException,
) )
from plane.license.utils.instance_value import get_configuration_value
class EmailProvider(CredentialAdapter): class EmailProvider(CredentialAdapter):
@ -23,6 +27,21 @@ class EmailProvider(CredentialAdapter):
self.code = code self.code = code
self.is_signup = is_signup 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): def set_user_data(self):
if self.is_signup: if self.is_signup:
# Check if the user already exists # Check if the user already exists

View File

@ -26,23 +26,20 @@ class MagicCodeProvider(CredentialAdapter):
code=None, code=None,
): ):
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = ( (
get_configuration_value( EMAIL_HOST,
[ ENABLE_MAGIC_LINK_LOGIN,
{ ) = get_configuration_value(
"key": "EMAIL_HOST", [
"default": os.environ.get("EMAIL_HOST"), {
}, "key": "EMAIL_HOST",
{ "default": os.environ.get("EMAIL_HOST"),
"key": "EMAIL_HOST_USER", },
"default": os.environ.get("EMAIL_HOST_USER"), {
}, "key": "ENABLE_MAGIC_LINK_LOGIN",
{ "default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
"key": "EMAIL_HOST_PASSWORD", },
"default": os.environ.get("EMAIL_HOST_PASSWORD"), ]
},
]
)
) )
if not (EMAIL_HOST): if not (EMAIL_HOST):
@ -52,6 +49,15 @@ class MagicCodeProvider(CredentialAdapter):
payload={"email": str(self.key)}, 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) super().__init__(request, self.provider)
self.key = key self.key = key
self.code = code self.code = code

View File

@ -24,62 +24,59 @@ class EmailCheckSignUpEndpoint(APIView):
] ]
def post(self, request): 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: 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) 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: except ValidationError:
exc = AuthenticationException( raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"], error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL", error_message="INVALID_EMAIL",
) )
except AuthenticationException as e:
return Response( return Response(
exc.get_error_dict(), e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
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): class EmailCheckSignInEndpoint(APIView):
@ -88,61 +85,59 @@ class EmailCheckSignInEndpoint(APIView):
] ]
def post(self, request): 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: 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) 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: except ValidationError:
exc = AuthenticationException( raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"], error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL", error_message="INVALID_EMAIL",
) )
except AuthenticationException as e:
return Response( return Response(
exc.get_error_dict(), e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
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,
)

View File

@ -90,7 +90,9 @@ class SignInAuthEndpoint(View):
) )
return HttpResponseRedirect(url) 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( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"], error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST", error_message="USER_DOES_NOT_EXIST",
@ -105,6 +107,22 @@ 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, key=email, code=password, is_signup=False request=request, key=email, code=password, is_signup=False
@ -197,7 +215,26 @@ class SignUpAuthEndpoint(View):
) )
return HttpResponseRedirect(url) 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( 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",

View File

@ -95,7 +95,26 @@ class MagicSignInEndpoint(View):
) )
return HttpResponseRedirect(url) 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( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"], error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST", error_message="USER_DOES_NOT_EXIST",
@ -167,8 +186,25 @@ class MagicSignUpEndpoint(View):
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) 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( 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",

View File

@ -63,23 +63,13 @@ class ForgotPasswordEndpoint(APIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = ( (EMAIL_HOST,) = get_configuration_value(
get_configuration_value( [
[ {
{ "key": "EMAIL_HOST",
"key": "EMAIL_HOST", "default": os.environ.get("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"),
},
]
)
) )
if not (EMAIL_HOST): if not (EMAIL_HOST):

View File

@ -36,55 +36,60 @@ class CSRFTokenEndpoint(APIView):
class ChangePasswordEndpoint(APIView): class ChangePasswordEndpoint(APIView):
def post(self, request): def post(self, request):
serializer = ChangePasswordSerializer(data=request.data)
user = User.objects.get(pk=request.user.id) 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 old_password = request.data.get("old_password", False)
results = zxcvbn(serializer.data.get("new_password")) new_password = request.data.get("new_password", False)
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 if not old_password or not new_password:
user.set_password(serializer.data.get("new_password")) exc = AuthenticationException(
user.is_password_autoset = False error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
user.save() error_message="MISSING_PASSWORD",
user_login(user=user, request=request, is_app=True) payload={"error": "Old or new password is missing"},
return Response( )
{"message": "Password updated successfully"}, return Response(
status=status.HTTP_200_OK, 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): class SetUserPasswordEndpoint(APIView):
def post(self, request): def post(self, request):
user = User.objects.get(pk=request.user.id) user = User.objects.get(pk=request.user.id)

View File

@ -68,6 +68,17 @@ class EmailCheckEndpoint(APIView):
# If existing user # If existing user
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, "existing": True,

View File

@ -83,7 +83,10 @@ class SignInAuthSpaceEndpoint(View):
) )
return HttpResponseRedirect(url) 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( exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"], error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST", error_message="USER_DOES_NOT_EXIST",
@ -98,6 +101,22 @@ class SignInAuthSpaceEndpoint(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_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
@ -185,7 +204,26 @@ class SignUpAuthSpaceEndpoint(View):
) )
return HttpResponseRedirect(url) 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( 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",

View File

@ -90,11 +90,14 @@ class MagicSignInSpaceEndpoint(View):
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
if not User.objects.filter(email=email).exists(): existing_user = User.objects.filter(email=email).first()
params = {
"error_code": "USER_DOES_NOT_EXIST", if not existing_user:
"error_message": "User could not be found with the given email.", 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: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
@ -103,6 +106,22 @@ class MagicSignInSpaceEndpoint(View):
) )
return HttpResponseRedirect(url) 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: try:
provider = MagicCodeProvider( provider = MagicCodeProvider(
request=request, key=f"magic_{email}", code=code request=request, key=f"magic_{email}", code=code
@ -155,8 +174,25 @@ class MagicSignUpSpaceEndpoint(View):
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) 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( 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",

View File

@ -1,6 +1,3 @@
# Python imports
from urllib.parse import urlencode, urljoin
# Django imports # Django imports
from django.views import View from django.views import View
from django.contrib.auth import logout from django.contrib.auth import logout

View File

@ -1,6 +1,9 @@
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.management import BaseCommand, CommandError 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 from plane.license.utils.instance_value import get_email_configuration
@ -37,10 +40,10 @@ class Command(BaseCommand):
timeout=30, timeout=30,
) )
# Prepare email details # Prepare email details
subject = "Email Notification from Plane" subject = "Test email from Plane"
message = (
"This is a sample email notification sent from Plane application." 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...")) self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
@ -48,11 +51,14 @@ class Command(BaseCommand):
try: try:
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject=subject, subject=subject,
body=message, body=text_content,
from_email=EMAIL_FROM, from_email=EMAIL_FROM,
to=[receiver_email], to=[
receiver_email,
],
connection=connection, connection=connection,
) )
msg.attach_alternative(html_content, "text/html")
msg.send() msg.send()
self.stdout.write(self.style.SUCCESS("Email successfully sent")) self.stdout.write(self.style.SUCCESS("Email successfully sent"))
except Exception as e: except Exception as e:

View File

@ -16,4 +16,5 @@ from .admin import (
InstanceAdminSignUpEndpoint, InstanceAdminSignUpEndpoint,
InstanceAdminUserMeEndpoint, InstanceAdminUserMeEndpoint,
InstanceAdminSignOutEndpoint, InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
) )

View File

@ -316,6 +316,20 @@ class InstanceAdminSignInEndpoint(View):
# Fetch the user # Fetch the user
user = User.objects.filter(email=email).first() 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 # Error out if the user is not present
if not user: if not user:
exc = AuthenticationException( 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): class InstanceAdminSignOutEndpoint(View):
permission_classes = [ permission_classes = [

View File

@ -10,6 +10,7 @@ from plane.license.api.views import (
SignUpScreenVisitedEndpoint, SignUpScreenVisitedEndpoint,
InstanceAdminUserMeEndpoint, InstanceAdminUserMeEndpoint,
InstanceAdminSignOutEndpoint, InstanceAdminSignOutEndpoint,
InstanceAdminUserSessionEndpoint,
) )
urlpatterns = [ urlpatterns = [
@ -28,6 +29,11 @@ urlpatterns = [
InstanceAdminUserMeEndpoint.as_view(), InstanceAdminUserMeEndpoint.as_view(),
name="instance-admins", name="instance-admins",
), ),
path(
"admins/session/",
InstanceAdminUserSessionEndpoint.as_view(),
name="instance-admin-session",
),
path( path(
"admins/sign-out/", "admins/sign-out/",
InstanceAdminSignOutEndpoint.as_view(), InstanceAdminSignOutEndpoint.as_view(),

View 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>

View File

@ -28,7 +28,7 @@ http {
} }
location /god-mode/ { location /god-mode/ {
proxy_pass http://admin:3000/god-mode/; proxy_pass http://admin:3001/god-mode/;
} }
location /api/ { location /api/ {
@ -45,7 +45,7 @@ http {
location /spaces/ { location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break; rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://space:4000/spaces/; proxy_pass http://space:3002/spaces/;
} }
location /${BUCKET_NAME}/ { location /${BUCKET_NAME}/ {

View File

@ -12,7 +12,8 @@
"packages/tailwind-config-custom", "packages/tailwind-config-custom",
"packages/tsconfig", "packages/tsconfig",
"packages/ui", "packages/ui",
"packages/types" "packages/types",
"packages/constants"
], ],
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
@ -25,7 +26,7 @@
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint-config-custom": "*", "eslint-config-custom": "*",
"postcss": "^8.4.38", "postcss": "^8.4.29",
"prettier": "latest", "prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4", "prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",

View File

@ -0,0 +1,10 @@
{
"name": "@plane/constants",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
}
}

View File

@ -0,0 +1,371 @@
import { ReactNode } from "react";
import Link from "next/link";
export enum EPageTypes {
PUBLIC = "PUBLIC",
NON_AUTHENTICATED = "NON_AUTHENTICATED",
SET_PASSWORD = "SET_PASSWORD",
ONBOARDING = "ONBOARDING",
AUTHENTICATED = "AUTHENTICATED",
}
export enum EAuthModes {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
// TODO: remove this
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export enum EAuthErrorCodes {
// Global
INSTANCE_NOT_CONFIGURED = "5000",
INVALID_EMAIL = "5005",
EMAIL_REQUIRED = "5010",
SIGNUP_DISABLED = "5015",
MAGIC_LINK_LOGIN_DISABLED = "5017",
PASSWORD_LOGIN_DISABLED = "5019",
SMTP_NOT_CONFIGURED = "5025",
// Password strength
INVALID_PASSWORD = "5020",
// Sign Up
USER_ACCOUNT_DEACTIVATED = "5019",
USER_ALREADY_EXIST = "5030",
AUTHENTICATION_FAILED_SIGN_UP = "5035",
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5040",
INVALID_EMAIL_SIGN_UP = "5045",
INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
// Sign In
USER_DOES_NOT_EXIST = "5060",
AUTHENTICATION_FAILED_SIGN_IN = "5065",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
INVALID_EMAIL_SIGN_IN = "5075",
INVALID_EMAIL_MAGIC_SIGN_IN = "5080",
MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED = "5085",
// Both Sign in and Sign up for magic
INVALID_MAGIC_CODE = "5090",
EXPIRED_MAGIC_CODE = "5095",
EMAIL_CODE_ATTEMPT_EXHAUSTED = "5100",
// Oauth
GOOGLE_NOT_CONFIGURED = "5105",
GITHUB_NOT_CONFIGURED = "5110",
GOOGLE_OAUTH_PROVIDER_ERROR = "5115",
GITHUB_OAUTH_PROVIDER_ERROR = "5120",
// Reset Password
INVALID_PASSWORD_TOKEN = "5125",
EXPIRED_PASSWORD_TOKEN = "5130",
// Change password
INCORRECT_OLD_PASSWORD = "5135",
MISSING_PASSWORD= "5138",
INVALID_NEW_PASSWORD = "5140",
// set passowrd
PASSWORD_ALREADY_SET = "5145",
// Admin
ADMIN_ALREADY_EXIST = "5150",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
INVALID_ADMIN_EMAIL = "5160",
INVALID_ADMIN_PASSWORD = "5165",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthErrorCodes;
title: string;
message: ReactNode;
};
const errorCodeMessages: {
[key in EAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
// global
[EAuthErrorCodes.INSTANCE_NOT_CONFIGURED]: {
title: `Instance not configured`,
message: () => `Instance not configured. Please contact your administrator.`,
},
[EAuthErrorCodes.SIGNUP_DISABLED]: {
title: `Sign up disabled`,
message: () => `Sign up disabled. Please contact your administrator.`,
},
[EAuthErrorCodes.INVALID_PASSWORD]: {
title: `Invalid password`,
message: () => `Invalid password. Please try again.`,
},
[EAuthErrorCodes.SMTP_NOT_CONFIGURED]: {
title: `SMTP not configured`,
message: () => `SMTP not configured. Please contact your administrator.`,
},
// email check in both sign up and sign in
[EAuthErrorCodes.INVALID_EMAIL]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_REQUIRED]: {
title: `Email required`,
message: () => `Email required. Please try again.`,
},
// sign up
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
title: `User already exists`,
message: (email = undefined) => (
<div>
Your account is already registered.&nbsp;
<Link
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
href={`/sign-in${email ? `?email=${email}` : ``}`}
>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED]: {
title: `Email and code required`,
message: () => `Email and code required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
// sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
title: `User does not exist`,
message: (email = undefined) => (
<div>
No account found.&nbsp;
<Link
className="underline underline-offset-4 font-medium hover:font-bold transition-all"
href={`/${email ? `?email=${email}` : ``}`}
>
Create one
</Link>
&nbsp;to get started.
</div>
),
},
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
[EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED]: {
title: `Email and code required`,
message: () => `Email and code required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
// Both Sign in and Sign up
[EAuthenticationErrorCodes.INVALID_MAGIC_CODE]: {
title: `Authentication failed`,
message: () => `Invalid magic code. Please try again.`,
},
[EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE]: {
title: `Expired magic code`,
message: () => `Expired magic code. Please try again.`,
},
[EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED]: {
title: `Expired magic code`,
message: () => `Expired magic code. Please try again.`,
},
// Oauth
[EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED]: {
title: `Google not configured`,
message: () => `Google not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED]: {
title: `GitHub not configured`,
message: () => `GitHub not configured. Please contact your administrator.`,
},
[EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR]: {
title: `Google OAuth provider error`,
message: () => `Google OAuth provider error. Please try again.`,
},
[EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR]: {
title: `GitHub OAuth provider error`,
message: () => `GitHub OAuth provider error. Please try again.`,
},
// Reset Password
[EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN]: {
title: `Invalid password token`,
message: () => `Invalid password token. Please try again.`,
},
[EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN]: {
title: `Expired password token`,
message: () => `Expired password token. Please try again.`,
},
// Change password
[EAuthenticationErrorCodes.MISSING_PASSWORD]: {
title: `Password required`,
message: () => `Password required. Please try again.`,
},
[EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: {
title: `Incorrect old password`,
message: () => `Incorrect old password. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_NEW_PASSWORD]: {
title: `Invalid new password`,
message: () => `Invalid new password. Please try again.`,
},
// set password
[EAuthenticationErrorCodes.PASSWORD_ALREADY_SET]: {
title: `Password already set`,
message: () => `Password already set. Please try again.`,
},
// admin
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: `Admin user already exists`,
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: `Admin user does not exist`,
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
};
export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.INVALID_EMAIL,
EAuthenticationErrorCodes.EMAIL_REQUIRED,
EAuthenticationErrorCodes.SIGNUP_DISABLED,
EAuthenticationErrorCodes.INVALID_PASSWORD,
EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED,
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.INVALID_MAGIC_CODE,
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE,
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED,
EAuthenticationErrorCodes.GOOGLE_NOT_CONFIGURED,
EAuthenticationErrorCodes.GITHUB_NOT_CONFIGURED,
EAuthenticationErrorCodes.GOOGLE_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.GITHUB_OAUTH_PROVIDER_ERROR,
EAuthenticationErrorCodes.INVALID_PASSWORD_TOKEN,
EAuthenticationErrorCodes.EXPIRED_PASSWORD_TOKEN,
EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD,
EAuthenticationErrorCodes.INVALID_NEW_PASSWORD,
EAuthenticationErrorCodes.PASSWORD_ALREADY_SET,
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
];
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};

View File

@ -0,0 +1 @@
export * from "./auth";

View File

@ -65,6 +65,8 @@ export const AuthRoot = observer(() => {
const { config: instanceConfig } = useInstance(); const { config: instanceConfig } = useInstance();
// derived values // derived values
const isSmtpConfigured = instanceConfig?.is_smtp_configured; const isSmtpConfigured = instanceConfig?.is_smtp_configured;
const isMagicLoginEnabled = instanceConfig?.is_magic_login_enabled;
const isEmailPasswordEnabled = instanceConfig?.is_email_password_enabled;
const { header, subHeader } = getHeaderSubHeader(authMode); const { header, subHeader } = getHeaderSubHeader(authMode);
@ -87,9 +89,9 @@ export const AuthRoot = observer(() => {
setAuthStep(EAuthSteps.PASSWORD); setAuthStep(EAuthSteps.PASSWORD);
} else { } else {
// Else if SMTP is configured, move to unique code sign-in/ sign-up. // Else if SMTP is configured, move to unique code sign-in/ sign-up.
if (isSmtpConfigured) { if (isSmtpConfigured && isMagicLoginEnabled) {
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
} else { } else if (isEmailPasswordEnabled) {
// Else show error message if SMTP is not configured and password is not set. // Else show error message if SMTP is not configured and password is not set.
if (res.existing) { if (res.existing) {
setAuthMode(null); setAuthMode(null);

View File

@ -151,6 +151,7 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
placeholder="gets-sets-flys" placeholder="gets-sets-flys"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoFocus autoFocus
autoComplete="off"
/> />
<div className="flex w-full items-center justify-between px-1 text-xs"> <div className="flex w-full items-center justify-between px-1 text-xs">
<p className="flex items-center gap-1 font-medium text-green-700"> <p className="flex items-center gap-1 font-medium text-green-700">

View File

@ -40,7 +40,7 @@ const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
useEffect(() => { useEffect(() => {
if (workspaceSlug && projectId && settings) { if (workspaceSlug && projectId && settings) {
const viewsAcceptable: string[] = []; const viewsAcceptable: string[] = [];
let currentBoard: TIssueBoardKeys | null = null; const currentBoard: TIssueBoardKeys | null = null;
if (settings?.views?.list) viewsAcceptable.push("list"); if (settings?.views?.list) viewsAcceptable.push("list");
if (settings?.views?.kanban) viewsAcceptable.push("kanban"); if (settings?.views?.kanban) viewsAcceptable.push("kanban");
@ -48,19 +48,19 @@ const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
if (settings?.views?.gantt) viewsAcceptable.push("gantt"); if (settings?.views?.gantt) viewsAcceptable.push("gantt");
if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
if (board) { // if (board) {
if (viewsAcceptable.includes(board.toString())) { // if (viewsAcceptable.includes(board.toString())) {
currentBoard = board.toString() as TIssueBoardKeys; // currentBoard = board.toString() as TIssueBoardKeys;
} else { // } else {
if (viewsAcceptable && viewsAcceptable.length > 0) { // if (viewsAcceptable && viewsAcceptable.length > 0) {
currentBoard = viewsAcceptable[0] as TIssueBoardKeys; // currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
} // }
} // }
} else { // } else {
if (viewsAcceptable && viewsAcceptable.length > 0) { // if (viewsAcceptable && viewsAcceptable.length > 0) {
currentBoard = viewsAcceptable[0] as TIssueBoardKeys; // currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
} // }
} // }
if (currentBoard) { if (currentBoard) {
if (activeLayout === null || activeLayout !== currentBoard) { if (activeLayout === null || activeLayout !== currentBoard) {

View File

@ -39,12 +39,12 @@ export enum EAuthenticationErrorCodes {
INVALID_EMAIL = "5012", INVALID_EMAIL = "5012",
EMAIL_REQUIRED = "5013", EMAIL_REQUIRED = "5013",
// Sign Up // Sign Up
USER_ACCOUNT_DEACTIVATED = "5019",
USER_ALREADY_EXIST = "5003", USER_ALREADY_EXIST = "5003",
REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5015", REQUIRED_EMAIL_PASSWORD_SIGN_UP = "5015",
AUTHENTICATION_FAILED_SIGN_UP = "5006", AUTHENTICATION_FAILED_SIGN_UP = "5006",
INVALID_EMAIL_SIGN_UP = "5017", INVALID_EMAIL_SIGN_UP = "5017",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5023", MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5023",
INVALID_EMAIL_MAGIC_SIGN_UP = "5019",
// Sign In // Sign In
USER_DOES_NOT_EXIST = "5004", USER_DOES_NOT_EXIST = "5004",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5014", REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5014",
@ -140,12 +140,14 @@ const errorCodeMessages: {
title: `Email and code required`, title: `Email and code required`,
message: () => `Email and code required. Please try again.`, message: () => `Email and code required. Please try again.`,
}, },
[EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP]: {
title: `Invalid email`,
message: () => `Invalid email. Please try again.`,
},
// sign in // sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Please reach out to support@plane.so</div>,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
title: `User does not exist`, title: `User does not exist`,
message: (email = undefined) => ( message: (email = undefined) => (
@ -250,7 +252,6 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP, EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP, EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_UP,
EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED, EAuthenticationErrorCodes.MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_UP,
EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN, EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN, EAuthenticationErrorCodes.INVALID_EMAIL_SIGN_IN,
EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN, EAuthenticationErrorCodes.INVALID_EMAIL_MAGIC_SIGN_IN,
@ -273,6 +274,7 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP, EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_UP,
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN, EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_SIGN_IN,
EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED, EAuthenticationErrorCodes.MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
]; ];
if (toastAlertErrorCodes.includes(errorCode)) if (toastAlertErrorCodes.includes(errorCode))

View File

@ -22,6 +22,7 @@
"@plane/rich-text-editor": "*", "@plane/rich-text-editor": "*",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/constants": "*",
"@sentry/nextjs": "^7.108.0", "@sentry/nextjs": "^7.108.0",
"axios": "^1.3.4", "axios": "^1.3.4",
"clsx": "^2.0.0", "clsx": "^2.0.0",

View File

@ -68,6 +68,10 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
} }
}, [error_code, authMode]); }, [error_code, authMode]);
const isSMTPConfigured = instance?.config?.is_smtp_configured || false;
const isMagicLoginEnabled = instance?.config?.is_magic_login_enabled || false;
const isEmailPasswordEnabled = instance?.config?.is_email_password_enabled || false;
// submit handler- email verification // submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => { const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email); setEmail(data.email);
@ -80,19 +84,21 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
if (response.is_password_autoset) { if (response.is_password_autoset) {
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email); generateEmailUniqueCode(data.email);
} else { } else if (isEmailPasswordEnabled) {
setIsPasswordAutoset(false); setIsPasswordAutoset(false);
setAuthStep(EAuthSteps.PASSWORD); setAuthStep(EAuthSteps.PASSWORD);
} }
} else { } else {
if (instance && instance?.config?.is_smtp_configured) { if (isSMTPConfigured && isMagicLoginEnabled) {
setAuthStep(EAuthSteps.UNIQUE_CODE); setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email); generateEmailUniqueCode(data.email);
} else setAuthStep(EAuthSteps.PASSWORD); } else if (isEmailPasswordEnabled) {
setAuthStep(EAuthSteps.PASSWORD);
}
} }
}) })
.catch((error) => { .catch((error) => {
const errorhandler = authErrorHandler(error?.error_code.toString(), data?.email || undefined); const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
if (errorhandler?.type) setErrorInfo(errorhandler); if (errorhandler?.type) setErrorInfo(errorhandler);
}); });
}; };
@ -113,8 +119,6 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
const isOAuthEnabled = const isOAuthEnabled =
(instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled)) || false; (instance?.config && (instance?.config?.is_google_enabled || instance?.config?.is_github_enabled)) || false;
const isSMTPConfigured = (instance?.config && instance?.config?.is_smtp_configured) || false;
return ( return (
<div className="relative flex flex-col space-y-6"> <div className="relative flex flex-col space-y-6">
<AuthHeader <AuthHeader

View File

@ -8,7 +8,7 @@ import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@pla
import { WORKSPACE_CREATED } from "@/constants/event-tracker"; import { WORKSPACE_CREATED } from "@/constants/event-tracker";
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace"; import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
// hooks // hooks
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store"; import { useEventTracker, useUserProfile, useWorkspace } from "@/hooks/store";
// services // services
import { WorkspaceService } from "@/services/workspace.service"; import { WorkspaceService } from "@/services/workspace.service";
@ -28,7 +28,8 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
const [slugError, setSlugError] = useState(false); const [slugError, setSlugError] = useState(false);
const [invalidSlug, setInvalidSlug] = useState(false); const [invalidSlug, setInvalidSlug] = useState(false);
// store hooks // store hooks
const { updateCurrentUser } = useUser(); const { updateUserProfile } = useUserProfile();
const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace(); const { createWorkspace, fetchWorkspaces, workspaces } = useWorkspace();
const { captureWorkspaceEvent } = useEventTracker(); const { captureWorkspaceEvent } = useEventTracker();
// form info // form info
@ -111,7 +112,7 @@ export const CreateWorkspace: React.FC<Props> = (props) => {
}; };
await stepChange(payload); await stepChange(payload);
await updateCurrentUser({ await updateUserProfile({
last_workspace_id: firstWorkspace?.id, last_workspace_id: firstWorkspace?.id,
}); });
}; };

View File

@ -62,7 +62,7 @@ export const PublishProjectModal: React.FC<Props> = observer((props) => {
const [isUpdateRequired, setIsUpdateRequired] = useState(false); const [isUpdateRequired, setIsUpdateRequired] = useState(false);
// const plane_deploy_url = instance?.config?.space_base_url || ""; // const plane_deploy_url = instance?.config?.space_base_url || "";
const SPACE_URL = SPACE_BASE_URL + SPACE_BASE_PATH; const SPACE_URL = (SPACE_BASE_URL === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
// router // router
const router = useRouter(); const router = useRouter();

View File

@ -12,7 +12,7 @@ import { IWorkspace } from "@plane/types";
// plane ui // plane ui
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
// hooks // hooks
import { useAppTheme, useUser, useWorkspace } from "@/hooks/store"; import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import { WorkspaceLogo } from "./logo"; import { WorkspaceLogo } from "./logo";
// types // types
// Static Data // Static Data
@ -56,10 +56,12 @@ export const WorkspaceSidebarDropdown = observer(() => {
const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { sidebarCollapsed, toggleSidebar } = useAppTheme();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { const {
updateCurrentUser, // updateCurrentUser,
// isUserInstanceAdmin, // isUserInstanceAdmin,
signOut, signOut,
} = useUser(); } = useUser();
const { updateUserProfile } = useUserProfile();
const isUserInstanceAdmin = false; const isUserInstanceAdmin = false;
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
// popper-js refs // popper-js refs
@ -78,7 +80,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
], ],
}); });
const handleWorkspaceNavigation = (workspace: IWorkspace) => const handleWorkspaceNavigation = (workspace: IWorkspace) =>
updateCurrentUser({ updateUserProfile({
last_workspace_id: workspace?.id, last_workspace_id: workspace?.id,
}); });
const handleSignOut = async () => { const handleSignOut = async () => {

View File

@ -45,6 +45,7 @@ export enum EAuthenticationErrorCodes {
INVALID_EMAIL_MAGIC_SIGN_UP = "5050", INVALID_EMAIL_MAGIC_SIGN_UP = "5050",
MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055", MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED = "5055",
// Sign In // Sign In
USER_ACCOUNT_DEACTIVATED = "5019",
USER_DOES_NOT_EXIST = "5060", USER_DOES_NOT_EXIST = "5060",
AUTHENTICATION_FAILED_SIGN_IN = "5065", AUTHENTICATION_FAILED_SIGN_IN = "5065",
REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070", REQUIRED_EMAIL_PASSWORD_SIGN_IN = "5070",
@ -65,6 +66,7 @@ export enum EAuthenticationErrorCodes {
EXPIRED_PASSWORD_TOKEN = "5130", EXPIRED_PASSWORD_TOKEN = "5130",
// Change password // Change password
INCORRECT_OLD_PASSWORD = "5135", INCORRECT_OLD_PASSWORD = "5135",
MISSING_PASSWORD = "5138",
INVALID_NEW_PASSWORD = "5140", INVALID_NEW_PASSWORD = "5140",
// set passowrd // set passowrd
PASSWORD_ALREADY_SET = "5145", PASSWORD_ALREADY_SET = "5145",
@ -155,6 +157,11 @@ const errorCodeMessages: {
}, },
// sign in // sign in
[EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED]: {
title: `User account deactivated`,
message: () => <div>Your account is deactivated. Contact support@plane.so.</div>,
},
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: { [EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
title: `User does not exist`, title: `User does not exist`,
message: (email = undefined) => ( message: (email = undefined) => (
@ -234,6 +241,10 @@ const errorCodeMessages: {
}, },
// Change password // Change password
[EAuthenticationErrorCodes.MISSING_PASSWORD]: {
title: `Password required`,
message: () => `Password required. Please try again.`,
},
[EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: { [EAuthenticationErrorCodes.INCORRECT_OLD_PASSWORD]: {
title: `Incorrect old password`, title: `Incorrect old password`,
message: () => `Incorrect old password. Please try again.`, message: () => `Incorrect old password. Please try again.`,
@ -343,6 +354,7 @@ export const authErrorHandler = (
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED, EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.USER_ACCOUNT_DEACTIVATED,
]; ];
if (bannerAlertErrorCodes.includes(errorCode)) if (bannerAlertErrorCodes.includes(errorCode))

View File

@ -30,6 +30,7 @@
"@plane/rich-text-editor": "*", "@plane/rich-text-editor": "*",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/constants": "*",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@sentry/nextjs": "^7.108.0", "@sentry/nextjs": "^7.108.0",
"axios": "^1.1.3", "axios": "^1.1.3",

View File

@ -8,7 +8,7 @@ import { IWorkspace } from "@plane/types";
// hooks // hooks
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { CreateWorkspaceForm } from "@/components/workspace"; import { CreateWorkspaceForm } from "@/components/workspace";
import { useUser } from "@/hooks/store"; import { useUser, useUserProfile } from "@/hooks/store";
// layouts // layouts
import DefaultLayout from "@/layouts/default-layout"; import DefaultLayout from "@/layouts/default-layout";
// components // components
@ -25,7 +25,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { updateCurrentUser } = useUser(); const { updateUserProfile } = useUserProfile();
// states // states
const [defaultValues, setDefaultValues] = useState({ const [defaultValues, setDefaultValues] = useState({
name: "", name: "",
@ -36,7 +36,7 @@ const CreateWorkspacePage: NextPageWithLayout = observer(() => {
const { theme } = useTheme(); const { theme } = useTheme();
const onSubmit = async (workspace: IWorkspace) => { const onSubmit = async (workspace: IWorkspace) => {
await updateCurrentUser({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); await updateUserProfile({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`));
}; };
return ( return (

View File

@ -21,14 +21,13 @@ import { ROLE } from "@/constants/workspace";
// helpers // helpers
import { truncateText } from "@/helpers/string.helper"; import { truncateText } from "@/helpers/string.helper";
import { getUserRole } from "@/helpers/user.helper"; import { getUserRole } from "@/helpers/user.helper";
import { useEventTracker, useUser, useWorkspace } from "@/hooks/store"; import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/store";
import DefaultLayout from "@/layouts/default-layout"; import DefaultLayout from "@/layouts/default-layout";
// types // types
import { NextPageWithLayout } from "@/lib/types"; import { NextPageWithLayout } from "@/lib/types";
// wrappers // wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// services // services
import { UserService } from "@/services/user.service";
import { WorkspaceService } from "@/services/workspace.service"; import { WorkspaceService } from "@/services/workspace.service";
// images // images
import emptyInvitation from "public/empty-state/invitation.svg"; import emptyInvitation from "public/empty-state/invitation.svg";
@ -36,7 +35,6 @@ import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-l
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const userService = new UserService();
const UserInvitationsPage: NextPageWithLayout = observer(() => { const UserInvitationsPage: NextPageWithLayout = observer(() => {
// states // states
@ -47,6 +45,8 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
// store hooks // store hooks
const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker(); const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { fetchWorkspaces } = useWorkspace(); const { fetchWorkspaces } = useWorkspace();
// next-themes // next-themes
const { theme } = useTheme(); const { theme } = useTheme();
@ -95,8 +95,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
state: "SUCCESS", state: "SUCCESS",
element: "Workspace invitations page", element: "Workspace invitations page",
}); });
userService updateUserProfile({ last_workspace_id: redirectWorkspace?.id })
.updateUser({ last_workspace_id: redirectWorkspace?.id })
.then(() => { .then(() => {
setIsJoiningWorkspaces(false); setIsJoiningWorkspaces(false);
fetchWorkspaces().then(() => { fetchWorkspaces().then(() => {

View File

@ -1,12 +1,15 @@
import { ReactElement, useEffect, useState } from "react"; import { ReactElement, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
// ui // ui
import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; import { Button, Input, Spinner, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// components // components
import { PasswordStrengthMeter } from "@/components/account";
import { PageHead } from "@/components/core"; import { PageHead } from "@/components/core";
import { SidebarHamburgerToggle } from "@/components/core/sidebar"; import { SidebarHamburgerToggle } from "@/components/core/sidebar";
import { getPasswordStrength } from "@/helpers/password.helper";
// hooks // hooks
import { useAppTheme, useUser } from "@/hooks/store"; import { useAppTheme, useUser } from "@/hooks/store";
// layout // layout
@ -14,6 +17,7 @@ import { ProfileSettingsLayout } from "@/layouts/settings-layout";
// types // types
import { NextPageWithLayout } from "@/lib/types"; import { NextPageWithLayout } from "@/lib/types";
// services // services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
export interface FormValues { export interface FormValues {
@ -29,9 +33,18 @@ const defaultValues: FormValues = {
}; };
export const userService = new UserService(); export const userService = new UserService();
export const authService = new AuthService();
const ChangePasswordPage: NextPageWithLayout = observer(() => { const ChangePasswordPage: NextPageWithLayout = observer(() => {
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPageLoading, setIsPageLoading] = useState(true); const [isPageLoading, setIsPageLoading] = useState(true);
const [showPassword, setShowPassword] = useState({
oldPassword: false,
password: false,
retypePassword: false,
});
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
// router // router
const router = useRouter(); const router = useRouter();
// store hooks // store hooks
@ -42,32 +55,54 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
const { const {
control, control,
handleSubmit, handleSubmit,
watch,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
reset,
} = useForm<FormValues>({ defaultValues }); } = useForm<FormValues>({ defaultValues });
const oldPassword = watch("old_password");
const password = watch("new_password");
const retypePassword = watch("confirm_password");
const handleShowPassword = (key: keyof typeof showPassword) =>
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
const handleChangePassword = async (formData: FormValues) => { const handleChangePassword = async (formData: FormValues) => {
if (formData.new_password !== formData.confirm_password) { try {
if (!csrfToken) throw new Error("csrf token not found");
const changePasswordPromise = userService
.changePassword(csrfToken, {
old_password: formData.old_password,
new_password: formData.new_password,
})
.then(() => {
reset(defaultValues);
});
setPromiseToast(changePasswordPromise, {
loading: "Changing password...",
success: {
title: "Success!",
message: () => "Password changed successfully.",
},
error: {
title: "Error!",
message: () => "Something went wrong. Please try again 1.",
},
});
} catch (err: any) {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
message: "The new password and the confirm password don't match.", message: err?.error ?? "Something went wrong. Please try again 2.",
}); });
return;
} }
const changePasswordPromise = userService.changePassword(formData);
setPromiseToast(changePasswordPromise, {
loading: "Changing password...",
success: {
title: "Success!",
message: () => "Password changed successfully.",
},
error: {
title: "Error!",
message: () => "Something went wrong. Please try again.",
},
});
}; };
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
useEffect(() => { useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
@ -75,6 +110,25 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
else setIsPageLoading(false); else setIsPageLoading(false);
}, [currentUser, router]); }, [currentUser, router]);
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
!!oldPassword &&
!!password &&
!!retypePassword &&
getPasswordStrength(password) >= 3 &&
password === retypePassword &&
password !== oldPassword
? false
: true,
[isSubmitting, oldPassword, password, retypePassword]
);
const passwordSupport = password.length > 0 && (getPasswordStrength(password) < 3 || isPasswordInputFocused) && (
<PasswordStrengthMeter password={password} />
);
if (isPageLoading) if (isPageLoading)
return ( return (
<div className="grid h-screen w-full place-items-center"> <div className="grid h-screen w-full place-items-center">
@ -93,82 +147,126 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
onSubmit={handleSubmit(handleChangePassword)} onSubmit={handleSubmit(handleChangePassword)}
className="mx-auto md:mt-16 mt-10 flex h-full w-full flex-col gap-8 px-4 md:px-8 pb-8 lg:w-3/5" className="mx-auto md:mt-16 mt-10 flex h-full w-full flex-col gap-8 px-4 md:px-8 pb-8 lg:w-3/5"
> >
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<h3 className="text-xl font-medium">Change password</h3> <h3 className="text-xl font-medium">Change password</h3>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3"> <div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-1 "> <div className="flex flex-col gap-1 ">
<h4 className="text-sm">Current password</h4> <h4 className="text-sm">Current password</h4>
<Controller <div className="relative flex items-center rounded-md">
control={control} <Controller
name="old_password" control={control}
rules={{ name="old_password"
required: "This field is required", rules={{
}} required: "This field is required",
render={({ field: { value, onChange } }) => ( }}
<Input render={({ field: { value, onChange } }) => (
id="old_password" <Input
type="password" id="old_password"
value={value} type={showPassword?.oldPassword ? "text" : "password"}
onChange={onChange} value={value}
placeholder="Old password" onChange={onChange}
className="w-full rounded-md font-medium" placeholder="Old password"
hasError={Boolean(errors.old_password)} className="w-full rounded-md font-medium"
hasError={Boolean(errors.old_password)}
/>
)}
/>
{showPassword?.oldPassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("oldPassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("oldPassword")}
/> />
)} )}
/> </div>
{errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>} {errors.old_password && <span className="text-xs text-red-500">{errors.old_password.message}</span>}
</div> </div>
<div className="flex flex-col gap-1 "> <div className="flex flex-col gap-1 ">
<h4 className="text-sm">New password</h4> <h4 className="text-sm">New password</h4>
<Controller <div className="relative flex items-center rounded-md">
control={control} <Controller
name="new_password" control={control}
rules={{ name="new_password"
required: "This field is required", rules={{
}} required: "This field is required",
render={({ field: { value, onChange } }) => ( }}
<Input render={({ field: { value, onChange } }) => (
id="new_password" <Input
type="password" id="new_password"
value={value} type={showPassword?.password ? "text" : "password"}
placeholder="New password" value={value}
onChange={onChange} placeholder="New password"
className="w-full" onChange={onChange}
hasError={Boolean(errors.new_password)} className="w-full"
hasError={Boolean(errors.new_password)}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
/>
)}
/>
{showPassword?.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/> />
)} )}
/> </div>
{errors.new_password && <span className="text-xs text-red-500">{errors.new_password.message}</span>} {passwordSupport}
</div> </div>
<div className="flex flex-col gap-1 "> <div className="flex flex-col gap-1 ">
<h4 className="text-sm">Confirm password</h4> <h4 className="text-sm">Confirm password</h4>
<Controller <div className="relative flex items-center rounded-md">
control={control} <Controller
name="confirm_password" control={control}
rules={{ name="confirm_password"
required: "This field is required", rules={{
}} required: "This field is required",
render={({ field: { value, onChange } }) => ( }}
<Input render={({ field: { value, onChange } }) => (
id="confirm_password" <Input
type="password" id="confirm_password"
placeholder="Confirm password" type={showPassword?.retypePassword ? "text" : "password"}
value={value} placeholder="Confirm password"
onChange={onChange} value={value}
className="w-full" onChange={onChange}
hasError={Boolean(errors.confirm_password)} className="w-full"
hasError={Boolean(errors.confirm_password)}
/>
)}
/>
{showPassword?.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/> />
)} )}
/> </div>
{errors.confirm_password && ( {!!retypePassword && password !== retypePassword && (
<span className="text-xs text-red-500">{errors.confirm_password.message}</span> <span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isSubmitting}> <Button variant="primary" type="submit" loading={isSubmitting} disabled={isButtonDisabled}>
{isSubmitting ? "Changing password..." : "Change password"} {isSubmitting ? "Changing password..." : "Change password"}
</Button> </Button>
</div> </div>

View File

@ -143,8 +143,12 @@ export class UserService extends APIService {
}); });
} }
async changePassword(data: { old_password: string; new_password: string; confirm_password: string }): Promise<any> { async changePassword(token: string, data: { old_password: string; new_password: string }): Promise<any> {
return this.post(`/api/users/me/change-password/`, data) return this.post(`/auth/change-password/`, data, {
headers: {
"X-CSRFTOKEN": token,
},
})
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;

View File

@ -6917,7 +6917,7 @@ postcss@8.4.31:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
postcss@^8.4.23, postcss@^8.4.38: postcss@^8.4.23, postcss@^8.4.29, postcss@^8.4.38:
version "8.4.38" version "8.4.38"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@ -7945,8 +7945,16 @@ streamx@^2.15.0, streamx@^2.16.1:
optionalDependencies: optionalDependencies:
bare-events "^2.2.0" bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: "string-width-cjs@npm:string-width@^4.2.0":
name string-width-cjs version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8026,7 +8034,14 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==