fix: admin and space redirections (#4419)

* dev: add admin and space base url

* fix: formatting

* dev: add app,space and admin base url to the api env

* fix: updated app base urls redirection

* dev: add change password endpoint

* dev: add none as default for base url

* dev: space password management endpoints

* fix: docker env update

* fix: docker and env settings

* fix: docker changes

* fix: next config update

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: guru_sainath <gurusainath007@gmail.com>
This commit is contained in:
sriram veeraghanta 2024-05-10 02:32:42 +05:30 committed by GitHub
parent 2320b33189
commit 547a76ae55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 746 additions and 367 deletions

View file

@ -44,3 +44,8 @@ WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=

View file

@ -1,6 +1,4 @@
# Python imports
from urllib.parse import urlparse
import zoneinfo
# Django imports

View file

@ -7,6 +7,7 @@ from .views import (
ForgotPasswordEndpoint,
SetUserPasswordEndpoint,
ResetPasswordEndpoint,
ChangePasswordEndpoint,
# App
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
@ -18,6 +19,8 @@ from .views import (
SignInAuthEndpoint,
SignOutAuthEndpoint,
SignUpAuthEndpoint,
ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint,
# Space
EmailCheckEndpoint,
GitHubCallbackSpaceEndpoint,
@ -176,6 +179,21 @@ urlpatterns = [
ResetPasswordEndpoint.as_view(),
name="forgot-password",
),
path(
"spaces/forgot-password/",
ForgotPasswordSpaceEndpoint.as_view(),
name="forgot-password",
),
path(
"spaces/reset-password/<uidb64>/<token>/",
ResetPasswordSpaceEndpoint.as_view(),
name="forgot-password",
),
path(
"change-password/",
ChangePasswordEndpoint.as_view(),
name="forgot-password",
),
path(
"set-password/",
SetUserPasswordEndpoint.as_view(),

View file

@ -1,8 +1,19 @@
# Python imports
from urllib.parse import urlsplit
# Django imports
from django.conf import settings
def base_host(request):
def base_host(request, is_admin=False, is_space=False):
"""Utility function to return host / origin from the request"""
if is_admin and settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL
if is_space and settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL
return (
request.META.get("HTTP_ORIGIN")
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"

View file

@ -1,8 +1,6 @@
from .common import (
ChangePasswordEndpoint,
CSRFTokenEndpoint,
ForgotPasswordEndpoint,
ResetPasswordEndpoint,
SetUserPasswordEndpoint,
)
@ -50,3 +48,12 @@ from .space.magic import (
from .space.signout import SignOutAuthSpaceEndpoint
from .space.check import EmailCheckEndpoint
from .space.password_management import (
ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint,
)
from .app.password_management import (
ForgotPasswordEndpoint,
ResetPasswordEndpoint,
)

View file

@ -2,7 +2,6 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View

View file

@ -3,18 +3,17 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
# Module imports
from plane.authentication.provider.oauth.google import GoogleOAuthProvider
from plane.authentication.utils.login import user_login
from plane.authentication.utils.redirection_path import get_redirection_path
from plane.authentication.utils.workspace_project_join import (
process_workspace_project_invitations,
)
# Module imports
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (

View file

@ -0,0 +1,202 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from zxcvbn import zxcvbn
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Module imports
from plane.bgtasks.forgot_password_task import forgot_password
from plane.license.models import Instance
from plane.db.models import User
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# 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_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"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
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,
)
class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

View file

@ -1,21 +1,3 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.middleware.csrf import get_token
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
@ -29,15 +11,12 @@ from plane.app.serializers import (
UserSerializer,
)
from plane.authentication.utils.login import user_login
from plane.bgtasks.forgot_password_task import forgot_password
from plane.db.models import User
from plane.license.models import Instance
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
from django.middleware.csrf import get_token
class CSRFTokenEndpoint(APIView):
@ -55,174 +34,6 @@ class CSRFTokenEndpoint(APIView):
)
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# 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_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"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
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,
)
class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
class ChangePasswordEndpoint(APIView):
def post(self, request):
serializer = ChangePasswordSerializer(data=request.data)

View file

@ -37,7 +37,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -59,7 +59,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -78,7 +78,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -93,7 +93,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -107,7 +107,7 @@ class SignInAuthSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to next path
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
str(next_path) if next_path else "/",
)
return HttpResponseRedirect(url)
@ -116,7 +116,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -140,7 +140,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -161,7 +161,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -180,7 +180,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -195,7 +195,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -209,7 +209,7 @@ class SignUpAuthSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to referer path
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces",
)
return HttpResponseRedirect(url)
@ -218,7 +218,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)

View file

@ -3,7 +3,6 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
@ -22,7 +21,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
def get(self, request):
# Get host and next path
request.session["host"] = base_host(request=request)
request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path")
if next_path:
request.session["next_path"] = str(next_path)
@ -40,7 +39,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)

View file

@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
class GoogleOauthInitiateSpaceEndpoint(View):
def get(self, request):
request.session["host"] = base_host(request=request)
request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path")
if next_path:
request.session["next_path"] = str(next_path)
@ -37,7 +37,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -53,7 +53,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)

View file

@ -2,7 +2,6 @@
from urllib.parse import urlencode, urljoin
# Django imports
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.views import View
@ -48,7 +47,7 @@ class MagicGenerateSpaceEndpoint(APIView):
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
)
origin = base_host(request=request)
origin = base_host(request=request, is_space=True)
email = request.data.get("email", False)
try:
# Clean up the email
@ -86,7 +85,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -99,7 +98,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -118,7 +117,7 @@ class MagicSignInSpaceEndpoint(View):
else:
# Get the redirection path
path = str(next_path) if next_path else "spaces"
url = urljoin(base_host(request=request), path)
url = urljoin(base_host(request=request, is_space=True), path)
return HttpResponseRedirect(url)
except AuthenticationException as e:
@ -126,7 +125,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -152,7 +151,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -166,7 +165,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
@ -180,7 +179,7 @@ class MagicSignUpSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to referer path
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces",
)
return HttpResponseRedirect(url)
@ -190,7 +189,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)

View file

@ -0,0 +1,202 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from zxcvbn import zxcvbn
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Module imports
from plane.bgtasks.forgot_password_task import forgot_password
from plane.license.models import Instance
from plane.db.models import User
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordSpaceEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# 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_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"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
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,
)
class ResetPasswordSpaceEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request, is_space=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

View file

@ -24,11 +24,11 @@ class SignOutAuthSpaceEndpoint(View):
# Log the user out
logout(request)
url = urljoin(
base_host(request=request),
base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode({"success": "true"}),
)
return HttpResponseRedirect(url)
except Exception:
return HttpResponseRedirect(
base_host(request=request), "accounts/sign-in"
base_host(request=request, is_space=True), "accounts/sign-in"
)

View file

@ -106,7 +106,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -118,7 +118,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="ADMIN_ALREADY_EXIST",
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -147,7 +147,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -169,7 +169,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -191,7 +191,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -213,7 +213,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -248,7 +248,9 @@ class InstanceAdminSignUpEndpoint(View):
# get tokens for user
user_login(request=request, user=user)
url = urljoin(base_host(request=request), "god-mode/general")
url = urljoin(
base_host(request=request, is_admin=True), "god-mode/general"
)
return HttpResponseRedirect(url)
@ -269,7 +271,7 @@ class InstanceAdminSignInEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -290,7 +292,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -308,7 +310,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -328,7 +330,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -345,7 +347,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -362,7 +364,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@ -377,7 +379,9 @@ class InstanceAdminSignInEndpoint(View):
# get tokens for user
user_login(request=request, user=user)
url = urljoin(base_host(request=request), "god-mode/general")
url = urljoin(
base_host(request=request, is_admin=True), "god-mode/general"
)
return HttpResponseRedirect(url)
@ -411,11 +415,11 @@ class InstanceAdminSignOutEndpoint(View):
# Log the user out
logout(request)
url = urljoin(
base_host(request=request),
base_host(request=request, is_admin=True),
"accounts/sign-in?" + urlencode({"success": "true"}),
)
return HttpResponseRedirect(url)
except Exception:
return HttpResponseRedirect(
base_host(request=request), "accounts/sign-in"
base_host(request=request, is_admin=True), "accounts/sign-in"
)

View file

@ -2,6 +2,7 @@
import os
# Django imports
from django.conf import settings
# Third party imports
from rest_framework import status
@ -148,9 +149,13 @@ class InstanceEndpoint(BaseAPIView):
)
# is smtp configured
data["is_smtp_configured"] = (
bool(EMAIL_HOST)
)
data["is_smtp_configured"] = bool(EMAIL_HOST)
# Base URL
data["admin_base_url"] = settings.ADMIN_BASE_URL
data["space_base_url"] = settings.SPACE_BASE_URL
data["app_base_url"] = settings.APP_BASE_URL
instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() > 1

View file

@ -342,3 +342,8 @@ CSRF_COOKIE_SECURE = secure_origins
CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = cors_allowed_origins
CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
# Base URLs
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
APP_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)